package fsnoder
import (
"bytes"
"fmt"
"io"
"github.com/go-git/go-git/v5/utils/merkletrie/noder"
)
// New function creates a full merkle trie from the string description of
// a filesystem tree. See examples of the string format in the package
// description.
func New(s string) (noder.Noder, error) {
return decodeDir([]byte(s), root)
}
const (
root = true
nonRoot = false
)
// Expected data: a fsnoder description, for example: A(foo bar qux ...).
// When isRoot is true, unnamed dirs are supported, for example: (foo
// bar qux ...)
func decodeDir(data []byte, isRoot bool) (*dir, error) {
data = bytes.TrimSpace(data)
if len(data) == 0 {
return nil, io.EOF
}
// get the name of the dir and remove it from the data. In case the
// there is no name and isRoot is true, just use "" as the name.
var name string
switch end := bytes.IndexRune(data, dirStartMark); end {
case -1:
return nil, fmt.Errorf("%c not found", dirStartMark)
case 0:
if isRoot {
name = ""
} else {
return nil, fmt.Errorf("inner unnamed dirs not allowed: %s", data)
}
default:
name = string(data[0:end])
data = data[end:]
}
// check data ends with the dirEndMark
if data[len(data)-1] != dirEndMark {
return nil, fmt.Errorf("malformed data: last %q not found",
dirEndMark)
}
data = data[1 : len(data)-1] // remove initial '(' and last ')'
children, err := decodeChildren(data)
if err != nil {
return nil, err
}
return newDir(name, children)
}
func isNumber(b rune) bool {
return '0' <= b && b <= '9'
}
func isLetter(b rune) bool {
return ('a' <= b && b <= 'z') || ('A' <= b && b <= 'Z')
}
func decodeChildren(data []byte) ([]noder.Noder, error) {
data = bytes.TrimSpace(data)
if len(data) == 0 {
return nil, nil
}
chunks := split(data)
ret := make([]noder.Noder, len(chunks))
var err error
for i, c := range chunks {
ret[i], err = decodeChild(c)
if err != nil {
return nil, fmt.Errorf("malformed element %d (%s): %s", i, c, err)
}
}
return ret, nil
}
// returns the description of the elements of a dir. It is just looking
// for spaces if they are not part of inner dirs.
func split(data []byte) [][]byte {
chunks := [][]byte{}
start := 0
dirDepth := 0
for i, b := range data {
switch b {
case dirStartMark:
dirDepth++
case dirEndMark:
dirDepth--
case dirElementSep:
if dirDepth == 0 {
chunks = append(chunks, data[start:i+1])
start = i + 1
}
}
}
chunks = append(chunks, data[start:])
return chunks
}
// A child can be a file or a dir.
func decodeChild(data []byte) (noder.Noder, error) {
clean := bytes.TrimSpace(data)
if len(data) < 3 {
return nil, fmt.Errorf("element too short: %s", clean)
}
fileNameEnd := bytes.IndexRune(data, fileStartMark)
dirNameEnd := bytes.IndexRune(data, dirStartMark)
switch {
case fileNameEnd == -1 && dirNameEnd == -1:
return nil, fmt.Errorf(
"malformed child, no file or dir start mark found")
case fileNameEnd == -1:
return decodeDir(clean, nonRoot)
case dirNameEnd == -1:
return decodeFile(clean)
case dirNameEnd < fileNameEnd:
return decodeDir(clean, nonRoot)
case dirNameEnd > fileNameEnd:
return decodeFile(clean)
}
return nil, fmt.Errorf("unreachable")
}
func decodeFile(data []byte) (noder.Noder, error) {
nameEnd := bytes.IndexRune(data, fileStartMark)
if nameEnd == -1 {
return nil, fmt.Errorf("malformed file, no %c found", fileStartMark)
}
contentStart := nameEnd + 1
contentEnd := bytes.IndexRune(data, fileEndMark)
if contentEnd == -1 {
return nil, fmt.Errorf("malformed file, no %c found", fileEndMark)
}
switch {
case nameEnd > contentEnd:
return nil, fmt.Errorf("malformed file, found %c before %c",
fileEndMark, fileStartMark)
case contentStart == contentEnd:
name := string(data[:nameEnd])
if !validFileName(name) {
return nil, fmt.Errorf("invalid file name")
}
return newFile(name, "")
default:
name := string(data[:nameEnd])
if !validFileName(name) {
return nil, fmt.Errorf("invalid file name")
}
contents := string(data[contentStart:contentEnd])
if !validFileContents(contents) {
return nil, fmt.Errorf("invalid file contents")
}
return newFile(name, contents)
}
}
func validFileName(s string) bool {
for _, c := range s {
if !isLetter(c) && c != '.' {
return false
}
}
return true
}
func validFileContents(s string) bool {
for _, c := range s {
if !isNumber(c) {
return false
}
}
return true
}
// HashEqual returns if a and b have the same hash.
func HashEqual(a, b noder.Hasher) bool {
return bytes.Equal(a.Hash(), b.Hash())
}