7VEHGTEYCNDRXUT7BV5BVNO6SKE3QRYC324Y4DMUPYVVVEOXMETAC
package pijul
import (
"time"
"github.com/BurntSushi/toml"
)
type Change struct {
Hash string
Header ChangeHeader
Dependencies []string
ExtraKnown []string
}
type ChangeHeader struct {
Message string `toml:"message"`
Description string `toml:"description"`
Timestamp time.Time `toml:"timestamp"`
Authors []map[string]string `toml:"authors"`
}
var ch ChangeHeader
err := toml.Unmarshal(b, &ch)
return ch, err
}
func parseChangeHeader(input []byte) (rest []byte, value ChangeHeader, err error) {
return mapWithError(
alt(takeUntil("# Dependencies"), takeUntil("# Hunks")),
decodeChangeHeader,
)(input)
}
func ParseChange(input []byte) (Change, error) {
var c Change
rest, header, err := parseChangeHeader(input)
if err != nil {
return Change{}, err
}
c.Header = header
rest, dependencies, err := parseDependencies(rest)
if err != nil {
return Change{}, err
}
deps := map[int]string{}
for _, dep := range dependencies {
switch dep.typ {
case numbered:
c.Dependencies = append(c.Dependencies, dep.hash)
deps[dep.number] = dep.hash
case numberedPlus:
deps[dep.number] = dep.hash
case extraKnown:
c.ExtraKnown = append(c.ExtraKnown, dep.hash)
}
}
// TODO: hunks
return c, nil
}
type depType int
const (
numbered depType = iota
numberedPlus
extraKnown
extraUnknown
)
type printableDependency struct {
typ depType
number int
hash string
}
func parseDependency(input []byte) ([]byte, printableDependency, error) {
rest, n, err := delimited(
tag("["),
alt(
positiveInt,
value(-1, tag("*")),
// I don't think the syntax for ExtraUnknown will ever match, so I'll skip it.
),
tag("]"),
)(input)
if err != nil {
return rest, printableDependency{}, err
}
rest, plus, err := terminated(
alt(tag("+"), tag(" ")),
space0,
)(rest)
if err != nil {
return rest, printableDependency{}, err
}
var dep printableDependency
if n == -1 {
dep.typ = extraKnown
} else {
dep.number = n
if plus == "+" {
dep.typ = numberedPlus
} else {
dep.typ = numbered
}
}
rest, hash, err := delimited(
space0,
takeWhile(func(c byte) bool {
return '0' <= c && c <= '9' || 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z'
}),
recognize3(space0, opt(parseComment), lineEnding),
)(rest)
if err != nil {
return rest, printableDependency{}, err
}
dep.hash = string(hash)
return rest, dep, nil
}
func parseComment(input []byte) ([]byte, []byte, error) {
return preceded(
tag("#"),
takeUntil("\n"),
)(input)
}
func parseDependencies(input []byte) ([]byte, []printableDependency, error) {
return alt(
preceded(
recognize4(tag("# Dependencies"), space0, lineEnding, multispace0),
many0(terminated(parseDependency, multispace0)),
),
value([]printableDependency{}, multispace0),
)(input)
}
func decodeChangeHeader(b []byte) (ChangeHeader, error) {
github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/klauspost/compress v1.16.3 h1:XuJt9zzcnaz6a16/OU53ZjWp/v7/42WcR5t2a0PcNQY=
github.com/klauspost/compress v1.16.3/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug=
golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
}
}
// uint64LE parses a little-endian 64-bit unsigned integer.
func uint64LE(input []byte) ([]byte, uint64, error) {
if len(input) < 8 {
return input, 0, fmt.Errorf("need 8 bytes to parse a 64-bit integer; only got %d", len(input))
}
return input[8:], binary.LittleEndian.Uint64(input), nil
}
func lengthData[T constraints.Integer](p parser[T]) parser[[]byte] {
return func(data []byte) ([]byte, []byte, error) {
data, length, err := p(data)
if err != nil {
return data, nil, err
}
if int(length) > len(data) {
return data, nil, fmt.Errorf("need %d bytes, only have %d", length, len(data))
}
return data[length:], data[:length], nil
func toString(p parser[[]byte]) parser[string] {
return mapValue(p, func(b []byte) string {
return string(b)
})
}
// option parses a serialized Rust Option<T>, with the first byte being 0 for
// None and 1 for Some.
func option[T any](p parser[T]) parser[*T] {
return func(data []byte) ([]byte, *T, error) {
if len(data) == 0 {
return data, nil, io.ErrUnexpectedEOF
}
switch data[0] {
case 0:
return data[1:], nil, nil
case 1:
data, v, err := p(data[1:])
if err != nil {
return data, nil, err
}
return data, &v, nil
default:
return data, nil, fmt.Errorf("want 0 or 1, got 0x%02x", data[0])
}
}
}
// vec parses a serialized Rust Vec<T>, starting with a 64-bit length.
func vec[T any](p parser[T]) parser[[]T] {
return func(data []byte) ([]byte, []T, error) {
data, length, err := uint64LE(data)
if err != nil {
return data, nil, err
}
vector := make([]T, length)
for i := range vector {
var v T
data, v, err = p(data)
if err != nil {
return data, nil, err
}
vector[i] = v
}
return data, vector, nil
}
}
// hashMap parses a serialized Rust HashMap<K,V>, starting with a 64-bit
// length.
func hashMap[K comparable, V any](pk parser[K], pv parser[V]) parser[map[K]V] {
return func(data []byte) ([]byte, map[K]V, error) {
data, length, err := uint64LE(data)
if err != nil {
return data, nil, err
}
m := make(map[K]V, length)
for i := 0; i < int(length); i++ {
var key K
var val V
data, key, err = pk(data)
if err != nil {
return data, nil, err
}
data, val, err = pv(data)
if err != nil {
return data, nil, err
}
m[key] = val
}
return data, m, nil
}
}
var changeParsingTests = []struct {
raw string
parsed Change
}{
{
`message = ''
timestamp = '2023-03-24T17:52:08.476452868Z'
authors = []
# Hunks
1. Root add
up 1.0, new 0:0
`,
Change{
Header: ChangeHeader{
Timestamp: time.Date(2023, 3, 24, 17, 52, 8, 476452868, time.UTC),
Authors: []map[string]string{},
},
//go:embed testdata/CB7A3PP3XC6JY3QYUUNB4WCXGNFJMHTTH54MBAWBUVQL7TTHWBBQC.change
var CB7A3P []byte
var CB7A3P_golden = Change{
Version: 6,
Message: "Say hello",
Description: "a trivial change",
Timestamp: time.Date(2023, 3, 27, 18, 51, 58, 26097601, time.UTC),
Authors: []map[string]string{
{
"key": "BCEXYuKWaQ96btsk8UyBZWHLjn1Brhykv8tuZGPRjzFn",
{
`message = 'Add some gibberish'
timestamp = '2023-03-24T17:52:08.476298107Z'
[[authors]]
key = 'BCEXYuKWaQ96btsk8UyBZWHLjn1Brhykv8tuZGPRjzFn'
# Dependencies
[2] AYY5CBLPBVTCHWSC7HDSZDL7KCUFJNX3NPNN6Q7ITTWF232IS4PQC #
# Hunks
1. File addition: "gibberish" in "" "binary"
up 2.1, new 1618:1638
+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
`,
Change{
Header: ChangeHeader{
Message: "Add some gibberish",
Timestamp: time.Date(2023, 3, 24, 17, 52, 8, 476298107, time.UTC),
Authors: []map[string]string{
{
"key": "BCEXYuKWaQ96btsk8UyBZWHLjn1Brhykv8tuZGPRjzFn",
},
},
},
Dependencies: []string{"AYY5CBLPBVTCHWSC7HDSZDL7KCUFJNX3NPNN6Q7ITTWF232IS4PQC"},
},
},
func TestChangeHeaderParsing(t *testing.T) {
for i, c := range changeParsingTests {
parsed, err := ParseChange([]byte(c.raw))
if err != nil {
t.Fatalf("Error parsing case %d: %v", i, err)
}
if !reflect.DeepEqual(parsed, c.parsed) {
t.Fatalf("got %#v, want %#v", parsed, c.parsed)
}
func TestDeserializeChange(t *testing.T) {
c, err := DeserializeChange(CB7A3P)
if err != nil {
t.Fatalf("deserialization error: %v", err)
}
if !reflect.DeepEqual(c, CB7A3P_golden) {
t.Fatalf("got %#v, want %#v", c, CB7A3P_golden)
package pijul
import (
"bytes"
"encoding/binary"
"fmt"
"io"
"time"
"github.com/klauspost/compress/zstd"
)
type Change struct {
Version uint64
Message string
Description string
Timestamp time.Time
Authors []map[string]string
}
type offsets struct {
Version uint64
HashedLen uint64
UnhashedOffset uint64
UnhashedLen uint64
ContentsOffset uint64
ContentsLen uint64
Total uint64
}
func DeserializeChange(data []byte) (Change, error) {
br := bytes.NewReader(data)
var off offsets
err := binary.Read(br, binary.LittleEndian, &off)
if err != nil {
return Change{}, fmt.Errorf("error reading 'offsets' header: %w", err)
}
zr, err := zstd.NewReader(br)
if err != nil {
return Change{}, fmt.Errorf("error creating zstandard decompressor: %w", err)
}
defer zr.Close()
hashed, err := io.ReadAll(zr)
if err != nil {
return Change{}, fmt.Errorf("error getting the 'hashed' data chunk from the change: %w", err)
}
var c Change
err = c.parseHashedData(hashed)
if err != nil {
return Change{}, err
}
// TODO: unhashed and contents
return c, nil
}
// parseHashedData parses the portion of the change's data that is stored in the
// Hashed struct in libpijul.
func (c *Change) parseHashedData(data []byte) error {
var err error
data, c.Version, err = uint64LE(data)
if err != nil {
return err
}
data, c.Message, err = toString(lengthData(uint64LE))(data)
if err != nil {
return err
}
data, description, err := option(toString(lengthData(uint64LE)))(data)
if err != nil {
return err
}
if description != nil {
c.Description = *description
}
data, c.Timestamp, err = mapWithError(lengthData(uint64LE), func(b []byte) (time.Time, error) {
return time.ParseInLocation("2006-01-02T15:04:05.999999999Z", string(b), time.UTC)
})(data)
if err != nil {
return err
}
data, c.Authors, err = vec(hashMap(toString(lengthData(uint64LE)), toString(lengthData(uint64LE))))(data)
if err != nil {
return err
}
//TODO
return nil
}
/*
Hexdump of hashed data:
0000 06 00 00 00 00 00 00 00 09 00 00 00 00 00 00 00 ................
0010 53 61 79 20 68 65 6c 6c 6f 01 10 00 00 00 00 00 Say hello.......
0020 00 00 61 20 74 72 69 76 69 61 6c 20 63 68 61 6e ..a trivial chan
0030 67 65 1e 00 00 00 00 00 00 00 32 30 32 33 2d 30 ge........2023-0
0040 33 2d 32 37 54 31 38 3a 35 31 3a 35 38 2e 30 32 3-27T18:51:58.02
0050 36 30 39 37 36 30 31 5a 01 00 00 00 00 00 00 00 6097601Z........
0060 01 00 00 00 00 00 00 00 03 00 00 00 00 00 00 00 ................
0070 6b 65 79 2c 00 00 00 00 00 00 00 42 43 45 58 59 key,.......BCEXY
0080 75 4b 57 61 51 39 36 62 74 73 6b 38 55 79 42 5a uKWaQ96btsk8UyBZ
0090 57 48 4c 6a 6e 31 42 72 68 79 6b 76 38 74 75 5a WHLjn1Brhykv8tuZ
00a0 47 50 52 6a 7a 46 6e 01 00 00 00 00 00 00 00 01 GPRjzFn.........
00b0 00 00 00 e7 82 b1 d7 e4 17 64 e4 fe 45 2d 6f 24 .........d..E-o$
00c0 22 40 26 16 12 b7 0f 42 70 d9 ac d8 4e 5a 82 ea "@&....Bp...NZ..
00d0 85 ab 57 00 00 00 00 00 00 00 00 00 00 00 00 00 ..W.............
00e0 00 00 00 01 00 00 00 00 00 00 00 03 00 00 00 00 ................
00f0 00 00 00 01 00 00 00 00 00 00 00 01 01 00 00 00 ................
0100 e7 82 b1 d7 e4 17 64 e4 fe 45 2d 6f 24 22 40 26 ......d..E-o$"@&
0110 16 12 b7 0f 42 70 d9 ac d8 4e 5a 82 ea 85 ab 57 ....Bp...NZ....W
0120 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0130 11 00 00 00 00 00 00 00 00 1d 00 00 00 00 00 00 ................
0140 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0150 00 00 01 00 00 00 00 00 00 00 00 1d 00 00 00 00 ................
0160 00 00 00 00 00 00 00 00 00 00 00 11 1e 00 00 00 ................
0170 00 00 00 00 1e 00 00 00 00 00 00 00 01 00 00 00 ................
0180 00 00 00 00 00 00 00 00 00 01 00 00 00 00 01 00 ................
0190 00 00 00 00 00 00 00 1e 00 00 00 00 00 00 00 00 ................
01a0 00 00 00 00 00 00 00 01 1f 00 00 00 00 00 00 00 ................
01b0 2c 00 00 00 00 00 00 00 00 1e 00 00 00 00 00 00 ,...............
01c0 00 05 00 00 00 00 00 00 00 68 65 6c 6c 6f 01 05 .........hello..
01d0 00 00 00 00 00 00 00 55 54 46 2d 38 01 00 00 00 .......UTF-8....
01e0 9e b3 f7 5c 66 9f 18 ec 4f f7 30 51 42 fe 79 bd ...\f...O.0QB.y.
01f0 52 87 24 01 1a c5 77 e8 f9 5b 14 d9 ee f7 21 48 R.$...w..[....!H
0200 7b 22 73 69 67 6e 61 74 75 72 65 22 3a 22 34 63 {"signature":"4c
0210 4b 4c 47 31 41 32 77 64 33 70 70 36 68 42 4d 61 KLG1A2wd3pp6hBMa
0220 63 65 46 43 47 6f 38 41 44 68 55 61 44 4e 7a 33 ceFCGo8ADhUaDNz3
0230 31 4d 4a 65 5a 78 4e 46 59 6e 42 66 38 79 50 65 1MJeZxNFYnBf8yPe
0240 70 58 66 67 6f 72 39 6f 37 38 79 38 38 43 6f 6d pXfgor9o78y88Com
0250 43 33 62 31 59 7a 44 63 63 75 63 44 62 50 62 65 C3b1YzDccucDbPbe
0260 7a 39 50 70 78 65 22 7d 00 00 05 00 00 00 00 00 z9Ppxe"}........
0270 00 00 68 65 6c 6c 6f 01 05 00 00 00 00 00 00 00 ..hello.........
0280 55 54 46 2d 38 00 00 68 65 6c 6c 6f 2c 20 77 6f UTF-8..hello, wo
0290 72 6c 64 0a 00 rld..
*/