Repository: kopia/kopia
Pull request #2956 (2023-04-18 16:28:14)

feat(repository): Reduce memory usage when parsing manifests
Golang's stdlib JSON decoder allocates lots of memory when decoding large objects/arrays because it needs to have the entire object/array in-memory (see a [medium post](https://medium.com/safetycultureengineering/analyzing-and-improving-memory-usage-in-go-46be8c3be0a8) by another person for some debugging of this phenomenon in an unrelated project). This can cause OOMs or increased costs when accessing manifests in a kopia repo that has lots of snapshots in it

This PR adds some manual parsing to the decoding of manifests in kopia. It manually parses the `manifest` struct and pulls off the `[` and `]` denoting the array of manifest entries. Parsing of individual entries in the array is deferred to the stdlib decoder as it behaves reasonably well for those

This does not completely solve the memory problem when there are large numbers of manifests in a repo, but it does help

Some [benchmarking](https://github.com/alcionai/corso/blob/json-debug-benchmarks/src/cmd/jsondebug/benchmark/benchmark_test.go) results for various configurations (sorry the benchmark is in an unrelated repo, we were making things on the fly). The benchmark ran against a single serialized manifest entry that contained 1000 randomly generated kopia snapshot manifests in the array. 500 runs of the benchmark were done with the input gzipped like kopia has it. `Stdlib` is the golang JSON decoder. `JsonParser` uses the JsonParser [library](https://github.com/buger/jsonparser) to extract data, though it still requires having the full JSON array in-memory. `Array` is the implementation in this PR. `ArrayFull` is an extension of this implementation that only uses golang's stdlib decoder to get the values of individual fields in each array entry
```text
Benchmark_FromFile/Stdlib/Zipped-6                   500             42829 ns/op           13125 B/op         72 allocs/op
Benchmark_FromFile/JsonParser/Zipped-6               500             38351 ns/op           22079 B/op         83 allocs/op
Benchmark_FromFile/Array/Zipped-6                    500             42462 ns/op            4764 B/op         74 allocs/op
Benchmark_FromFile/ArrayFull/Zipped-6                500             49263 ns/op            6356 B/op        168 allocs/op
```

codecov[bot]: ## [Codecov](https://codecov.io/gh/kopia/kopia/pull/2956?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia) Report
Patch coverage: **`81.33`**% and project coverage change: **`+0.01`** :tada:
> Comparison is base [(`58d9e94`)](https://codecov.io/gh/kopia/kopia/commit/58d9e94d0090d3a6bf71aed871a8245bfa6afb8d?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia) 75.28% compared to head [(`ababc21`)](https://codecov.io/gh/kopia/kopia/pull/2956?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia) 75.30%.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #2956      +/-   ##
==========================================
+ Coverage   75.28%   75.30%   +0.01%     
==========================================
  Files         456      457       +1     
  Lines       36021    36093      +72     
==========================================
+ Hits        27120    27181      +61     
- Misses       6998     7005       +7     
- Partials     1903     1907       +4     
```


| [Impacted Files](https://codecov.io/gh/kopia/kopia/pull/2956?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia) | Coverage Δ | |
|---|---|---|
| [repo/manifest/serialized.go](https://codecov.io/gh/kopia/kopia/pull/2956?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-cmVwby9tYW5pZmVzdC9zZXJpYWxpemVkLmdv) | `80.28% <80.28%> (ø)` | |
| [repo/manifest/committed\_manifest\_manager.go](https://codecov.io/gh/kopia/kopia/pull/2956?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-cmVwby9tYW5pZmVzdC9jb21taXR0ZWRfbWFuaWZlc3RfbWFuYWdlci5nbw==) | `81.97% <100.00%> (+1.37%)` | :arrow_up: |

... and [8 files with indirect coverage changes](https://codecov.io/gh/kopia/kopia/pull/2956/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia)

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia)

</details>

[:umbrella: View full report in Codecov by Sentry](https://codecov.io/gh/kopia/kopia/pull/2956?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia).


Patch:
diff --git a/go.mod b/go.mod
index de58b68f..efdbef55 100644
--- a/go.mod
+++ b/go.mod
@@ -8,7 +8,7 @@ require (
 	github.com/Azure/azure-storage-blob-go v0.15.0
 	github.com/alecthomas/kingpin/v2 v2.3.2
 	github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137
-	github.com/aws/aws-sdk-go v1.44.245
+	github.com/aws/aws-sdk-go v1.44.246
 	github.com/chmduquesne/rollinghash v4.0.0+incompatible
 	github.com/chromedp/cdproto v0.0.0-20230220211738-2b1ec77315c9
 	github.com/chromedp/chromedp v0.9.1
@@ -24,7 +24,7 @@ require (
 	github.com/google/uuid v1.3.0
 	github.com/gorilla/mux v1.8.0
 	github.com/hanwen/go-fuse/v2 v2.2.0
-	github.com/klauspost/compress v1.16.4
+	github.com/klauspost/compress v1.16.5
 	github.com/klauspost/pgzip v1.2.5
 	github.com/klauspost/reedsolomon v1.11.7
 	github.com/kopia/htmluibuild v0.0.0-20230326183719-f482ef17e2c9
@@ -61,7 +61,7 @@ require (
 	golang.org/x/sys v0.7.0
 	golang.org/x/term v0.7.0
 	golang.org/x/text v0.9.0
-	google.golang.org/api v0.118.0
+	google.golang.org/api v0.119.0
 	google.golang.org/grpc v1.54.0
 	google.golang.org/protobuf v1.30.0
 	gopkg.in/kothar/go-backblaze.v0 v0.0.0-20210124194846-35409b867216
@@ -96,7 +96,7 @@ require (
 	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
 	github.com/google/pprof v0.0.0-20211214055906-6f57359322fd // indirect
 	github.com/google/readahead v0.0.0-20161222183148-eaceba169032 // indirect
-	github.com/google/s2a-go v0.1.0 // indirect
+	github.com/google/s2a-go v0.1.2 // indirect
 	github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
 	github.com/googleapis/gax-go/v2 v2.8.0 // indirect
 	github.com/jmespath/go-jmespath v0.4.0 // indirect
@@ -122,7 +122,7 @@ require (
 	go.uber.org/atomic v1.9.0 // indirect
 	golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
-	google.golang.org/genproto v0.0.0-20230403163135-c38d8f061ccd // indirect
+	google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
 	gopkg.in/ini.v1 v1.67.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
diff --git a/go.sum b/go.sum
index ff9c667a..3ded65b7 100644
--- a/go.sum
+++ b/go.sum
@@ -44,8 +44,8 @@ github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8V
 github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0=
 github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30=
 github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
-github.com/aws/aws-sdk-go v1.44.245 h1:KtY2s4q31/kn33AdV63R5t77mdxsI7rq3YT7Mgo805M=
-github.com/aws/aws-sdk-go v1.44.245/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
+github.com/aws/aws-sdk-go v1.44.246 h1:iLxPX6JU0bxAci9R6/bp8rX0kL871ByCTx0MZlQWv1U=
+github.com/aws/aws-sdk-go v1.44.246/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
 github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
@@ -163,8 +163,8 @@ github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8
 github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
 github.com/google/readahead v0.0.0-20161222183148-eaceba169032 h1:6Be3nkuJFyRfCgr6qTIzmRp8y9QwDIbqy/nYr9WDPos=
 github.com/google/readahead v0.0.0-20161222183148-eaceba169032/go.mod h1:qYysrqQXuV4tzsizt4oOQ6mrBZQ0xnQXP3ylXX8Jk5Y=
-github.com/google/s2a-go v0.1.0 h1:3Qm0liEiCErViKERO2Su5wp+9PfMRiuS6XB5FvpKnYQ=
-github.com/google/s2a-go v0.1.0/go.mod h1:OJpEgntRZo8ugHpF9hkoLJbS5dSI20XZeXJ9JVywLlM=
+github.com/google/s2a-go v0.1.2 h1:WVtYAYuYxKeYajAmThMRYWP6K3wXkcqbGHeUgeubUHY=
+github.com/google/s2a-go v0.1.2/go.mod h1:OJpEgntRZo8ugHpF9hkoLJbS5dSI20XZeXJ9JVywLlM=
 github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -188,8 +188,8 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm
 github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
 github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
 github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
-github.com/klauspost/compress v1.16.4 h1:91KN02FnsOYhuunwU4ssRe8lc2JosWmizWa91B5v1PU=
-github.com/klauspost/compress v1.16.4/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
+github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI=
+github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
 github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
 github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
 github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
@@ -423,8 +423,8 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
 golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
-google.golang.org/api v0.118.0 h1:FNfHq9Z2GKULxu7cEhCaB0wWQHg43UpomrrN+24ZRdE=
-google.golang.org/api v0.118.0/go.mod h1:76TtD3vkgmZ66zZzp72bUUklpmQmKlhh6sYtIjYK+5E=
+google.golang.org/api v0.119.0 h1:Dzq+ARD6+8jmd5wknJE1crpuzu1JiovEU6gCp9PkoKA=
+google.golang.org/api v0.119.0/go.mod h1:CrSvlNEFCFLae9ZUtL1z+61+rEBD7J/aCYwVYKZoWFU=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
@@ -433,8 +433,8 @@ google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoA
 google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
 google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
 google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
-google.golang.org/genproto v0.0.0-20230403163135-c38d8f061ccd h1:sLpv7bNL1AsX3fdnWh9WVh7ejIzXdOc1RRHGeAmeStU=
-google.golang.org/genproto v0.0.0-20230403163135-c38d8f061ccd/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak=
+google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
+google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
 google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
diff --git a/repo/manifest/committed_manifest_manager.go b/repo/manifest/committed_manifest_manager.go
index 048d2862..32638a3a 100644
--- a/repo/manifest/committed_manifest_manager.go
+++ b/repo/manifest/committed_manifest_manager.go
@@ -346,11 +346,13 @@ func loadManifestContent(ctx context.Context, b contentManager, contentID conten
 		return man, errors.Wrapf(err, "unable to unpack manifest data %q", contentID)
 	}
 
-	if err := json.NewDecoder(gz).Decode(&man); err != nil {
-		return man, errors.Wrapf(err, "unable to parse manifest %q", contentID)
-	}
+	// Will be GC-ed even if we don't close it?
+	//nolint:errcheck
+	defer gz.Close()
+
+	man, err = decodeManifestArray(gz)
 
-	return man, nil
+	return man, errors.Wrapf(err, "unable to parse manifest %q", contentID)
 }
 
 func newCommittedManager(b contentManager) *committedManifestManager {
diff --git a/repo/manifest/serialized.go b/repo/manifest/serialized.go
index 05afb177..a99f2ff9 100644
--- a/repo/manifest/serialized.go
+++ b/repo/manifest/serialized.go
@@ -2,7 +2,11 @@
 
 import (
 	"encoding/json"
+	"io"
+	"strings"
 	"time"
+
+	"github.com/pkg/errors"
 )
 
 type manifest struct {
@@ -16,3 +20,122 @@ type manifestEntry struct {
 	Deleted bool              `json:"deleted,omitempty"`
 	Content json.RawMessage   `json:"data"`
 }
+
+const (
+	objectOpen  = "{"
+	objectClose = "}"
+	arrayOpen   = "["
+	arrayClose  = "]"
+)
+
+var errEOF = errors.New("unexpected end of input")
+
+func expectDelimToken(dec *json.Decoder, expectedToken string) error {
+	t, err := dec.Token()
+	if errors.Is(err, io.EOF) {
+		return errors.WithStack(errEOF)
+	} else if err != nil {
+		return errors.Wrap(err, "reading JSON token")
+	}
+
+	d, ok := t.(json.Delim)
+	if !ok {
+		return errors.Errorf("unexpected token: (%T) %v", t, t)
+	} else if d.String() != expectedToken {
+		return errors.Errorf("unexpected token; wanted %s, got %s", expectedToken, d)
+	}
+
+	return nil
+}
+
+func stringToken(dec *json.Decoder) (string, error) {
+	t, err := dec.Token()
+	if errors.Is(err, io.EOF) {
+		return "", errors.WithStack(errEOF)
+	} else if err != nil {
+		return "", errors.Wrap(err, "reading JSON token")
+	}
+
+	l, ok := t.(string)
+	if !ok {
+		return "", errors.Errorf("unexpected token (%T) %v; wanted field name", t, t)
+	}
+
+	return l, nil
+}
+
+func decodeManifestArray(r io.Reader) (manifest, error) {
+	var (
+		dec = json.NewDecoder(r)
+		res = manifest{}
+	)
+
+	if err := expectDelimToken(dec, objectOpen); err != nil {
+		return res, err
+	}
+
+	// Need to manually decode fields here since we can't reuse the stdlib
+	// decoder due to memory issues.
+	if err := parseFields(dec, &res); err != nil {
+		return res, err
+	}
+
+	// Consumes closing object curly brace after we're done. Don't need to check
+	// for EOF because json.Decode only guarantees decoding the next JSON item in
+	// the stream so this follows that.
+	return res, expectDelimToken(dec, objectClose)
+}
+
+func parseFields(dec *json.Decoder, res *manifest) error {
+	var seen bool
+
+	for dec.More() {
+		l, err := stringToken(dec)
+		if err != nil {
+			return err
+		}
+
+		// Only have `entries` field right now. Skip other fields.
+		if !strings.EqualFold("entries", l) {
+			continue
+		}
+
+		if seen {
+			return errors.New("repeated Entries field")
+		}
+
+		seen = true
+
+		if err := decodeArray(dec, &res.Entries); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+// decodeArray decodes an array of *manifestEntry and returns them in output. If
+// an error occurs output may contain intermediate state.
+//
+// This can be made into a generic function pretty easily if it's needed in
+// other places.
+func decodeArray(dec *json.Decoder, output *[]*manifestEntry) error {
+	// Consume starting bracket.
+	if err := expectDelimToken(dec, arrayOpen); err != nil {
+		return err
+	}
+
+	// Read elements.
+	for dec.More() {
+		var tmp *manifestEntry
+
+		if err := dec.Decode(&tmp); err != nil {
+			return errors.Wrap(err, "decoding array element")
+		}
+
+		*output = append(*output, tmp)
+	}
+
+	// Consume ending bracket.
+	return expectDelimToken(dec, arrayClose)
+}


Test patch:
diff --git a/repo/manifest/serialized_test.go b/repo/manifest/serialized_test.go
new file mode 100644
index 00000000..1529adcc
--- /dev/null
+++ b/repo/manifest/serialized_test.go
@@ -0,0 +1,67 @@
+package manifest
+
+import (
+	"bytes"
+	"encoding/json"
+	"reflect"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"github.com/kopia/kopia/repo/manifest/testdata"
+)
+
+func TestManifestDecode_GoodInput(t *testing.T) {
+	table := []struct {
+		name  string
+		input []byte
+	}{
+		{
+			name:  "MultipleManifests",
+			input: []byte(testdata.GoodManifests),
+		},
+		{
+			name:  "IgnoredField",
+			input: []byte(testdata.IgnoredField),
+		},
+		{
+			name:  "StopsAtStructEnd",
+			input: []byte(testdata.ExtraInputAtEnd),
+		},
+		{
+			name:  "CaseInsensitive",
+			input: []byte(testdata.CaseInsensitive),
+		},
+	}
+
+	for _, test := range table {
+		t.Run(test.name, func(t *testing.T) {
+			stdlibDec := manifest{}
+
+			stdReader := bytes.NewReader(test.input)
+			require.NoError(t, json.NewDecoder(stdReader).Decode(&stdlibDec))
+
+			arrReader := bytes.NewReader(test.input)
+			arrDec, err := decodeManifestArray(arrReader)
+			require.NoError(t, err)
+
+			assert.Equal(t, stdlibDec, arrDec)
+
+			assert.True(t, reflect.DeepEqual(stdlibDec, arrDec))
+		})
+	}
+}
+
+func TestManifestDecode_BadInput(t *testing.T) {
+	for _, test := range testdata.BadInputs {
+		t.Run(test.Name, func(t *testing.T) {
+			r := bytes.NewReader([]byte(test.Input))
+			_, err := decodeManifestArray(r)
+
+			t.Logf("%v", err)
+
+			assert.Error(t, err)
+		})
+	}
+}
diff --git a/repo/manifest/testdata/manifests.go b/repo/manifest/testdata/manifests.go
new file mode 100644
index 00000000..07274c24
--- /dev/null
+++ b/repo/manifest/testdata/manifests.go
@@ -0,0 +1,879 @@
+package testdata
+
+type testInput struct {
+	Name  string
+	Input string
+}
+
+var (
+	BadInputs = []testInput{
+		{
+			Name: "RepeatedEntriesField",
+			Input: `
+{
+	"entries": [
+	{
+		"id": "25905b6f222a153561543baea0a67043",
+		"labels": {
+			"hostname": "host-name",
+			"path": "/root/tmp/test",
+			"type": "snapshot",
+			"username": "user-name"
+		},
+		"modified": "2023-03-16T20:46:59.70714Z",
+		"data": {
+			"id": "",
+			"source": {
+				"host": "host-name",
+				"userName": "user-name",
+				"path": "/root/tmp/test"
+			},
+			"description": "",
+			"startTime": "2023-03-16T20:46:55.76843Z",
+			"endTime": "2023-03-16T20:46:59.707064Z",
+			"stats": {
+				"totalSize": 536927459,
+				"excludedTotalSize": 0,
+				"fileCount": 18,
+				"cachedFiles": 0,
+				"nonCachedFiles": 18,
+				"dirCount": 14,
+				"excludedFileCount": 0,
+				"excludedDirCount": 0,
+				"ignoredErrorCount": 0,
+				"errorCount": 0
+			},
+			"rootEntry": {
+				"name": "test",
+				"type": "d",
+				"mode": "0777",
+				"mtime": "1754-08-30T22:43:41.128654848Z",
+				"obj": "k74647859396c88127696f426b4c79088",
+				"summ": {
+					"size": 536927459,
+					"files": 18,
+					"symlinks": 0,
+					"dirs": 14,
+					"maxTime": "2023-03-16T20:46:56.187394Z",
+					"numFailed": 0
+				}
+			},
+		}
+	},
+	],
+	entries: []
+}`,
+		},
+		{
+			Name: "MissingObjectStart",
+			Input: `
+	"entries": [
+	{
+		"id": "25905b6f222a153561543baea0a67043",
+		"labels": {
+			"hostname": "host-name",
+			"path": "/root/tmp/test",
+			"type": "snapshot",
+			"username": "user-name"
+		},
+		"modified": "2023-03-16T20:46:59.70714Z",
+		"data": {
+			"id": "",
+			"source": {
+				"host": "host-name",
+				"userName": "user-name",
+				"path": "/root/tmp/test"
+			},
+			"description": "",
+			"startTime": "2023-03-16T20:46:55.76843Z",
+			"endTime": "2023-03-16T20:46:59.707064Z",
+			"stats": {
+				"totalSize": 536927459,
+				"excludedTotalSize": 0,
+				"fileCount": 18,
+				"cachedFiles": 0,
+				"nonCachedFiles": 18,
+				"dirCount": 14,
+				"excludedFileCount": 0,
+				"excludedDirCount": 0,
+				"ignoredErrorCount": 0,
+				"errorCount": 0
+			},
+			"rootEntry": {
+				"name": "test",
+				"type": "d",
+				"mode": "0777",
+				"mtime": "1754-08-30T22:43:41.128654848Z",
+				"obj": "k74647859396c88127696f426b4c79088",
+				"summ": {
+					"size": 536927459,
+					"files": 18,
+					"symlinks": 0,
+					"dirs": 14,
+					"maxTime": "2023-03-16T20:46:56.187394Z",
+					"numFailed": 0
+				}
+			}
+		}
+	}
+	]
+}`,
+		},
+		{
+			Name: "MissingObjectEnd",
+			Input: `
+{
+	"entries": [
+	{
+		"id": "25905b6f222a153561543baea0a67043",
+		"labels": {
+			"hostname": "host-name",
+			"path": "/root/tmp/test",
+			"type": "snapshot",
+			"username": "user-name"
+		},
+		"modified": "2023-03-16T20:46:59.70714Z",
+		"data": {
+			"id": "",
+			"source": {
+				"host": "host-name",
+				"userName": "user-name",
+				"path": "/root/tmp/test"
+			},
+			"description": "",
+			"startTime": "2023-03-16T20:46:55.76843Z",
+			"endTime": "2023-03-16T20:46:59.707064Z",
+			"stats": {
+				"totalSize": 536927459,
+				"excludedTotalSize": 0,
+				"fileCount": 18,
+				"cachedFiles": 0,
+				"nonCachedFiles": 18,
+				"dirCount": 14,
+				"excludedFileCount": 0,
+				"excludedDirCount": 0,
+				"ignoredErrorCount": 0,
+				"errorCount": 0
+			},
+			"rootEntry": {
+				"name": "test",
+				"type": "d",
+				"mode": "0777",
+				"mtime": "1754-08-30T22:43:41.128654848Z",
+				"obj": "k74647859396c88127696f426b4c79088",
+				"summ": {
+					"size": 536927459,
+					"files": 18,
+					"symlinks": 0,
+					"dirs": 14,
+					"maxTime": "2023-03-16T20:46:56.187394Z",
+					"numFailed": 0
+				}
+			}
+		}
+	}
+	]`,
+		},
+		{
+			Name: "MissingArrayStart",
+			Input: `
+{
+	"entries":
+	{
+		"id": "25905b6f222a153561543baea0a67043",
+		"labels": {
+			"hostname": "host-name",
+			"path": "/root/tmp/test",
+			"type": "snapshot",
+			"username": "user-name"
+		},
+		"modified": "2023-03-16T20:46:59.70714Z",
+		"data": {
+			"id": "",
+			"source": {
+				"host": "host-name",
+				"userName": "user-name",
+				"path": "/root/tmp/test"
+			},
+			"description": "",
+			"startTime": "2023-03-16T20:46:55.76843Z",
+			"endTime": "2023-03-16T20:46:59.707064Z",
+			"stats": {
+				"totalSize": 536927459,
+				"excludedTotalSize": 0,
+				"fileCount": 18,
+				"cachedFiles": 0,
+				"nonCachedFiles": 18,
+				"dirCount": 14,
+				"excludedFileCount": 0,
+				"excludedDirCount": 0,
+				"ignoredErrorCount": 0,
+				"errorCount": 0
+			},
+			"rootEntry": {
+				"name": "test",
+				"type": "d",
+				"mode": "0777",
+				"mtime": "1754-08-30T22:43:41.128654848Z",
+				"obj": "k74647859396c88127696f426b4c79088",
+				"summ": {
+					"size": 536927459,
+					"files": 18,
+					"symlinks": 0,
+					"dirs": 14,
+					"maxTime": "2023-03-16T20:46:56.187394Z",
+					"numFailed": 0
+				}
+			}
+		}
+	}
+	]
+}`,
+		},
+		{
+			Name: "MissingArrayEnd",
+			Input: `
+{
+	"entries": [
+	{
+		"id": "25905b6f222a153561543baea0a67043",
+		"labels": {
+			"hostname": "host-name",
+			"path": "/root/tmp/test",
+			"type": "snapshot",
+			"username": "user-name"
+		},
+		"modified": "2023-03-16T20:46:59.70714Z",
+		"data": {
+			"id": "",
+			"source": {
+				"host": "host-name",
+				"userName": "user-name",
+				"path": "/root/tmp/test"
+			},
+			"description": "",
+			"startTime": "2023-03-16T20:46:55.76843Z",
+			"endTime": "2023-03-16T20:46:59.707064Z",
+			"stats": {
+				"totalSize": 536927459,
+				"excludedTotalSize": 0,
+				"fileCount": 18,
+				"cachedFiles": 0,
+				"nonCachedFiles": 18,
+				"dirCount": 14,
+				"excludedFileCount": 0,
+				"excludedDirCount": 0,
+				"ignoredErrorCount": 0,
+				"errorCount": 0
+			},
+			"rootEntry": {
+				"name": "test",
+				"type": "d",
+				"mode": "0777",
+				"mtime": "1754-08-30T22:43:41.128654848Z",
+				"obj": "k74647859396c88127696f426b4c79088",
+				"summ": {
+					"size": 536927459,
+					"files": 18,
+					"symlinks": 0,
+					"dirs": 14,
+					"maxTime": "2023-03-16T20:46:56.187394Z",
+					"numFailed": 0
+				}
+			}
+		}
+	}
+}`,
+		},
+		{
+			Name: "MissingInnerObjectStart",
+			Input: `
+{
+	"entries": [
+		"id": "25905b6f222a153561543baea0a67043",
+		"labels": {
+			"hostname": "host-name",
+			"path": "/root/tmp/test",
+			"type": "snapshot",
+			"username": "user-name"
+		},
+		"modified": "2023-03-16T20:46:59.70714Z",
+		"data": {
+			"id": "",
+			"source": {
+				"host": "host-name",
+				"userName": "user-name",
+				"path": "/root/tmp/test"
+			},
+			"description": "",
+			"startTime": "2023-03-16T20:46:55.76843Z",
+			"endTime": "2023-03-16T20:46:59.707064Z",
+			"stats": {
+				"totalSize": 536927459,
+				"excludedTotalSize": 0,
+				"fileCount": 18,
+				"cachedFiles": 0,
+				"nonCachedFiles": 18,
+				"dirCount": 14,
+				"excludedFileCount": 0,
+				"excludedDirCount": 0,
+				"ignoredErrorCount": 0,
+				"errorCount": 0
+			},
+			"rootEntry": {
+				"name": "test",
+				"type": "d",
+				"mode": "0777",
+				"mtime": "1754-08-30T22:43:41.128654848Z",
+				"obj": "k74647859396c88127696f426b4c79088",
+				"summ": {
+					"size": 536927459,
+					"files": 18,
+					"symlinks": 0,
+					"dirs": 14,
+					"maxTime": "2023-03-16T20:46:56.187394Z",
+					"numFailed": 0
+				}
+			}
+		}
+	},
+	]
+}`,
+		},
+		{
+			Name: "BadInnerObject",
+			Input: `
+{
+	"entries": [
+	{
+		"id": "25905b6f222a153561543baea0a67043",
+		"labels": {
+			"hostname": "host-name",
+			"path": "/root/tmp/test",
+			"type": "snapshot",
+			"username": "user-name"
+		},
+		"modified": "2023-03-16T20:46:59.70714Z",
+		"data": {
+			"id": "",
+			"source": {
+				"host": "host-name",
+				"userName": "user-name",
+				"path": "/root/tmp/test"
+			},
+			"description": "",
+			"startTime": "2023-03-16T20:46:55.76843Z",
+			"endTime": "2023-03-16T20:46:59.707064Z",
+			"stats": {
+				"totalSize": 536927459,
+				"excludedTotalSize": 0,
+				"fileCount": 18,
+				"cachedFiles": 0,
+				"nonCachedFiles": 18,
+				"dirCount": 14,
+				"excludedFileCount": 0,
+				"excludedDirCount": 0,
+				"ignoredErrorCount": 0,
+				"errorCount": 0
+			},
+			"rootEntry": {
+				"name": "test",
+				"type": "d",
+				"mode": "0777",
+				"mtime": "1754-08-30T22:43:41.128654848Z",
+				"obj": "k74647859396c88127696f426b4c79088",
+				"summ": {
+					"size": 536927459,
+					"files": 18,
+					"symlinks": 0,
+					"dirs": 14,
+					"maxTime": "2023-03-16T20:46:56.187394Z",
+					"numFailed": 0
+				}
+		}
+	}
+	]
+}`,
+		},
+		{
+			Name: "MissingInnerObjectEnd",
+			Input: `
+{
+	"entries": [
+	{
+		"id": "25905b6f222a153561543baea0a67043",
+		"labels": {
+			"hostname": "host-name",
+			"path": "/root/tmp/test",
+			"type": "snapshot",
+			"username": "user-name"
+		},
+		"modified": "2023-03-16T20:46:59.70714Z",
+		"data": {
+			"id": "",
+			"source": {
+				"host": "host-name",
+				"userName": "user-name",
+				"path": "/root/tmp/test"
+			},
+			"description": "",
+			"startTime": "2023-03-16T20:46:55.76843Z",
+			"endTime": "2023-03-16T20:46:59.707064Z",
+			"stats": {
+				"totalSize": 536927459,
+				"excludedTotalSize": 0,
+				"fileCount": 18,
+				"cachedFiles": 0,
+				"nonCachedFiles": 18,
+				"dirCount": 14,
+				"excludedFileCount": 0,
+				"excludedDirCount": 0,
+				"ignoredErrorCount": 0,
+				"errorCount": 0
+			},
+			"rootEntry": {
+				"name": "test",
+				"type": "d",
+				"mode": "0777",
+				"mtime": "1754-08-30T22:43:41.128654848Z",
+				"obj": "k74647859396c88127696f426b4c79088",
+				"summ": {
+					"size": 536927459,
+					"files": 18,
+					"symlinks": 0,
+					"dirs": 14,
+					"maxTime": "2023-03-16T20:46:56.187394Z",
+					"numFailed": 0
+				}
+			}
+		}
+	]
+}`,
+		},
+	}
+)
+
+const (
+	GoodManifests = `
+{
+	"entries": [
+	{
+		"id": "2e14cba9427c57223dd768bd1ddf694c",
+		"labels": {
+			"hostname": "host-name",
+			"path": "/root/tmp/test",
+			"tag": "value",
+			"type": "snapshot",
+			"username": "user-name"
+		},
+		"modified": "2023-03-17T01:08:32.962808Z",
+		"data": {
+			"id": "",
+			"source": {
+				"host": "host-name",
+				"userName": "user-name",
+				"path": "/root/tmp/test"
+			},
+			"description": "",
+			"startTime": "2023-03-17T01:08:29.674573Z",
+			"endTime": "2023-03-17T01:08:32.962614Z",
+			"stats": {
+				"totalSize": 427221,
+				"excludedTotalSize": 0,
+				"fileCount": 143,
+				"cachedFiles": 0,
+				"nonCachedFiles": 143,
+				"dirCount": 10,
+				"excludedFileCount": 0,
+				"excludedDirCount": 0,
+				"ignoredErrorCount": 0,
+				"errorCount": 0
+			},
+			"rootEntry": {
+				"name": "test",
+				"type": "d",
+				"mode": "0777",
+				"mtime": "1754-08-30T22:43:41.128654848Z",
+				"obj": "kfe00a91781912fc352edca26571a5f83",
+				"summ": {
+					"size": 427221,
+					"files": 143,
+					"symlinks": 0,
+					"dirs": 10,
+					"maxTime": "2023-03-17T01:08:29.677079Z",
+					"numFailed": 0
+				}
+			},
+			"tags": {
+				"tag": "value"
+			}
+		}
+	},
+	{
+		"id": "2c54893efd80bcda7102f622da5c63ee",
+		"labels": {
+			"hostname": "host-name",
+			"path": "/root/tmp/test",
+			"type": "snapshot",
+			"username": "user-name"
+		},
+		"modified": "2023-03-17T01:11:34.506121Z",
+		"data": {
+			"id": "",
+			"source": {
+				"host": "host-name",
+				"userName": "user-name",
+				"path": "/root/tmp/test"
+			},
+			"description": "",
+			"startTime": "2023-03-17T01:11:22.34148Z",
+			"endTime": "2023-03-17T01:11:34.505952Z",
+			"stats": {
+				"totalSize": 427221,
+				"excludedTotalSize": 0,
+				"fileCount": 2,
+				"cachedFiles": 141,
+				"nonCachedFiles": 2,
+				"dirCount": 10,
+				"excludedFileCount": 0,
+				"excludedDirCount": 0,
+				"ignoredErrorCount": 0,
+				"errorCount": 0
+			},
+			"rootEntry": {
+				"name": "test",
+				"type": "d",
+				"mode": "0777",
+				"mtime": "1754-08-30T22:43:41.128654848Z",
+				"obj": "k4f1a9e8049091615cbe4ad93507680f3",
+				"summ": {
+					"size": 427221,
+					"files": 143,
+					"symlinks": 0,
+					"dirs": 10,
+					"maxTime": "2023-03-17T01:11:22.725375Z",
+					"numFailed": 0
+				}
+			}
+		}
+	}
+	]
+}
+`
+	IgnoredField = `
+{
+	"entries": [
+	{
+		"id": "2e14cba9427c57223dd768bd1ddf694c",
+		"labels": {
+			"hostname": "host-name",
+			"path": "/root/tmp/test",
+			"tag": "value",
+			"type": "snapshot",
+			"username": "user-name"
+		},
+		"modified": "2023-03-17T01:08:32.962808Z",
+		"data": {
+			"id": "",
+			"source": {
+				"host": "host-name",
+				"userName": "user-name",
+				"path": "/root/tmp/test"
+			},
+			"description": "",
+			"startTime": "2023-03-17T01:08:29.674573Z",
+			"endTime": "2023-03-17T01:08:32.962614Z",
+			"stats": {
+				"totalSize": 427221,
+				"excludedTotalSize": 0,
+				"fileCount": 143,
+				"cachedFiles": 0,
+				"nonCachedFiles": 143,
+				"dirCount": 10,
+				"excludedFileCount": 0,
+				"excludedDirCount": 0,
+				"ignoredErrorCount": 0,
+				"errorCount": 0
+			},
+			"rootEntry": {
+				"name": "test",
+				"type": "d",
+				"mode": "0777",
+				"mtime": "1754-08-30T22:43:41.128654848Z",
+				"obj": "kfe00a91781912fc352edca26571a5f83",
+				"summ": {
+					"size": 427221,
+					"files": 143,
+					"symlinks": 0,
+					"dirs": 10,
+					"maxTime": "2023-03-17T01:08:29.677079Z",
+					"numFailed": 0
+				}
+			},
+			"tags": {
+				"tag": "value"
+			}
+		}
+	},
+	{
+		"id": "2c54893efd80bcda7102f622da5c63ee",
+		"labels": {
+			"hostname": "host-name",
+			"path": "/root/tmp/test",
+			"type": "snapshot",
+			"username": "user-name"
+		},
+		"modified": "2023-03-17T01:11:34.506121Z",
+		"data": {
+			"id": "",
+			"source": {
+				"host": "host-name",
+				"userName": "user-name",
+				"path": "/root/tmp/test"
+			},
+			"description": "",
+			"startTime": "2023-03-17T01:11:22.34148Z",
+			"endTime": "2023-03-17T01:11:34.505952Z",
+			"stats": {
+				"totalSize": 427221,
+				"excludedTotalSize": 0,
+				"fileCount": 2,
+				"cachedFiles": 141,
+				"nonCachedFiles": 2,
+				"dirCount": 10,
+				"excludedFileCount": 0,
+				"excludedDirCount": 0,
+				"ignoredErrorCount": 0,
+				"errorCount": 0
+			},
+			"rootEntry": {
+				"name": "test",
+				"type": "d",
+				"mode": "0777",
+				"mtime": "1754-08-30T22:43:41.128654848Z",
+				"obj": "k4f1a9e8049091615cbe4ad93507680f3",
+				"summ": {
+					"size": 427221,
+					"files": 143,
+					"symlinks": 0,
+					"dirs": 10,
+					"maxTime": "2023-03-17T01:11:22.725375Z",
+					"numFailed": 0
+				}
+			}
+		}
+	}
+	],
+	"ignored": "hello world"
+}`
+	ExtraInputAtEnd = `
+{
+	"entries": [
+	{
+		"id": "2e14cba9427c57223dd768bd1ddf694c",
+		"labels": {
+			"hostname": "host-name",
+			"path": "/root/tmp/test",
+			"tag": "value",
+			"type": "snapshot",
+			"username": "user-name"
+		},
+		"modified": "2023-03-17T01:08:32.962808Z",
+		"data": {
+			"id": "",
+			"source": {
+				"host": "host-name",
+				"userName": "user-name",
+				"path": "/root/tmp/test"
+			},
+			"description": "",
+			"startTime": "2023-03-17T01:08:29.674573Z",
+			"endTime": "2023-03-17T01:08:32.962614Z",
+			"stats": {
+				"totalSize": 427221,
+				"excludedTotalSize": 0,
+				"fileCount": 143,
+				"cachedFiles": 0,
+				"nonCachedFiles": 143,
+				"dirCount": 10,
+				"excludedFileCount": 0,
+				"excludedDirCount": 0,
+				"ignoredErrorCount": 0,
+				"errorCount": 0
+			},
+			"rootEntry": {
+				"name": "test",
+				"type": "d",
+				"mode": "0777",
+				"mtime": "1754-08-30T22:43:41.128654848Z",
+				"obj": "kfe00a91781912fc352edca26571a5f83",
+				"summ": {
+					"size": 427221,
+					"files": 143,
+					"symlinks": 0,
+					"dirs": 10,
+					"maxTime": "2023-03-17T01:08:29.677079Z",
+					"numFailed": 0
+				}
+			},
+			"tags": {
+				"tag": "value"
+			}
+		}
+	},
+	{
+		"id": "2c54893efd80bcda7102f622da5c63ee",
+		"labels": {
+			"hostname": "host-name",
+			"path": "/root/tmp/test",
+			"type": "snapshot",
+			"username": "user-name"
+		},
+		"modified": "2023-03-17T01:11:34.506121Z",
+		"data": {
+			"id": "",
+			"source": {
+				"host": "host-name",
+				"userName": "user-name",
+				"path": "/root/tmp/test"
+			},
+			"description": "",
+			"startTime": "2023-03-17T01:11:22.34148Z",
+			"endTime": "2023-03-17T01:11:34.505952Z",
+			"stats": {
+				"totalSize": 427221,
+				"excludedTotalSize": 0,
+				"fileCount": 2,
+				"cachedFiles": 141,
+				"nonCachedFiles": 2,
+				"dirCount": 10,
+				"excludedFileCount": 0,
+				"excludedDirCount": 0,
+				"ignoredErrorCount": 0,
+				"errorCount": 0
+			},
+			"rootEntry": {
+				"name": "test",
+				"type": "d",
+				"mode": "0777",
+				"mtime": "1754-08-30T22:43:41.128654848Z",
+				"obj": "k4f1a9e8049091615cbe4ad93507680f3",
+				"summ": {
+					"size": 427221,
+					"files": 143,
+					"symlinks": 0,
+					"dirs": 10,
+					"maxTime": "2023-03-17T01:11:22.725375Z",
+					"numFailed": 0
+				}
+			}
+		}
+	}
+	]
+}abcdefg`
+	CaseInsensitive = `
+{
+	"Entries": [
+	{
+		"id": "2e14cba9427c57223dd768bd1ddf694c",
+		"labels": {
+			"hostname": "host-name",
+			"path": "/root/tmp/test",
+			"tag": "value",
+			"type": "snapshot",
+			"username": "user-name"
+		},
+		"modified": "2023-03-17T01:08:32.962808Z",
+		"data": {
+			"id": "",
+			"source": {
+				"host": "host-name",
+				"userName": "user-name",
+				"path": "/root/tmp/test"
+			},
+			"description": "",
+			"startTime": "2023-03-17T01:08:29.674573Z",
+			"endTime": "2023-03-17T01:08:32.962614Z",
+			"stats": {
+				"totalSize": 427221,
+				"excludedTotalSize": 0,
+				"fileCount": 143,
+				"cachedFiles": 0,
+				"nonCachedFiles": 143,
+				"dirCount": 10,
+				"excludedFileCount": 0,
+				"excludedDirCount": 0,
+				"ignoredErrorCount": 0,
+				"errorCount": 0
+			},
+			"rootEntry": {
+				"name": "test",
+				"type": "d",
+				"mode": "0777",
+				"mtime": "1754-08-30T22:43:41.128654848Z",
+				"obj": "kfe00a91781912fc352edca26571a5f83",
+				"summ": {
+					"size": 427221,
+					"files": 143,
+					"symlinks": 0,
+					"dirs": 10,
+					"maxTime": "2023-03-17T01:08:29.677079Z",
+					"numFailed": 0
+				}
+			},
+			"tags": {
+				"tag": "value"
+			}
+		}
+	},
+	{
+		"id": "2c54893efd80bcda7102f622da5c63ee",
+		"labels": {
+			"hostname": "host-name",
+			"path": "/root/tmp/test",
+			"type": "snapshot",
+			"username": "user-name"
+		},
+		"modified": "2023-03-17T01:11:34.506121Z",
+		"data": {
+			"id": "",
+			"source": {
+				"host": "host-name",
+				"userName": "user-name",
+				"path": "/root/tmp/test"
+			},
+			"description": "",
+			"startTime": "2023-03-17T01:11:22.34148Z",
+			"endTime": "2023-03-17T01:11:34.505952Z",
+			"stats": {
+				"totalSize": 427221,
+				"excludedTotalSize": 0,
+				"fileCount": 2,
+				"cachedFiles": 141,
+				"nonCachedFiles": 2,
+				"dirCount": 10,
+				"excludedFileCount": 0,
+				"excludedDirCount": 0,
+				"ignoredErrorCount": 0,
+				"errorCount": 0
+			},
+			"rootEntry": {
+				"name": "test",
+				"type": "d",
+				"mode": "0777",
+				"mtime": "1754-08-30T22:43:41.128654848Z",
+				"obj": "k4f1a9e8049091615cbe4ad93507680f3",
+				"summ": {
+					"size": 427221,
+					"files": 143,
+					"symlinks": 0,
+					"dirs": 10,
+					"maxTime": "2023-03-17T01:11:22.725375Z",
+					"numFailed": 0
+				}
+			}
+		}
+	}
+	]
+}`
+)