diff options
| -rw-r--r-- | object/tree/mode/append.go | 8 | ||||
| -rw-r--r-- | object/tree/mode/details.go | 48 | ||||
| -rw-r--r-- | object/tree/mode/doc.go | 6 | ||||
| -rw-r--r-- | object/tree/mode/mode.go | 24 | ||||
| -rw-r--r-- | object/tree/mode/ops.go | 38 | ||||
| -rw-r--r-- | object/tree/mode/parse.go | 43 |
6 files changed, 167 insertions, 0 deletions
diff --git a/object/tree/mode/append.go b/object/tree/mode/append.go new file mode 100644 index 00000000..e8f118ee --- /dev/null +++ b/object/tree/mode/append.go @@ -0,0 +1,8 @@ +package mode + +import "strconv" + +// Append appends the canonical octal encoding of the mode to dst. +func (mode Mode) Append(dst []byte) []byte { + return strconv.AppendUint(dst, uint64(mode), 8) +} diff --git a/object/tree/mode/details.go b/object/tree/mode/details.go new file mode 100644 index 00000000..90ead2ed --- /dev/null +++ b/object/tree/mode/details.go @@ -0,0 +1,48 @@ +package mode + +import "lindenii.org/go/furgit/object/typ" + +type modeDetails struct { + valid bool + isBlobLike bool + isRegularFile bool + objectType typ.Type +} + +func (mode Mode) details() modeDetails { + return modeTable[mode] +} + +//nolint:gochecknoglobals +var modeTable = map[Mode]modeDetails{ + Directory: { + valid: true, + isBlobLike: false, + isRegularFile: false, + objectType: typ.TypeTree, + }, + Regular: { + valid: true, + isBlobLike: true, + isRegularFile: true, + objectType: typ.TypeBlob, + }, + Executable: { + valid: true, + isBlobLike: true, + isRegularFile: true, + objectType: typ.TypeBlob, + }, + Symlink: { + valid: true, + isBlobLike: true, + isRegularFile: false, + objectType: typ.TypeBlob, + }, + Gitlink: { + valid: true, + isBlobLike: false, + isRegularFile: false, + objectType: typ.TypeCommit, + }, +} diff --git a/object/tree/mode/doc.go b/object/tree/mode/doc.go new file mode 100644 index 00000000..8e3f5984 --- /dev/null +++ b/object/tree/mode/doc.go @@ -0,0 +1,6 @@ +// Package mode provides canonical Git tree entry modes. +// +// A [Mode] is the octal file mode stored in a tree entry, +// such as a regular file, executable, symlink, directory, or gitlink. +// It rejects malformed, unsupported, and zero-padded encodings. +package mode diff --git a/object/tree/mode/mode.go b/object/tree/mode/mode.go new file mode 100644 index 00000000..70b6fd38 --- /dev/null +++ b/object/tree/mode/mode.go @@ -0,0 +1,24 @@ +package mode + +// Mode represents the mode of an entry in a Git tree. +type Mode uint32 + +const ( + // Directory is the mode of a subtree entry. + Directory Mode = 0o40000 + + // Regular is the mode of a non-executable regular file. + Regular Mode = 0o100644 + + // Executable is the mode of an executable regular file. + Executable Mode = 0o100755 + + // Symlink is the mode of a symbolic link. + Symlink Mode = 0o120000 + + // Gitlink is the mode of a gitlink (submodule commit) entry. + Gitlink Mode = 0o160000 +) + +// maxModeDigits is the largest number of octal digits in any canonical mode. +const maxModeDigits = 6 diff --git a/object/tree/mode/ops.go b/object/tree/mode/ops.go new file mode 100644 index 00000000..149d95db --- /dev/null +++ b/object/tree/mode/ops.go @@ -0,0 +1,38 @@ +package mode + +import "lindenii.org/go/furgit/object/typ" + +// IsValid reports whether mode is a canonical mode that Git writes. +func (mode Mode) IsValid() bool { + return mode.details().valid +} + +// IsBlobLike reports whether mode names one blob-like tree entry kind. +// +// Blob-like entries store blob object IDs as their targets. +func (mode Mode) IsBlobLike() bool { + return mode.details().isBlobLike +} + +// IsRegularFile reports whether mode names one regular-file variant. +func (mode Mode) IsRegularFile() bool { + return mode.details().isRegularFile +} + +// HasSameType reports whether mode and other describe the same tree entry kind. +// +// Regular files and executable files have the same type for diff-status purposes. +func (mode Mode) HasSameType(other Mode) bool { + if mode == other { + return true + } + + return mode.details().isRegularFile && other.details().isRegularFile +} + +// ObjectType returns the type of object that an entry with this mode targets. +// +// It returns [typ.TypeUnknown] for invalid modes. +func (mode Mode) ObjectType() typ.Type { + return mode.details().objectType +} diff --git a/object/tree/mode/parse.go b/object/tree/mode/parse.go new file mode 100644 index 00000000..7839a89a --- /dev/null +++ b/object/tree/mode/parse.go @@ -0,0 +1,43 @@ +package mode + +import ( + "errors" + "fmt" +) + +// ErrInvalidMode indicates a malformed or unsupported tree entry mode. +var ErrInvalidMode = errors.New("object/tree/mode: invalid mode") + +// Parse decodes a canonical octal tree entry mode. +// +// It accepts only the modes Git itself writes +// and rejects malformed, unsupported, and zero-padded encodings. +func Parse(raw []byte) (Mode, error) { + if len(raw) == 0 { + return 0, fmt.Errorf("%w: empty mode", ErrInvalidMode) + } + + if raw[0] == '0' { + return 0, fmt.Errorf("%w: zero-padded mode %q", ErrInvalidMode, raw) + } + + if len(raw) > maxModeDigits { + return 0, fmt.Errorf("%w: mode %q too long", ErrInvalidMode, raw) + } + + var mode Mode + + for _, c := range raw { + if c < '0' || c > '7' { + return 0, fmt.Errorf("%w: non-octal byte in mode %q", ErrInvalidMode, raw) + } + + mode = mode<<3 | Mode(c-'0') + } + + if !mode.IsValid() { + return 0, fmt.Errorf("%w: unsupported mode %q", ErrInvalidMode, raw) + } + + return mode, nil +} |
