aboutsummaryrefslogblamecommitdiffstats
path: root/utils/merkletrie/internal/fsnoder/new.go
blob: 3ea4733fb302b4f91d38f14fa86afaa083f7309a (plain) (tree)































































































































































































                                                                                          
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())
}