package fsnoder import ( "bytes" "fmt" "io" "gopkg.in/src-d/go-git.v4/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 (a single letter) 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 if data[0] == dirStartMark { if isRoot { name = "" } else { return nil, fmt.Errorf("inner unnamed dirs not allowed: %s", data) } } else { name = string(data[0]) data = data[1:] } // check that data is enclosed in parents and it is big enough and // remove them. if len(data) < 2 { return nil, fmt.Errorf("malformed data: too short") } if data[0] != dirStartMark { return nil, fmt.Errorf("malformed data: first %q not found", dirStartMark) } 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 byte) bool { return '0' <= b && b <= '9' } func isLetter(b byte) 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) } switch clean[1] { case fileStartMark: return decodeFile(clean) case dirStartMark: return decodeDir(clean, nonRoot) default: if clean[0] == dirStartMark { return nil, fmt.Errorf("non-root unnamed dir are not allowed: %s", clean) } return nil, fmt.Errorf("malformed dir element: %s", clean) } } func decodeFile(data []byte) (noder.Noder, error) { if len(data) == 3 { return decodeEmptyFile(data) } if len(data) != 4 { return nil, fmt.Errorf("length is not 4") } if !isLetter(data[0]) { return nil, fmt.Errorf("name must be a letter") } if data[1] != '<' { return nil, fmt.Errorf("wrong file start character") } if !isNumber(data[2]) { return nil, fmt.Errorf("contents must be a number") } if data[3] != '>' { return nil, fmt.Errorf("wrong file end character") } name := string(data[0]) contents := string(data[2]) return newFile(name, contents) } func decodeEmptyFile(data []byte) (noder.Noder, error) { if len(data) != 3 { return nil, fmt.Errorf("length is not 3: %s", data) } if !isLetter(data[0]) { return nil, fmt.Errorf("name must be a letter: %s", data) } if data[1] != '<' { return nil, fmt.Errorf("wrong file start character: %s", data) } if data[2] != '>' { return nil, fmt.Errorf("wrong file end character: %s", data) } name := string(data[0]) return newFile(name, "") } // HashEqual returns if a and b have the same hash. func HashEqual(a, b noder.Hasher) bool { return bytes.Equal(a.Hash(), b.Hash()) }