~sbinet/star-tex-patches

[PATCH star-tex 1/2] kpath: first import

Details
Message ID
<zzNM5pff0R2QAtEYS1pmHz3E5lf1ZdGsBRZaxbhQ@cp7-web-041.plabs.ch>
DKIM signature
missing
Download raw message
Patch: +521 -0
Signed-off-by: Sebastien Binet <s@sbinet.org>
---
 kpath/config.go     |  32 +++++++
 kpath/db.go         |  48 ++++++++++
 kpath/kpath.go      | 152 +++++++++++++++++++++++++++++
 kpath/kpath_test.go | 229 ++++++++++++++++++++++++++++++++++++++++++++
 kpath/stringset.go  |  60 ++++++++++++
 5 files changed, 521 insertions(+)
 create mode 100644 kpath/config.go
 create mode 100644 kpath/db.go
 create mode 100644 kpath/kpath.go
 create mode 100644 kpath/kpath_test.go
 create mode 100644 kpath/stringset.go

diff --git a/kpath/config.go b/kpath/config.go
new file mode 100644
index 0000000..4eb540a
--- /dev/null
+++ b/kpath/config.go
@@ -0,0 +1,32 @@
// Copyright ©2021 The star-tex Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package kpath

import (
	"bufio"
	"bytes"
	"fmt"
	"io"
)

func parseConfig(r io.Reader) (Context, error) {
	sc := bufio.NewScanner(r)
	for sc.Scan() {
		raw := bytes.TrimSpace(sc.Bytes())
		if len(raw) == 0 {
			continue
		}
		if raw[0] == '%' {
			continue
		}
	}

	err := sc.Err()
	if err != nil && err != io.EOF {
		return Context{}, fmt.Errorf("could not scan config file: %w", err)
	}

	panic("not implemented")
}
diff --git a/kpath/db.go b/kpath/db.go
new file mode 100644
index 0000000..8c91743
--- /dev/null
+++ b/kpath/db.go
@@ -0,0 +1,48 @@
// Copyright ©2021 The star-tex Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package kpath

import (
	"bufio"
	"fmt"
	"io"
	"path/filepath"
	"strings"
)

func parseDB(root string, r io.Reader) (Context, error) {
	db := make(map[string][]string)
	sc := bufio.NewScanner(r)
	dir := root
	for sc.Scan() {
		txt := strings.TrimSpace(sc.Text())
		if txt == "" {
			continue
		}
		if txt[0] == '%' {
			continue
		}

		switch {
		case isDirDB(txt):
			dir = filepath.Join(root, strings.TrimRight(txt, ":"))
		default:
			db[txt] = append(db[txt], filepath.Join(dir, txt))
		}
	}

	err := sc.Err()
	if err != nil && err != io.EOF {
		return Context{}, fmt.Errorf("could not scan db file: %w", err)
	}

	return Context{db: db}, nil
}

func isDirDB(name string) bool {
	return strings.HasSuffix(name, ":") && (strings.HasPrefix(name, "/") ||
		strings.HasPrefix(name, "./") ||
		strings.HasPrefix(name, "../"))
}
diff --git a/kpath/kpath.go b/kpath/kpath.go
new file mode 100644
index 0000000..1684e49
--- /dev/null
+++ b/kpath/kpath.go
@@ -0,0 +1,152 @@
// Copyright ©2021 The star-tex Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package kpath provides tools to locate TeX related files.
//
// It loosely mimicks Kpathsea, as described in:
//  - https://texdoc.org/serve/kpathsea/0
package kpath // import "git.sr.ht/~sbinet/star-tex/kpath"

import (
	"fmt"
	"io"
	"path/filepath"
	"strings"
)

// Context holds state to efficiently search for files in a TDS
// (TeX Directory Structure), as described in:
//  - http://tug.org/tds/tds.pdf
type Context struct {
	exts strset              // known common suffices
	db   map[string][]string // db of filename->dirs
}

func (ctx *Context) init() {
	if ctx.exts.db == nil {
		ctx.exts = strsets["tex"]
	}
	if ctx.db == nil {
		ctx.db = make(map[string][]string)
	}
}

