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). func Parse(body []byte, objectFormat id.ObjectFormat) (*Tree, error) { tree := new(Tree) idSize := objectFormat.Size() seen := make(map[string]struct{}) 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 := string(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 _, dup := seen[entry.Name]; dup { return nil, fmt.Errorf("%w: duplicate entry name %q", ErrInvalidTree, entry.Name) } seen[entry.Name] = struct{}{} tree.entries = append(tree.entries, entry) } return tree, nil }