Repository: kopia/kopia
Pull request #3356 (2023-09-26 01:47:54)

fix(repository): fixed handling of content.Info
Previously `content.Info` was an interface which was implemented by:

* `index.InfoStruct`
* `index.indexEntryInfoV1`
* `index.indexEntryInfoV2`

The last 2 implementations were relying on memory-mapped files which in rare cases could be closed while Kopia was still processing them leading to #2599.

This changes fixes the bug and strictly separates `content.Info` (which is now always a struct) from the other two (which were renamed as `index.InfoReader` and only used inside repo/content/...).

In addition to being safer, this _should_ reduce memory allocations. Perf testing pending...

Quick testing results using https://github.com/kopia/benchmark-scenarios/tree/main/runbench

E2E duration is slightly better, average memory usage is about 5% up, fewer heap objects, more heap bytes, which looks reasonable.

```
$ runbench --kopia-exe ~/go/bin/kopia --compare-to-exe ~/go/bin/kopia-baseline --min-duration 30s ./snapshot-linux-parallel-4.sh
DIFF duration: current:5.1 baseline:5.5 change:-7.4 %
DIFF repo_size: current:1081613865.2 baseline:1081621658.6 change:-0.0 %
DIFF num_files: current:60.0 baseline:60.0 change:0%
DIFF avg_heap_objects: current:4766920.0 baseline:4833920.4 change:-1.4 %
DIFF avg_heap_bytes: current:815716492.8 baseline:687807777.6 change:+18.6 %
DIFF avg_ram: current:230.8 baseline:218.5 change:+5.6 %
DIFF max_ram: current:334.5 baseline:306.0 change:+9.3 %
DIFF avg_cpu: current:176.8 baseline:157.0 change:+12.6 %
DIFF max_cpu: current:254.8 baseline:245.8 change:+3.6 %

$ runbench --kopia-exe ~/go/bin/kopia --compare-to-exe ~/go/bin/kopia-baseline --min-duration 30s ./snapshot-isos-parallel-2.sh
DIFF duration: current:14.6 baseline:14.8 change:-1.6 %
DIFF repo_size: current:7749662039.5 baseline:7749641536.0 change:+0.0 %
DIFF num_files: current:336.0 baseline:336.0 change:0%
DIFF avg_heap_objects: current:337390.0 baseline:354121.5 change:-4.7 %
DIFF avg_heap_bytes: current:264491528.0 baseline:266402352.0 change:-0.7 %
DIFF avg_ram: current:165.4 baseline:155.5 change:+6.4 %
DIFF max_ram: current:194.7 baseline:185.8 change:+4.8 %
DIFF avg_cpu: current:251.5 baseline:248.3 change:+1.3 %
DIFF max_cpu: current:283.0 baseline:282.0 change:+0.3 %

```

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/kopia/kopia/pull/3356?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia) Report
Attention: `5 lines` in your changes are missing coverage. Please review.
> Comparison is base [(`814738c`)](https://app.codecov.io/gh/kopia/kopia/commit/814738c74e7dcf02dbcea676c857e690afd57d7c?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia) 75.88% compared to head [(`f6bf5b5`)](https://app.codecov.io/gh/kopia/kopia/pull/3356?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia) 75.81%.
> Report is 2 commits behind head on master.

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


```diff
@@            Coverage Diff             @@
##           master    #3356      +/-   ##
==========================================
- Coverage   75.88%   75.81%   -0.07%     
==========================================
  Files         462      463       +1     
  Lines       37082    37101      +19     
==========================================
- Hits        28138    28129       -9     
- Misses       7020     7040      +20     
- Partials     1924     1932       +8     
```


| [Files](https://app.codecov.io/gh/kopia/kopia/pull/3356?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia) | Coverage Δ | |
|---|---|---|
| [cli/command\_index\_inspect.go](https://app.codecov.io/gh/kopia/kopia/pull/3356?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-Y2xpL2NvbW1hbmRfaW5kZXhfaW5zcGVjdC5nbw==) | `80.55% <100.00%> (ø)` | |
| [cli/json\_output.go](https://app.codecov.io/gh/kopia/kopia/pull/3356?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-Y2xpL2pzb25fb3V0cHV0Lmdv) | `76.00% <ø> (-0.63%)` | :arrow_down: |
| [repo/api\_server\_repository.go](https://app.codecov.io/gh/kopia/kopia/pull/3356?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-cmVwby9hcGlfc2VydmVyX3JlcG9zaXRvcnkuZ28=) | `69.43% <100.00%> (ø)` | |
| [repo/content/committed\_content\_index.go](https://app.codecov.io/gh/kopia/kopia/pull/3356?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-cmVwby9jb250ZW50L2NvbW1pdHRlZF9jb250ZW50X2luZGV4Lmdv) | `79.35% <100.00%> (ø)` | |
| [repo/content/content\_index\_recovery.go](https://app.codecov.io/gh/kopia/kopia/pull/3356?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-cmVwby9jb250ZW50L2NvbnRlbnRfaW5kZXhfcmVjb3ZlcnkuZ28=) | `59.39% <100.00%> (ø)` | |
| [repo/content/content\_manager\_indexes.go](https://app.codecov.io/gh/kopia/kopia/pull/3356?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-cmVwby9jb250ZW50L2NvbnRlbnRfbWFuYWdlcl9pbmRleGVzLmdv) | `72.72% <100.00%> (ø)` | |
| [repo/content/content\_prefetch.go](https://app.codecov.io/gh/kopia/kopia/pull/3356?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-cmVwby9jb250ZW50L2NvbnRlbnRfcHJlZmV0Y2guZ28=) | `87.50% <100.00%> (ø)` | |
| [repo/content/index/index.go](https://app.codecov.io/gh/kopia/kopia/pull/3356?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-cmVwby9jb250ZW50L2luZGV4L2luZGV4Lmdv) | `84.61% <ø> (ø)` | |
| [repo/content/index/index\_builder.go](https://app.codecov.io/gh/kopia/kopia/pull/3356?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-cmVwby9jb250ZW50L2luZGV4L2luZGV4X2J1aWxkZXIuZ28=) | `78.80% <100.00%> (+0.14%)` | :arrow_up: |
| [repo/content/index/index\_v1.go](https://app.codecov.io/gh/kopia/kopia/pull/3356?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia#diff-cmVwby9jb250ZW50L2luZGV4L2luZGV4X3YxLmdv) | `75.00% <100.00%> (-1.18%)` | :arrow_down: |
| ... and [9 more](https://app.codecov.io/gh/kopia/kopia/pull/3356?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia) | |

... and [13 files with indirect coverage changes](https://app.codecov.io/gh/kopia/kopia/pull/3356/indirect-changes?src=pr&el=tree-more&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://app.codecov.io/gh/kopia/kopia/pull/3356?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kopia).

jkowalski: (rebased to lastest master)

Patch:
diff --git a/.github/workflows/ossf-scorecard.yml b/.github/workflows/ossf-scorecard.yml
index 8398ae14..ba0bc234 100644
--- a/.github/workflows/ossf-scorecard.yml
+++ b/.github/workflows/ossf-scorecard.yml
@@ -31,7 +31,7 @@ jobs:
           persist-credentials: false
       -
         name: "Run analysis"
-        uses: ossf/scorecard-action@08b4669551908b1024bb425080c797723083c031 # v2.2.0
+        uses: ossf/scorecard-action@483ef80eb98fb506c348f7d62e28055e49fe2398 # v2.3.0
         with:
           results_file: results.sarif
           results_format: sarif
@@ -39,7 +39,7 @@ jobs:
       -
         # Upload the results to GitHub's code scanning dashboard.
         name: "Upload to results to dashboard"
-        uses: github/codeql-action/upload-sarif@ddccb873888234080b77e9bc2d4764d5ccaaccf9 # v2.21.9
+        uses: github/codeql-action/upload-sarif@fdcae64e1484d349b3366718cdfef3d404390e85 # v2.22.1
         with:
           sarif_file: results.sarif
       -
diff --git a/cli/command_index_inspect.go b/cli/command_index_inspect.go
index f2ba3178..e25c6db4 100644
--- a/cli/command_index_inspect.go
+++ b/cli/command_index_inspect.go
@@ -169,7 +169,7 @@ func (c *commandIndexInspect) inspectSingleIndexBlob(ctx context.Context, rep re
 	}
 
 	for _, ent := range entries {
-		output <- indexBlobPlusContentInfo{bm, content.ToInfoStruct(ent)}
+		output <- indexBlobPlusContentInfo{bm, ent}
 	}
 
 	return nil
diff --git a/cli/command_repository_upgrade.go b/cli/command_repository_upgrade.go
index 2905a70b..fe1a3830 100644
--- a/cli/command_repository_upgrade.go
+++ b/cli/command_repository_upgrade.go
@@ -13,7 +13,6 @@
 	"github.com/kopia/kopia/internal/gather"
 	"github.com/kopia/kopia/repo"
 	"github.com/kopia/kopia/repo/content"
-	"github.com/kopia/kopia/repo/content/index"
 	"github.com/kopia/kopia/repo/content/indexblob"
 	"github.com/kopia/kopia/repo/format"
 )
@@ -92,14 +91,14 @@ func (c *commandRepositoryUpgrade) setup(svc advancedAppServices, parent command
 }
 
 // assign store the info struct in a map that can be used to compare indexes.
-func assign(iif content.Info, i int, m map[content.ID][2]index.Info) {
+func assign(iif content.Info, i int, m map[content.ID][2]content.Info) {
 	v := m[iif.GetContentID()]
 	v[i] = iif
 	m[iif.GetContentID()] = v
 }
 
 // loadIndexBlobs load index blobs into indexEntries map.  indexEntries map will allow comparison betweel two indexes (index at which == 0 and index at which == 1).
-func loadIndexBlobs(ctx context.Context, indexEntries map[content.ID][2]index.Info, sm *content.SharedManager, which int, indexBlobInfos []indexblob.Metadata) error {
+func loadIndexBlobs(ctx context.Context, indexEntries map[content.ID][2]content.Info, sm *content.SharedManager, which int, indexBlobInfos []indexblob.Metadata) error {
 	d := gather.WriteBuffer{}
 
 	for _, indexBlobInfo := range indexBlobInfos {
@@ -121,7 +120,7 @@ func loadIndexBlobs(ctx context.Context, indexEntries map[content.ID][2]index.In
 // validateAction returns an error if the new V1 index blob content does not match the source V0 index blob content.
 // This is used to check that the upgraded index (V1 index) reflects the content of the old V0 index.
 func (c *commandRepositoryUpgrade) validateAction(ctx context.Context, rep repo.DirectRepositoryWriter) error {
-	indexEntries := map[content.ID][2]index.Info{}
+	indexEntries := map[content.ID][2]content.Info{}
 
 	sm := rep.ContentManager().SharedManager
 
@@ -155,20 +154,23 @@ func (c *commandRepositoryUpgrade) validateAction(ctx context.Context, rep repo.
 
 	var msgs []string // a place to keep messages from the index comparison process
 
+	var zeroInfo content.Info
+
 	// both indexes will have matching contentiDs with matching indexInfo structures.
+	//nolint:gocritic
 	for contentID, indexEntryPairs := range indexEntries {
 		iep0 := indexEntryPairs[0] // first entry of index entry pair
 		iep1 := indexEntryPairs[1] // second entry of index entry pair
 
 		// check that both the new and old indexes have entries for the same content
-		if iep0 != nil && iep1 != nil {
+		if iep0 != zeroInfo && iep1 != zeroInfo {
 			// this is the happy-path, check the entries.  any problems found will be added to msgs
 			msgs = append(msgs, CheckIndexInfo(iep0, iep1)...)
 			continue
 		}
 
 		// one of iep0 or iep1 are nil .. find out which one and add an appropriate message.
-		if iep0 != nil {
+		if iep0 != zeroInfo {
 			msgs = append(msgs, fmt.Sprintf("lop-sided index entries for contentID %q at blob %q", contentID, iep0.GetPackBlobID()))
 			continue
 		}
@@ -194,7 +196,7 @@ func (c *commandRepositoryUpgrade) validateAction(ctx context.Context, rep repo.
 }
 
 // CheckIndexInfo compare two index infos.  If a mismatch exists, return an error with diagnostic information.
-func CheckIndexInfo(i0, i1 index.Info) []string {
+func CheckIndexInfo(i0, i1 content.Info) []string {
 	var q []string
 
 	switch {
diff --git a/cli/json_output.go b/cli/json_output.go
index 975981df..4649959d 100644
--- a/cli/json_output.go
+++ b/cli/json_output.go
@@ -7,7 +7,6 @@
 
 	"github.com/alecthomas/kingpin/v2"
 
-	"github.com/kopia/kopia/repo/content"
 	"github.com/kopia/kopia/snapshot"
 )
 
@@ -54,8 +53,6 @@ func (c *jsonOutput) cleanupSnapshotManifestListForJSON(manifests []*snapshot.Ma
 
 func (c *jsonOutput) cleanupForJSON(v interface{}) interface{} {
 	switch v := v.(type) {
-	case content.Info:
-		return content.ToInfoStruct(v)
 	case *snapshot.Manifest:
 		return c.cleanupSnapshotManifestForJSON(v)
 	case []*snapshot.Manifest:
diff --git a/go.mod b/go.mod
index a03c7a16..065475bc 100644
--- a/go.mod
+++ b/go.mod
@@ -4,7 +4,7 @@ go 1.21
 
 require (
 	cloud.google.com/go/storage v1.33.0
-	github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.2
+	github.com/Azure/azure-sdk-for-go/sdk/azcore v1.8.0
 	github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.1
 	github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.1.0
 	github.com/Azure/azure-storage-blob-go v0.15.0
@@ -40,7 +40,7 @@ require (
 	github.com/pkg/profile v1.7.0
 	github.com/pkg/sftp v1.13.6
 	github.com/prometheus/client_golang v1.17.0
-	github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16
+	github.com/prometheus/client_model v0.5.0
 	github.com/prometheus/common v0.44.0
 	github.com/sanity-io/litter v1.5.5
 	github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
@@ -55,16 +55,16 @@ require (
 	go.opentelemetry.io/otel/trace v1.19.0
 	go.uber.org/multierr v1.11.0
 	go.uber.org/zap v1.26.0
-	golang.org/x/crypto v0.13.0
+	golang.org/x/crypto v0.14.0
 	golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
-	golang.org/x/mod v0.12.0
-	golang.org/x/net v0.15.0
-	golang.org/x/oauth2 v0.12.0
-	golang.org/x/sync v0.3.0
-	golang.org/x/sys v0.12.0
-	golang.org/x/term v0.12.0
+	golang.org/x/mod v0.13.0
+	golang.org/x/net v0.17.0
+	golang.org/x/oauth2 v0.13.0
+	golang.org/x/sync v0.4.0
+	golang.org/x/sys v0.13.0
+	golang.org/x/term v0.13.0
 	golang.org/x/text v0.13.0
-	google.golang.org/api v0.143.0
+	google.golang.org/api v0.146.0
 	google.golang.org/grpc v1.58.2
 	google.golang.org/protobuf v1.31.0
 	gopkg.in/kothar/go-backblaze.v0 v0.0.0-20210124194846-35409b867216
diff --git a/go.sum b/go.sum
index d720ac6a..100ed40b 100644
--- a/go.sum
+++ b/go.sum
@@ -11,8 +11,8 @@ cloud.google.com/go/storage v1.33.0 h1:PVrDOkIC8qQVa1P3SXGpQvfuJhN2LHOoyZvWs8D2X
 cloud.google.com/go/storage v1.33.0/go.mod h1:Hhh/dogNRGca7IWv1RC2YqEn0c0G77ctA/OxflYkiD8=
 github.com/Azure/azure-pipeline-go v0.2.3 h1:7U9HBg1JFK3jHl5qmo4CTZKFTVgMwdFHMVtCdfBE21U=
 github.com/Azure/azure-pipeline-go v0.2.3/go.mod h1:x841ezTBIMG6O3lAcl8ATHnsOPVl2bqk7S3ta6S6u4k=
-github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.2 h1:t5+QXLCK9SVi0PPdaY0PrFvYUo24KwA0QwxnaHRSVd4=
-github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.2/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.8.0 h1:9kDVnTz3vbfweTqAUmk/a/pH5pWFCHtvRpHYC0G/dcA=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.8.0/go.mod h1:3Ug6Qzto9anB6mGlEdgYMDF5zHQ+wwhEaYR4s17PHMw=
 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.1 h1:LNHhpdK7hzUcx/k1LIcuh5k7k1LGIWLQfCjaneSj7Fc=
 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.1/go.mod h1:uE9zaUfEQT/nbQjVi2IblCG9iaLtZsuYZ8ne+PuQ02M=
 github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 h1:sXr+ck84g/ZlZUOZiNELInmMgOsuGwdjjVkEIde0OtY=
@@ -256,8 +256,8 @@ github.com/pquerna/ffjson v0.0.0-20190930134022-aa0246cd15f7/go.mod h1:YARuvh7BU
 github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
 github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 h1:v7DLqVdK4VrYkVD5diGdl4sxJurKJEMnODWRJlxV9oM=
-github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
+github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
+github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
 github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
 github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
 github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI=
@@ -330,8 +330,8 @@ golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPh
 golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
-golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
-golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
+golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
+golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
 golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
@@ -339,8 +339,8 @@ golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTk
 golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
 golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
-golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
-golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY=
+golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -354,19 +354,19 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
 golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
-golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
-golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
+golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
+golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
-golang.org/x/oauth2 v0.12.0 h1:smVPGxink+n1ZI5pkQa8y6fZT0RW0MgCO5bFpepy4B4=
-golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4=
+golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY=
+golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
-golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
+golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ=
+golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -384,13 +384,13 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
-golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
+golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU=
-golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
+golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
+golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -410,8 +410,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/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.143.0 h1:o8cekTkqhywkbZT6p1UHJPZ9+9uuCAJs/KYomxZB8fA=
-google.golang.org/api v0.143.0/go.mod h1:FoX9DO9hT7DLNn97OuoZAGSDuNAXdJRuGK98rSUgurk=
+google.golang.org/api v0.146.0 h1:9aBYT4vQXt9dhCuLNfwfd3zpwu8atg0yPkjBymwSrOM=
+google.golang.org/api v0.146.0/go.mod h1:OARJqIfoYjXJj4C1AiBSXYZt03qsoz8FQYU6fBEfrHM=
 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=
diff --git a/repo/api_server_repository.go b/repo/api_server_repository.go
index f4e44a99..07173b37 100644
--- a/repo/api_server_repository.go
+++ b/repo/api_server_repository.go
@@ -186,13 +186,13 @@ func (r *apiServerRepository) NewWriter(ctx context.Context, opt WriteSessionOpt
 }
 
 func (r *apiServerRepository) ContentInfo(ctx context.Context, contentID content.ID) (content.Info, error) {
-	var bi content.InfoStruct
+	var bi content.Info
 
 	if err := r.cli.Get(ctx, "contents/"+contentID.String()+"?info=1", content.ErrContentNotFound, &bi); err != nil {
-		return nil, errors.Wrap(err, "ContentInfo")
+		return content.Info{}, errors.Wrap(err, "ContentInfo")
 	}
 
-	return &bi, nil
+	return bi, nil
 }
 
 func (r *apiServerRepository) GetContent(ctx context.Context, contentID content.ID) ([]byte, error) {
diff --git a/repo/content/committed_content_index.go b/repo/content/committed_content_index.go
index 902c999c..ef0ae5eb 100644
--- a/repo/content/committed_content_index.go
+++ b/repo/content/committed_content_index.go
@@ -66,20 +66,20 @@ func (c *committedContentIndex) getContent(contentID ID) (Info, error) {
 	info, err := c.merged.GetInfo(contentID)
 	if info != nil {
 		if shouldIgnore(info, c.deletionWatermark) {
-			return nil, ErrContentNotFound
+			return index.Info{}, ErrContentNotFound
 		}
 
-		return info, nil
+		return index.ToInfoStruct(info), nil
 	}
 
 	if err == nil {
-		return nil, ErrContentNotFound
+		return index.Info{}, ErrContentNotFound
 	}
 
-	return nil, errors.Wrap(err, "error getting content info from index")
+	return index.Info{}, errors.Wrap(err, "error getting content info from index")
 }
 
-func shouldIgnore(id Info, deletionWatermark time.Time) bool {
+func shouldIgnore(id index.InfoReader, deletionWatermark time.Time) bool {
 	if !id.GetDeleted() {
 		return false
 	}
@@ -131,12 +131,12 @@ func (c *committedContentIndex) listContents(r IDRange, cb func(i Info) error) e
 	c.mu.RUnlock()
 
 	//nolint:wrapcheck
-	return m.Iterate(r, func(i Info) error {
+	return m.Iterate(r, func(i index.InfoReader) error {
 		if shouldIgnore(i, deletionWatermark) {
 			return nil
 		}
 
-		return cb(i)
+		return cb(index.ToInfoStruct(i))
 	})
 }
 
@@ -257,8 +257,8 @@ func (c *committedContentIndex) combineSmallIndexes(m index.Merged) (index.Merge
 	b := index.Builder{}
 
 	for _, ndx := range toMerge {
-		if err := ndx.Iterate(index.AllIDs, func(i Info) error {
-			b.Add(i)
+		if err := ndx.Iterate(index.AllIDs, func(i index.InfoReader) error {
+			b.Add(index.ToInfoStruct(i))
 			return nil
 		}); err != nil {
 			return nil, errors.Wrap(err, "unable to iterate index entries")
diff --git a/repo/content/content_index_recovery.go b/repo/content/content_index_recovery.go
index 1de77e54..5adf99ab 100644
--- a/repo/content/content_index_recovery.go
+++ b/repo/content/content_index_recovery.go
@@ -29,7 +29,7 @@ func (bm *WriteManager) RecoverIndexFromPackBlob(ctx context.Context, packFile b
 
 	var recovered []Info
 
-	err = ndx.Iterate(index.AllIDs, func(i Info) error {
+	err = ndx.Iterate(index.AllIDs, func(i index.InfoReader) error {
 		// 'i' is ephemeral and will depend on temporary buffers which
 		// won't be available when this function returns, we need to
 		// convert it to durable struct.
diff --git a/repo/content/content_manager.go b/repo/content/content_manager.go
index 4d4b5c6b..de376a4d 100644
--- a/repo/content/content_manager.go
+++ b/repo/content/content_manager.go
@@ -186,11 +186,19 @@ func (bm *WriteManager) deletePreexistingContent(ctx context.Context, ci Info) e
 		return errors.Wrap(err, "unable to create pack")
 	}
 
-	pp.currentPackItems[ci.GetContentID()] = &deletedInfo{ci, bm.contentWriteTime(ci.GetTimestampSeconds())}
+	pp.currentPackItems[ci.GetContentID()] = deletedInfo(ci, bm.contentWriteTime(ci.GetTimestampSeconds()))
 
 	return nil
 }
 
+func deletedInfo(is Info, deletedTime int64) Info {
+	// clone and set deleted time
+	is.Deleted = true
+	is.TimestampSeconds = deletedTime
+
+	return is
+}
+
 // contentWriteTime returns content write time for new content
 // by computing max(timeNow().Unix(), previousUnixTimeSeconds + 1).
 func (bm *WriteManager) contentWriteTime(previousUnixTimeSeconds int64) int64 {
@@ -202,19 +210,6 @@ func (bm *WriteManager) contentWriteTime(previousUnixTimeSeconds int64) int64 {
 	return previousUnixTimeSeconds + 1
 }
 
-type deletedInfo struct {
-	Info
-	deletedTime int64
-}
-
-func (d *deletedInfo) GetDeleted() bool {
-	return true
-}
-
-func (d *deletedInfo) GetTimestampSeconds() int64 {
-	return d.deletedTime
-}
-
 func (bm *WriteManager) maybeFlushBasedOnTimeUnlocked(ctx context.Context) error {
 	bm.lock()
 	shouldFlush := bm.timeNow().After(bm.flushPackIndexesAfter)
@@ -310,7 +305,7 @@ func (bm *WriteManager) addToPackUnlocked(ctx context.Context, contentID ID, dat
 		return errors.Wrap(err, "unable to create pending pack")
 	}
 
-	info := &InfoStruct{
+	info := Info{
 		Deleted:          isDeleted,
 		ContentID:        contentID,
 		PackBlobID:       pp.packBlobID,
@@ -665,11 +660,11 @@ func (bm *WriteManager) getContentDataAndInfo(ctx context.Context, contentID ID,
 
 	pp, bi, err := bm.getContentInfoReadLocked(ctx, contentID)
 	if err != nil {
-		return nil, err
+		return Info{}, err
 	}
 
 	if err := bm.getContentDataReadLocked(ctx, pp, bi, output); err != nil {
-		return nil, err
+		return Info{}, err
 	}
 
 	return bi, nil
@@ -885,7 +880,7 @@ func (bm *WriteManager) getOverlayContentInfoReadLocked(contentID ID) (*pendingP
 		return nil, ci, true
 	}
 
-	return nil, nil, false
+	return nil, Info{}, false
 }
 
 // +checklocksread:bm.mu
@@ -896,7 +891,7 @@ func (bm *WriteManager) getContentInfoReadLocked(ctx context.Context, contentID
 
 	// see if the content existed before
 	if err := bm.maybeRefreshIndexes(ctx); err != nil {
-		return nil, nil, err
+		return nil, Info{}, err
 	}
 
 	info, err := bm.committedContents.getContent(contentID)
@@ -912,7 +907,7 @@ func (bm *WriteManager) ContentInfo(ctx context.Context, contentID ID) (Info, er
 	_, bi, err := bm.getContentInfoReadLocked(ctx, contentID)
 	if err != nil {
 		bm.log.Debugf("ContentInfo(%q) - error %v", contentID, err)
-		return nil, err
+		return Info{}, err
 	}
 
 	return bi, err
diff --git a/repo/content/content_manager_indexes.go b/repo/content/content_manager_indexes.go
index b55e3ee9..07742ffc 100644
--- a/repo/content/content_manager_indexes.go
+++ b/repo/content/content_manager_indexes.go
@@ -77,7 +77,7 @@ func ParseIndexBlob(blobID blob.ID, encrypted gather.Bytes, crypter blobcrypto.C
 
 	var results []Info
 
-	err = ndx.Iterate(index.AllIDs, func(i Info) error {
+	err = ndx.Iterate(index.AllIDs, func(i index.InfoReader) error {
 		results = append(results, index.ToInfoStruct(i))
 		return nil
 	})
diff --git a/repo/content/content_prefetch.go b/repo/content/content_prefetch.go
index 114f415f..453cfd35 100644
--- a/repo/content/content_prefetch.go
+++ b/repo/content/content_prefetch.go
@@ -68,7 +68,7 @@ func (bm *WriteManager) PrefetchContents(ctx context.Context, contentIDs []ID, h
 
 	for _, ci := range contentIDs {
 		_, bi, _ := bm.getContentInfoReadLocked(ctx, ci)
-		if bi == nil {
+		if bi == (Info{}) {
 			continue
 		}
 
diff --git a/repo/content/index/index.go b/repo/content/index/index.go
index 6e872db7..e9f6c911 100644
--- a/repo/content/index/index.go
+++ b/repo/content/index/index.go
@@ -18,10 +18,10 @@
 type Index interface {
 	io.Closer
 	ApproximateCount() int
-	GetInfo(contentID ID) (Info, error)
+	GetInfo(contentID ID) (InfoReader, error)
 
 	// invoked the provided callback for all entries such that entry.ID >= startID and entry.ID < endID
-	Iterate(r IDRange, cb func(Info) error) error
+	Iterate(r IDRange, cb func(InfoReader) error) error
 }
 
 // Open reads an Index from a given reader. The caller must call Close() when the index is no longer used.
diff --git a/repo/content/index/index_builder.go b/repo/content/index/index_builder.go
index afba2a9d..ec541c77 100644
--- a/repo/content/index/index_builder.go
+++ b/repo/content/index/index_builder.go
@@ -37,7 +37,8 @@ func (b Builder) Clone() Builder {
 func (b Builder) Add(i Info) {
 	cid := i.GetContentID()
 
-	if contentInfoGreaterThan(i, b[cid]) {
+	old, found := b[cid]
+	if !found || contentInfoGreaterThanStruct(i, old) {
 		b[cid] = i
 	}
 }
diff --git a/repo/content/index/index_v1.go b/repo/content/index/index_v1.go
index 5237d285..f9f527df 100644
--- a/repo/content/index/index_v1.go
+++ b/repo/content/index/index_v1.go
@@ -106,7 +106,7 @@ func (e indexEntryInfoV1) GetEncryptionKeyID() byte {
 	return 0
 }
 
-var _ Info = indexEntryInfoV1{}
+var _ InfoReader = indexEntryInfoV1{}
 
 type indexV1 struct {
 	hdr    v1HeaderInfo
@@ -125,7 +125,7 @@ func (b *indexV1) ApproximateCount() int {
 // Iterate invokes the provided callback function for a range of contents in the index, sorted alphabetically.
 // The iteration ends when the callback returns an error, which is propagated to the caller or when
 // all contents have been visited.
-func (b *indexV1) Iterate(r IDRange, cb func(Info) error) error {
+func (b *indexV1) Iterate(r IDRange, cb func(InfoReader) error) error {
 	startPos, err := b.findEntryPosition(r.StartID)
 	if err != nil {
 		return errors.Wrap(err, "could not find starting position")
@@ -241,7 +241,7 @@ func (b *indexV1) findEntry(output []byte, contentID ID) ([]byte, error) {
 }
 
 // GetInfo returns information about a given content. If a content is not found, nil is returned.
-func (b *indexV1) GetInfo(contentID ID) (Info, error) {
+func (b *indexV1) GetInfo(contentID ID) (InfoReader, error) {
 	var entryBuf [v1MaxEntrySize]byte
 
 	e, err := b.findEntry(entryBuf[:0], contentID)
@@ -256,7 +256,7 @@ func (b *indexV1) GetInfo(contentID ID) (Info, error) {
 	return b.entryToInfo(contentID, e)
 }
 
-func (b *indexV1) entryToInfo(contentID ID, entryData []byte) (Info, error) {
+func (b *indexV1) entryToInfo(contentID ID, entryData []byte) (InfoReader, error) {
 	if len(entryData) != v1EntryLength {
 		return nil, errors.Errorf("invalid entry length: %v", len(entryData))
 	}
diff --git a/repo/content/index/index_v2.go b/repo/content/index/index_v2.go
index 31f47baa..c170bfcc 100644
--- a/repo/content/index/index_v2.go
+++ b/repo/content/index/index_v2.go
@@ -224,7 +224,7 @@ func (e indexV2EntryInfo) Timestamp() time.Time {
 	return time.Unix(e.GetTimestampSeconds(), 0)
 }
 
-var _ Info = indexV2EntryInfo{}
+var _ InfoReader = indexV2EntryInfo{}
 
 type v2HeaderInfo struct {
 	version       int
@@ -277,7 +277,7 @@ func (b *indexV2) ApproximateCount() int {
 // Iterate invokes the provided callback function for a range of contents in the index, sorted alphabetically.
 // The iteration ends when the callback returns an error, which is propagated to the caller or when
 // all contents have been visited.
-func (b *indexV2) Iterate(r IDRange, cb func(Info) error) error {
+func (b *indexV2) Iterate(r IDRange, cb func(InfoReader) error) error {
 	startPos, err := b.findEntryPosition(r.StartID)
 	if err != nil {
 		return errors.Wrap(err, "could not find starting position")
@@ -389,7 +389,7 @@ func (b *indexV2) findEntry(contentID ID) ([]byte, error) {
 }
 
 // GetInfo returns information about a given content. If a content is not found, nil is returned.
-func (b *indexV2) GetInfo(contentID ID) (Info, error) {
+func (b *indexV2) GetInfo(contentID ID) (InfoReader, error) {
 	e, err := b.findEntry(contentID)
 	if err != nil {
 		return nil, err
@@ -402,7 +402,7 @@ func (b *indexV2) GetInfo(contentID ID) (Info, error) {
 	return b.entryToInfo(contentID, e)
 }
 
-func (b *indexV2) entryToInfo(contentID ID, entryData []byte) (Info, error) {
+func (b *indexV2) entryToInfo(contentID ID, entryData []byte) (InfoReader, error) {
 	if len(entryData) < v2EntryMinLength {
 		return nil, errors.Errorf("invalid entry length: %v", len(entryData))
 	}
diff --git a/repo/content/index/info.go b/repo/content/index/info.go
index 304f316d..6e5b36bd 100644
--- a/repo/content/index/info.go
+++ b/repo/content/index/info.go
@@ -7,10 +7,10 @@
 	"github.com/kopia/kopia/repo/compression"
 )
 
-// Info is an information about a single piece of content managed by Manager.
+// InfoReader is an information about a single piece of content managed by Manager.
 //
 //nolint:interfacebloat
-type Info interface {
+type InfoReader interface {
 	GetContentID() ID
 	GetPackBlobID() blob.ID
 	GetTimestampSeconds() int64
@@ -24,62 +24,58 @@ type Info interface {
 	GetEncryptionKeyID() byte
 }
 
-// InfoStruct is an implementation of Info based on a structure.
-type InfoStruct struct {
-	ContentID           ID                   `json:"contentID"`
+// Info is an implementation of Info based on a structure.
+type Info struct {
 	PackBlobID          blob.ID              `json:"packFile,omitempty"`
 	TimestampSeconds    int64                `json:"time"`
 	OriginalLength      uint32               `json:"originalLength"`
 	PackedLength        uint32               `json:"length"`
 	PackOffset          uint32               `json:"packOffset,omitempty"`
+	CompressionHeaderID compression.HeaderID `json:"compression,omitempty"`
+	ContentID           ID                   `json:"contentID"`
 	Deleted             bool                 `json:"deleted"`
 	FormatVersion       byte                 `json:"formatVersion"`
-	CompressionHeaderID compression.HeaderID `json:"compression,omitempty"`
 	EncryptionKeyID     byte                 `json:"encryptionKeyID,omitempty"`
 }
 
 // GetContentID implements the Info interface.
-func (i *InfoStruct) GetContentID() ID { return i.ContentID }
+func (i Info) GetContentID() ID { return i.ContentID }
 
 // GetPackBlobID implements the Info interface.
-func (i *InfoStruct) GetPackBlobID() blob.ID { return i.PackBlobID }
+func (i Info) GetPackBlobID() blob.ID { return i.PackBlobID }
 
 // GetTimestampSeconds implements the Info interface.
-func (i *InfoStruct) GetTimestampSeconds() int64 { return i.TimestampSeconds }
+func (i Info) GetTimestampSeconds() int64 { return i.TimestampSeconds }
 
 // GetOriginalLength implements the Info interface.
-func (i *InfoStruct) GetOriginalLength() uint32 { return i.OriginalLength }
+func (i Info) GetOriginalLength() uint32 { return i.OriginalLength }
 
 // GetPackedLength implements the Info interface.
-func (i *InfoStruct) GetPackedLength() uint32 { return i.PackedLength }
+func (i Info) GetPackedLength() uint32 { return i.PackedLength }
 
 // GetPackOffset implements the Info interface.
-func (i *InfoStruct) GetPackOffset() uint32 { return i.PackOffset }
+func (i Info) GetPackOffset() uint32 { return i.PackOffset }
 
 // GetDeleted implements the Info interface.
-func (i *InfoStruct) GetDeleted() bool { return i.Deleted }
+func (i Info) GetDeleted() bool { return i.Deleted }
 
 // GetFormatVersion implements the Info interface.
-func (i *InfoStruct) GetFormatVersion() byte { return i.FormatVersion }
+func (i Info) GetFormatVersion() byte { return i.FormatVersion }
 
 // GetCompressionHeaderID implements the Info interface.
-func (i *InfoStruct) GetCompressionHeaderID() compression.HeaderID { return i.CompressionHeaderID }
+func (i Info) GetCompressionHeaderID() compression.HeaderID { return i.CompressionHeaderID }
 
 // GetEncryptionKeyID implements the Info interface.
-func (i *InfoStruct) GetEncryptionKeyID() byte { return i.EncryptionKeyID }
+func (i Info) GetEncryptionKeyID() byte { return i.EncryptionKeyID }
 
 // Timestamp implements the Info interface.
-func (i *InfoStruct) Timestamp() time.Time {
+func (i Info) Timestamp() time.Time {
 	return time.Unix(i.GetTimestampSeconds(), 0)
 }
 
-// ToInfoStruct converts the provided Info to *InfoStruct.
-func ToInfoStruct(i Info) *InfoStruct {
-	if is, ok := i.(*InfoStruct); ok {
-		return is
-	}
-
-	return &InfoStruct{
+// ToInfoStruct converts the provided Info to InfoStruct.
+func ToInfoStruct(i InfoReader) Info {
+	return Info{
 		ContentID:           i.GetContentID(),
 		PackBlobID:          i.GetPackBlobID(),
 		TimestampSeconds:    i.GetTimestampSeconds(),
diff --git a/repo/content/index/merged.go b/repo/content/index/merged.go
index 20e0a61f..d96e9703 100644
--- a/repo/content/index/merged.go
+++ b/repo/content/index/merged.go
@@ -33,17 +33,23 @@ func (m Merged) Close() error {
 	return errors.Wrap(err, "closing index shards")
 }
 
-func contentInfoGreaterThan(a, b Info) bool {
-	if b == nil {
-		// everyrhing is greater than nil
-		return true
+func contentInfoGreaterThan(a, b InfoReader) bool {
+	if l, r := a.GetTimestampSeconds(), b.GetTimestampSeconds(); l != r {
+		// different timestamps, higher one wins
+		return l > r
 	}
 
-	if a == nil {
-		// nil is less than everything
-		return false
+	if l, r := a.GetDeleted(), b.GetDeleted(); l != r {
+		// non-deleted is greater than deleted.
+		return !a.GetDeleted()
 	}
 
+	// both same time, both deleted, we must ensure we always resolve to the same pack blob.
+	// since pack blobs are random and unique, simple lexicographic ordering will suffice.
+	return a.GetPackBlobID() > b.GetPackBlobID()
+}
+
+func contentInfoGreaterThanStruct(a, b Info) bool {
 	if l, r := a.GetTimestampSeconds(), b.GetTimestampSeconds(); l != r {
 		// different timestamps, higher one wins
 		return l > r
@@ -60,8 +66,8 @@ func contentInfoGreaterThan(a, b Info) bool {
 }
 
 // GetInfo returns information about a single content. If a content is not found, returns (nil,nil).
-func (m Merged) GetInfo(id ID) (Info, error) {
-	var best Info
+func (m Merged) GetInfo(id ID) (InfoReader, error) {
+	var best InfoReader
 
 	for _, ndx := range m {
 		i, err := ndx.GetInfo(id)
@@ -69,7 +75,7 @@ func (m Merged) GetInfo(id ID) (Info, error) {
 			return nil, errors.Wrapf(err, "error getting id %v from index shard", id)
 		}
 
-		if contentInfoGreaterThan(i, best) {
+		if i != nil && (best == nil || contentInfoGreaterThan(i, best)) {
 			best = i
 		}
 	}
@@ -78,8 +84,8 @@ func (m Merged) GetInfo(id ID) (Info, error) {
 }
 
 type nextInfo struct {
-	it Info
-	ch <-chan Info
+	it InfoReader
+	ch <-chan InfoReader
 }
 
 type nextInfoHeap []*nextInfo
@@ -107,14 +113,14 @@ func (h *nextInfoHeap) Pop() interface{} {
 	return x
 }
 
-func iterateChan(r IDRange, ndx Index, done chan bool, wg *sync.WaitGroup) <-chan Info {
-	ch := make(chan Info, 1)
+func iterateChan(r IDRange, ndx Index, done chan bool, wg *sync.WaitGroup) <-chan InfoReader {
+	ch := make(chan InfoReader, 1)
 
 	go func() {
 		defer wg.Done()
 		defer close(ch)
 
-		_ = ndx.Iterate(r, func(i Info) error {
+		_ = ndx.Iterate(r, func(i InfoReader) error {
 			select {
 			case <-done:
 				return errors.New("end of iteration")
@@ -129,7 +135,7 @@ func iterateChan(r IDRange, ndx Index, done chan bool, wg *sync.WaitGroup) <-cha
 
 // Iterate invokes the provided callback for all unique content IDs in the underlying sources until either
 // all contents have been visited or until an error is returned by the callback.
-func (m Merged) Iterate(r IDRange, cb func(i Info) error) error {
+func (m Merged) Iterate(r IDRange, cb func(i InfoReader) error) error {
 	var minHeap nextInfoHeap
 
 	done := make(chan bool)
@@ -152,7 +158,7 @@ func (m Merged) Iterate(r IDRange, cb func(i Info) error) error {
 	defer wg.Wait()
 	defer close(done)
 
-	var pendingItem Info
+	var pendingItem InfoReader
 
 	for len(minHeap) > 0 {
 		//nolint:forcetypeassert
@@ -165,7 +171,7 @@ func (m Merged) Iterate(r IDRange, cb func(i Info) error) error {
 			}
 
 			pendingItem = min.it
-		} else if contentInfoGreaterThan(min.it, pendingItem) {
+		} else if min.it != nil && contentInfoGreaterThan(min.it, pendingItem) {
 			pendingItem = min.it
 		}
 
diff --git a/repo/content/indexblob/index_blob_manager_v0.go b/repo/content/indexblob/index_blob_manager_v0.go
index 97382a0b..1f76f493 100644
--- a/repo/content/indexblob/index_blob_manager_v0.go
+++ b/repo/content/indexblob/index_blob_manager_v0.go
@@ -564,8 +564,8 @@ func addIndexBlobsToBuilder(ctx context.Context, enc *EncryptionManager, bld ind
 		return errors.Wrapf(err, "unable to open index blob %q", indexBlobID)
 	}
 
-	_ = ndx.Iterate(index.AllIDs, func(i index.Info) error {
-		bld.Add(i)
+	_ = ndx.Iterate(index.AllIDs, func(i index.InfoReader) error {
+		bld.Add(index.ToInfoStruct(i))
 		return nil
 	})
 
diff --git a/repo/content/info.go b/repo/content/info.go
index 79b83778..88e19ee2 100644
--- a/repo/content/info.go
+++ b/repo/content/info.go
@@ -13,12 +13,9 @@
 	// IDPrefix represents a content ID prefix (empty string or single character between 'g' and 'z').
 	IDPrefix = index.IDPrefix
 
-	// Info is an information about a single piece of content managed by Manager.
+	// Info describes a single piece of content.
 	Info = index.Info
 
-	// InfoStruct is an implementation of Info based on a structure.
-	InfoStruct = index.InfoStruct
-
 	// IDRange represents a range of IDs.
 	IDRange = index.IDRange
 )
@@ -28,11 +25,6 @@
 //nolint:gochecknoglobals
 var EmptyID = index.EmptyID
 
-// ToInfoStruct converts the provided Info to *InfoStruct.
-func ToInfoStruct(i Info) *InfoStruct {
-	return index.ToInfoStruct(i)
-}
-
 // IDFromHash creates and validates content ID from a prefix and hash.
 func IDFromHash(prefix IDPrefix, hash []byte) (ID, error) {
 	//nolint:wrapcheck
diff --git a/repo/grpc_repository_client.go b/repo/grpc_repository_client.go
index 81275eab..3f925850 100644
--- a/repo/grpc_repository_client.go
+++ b/repo/grpc_repository_client.go
@@ -592,10 +592,10 @@ func (r *grpcInnerSession) contentInfo(ctx context.Context, contentID content.ID
 		case *apipb.SessionResponse_GetContentInfo:
 			contentID, err := content.ParseID(rr.GetContentInfo.GetInfo().GetId())
 			if err != nil {
-				return nil, errors.Wrap(err, "invalid content ID")
+				return content.Info{}, errors.Wrap(err, "invalid content ID")
 			}
 
-			return &content.InfoStruct{
+			return content.Info{
 				ContentID:        contentID,
 				PackedLength:     rr.GetContentInfo.GetInfo().GetPackedLength(),
 				TimestampSeconds: rr.GetContentInfo.GetInfo().GetTimestampSeconds(),
@@ -607,11 +607,11 @@ func (r *grpcInnerSession) contentInfo(ctx context.Context, contentID content.ID
 			}, nil
 
 		default:
-			return nil, unhandledSessionResponse(resp)
+			return content.Info{}, unhandledSessionResponse(resp)
 		}
 	}
 
-	return nil, errNoSessionResponse()
+	return content.Info{}, errNoSessionResponse()
 }
 
 func errorFromSessionResponse(rr *apipb.ErrorResponse) error {
diff --git a/site/content/docs/Repositories/_index.md b/site/content/docs/Repositories/_index.md
index 2ca76514..1b969394 100644
--- a/site/content/docs/Repositories/_index.md
+++ b/site/content/docs/Repositories/_index.md
@@ -17,7 +17,7 @@ Kopia allows you to save your [encrypted](../features/#end-to-end-zero-knowledge
 * [Google Drive](#google-drive)
   * Kopia supports Google Drive natively and through Kopia's Rclone option (see below)
   * Native support for Google Drive in Kopia is currently experimental
-  * Native Google Drive support operates differently than Kopia's support for Google Drive through Rclone; you will not be able to use the two interchangably, so pick one
+  * Native Google Drive support operates differently than Kopia's support for Google Drive through Rclone; you will not be able to use the two interchangeably, so pick one
 * All remote servers or cloud storage that support [WebDAV](#webdav) 
 * All remote servers or cloud storage that support [SFTP](#sftp)
 * Some of the cloud storages supported by [Rclone](#rclone) 
@@ -166,8 +166,7 @@ Once you do all that, your repository should be created and you can start backin
 #### Creating a Repository
 
 There are three methods to create a `repository` for Google Cloud Storage: one that requires you to install Google Cloud SDK; the other method allows you to generate credentials without Google Cloud SDK; and the third method allows you to use Google Cloud Storage through Kopia's [S3 `repository` option](#amazon-s3-and-s3-compatible-cloud-storage):
-
-***Method #1: Installing Google Cloud SDK
+##### Method #1: Installing Google Cloud SDK
 
 1. Create a storage bucket in [Google Cloud Console](https://console.cloud.google.com/storage/)
 2. Install [Google Cloud SDK](https://cloud.google.com/sdk/)
@@ -187,7 +186,7 @@ There are also various other options (such as [actions](../advanced/actions/)) y
 
 You will be asked to enter the repository password that you want. Remember, this [password is used to encrypt your data](../faqs/#how-do-i-enable-encryption), so make sure it is a secure password!
 
-***Method #2: Creating a Service Account and Using the JSON Key File
+##### Method #2: Creating a Service Account and Using the JSON Key File
 
 1. Create a storage bucket in [Google Cloud Console](https://console.cloud.google.com/storage/)
 2. Create a Google Cloud Service Account that allows you to access your storage bucket. Directions are available on [Google Cloud's website](https://cloud.google.com/authentication/getting-started#create-service-account-console). Make sure to download the JSON key file for your service account and keep it safe.
@@ -202,7 +201,7 @@ There are also various other options (such as [actions](../advanced/actions/)) y
 
 You will be asked to enter the repository password that you want. Remember, this [password is used to encrypt your data](../faqs/#how-do-i-enable-encryption), so make sure it is a secure password!
 
-***Method #3: Enabling Amazon S3 Interoperability in Google Cloud Storage
+##### Method #3: Enabling Amazon S3 Interoperability in Google Cloud Storage
 
 1. Create a storage bucket in [Google Cloud Console](https://console.cloud.google.com/storage/)
 2. Go to [Settings and then Interoperability](https://console.cloud.google.com/storage/settings;tab=interoperability) in your Google Cloud Storage account


Test patch:
diff --git a/cli/command_repository_upgrade_test.go b/cli/command_repository_upgrade_test.go
index 165301a7..7ac1fe0f 100644
--- a/cli/command_repository_upgrade_test.go
+++ b/cli/command_repository_upgrade_test.go
@@ -8,7 +8,7 @@
 	"github.com/stretchr/testify/require"
 
 	"github.com/kopia/kopia/cli"
-	"github.com/kopia/kopia/repo/content/index"
+	"github.com/kopia/kopia/repo/content"
 	"github.com/kopia/kopia/repo/format"
 	"github.com/kopia/kopia/tests/testenv"
 )
@@ -298,120 +298,120 @@ func (s *formatSpecificTestSuite) TestRepositoryUpgradeStatusWhileLocked(t *test
 
 func TestRepositoryUpgrade_checkIndexInfo(t *testing.T) {
 	tcs := []struct {
-		indexInfo0   index.Info
-		indexInfo1   index.Info
+		indexInfo0   content.Info
+		indexInfo1   content.Info
 		expectRegexs []string
 	}{
 		{
-			indexInfo0:   &index.InfoStruct{PackBlobID: "a"},
-			indexInfo1:   &index.InfoStruct{PackBlobID: "a"},
+			indexInfo0:   content.Info{PackBlobID: "a"},
+			indexInfo1:   content.Info{PackBlobID: "a"},
 			expectRegexs: []string{},
 		},
 		{
-			indexInfo0: &index.InfoStruct{PackBlobID: "a"},
-			indexInfo1: &index.InfoStruct{PackBlobID: "b"},
+			indexInfo0: content.Info{PackBlobID: "a"},
+			indexInfo1: content.Info{PackBlobID: "b"},
 			expectRegexs: []string{
 				`do not match: "a", "b".*PackBlobID`,
 			},
 		},
 		{
-			indexInfo0:   &index.InfoStruct{TimestampSeconds: 1},
-			indexInfo1:   &index.InfoStruct{TimestampSeconds: 1},
+			indexInfo0:   content.Info{TimestampSeconds: 1},
+			indexInfo1:   content.Info{TimestampSeconds: 1},
 			expectRegexs: []string{},
 		},
 		{
-			indexInfo0: &index.InfoStruct{TimestampSeconds: 1},
-			indexInfo1: &index.InfoStruct{TimestampSeconds: 2},
+			indexInfo0: content.Info{TimestampSeconds: 1},
+			indexInfo1: content.Info{TimestampSeconds: 2},
 			expectRegexs: []string{
 				"do not match.*TimestampSeconds",
 			},
 		},
 		{
-			indexInfo0:   &index.InfoStruct{OriginalLength: 1},
-			indexInfo1:   &index.InfoStruct{OriginalLength: 1},
+			indexInfo0:   content.Info{OriginalLength: 1},
+			indexInfo1:   content.Info{OriginalLength: 1},
 			expectRegexs: []string{},
 		},
 		{
-			indexInfo0: &index.InfoStruct{OriginalLength: 1},
-			indexInfo1: &index.InfoStruct{OriginalLength: 2},
+			indexInfo0: content.Info{OriginalLength: 1},
+			indexInfo1: content.Info{OriginalLength: 2},
 			expectRegexs: []string{
 				"do not match.*OriginalLength",
 			},
 		},
 		{
-			indexInfo0:   &index.InfoStruct{PackedLength: 1},
-			indexInfo1:   &index.InfoStruct{PackedLength: 1},
+			indexInfo0:   content.Info{PackedLength: 1},
+			indexInfo1:   content.Info{PackedLength: 1},
 			expectRegexs: []string{},
 		},
 		{
-			indexInfo0: &index.InfoStruct{PackedLength: 1},
-			indexInfo1: &index.InfoStruct{PackedLength: 2},
+			indexInfo0: content.Info{PackedLength: 1},
+			indexInfo1: content.Info{PackedLength: 2},
 			expectRegexs: []string{
 				"do not match.*PackedLength",
 			},
 		},
 		{
-			indexInfo0:   &index.InfoStruct{PackOffset: 1},
-			indexInfo1:   &index.InfoStruct{PackOffset: 1},
+			indexInfo0:   content.Info{PackOffset: 1},
+			indexInfo1:   content.Info{PackOffset: 1},
 			expectRegexs: []string{},
 		},
 		{
-			indexInfo0: &index.InfoStruct{PackOffset: 1},
-			indexInfo1: &index.InfoStruct{PackOffset: 2},
+			indexInfo0: content.Info{PackOffset: 1},
+			indexInfo1: content.Info{PackOffset: 2},
 			expectRegexs: []string{
 				"do not match.*PackOffset",
 			},
 		},
 		{
-			indexInfo0:   &index.InfoStruct{Deleted: true},
-			indexInfo1:   &index.InfoStruct{Deleted: true},
+			indexInfo0:   content.Info{Deleted: true},
+			indexInfo1:   content.Info{Deleted: true},
 			expectRegexs: []string{},
 		},
 		{
-			indexInfo0: &index.InfoStruct{Deleted: false},
-			indexInfo1: &index.InfoStruct{Deleted: true},
+			indexInfo0: content.Info{Deleted: false},
+			indexInfo1: content.Info{Deleted: true},
 			expectRegexs: []string{
 				"do not match.*Deleted",
 			},
 		},
 		// simple logic error can make result of this false... so check
 		{
-			indexInfo0:   &index.InfoStruct{Deleted: false},
-			indexInfo1:   &index.InfoStruct{Deleted: false},
+			indexInfo0:   content.Info{Deleted: false},
+			indexInfo1:   content.Info{Deleted: false},
 			expectRegexs: []string{},
 		},
 		{
-			indexInfo0:   &index.InfoStruct{FormatVersion: 1},
-			indexInfo1:   &index.InfoStruct{FormatVersion: 1},
+			indexInfo0:   content.Info{FormatVersion: 1},
+			indexInfo1:   content.Info{FormatVersion: 1},
 			expectRegexs: []string{},
 		},
 		{
-			indexInfo0: &index.InfoStruct{FormatVersion: 1},
-			indexInfo1: &index.InfoStruct{FormatVersion: 2},
+			indexInfo0: content.Info{FormatVersion: 1},
+			indexInfo1: content.Info{FormatVersion: 2},
 			expectRegexs: []string{
 				"do not match.*FormatVersion",
 			},
 		},
 		{
-			indexInfo0:   &index.InfoStruct{CompressionHeaderID: 1},
-			indexInfo1:   &index.InfoStruct{CompressionHeaderID: 1},
+			indexInfo0:   content.Info{CompressionHeaderID: 1},
+			indexInfo1:   content.Info{CompressionHeaderID: 1},
 			expectRegexs: []string{},
 		},
 		{
-			indexInfo0: &index.InfoStruct{CompressionHeaderID: 1},
-			indexInfo1: &index.InfoStruct{CompressionHeaderID: 2},
+			indexInfo0: content.Info{CompressionHeaderID: 1},
+			indexInfo1: content.Info{CompressionHeaderID: 2},
 			expectRegexs: []string{
 				"do not match.*CompressionHeaderID",
 			},
 		},
 		{
-			indexInfo0:   &index.InfoStruct{EncryptionKeyID: 1},
-			indexInfo1:   &index.InfoStruct{EncryptionKeyID: 1},
+			indexInfo0:   content.Info{EncryptionKeyID: 1},
+			indexInfo1:   content.Info{EncryptionKeyID: 1},
 			expectRegexs: []string{},
 		},
 		{
-			indexInfo0: &index.InfoStruct{EncryptionKeyID: 1},
-			indexInfo1: &index.InfoStruct{EncryptionKeyID: 2},
+			indexInfo0: content.Info{EncryptionKeyID: 1},
+			indexInfo1: content.Info{EncryptionKeyID: 2},
 			expectRegexs: []string{
 				"do not match.*EncryptionKeyID",
 			},
diff --git a/cli/command_snapshot_fix_test.go b/cli/command_snapshot_fix_test.go
index c7cf904f..9f3d2c0e 100644
--- a/cli/command_snapshot_fix_test.go
+++ b/cli/command_snapshot_fix_test.go
@@ -386,14 +386,14 @@ func forgetContents(t *testing.T, env *testenv.CLITest, contentIDs ...string) {
 	env.RunAndExpectSuccess(t, append([]string{"blob", "rm"}, blobIDs...)...)
 }
 
-func mustGetContentMap(t *testing.T, env *testenv.CLITest) map[content.ID]content.InfoStruct {
+func mustGetContentMap(t *testing.T, env *testenv.CLITest) map[content.ID]content.Info {
 	t.Helper()
 
-	var contents1 []content.InfoStruct
+	var contents1 []content.Info
 
 	testutil.MustParseJSONLines(t, env.RunAndExpectSuccess(t, "content", "ls", "--json"), &contents1)
 
-	contentMap := map[content.ID]content.InfoStruct{}
+	contentMap := map[content.ID]content.Info{}
 	for _, v := range contents1 {
 		contentMap[v.ContentID] = v
 	}
diff --git a/internal/indextest/indextest.go b/internal/indextest/indextest.go
index 2ce529d5..2d3c9bec 100644
--- a/internal/indextest/indextest.go
+++ b/internal/indextest/indextest.go
@@ -62,7 +62,7 @@ func InfoDiff(i1, i2 index.Info, ignore ...string) []string {
 	// dear future reader, if this fails because the number of methods has changed,
 	// you need to add additional verification above.
 	//nolint:gomnd
-	if cnt := reflect.TypeOf((*index.Info)(nil)).Elem().NumMethod(); cnt != 11 {
+	if cnt := reflect.TypeOf((*index.InfoReader)(nil)).Elem().NumMethod(); cnt != 11 {
 		diffs = append(diffs, fmt.Sprintf("unexpected number of methods on content.Info: %v, must update the test", cnt))
 	}
 
diff --git a/repo/content/committed_content_index_cache_test.go b/repo/content/committed_content_index_cache_test.go
index 76a10e89..e4638a88 100644
--- a/repo/content/committed_content_index_cache_test.go
+++ b/repo/content/committed_content_index_cache_test.go
@@ -48,8 +48,8 @@ func testCache(t *testing.T, cache committedContentIndexCache, fakeTime *faketim
 	}
 
 	require.NoError(t, cache.addContentToCache(ctx, "ndx1", mustBuildIndex(t, index.Builder{
-		mustParseID(t, "c1"): &InfoStruct{PackBlobID: "p1234", ContentID: mustParseID(t, "c1")},
-		mustParseID(t, "c2"): &InfoStruct{PackBlobID: "p1234", ContentID: mustParseID(t, "c2")},
+		mustParseID(t, "c1"): Info{PackBlobID: "p1234", ContentID: mustParseID(t, "c1")},
+		mustParseID(t, "c2"): Info{PackBlobID: "p1234", ContentID: mustParseID(t, "c2")},
 	})))
 
 	has, err = cache.hasIndexBlobID(ctx, "ndx1")
@@ -60,13 +60,13 @@ func testCache(t *testing.T, cache committedContentIndexCache, fakeTime *faketim
 	}
 
 	require.NoError(t, cache.addContentToCache(ctx, "ndx2", mustBuildIndex(t, index.Builder{
-		mustParseID(t, "c3"): &InfoStruct{PackBlobID: "p2345", ContentID: mustParseID(t, "c3")},
-		mustParseID(t, "c4"): &InfoStruct{PackBlobID: "p2345", ContentID: mustParseID(t, "c4")},
+		mustParseID(t, "c3"): Info{PackBlobID: "p2345", ContentID: mustParseID(t, "c3")},
+		mustParseID(t, "c4"): Info{PackBlobID: "p2345", ContentID: mustParseID(t, "c4")},
 	})))
 
 	require.NoError(t, cache.addContentToCache(ctx, "ndx2", mustBuildIndex(t, index.Builder{
-		mustParseID(t, "c3"): &InfoStruct{PackBlobID: "p2345", ContentID: mustParseID(t, "c3")},
-		mustParseID(t, "c4"): &InfoStruct{PackBlobID: "p2345", ContentID: mustParseID(t, "c4")},
+		mustParseID(t, "c3"): Info{PackBlobID: "p2345", ContentID: mustParseID(t, "c3")},
+		mustParseID(t, "c4"): Info{PackBlobID: "p2345", ContentID: mustParseID(t, "c4")},
 	})))
 
 	ndx1, err := cache.openIndex(ctx, "ndx1")
diff --git a/repo/content/content_manager_test.go b/repo/content/content_manager_test.go
index 51d7c4f4..4f8f734e 100644
--- a/repo/content/content_manager_test.go
+++ b/repo/content/content_manager_test.go
@@ -970,7 +970,7 @@ func (s *contentManagerSuite) TestDeleteAfterUndelete(t *testing.T) {
 		t.Fatal("error while flushing:", err)
 	}
 
-	c2Want = withDeleted{c2Want, true}
+	c2Want = withDeleted(c2Want)
 	deleteContentAfterUndeleteAndCheck(ctx, t, bm, content2, c2Want)
 }
 
@@ -983,7 +983,7 @@ func deleteContentAfterUndeleteAndCheck(ctx context.Context, t *testing.T, bm *W
 		t.Fatalf("Expected content %q to be deleted, got: %#v", id, got)
 	}
 
-	if diff := indextest.InfoDiff(want, got, "GetTimestampSeconds"); len(diff) != 0 {
+	if diff := indextest.InfoDiff(want, got, "GetTimestampSeconds", "Timestamp"); len(diff) != 0 {
 		t.Fatalf("Content %q info does not match\ndiff: %v", id, diff)
 	}
 
@@ -2673,13 +2673,10 @@ func verifyBlobCount(t *testing.T, data blobtesting.DataMap, want map[blob.ID]in
 	}
 }
 
-type withDeleted struct {
-	index.Info
-	deleted bool
-}
+func withDeleted(i Info) Info {
+	i.Deleted = true
 
-func (o withDeleted) GetDeleted() bool {
-	return o.deleted
+	return i
 }
 
 var (
diff --git a/repo/content/index/merged_test.go b/repo/content/index/merged_test.go
index 0acbd12c..adce9bcf 100644
--- a/repo/content/index/merged_test.go
+++ b/repo/content/index/merged_test.go
@@ -14,25 +14,25 @@
 
 func TestMerged(t *testing.T) {
 	i1, err := indexWithItems(
-		&InfoStruct{ContentID: mustParseID(t, "aabbcc"), TimestampSeconds: 1, PackBlobID: "xx", PackOffset: 11},
-		&InfoStruct{ContentID: mustParseID(t, "ddeeff"), TimestampSeconds: 1, PackBlobID: "xx", PackOffset: 111},
-		&InfoStruct{ContentID: mustParseID(t, "z010203"), TimestampSeconds: 1, PackBlobID: "xx", PackOffset: 111},
-		&InfoStruct{ContentID: mustParseID(t, "de1e1e"), TimestampSeconds: 4, PackBlobID: "xx", PackOffset: 111},
+		Info{ContentID: mustParseID(t, "aabbcc"), TimestampSeconds: 1, PackBlobID: "xx", PackOffset: 11},
+		Info{ContentID: mustParseID(t, "ddeeff"), TimestampSeconds: 1, PackBlobID: "xx", PackOffset: 111},
+		Info{ContentID: mustParseID(t, "z010203"), TimestampSeconds: 1, PackBlobID: "xx", PackOffset: 111},
+		Info{ContentID: mustParseID(t, "de1e1e"), TimestampSeconds: 4, PackBlobID: "xx", PackOffset: 111},
 	)
 	require.NoError(t, err)
 
 	i2, err := indexWithItems(
-		&InfoStruct{ContentID: mustParseID(t, "aabbcc"), TimestampSeconds: 3, PackBlobID: "yy", PackOffset: 33},
-		&InfoStruct{ContentID: mustParseID(t, "xaabbcc"), TimestampSeconds: 1, PackBlobID: "xx", PackOffset: 111},
-		&InfoStruct{ContentID: mustParseID(t, "de1e1e"), TimestampSeconds: 4, PackBlobID: "xx", PackOffset: 222, Deleted: true},
+		Info{ContentID: mustParseID(t, "aabbcc"), TimestampSeconds: 3, PackBlobID: "yy", PackOffset: 33},
+		Info{ContentID: mustParseID(t, "xaabbcc"), TimestampSeconds: 1, PackBlobID: "xx", PackOffset: 111},
+		Info{ContentID: mustParseID(t, "de1e1e"), TimestampSeconds: 4, PackBlobID: "xx", PackOffset: 222, Deleted: true},
 	)
 	require.NoError(t, err)
 
 	i3, err := indexWithItems(
-		&InfoStruct{ContentID: mustParseID(t, "aabbcc"), TimestampSeconds: 2, PackBlobID: "zz", PackOffset: 22},
-		&InfoStruct{ContentID: mustParseID(t, "ddeeff"), TimestampSeconds: 1, PackBlobID: "zz", PackOffset: 222},
-		&InfoStruct{ContentID: mustParseID(t, "k010203"), TimestampSeconds: 1, PackBlobID: "xx", PackOffset: 111},
-		&InfoStruct{ContentID: mustParseID(t, "k020304"), TimestampSeconds: 1, PackBlobID: "xx", PackOffset: 111},
+		Info{ContentID: mustParseID(t, "aabbcc"), TimestampSeconds: 2, PackBlobID: "zz", PackOffset: 22},
+		Info{ContentID: mustParseID(t, "ddeeff"), TimestampSeconds: 1, PackBlobID: "zz", PackOffset: 222},
+		Info{ContentID: mustParseID(t, "k010203"), TimestampSeconds: 1, PackBlobID: "xx", PackOffset: 111},
+		Info{ContentID: mustParseID(t, "k020304"), TimestampSeconds: 1, PackBlobID: "xx", PackOffset: 111},
 	)
 	require.NoError(t, err)
 
@@ -46,7 +46,7 @@ func TestMerged(t *testing.T) {
 
 	require.Equal(t, uint32(33), i.GetPackOffset())
 
-	require.NoError(t, m.Iterate(AllIDs, func(i Info) error {
+	require.NoError(t, m.Iterate(AllIDs, func(i InfoReader) error {
 		if i.GetContentID() == mustParseID(t, "de1e1e") {
 			if i.GetDeleted() {
 				t.Errorf("iteration preferred deleted content over non-deleted")
@@ -59,7 +59,7 @@ func TestMerged(t *testing.T) {
 
 	// error is propagated.
 	someErr := errors.Errorf("some error")
-	require.ErrorIs(t, m.Iterate(AllIDs, func(i Info) error {
+	require.ErrorIs(t, m.Iterate(AllIDs, func(i InfoReader) error {
 		if i.GetContentID() == mustParseID(t, "aabbcc") {
 			return someErr
 		}
@@ -70,7 +70,7 @@ func TestMerged(t *testing.T) {
 	fmt.Println("=========== END")
 
 	// empty merged index does not invoke callback during iteration.
-	require.NoError(t, Merged{}.Iterate(AllIDs, func(i Info) error {
+	require.NoError(t, Merged{}.Iterate(AllIDs, func(i InfoReader) error {
 		return someErr
 	}))
 
@@ -152,7 +152,7 @@ type failingIndex struct {
 	err error
 }
 
-func (i failingIndex) GetInfo(contentID ID) (Info, error) {
+func (i failingIndex) GetInfo(contentID ID) (InfoReader, error) {
 	return nil, i.err
 }
 
@@ -168,23 +168,23 @@ func TestMergedGetInfoError(t *testing.T) {
 
 func TestMergedIndexIsConsistent(t *testing.T) {
 	i1, err := indexWithItems(
-		&InfoStruct{ContentID: mustParseID(t, "aabbcc"), TimestampSeconds: 1, PackBlobID: "xx", PackOffset: 11},
-		&InfoStruct{ContentID: mustParseID(t, "bbccdd"), TimestampSeconds: 1, PackBlobID: "xx", PackOffset: 11},
-		&InfoStruct{ContentID: mustParseID(t, "ccddee"), TimestampSeconds: 1, PackBlobID: "ff", PackOffset: 11, Deleted: true},
+		Info{ContentID: mustParseID(t, "aabbcc"), TimestampSeconds: 1, PackBlobID: "xx", PackOffset: 11},
+		Info{ContentID: mustParseID(t, "bbccdd"), TimestampSeconds: 1, PackBlobID: "xx", PackOffset: 11},
+		Info{ContentID: mustParseID(t, "ccddee"), TimestampSeconds: 1, PackBlobID: "ff", PackOffset: 11, Deleted: true},
 	)
 	require.NoError(t, err)
 
 	i2, err := indexWithItems(
-		&InfoStruct{ContentID: mustParseID(t, "aabbcc"), TimestampSeconds: 1, PackBlobID: "yy", PackOffset: 33},
-		&InfoStruct{ContentID: mustParseID(t, "bbccdd"), TimestampSeconds: 1, PackBlobID: "yy", PackOffset: 11, Deleted: true},
-		&InfoStruct{ContentID: mustParseID(t, "ccddee"), TimestampSeconds: 1, PackBlobID: "gg", PackOffset: 11, Deleted: true},
+		Info{ContentID: mustParseID(t, "aabbcc"), TimestampSeconds: 1, PackBlobID: "yy", PackOffset: 33},
+		Info{ContentID: mustParseID(t, "bbccdd"), TimestampSeconds: 1, PackBlobID: "yy", PackOffset: 11, Deleted: true},
+		Info{ContentID: mustParseID(t, "ccddee"), TimestampSeconds: 1, PackBlobID: "gg", PackOffset: 11, Deleted: true},
 	)
 	require.NoError(t, err)
 
 	i3, err := indexWithItems(
-		&InfoStruct{ContentID: mustParseID(t, "aabbcc"), TimestampSeconds: 1, PackBlobID: "zz", PackOffset: 22},
-		&InfoStruct{ContentID: mustParseID(t, "bbccdd"), TimestampSeconds: 1, PackBlobID: "zz", PackOffset: 11, Deleted: true},
-		&InfoStruct{ContentID: mustParseID(t, "ccddee"), TimestampSeconds: 1, PackBlobID: "hh", PackOffset: 11, Deleted: true},
+		Info{ContentID: mustParseID(t, "aabbcc"), TimestampSeconds: 1, PackBlobID: "zz", PackOffset: 22},
+		Info{ContentID: mustParseID(t, "bbccdd"), TimestampSeconds: 1, PackBlobID: "zz", PackOffset: 11, Deleted: true},
+		Info{ContentID: mustParseID(t, "ccddee"), TimestampSeconds: 1, PackBlobID: "hh", PackOffset: 11, Deleted: true},
 	)
 	require.NoError(t, err)
 
@@ -229,7 +229,7 @@ func iterateIDRange(t *testing.T, m Index, r IDRange) []ID {
 
 	var inOrder []ID
 
-	require.NoError(t, m.Iterate(r, func(i Info) error {
+	require.NoError(t, m.Iterate(r, func(i InfoReader) error {
 		inOrder = append(inOrder, i.GetContentID())
 		return nil
 	}))
diff --git a/repo/content/index/packindex_test.go b/repo/content/index/packindex_test.go
index b0ba21db..d3454b39 100644
--- a/repo/content/index/packindex_test.go
+++ b/repo/content/index/packindex_test.go
@@ -112,7 +112,7 @@ func testPackIndex(t *testing.T, version int) {
 	var infos []Info
 	// deleted contents with all information
 	for i := 0; i < 100; i++ {
-		infos = append(infos, &InfoStruct{
+		infos = append(infos, Info{
 			TimestampSeconds:    randomUnixTime(),
 			Deleted:             true,
 			ContentID:           deterministicContentID(t, "deleted-packed", i),
@@ -127,7 +127,7 @@ func testPackIndex(t *testing.T, version int) {
 	}
 	// non-deleted content
 	for i := 0; i < 100; i++ {
-		infos = append(infos, &InfoStruct{
+		infos = append(infos, Info{
 			TimestampSeconds:    randomUnixTime(),
 			ContentID:           deterministicContentID(t, "packed", i),
 			PackBlobID:          deterministicPackBlobID(i),
@@ -142,7 +142,7 @@ func testPackIndex(t *testing.T, version int) {
 
 	// dear future reader, if this fails because the number of methods has changed,
 	// you need to add additional test cases above.
-	if cnt := reflect.TypeOf((*Info)(nil)).Elem().NumMethod(); cnt != 11 {
+	if cnt := reflect.TypeOf((*InfoReader)(nil)).Elem().NumMethod(); cnt != 11 {
 		t.Fatalf("unexpected number of methods on content.Info: %v, must update the test", cnt)
 	}
 
@@ -202,22 +202,22 @@ func testPackIndex(t *testing.T, version int) {
 
 		if version == 1 {
 			// v1 does not preserve original length.
-			want = withOriginalLength{want, want.GetPackedLength() - fakeEncryptionOverhead}
+			want = withOriginalLength(want, want.GetPackedLength()-fakeEncryptionOverhead)
 		}
 
-		require.Equal(t, ToInfoStruct(want), ToInfoStruct(info2))
+		require.Equal(t, want, ToInfoStruct(info2))
 	}
 
 	cnt := 0
 
-	require.NoError(t, ndx.Iterate(AllIDs, func(info2 Info) error {
+	require.NoError(t, ndx.Iterate(AllIDs, func(info2 InfoReader) error {
 		want := infoMap[info2.GetContentID()]
 		if version == 1 {
 			// v1 does not preserve original length.
-			want = withOriginalLength{want, want.GetPackedLength() - fakeEncryptionOverhead}
+			want = withOriginalLength(want, want.GetPackedLength()-fakeEncryptionOverhead)
 		}
 
-		require.Equal(t, ToInfoStruct(want), ToInfoStruct(info2))
+		require.Equal(t, want, ToInfoStruct(info2))
 		cnt++
 		return nil
 	}))
@@ -244,7 +244,7 @@ func testPackIndex(t *testing.T, version int) {
 	for _, prefix := range prefixes {
 		cnt2 := 0
 		prefix := prefix
-		require.NoError(t, ndx.Iterate(PrefixRange(prefix), func(info2 Info) error {
+		require.NoError(t, ndx.Iterate(PrefixRange(prefix), func(info2 InfoReader) error {
 			cnt2++
 			if !strings.HasPrefix(info2.GetContentID().String(), string(prefix)) {
 				t.Errorf("unexpected item %v when iterating prefix %v", info2.GetContentID(), prefix)
@@ -257,15 +257,15 @@ func testPackIndex(t *testing.T, version int) {
 
 func TestPackIndexPerContentLimits(t *testing.T) {
 	cases := []struct {
-		info   *InfoStruct
+		info   Info
 		errMsg string
 	}{
-		{&InfoStruct{PackedLength: v2MaxContentLength}, "maximum content length is too high"},
-		{&InfoStruct{PackedLength: v2MaxContentLength - 1}, ""},
-		{&InfoStruct{OriginalLength: v2MaxContentLength}, "maximum content length is too high"},
-		{&InfoStruct{OriginalLength: v2MaxContentLength - 1}, ""},
-		{&InfoStruct{PackOffset: v2MaxPackOffset}, "pack offset 1073741824 is too high"},
-		{&InfoStruct{PackOffset: v2MaxPackOffset - 1}, ""},
+		{Info{PackedLength: v2MaxContentLength}, "maximum content length is too high"},
+		{Info{PackedLength: v2MaxContentLength - 1}, ""},
+		{Info{OriginalLength: v2MaxContentLength}, "maximum content length is too high"},
+		{Info{OriginalLength: v2MaxContentLength - 1}, ""},
+		{Info{PackOffset: v2MaxPackOffset}, "pack offset 1073741824 is too high"},
+		{Info{PackOffset: v2MaxPackOffset - 1}, ""},
 	}
 
 	for _, tc := range cases {
@@ -287,7 +287,7 @@ func TestPackIndexPerContentLimits(t *testing.T) {
 			got, err := pi.GetInfo(cid)
 			require.NoError(t, err)
 
-			require.Equal(t, ToInfoStruct(got), ToInfoStruct(tc.info))
+			require.Equal(t, ToInfoStruct(got), tc.info)
 		} else {
 			err := b.buildV2(&result)
 			require.Error(t, err)
@@ -302,7 +302,7 @@ func TestSortedContents(t *testing.T) {
 	for i := 0; i < 100; i++ {
 		v := deterministicContentID(t, "", i)
 
-		b.Add(&InfoStruct{
+		b.Add(Info{
 			ContentID: v,
 		})
 	}
@@ -322,34 +322,34 @@ func TestSortedContents(t *testing.T) {
 func TestSortedContents2(t *testing.T) {
 	b := Builder{}
 
-	b.Add(&InfoStruct{
+	b.Add(Info{
 		ContentID: mustParseID(t, "0123"),
 	})
-	b.Add(&InfoStruct{
+	b.Add(Info{
 		ContentID: mustParseID(t, "1023"),
 	})
-	b.Add(&InfoStruct{
+	b.Add(Info{
 		ContentID: mustParseID(t, "0f23"),
 	})
-	b.Add(&InfoStruct{
+	b.Add(Info{
 		ContentID: mustParseID(t, "f023"),
 	})
-	b.Add(&InfoStruct{
+	b.Add(Info{
 		ContentID: mustParseID(t, "g0123"),
 	})
-	b.Add(&InfoStruct{
+	b.Add(Info{
 		ContentID: mustParseID(t, "g1023"),
 	})
-	b.Add(&InfoStruct{
+	b.Add(Info{
 		ContentID: mustParseID(t, "i0123"),
 	})
-	b.Add(&InfoStruct{
+	b.Add(Info{
 		ContentID: mustParseID(t, "i1023"),
 	})
-	b.Add(&InfoStruct{
+	b.Add(Info{
 		ContentID: mustParseID(t, "h0123"),
 	})
-	b.Add(&InfoStruct{
+	b.Add(Info{
 		ContentID: mustParseID(t, "h1023"),
 	})
 
@@ -372,7 +372,7 @@ func TestPackIndexV2TooManyUniqueFormats(t *testing.T) {
 	for i := 0; i < v2MaxFormatCount; i++ {
 		v := deterministicContentID(t, "", i)
 
-		b.Add(&InfoStruct{
+		b.Add(Info{
 			ContentID:           v,
 			PackBlobID:          blob.ID(v.String()),
 			FormatVersion:       1,
@@ -383,7 +383,7 @@ func TestPackIndexV2TooManyUniqueFormats(t *testing.T) {
 	require.NoError(t, b.buildV2(io.Discard))
 
 	// add one more to push it over the edge
-	b.Add(&InfoStruct{
+	b.Add(Info{
 		ContentID:           deterministicContentID(t, "", v2MaxFormatCount),
 		FormatVersion:       1,
 		CompressionHeaderID: compression.HeaderID(5000),
@@ -404,7 +404,7 @@ func fuzzTestIndexOpen(originalData []byte) {
 			return
 		}
 		cnt := 0
-		_ = ndx.Iterate(AllIDs, func(cb Info) error {
+		_ = ndx.Iterate(AllIDs, func(cb InfoReader) error {
 			if cnt < 10 {
 				_, _ = ndx.GetInfo(cb.GetContentID())
 			}
@@ -466,7 +466,7 @@ func TestShard(t *testing.T) {
 
 	// add ID to the builder
 	for _, id := range ids {
-		b.Add(&InfoStruct{
+		b.Add(Info{
 			ContentID: deterministicContentID(t, "", id),
 		})
 	}
@@ -516,13 +516,11 @@ func verifyAllShardedIDs(t *testing.T, sharded []Builder, numTotal, numShards in
 	return lens
 }
 
-type withOriginalLength struct {
-	Info
-	originalLength uint32
-}
+func withOriginalLength(is Info, originalLength uint32) Info {
+	// clone and override original length
+	is.OriginalLength = originalLength
 
-func (o withOriginalLength) GetOriginalLength() uint32 {
-	return o.originalLength
+	return is
 }
 
 func mustParseID(t *testing.T, s string) ID {
diff --git a/repo/object/object_manager_test.go b/repo/object/object_manager_test.go
index 70285031..db5f9958 100644
--- a/repo/object/object_manager_test.go
+++ b/repo/object/object_manager_test.go
@@ -88,10 +88,10 @@ func (f *fakeContentManager) ContentInfo(ctx context.Context, contentID content.
 	defer f.mu.Unlock()
 
 	if d, ok := f.data[contentID]; ok {
-		return &content.InfoStruct{ContentID: contentID, PackedLength: uint32(len(d))}, nil
+		return content.Info{ContentID: contentID, PackedLength: uint32(len(d))}, nil
 	}
 
-	return nil, blob.ErrBlobNotFound
+	return content.Info{}, blob.ErrBlobNotFound
 }
 
 func (f *fakeContentManager) Flush(ctx context.Context) error {
diff --git a/tests/end_to_end_test/policy_test.go b/tests/end_to_end_test/policy_test.go
index 4b5e3368..3b2899fa 100644
--- a/tests/end_to_end_test/policy_test.go
+++ b/tests/end_to_end_test/policy_test.go
@@ -23,7 +23,7 @@ func TestDefaultGlobalPolicy(t *testing.T) {
 
 	// verify we created global policy entry
 
-	var contents []content.InfoStruct
+	var contents []content.Info
 
 	testutil.MustParseJSONLines(t, e.RunAndExpectSuccess(t, "content", "ls", "--json"), &contents)
 
diff --git a/tests/end_to_end_test/snapshot_gc_test.go b/tests/end_to_end_test/snapshot_gc_test.go
index c2de8d43..eab0bddc 100644
--- a/tests/end_to_end_test/snapshot_gc_test.go
+++ b/tests/end_to_end_test/snapshot_gc_test.go
@@ -59,7 +59,7 @@ func (s *formatSpecificTestSuite) TestSnapshotGC(t *testing.T) {
 	e.RunAndExpectSuccess(t, "maintenance", "run", "--full", "--safety=full")
 
 	// data block + directory block + manifest block + manifest block from manifest deletion
-	var contentInfo []content.InfoStruct
+	var contentInfo []content.Info
 
 	testutil.MustParseJSONLines(t, e.RunAndExpectSuccess(t, "content", "list", "--json"), &contentInfo)