// func New(root string) Context {
// 	ctx := Context{}
// 	ctx.init()
// 	return ctx
// }
//
// // NewFromDB creates a kpath search from a TeX .cnf configuration file.
// func NewFromConfig(cfg io.Reader) (Context, error) {
// 	ctx, err := parseConfig(cfg)
// 	if err != nil {
// 		return Context{}, fmt.Errorf("kpath: could not parse config: %w", err)
// 	}
//
// 	ctx.init()
// 	return ctx, nil
// }

// NewFromDB creates a kpath search from a TeX ls-R db file.
func NewFromDB(r io.Reader) (Context, error) {
	dir := "/"
	if f, ok := r.(interface{ Name() string }); ok {
		dir = filepath.Dir(f.Name())
	}
	ctx, err := parseDB(dir, r)
	if err != nil {
		return Context{}, fmt.Errorf("kpath: could not parse db file: %w", err)
	}

	ctx.init()
	return ctx, nil
}

// Find returns the full path to the named file if it could be found within the
// TeXMF distribution system.
// Find returns an error if no file or more than one file were found.
func (ctx Context) Find(name string) (string, error) {
	names, err := ctx.FindAll(name)
	if err != nil {
		return "", err
	}

	switch n := len(names); n {
	case 1:
		return names[0], nil
	case 0:
		return "", fmt.Errorf("kpath: could not find a match for %q", name)
	default:
		return "", fmt.Errorf("kpath: too many hits for file %q (n=%d)", name, n)
	}
}

// FindAll returns the full path to all the files matching name that could be
// found within the TeXMF distribution system.
// Find returns an error if no file was found.
func (ctx Context) FindAll(name string) ([]string, error) {
	// TODO(sbinet): handle (all) standard exts.
	// TODO(sbinet): handle multi-root TEXMFs
	// FIXME(sbinet): normalize all paths to use UNIX path separator ?

	subdir := strings.Contains(name, string(filepath.Separator))
	ext := filepath.Ext(name)
	switch ext {
	case "":
		// try some extensions.
		for _, ext := range ctx.exts.ks {
			names, ok := ctx.lookup(name+ext, subdir)
			if ok {
				return names, nil
			}
		}

		names, ok := ctx.lookup(name, subdir)
		if ok {
			return names, nil
		}

	default:

		if !ctx.exts.has(ext) {
			for _, ext := range ctx.exts.ks {
				names, ok := ctx.lookup(name+ext, subdir)
				if ok {
					return names, nil
				}
			}
		}

		names, ok := ctx.lookup(name, subdir)
		if ok {
			return names, nil
		}
	}

	return nil, fmt.Errorf("kpath: could not find file %q", name)
}

func (s Context) lookup(name string, subdir bool) ([]string, bool) {
	if !subdir {
		names, ok := s.db[name]
		return names, ok
	}

	var (
		ok    = false
		names = make([]string, 0, 16)
	)
	for _, vs := range s.db {
		for _, v := range vs {
			if !strings.HasSuffix(v, name) {
				continue
			}
			names = append(names, v)
			ok = true
		}
	}

	return names, ok
}
diff --git a/kpath/kpath_test.go b/kpath/kpath_test.go
new file mode 100644
index 0000000..02994d2
--- /dev/null
+++ b/kpath/kpath_test.go
@@ -0,0 +1,229 @@
// Copyright ©2021 The star-tex Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package kpath

import (
	"fmt"
	"io"
	"strings"
	"testing"
)

type namedFile struct {
	io.Reader
	name string
}

func (f namedFile) Name() string { return f.name }

