kpath.go 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. // Copyright 2023 The Knuth Authors. All rights reserved.
  2. // Use of this source code is governed by a BSD-style
  3. // license that can be found in the LICENSE file.
  4. // Package kpath provides tools to locate TeX related files.
  5. //
  6. // It loosely mimicks Kpathsea, as described in:
  7. // - https://texdoc.org/serve/kpathsea/0
  8. package kpath // import "modernc.org/knuth/kpath"
  9. import (
  10. "fmt"
  11. "io"
  12. "io/fs"
  13. "os"
  14. stdpath "path"
  15. "path/filepath"
  16. "strings"
  17. "sync"
  18. "modernc.org/knuth/internal/tds"
  19. )
  20. var (
  21. once sync.Once
  22. tdsCtx Context
  23. )
  24. // New returns a minimal kpath context initialized with the content of
  25. // a minimal TeX Directory Structure.
  26. func New() Context {
  27. once.Do(func() {
  28. tdsCtx, _ = NewFromFS(tds.FS)
  29. })
  30. return tdsCtx
  31. }
  32. // Context holds state to efficiently search for files in a TDS
  33. // (TeX Directory Structure), as described in:
  34. // - http://tug.org/tds/tds.pdf
  35. type Context struct {
  36. exts strset // known common suffices
  37. db map[string][]string // db of filename->dirs
  38. fs fs.FS
  39. }
  40. func (ctx *Context) init(root fs.FS) {
  41. if ctx.exts.db == nil {
  42. ctx.exts = strsets["tex"]
  43. }
  44. if ctx.db == nil {
  45. ctx.db = make(map[string][]string)
  46. }
  47. ctx.fs = root
  48. }
  49. // // NewFromDB creates a kpath search from a TeX .cnf configuration file.
  50. // func NewFromConfig(cfg io.Reader) (Context, error) {
  51. // ctx, err := parseConfig(cfg)
  52. // if err != nil {
  53. // return Context{}, fmt.Errorf("kpath: could not parse config: %w", err)
  54. // }
  55. //
  56. // ctx.init()
  57. // return ctx, nil
  58. // }
  59. // NewFromDB creates a kpath search from a TeX ls-R db file.
  60. func NewFromDB(r io.Reader) (Context, error) {
  61. return newFromDB(os.DirFS("/"), r)
  62. }
  63. func newFromDB(root fs.FS, r io.Reader) (Context, error) {
  64. dir := "/"
  65. if f, ok := r.(interface{ Name() string }); ok {
  66. dir = stdpath.Dir(filepath.ToSlash(f.Name()))
  67. }
  68. ctx, err := parseDB(dir, r)
  69. if err != nil {
  70. return Context{}, fmt.Errorf("kpath: could not parse db file: %w", err)
  71. }
  72. ctx.init(root)
  73. return ctx, nil
  74. }
  75. // NewFromFS creates a kpath search context from the provided filesystem.
  76. //
  77. // NewFromFS checks first whether an ls-R database exists at the root of the
  78. // provided filesystem, and otherwise walks the whole fs.
  79. func NewFromFS(fsys fs.FS) (Context, error) {
  80. var ctx Context
  81. ctx.init(fsys)
  82. if _, err := fs.Stat(fsys, "ls-R"); err == nil {
  83. db, err := fsys.Open("ls-R")
  84. if err != nil {
  85. return ctx, fmt.Errorf("kpath: could not open db file: %w", err)
  86. }
  87. defer db.Close()
  88. return newFromDB(fsys, db)
  89. }
  90. err := fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
  91. if err != nil {
  92. return err
  93. }
  94. if d.IsDir() {
  95. return nil
  96. }
  97. fname := stdpath.Base(filepath.ToSlash(path))
  98. ctx.db[fname] = append(ctx.db[fname], path)
  99. return nil
  100. })
  101. if err != nil {
  102. return ctx, fmt.Errorf("kpath: could not walk fs: %w", err)
  103. }
  104. return ctx, nil
  105. }
  106. // FS returns the underlying filesystem this context is using.
  107. func (ctx Context) FS() fs.FS {
  108. return ctx.fs
  109. }
  110. // Open opens the named file for reading.
  111. func (ctx Context) Open(name string) (fs.File, error) {
  112. f, err := ctx.fs.Open(name)
  113. if err == nil {
  114. return f, nil
  115. }
  116. // FIXME(sbinet): ctx.fs.Open may fail to open the named file because
  117. // of absolute vs relative path issues.
  118. // e.g.:
  119. // - ctx.fs is rooted at /usr/share/texmf-dist
  120. // - one requests /usr/share/texmf-dist/foo.txt (which exists)
  121. // Name is thus "/usr/share/texmf-dist/foo.txt",
  122. // but from the POV of ctx.fs, only "foo.txt" exists.
  123. // Giving the absolute path from "/" won't work.
  124. //
  125. // In the meantime, resort to just calling to os.Open.
  126. return os.Open(name)
  127. }
  128. // Find returns the full path to the named file if it could be found within the
  129. // TeXMF distribution system.
  130. // Find returns an error if no file or more than one file were found.
  131. func (ctx Context) Find(name string) (string, error) {
  132. names, err := ctx.FindAll(name)
  133. if err != nil {
  134. return "", err
  135. }
  136. switch n := len(names); n {
  137. case 1:
  138. return names[0], nil
  139. case 0:
  140. return "", fmt.Errorf("kpath: could not find a match for %q", name)
  141. default:
  142. return "", fmt.Errorf("kpath: too many hits for file %q (n=%d)", name, n)
  143. }
  144. }
  145. // FindAll returns the full path to all the files matching name that could be
  146. // found within the TeXMF distribution system.
  147. // Find returns an error if no file was found.
  148. func (ctx Context) FindAll(name string) ([]string, error) {
  149. // TODO(sbinet): handle (all) standard exts.
  150. // TODO(sbinet): handle multi-root TEXMFs
  151. orig := name
  152. name = filepath.ToSlash(name)
  153. var (
  154. subdir = strings.Contains(name, "/")
  155. ext = stdpath.Ext(name)
  156. )
  157. switch ext {
  158. case "":
  159. // try some extensions.
  160. for _, ext := range ctx.exts.ks {
  161. names, ok := ctx.lookup(name+ext, subdir)
  162. if ok {
  163. return names, nil
  164. }
  165. }
  166. names, ok := ctx.lookup(name, subdir)
  167. if ok {
  168. return names, nil
  169. }
  170. default:
  171. if !ctx.exts.has(ext) {
  172. for _, ext := range ctx.exts.ks {
  173. names, ok := ctx.lookup(name+ext, subdir)
  174. if ok {
  175. return names, nil
  176. }
  177. }
  178. }
  179. names, ok := ctx.lookup(name, subdir)
  180. if ok {
  181. return names, nil
  182. }
  183. }
  184. return nil, fmt.Errorf("kpath: could not find file %q", orig)
  185. }
  186. func (ctx Context) lookup(name string, subdir bool) ([]string, bool) {
  187. if !subdir {
  188. names, ok := ctx.db[name]
  189. return names, ok
  190. }
  191. var (
  192. ok = false
  193. names = make([]string, 0, 16)
  194. )
  195. for _, vs := range ctx.db {
  196. for _, v := range vs {
  197. if !strings.HasSuffix(v, name) {
  198. continue
  199. }
  200. names = append(names, v)
  201. ok = true
  202. }
  203. }
  204. return names, ok
  205. }