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