func TestFindFromDB(t *testing.T) {
	const (
		dbname = "/usr/share/texmf-dist/ls-R"
		db     = `%% a fake ls-R db.
./:
.:
../texmf-dist:
hello.tex
world.tex
dup.tex

./dir1/dir11:
file-dir1.tex
dup.tex
base.tex

./dir2:
file-dir2.tex
file-dir2.tfm

./dir2/dir11:
base.tex

./dir3:
dup.tex

./dir4:
f1.bar
f1.bar.tex
f2.bar
f2.bar.sty
f3.bar
f3.bar.styx

./dir5:
some_file
some_file.txt
`
	)

	ctx, err := NewFromDB(namedFile{strings.NewReader(db), dbname})
	if err != nil {
		t.Fatal(err)
	}

	for _, tc := range []struct {
		name string
		want string
		err  error
	}{
		{
			name: "hello.tex",
			want: "/usr/share/texmf-dist/hello.tex",
		},
		{
			name: "hello",
			want: "/usr/share/texmf-dist/hello.tex",
		},
		{
			name: "world.tex",
			want: "/usr/share/texmf-dist/world.tex",
		},
		{
			name: "file-dir1.tex",
			want: "/usr/share/texmf-dist/dir1/dir11/file-dir1.tex",
		},
		{
			name: "dir11/file-dir1.tex",
			want: "/usr/share/texmf-dist/dir1/dir11/file-dir1.tex",
		},
		{
			name: "dir11/file-dir1",
			want: "/usr/share/texmf-dist/dir1/dir11/file-dir1.tex",
		},
		{
			name: "file-dir2.tfm",
			want: "/usr/share/texmf-dist/dir2/file-dir2.tfm",
		},
		{
			name: "file-dir2.tex",
			want: "/usr/share/texmf-dist/dir2/file-dir2.tex",
		},
		{
			name: "file-dir2",
			want: "/usr/share/texmf-dist/dir2/file-dir2.tex",
		},
		{
			name: "dup.tex",
			err:  fmt.Errorf(`kpath: too many hits for file "dup.tex" (n=3)`),
		},
		{
			name: "dir11/base",
			err:  fmt.Errorf(`kpath: too many hits for file "dir11/base" (n=2)`),
		},
		{
			name: "f1.bar.tex",
			want: "/usr/share/texmf-dist/dir4/f1.bar.tex",
		},
		{
			name: "f1.bar",
			want: "/usr/share/texmf-dist/dir4/f1.bar.tex",
		},
		{
			name: "f2.bar",
			want: "/usr/share/texmf-dist/dir4/f2.bar.sty",
		},
		{
			name: "f3.bar",
			want: "/usr/share/texmf-dist/dir4/f3.bar",
		},
		{
			name: "some_file",
			want: "/usr/share/texmf-dist/dir5/some_file",
		},
	} {
		t.Run(tc.name, func(t *testing.T) {
			got, err := ctx.Find(tc.name)
			switch {
			case err == nil && tc.err == nil:
				// ok.
			case err != nil && tc.err != nil:
				if got, want := err.Error(), tc.err.Error(); got != want {
					t.Fatalf("invalid error:\ngot= %s\nwant=%s\n", got, want)
				}
				return
			case err != nil && tc.err == nil:
				t.Fatalf("could not run kpath-find: %+v", err)
			case err == nil && tc.err != nil:
				t.Fatalf("missing error. expected: %+v", tc.err)
			}

			if got != tc.want {
				t.Fatalf("invalid file named:\ngot= %s\nwant=%s", got, tc.want)
			}
		})
	}
}

