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