diff --git a/go.mod b/go.mod
index c6f04c6..792ba0d 100644
--- a/go.mod
+++ b/go.mod
@@ -7,6 +7,7 @@ require (
github.com/stretchr/testify v1.11.1
github.com/urfave/cli v1.22.17
golang.org/x/net v0.55.0
+ golang.org/x/tools v0.45.0
)
require (
diff --git a/go.sum b/go.sum
index 1bd2c0c..7a15131 100644
--- a/go.sum
+++ b/go.sum
@@ -31,6 +31,8 @@ golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
+golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
+golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/pkg/xsd/extension.go b/pkg/xsd/extension.go
index 09bd63e..40367d3 100644
--- a/pkg/xsd/extension.go
+++ b/pkg/xsd/extension.go
@@ -43,13 +43,13 @@ func (ext *Extension) Elements() []Element {
goNames[attr.GoName()] = struct{}{}
}
- final := []Element{}
- for _, element := range elements {
+ final := make([]Element, len(elements))
+ for i, element := range elements {
if _, found := goNames[element.GoFieldName()]; found {
element.FieldOverride = true
}
goNames[element.GoFieldName()] = struct{}{}
- final = append(final, element)
+ final[i] = element
}
return final
}
diff --git a/pkg/xsd/schema.go b/pkg/xsd/schema.go
index 92dfe93..6198d75 100644
--- a/pkg/xsd/schema.go
+++ b/pkg/xsd/schema.go
@@ -6,7 +6,7 @@ import (
"io"
"os"
"path/filepath"
- "sort"
+ "slices"
"strings"
"golang.org/x/net/html/charset"
@@ -312,7 +312,7 @@ func (sch *Schema) GoImportsNeeded() []string {
for _, importedMod := range sch.importedModules {
imports = append(imports, fmt.Sprintf("%s/%s", sch.ModulesPath, importedMod.GoPackageName()))
}
- sort.Strings(imports)
+ slices.Sort(imports)
return imports
}
diff --git a/pkg/xsd/workspace.go b/pkg/xsd/workspace.go
index 8ec8c9b..6f39795 100644
--- a/pkg/xsd/workspace.go
+++ b/pkg/xsd/workspace.go
@@ -7,6 +7,7 @@ import (
type Workspace struct {
Cache map[string]*Schema // Parsed XSD schemas by its filename (user specifies initial one, and we load dependencies)
+ Loaded map[string]*Schema // Parsed AND resolved schemas by its filename
GoModulesPath string // user requested go package path (example: github.com/gocomply/scap)
xmlnsOverrides xmlnsOverrides // user-supplied xmlns overrides
}
@@ -14,6 +15,7 @@ type Workspace struct {
func NewWorkspace(goModulesPath, xsdPath string, xmlnsOverrides []string) (*Workspace, error) {
ws := Workspace{
Cache: map[string]*Schema{},
+ Loaded: map[string]*Schema{},
GoModulesPath: goModulesPath,
}
var err error
@@ -29,11 +31,34 @@ func NewWorkspace(goModulesPath, xsdPath string, xmlnsOverrides []string) (*Work
return &ws, ws.compile()
}
+// merges unique elements of newer into origin, compared by getName.
+func merge[T any, M comparable](newer, origin []T, getName func(T) M) []T {
+ names := make(map[M]struct{})
+ for _, o := range origin {
+ names[getName(o)] = struct{}{}
+ }
+
+ for _, n := range newer {
+ name := getName(n)
+ if _, ok := names[name]; ok {
+ continue
+ }
+ origin = append(origin, n)
+ names[name] = struct{}{}
+ }
+ return origin
+}
+
func (ws *Workspace) loadXsd(xsdPath string, shouldBeInlined bool) (*Schema, error) {
- cached, found := ws.Cache[xsdPath]
- if found {
+ xsdPath = filepath.Clean(xsdPath)
+
+ if schema, found := ws.Loaded[xsdPath]; found {
+ return schema, nil
+ }
+ if cached, found := ws.Cache[xsdPath]; found {
return cached, nil
}
+
fmt.Println("\tParsing:", xsdPath)
schema, err := ReadSchemaFromFile(xsdPath)
@@ -45,6 +70,8 @@ func (ws *Workspace) loadXsd(xsdPath string, shouldBeInlined bool) (*Schema, err
schema.filePath = xsdPath
schema.goPackageNameOverride = ws.xmlnsOverrides.override(schema.TargetNamespace)
+ ws.Loaded[xsdPath] = schema
+
if !shouldBeInlined {
// Cache all loaded schemas in the workspace, unless it was brought in by xsd:include element.
// Unlike xsd:import, xsd:include does not result in a separate schema in the workspace.
@@ -60,13 +87,13 @@ func (ws *Workspace) loadXsd(xsdPath string, shouldBeInlined bool) (*Schema, err
}
isch := si.IncludedSchema
- schema.Imports = append(isch.Imports, schema.Imports...)
- schema.Elements = append(isch.Elements, schema.Elements...)
- schema.Attributes = append(isch.Attributes, schema.Attributes...)
- schema.AttributeGroups = append(isch.AttributeGroups, schema.AttributeGroups...)
- schema.ComplexTypes = append(isch.ComplexTypes, schema.ComplexTypes...)
- schema.SimpleTypes = append(isch.SimpleTypes, schema.SimpleTypes...)
- schema.inlinedElements = append(isch.inlinedElements, schema.inlinedElements...)
+ schema.Imports = merge(isch.Imports, schema.Imports, func(i Import) string { return i.Namespace + i.SchemaLocation })
+ schema.Elements = merge(isch.Elements, schema.Elements, func(e Element) string { return e.Name })
+ schema.Attributes = merge(isch.Attributes, schema.Attributes, func(a Attribute) string { return a.Name })
+ schema.AttributeGroups = merge(isch.AttributeGroups, schema.AttributeGroups, func(ag AttributeGroup) string { return ag.Name })
+ schema.ComplexTypes = merge(isch.ComplexTypes, schema.ComplexTypes, func(ct ComplexType) string { return ct.Name })
+ schema.SimpleTypes = merge(isch.SimpleTypes, schema.SimpleTypes, func(st SimpleType) string { return st.Name })
+ schema.inlinedElements = merge(isch.inlinedElements, schema.inlinedElements, func(e Element) string { return e.Name })
for key, sch := range isch.importedModules {
schema.importedModules[key] = sch
}
diff --git a/tests/smoke_test.go b/tests/smoke_test.go
index 3213142..1e795f2 100644
--- a/tests/smoke_test.go
+++ b/tests/smoke_test.go
@@ -11,6 +11,7 @@ import (
"github.com/gocomply/xsd2go/pkg/xsd2go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
+ "golang.org/x/tools/txtar"
)
func TestSanity(t *testing.T) {
@@ -58,3 +59,59 @@ func locateGeneratedFile(outputDir string) (string, error) {
}
return golangFiles[0], nil
}
+
+func extractTxtar(t *testing.T, name string) (dir string) {
+ t.Helper()
+
+ data, err := os.ReadFile(name)
+ require.NoError(t, err)
+
+ ar := txtar.Parse(data)
+
+ dir = os.TempDir()
+
+ for _, f := range ar.Files {
+ path := filepath.Join(dir, f.Name)
+
+ require.NoError(t,
+ os.MkdirAll(filepath.Dir(path), 0755))
+
+ require.NoError(t,
+ os.WriteFile(path, f.Data, 0600))
+ }
+
+ return
+}
+
+func TestCircularImport(t *testing.T) {
+ workdir, err := os.Getwd()
+ require.NoError(t, err)
+
+ xsdFiles, err := filepath.Glob("xsd-examples/modules/*.txtar")
+ require.NoError(t, err)
+ assert.NotEmpty(t, xsdFiles)
+
+ for _, xsdPath := range xsdFiles {
+ dir := extractTxtar(t, filepath.Join(workdir, xsdPath))
+ t.Chdir(dir)
+
+ data, err := os.ReadFile(filepath.Join(workdir, xsdPath+".out"))
+ require.NoError(t, err)
+ expectar := txtar.Parse(data)
+
+ err = xsd2go.Convert(
+ "a.xsd",
+ "example.com/test",
+ "out",
+ nil,
+ )
+ require.NoError(t, err)
+
+ for _, expect := range expectar.Files {
+ got, err := os.ReadFile(filepath.Join(dir, "out", expect.Name))
+ require.NoError(t, err)
+
+ assert.Equal(t, strings.ReplaceAll(string(expect.Data), "\r\n", "\n"), string(got))
+ }
+ }
+}
diff --git a/tests/xsd-examples/modules/circulair-import.txtar b/tests/xsd-examples/modules/circulair-import.txtar
new file mode 100644
index 0000000..5bf5c0e
--- /dev/null
+++ b/tests/xsd-examples/modules/circulair-import.txtar
@@ -0,0 +1,33 @@
+-- a.xsd --
+
+
+
+
+
+
+
+
+
+
+
+
+-- b.xsd --
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/xsd-examples/modules/circulair-import.txtar.out b/tests/xsd-examples/modules/circulair-import.txtar.out
new file mode 100644
index 0000000..91900ae
--- /dev/null
+++ b/tests/xsd-examples/modules/circulair-import.txtar.out
@@ -0,0 +1,36 @@
+-- a/models.go --
+// Code generated by https://github.com/gocomply/xsd2go; DO NOT EDIT.
+// Models for urn:a
+package a
+
+import (
+ "encoding/xml"
+ "example.com/test/out/b"
+)
+
+// XSD ComplexType declarations
+
+type Atype struct {
+ XMLName xml.Name
+ B b.Btype `xml:"b"`
+}
+
+// XSD SimpleType declarations
+-- b/models.go --
+// Code generated by https://github.com/gocomply/xsd2go; DO NOT EDIT.
+// Models for urn:b
+package b
+
+import (
+ "encoding/xml"
+ "example.com/test/out/a"
+)
+
+// XSD ComplexType declarations
+
+type Btype struct {
+ XMLName xml.Name
+ A a.Atype `xml:"a"`
+}
+
+// XSD SimpleType declarations
diff --git a/tests/xsd-examples/modules/circulair-include.txtar b/tests/xsd-examples/modules/circulair-include.txtar
new file mode 100644
index 0000000..27ecbd8
--- /dev/null
+++ b/tests/xsd-examples/modules/circulair-include.txtar
@@ -0,0 +1,21 @@
+-- a.xsd --
+
+
+
+
+
+
+
+
+-- b.xsd --
+
+
+
+
+
+
+
diff --git a/tests/xsd-examples/modules/circulair-include.txtar.out b/tests/xsd-examples/modules/circulair-include.txtar.out
new file mode 100644
index 0000000..b841ca6
--- /dev/null
+++ b/tests/xsd-examples/modules/circulair-include.txtar.out
@@ -0,0 +1,20 @@
+-- a/models.go --
+// Code generated by https://github.com/gocomply/xsd2go; DO NOT EDIT.
+// Models for urn:test
+package a
+
+import (
+ "encoding/xml"
+)
+
+// XSD ComplexType declarations
+
+type Atype struct {
+ XMLName xml.Name
+}
+
+type Btype struct {
+ XMLName xml.Name
+}
+
+// XSD SimpleType declarations
diff --git a/vendor/golang.org/x/tools/LICENSE b/vendor/golang.org/x/tools/LICENSE
new file mode 100644
index 0000000..2a7cf70
--- /dev/null
+++ b/vendor/golang.org/x/tools/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2009 The Go Authors.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google LLC nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/vendor/golang.org/x/tools/PATENTS b/vendor/golang.org/x/tools/PATENTS
new file mode 100644
index 0000000..7330990
--- /dev/null
+++ b/vendor/golang.org/x/tools/PATENTS
@@ -0,0 +1,22 @@
+Additional IP Rights Grant (Patents)
+
+"This implementation" means the copyrightable works distributed by
+Google as part of the Go project.
+
+Google hereby grants to You a perpetual, worldwide, non-exclusive,
+no-charge, royalty-free, irrevocable (except as stated in this section)
+patent license to make, have made, use, offer to sell, sell, import,
+transfer and otherwise run, modify and propagate the contents of this
+implementation of Go, where such license applies only to those patent
+claims, both currently owned or controlled by Google and acquired in
+the future, licensable by Google that are necessarily infringed by this
+implementation of Go. This grant does not include claims that would be
+infringed only as a consequence of further modification of this
+implementation. If you or your agent or exclusive licensee institute or
+order or agree to the institution of patent litigation against any
+entity (including a cross-claim or counterclaim in a lawsuit) alleging
+that this implementation of Go or any code incorporated within this
+implementation of Go constitutes direct or contributory patent
+infringement, or inducement of patent infringement, then any patent
+rights granted to you under this License for this implementation of Go
+shall terminate as of the date such litigation is filed.
diff --git a/vendor/golang.org/x/tools/txtar/archive.go b/vendor/golang.org/x/tools/txtar/archive.go
new file mode 100644
index 0000000..85e4dc4
--- /dev/null
+++ b/vendor/golang.org/x/tools/txtar/archive.go
@@ -0,0 +1,143 @@
+// Copyright 2018 The Go 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 txtar implements a trivial text-based file archive format.
+//
+// The goals for the format are:
+//
+// - be trivial enough to create and edit by hand.
+// - be able to store trees of text files describing go command test cases.
+// - diff nicely in git history and code reviews.
+//
+// Non-goals include being a completely general archive format,
+// storing binary data, storing file modes, storing special files like
+// symbolic links, and so on.
+//
+// # Txtar format
+//
+// A txtar archive is zero or more comment lines and then a sequence of file entries.
+// Each file entry begins with a file marker line of the form "-- FILENAME --"
+// and is followed by zero or more file content lines making up the file data.
+// The comment or file content ends at the next file marker line.
+// The file marker line must begin with the three-byte sequence "-- "
+// and end with the three-byte sequence " --", but the enclosed
+// file name can be surrounding by additional white space,
+// all of which is stripped.
+//
+// If the txtar file is missing a trailing newline on the final line,
+// parsers should consider a final newline to be present anyway.
+//
+// There are no possible syntax errors in a txtar archive.
+package txtar
+
+import (
+ "bytes"
+ "fmt"
+ "os"
+ "strings"
+)
+
+// An Archive is a collection of files.
+type Archive struct {
+ Comment []byte
+ Files []File
+}
+
+// A File is a single file in an archive.
+type File struct {
+ Name string // name of file ("foo/bar.txt")
+ Data []byte // text content of file
+}
+
+// Format returns the serialized form of an Archive.
+// It is assumed that the Archive data structure is well-formed:
+// a.Comment and all a.File[i].Data contain no file marker lines,
+// and all a.File[i].Name is non-empty.
+func Format(a *Archive) []byte {
+ var buf bytes.Buffer
+ buf.Write(fixNL(a.Comment))
+ for _, f := range a.Files {
+ fmt.Fprintf(&buf, "-- %s --\n", f.Name)
+ buf.Write(fixNL(f.Data))
+ }
+ return buf.Bytes()
+}
+
+// ParseFile parses the named file as an archive.
+func ParseFile(file string) (*Archive, error) {
+ data, err := os.ReadFile(file)
+ if err != nil {
+ return nil, err
+ }
+ return Parse(data), nil
+}
+
+// Parse parses the serialized form of an Archive.
+// The returned Archive holds slices of data.
+func Parse(data []byte) *Archive {
+ a := new(Archive)
+ var name string
+ a.Comment, name, data = findFileMarker(data)
+ for name != "" {
+ f := File{name, nil}
+ f.Data, name, data = findFileMarker(data)
+ a.Files = append(a.Files, f)
+ }
+ return a
+}
+
+var (
+ newlineMarker = []byte("\n-- ")
+ marker = []byte("-- ")
+ markerEnd = []byte(" --")
+)
+
+// findFileMarker finds the next file marker in data,
+// extracts the file name, and returns the data before the marker,
+// the file name, and the data after the marker.
+// If there is no next marker, findFileMarker returns before = fixNL(data), name = "", after = nil.
+func findFileMarker(data []byte) (before []byte, name string, after []byte) {
+ var i int
+ for {
+ if name, after = isMarker(data[i:]); name != "" {
+ return data[:i], name, after
+ }
+ j := bytes.Index(data[i:], newlineMarker)
+ if j < 0 {
+ return fixNL(data), "", nil
+ }
+ i += j + 1 // positioned at start of new possible marker
+ }
+}
+
+// isMarker checks whether data begins with a file marker line.
+// If so, it returns the name from the line and the data after the line.
+// Otherwise it returns name == "" with an unspecified after.
+func isMarker(data []byte) (name string, after []byte) {
+ if !bytes.HasPrefix(data, marker) {
+ return "", nil
+ }
+ if i := bytes.IndexByte(data, '\n'); i >= 0 {
+ data, after = data[:i], data[i+1:]
+ if data[i-1] == '\r' { // handle \r\n line ending
+ data = data[:i-1]
+ }
+ }
+ if !(bytes.HasSuffix(data, markerEnd) && len(data) >= len(marker)+len(markerEnd)) {
+ return "", nil
+ }
+ return strings.TrimSpace(string(data[len(marker) : len(data)-len(markerEnd)])), after
+}
+
+// If data is empty or ends in \n, fixNL returns data.
+// Otherwise fixNL returns a new slice consisting of data with a final \n added.
+func fixNL(data []byte) []byte {
+ if len(data) == 0 || data[len(data)-1] == '\n' {
+ return data
+ }
+ d := make([]byte, len(data)+1)
+ copy(d, data)
+ d[len(data)] = '\n'
+ return d
+}
diff --git a/vendor/golang.org/x/tools/txtar/fs.go b/vendor/golang.org/x/tools/txtar/fs.go
new file mode 100644
index 0000000..fc8df12
--- /dev/null
+++ b/vendor/golang.org/x/tools/txtar/fs.go
@@ -0,0 +1,257 @@
+// Copyright 2024 The Go 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 txtar
+
+import (
+ "errors"
+ "fmt"
+ "io"
+ "io/fs"
+ "path"
+ "slices"
+ "time"
+)
+
+// FS returns the file system form of an Archive.
+// It returns an error if any of the file names in the archive
+// are not valid file system names.
+// The archive must not be modified while the FS is in use.
+//
+// If the file system detects that it has been modified, calls to the
+// file system return an ErrModified error.
+func FS(a *Archive) (fs.FS, error) {
+ // Create a filesystem with a root directory.
+ root := &node{fileinfo: fileinfo{path: ".", mode: readOnlyDir}}
+ fsys := &filesystem{a, map[string]*node{root.path: root}}
+
+ if err := initFiles(fsys); err != nil {
+ return nil, fmt.Errorf("cannot create fs.FS from txtar.Archive: %s", err)
+ }
+ return fsys, nil
+}
+
+const (
+ readOnly fs.FileMode = 0o444 // read only mode
+ readOnlyDir = readOnly | fs.ModeDir
+)
+
+// ErrModified indicates that file system returned by FS
+// noticed that the underlying archive has been modified
+// since the call to FS. Detection of modification is best effort,
+// to help diagnose misuse of the API, and is not guaranteed.
+var ErrModified error = errors.New("txtar.Archive has been modified during txtar.FS")
+
+// A filesystem is a simple in-memory file system for txtar archives,
+// represented as a map from valid path names to information about the
+// files or directories they represent.
+//
+// File system operations are read only. Modifications to the underlying
+// *Archive may race. To help prevent this, the filesystem tries
+// to detect modification during Open and return ErrModified if it
+// is able to detect a modification.
+type filesystem struct {
+ ar *Archive
+ nodes map[string]*node
+}
+
+// node is a file or directory in the tree of a filesystem.
+type node struct {
+ fileinfo // fs.FileInfo and fs.DirEntry implementation
+ idx int // index into ar.Files (for files)
+ entries []fs.DirEntry // subdirectories and files (for directories)
+}
+
+var _ fs.FS = (*filesystem)(nil)
+var _ fs.DirEntry = (*node)(nil)
+
+// initFiles initializes fsys from fsys.ar.Files. Returns an error if there are any
+// invalid file names or collisions between file or directories.
+func initFiles(fsys *filesystem) error {
+ for idx, file := range fsys.ar.Files {
+ name := file.Name
+ if !fs.ValidPath(name) {
+ return fmt.Errorf("file %q is an invalid path", name)
+ }
+
+ n := &node{idx: idx, fileinfo: fileinfo{path: name, size: len(file.Data), mode: readOnly}}
+ if err := insert(fsys, n); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// insert adds node n as an entry to its parent directory within the filesystem.
+func insert(fsys *filesystem, n *node) error {
+ if m := fsys.nodes[n.path]; m != nil {
+ return fmt.Errorf("duplicate path %q", n.path)
+ }
+ fsys.nodes[n.path] = n
+
+ // fsys.nodes contains "." to prevent infinite loops.
+ parent, err := directory(fsys, path.Dir(n.path))
+ if err != nil {
+ return err
+ }
+ parent.entries = append(parent.entries, n)
+ return nil
+}
+
+// directory returns the directory node with the path dir and lazily-creates it
+// if it does not exist.
+func directory(fsys *filesystem, dir string) (*node, error) {
+ if m := fsys.nodes[dir]; m != nil && m.IsDir() {
+ return m, nil // pre-existing directory
+ }
+
+ n := &node{fileinfo: fileinfo{path: dir, mode: readOnlyDir}}
+ if err := insert(fsys, n); err != nil {
+ return nil, err
+ }
+ return n, nil
+}
+
+// dataOf returns the data associated with the file t.
+// May return ErrModified if fsys.ar has been modified.
+func dataOf(fsys *filesystem, n *node) ([]byte, error) {
+ if n.idx >= len(fsys.ar.Files) {
+ return nil, ErrModified
+ }
+
+ f := fsys.ar.Files[n.idx]
+ if f.Name != n.path || len(f.Data) != n.size {
+ return nil, ErrModified
+ }
+ return f.Data, nil
+}
+
+func (fsys *filesystem) Open(name string) (fs.File, error) {
+ if !fs.ValidPath(name) {
+ return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrInvalid}
+ }
+
+ n := fsys.nodes[name]
+ switch {
+ case n == nil:
+ return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
+ case n.IsDir():
+ return &openDir{fileinfo: n.fileinfo, entries: n.entries}, nil
+ default:
+ data, err := dataOf(fsys, n)
+ if err != nil {
+ return nil, err
+ }
+ return &openFile{fileinfo: n.fileinfo, data: data}, nil
+ }
+}
+
+func (fsys *filesystem) ReadFile(name string) ([]byte, error) {
+ file, err := fsys.Open(name)
+ if err != nil {
+ return nil, err
+ }
+ if file, ok := file.(*openFile); ok {
+ return slices.Clone(file.data), nil
+ }
+ return nil, &fs.PathError{Op: "read", Path: name, Err: fs.ErrInvalid}
+}
+
+// A fileinfo implements fs.FileInfo and fs.DirEntry for a given archive file.
+type fileinfo struct {
+ path string // unique path to the file or directory within a filesystem
+ size int
+ mode fs.FileMode
+}
+
+var _ fs.FileInfo = (*fileinfo)(nil)
+var _ fs.DirEntry = (*fileinfo)(nil)
+
+func (i *fileinfo) Name() string { return path.Base(i.path) }
+func (i *fileinfo) Size() int64 { return int64(i.size) }
+func (i *fileinfo) Mode() fs.FileMode { return i.mode }
+func (i *fileinfo) Type() fs.FileMode { return i.mode.Type() }
+func (i *fileinfo) ModTime() time.Time { return time.Time{} }
+func (i *fileinfo) IsDir() bool { return i.mode&fs.ModeDir != 0 }
+func (i *fileinfo) Sys() any { return nil }
+func (i *fileinfo) Info() (fs.FileInfo, error) { return i, nil }
+
+// An openFile is a regular (non-directory) fs.File open for reading.
+type openFile struct {
+ fileinfo
+ data []byte
+ offset int64
+}
+
+var _ fs.File = (*openFile)(nil)
+
+func (f *openFile) Stat() (fs.FileInfo, error) { return &f.fileinfo, nil }
+func (f *openFile) Close() error { return nil }
+func (f *openFile) Read(b []byte) (int, error) {
+ if f.offset >= int64(len(f.data)) {
+ return 0, io.EOF
+ }
+ if f.offset < 0 {
+ return 0, &fs.PathError{Op: "read", Path: f.path, Err: fs.ErrInvalid}
+ }
+ n := copy(b, f.data[f.offset:])
+ f.offset += int64(n)
+ return n, nil
+}
+
+func (f *openFile) Seek(offset int64, whence int) (int64, error) {
+ switch whence {
+ case 0:
+ // offset += 0
+ case 1:
+ offset += f.offset
+ case 2:
+ offset += int64(len(f.data))
+ }
+ if offset < 0 || offset > int64(len(f.data)) {
+ return 0, &fs.PathError{Op: "seek", Path: f.path, Err: fs.ErrInvalid}
+ }
+ f.offset = offset
+ return offset, nil
+}
+
+func (f *openFile) ReadAt(b []byte, offset int64) (int, error) {
+ if offset < 0 || offset > int64(len(f.data)) {
+ return 0, &fs.PathError{Op: "read", Path: f.path, Err: fs.ErrInvalid}
+ }
+ n := copy(b, f.data[offset:])
+ if n < len(b) {
+ return n, io.EOF
+ }
+ return n, nil
+}
+
+// A openDir is a directory fs.File (so also an fs.ReadDirFile) open for reading.
+type openDir struct {
+ fileinfo
+ entries []fs.DirEntry
+ offset int
+}
+
+var _ fs.ReadDirFile = (*openDir)(nil)
+
+func (d *openDir) Stat() (fs.FileInfo, error) { return &d.fileinfo, nil }
+func (d *openDir) Close() error { return nil }
+func (d *openDir) Read(b []byte) (int, error) {
+ return 0, &fs.PathError{Op: "read", Path: d.path, Err: fs.ErrInvalid}
+}
+
+func (d *openDir) ReadDir(count int) ([]fs.DirEntry, error) {
+ n := len(d.entries) - d.offset
+ if n == 0 && count > 0 {
+ return nil, io.EOF
+ }
+ if count > 0 && n > count {
+ n = count
+ }
+ list := make([]fs.DirEntry, n)
+ copy(list, d.entries[d.offset:d.offset+n])
+ d.offset += n
+ return list, nil
+}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 2c8dd75..d1ceda1 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -47,6 +47,9 @@ golang.org/x/text/internal/utf8internal
golang.org/x/text/language
golang.org/x/text/runes
golang.org/x/text/transform
+# golang.org/x/tools v0.45.0
+## explicit; go 1.25.0
+golang.org/x/tools/txtar
# gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127
## explicit
# gopkg.in/yaml.v3 v3.0.1