Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • master
  • 0.1.0
  • 0.1.1
3 results

Target

Select target project
  • oss/utilities/requirements-manager
1 result
Select Git revision
  • master
  • 0.1.0
  • 0.1.1
3 results
Show changes
Showing
with 2305 additions and 0 deletions
// Copyright 2009 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 token defines constants representing the lexical tokens of the gcfg
// configuration syntax and basic operations on tokens (printing, predicates).
//
// Note that the API for the token package may change to accommodate new
// features or implementation changes in gcfg.
//
package token
import "strconv"
// Token is the set of lexical tokens of the gcfg configuration syntax.
type Token int
// The list of tokens.
const (
// Special tokens
ILLEGAL Token = iota
EOF
COMMENT
literal_beg
// Identifiers and basic type literals
// (these tokens stand for classes of literals)
IDENT // section-name, variable-name
STRING // "subsection-name", variable value
literal_end
operator_beg
// Operators and delimiters
ASSIGN // =
LBRACK // [
RBRACK // ]
EOL // \n
operator_end
)
var tokens = [...]string{
ILLEGAL: "ILLEGAL",
EOF: "EOF",
COMMENT: "COMMENT",
IDENT: "IDENT",
STRING: "STRING",
ASSIGN: "=",
LBRACK: "[",
RBRACK: "]",
EOL: "\n",
}
// String returns the string corresponding to the token tok.
// For operators and delimiters, the string is the actual token character
// sequence (e.g., for the token ASSIGN, the string is "="). For all other
// tokens the string corresponds to the token constant name (e.g. for the
// token IDENT, the string is "IDENT").
//
func (tok Token) String() string {
s := ""
if 0 <= tok && tok < Token(len(tokens)) {
s = tokens[tok]
}
if s == "" {
s = "token(" + strconv.Itoa(int(tok)) + ")"
}
return s
}
// Predicates
// IsLiteral returns true for tokens corresponding to identifiers
// and basic type literals; it returns false otherwise.
//
func (tok Token) IsLiteral() bool { return literal_beg < tok && tok < literal_end }
// IsOperator returns true for tokens corresponding to operators and
// delimiters; it returns false otherwise.
//
func (tok Token) IsOperator() bool { return operator_beg < tok && tok < operator_end }
package types
// BoolValues defines the name and value mappings for ParseBool.
var BoolValues = map[string]interface{}{
"true": true, "yes": true, "on": true, "1": true,
"false": false, "no": false, "off": false, "0": false,
}
var boolParser = func() *EnumParser {
ep := &EnumParser{}
ep.AddVals(BoolValues)
return ep
}()
// ParseBool parses bool values according to the definitions in BoolValues.
// Parsing is case-insensitive.
func ParseBool(s string) (bool, error) {
v, err := boolParser.Parse(s)
if err != nil {
return false, err
}
return v.(bool), nil
}
// Package types defines helpers for type conversions.
//
// The API for this package is not finalized yet.
package types
package types
import (
"fmt"
"reflect"
"strings"
)
// EnumParser parses "enum" values; i.e. a predefined set of strings to
// predefined values.
type EnumParser struct {
Type string // type name; if not set, use type of first value added
CaseMatch bool // if true, matching of strings is case-sensitive
// PrefixMatch bool
vals map[string]interface{}
}
// AddVals adds strings and values to an EnumParser.
func (ep *EnumParser) AddVals(vals map[string]interface{}) {
if ep.vals == nil {
ep.vals = make(map[string]interface{})
}
for k, v := range vals {
if ep.Type == "" {
ep.Type = reflect.TypeOf(v).Name()
}
if !ep.CaseMatch {
k = strings.ToLower(k)
}
ep.vals[k] = v
}
}
// Parse parses the string and returns the value or an error.
func (ep EnumParser) Parse(s string) (interface{}, error) {
if !ep.CaseMatch {
s = strings.ToLower(s)
}
v, ok := ep.vals[s]
if !ok {
return false, fmt.Errorf("failed to parse %s %#q", ep.Type, s)
}
return v, nil
}
package types
import (
"fmt"
"strings"
)
// An IntMode is a mode for parsing integer values, representing a set of
// accepted bases.
type IntMode uint8
// IntMode values for ParseInt; can be combined using binary or.
const (
Dec IntMode = 1 << iota
Hex
Oct
)
// String returns a string representation of IntMode; e.g. `IntMode(Dec|Hex)`.
func (m IntMode) String() string {
var modes []string
if m&Dec != 0 {
modes = append(modes, "Dec")
}
if m&Hex != 0 {
modes = append(modes, "Hex")
}
if m&Oct != 0 {
modes = append(modes, "Oct")
}
return "IntMode(" + strings.Join(modes, "|") + ")"
}
var errIntAmbig = fmt.Errorf("ambiguous integer value; must include '0' prefix")
func prefix0(val string) bool {
return strings.HasPrefix(val, "0") || strings.HasPrefix(val, "-0")
}
func prefix0x(val string) bool {
return strings.HasPrefix(val, "0x") || strings.HasPrefix(val, "-0x")
}
// ParseInt parses val using mode into intptr, which must be a pointer to an
// integer kind type. Non-decimal value require prefix `0` or `0x` in the cases
// when mode permits ambiguity of base; otherwise the prefix can be omitted.
func ParseInt(intptr interface{}, val string, mode IntMode) error {
val = strings.TrimSpace(val)
verb := byte(0)
switch mode {
case Dec:
verb = 'd'
case Dec + Hex:
if prefix0x(val) {
verb = 'v'
} else {
verb = 'd'
}
case Dec + Oct:
if prefix0(val) && !prefix0x(val) {
verb = 'v'
} else {
verb = 'd'
}
case Dec + Hex + Oct:
verb = 'v'
case Hex:
if prefix0x(val) {
verb = 'v'
} else {
verb = 'x'
}
case Oct:
verb = 'o'
case Hex + Oct:
if prefix0(val) {
verb = 'v'
} else {
return errIntAmbig
}
}
if verb == 0 {
panic("unsupported mode")
}
return ScanFully(intptr, val, verb)
}
package types
import (
"fmt"
"io"
"reflect"
)
// ScanFully uses fmt.Sscanf with verb to fully scan val into ptr.
func ScanFully(ptr interface{}, val string, verb byte) error {
t := reflect.ValueOf(ptr).Elem().Type()
// attempt to read extra bytes to make sure the value is consumed
var b []byte
n, err := fmt.Sscanf(val, "%"+string(verb)+"%s", ptr, &b)
switch {
case n < 1 || n == 1 && err != io.EOF:
return fmt.Errorf("failed to parse %q as %v: %v", val, t, err)
case n > 1:
return fmt.Errorf("failed to parse %q as %v: extra characters %q", val, t, string(b))
}
// n == 1 && err == io.EOF
return nil
}
/coverage.txt
/vendor
Gopkg.lock
Gopkg.toml
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor 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, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2017 Sourced Technologies S.L.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
# Go parameters
GOCMD = go
GOTEST = $(GOCMD) test
WASIRUN_WRAPPER := $(CURDIR)/scripts/wasirun-wrapper
.PHONY: test
test:
$(GOTEST) -race ./...
test-coverage:
echo "" > $(COVERAGE_REPORT); \
$(GOTEST) -coverprofile=$(COVERAGE_REPORT) -coverpkg=./... -covermode=$(COVERAGE_MODE) ./...
.PHONY: wasitest
wasitest: export GOARCH=wasm
wasitest: export GOOS=wasip1
wasitest:
$(GOTEST) -exec $(WASIRUN_WRAPPER) ./...
# go-billy [![GoDoc](https://godoc.org/gopkg.in/go-git/go-billy.v5?status.svg)](https://pkg.go.dev/github.com/go-git/go-billy/v5) [![Test](https://github.com/go-git/go-billy/workflows/Test/badge.svg)](https://github.com/go-git/go-billy/actions?query=workflow%3ATest)
The missing interface filesystem abstraction for Go.
Billy implements an interface based on the `os` standard library, allowing to develop applications without dependency on the underlying storage. Makes it virtually free to implement mocks and testing over filesystem operations.
Billy was born as part of [go-git/go-git](https://github.com/go-git/go-git) project.
## Installation
```go
import "github.com/go-git/go-billy/v5" // with go modules enabled (GO111MODULE=on or outside GOPATH)
import "github.com/go-git/go-billy" // with go modules disabled
```
## Usage
Billy exposes filesystems using the
[`Filesystem` interface](https://pkg.go.dev/github.com/go-git/go-billy/v5?tab=doc#Filesystem).
Each filesystem implementation gives you a `New` method, whose arguments depend on
the implementation itself, that returns a new `Filesystem`.
The following example caches in memory all readable files in a directory from any
billy's filesystem implementation.
```go
func LoadToMemory(origin billy.Filesystem, path string) (*memory.Memory, error) {
memory := memory.New()
files, err := origin.ReadDir("/")
if err != nil {
return nil, err
}
for _, file := range files {
if file.IsDir() {
continue
}
src, err := origin.Open(file.Name())
if err != nil {
return nil, err
}
dst, err := memory.Create(file.Name())
if err != nil {
return nil, err
}
if _, err = io.Copy(dst, src); err != nil {
return nil, err
}
if err := dst.Close(); err != nil {
return nil, err
}
if err := src.Close(); err != nil {
return nil, err
}
}
return memory, nil
}
```
## Why billy?
The library billy deals with storage systems and Billy is the name of a well-known, IKEA
bookcase. That's it.
## License
Apache License Version 2.0, see [LICENSE](LICENSE)
package billy
import (
"errors"
"io"
"os"
"time"
)
var (
ErrReadOnly = errors.New("read-only filesystem")
ErrNotSupported = errors.New("feature not supported")
ErrCrossedBoundary = errors.New("chroot boundary crossed")
)
// Capability holds the supported features of a billy filesystem. This does
// not mean that the capability has to be supported by the underlying storage.
// For example, a billy filesystem may support WriteCapability but the
// storage be mounted in read only mode.
type Capability uint64
const (
// WriteCapability means that the fs is writable.
WriteCapability Capability = 1 << iota
// ReadCapability means that the fs is readable.
ReadCapability
// ReadAndWriteCapability is the ability to open a file in read and write mode.
ReadAndWriteCapability
// SeekCapability means it is able to move position inside the file.
SeekCapability
// TruncateCapability means that a file can be truncated.
TruncateCapability
// LockCapability is the ability to lock a file.
LockCapability
// DefaultCapabilities lists all capable features supported by filesystems
// without Capability interface. This list should not be changed until a
// major version is released.
DefaultCapabilities Capability = WriteCapability | ReadCapability |
ReadAndWriteCapability | SeekCapability | TruncateCapability |
LockCapability
// AllCapabilities lists all capable features.
AllCapabilities Capability = WriteCapability | ReadCapability |
ReadAndWriteCapability | SeekCapability | TruncateCapability |
LockCapability
)
// Filesystem abstract the operations in a storage-agnostic interface.
// Each method implementation mimics the behavior of the equivalent functions
// at the os package from the standard library.
type Filesystem interface {
Basic
TempFile
Dir
Symlink
Chroot
}
// Basic abstract the basic operations in a storage-agnostic interface as
// an extension to the Basic interface.
type Basic interface {
// Create creates the named file with mode 0666 (before umask), truncating
// it if it already exists. If successful, methods on the returned File can
// be used for I/O; the associated file descriptor has mode O_RDWR.
Create(filename string) (File, error)
// Open opens the named file for reading. If successful, methods on the
// returned file can be used for reading; the associated file descriptor has
// mode O_RDONLY.
Open(filename string) (File, error)
// OpenFile is the generalized open call; most users will use Open or Create
// instead. It opens the named file with specified flag (O_RDONLY etc.) and
// perm, (0666 etc.) if applicable. If successful, methods on the returned
// File can be used for I/O.
OpenFile(filename string, flag int, perm os.FileMode) (File, error)
// Stat returns a FileInfo describing the named file.
Stat(filename string) (os.FileInfo, error)
// Rename renames (moves) oldpath to newpath. If newpath already exists and
// is not a directory, Rename replaces it. OS-specific restrictions may
// apply when oldpath and newpath are in different directories.
Rename(oldpath, newpath string) error
// Remove removes the named file or directory.
Remove(filename string) error
// Join joins any number of path elements into a single path, adding a
// Separator if necessary. Join calls filepath.Clean on the result; in
// particular, all empty strings are ignored. On Windows, the result is a
// UNC path if and only if the first path element is a UNC path.
Join(elem ...string) string
}
type TempFile interface {
// TempFile creates a new temporary file in the directory dir with a name
// beginning with prefix, opens the file for reading and writing, and
// returns the resulting *os.File. If dir is the empty string, TempFile
// uses the default directory for temporary files (see os.TempDir).
// Multiple programs calling TempFile simultaneously will not choose the
// same file. The caller can use f.Name() to find the pathname of the file.
// It is the caller's responsibility to remove the file when no longer
// needed.
TempFile(dir, prefix string) (File, error)
}
// Dir abstract the dir related operations in a storage-agnostic interface as
// an extension to the Basic interface.
type Dir interface {
// ReadDir reads the directory named by dirname and returns a list of
// directory entries sorted by filename.
ReadDir(path string) ([]os.FileInfo, error)
// MkdirAll creates a directory named path, along with any necessary
// parents, and returns nil, or else returns an error. The permission bits
// perm are used for all directories that MkdirAll creates. If path is/
// already a directory, MkdirAll does nothing and returns nil.
MkdirAll(filename string, perm os.FileMode) error
}
// Symlink abstract the symlink related operations in a storage-agnostic
// interface as an extension to the Basic interface.
type Symlink interface {
// Lstat returns a FileInfo describing the named file. If the file is a
// symbolic link, the returned FileInfo describes the symbolic link. Lstat
// makes no attempt to follow the link.
Lstat(filename string) (os.FileInfo, error)
// Symlink creates a symbolic-link from link to target. target may be an
// absolute or relative path, and need not refer to an existing node.
// Parent directories of link are created as necessary.
Symlink(target, link string) error
// Readlink returns the target path of link.
Readlink(link string) (string, error)
}
// Change abstract the FileInfo change related operations in a storage-agnostic
// interface as an extension to the Basic interface
type Change interface {
// Chmod changes the mode of the named file to mode. If the file is a
// symbolic link, it changes the mode of the link's target.
Chmod(name string, mode os.FileMode) error
// Lchown changes the numeric uid and gid of the named file. If the file is
// a symbolic link, it changes the uid and gid of the link itself.
Lchown(name string, uid, gid int) error
// Chown changes the numeric uid and gid of the named file. If the file is a
// symbolic link, it changes the uid and gid of the link's target.
Chown(name string, uid, gid int) error
// Chtimes changes the access and modification times of the named file,
// similar to the Unix utime() or utimes() functions.
//
// The underlying filesystem may truncate or round the values to a less
// precise time unit.
Chtimes(name string, atime time.Time, mtime time.Time) error
}
// Chroot abstract the chroot related operations in a storage-agnostic interface
// as an extension to the Basic interface.
type Chroot interface {
// Chroot returns a new filesystem from the same type where the new root is
// the given path. Files outside of the designated directory tree cannot be
// accessed.
Chroot(path string) (Filesystem, error)
// Root returns the root path of the filesystem.
Root() string
}
// File represent a file, being a subset of the os.File
type File interface {
// Name returns the name of the file as presented to Open.
Name() string
io.Writer
// TODO: Add io.WriterAt for v6
// io.WriterAt
io.Reader
io.ReaderAt
io.Seeker
io.Closer
// Lock locks the file like e.g. flock. It protects against access from
// other processes.
Lock() error
// Unlock unlocks the file.
Unlock() error
// Truncate the file.
Truncate(size int64) error
}
// Capable interface can return the available features of a filesystem.
type Capable interface {
// Capabilities returns the capabilities of a filesystem in bit flags.
Capabilities() Capability
}
// Capabilities returns the features supported by a filesystem. If the FS
// does not implement Capable interface it returns all features.
func Capabilities(fs Basic) Capability {
capable, ok := fs.(Capable)
if !ok {
return DefaultCapabilities
}
return capable.Capabilities()
}
// CapabilityCheck tests the filesystem for the provided capabilities and
// returns true in case it supports all of them.
func CapabilityCheck(fs Basic, capabilities Capability) bool {
fsCaps := Capabilities(fs)
return fsCaps&capabilities == capabilities
}
package chroot
import (
"os"
"path/filepath"
"strings"
"github.com/go-git/go-billy/v5"
"github.com/go-git/go-billy/v5/helper/polyfill"
)
// ChrootHelper is a helper to implement billy.Chroot.
type ChrootHelper struct {
underlying billy.Filesystem
base string
}
// New creates a new filesystem wrapping up the given 'fs'.
// The created filesystem has its base in the given ChrootHelperectory of the
// underlying filesystem.
func New(fs billy.Basic, base string) billy.Filesystem {
return &ChrootHelper{
underlying: polyfill.New(fs),
base: base,
}
}
func (fs *ChrootHelper) underlyingPath(filename string) (string, error) {
if isCrossBoundaries(filename) {
return "", billy.ErrCrossedBoundary
}
return fs.Join(fs.Root(), filename), nil
}
func isCrossBoundaries(path string) bool {
path = filepath.ToSlash(path)
path = filepath.Clean(path)
return strings.HasPrefix(path, ".."+string(filepath.Separator))
}
func (fs *ChrootHelper) Create(filename string) (billy.File, error) {
fullpath, err := fs.underlyingPath(filename)
if err != nil {
return nil, err
}
f, err := fs.underlying.Create(fullpath)
if err != nil {
return nil, err
}
return newFile(fs, f, filename), nil
}
func (fs *ChrootHelper) Open(filename string) (billy.File, error) {
fullpath, err := fs.underlyingPath(filename)
if err != nil {
return nil, err
}
f, err := fs.underlying.Open(fullpath)
if err != nil {
return nil, err
}
return newFile(fs, f, filename), nil
}
func (fs *ChrootHelper) OpenFile(filename string, flag int, mode os.FileMode) (billy.File, error) {
fullpath, err := fs.underlyingPath(filename)
if err != nil {
return nil, err
}
f, err := fs.underlying.OpenFile(fullpath, flag, mode)
if err != nil {
return nil, err
}
return newFile(fs, f, filename), nil
}
func (fs *ChrootHelper) Stat(filename string) (os.FileInfo, error) {
fullpath, err := fs.underlyingPath(filename)
if err != nil {
return nil, err
}
return fs.underlying.Stat(fullpath)
}
func (fs *ChrootHelper) Rename(from, to string) error {
var err error
from, err = fs.underlyingPath(from)
if err != nil {
return err
}
to, err = fs.underlyingPath(to)
if err != nil {
return err
}
return fs.underlying.Rename(from, to)
}
func (fs *ChrootHelper) Remove(path string) error {
fullpath, err := fs.underlyingPath(path)
if err != nil {
return err
}
return fs.underlying.Remove(fullpath)
}
func (fs *ChrootHelper) Join(elem ...string) string {
return fs.underlying.Join(elem...)
}
func (fs *ChrootHelper) TempFile(dir, prefix string) (billy.File, error) {
fullpath, err := fs.underlyingPath(dir)
if err != nil {
return nil, err
}
f, err := fs.underlying.(billy.TempFile).TempFile(fullpath, prefix)
if err != nil {
return nil, err
}
return newFile(fs, f, fs.Join(dir, filepath.Base(f.Name()))), nil
}
func (fs *ChrootHelper) ReadDir(path string) ([]os.FileInfo, error) {
fullpath, err := fs.underlyingPath(path)
if err != nil {
return nil, err
}
return fs.underlying.(billy.Dir).ReadDir(fullpath)
}
func (fs *ChrootHelper) MkdirAll(filename string, perm os.FileMode) error {
fullpath, err := fs.underlyingPath(filename)
if err != nil {
return err
}
return fs.underlying.(billy.Dir).MkdirAll(fullpath, perm)
}
func (fs *ChrootHelper) Lstat(filename string) (os.FileInfo, error) {
fullpath, err := fs.underlyingPath(filename)
if err != nil {
return nil, err
}
return fs.underlying.(billy.Symlink).Lstat(fullpath)
}
func (fs *ChrootHelper) Symlink(target, link string) error {
target = filepath.FromSlash(target)
// only rewrite target if it's already absolute
if filepath.IsAbs(target) || strings.HasPrefix(target, string(filepath.Separator)) {
target = fs.Join(fs.Root(), target)
target = filepath.Clean(filepath.FromSlash(target))
}
link, err := fs.underlyingPath(link)
if err != nil {
return err
}
return fs.underlying.(billy.Symlink).Symlink(target, link)
}
func (fs *ChrootHelper) Readlink(link string) (string, error) {
fullpath, err := fs.underlyingPath(link)
if err != nil {
return "", err
}
target, err := fs.underlying.(billy.Symlink).Readlink(fullpath)
if err != nil {
return "", err
}
if !filepath.IsAbs(target) && !strings.HasPrefix(target, string(filepath.Separator)) {
return target, nil
}
target, err = filepath.Rel(fs.base, target)
if err != nil {
return "", err
}
return string(os.PathSeparator) + target, nil
}
func (fs *ChrootHelper) Chroot(path string) (billy.Filesystem, error) {
fullpath, err := fs.underlyingPath(path)
if err != nil {
return nil, err
}
return New(fs.underlying, fullpath), nil
}
func (fs *ChrootHelper) Root() string {
return fs.base
}
func (fs *ChrootHelper) Underlying() billy.Basic {
return fs.underlying
}
// Capabilities implements the Capable interface.
func (fs *ChrootHelper) Capabilities() billy.Capability {
return billy.Capabilities(fs.underlying)
}
type file struct {
billy.File
name string
}
func newFile(fs billy.Filesystem, f billy.File, filename string) billy.File {
filename = fs.Join(fs.Root(), filename)
filename, _ = filepath.Rel(fs.Root(), filename)
return &file{
File: f,
name: filename,
}
}
func (f *file) Name() string {
return f.name
}
package polyfill
import (
"os"
"path/filepath"
"github.com/go-git/go-billy/v5"
)
// Polyfill is a helper that implements all missing method from billy.Filesystem.
type Polyfill struct {
billy.Basic
c capabilities
}
type capabilities struct{ tempfile, dir, symlink, chroot bool }
// New creates a new filesystem wrapping up 'fs' the intercepts all the calls
// made and errors if fs doesn't implement any of the billy interfaces.
func New(fs billy.Basic) billy.Filesystem {
if original, ok := fs.(billy.Filesystem); ok {
return original
}
h := &Polyfill{Basic: fs}
_, h.c.tempfile = h.Basic.(billy.TempFile)
_, h.c.dir = h.Basic.(billy.Dir)
_, h.c.symlink = h.Basic.(billy.Symlink)
_, h.c.chroot = h.Basic.(billy.Chroot)
return h
}
func (h *Polyfill) TempFile(dir, prefix string) (billy.File, error) {
if !h.c.tempfile {
return nil, billy.ErrNotSupported
}
return h.Basic.(billy.TempFile).TempFile(dir, prefix)
}
func (h *Polyfill) ReadDir(path string) ([]os.FileInfo, error) {
if !h.c.dir {
return nil, billy.ErrNotSupported
}
return h.Basic.(billy.Dir).ReadDir(path)
}
func (h *Polyfill) MkdirAll(filename string, perm os.FileMode) error {
if !h.c.dir {
return billy.ErrNotSupported
}
return h.Basic.(billy.Dir).MkdirAll(filename, perm)
}
func (h *Polyfill) Symlink(target, link string) error {
if !h.c.symlink {
return billy.ErrNotSupported
}
return h.Basic.(billy.Symlink).Symlink(target, link)
}
func (h *Polyfill) Readlink(link string) (string, error) {
if !h.c.symlink {
return "", billy.ErrNotSupported
}
return h.Basic.(billy.Symlink).Readlink(link)
}
func (h *Polyfill) Lstat(path string) (os.FileInfo, error) {
if !h.c.symlink {
return nil, billy.ErrNotSupported
}
return h.Basic.(billy.Symlink).Lstat(path)
}
func (h *Polyfill) Chroot(path string) (billy.Filesystem, error) {
if !h.c.chroot {
return nil, billy.ErrNotSupported
}
return h.Basic.(billy.Chroot).Chroot(path)
}
func (h *Polyfill) Root() string {
if !h.c.chroot {
return string(filepath.Separator)
}
return h.Basic.(billy.Chroot).Root()
}
func (h *Polyfill) Underlying() billy.Basic {
return h.Basic
}
// Capabilities implements the Capable interface.
func (h *Polyfill) Capabilities() billy.Capability {
return billy.Capabilities(h.Basic)
}
// Package memfs provides a billy filesystem base on memory.
package memfs // import "github.com/go-git/go-billy/v5/memfs"
import (
"errors"
"fmt"
"io"
"os"
"path/filepath"
"sort"
"strings"
"syscall"
"time"
"github.com/go-git/go-billy/v5"
"github.com/go-git/go-billy/v5/helper/chroot"
"github.com/go-git/go-billy/v5/util"
)
const separator = filepath.Separator
var errNotLink = errors.New("not a link")
// Memory a very convenient filesystem based on memory files.
type Memory struct {
s *storage
tempCount int
}
// New returns a new Memory filesystem.
func New() billy.Filesystem {
fs := &Memory{s: newStorage()}
fs.s.New("/", 0755|os.ModeDir, 0)
return chroot.New(fs, string(separator))
}
func (fs *Memory) Create(filename string) (billy.File, error) {
return fs.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
}
func (fs *Memory) Open(filename string) (billy.File, error) {
return fs.OpenFile(filename, os.O_RDONLY, 0)
}
func (fs *Memory) OpenFile(filename string, flag int, perm os.FileMode) (billy.File, error) {
f, has := fs.s.Get(filename)
if !has {
if !isCreate(flag) {
return nil, os.ErrNotExist
}
var err error
f, err = fs.s.New(filename, perm, flag)
if err != nil {
return nil, err
}
} else {
if isExclusive(flag) {
return nil, os.ErrExist
}
if target, isLink := fs.resolveLink(filename, f); isLink {
if target != filename {
return fs.OpenFile(target, flag, perm)
}
}
}
if f.mode.IsDir() {
return nil, fmt.Errorf("cannot open directory: %s", filename)
}
return f.Duplicate(filename, perm, flag), nil
}
func (fs *Memory) resolveLink(fullpath string, f *file) (target string, isLink bool) {
if !isSymlink(f.mode) {
return fullpath, false
}
target = string(f.content.bytes)
if !isAbs(target) {
target = fs.Join(filepath.Dir(fullpath), target)
}
return target, true
}
// On Windows OS, IsAbs validates if a path is valid based on if stars with a
// unit (eg.: `C:\`) to assert that is absolute, but in this mem implementation
// any path starting by `separator` is also considered absolute.
func isAbs(path string) bool {
return filepath.IsAbs(path) || strings.HasPrefix(path, string(separator))
}
func (fs *Memory) Stat(filename string) (os.FileInfo, error) {
f, has := fs.s.Get(filename)
if !has {
return nil, os.ErrNotExist
}
fi, _ := f.Stat()
var err error
if target, isLink := fs.resolveLink(filename, f); isLink {
fi, err = fs.Stat(target)
if err != nil {
return nil, err
}
}
// the name of the file should always the name of the stated file, so we
// overwrite the Stat returned from the storage with it, since the
// filename may belong to a link.
fi.(*fileInfo).name = filepath.Base(filename)
return fi, nil
}
func (fs *Memory) Lstat(filename string) (os.FileInfo, error) {
f, has := fs.s.Get(filename)
if !has {
return nil, os.ErrNotExist
}
return f.Stat()
}
type ByName []os.FileInfo
func (a ByName) Len() int { return len(a) }
func (a ByName) Less(i, j int) bool { return a[i].Name() < a[j].Name() }
func (a ByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (fs *Memory) ReadDir(path string) ([]os.FileInfo, error) {
if f, has := fs.s.Get(path); has {
if target, isLink := fs.resolveLink(path, f); isLink {
if target != path {
return fs.ReadDir(target)
}
}
} else {
return nil, &os.PathError{Op: "open", Path: path, Err: syscall.ENOENT}
}
var entries []os.FileInfo
for _, f := range fs.s.Children(path) {
fi, _ := f.Stat()
entries = append(entries, fi)
}
sort.Sort(ByName(entries))
return entries, nil
}
func (fs *Memory) MkdirAll(path string, perm os.FileMode) error {
_, err := fs.s.New(path, perm|os.ModeDir, 0)
return err
}
func (fs *Memory) TempFile(dir, prefix string) (billy.File, error) {
return util.TempFile(fs, dir, prefix)
}
func (fs *Memory) getTempFilename(dir, prefix string) string {
fs.tempCount++
filename := fmt.Sprintf("%s_%d_%d", prefix, fs.tempCount, time.Now().UnixNano())
return fs.Join(dir, filename)
}
func (fs *Memory) Rename(from, to string) error {
return fs.s.Rename(from, to)
}
func (fs *Memory) Remove(filename string) error {
return fs.s.Remove(filename)
}
// Falls back to Go's filepath.Join, which works differently depending on the
// OS where the code is being executed.
func (fs *Memory) Join(elem ...string) string {
return filepath.Join(elem...)
}
func (fs *Memory) Symlink(target, link string) error {
_, err := fs.Lstat(link)
if err == nil {
return os.ErrExist
}
if !errors.Is(err, os.ErrNotExist) {
return err
}
return util.WriteFile(fs, link, []byte(target), 0777|os.ModeSymlink)
}
func (fs *Memory) Readlink(link string) (string, error) {
f, has := fs.s.Get(link)
if !has {
return "", os.ErrNotExist
}
if !isSymlink(f.mode) {
return "", &os.PathError{
Op: "readlink",
Path: link,
Err: fmt.Errorf("not a symlink"),
}
}
return string(f.content.bytes), nil
}
// Capabilities implements the Capable interface.
func (fs *Memory) Capabilities() billy.Capability {
return billy.WriteCapability |
billy.ReadCapability |
billy.ReadAndWriteCapability |
billy.SeekCapability |
billy.TruncateCapability
}
type file struct {
name string
content *content
position int64
flag int
mode os.FileMode
isClosed bool
}
func (f *file) Name() string {
return f.name
}
func (f *file) Read(b []byte) (int, error) {
n, err := f.ReadAt(b, f.position)
f.position += int64(n)
if errors.Is(err, io.EOF) && n != 0 {
err = nil
}
return n, err
}
func (f *file) ReadAt(b []byte, off int64) (int, error) {
if f.isClosed {
return 0, os.ErrClosed
}
if !isReadAndWrite(f.flag) && !isReadOnly(f.flag) {
return 0, errors.New("read not supported")
}
n, err := f.content.ReadAt(b, off)
return n, err
}
func (f *file) Seek(offset int64, whence int) (int64, error) {
if f.isClosed {
return 0, os.ErrClosed
}
switch whence {
case io.SeekCurrent:
f.position += offset
case io.SeekStart:
f.position = offset
case io.SeekEnd:
f.position = int64(f.content.Len()) + offset
}
return f.position, nil
}
func (f *file) Write(p []byte) (int, error) {
return f.WriteAt(p, f.position)
}
func (f *file) WriteAt(p []byte, off int64) (int, error) {
if f.isClosed {
return 0, os.ErrClosed
}
if !isReadAndWrite(f.flag) && !isWriteOnly(f.flag) {
return 0, errors.New("write not supported")
}
n, err := f.content.WriteAt(p, off)
f.position = off + int64(n)
return n, err
}
func (f *file) Close() error {
if f.isClosed {
return os.ErrClosed
}
f.isClosed = true
return nil
}
func (f *file) Truncate(size int64) error {
if size < int64(len(f.content.bytes)) {
f.content.bytes = f.content.bytes[:size]
} else if more := int(size) - len(f.content.bytes); more > 0 {
f.content.bytes = append(f.content.bytes, make([]byte, more)...)
}
return nil
}
func (f *file) Duplicate(filename string, mode os.FileMode, flag int) billy.File {
new := &file{
name: filename,
content: f.content,
mode: mode,
flag: flag,
}
if isTruncate(flag) {
new.content.Truncate()
}
if isAppend(flag) {
new.position = int64(new.content.Len())
}
return new
}
func (f *file) Stat() (os.FileInfo, error) {
return &fileInfo{
name: f.Name(),
mode: f.mode,
size: f.content.Len(),
}, nil
}
// Lock is a no-op in memfs.
func (f *file) Lock() error {
return nil
}
// Unlock is a no-op in memfs.
func (f *file) Unlock() error {
return nil
}
type fileInfo struct {
name string
size int
mode os.FileMode
}
func (fi *fileInfo) Name() string {
return fi.name
}
func (fi *fileInfo) Size() int64 {
return int64(fi.size)
}
func (fi *fileInfo) Mode() os.FileMode {
return fi.mode
}
func (*fileInfo) ModTime() time.Time {
return time.Now()
}
func (fi *fileInfo) IsDir() bool {
return fi.mode.IsDir()
}
func (*fileInfo) Sys() interface{} {
return nil
}
func (c *content) Truncate() {
c.bytes = make([]byte, 0)
}
func (c *content) Len() int {
return len(c.bytes)
}
func isCreate(flag int) bool {
return flag&os.O_CREATE != 0
}
func isExclusive(flag int) bool {
return flag&os.O_EXCL != 0
}
func isAppend(flag int) bool {
return flag&os.O_APPEND != 0
}
func isTruncate(flag int) bool {
return flag&os.O_TRUNC != 0
}
func isReadAndWrite(flag int) bool {
return flag&os.O_RDWR != 0
}
func isReadOnly(flag int) bool {
return flag == os.O_RDONLY
}
func isWriteOnly(flag int) bool {
return flag&os.O_WRONLY != 0
}
func isSymlink(m os.FileMode) bool {
return m&os.ModeSymlink != 0
}
package memfs
import (
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"sync"
)
type storage struct {
files map[string]*file
children map[string]map[string]*file
}
func newStorage() *storage {
return &storage{
files: make(map[string]*file, 0),
children: make(map[string]map[string]*file, 0),
}
}
func (s *storage) Has(path string) bool {
path = clean(path)
_, ok := s.files[path]
return ok
}
func (s *storage) New(path string, mode os.FileMode, flag int) (*file, error) {
path = clean(path)
if s.Has(path) {
if !s.MustGet(path).mode.IsDir() {
return nil, fmt.Errorf("file already exists %q", path)
}
return nil, nil
}
name := filepath.Base(path)
f := &file{
name: name,
content: &content{name: name},
mode: mode,
flag: flag,
}
s.files[path] = f
s.createParent(path, mode, f)
return f, nil
}
func (s *storage) createParent(path string, mode os.FileMode, f *file) error {
base := filepath.Dir(path)
base = clean(base)
if f.Name() == string(separator) {
return nil
}
if _, err := s.New(base, mode.Perm()|os.ModeDir, 0); err != nil {
return err
}
if _, ok := s.children[base]; !ok {
s.children[base] = make(map[string]*file, 0)
}
s.children[base][f.Name()] = f
return nil
}
func (s *storage) Children(path string) []*file {
path = clean(path)
l := make([]*file, 0)
for _, f := range s.children[path] {
l = append(l, f)
}
return l
}
func (s *storage) MustGet(path string) *file {
f, ok := s.Get(path)
if !ok {
panic(fmt.Errorf("couldn't find %q", path))
}
return f
}
func (s *storage) Get(path string) (*file, bool) {
path = clean(path)
if !s.Has(path) {
return nil, false
}
file, ok := s.files[path]
return file, ok
}
func (s *storage) Rename(from, to string) error {
from = clean(from)
to = clean(to)
if !s.Has(from) {
return os.ErrNotExist
}
move := [][2]string{{from, to}}
for pathFrom := range s.files {
if pathFrom == from || !strings.HasPrefix(pathFrom, from) {
continue
}
rel, _ := filepath.Rel(from, pathFrom)
pathTo := filepath.Join(to, rel)
move = append(move, [2]string{pathFrom, pathTo})
}
for _, ops := range move {
from := ops[0]
to := ops[1]
if err := s.move(from, to); err != nil {
return err
}
}
return nil
}
func (s *storage) move(from, to string) error {
s.files[to] = s.files[from]
s.files[to].name = filepath.Base(to)
s.children[to] = s.children[from]
defer func() {
delete(s.children, from)
delete(s.files, from)
delete(s.children[filepath.Dir(from)], filepath.Base(from))
}()
return s.createParent(to, 0644, s.files[to])
}
func (s *storage) Remove(path string) error {
path = clean(path)
f, has := s.Get(path)
if !has {
return os.ErrNotExist
}
if f.mode.IsDir() && len(s.children[path]) != 0 {
return fmt.Errorf("dir: %s contains files", path)
}
base, file := filepath.Split(path)
base = filepath.Clean(base)
delete(s.children[base], file)
delete(s.files, path)
return nil
}
func clean(path string) string {
return filepath.Clean(filepath.FromSlash(path))
}
type content struct {
name string
bytes []byte
m sync.RWMutex
}
func (c *content) WriteAt(p []byte, off int64) (int, error) {
if off < 0 {
return 0, &os.PathError{
Op: "writeat",
Path: c.name,
Err: errors.New("negative offset"),
}
}
c.m.Lock()
prev := len(c.bytes)
diff := int(off) - prev
if diff > 0 {
c.bytes = append(c.bytes, make([]byte, diff)...)
}
c.bytes = append(c.bytes[:off], p...)
if len(c.bytes) < prev {
c.bytes = c.bytes[:prev]
}
c.m.Unlock()
return len(p), nil
}
func (c *content) ReadAt(b []byte, off int64) (n int, err error) {
if off < 0 {
return 0, &os.PathError{
Op: "readat",
Path: c.name,
Err: errors.New("negative offset"),
}
}
c.m.RLock()
size := int64(len(c.bytes))
if off >= size {
c.m.RUnlock()
return 0, io.EOF
}
l := int64(len(b))
if off+l > size {
l = size - off
}
btr := c.bytes[off : off+l]
n = copy(b, btr)
if len(btr) < len(b) {
err = io.EOF
}
c.m.RUnlock()
return
}
//go:build !js
// +build !js
// Package osfs provides a billy filesystem for the OS.
package osfs
import (
"fmt"
"io/fs"
"os"
"sync"
"github.com/go-git/go-billy/v5"
)
const (
defaultDirectoryMode = 0o755
defaultCreateMode = 0o666
)
// Default Filesystem representing the root of the os filesystem.
var Default = &ChrootOS{}
// New returns a new OS filesystem.
// By default paths are deduplicated, but still enforced
// under baseDir. For more info refer to WithDeduplicatePath.
func New(baseDir string, opts ...Option) billy.Filesystem {
o := &options{
deduplicatePath: true,
}
for _, opt := range opts {
opt(o)
}
if o.Type == BoundOSFS {
return newBoundOS(baseDir, o.deduplicatePath)
}
return newChrootOS(baseDir)
}
// WithBoundOS returns the option of using a Bound filesystem OS.
func WithBoundOS() Option {
return func(o *options) {
o.Type = BoundOSFS
}
}
// WithChrootOS returns the option of using a Chroot filesystem OS.
func WithChrootOS() Option {
return func(o *options) {
o.Type = ChrootOSFS
}
}
// WithDeduplicatePath toggles the deduplication of the base dir in the path.
// This occurs when absolute links are being used.
// Assuming base dir /base/dir and an absolute symlink /base/dir/target:
//
// With DeduplicatePath (default): /base/dir/target
// Without DeduplicatePath: /base/dir/base/dir/target
//
// This option is only used by the BoundOS OS type.
func WithDeduplicatePath(enabled bool) Option {
return func(o *options) {
o.deduplicatePath = enabled
}
}
type options struct {
Type
deduplicatePath bool
}
type Type int
const (
ChrootOSFS Type = iota
BoundOSFS
)
func readDir(dir string) ([]os.FileInfo, error) {
entries, err := os.ReadDir(dir)
if err != nil {
return nil, err
}
infos := make([]fs.FileInfo, 0, len(entries))
for _, entry := range entries {
fi, err := entry.Info()
if err != nil {
return nil, err
}
infos = append(infos, fi)
}
return infos, nil
}
func tempFile(dir, prefix string) (billy.File, error) {
f, err := os.CreateTemp(dir, prefix)
if err != nil {
return nil, err
}
return &file{File: f}, nil
}
func openFile(fn string, flag int, perm os.FileMode, createDir func(string) error) (billy.File, error) {
if flag&os.O_CREATE != 0 {
if createDir == nil {
return nil, fmt.Errorf("createDir func cannot be nil if file needs to be opened in create mode")
}
if err := createDir(fn); err != nil {
return nil, err
}
}
f, err := os.OpenFile(fn, flag, perm)
if err != nil {
return nil, err
}
return &file{File: f}, err
}
// file is a wrapper for an os.File which adds support for file locking.
type file struct {
*os.File
m sync.Mutex
}
//go:build !js
// +build !js
/*
Copyright 2022 The Flux authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package osfs
import (
"fmt"
"os"
"path/filepath"
"strings"
securejoin "github.com/cyphar/filepath-securejoin"
"github.com/go-git/go-billy/v5"
)
// BoundOS is a fs implementation based on the OS filesystem which is bound to
// a base dir.
// Prefer this fs implementation over ChrootOS.
//
// Behaviours of note:
// 1. Read and write operations can only be directed to files which descends
// from the base dir.
// 2. Symlinks don't have their targets modified, and therefore can point
// to locations outside the base dir or to non-existent paths.
// 3. Readlink and Lstat ensures that the link file is located within the base
// dir, evaluating any symlinks that file or base dir may contain.
type BoundOS struct {
baseDir string
deduplicatePath bool
}
func newBoundOS(d string, deduplicatePath bool) billy.Filesystem {
return &BoundOS{baseDir: d, deduplicatePath: deduplicatePath}
}
func (fs *BoundOS) Create(filename string) (billy.File, error) {
return fs.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, defaultCreateMode)
}
func (fs *BoundOS) OpenFile(filename string, flag int, perm os.FileMode) (billy.File, error) {
fn, err := fs.abs(filename)
if err != nil {
return nil, err
}
return openFile(fn, flag, perm, fs.createDir)
}
func (fs *BoundOS) ReadDir(path string) ([]os.FileInfo, error) {
dir, err := fs.abs(path)
if err != nil {
return nil, err
}
return readDir(dir)
}
func (fs *BoundOS) Rename(from, to string) error {
f, err := fs.abs(from)
if err != nil {
return err
}
t, err := fs.abs(to)
if err != nil {
return err
}
// MkdirAll for target name.
if err := fs.createDir(t); err != nil {
return err
}
return os.Rename(f, t)
}
func (fs *BoundOS) MkdirAll(path string, perm os.FileMode) error {
dir, err := fs.abs(path)
if err != nil {
return err
}
return os.MkdirAll(dir, perm)
}
func (fs *BoundOS) Open(filename string) (billy.File, error) {
return fs.OpenFile(filename, os.O_RDONLY, 0)
}
func (fs *BoundOS) Stat(filename string) (os.FileInfo, error) {
filename, err := fs.abs(filename)
if err != nil {
return nil, err
}
return os.Stat(filename)
}
func (fs *BoundOS) Remove(filename string) error {
fn, err := fs.abs(filename)
if err != nil {
return err
}
return os.Remove(fn)
}
// TempFile creates a temporary file. If dir is empty, the file
// will be created within the OS Temporary dir. If dir is provided
// it must descend from the current base dir.
func (fs *BoundOS) TempFile(dir, prefix string) (billy.File, error) {
if dir != "" {
var err error
dir, err = fs.abs(dir)
if err != nil {
return nil, err
}
}
return tempFile(dir, prefix)
}
func (fs *BoundOS) Join(elem ...string) string {
return filepath.Join(elem...)
}
func (fs *BoundOS) RemoveAll(path string) error {
dir, err := fs.abs(path)
if err != nil {
return err
}
return os.RemoveAll(dir)
}
func (fs *BoundOS) Symlink(target, link string) error {
ln, err := fs.abs(link)
if err != nil {
return err
}
// MkdirAll for containing dir.
if err := fs.createDir(ln); err != nil {
return err
}
return os.Symlink(target, ln)
}
func (fs *BoundOS) Lstat(filename string) (os.FileInfo, error) {
filename = filepath.Clean(filename)
if !filepath.IsAbs(filename) {
filename = filepath.Join(fs.baseDir, filename)
}
if ok, err := fs.insideBaseDirEval(filename); !ok {
return nil, err
}
return os.Lstat(filename)
}
func (fs *BoundOS) Readlink(link string) (string, error) {
if !filepath.IsAbs(link) {
link = filepath.Clean(filepath.Join(fs.baseDir, link))
}
if ok, err := fs.insideBaseDirEval(link); !ok {
return "", err
}
return os.Readlink(link)
}
// Chroot returns a new OS filesystem, with the base dir set to the
// result of joining the provided path with the underlying base dir.
func (fs *BoundOS) Chroot(path string) (billy.Filesystem, error) {
joined, err := securejoin.SecureJoin(fs.baseDir, path)
if err != nil {
return nil, err
}
return New(joined), nil
}
// Root returns the current base dir of the billy.Filesystem.
// This is required in order for this implementation to be a drop-in
// replacement for other upstream implementations (e.g. memory and osfs).
func (fs *BoundOS) Root() string {
return fs.baseDir
}
func (fs *BoundOS) createDir(fullpath string) error {
dir := filepath.Dir(fullpath)
if dir != "." {
if err := os.MkdirAll(dir, defaultDirectoryMode); err != nil {
return err
}
}
return nil
}
// abs transforms filename to an absolute path, taking into account the base dir.
// Relative paths won't be allowed to ascend the base dir, so `../file` will become
// `/working-dir/file`.
//
// Note that if filename is a symlink, the returned address will be the target of the
// symlink.
func (fs *BoundOS) abs(filename string) (string, error) {
if filename == fs.baseDir {
filename = string(filepath.Separator)
}
path, err := securejoin.SecureJoin(fs.baseDir, filename)
if err != nil {
return "", nil
}
if fs.deduplicatePath {
vol := filepath.VolumeName(fs.baseDir)
dup := filepath.Join(fs.baseDir, fs.baseDir[len(vol):])
if strings.HasPrefix(path, dup+string(filepath.Separator)) {
return fs.abs(path[len(dup):])
}
}
return path, nil
}
// insideBaseDir checks whether filename is located within
// the fs.baseDir.
func (fs *BoundOS) insideBaseDir(filename string) (bool, error) {
if filename == fs.baseDir {
return true, nil
}
if !strings.HasPrefix(filename, fs.baseDir+string(filepath.Separator)) {
return false, fmt.Errorf("path outside base dir")
}
return true, nil
}
// insideBaseDirEval checks whether filename is contained within
// a dir that is within the fs.baseDir, by first evaluating any symlinks
// that either filename or fs.baseDir may contain.
func (fs *BoundOS) insideBaseDirEval(filename string) (bool, error) {
// "/" contains all others.
if fs.baseDir == "/" {
return true, nil
}
dir, err := filepath.EvalSymlinks(filepath.Dir(filename))
if dir == "" || os.IsNotExist(err) {
dir = filepath.Dir(filename)
}
wd, err := filepath.EvalSymlinks(fs.baseDir)
if wd == "" || os.IsNotExist(err) {
wd = fs.baseDir
}
if filename != wd && dir != wd && !strings.HasPrefix(dir, wd+string(filepath.Separator)) {
return false, fmt.Errorf("%q: path outside base dir %q: %w", filename, fs.baseDir, os.ErrNotExist)
}
return true, nil
}
//go:build !js
// +build !js
package osfs
import (
"os"
"path/filepath"
"github.com/go-git/go-billy/v5"
"github.com/go-git/go-billy/v5/helper/chroot"
)
// ChrootOS is a legacy filesystem based on a "soft chroot" of the os filesystem.
// Although this is still the default os filesystem, consider using BoundOS instead.
//
// Behaviours of note:
// 1. A "soft chroot" translates the base dir to "/" for the purposes of the
// fs abstraction.
// 2. Symlinks targets may be modified to be kept within the chroot bounds.
// 3. Some file modes does not pass-through the fs abstraction.
// 4. The combination of 1 and 2 may cause go-git to think that a Git repository
// is dirty, when in fact it isn't.
type ChrootOS struct{}
func newChrootOS(baseDir string) billy.Filesystem {
return chroot.New(&ChrootOS{}, baseDir)
}
func (fs *ChrootOS) Create(filename string) (billy.File, error) {
return fs.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, defaultCreateMode)
}
func (fs *ChrootOS) OpenFile(filename string, flag int, perm os.FileMode) (billy.File, error) {
return openFile(filename, flag, perm, fs.createDir)
}
func (fs *ChrootOS) createDir(fullpath string) error {
dir := filepath.Dir(fullpath)
if dir != "." {
if err := os.MkdirAll(dir, defaultDirectoryMode); err != nil {
return err
}
}
return nil
}
func (fs *ChrootOS) ReadDir(dir string) ([]os.FileInfo, error) {
return readDir(dir)
}
func (fs *ChrootOS) Rename(from, to string) error {
if err := fs.createDir(to); err != nil {
return err
}
return rename(from, to)
}
func (fs *ChrootOS) MkdirAll(path string, perm os.FileMode) error {
return os.MkdirAll(path, defaultDirectoryMode)
}
func (fs *ChrootOS) Open(filename string) (billy.File, error) {
return fs.OpenFile(filename, os.O_RDONLY, 0)
}
func (fs *ChrootOS) Stat(filename string) (os.FileInfo, error) {
return os.Stat(filename)
}
func (fs *ChrootOS) Remove(filename string) error {
return os.Remove(filename)
}
func (fs *ChrootOS) TempFile(dir, prefix string) (billy.File, error) {
if err := fs.createDir(dir + string(os.PathSeparator)); err != nil {
return nil, err
}
return tempFile(dir, prefix)
}
func (fs *ChrootOS) Join(elem ...string) string {
return filepath.Join(elem...)
}
func (fs *ChrootOS) RemoveAll(path string) error {
return os.RemoveAll(filepath.Clean(path))
}
func (fs *ChrootOS) Lstat(filename string) (os.FileInfo, error) {
return os.Lstat(filepath.Clean(filename))
}
func (fs *ChrootOS) Symlink(target, link string) error {
if err := fs.createDir(link); err != nil {
return err
}
return os.Symlink(target, link)
}
func (fs *ChrootOS) Readlink(link string) (string, error) {
return os.Readlink(link)
}
// Capabilities implements the Capable interface.
func (fs *ChrootOS) Capabilities() billy.Capability {
return billy.DefaultCapabilities
}
//go:build js
// +build js
package osfs
import (
"github.com/go-git/go-billy/v5"
"github.com/go-git/go-billy/v5/helper/chroot"
"github.com/go-git/go-billy/v5/memfs"
)
// globalMemFs is the global memory fs
var globalMemFs = memfs.New()
// Default Filesystem representing the root of in-memory filesystem for a
// js/wasm environment.
var Default = memfs.New()
// New returns a new OS filesystem.
func New(baseDir string, _ ...Option) billy.Filesystem {
return chroot.New(Default, Default.Join("/", baseDir))
}
type options struct {
}
package osfs
type Option func(*options)