package tree
import (
"bytes"
"fmt"
"lindenii.org/go/furgit/object/id"
"lindenii.org/go/furgit/object/tree/mode"
)
// Parse decodes a tree object body into a fully materialized Tree.
//
// It enforces canonical modes, nonempty slash-free names,
// correctly sized object IDs, and strictly increasing Git tree order.
// It does not enforce fsck-level name policy
// (for example ".", "..", ".git", or platform-specific aliases).
//
// The returned tree aliases body:
// each entry's Name shares body's backing array.
// The tree inherits body's lifetime
// and must not be mutated unless body may be.
// Use [Tree.Clone] for an independent copy.
//
// Labels: Life-Parent, Mut-No.
func Parse(body []byte, objectFormat id.ObjectFormat) (*Tree, error) {
tree := new(Tree)
idSize := objectFormat.Size()
const minEntryOverhead = 5 + 1 + 1 + 1 // mode, space, name, NUL
if estimate := len(body) / (minEntryOverhead + idSize); estimate > 0 {
tree.entries = make([]Entry, 0, estimate)
}
i := 0
for i < len(body) {
space := bytes.IndexByte(body[i:], ' ')
if space < 0 {
return nil, fmt.Errorf("%w: missing mode terminator at offset %d", ErrInvalidTree, i)
}
entryMode, err := mode.Parse(body[i : i+space])
if err != nil {
return nil, fmt.Errorf("%w: %w", ErrInvalidTree, err)
}
i += space + 1
nul := bytes.IndexByte(body[i:], 0)
if nul < 0 {
return nil, fmt.Errorf("%w: missing name terminator at offset %d", ErrInvalidTree, i)
}
name := body[i : i+nul]
i += nul + 1
err = validateName(name)
if err != nil {
return nil, err
}
idEnd := i + idSize
if idEnd > len(body) {
return nil, fmt.Errorf("%w: truncated object id at offset %d", ErrInvalidTree, i)
}
oid, err := objectFormat.FromBytes(body[i:idEnd])
if err != nil {
return nil, fmt.Errorf("%w: object id at offset %d: %w", ErrInvalidTree, i, err)
}
i = idEnd
entry := Entry{Mode: entryMode, Name: name, ID: oid}
if len(tree.entries) > 0 {
prev := tree.entries[len(tree.entries)-1]
if nameCompare(prev.Name, prev.Mode == mode.Directory, entry.Name, entryMode == mode.Directory) >= 0 {
return nil, fmt.Errorf("%w: entry %q out of order or duplicated", ErrInvalidTree, entry.Name)
}
}
if entryMode == mode.Directory && hasNonDirNamed(tree.entries, entry.Name) {
return nil, fmt.Errorf("%w: duplicate entry name %q", ErrInvalidTree, entry.Name)
}
tree.entries = append(tree.entries, entry)
}
return tree, nil
}
// hasNonDirNamed reports whether entries, sorted in Git tree order,
// holds a non-directory entry whose name equals name.
//
// The match sorts immediately below a directory of the same name,
// so the search gallops from the back before binary searching the bracket.
func hasNonDirNamed(entries []Entry, name []byte) bool {
lo, hi := 0, len(entries)
for stride := 1; stride < hi-lo; stride *= 2 {
mid := hi - stride
if nameCompare(entries[mid].Name, entries[mid].Mode == mode.Directory, name, false) < 0 {
lo = mid + 1
break
}
hi = mid
}
for lo < hi {
mid := lo + (hi-lo)/2
if nameCompare(entries[mid].Name, entries[mid].Mode == mode.Directory, name, false) < 0 {
lo = mid + 1
} else {
hi = mid
}
}
return lo < len(entries) &&
nameCompare(entries[lo].Name, entries[lo].Mode == mode.Directory, name, false) == 0
}