func TestFindFromConfig(t *testing.T) {
	const cfg = `%% texmf.cnf configuration

TEXMFROOT = /usr/share
TEXMFDIST = $TEXMFROOT/texmf-dist
TEXMFMAIN = ${TEXMFDIST}

% Local additions to the distribution trees.
TEXMFLOCAL = /usr/local/share/texmf;/usr/share/texmf

% TEXMFSYSVAR, where *-sys store cached runtime data.
TEXMFSYSVAR = /var/lib/texmf

% TEXMFSYSCONFIG, where *-sys store configuration data.
TEXMFSYSCONFIG = /etc/texmf

% Per-user texmf tree(s) -- organized per the TDS, as usual.  To define
% more than one per-user tree, set this to a list of directories in
% braces, as described above.  (This used to be HOMETEXMF.)  ~ expands
% to %USERPROFILE% on Windows, $HOME otherwise.
TEXMFHOME = ~/texmf

% This is the value manipulated by tlmgr's auxtrees subcommand in the
% root texmf.cnf. Kpathsea warns about a literally empty string for a
% value, hence the empty braces.
TEXMFAUXTREES = {}

TEXMF = {$TEXMFCONFIG,$TEXMFVAR,$TEXMFHOME,!!$TEXMFSYSCONFIG,!!$TEXMFSYSVAR,!!$TEXMFLOCAL,!!$TEXMFDIST}

TEXMFDBS = {!!$TEXMFLOCAL,!!$TEXMFSYSCONFIG,!!$TEXMFSYSVAR,!!$TEXMFDIST}

SYSTEXMF = $TEXMFSYSVAR;$TEXMFLOCAL;$TEXMFDIST

TEXMFCACHE = $TEXMFSYSVAR;$TEXMFVAR

VARTEXFONTS = $TEXMFVAR/fonts

%%%%%%%%%%%%%%%%%%%%

TEXINPUTS.tex           = .;$TEXMF/tex/{plain,generic,}//
TEXINPUTS.fontinst      = .;$TEXMF/{tex,fonts/afm}//
TEXINPUTS.amstex        = .;$TEXMF/tex/{amstex,plain,generic,}//
TEXINPUTS.csplain       = .;$TEXMF/tex/{csplain,plain,generic,}//
TEXINPUTS.eplain        = .;$TEXMF/tex/{eplain,plain,generic,}//
TEXINPUTS.ftex          = .;$TEXMF/tex/{formate,plain,generic,}//
TEXINPUTS.mex           = .;$TEXMF/tex/{mex,plain,generic,}//
TEXINPUTS.texinfo       = .;$TEXMF/tex/{texinfo,plain,generic,}//

% support the original xdvi.  Must come before the generic settings.
PKFONTS.XDvi   = .;$TEXMF/%s;$VARTEXFONTS/pk/{%m,modeless}//
VFFONTS.XDvi   = .;$TEXMF/%s
PSHEADERS.XDvi = .;$TEXMF/%q{dvips,fonts/type1}//
TEXPICTS.XDvi  = .;$TEXMF/%q{dvips,tex}//

`

	defer func() {
		// FIXME(sbinet): implement config parser.
		err := recover()
		if err == nil {
			t.Fatalf("expected panic")
		}
	}()

	ctx, err := parseConfig(strings.NewReader(cfg))
	if err != nil {
		t.Fatal(err)
	}

	_ = ctx // FIXME(sbinet): test Find/FindAll
}
diff --git a/kpath/stringset.go b/kpath/stringset.go
new file mode 100644
index 0000000..067a445
--- /dev/null
+++ b/kpath/stringset.go
@@ -0,0 +1,60 @@
// Copyright ©2021 The star-tex Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package kpath

type strset struct {
	db map[string]struct{}
	ks []string
}

func newStrSet(vs ...string) strset {
	set := strset{
		db: make(map[string]struct{}, len(vs)),
		ks: make([]string, len(vs)),
	}
	for i, v := range vs {
		set.db[v] = struct{}{}
		set.ks[i] = v
	}
	return set
}

func (set strset) has(k string) bool {
	_, ok := set.db[k]
	return ok
}

var (
	strsets = map[string]strset{
		"tex": newStrSet(
			".tex",
			".sty", ".cls", ".fd", ".aux", ".bbl", ".def", ".clo", ".ldf",
		),
		"texpool":            newStrSet(".pool"),
		"TeX system sources": newStrSet(".dtx", ".ins"),

		"gf":   newStrSet(".gf"),
		"pk":   newStrSet(".pk"),
		"tfm":  newStrSet(".tfm"),
		"afm":  newStrSet(".afm"),
		"base": newStrSet(".base"),
		"bib":  newStrSet(".bib"),
		"bst":  newStrSet(".bst"),
		"cnf":  newStrSet(".cnf"),
		"fmt":  newStrSet(".fmt"),
		"mf":   newStrSet(".mf"),
		"mft":  newStrSet(".mft"),
		"mp":   newStrSet(".mp"),
		"ofm":  newStrSet(".ofm", ".tfm"),
		"vf":   newStrSet(".vf"),
		"lig":  newStrSet(".lig"),

		"enc files":      newStrSet(".enc"),
		"type1 fonts":    newStrSet(".pfa", ".pfb"),
		"truetype fonts": newStrSet(".ttf", ".ttc", ".TTF", ".TTC", ".dfont"),
		"type42 fonts":   newStrSet(".t42", ".T42"),
		"opentype fonts": newStrSet(".otf", ".OTF"),
	}
)
-- 
2.30.1
Reply to thread Export thread (mbox)