aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Runxi Yu2026-06-07 11:01:26 +0000
committerGravatar Runxi Yu2026-06-07 11:04:43 +0000
commitc2d883c7c58a3bd64d799dd000fe7254e450a4e5 (patch)
treea4456d8fc3718f235c2e934d42c056b3bcd3e1f9
parentobject/tag: Add (diff)
signatureNo signature
object/tree/mode: Initialize
-rw-r--r--object/tree/mode/append.go8
-rw-r--r--object/tree/mode/details.go48
-rw-r--r--object/tree/mode/doc.go6
-rw-r--r--object/tree/mode/mode.go24
-rw-r--r--object/tree/mode/ops.go38
-rw-r--r--object/tree/mode/parse.go43
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
+}