Repository: kopia/kopia
Pull request #1026 (2021-04-27 08:53:44)

build(deps): bump github.com/aws/aws-sdk-go from 1.38.25 to 1.38.26
Bumps [github.com/aws/aws-sdk-go](https://github.com/aws/aws-sdk-go) from 1.38.25 to 1.38.26.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/aws/aws-sdk-go/releases">github.com/aws/aws-sdk-go's releases</a>.</em></p>
<blockquote>
<h1>Release v1.38.26 (2021-04-26)</h1>
<h3>Service Client Updates</h3>
<ul>
<li><code>service/codeguru-reviewer</code>: Updates service API and documentation</li>
<li><code>service/ec2</code>: Updates service API and documentation
<ul>
<li>Adding support for Red Hat Enterprise Linux with HA for Reserved Instances.</li>
</ul>
</li>
<li><code>service/eks</code>: Updates service API and documentation</li>
<li><code>service/glue</code>: Updates service API and documentation
<ul>
<li>Adding Kafka Client Auth Related Parameters</li>
</ul>
</li>
<li><code>service/iotwireless</code>: Updates service API and documentation</li>
<li><code>service/kinesisanalyticsv2</code>: Updates service API and documentation</li>
<li><code>service/mediaconvert</code>: Updates service API and documentation
<ul>
<li>Documentation updates for mediaconvert</li>
</ul>
</li>
<li><code>service/personalize</code>: Updates service API, documentation, and paginators</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a href="https://github.com/aws/aws-sdk-go/blob/main/CHANGELOG.md">github.com/aws/aws-sdk-go's changelog</a>.</em></p>
<blockquote>
<h1>Release v1.38.26 (2021-04-26)</h1>
<h3>Service Client Updates</h3>
<ul>
<li><code>service/codeguru-reviewer</code>: Updates service API and documentation</li>
<li><code>service/ec2</code>: Updates service API and documentation
<ul>
<li>Adding support for Red Hat Enterprise Linux with HA for Reserved Instances.</li>
</ul>
</li>
<li><code>service/eks</code>: Updates service API and documentation</li>
<li><code>service/glue</code>: Updates service API and documentation
<ul>
<li>Adding Kafka Client Auth Related Parameters</li>
</ul>
</li>
<li><code>service/iotwireless</code>: Updates service API and documentation</li>
<li><code>service/kinesisanalyticsv2</code>: Updates service API and documentation</li>
<li><code>service/mediaconvert</code>: Updates service API and documentation
<ul>
<li>Documentation updates for mediaconvert</li>
</ul>
</li>
<li><code>service/personalize</code>: Updates service API, documentation, and paginators</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/aws/aws-sdk-go/commit/92c450718057ade80d0c3ad73c9f59605f92260a"><code>92c4507</code></a> Release v1.38.26 (2021-04-26) (<a href="https://github-redirect.dependabot.com/aws/aws-sdk-go/issues/3880">#3880</a>)</li>
<li>See full diff in <a href="https://github.com/aws/aws-sdk-go/compare/v1.38.25...v1.38.26">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/aws/aws-sdk-go&package-manager=go_modules&previous-version=1.38.25&new-version=1.38.26)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
Dependabot will merge this PR once CI passes on it, as requested by @julio-lopez.

[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>

julio-lopez: @dependabot merge

Patch:
diff --git a/cli/command_content_list.go b/cli/command_content_list.go
index 75bc5d23..03a79917 100644
--- a/cli/command_content_list.go
+++ b/cli/command_content_list.go
@@ -35,11 +35,11 @@ func runContentListCommand(ctx context.Context, rep repo.DirectRepository) error
 			IncludeDeleted: *contentListIncludeDeleted || *contentListDeletedOnly,
 		},
 		func(b content.Info) error {
-			if *contentListDeletedOnly && !b.Deleted {
+			if *contentListDeletedOnly && !b.GetDeleted() {
 				return nil
 			}
 
-			totalSize.Add(int64(b.PackedLength))
+			totalSize.Add(int64(b.GetPackedLength()))
 
 			if jsonOutput {
 				jl.emit(b)
@@ -48,18 +48,18 @@ func(b content.Info) error {
 
 			if *contentListLong {
 				optionalDeleted := ""
-				if b.Deleted {
+				if b.GetDeleted() {
 					optionalDeleted = " (deleted)"
 				}
 				fmt.Printf("%v %v %v %v+%v%v\n",
-					b.ID,
+					b.GetContentID(),
 					formatTimestamp(b.Timestamp()),
-					b.PackBlobID,
-					b.PackOffset,
-					maybeHumanReadableBytes(*contentListHuman, int64(b.PackedLength)),
+					b.GetPackBlobID(),
+					b.GetPackOffset(),
+					maybeHumanReadableBytes(*contentListHuman, int64(b.GetPackedLength())),
 					optionalDeleted)
 			} else {
-				fmt.Printf("%v\n", b.ID)
+				fmt.Printf("%v\n", b.GetContentID())
 			}
 
 			return nil
diff --git a/cli/command_content_stats.go b/cli/command_content_stats.go
index 882e402b..ab2d2132 100644
--- a/cli/command_content_stats.go
+++ b/cli/command_content_stats.go
@@ -39,12 +39,12 @@ func runContentStatsCommand(ctx context.Context, rep repo.DirectRepository) erro
 			Range: contentIDRange(),
 		},
 		func(b content.Info) error {
-			totalSize += int64(b.PackedLength)
+			totalSize += int64(b.GetPackedLength())
 			count++
 			for s := range countMap {
-				if b.PackedLength < s {
+				if b.GetPackedLength() < s {
 					countMap[s]++
-					totalSizeOfContentsUnder[s] += int64(b.PackedLength)
+					totalSizeOfContentsUnder[s] += int64(b.GetPackedLength())
 				}
 			}
 			return nil
diff --git a/cli/command_content_verify.go b/cli/command_content_verify.go
index bc96d2dd..74b54dc9 100644
--- a/cli/command_content_verify.go
+++ b/cli/command_content_verify.go
@@ -60,7 +60,7 @@ func runContentVerifyCommand(ctx context.Context, rep repo.DirectRepository) err
 		Parallel:       *contentVerifyParallel,
 		IncludeDeleted: *contentVerifyIncludeDeleted,
 	}, func(ci content.Info) error {
-		if err := contentVerify(ctx, rep.ContentReader(), &ci, blobMap); err != nil {
+		if err := contentVerify(ctx, rep.ContentReader(), ci, blobMap); err != nil {
 			log(ctx).Errorf("error %v", err)
 			atomic.AddInt32(&errorCount, 1)
 		} else {
@@ -86,22 +86,22 @@ func runContentVerifyCommand(ctx context.Context, rep repo.DirectRepository) err
 	return errors.Errorf("encountered %v errors", errorCount)
 }
 
-func contentVerify(ctx context.Context, r content.Reader, ci *content.Info, blobMap map[blob.ID]blob.Metadata) error {
+func contentVerify(ctx context.Context, r content.Reader, ci content.Info, blobMap map[blob.ID]blob.Metadata) error {
 	if *contentVerifyFull {
-		if _, err := r.GetContent(ctx, ci.ID); err != nil {
-			return errors.Wrapf(err, "content %v is invalid", ci.ID)
+		if _, err := r.GetContent(ctx, ci.GetContentID()); err != nil {
+			return errors.Wrapf(err, "content %v is invalid", ci.GetContentID())
 		}
 
 		return nil
 	}
 
-	bi, ok := blobMap[ci.PackBlobID]
+	bi, ok := blobMap[ci.GetPackBlobID()]
 	if !ok {
-		return errors.Errorf("content %v depends on missing blob %v", ci.ID, ci.PackBlobID)
+		return errors.Errorf("content %v depends on missing blob %v", ci.GetContentID(), ci.GetPackBlobID())
 	}
 
-	if int64(ci.PackOffset+ci.PackedLength) > bi.Length {
-		return errors.Errorf("content %v out of bounds of its pack blob %v", ci.ID, ci.PackBlobID)
+	if int64(ci.GetPackOffset()+ci.GetPackedLength()) > bi.Length {
+		return errors.Errorf("content %v out of bounds of its pack blob %v", ci.GetContentID(), ci.GetPackBlobID())
 	}
 
 	return nil
diff --git a/cli/command_index_inspect.go b/cli/command_index_inspect.go
index c176d6ed..f9a41637 100644
--- a/cli/command_index_inspect.go
+++ b/cli/command_index_inspect.go
@@ -28,13 +28,13 @@ func runInspectIndexAction(ctx context.Context, rep repo.DirectRepository) error
 func dumpIndexBlobEntries(bm blob.Metadata, entries []content.Info) {
 	for _, ci := range entries {
 		state := "created"
-		if ci.Deleted {
+		if ci.GetDeleted() {
 			state = "deleted"
 		}
 
 		printStdout("%v %v %v %v %v %v %v %v\n",
 			formatTimestampPrecise(bm.Timestamp), bm.BlobID,
-			ci.ID, state, formatTimestampPrecise(ci.Timestamp()), ci.PackBlobID, ci.PackOffset, ci.PackedLength)
+			ci.GetContentID(), state, formatTimestampPrecise(ci.Timestamp()), ci.GetPackBlobID(), ci.GetPackOffset(), ci.GetPackedLength())
 	}
 }
 
diff --git a/cli/command_snapshot_verify.go b/cli/command_snapshot_verify.go
index 1d5a65bc..606bb97a 100644
--- a/cli/command_snapshot_verify.go
+++ b/cli/command_snapshot_verify.go
@@ -153,8 +153,8 @@ func (v *verifier) doVerifyObject(ctx context.Context, oid object.ID, path strin
 				continue
 			}
 
-			if _, ok := v.blobMap[ci.PackBlobID]; !ok {
-				v.reportError(ctx, path, errors.Errorf("object %v is backed by missing blob %v", oid, ci.PackBlobID))
+			if _, ok := v.blobMap[ci.GetPackBlobID()]; !ok {
+				v.reportError(ctx, path, errors.Errorf("object %v is backed by missing blob %v", oid, ci.GetPackBlobID()))
 				continue
 			}
 		}
diff --git a/cli/json_output.go b/cli/json_output.go
index 788a3c31..7d4a9f6d 100644
--- a/cli/json_output.go
+++ b/cli/json_output.go
@@ -5,6 +5,7 @@
 
 	"github.com/alecthomas/kingpin"
 
+	"github.com/kopia/kopia/repo/content"
 	"github.com/kopia/kopia/snapshot"
 )
 
@@ -47,6 +48,8 @@ func cleanupSnapshotManifestListForJSON(manifests []*snapshot.Manifest) interfac
 
 func cleanupForJSON(v interface{}) interface{} {
 	switch v := v.(type) {
+	case content.Info:
+		return content.ToInfoStruct(v)
 	case *snapshot.Manifest:
 		return cleanupSnapshotManifestForJSON(v)
 	case []*snapshot.Manifest:
diff --git a/go.mod b/go.mod
index 38a08321..4b633756 100644
--- a/go.mod
+++ b/go.mod
@@ -11,7 +11,7 @@ require (
 	github.com/StackExchange/wmi v0.0.0-20210224194228-fe8f1750fd46 // indirect
 	github.com/alecthomas/kingpin v0.0.0-20200323085623-b6657d9477a6 // this is pulling master, which is newer than v2
 	github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 // indirect
-	github.com/aws/aws-sdk-go v1.38.25
+	github.com/aws/aws-sdk-go v1.38.26
 	github.com/chmduquesne/rollinghash v4.0.0+incompatible
 	github.com/dgrijalva/jwt-go v3.2.0+incompatible
 	github.com/efarrer/iothrottler v0.0.1
diff --git a/go.sum b/go.sum
index 5b458a01..e500e22e 100644
--- a/go.sum
+++ b/go.sum
@@ -150,8 +150,8 @@ github.com/aws/aws-sdk-go v1.23.20/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpi
 github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
 github.com/aws/aws-sdk-go v1.35.20/go.mod h1:tlPOdRjfxPBpNIwqDj61rmsnA85v9jc0Ps9+muhnW+k=
 github.com/aws/aws-sdk-go v1.36.1/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
-github.com/aws/aws-sdk-go v1.38.25 h1:aNjeh7+MON05cZPtZ6do+KxVT67jPOSQXANA46gOQao=
-github.com/aws/aws-sdk-go v1.38.25/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
+github.com/aws/aws-sdk-go v1.38.26 h1:xHABHMEb/00NydXFy/2Lo+7yIgxGxN/6Fvll3l1Nwnc=
+github.com/aws/aws-sdk-go v1.38.26/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
 github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
 github.com/bcicen/jstream v1.0.1/go.mod h1:9ielPxqFry7Y4Tg3j4BfjPocfJ3TbsRtXOAYXYmRuAQ=
 github.com/beevik/ntp v0.3.0/go.mod h1:hIHWr+l3+/clUnF44zdK+CWW7fO8dR5cIylAQ76NRpg=
diff --git a/internal/server/grpc_session.go b/internal/server/grpc_session.go
index 2d15d2b9..91be04c2 100644
--- a/internal/server/grpc_session.go
+++ b/internal/server/grpc_session.go
@@ -186,14 +186,14 @@ func handleGetContentInfoRequest(ctx context.Context, dw repo.DirectRepositoryWr
 		Response: &grpcapi.SessionResponse_GetContentInfo{
 			GetContentInfo: &grpcapi.GetContentInfoResponse{
 				Info: &grpcapi.ContentInfo{
-					Id:               string(ci.ID),
-					PackedLength:     ci.PackedLength,
-					TimestampSeconds: ci.TimestampSeconds,
-					PackBlobId:       string(ci.PackBlobID),
-					PackOffset:       ci.PackOffset,
-					Deleted:          ci.Deleted,
-					FormatVersion:    uint32(ci.FormatVersion),
-					OriginalLength:   ci.OriginalLength,
+					Id:               string(ci.GetContentID()),
+					PackedLength:     ci.GetPackedLength(),
+					TimestampSeconds: ci.GetTimestampSeconds(),
+					PackBlobId:       string(ci.GetPackBlobID()),
+					PackOffset:       ci.GetPackOffset(),
+					Deleted:          ci.GetDeleted(),
+					FormatVersion:    uint32(ci.GetFormatVersion()),
+					OriginalLength:   ci.GetOriginalLength(),
 				},
 			},
 		},
diff --git a/repo/api_server_repository.go b/repo/api_server_repository.go
index db10f46a..deba3d3d 100644
--- a/repo/api_server_repository.go
+++ b/repo/api_server_repository.go
@@ -152,13 +152,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.Info
+	var bi content.InfoStruct
 
 	if err := r.cli.Get(ctx, "contents/"+string(contentID)+"?info=1", content.ErrContentNotFound, &bi); err != nil {
-		return content.Info{}, errors.Wrap(err, "ContentInfo")
+		return nil, 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/builder.go b/repo/content/builder.go
index 12397adc..20408c1a 100644
--- a/repo/content/builder.go
+++ b/repo/content/builder.go
@@ -1,27 +1,12 @@
 package content
 
 import (
-	"bufio"
-	"crypto/rand"
-	"encoding/binary"
 	"io"
 	"sort"
-
-	"github.com/pkg/errors"
-
-	"github.com/kopia/kopia/repo/blob"
-)
-
-const (
-	packHeaderSize = 8
-	deletedMarker  = 0x80000000
-
-	entryFixedHeaderLength = 20
-	randomSuffixSize       = 32
 )
 
 // packIndexBuilder prepares and writes content index.
-type packIndexBuilder map[ID]*Info
+type packIndexBuilder map[ID]Info
 
 // clone returns a deep clone of packIndexBuilder.
 func (b packIndexBuilder) clone() packIndexBuilder {
@@ -32,8 +17,7 @@ func (b packIndexBuilder) clone() packIndexBuilder {
 	r := packIndexBuilder{}
 
 	for k, v := range b {
-		i2 := *v
-		r[k] = &i2
+		r[k] = v
 	}
 
 	return r
@@ -41,155 +25,27 @@ func (b packIndexBuilder) clone() packIndexBuilder {
 
 // Add adds a new entry to the builder or conditionally replaces it if the timestamp is greater.
 func (b packIndexBuilder) Add(i Info) {
-	old, ok := b[i.ID]
-	if !ok || i.TimestampSeconds >= old.TimestampSeconds {
-		b[i.ID] = &i
+	old, ok := b[i.GetContentID()]
+	if !ok || i.GetTimestampSeconds() >= old.GetTimestampSeconds() {
+		b[i.GetContentID()] = i
 	}
 }
 
-func (b packIndexBuilder) sortedContents() []*Info {
-	var allContents []*Info
+func (b packIndexBuilder) sortedContents() []Info {
+	var allContents []Info
 
 	for _, v := range b {
 		allContents = append(allContents, v)
 	}
 
 	sort.Slice(allContents, func(i, j int) bool {
-		return allContents[i].ID < allContents[j].ID
+		return allContents[i].GetContentID() < allContents[j].GetContentID()
 	})
 
 	return allContents
 }
 
-type indexLayout struct {
-	packBlobIDOffsets map[blob.ID]uint32
-	entryCount        int
-	keyLength         int
-	entryLength       int
-	extraDataOffset   uint32
-}
-
 // Build writes the pack index to the provided output.
 func (b packIndexBuilder) Build(output io.Writer) error {
-	allContents := b.sortedContents()
-	layout := &indexLayout{
-		packBlobIDOffsets: map[blob.ID]uint32{},
-		keyLength:         -1,
-		entryLength:       entryFixedHeaderLength,
-		entryCount:        len(allContents),
-	}
-
-	w := bufio.NewWriter(output)
-
-	// prepare extra data to be appended at the end of an index.
-	extraData := prepareExtraData(allContents, layout)
-
-	// write header
-	header := make([]byte, packHeaderSize)
-	header[0] = 1 // version
-	header[1] = byte(layout.keyLength)
-	binary.BigEndian.PutUint16(header[2:4], uint16(layout.entryLength))
-	binary.BigEndian.PutUint32(header[4:8], uint32(layout.entryCount))
-
-	if _, err := w.Write(header); err != nil {
-		return errors.Wrap(err, "unable to write header")
-	}
-
-	// write all sorted contents.
-	entry := make([]byte, layout.entryLength)
-
-	for _, it := range allContents {
-		if err := writeEntry(w, it, layout, entry); err != nil {
-			return errors.Wrap(err, "unable to write entry")
-		}
-	}
-
-	if _, err := w.Write(extraData); err != nil {
-		return errors.Wrap(err, "error writing extra data")
-	}
-
-	randomSuffix := make([]byte, randomSuffixSize)
-	if _, err := rand.Read(randomSuffix); err != nil {
-		return errors.Wrap(err, "error getting random bytes for suffix")
-	}
-
-	if _, err := w.Write(randomSuffix); err != nil {
-		return errors.Wrap(err, "error writing extra random suffix to ensure indexes are always globally unique")
-	}
-
-	return w.Flush()
-}
-
-func prepareExtraData(allContents []*Info, layout *indexLayout) []byte {
-	var extraData []byte
-
-	var hashBuf [maxContentIDSize]byte
-
-	for i, it := range allContents {
-		if i == 0 {
-			layout.keyLength = len(contentIDToBytes(hashBuf[:0], it.ID))
-		}
-
-		if it.PackBlobID != "" {
-			if _, ok := layout.packBlobIDOffsets[it.PackBlobID]; !ok {
-				layout.packBlobIDOffsets[it.PackBlobID] = uint32(len(extraData))
-				extraData = append(extraData, []byte(it.PackBlobID)...)
-			}
-		}
-	}
-
-	layout.extraDataOffset = uint32(packHeaderSize + layout.entryCount*(layout.keyLength+layout.entryLength))
-
-	return extraData
-}
-
-func writeEntry(w io.Writer, it *Info, layout *indexLayout, entry []byte) error {
-	var hashBuf [maxContentIDSize]byte
-
-	k := contentIDToBytes(hashBuf[:0], it.ID)
-
-	if len(k) != layout.keyLength {
-		return errors.Errorf("inconsistent key length: %v vs %v", len(k), layout.keyLength)
-	}
-
-	if err := formatEntry(entry, it, layout); err != nil {
-		return errors.Wrap(err, "unable to format entry")
-	}
-
-	if _, err := w.Write(k); err != nil {
-		return errors.Wrap(err, "error writing entry key")
-	}
-
-	if _, err := w.Write(entry); err != nil {
-		return errors.Wrap(err, "error writing entry")
-	}
-
-	return nil
-}
-
-func formatEntry(entry []byte, it *Info, layout *indexLayout) error {
-	entryTimestampAndFlags := entry[0:8]
-	entryPackFileOffset := entry[8:12]
-	entryPackedOffset := entry[12:16]
-	entryPackedLength := entry[16:20]
-	timestampAndFlags := uint64(it.TimestampSeconds) << 16 // nolint:gomnd
-
-	if len(it.PackBlobID) == 0 {
-		return errors.Errorf("empty pack content ID for %v", it.ID)
-	}
-
-	binary.BigEndian.PutUint32(entryPackFileOffset, layout.extraDataOffset+layout.packBlobIDOffsets[it.PackBlobID])
-
-	if it.Deleted {
-		binary.BigEndian.PutUint32(entryPackedOffset, it.PackOffset|deletedMarker)
-	} else {
-		binary.BigEndian.PutUint32(entryPackedOffset, it.PackOffset)
-	}
-
-	binary.BigEndian.PutUint32(entryPackedLength, it.PackedLength)
-	timestampAndFlags |= uint64(it.FormatVersion) << 8 // nolint:gomnd
-	timestampAndFlags |= uint64(len(it.PackBlobID))
-	binary.BigEndian.PutUint64(entryTimestampAndFlags, timestampAndFlags)
-
-	return nil
+	return b.buildV1(output)
 }
diff --git a/repo/content/committed_content_index.go b/repo/content/committed_content_index.go
index c081ae03..6f0acb8d 100644
--- a/repo/content/committed_content_index.go
+++ b/repo/content/committed_content_index.go
@@ -47,14 +47,14 @@ func (c *committedContentIndex) getContent(contentID ID) (Info, error) {
 
 	info, err := c.merged.GetInfo(contentID)
 	if info != nil {
-		return *info, nil
+		return info, nil
 	}
 
 	if err == nil {
-		return Info{}, ErrContentNotFound
+		return nil, ErrContentNotFound
 	}
 
-	return Info{}, err
+	return nil, err
 }
 
 func (c *committedContentIndex) addContent(ctx context.Context, indexBlobID blob.ID, data []byte, use bool) error {
diff --git a/repo/content/committed_read_manager.go b/repo/content/committed_read_manager.go
index b5aa160e..65338daa 100644
--- a/repo/content/committed_read_manager.go
+++ b/repo/content/committed_read_manager.go
@@ -207,19 +207,19 @@ func (sm *SharedManager) getCacheForContentID(id ID) contentCache {
 	return sm.contentCache
 }
 
-func (sm *SharedManager) decryptContentAndVerify(payload []byte, bi *Info) ([]byte, error) {
+func (sm *SharedManager) decryptContentAndVerify(payload []byte, bi Info) ([]byte, error) {
 	sm.Stats.readContent(len(payload))
 
 	var hashBuf [maxHashSize]byte
 
-	iv, err := getPackedContentIV(hashBuf[:], bi.ID)
+	iv, err := getPackedContentIV(hashBuf[:], bi.GetContentID())
 	if err != nil {
 		return nil, err
 	}
 
 	decrypted, err := sm.decryptAndVerify(payload, iv)
 	if err != nil {
-		return nil, errors.Wrapf(err, "invalid checksum at %v offset %v length %v", bi.PackBlobID, bi.PackOffset, len(payload))
+		return nil, errors.Wrapf(err, "invalid checksum at %v offset %v length %v", bi.GetPackBlobID(), bi.GetPackOffset(), len(payload))
 	}
 
 	return decrypted, nil
diff --git a/repo/content/content_manager.go b/repo/content/content_manager.go
index b69ed37f..df0e0565 100644
--- a/repo/content/content_manager.go
+++ b/repo/content/content_manager.go
@@ -126,7 +126,7 @@ func (bm *WriteManager) DeleteContent(ctx context.Context, contentID ID) error {
 
 	// remove from all pending packs
 	for _, pp := range bm.pendingPacks {
-		if bi, ok := pp.currentPackItems[contentID]; ok && !bi.Deleted {
+		if bi, ok := pp.currentPackItems[contentID]; ok && !bi.GetDeleted() {
 			delete(pp.currentPackItems, contentID)
 			return nil
 		}
@@ -134,14 +134,14 @@ func (bm *WriteManager) DeleteContent(ctx context.Context, contentID ID) error {
 
 	// remove from all packs that are being written, since they will be committed to index soon
 	for _, pp := range bm.writingPacks {
-		if bi, ok := pp.currentPackItems[contentID]; ok && !bi.Deleted {
+		if bi, ok := pp.currentPackItems[contentID]; ok && !bi.GetDeleted() {
 			return bm.deletePreexistingContent(ctx, bi)
 		}
 	}
 
 	// if found in committed index, add another entry that's marked for deletion
 	if bi, ok := bm.packIndexBuilder[contentID]; ok {
-		return bm.deletePreexistingContent(ctx, *bi)
+		return bm.deletePreexistingContent(ctx, bi)
 	}
 
 	// see if the block existed before
@@ -155,18 +155,16 @@ func (bm *WriteManager) DeleteContent(ctx context.Context, contentID ID) error {
 
 // Intentionally passing bi by value.
 func (bm *WriteManager) deletePreexistingContent(ctx context.Context, ci Info) error {
-	if ci.Deleted {
+	if ci.GetDeleted() {
 		return nil
 	}
 
-	pp, err := bm.getOrCreatePendingPackInfoLocked(ctx, packPrefixForContentID(ci.ID))
+	pp, err := bm.getOrCreatePendingPackInfoLocked(ctx, packPrefixForContentID(ci.GetContentID()))
 	if err != nil {
 		return errors.Wrap(err, "unable to create pack")
 	}
 
-	ci.Deleted = true
-	ci.TimestampSeconds = bm.timeNow().Unix()
-	pp.currentPackItems[ci.ID] = ci
+	pp.currentPackItems[ci.GetContentID()] = &deletedInfo{ci, bm.timeNow().Unix()}
 
 	return nil
 }
@@ -249,9 +247,9 @@ func (bm *WriteManager) addToPackUnlocked(ctx context.Context, contentID ID, dat
 		return errors.Wrap(err, "unable to create pending pack")
 	}
 
-	info := Info{
+	info := &InfoStruct{
 		Deleted:          isDeleted,
-		ID:               contentID,
+		ContentID:        contentID,
 		PackBlobID:       pp.packBlobID,
 		PackOffset:       uint32(pp.currentPackData.Length()),
 		TimestampSeconds: bm.timeNow().Unix(),
@@ -313,29 +311,29 @@ func (bm *WriteManager) verifyInvariantsLocked() {
 func (bm *WriteManager) verifyCurrentPackItemsLocked() {
 	for _, pp := range bm.pendingPacks {
 		for k, cpi := range pp.currentPackItems {
-			bm.assertInvariant(cpi.ID == k, "content ID entry has invalid key: %v %v", cpi.ID, k)
+			bm.assertInvariant(cpi.GetContentID() == k, "content ID entry has invalid key: %v %v", cpi.GetContentID(), k)
 
-			if !cpi.Deleted {
-				bm.assertInvariant(cpi.PackBlobID == pp.packBlobID, "non-deleted pending pack item %q must be from the pending pack %q, was %q", cpi.ID, pp.packBlobID, cpi.PackBlobID)
+			if !cpi.GetDeleted() {
+				bm.assertInvariant(cpi.GetPackBlobID() == pp.packBlobID, "non-deleted pending pack item %q must be from the pending pack %q, was %q", cpi.GetContentID(), pp.packBlobID, cpi.GetPackBlobID())
 			}
 
-			bm.assertInvariant(cpi.TimestampSeconds != 0, "content has no timestamp: %v", cpi.ID)
+			bm.assertInvariant(cpi.GetTimestampSeconds() != 0, "content has no timestamp: %v", cpi.GetContentID())
 		}
 	}
 }
 
 func (bm *WriteManager) verifyPackIndexBuilderLocked() {
 	for k, cpi := range bm.packIndexBuilder {
-		bm.assertInvariant(cpi.ID == k, "content ID entry has invalid key: %v %v", cpi.ID, k)
+		bm.assertInvariant(cpi.GetContentID() == k, "content ID entry has invalid key: %v %v", cpi.GetContentID(), k)
 
-		if cpi.Deleted {
-			bm.assertInvariant(cpi.PackBlobID == "", "content can't be both deleted and have a pack content: %v", cpi.ID)
+		if cpi.GetDeleted() {
+			bm.assertInvariant(cpi.GetPackBlobID() == "", "content can't be both deleted and have a pack content: %v", cpi.GetContentID())
 		} else {
-			bm.assertInvariant(cpi.PackBlobID != "", "content that's not deleted must have a pack content: %+v", cpi)
-			bm.assertInvariant(cpi.FormatVersion == byte(bm.writeFormatVersion), "content that's not deleted must have a valid format version: %+v", cpi)
+			bm.assertInvariant(cpi.GetPackBlobID() != "", "content that's not deleted must have a pack content: %+v", cpi)
+			bm.assertInvariant(cpi.GetFormatVersion() == byte(bm.writeFormatVersion), "content that's not deleted must have a valid format version: %+v", cpi)
 		}
 
-		bm.assertInvariant(cpi.TimestampSeconds != 0, "content has no timestamp: %v", cpi.ID)
+		bm.assertInvariant(cpi.GetTimestampSeconds() != 0, "content has no timestamp: %v", cpi.GetContentID())
 	}
 }
 
@@ -424,7 +422,7 @@ func (bm *WriteManager) writePackAndAddToIndex(ctx context.Context, pp *pendingP
 	if err == nil {
 		// success, add pack index builder entries to index.
 		for _, info := range packFileIndex {
-			bm.packIndexBuilder.Add(*info)
+			bm.packIndexBuilder.Add(info)
 		}
 
 		pp.currentPackData.Close()
@@ -538,12 +536,12 @@ func (bm *WriteManager) RewriteContent(ctx context.Context, contentID ID) error
 		return err
 	}
 
-	data, err := bm.getContentDataUnlocked(ctx, pp, &bi)
+	data, err := bm.getContentDataUnlocked(ctx, pp, bi)
 	if err != nil {
 		return err
 	}
 
-	return bm.addToPackUnlocked(ctx, contentID, data, bi.Deleted)
+	return bm.addToPackUnlocked(ctx, contentID, data, bi.GetDeleted())
 }
 
 // UndeleteContent rewrites the content with the given ID if the content exists
@@ -557,11 +555,11 @@ func (bm *WriteManager) UndeleteContent(ctx context.Context, contentID ID) error
 		return err
 	}
 
-	if !bi.Deleted {
+	if !bi.GetDeleted() {
 		return nil
 	}
 
-	data, err := bm.getContentDataUnlocked(ctx, pp, &bi)
+	data, err := bm.getContentDataUnlocked(ctx, pp, bi)
 	if err != nil {
 		return err
 	}
@@ -631,7 +629,7 @@ func (bm *WriteManager) WriteContent(ctx context.Context, data []byte, prefix ID
 
 	// content already tracked
 	if _, bi, err := bm.getContentInfo(contentID); err == nil {
-		if !bi.Deleted {
+		if !bi.GetDeleted() {
 			formatLog(ctx).Debugf("write-content %v already-exists", contentID)
 			return contentID, nil
 		}
@@ -666,8 +664,8 @@ func (bm *WriteManager) GetContent(ctx context.Context, contentID ID) (v []byte,
 		return nil, err
 	}
 
-	// Return content even if it is bi.Deleted so it can be recovered during GC among others.
-	return bm.getContentDataUnlocked(ctx, pp, &bi)
+	// Return content even if it is bi.GetDeleted() so it can be recovered during GC among others.
+	return bm.getContentDataUnlocked(ctx, pp, bi)
 }
 
 func (bm *WriteManager) getOverlayContentInfo(contentID ID) (*pendingPackInfo, Info, bool) {
@@ -690,10 +688,10 @@ func (bm *WriteManager) getOverlayContentInfo(contentID ID) (*pendingPackInfo, I
 
 	// added contents, written to packs but not yet added to indexes
 	if ci, ok := bm.packIndexBuilder[contentID]; ok {
-		return nil, *ci, true
+		return nil, ci, true
 	}
 
-	return nil, Info{}, false
+	return nil, nil, false
 }
 
 func (bm *WriteManager) getContentInfo(contentID ID) (*pendingPackInfo, Info, error) {
@@ -711,7 +709,7 @@ func (bm *WriteManager) ContentInfo(ctx context.Context, contentID ID) (Info, er
 	_, bi, err := bm.getContentInfo(contentID)
 	if err != nil {
 		log(ctx).Debugf("ContentInfo(%q) - error %v", err)
-		return Info{}, err
+		return nil, err
 	}
 
 	return bi, err
diff --git a/repo/content/content_manager_indexes.go b/repo/content/content_manager_indexes.go
index 7c748037..0c9c87a2 100644
--- a/repo/content/content_manager_indexes.go
+++ b/repo/content/content_manager_indexes.go
@@ -162,9 +162,9 @@ func dropContentsFromBuilder(ctx context.Context, bld packIndexBuilder, opt Comp
 		formatLog(ctx).Debugf("drop-content-deleted-before %v", opt.DropDeletedBefore)
 
 		for _, i := range bld {
-			if i.Deleted && i.Timestamp().Before(opt.DropDeletedBefore) {
-				formatLog(ctx).Debugf("drop-from-index-old-deleted %v %v", i.ID, i.Timestamp())
-				delete(bld, i.ID)
+			if i.GetDeleted() && i.Timestamp().Before(opt.DropDeletedBefore) {
+				formatLog(ctx).Debugf("drop-from-index-old-deleted %v %v", i.GetContentID(), i.Timestamp())
+				delete(bld, i.GetContentID())
 			}
 		}
 
diff --git a/repo/content/content_manager_iterate.go b/repo/content/content_manager_iterate.go
index 169ff6b9..aa6d3c69 100644
--- a/repo/content/content_manager_iterate.go
+++ b/repo/content/content_manager_iterate.go
@@ -118,16 +118,16 @@ func (bm *WriteManager) IterateContents(ctx context.Context, opts IterateOptions
 
 	invokeCallback := func(i Info) error {
 		if !opts.IncludeDeleted {
-			if ci, ok := uncommitted[i.ID]; ok {
-				if ci.Deleted {
+			if ci, ok := uncommitted[i.GetContentID()]; ok {
+				if ci.GetDeleted() {
 					return nil
 				}
-			} else if i.Deleted {
+			} else if i.GetDeleted() {
 				return nil
 			}
 		}
 
-		if !opts.Range.Contains(i.ID) {
+		if !opts.Range.Contains(i.GetContentID()) {
 			return nil
 		}
 
@@ -140,7 +140,7 @@ func (bm *WriteManager) IterateContents(ctx context.Context, opts IterateOptions
 	}
 
 	for _, bi := range uncommitted {
-		_ = invokeCallback(*bi)
+		_ = invokeCallback(bi)
 	}
 
 	if err := bm.committedContents.listContents(opts.Range, invokeCallback); err != nil {
@@ -192,18 +192,18 @@ func (bm *WriteManager) IteratePacks(ctx context.Context, options IteratePackOpt
 			IncludeDeleted: options.IncludePacksWithOnlyDeletedContent,
 		},
 		func(ci Info) error {
-			if !options.matchesBlob(ci.PackBlobID) {
+			if !options.matchesBlob(ci.GetPackBlobID()) {
 				return nil
 			}
 
-			pi := packUsage[ci.PackBlobID]
+			pi := packUsage[ci.GetPackBlobID()]
 			if pi == nil {
 				pi = &PackInfo{}
-				packUsage[ci.PackBlobID] = pi
+				packUsage[ci.GetPackBlobID()] = pi
 			}
-			pi.PackID = ci.PackBlobID
+			pi.PackID = ci.GetPackBlobID()
 			pi.ContentCount++
-			pi.TotalSize += int64(ci.PackedLength)
+			pi.TotalSize += int64(ci.GetPackedLength())
 			if options.IncludeContentInfos {
 				pi.ContentInfos = append(pi.ContentInfos, ci)
 			}
diff --git a/repo/content/content_manager_lock_free.go b/repo/content/content_manager_lock_free.go
index 3fba83e8..dea02144 100644
--- a/repo/content/content_manager_lock_free.go
+++ b/repo/content/content_manager_lock_free.go
@@ -66,16 +66,16 @@ func ValidatePrefix(prefix ID) error {
 	return errors.Errorf("invalid prefix, must be a empty or single letter between 'g' and 'z'")
 }
 
-func (bm *WriteManager) getContentDataUnlocked(ctx context.Context, pp *pendingPackInfo, bi *Info) ([]byte, error) {
+func (bm *WriteManager) getContentDataUnlocked(ctx context.Context, pp *pendingPackInfo, bi Info) ([]byte, error) {
 	var payload []byte
 
-	if pp != nil && pp.packBlobID == bi.PackBlobID {
+	if pp != nil && pp.packBlobID == bi.GetPackBlobID() {
 		// we need to use a lock here in case somebody else writes to the pack at the same time.
-		payload = pp.currentPackData.AppendSectionTo(nil, int(bi.PackOffset), int(bi.PackedLength))
+		payload = pp.currentPackData.AppendSectionTo(nil, int(bi.GetPackOffset()), int(bi.GetPackedLength()))
 	} else {
 		var err error
 
-		payload, err = bm.getCacheForContentID(bi.ID).getContent(ctx, cacheKey(bi.ID), bi.PackBlobID, int64(bi.PackOffset), int64(bi.PackedLength))
+		payload, err = bm.getCacheForContentID(bi.GetContentID()).getContent(ctx, cacheKey(bi.GetContentID()), bi.GetPackBlobID(), int64(bi.GetPackOffset()), int64(bi.GetPackedLength()))
 		if err != nil {
 			return nil, errors.Wrap(err, "getCacheForContentID")
 		}
@@ -89,11 +89,11 @@ func (bm *WriteManager) preparePackDataContent(ctx context.Context, pp *pendingP
 	haveContent := false
 
 	for _, info := range pp.currentPackItems {
-		if info.PackBlobID == pp.packBlobID {
+		if info.GetPackBlobID() == pp.packBlobID {
 			haveContent = true
 		}
 
-		formatLog(ctx).Debugf("add-to-pack %v %v p:%v %v d:%v", pp.packBlobID, info.ID, info.PackBlobID, info.PackedLength, info.Deleted)
+		formatLog(ctx).Debugf("add-to-pack %v %v p:%v %v d:%v", pp.packBlobID, info.GetContentID(), info.GetPackBlobID(), info.GetPackedLength(), info.GetDeleted())
 
 		packFileIndex.Add(info)
 	}
diff --git a/repo/content/format.go b/repo/content/format.go
deleted file mode 100644
index b232f70d..00000000
--- a/repo/content/format.go
+++ /dev/null
@@ -1,82 +0,0 @@
-package content
-
-import (
-	"encoding/binary"
-	"math"
-
-	"github.com/pkg/errors"
-)
-
-const (
-	timestampShift           = 16
-	packedFormatVersionShift = 8
-)
-
-// Format describes a format of a single pack index. The actual structure is not used,
-// it's purely for documentation purposes.
-// The struct is byte-aligned.
-type Format struct {
-	Version    byte   // format version number must be 0x01
-	KeySize    byte   // size of each key in bytes
-	EntrySize  uint16 // size of each entry in bytes, big-endian
-	EntryCount uint32 // number of sorted (key,value) entries that follow
-
-	Entries []struct {
-		Key   []byte // key bytes (KeySize)
-		Entry entry
-	}
-
-	ExtraData []byte // extra data
-}
-
-type entry struct {
-	// big endian:
-	// 48 most significant bits - 48-bit timestamp in seconds since 1970/01/01 UTC
-	// 8 bits - format version (currently == 1)
-	// 8 least significant bits - length of pack content ID
-	timestampAndFlags uint64 //
-	packFileOffset    uint32 // 4 bytes, big endian, offset within index file where pack (blob) ID begins
-	packedOffset      uint32 // 4 bytes, big endian, offset within pack file where the contents begin
-	packedLength      uint32 // 4 bytes, big endian, content length
-}
-
-func (e *entry) parse(b []byte) error {
-	if len(b) < entryFixedHeaderLength {
-		return errors.Errorf("invalid entry length: %v", len(b))
-	}
-
-	e.timestampAndFlags = binary.BigEndian.Uint64(b[0:8])
-	e.packFileOffset = binary.BigEndian.Uint32(b[8:12])
-	e.packedOffset = binary.BigEndian.Uint32(b[12:16])
-	e.packedLength = binary.BigEndian.Uint32(b[16:20])
-
-	return nil
-}
-
-func (e *entry) IsDeleted() bool {
-	return e.packedOffset&0x80000000 != 0
-}
-
-func (e *entry) TimestampSeconds() int64 {
-	return int64(e.timestampAndFlags >> timestampShift)
-}
-
-func (e *entry) PackedFormatVersion() byte {
-	return byte(e.timestampAndFlags >> packedFormatVersionShift)
-}
-
-func (e *entry) PackFileLength() byte {
-	return byte(e.timestampAndFlags)
-}
-
-func (e *entry) PackFileOffset() uint32 {
-	return e.packFileOffset
-}
-
-func (e *entry) PackedOffset() uint32 {
-	return e.packedOffset & math.MaxInt32
-}
-
-func (e *entry) PackedLength() uint32 {
-	return e.packedLength
-}
diff --git a/repo/content/index.go b/repo/content/index.go
index 657eb68c..af04ca42 100644
--- a/repo/content/index.go
+++ b/repo/content/index.go
@@ -1,14 +1,10 @@
 package content
 
 import (
-	"bytes"
 	"encoding/binary"
 	"io"
-	"sort"
 
 	"github.com/pkg/errors"
-
-	"github.com/kopia/kopia/repo/blob"
 )
 
 const (
@@ -23,7 +19,7 @@ type packIndex interface {
 
 	ApproximateCount() int
 
-	GetInfo(contentID ID) (*Info, error)
+	GetInfo(contentID ID) (Info, 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
@@ -56,15 +52,8 @@ func PrefixRange(prefix ID) IDRange {
 // AllNonPrefixedIDs is an IDRange that contains all valid IDs non-prefixed IDs ('0' .. 'f').
 var AllNonPrefixedIDs = IDRange{"0", "g"}
 
-type index struct {
-	hdr      headerInfo
-	readerAt io.ReaderAt
-	// v1 index does not explicitly store per-content length so we compute it from packed length and fixed overhead
-	// provided by the encryptor.
-	v1PerContentOverhead uint32
-}
-
 type headerInfo struct {
+	version    int
 	keySize    int
 	valueSize  int
 	entryCount int
@@ -77,11 +66,8 @@ func readHeader(readerAt io.ReaderAt) (headerInfo, error) {
 		return headerInfo{}, errors.Wrap(err, "invalid header")
 	}
 
-	if header[0] != 1 {
-		return headerInfo{}, errors.Errorf("invalid header format: %v", header[0])
-	}
-
 	hi := headerInfo{
+		version:    int(header[0]),
 		keySize:    int(header[1]),
 		valueSize:  int(binary.BigEndian.Uint16(header[2:4])),
 		entryCount: int(binary.BigEndian.Uint32(header[4:8])),
@@ -94,203 +80,6 @@ func readHeader(readerAt io.ReaderAt) (headerInfo, error) {
 	return hi, nil
 }
 
-func (b *index) ApproximateCount() int {
-	return b.hdr.entryCount
-}
-
-// 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 *index) Iterate(r IDRange, cb func(Info) error) error {
-	startPos, err := b.findEntryPosition(r.StartID)
-	if err != nil {
-		return errors.Wrap(err, "could not find starting position")
-	}
-
-	stride := b.hdr.keySize + b.hdr.valueSize
-	entry := make([]byte, stride)
-
-	for i := startPos; i < b.hdr.entryCount; i++ {
-		n, err := b.readerAt.ReadAt(entry, int64(packHeaderSize+stride*i))
-		if err != nil || n != len(entry) {
-			return errors.Wrap(err, "unable to read from index")
-		}
-
-		key := entry[0:b.hdr.keySize]
-		value := entry[b.hdr.keySize:]
-
-		i, err := b.entryToInfo(bytesToContentID(key), value)
-		if err != nil {
-			return errors.Wrap(err, "invalid index data")
-		}
-
-		if i.ID >= r.EndID {
-			break
-		}
-
-		if err := cb(i); err != nil {
-			return err
-		}
-	}
-
-	return nil
-}
-
-func (b *index) findEntryPosition(contentID ID) (int, error) {
-	stride := b.hdr.keySize + b.hdr.valueSize
-
-	var entryArr [maxEntrySize]byte
-
-	var entryBuf []byte
-
-	if stride <= len(entryArr) {
-		entryBuf = entryArr[0:stride]
-	} else {
-		entryBuf = make([]byte, stride)
-	}
-
-	var readErr error
-
-	pos := sort.Search(b.hdr.entryCount, func(p int) bool {
-		if readErr != nil {
-			return false
-		}
-		_, err := b.readerAt.ReadAt(entryBuf, int64(packHeaderSize+stride*p))
-		if err != nil {
-			readErr = err
-			return false
-		}
-
-		return bytesToContentID(entryBuf[0:b.hdr.keySize]) >= contentID
-	})
-
-	return pos, readErr
-}
-
-func (b *index) findEntryPositionExact(idBytes, entryBuf []byte) (int, error) {
-	stride := b.hdr.keySize + b.hdr.valueSize
-
-	var readErr error
-
-	pos := sort.Search(b.hdr.entryCount, func(p int) bool {
-		if readErr != nil {
-			return false
-		}
-		_, err := b.readerAt.ReadAt(entryBuf, int64(packHeaderSize+stride*p))
-		if err != nil {
-			readErr = err
-			return false
-		}
-
-		return contentIDBytesGreaterOrEqual(entryBuf[0:b.hdr.keySize], idBytes)
-	})
-
-	return pos, readErr
-}
-
-func (b *index) findEntry(output []byte, contentID ID) ([]byte, error) {
-	var hashBuf [maxContentIDSize]byte
-
-	key := contentIDToBytes(hashBuf[:0], contentID)
-
-	// empty index blob, this is possible when compaction removes exactly everything
-	if b.hdr.keySize == unknownKeySize {
-		return nil, nil
-	}
-
-	if len(key) != b.hdr.keySize {
-		return nil, errors.Errorf("invalid content ID: %q (%v vs %v)", contentID, len(key), b.hdr.keySize)
-	}
-
-	stride := b.hdr.keySize + b.hdr.valueSize
-
-	var entryArr [maxEntrySize]byte
-
-	var entryBuf []byte
-
-	if stride <= len(entryArr) {
-		entryBuf = entryArr[0:stride]
-	} else {
-		entryBuf = make([]byte, stride)
-	}
-
-	position, err := b.findEntryPositionExact(key, entryBuf)
-	if err != nil {
-		return nil, err
-	}
-
-	if position >= b.hdr.entryCount {
-		return nil, nil
-	}
-
-	if _, err := b.readerAt.ReadAt(entryBuf, int64(packHeaderSize+stride*position)); err != nil {
-		return nil, errors.Wrap(err, "error reading header")
-	}
-
-	if bytes.Equal(entryBuf[0:len(key)], key) {
-		return append(output, entryBuf[len(key):]...), nil
-	}
-
-	return nil, nil
-}
-
-// GetInfo returns information about a given content. If a content is not found, nil is returned.
-func (b *index) GetInfo(contentID ID) (*Info, error) {
-	var entryBuf [maxEntrySize]byte
-
-	e, err := b.findEntry(entryBuf[:0], contentID)
-	if err != nil {
-		return nil, err
-	}
-
-	if e == nil {
-		return nil, nil
-	}
-
-	i, err := b.entryToInfo(contentID, e)
-	if err != nil {
-		return nil, err
-	}
-
-	return &i, err
-}
-
-func (b *index) entryToInfo(contentID ID, entryData []byte) (Info, error) {
-	var e entry
-	if err := e.parse(entryData); err != nil {
-		return Info{}, err
-	}
-
-	packFile := make([]byte, e.PackFileLength())
-
-	n, err := b.readerAt.ReadAt(packFile, int64(e.PackFileOffset()))
-	if err != nil || n != int(e.PackFileLength()) {
-		return Info{}, errors.Wrap(err, "can't read pack content ID")
-	}
-
-	pl := e.PackedLength()
-
-	return Info{
-		ID:               contentID,
-		Deleted:          e.IsDeleted(),
-		TimestampSeconds: e.TimestampSeconds(),
-		FormatVersion:    e.PackedFormatVersion(),
-		PackOffset:       e.PackedOffset(),
-		PackedLength:     pl,
-		OriginalLength:   pl - b.v1PerContentOverhead,
-		PackBlobID:       blob.ID(packFile),
-	}, nil
-}
-
-// Close closes the index and the underlying reader.
-func (b *index) Close() error {
-	if closer, ok := b.readerAt.(io.Closer); ok {
-		return closer.Close()
-	}
-
-	return nil
-}
-
 // openPackIndex reads an Index from a given reader. The caller must call Close() when the index is no longer used.
 func openPackIndex(readerAt io.ReaderAt, v1PerContentOverhead uint32) (packIndex, error) {
 	h, err := readHeader(readerAt)
@@ -298,5 +87,9 @@ func openPackIndex(readerAt io.ReaderAt, v1PerContentOverhead uint32) (packIndex
 		return nil, errors.Wrap(err, "invalid header")
 	}
 
-	return &index{hdr: h, readerAt: readerAt, v1PerContentOverhead: v1PerContentOverhead}, nil
+	if h.version != 1 {
+		return nil, errors.Errorf("invalid header format: %v", h.version)
+	}
+
+	return &indexV1{hdr: h, readerAt: readerAt, v1PerContentOverhead: v1PerContentOverhead}, nil
 }
diff --git a/repo/content/index_v1.go b/repo/content/index_v1.go
new file mode 100644
index 00000000..1691e677
--- /dev/null
+++ b/repo/content/index_v1.go
@@ -0,0 +1,425 @@
+package content
+
+import (
+	"bufio"
+	"bytes"
+	"crypto/rand"
+	"encoding/binary"
+	"io"
+	"sort"
+	"time"
+
+	"github.com/pkg/errors"
+
+	"github.com/kopia/kopia/repo/blob"
+)
+
+const (
+	packHeaderSize = 8
+	deletedMarker  = 0x80000000
+
+	entryFixedHeaderLength = 20
+	randomSuffixSize       = 32
+)
+
+// FormatV1 describes a format of a single pack index. The actual structure is not used,
+// it's purely for documentation purposes.
+// The struct is byte-aligned.
+type FormatV1 struct {
+	Version    byte   // format version number must be 0x01
+	KeySize    byte   // size of each key in bytes
+	EntrySize  uint16 // size of each entry in bytes, big-endian
+	EntryCount uint32 // number of sorted (key,value) entries that follow
+
+	Entries []struct {
+		Key   []byte // key bytes (KeySize)
+		Entry indexEntryInfoV1
+	}
+
+	ExtraData []byte // extra data
+}
+
+type indexEntryInfoV1 struct {
+	data      string // basically a byte array, but immutable
+	contentID ID
+	b         *indexV1
+}
+
+func (e indexEntryInfoV1) GetContentID() ID {
+	return e.contentID
+}
+
+// entry bytes 0..5: 48-bit big-endian timestamp in seconds since 1970/01/01 UTC.
+func (e indexEntryInfoV1) GetTimestampSeconds() int64 {
+	return decodeBigEndianUint48(e.data)
+}
+
+// entry byte 6: format version (currently always == 1).
+func (e indexEntryInfoV1) GetFormatVersion() byte {
+	return e.data[6]
+}
+
+// entry byte 7: length of pack content ID
+// entry bytes 8..11: 4 bytes, big endian, offset within index file where pack (blob) ID begins.
+func (e indexEntryInfoV1) GetPackBlobID() blob.ID {
+	nameLength := int(e.data[7])
+	nameOffset := decodeBigEndianUint32(e.data[8:])
+
+	var nameBuf [256]byte
+
+	n, err := e.b.readerAt.ReadAt(nameBuf[0:nameLength], int64(nameOffset))
+	if err != nil || n != nameLength {
+		return "-invalid-blob-id-"
+	}
+
+	return blob.ID(nameBuf[0:nameLength])
+}
+
+// entry bytes 12..15 - deleted flag (MSBit), 31 lower bits encode pack offset.
+func (e indexEntryInfoV1) GetDeleted() bool {
+	return e.data[12]&0x80 != 0
+}
+
+func (e indexEntryInfoV1) GetPackOffset() uint32 {
+	const packOffsetMask = 1<<31 - 1
+	return decodeBigEndianUint32(e.data[12:]) & packOffsetMask
+}
+
+// bytes 16..19: 4 bytes, big endian, content length.
+func (e indexEntryInfoV1) GetPackedLength() uint32 {
+	return decodeBigEndianUint32(e.data[16:])
+}
+
+func (e indexEntryInfoV1) GetOriginalLength() uint32 {
+	return e.GetPackedLength() - e.b.v1PerContentOverhead
+}
+
+func (e indexEntryInfoV1) Timestamp() time.Time {
+	return time.Unix(e.GetTimestampSeconds(), 0)
+}
+
+var _ Info = indexEntryInfoV1{}
+
+func decodeBigEndianUint48(d string) int64 {
+	return int64(d[0])<<40 | int64(d[1])<<32 | int64(d[2])<<24 | int64(d[3])<<16 | int64(d[4])<<8 | int64(d[5])
+}
+
+func decodeBigEndianUint32(d string) uint32 {
+	return uint32(d[0])<<24 | uint32(d[1])<<16 | uint32(d[2])<<8 | uint32(d[3])
+}
+
+type indexV1 struct {
+	hdr      headerInfo
+	readerAt io.ReaderAt
+	// v1 index does not explicitly store per-content length so we compute it from packed length and fixed overhead
+	// provided by the encryptor.
+	v1PerContentOverhead uint32
+}
+
+func (b *indexV1) ApproximateCount() int {
+	return b.hdr.entryCount
+}
+
+// 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 {
+	startPos, err := b.findEntryPosition(r.StartID)
+	if err != nil {
+		return errors.Wrap(err, "could not find starting position")
+	}
+
+	stride := b.hdr.keySize + b.hdr.valueSize
+	entry := make([]byte, stride)
+
+	for i := startPos; i < b.hdr.entryCount; i++ {
+		n, err := b.readerAt.ReadAt(entry, int64(packHeaderSize+stride*i))
+		if err != nil || n != len(entry) {
+			return errors.Wrap(err, "unable to read from index")
+		}
+
+		key := entry[0:b.hdr.keySize]
+
+		contentID := bytesToContentID(key)
+		if contentID >= r.EndID {
+			break
+		}
+
+		i, err := b.entryToInfo(contentID, entry[b.hdr.keySize:])
+		if err != nil {
+			return errors.Wrap(err, "invalid index data")
+		}
+
+		if err := cb(i); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func (b *indexV1) findEntryPosition(contentID ID) (int, error) {
+	stride := b.hdr.keySize + b.hdr.valueSize
+
+	var entryArr [maxEntrySize]byte
+
+	var entryBuf []byte
+
+	if stride <= len(entryArr) {
+		entryBuf = entryArr[0:stride]
+	} else {
+		entryBuf = make([]byte, stride)
+	}
+
+	var readErr error
+
+	pos := sort.Search(b.hdr.entryCount, func(p int) bool {
+		if readErr != nil {
+			return false
+		}
+		_, err := b.readerAt.ReadAt(entryBuf, int64(packHeaderSize+stride*p))
+		if err != nil {
+			readErr = err
+			return false
+		}
+
+		return bytesToContentID(entryBuf[0:b.hdr.keySize]) >= contentID
+	})
+
+	return pos, readErr
+}
+
+func (b *indexV1) findEntryPositionExact(idBytes, entryBuf []byte) (int, error) {
+	stride := b.hdr.keySize + b.hdr.valueSize
+
+	var readErr error
+
+	pos := sort.Search(b.hdr.entryCount, func(p int) bool {
+		if readErr != nil {
+			return false
+		}
+		_, err := b.readerAt.ReadAt(entryBuf, int64(packHeaderSize+stride*p))
+		if err != nil {
+			readErr = err
+			return false
+		}
+
+		return contentIDBytesGreaterOrEqual(entryBuf[0:b.hdr.keySize], idBytes)
+	})
+
+	return pos, readErr
+}
+
+func (b *indexV1) findEntry(output []byte, contentID ID) ([]byte, error) {
+	var hashBuf [maxContentIDSize]byte
+
+	key := contentIDToBytes(hashBuf[:0], contentID)
+
+	// empty index blob, this is possible when compaction removes exactly everything
+	if b.hdr.keySize == unknownKeySize {
+		return nil, nil
+	}
+
+	if len(key) != b.hdr.keySize {
+		return nil, errors.Errorf("invalid content ID: %q (%v vs %v)", contentID, len(key), b.hdr.keySize)
+	}
+
+	stride := b.hdr.keySize + b.hdr.valueSize
+
+	var entryArr [maxEntrySize]byte
+
+	var entryBuf []byte
+
+	if stride <= len(entryArr) {
+		entryBuf = entryArr[0:stride]
+	} else {
+		entryBuf = make([]byte, stride)
+	}
+
+	position, err := b.findEntryPositionExact(key, entryBuf)
+	if err != nil {
+		return nil, err
+	}
+
+	if position >= b.hdr.entryCount {
+		return nil, nil
+	}
+
+	if _, err := b.readerAt.ReadAt(entryBuf, int64(packHeaderSize+stride*position)); err != nil {
+		return nil, errors.Wrap(err, "error reading header")
+	}
+
+	if bytes.Equal(entryBuf[0:len(key)], key) {
+		return append(output, entryBuf[len(key):]...), nil
+	}
+
+	return nil, nil
+}
+
+// GetInfo returns information about a given content. If a content is not found, nil is returned.
+func (b *indexV1) GetInfo(contentID ID) (Info, error) {
+	var entryBuf [maxEntrySize]byte
+
+	e, err := b.findEntry(entryBuf[:0], contentID)
+	if err != nil {
+		return nil, err
+	}
+
+	if e == nil {
+		return nil, nil
+	}
+
+	return b.entryToInfo(contentID, e)
+}
+
+func (b *indexV1) entryToInfo(contentID ID, entryData []byte) (Info, error) {
+	if len(entryData) < entryFixedHeaderLength {
+		return nil, errors.Errorf("invalid entry length: %v", len(entryData))
+	}
+
+	// convert to 'entryData' string to make it read-only
+	return indexEntryInfoV1{string(entryData), contentID, b}, nil
+}
+
+// Close closes the index and the underlying reader.
+func (b *indexV1) Close() error {
+	if closer, ok := b.readerAt.(io.Closer); ok {
+		return closer.Close()
+	}
+
+	return nil
+}
+
+type indexBuilderV1 struct {
+	packBlobIDOffsets map[blob.ID]uint32
+	entryCount        int
+	keyLength         int
+	entryLength       int
+	extraDataOffset   uint32
+}
+
+// buildV1 writes the pack index to the provided output.
+func (b packIndexBuilder) buildV1(output io.Writer) error {
+	allContents := b.sortedContents()
+	b1 := &indexBuilderV1{
+		packBlobIDOffsets: map[blob.ID]uint32{},
+		keyLength:         -1,
+		entryLength:       entryFixedHeaderLength,
+		entryCount:        len(allContents),
+	}
+
+	w := bufio.NewWriter(output)
+
+	// prepare extra data to be appended at the end of an index.
+	extraData := b1.prepareExtraData(allContents)
+
+	// write header
+	header := make([]byte, packHeaderSize)
+	header[0] = 1 // version
+	header[1] = byte(b1.keyLength)
+	binary.BigEndian.PutUint16(header[2:4], uint16(b1.entryLength))
+	binary.BigEndian.PutUint32(header[4:8], uint32(b1.entryCount))
+
+	if _, err := w.Write(header); err != nil {
+		return errors.Wrap(err, "unable to write header")
+	}
+
+	// write all sorted contents.
+	entry := make([]byte, b1.entryLength)
+
+	for _, it := range allContents {
+		if err := b1.writeEntry(w, it, entry); err != nil {
+			return errors.Wrap(err, "unable to write entry")
+		}
+	}
+
+	if _, err := w.Write(extraData); err != nil {
+		return errors.Wrap(err, "error writing extra data")
+	}
+
+	randomSuffix := make([]byte, randomSuffixSize)
+	if _, err := rand.Read(randomSuffix); err != nil {
+		return errors.Wrap(err, "error getting random bytes for suffix")
+	}
+
+	if _, err := w.Write(randomSuffix); err != nil {
+		return errors.Wrap(err, "error writing extra random suffix to ensure indexes are always globally unique")
+	}
+
+	return w.Flush()
+}
+
+func (b *indexBuilderV1) prepareExtraData(allContents []Info) []byte {
+	var extraData []byte
+
+	var hashBuf [maxContentIDSize]byte
+
+	for i, it := range allContents {
+		if i == 0 {
+			b.keyLength = len(contentIDToBytes(hashBuf[:0], it.GetContentID()))
+		}
+
+		if it.GetPackBlobID() != "" {
+			if _, ok := b.packBlobIDOffsets[it.GetPackBlobID()]; !ok {
+				b.packBlobIDOffsets[it.GetPackBlobID()] = uint32(len(extraData))
+				extraData = append(extraData, []byte(it.GetPackBlobID())...)
+			}
+		}
+	}
+
+	b.extraDataOffset = uint32(packHeaderSize + b.entryCount*(b.keyLength+b.entryLength))
+
+	return extraData
+}
+
+func (b *indexBuilderV1) writeEntry(w io.Writer, it Info, entry []byte) error {
+	var hashBuf [maxContentIDSize]byte
+
+	k := contentIDToBytes(hashBuf[:0], it.GetContentID())
+
+	if len(k) != b.keyLength {
+		return errors.Errorf("inconsistent key length: %v vs %v", len(k), b.keyLength)
+	}
+
+	if err := b.formatEntry(entry, it); err != nil {
+		return errors.Wrap(err, "unable to format entry")
+	}
+
+	if _, err := w.Write(k); err != nil {
+		return errors.Wrap(err, "error writing entry key")
+	}
+
+	if _, err := w.Write(entry); err != nil {
+		return errors.Wrap(err, "error writing entry")
+	}
+
+	return nil
+}
+
+func (b *indexBuilderV1) formatEntry(entry []byte, it Info) error {
+	entryTimestampAndFlags := entry[0:8]
+	entryPackFileOffset := entry[8:12]
+	entryPackedOffset := entry[12:16]
+	entryPackedLength := entry[16:20]
+	timestampAndFlags := uint64(it.GetTimestampSeconds()) << 16 // nolint:gomnd
+
+	packBlobID := it.GetPackBlobID()
+	if len(packBlobID) == 0 {
+		return errors.Errorf("empty pack content ID for %v", it.GetContentID())
+	}
+
+	binary.BigEndian.PutUint32(entryPackFileOffset, b.extraDataOffset+b.packBlobIDOffsets[packBlobID])
+
+	if it.GetDeleted() {
+		binary.BigEndian.PutUint32(entryPackedOffset, it.GetPackOffset()|deletedMarker)
+	} else {
+		binary.BigEndian.PutUint32(entryPackedOffset, it.GetPackOffset())
+	}
+
+	binary.BigEndian.PutUint32(entryPackedLength, it.GetPackedLength())
+	timestampAndFlags |= uint64(it.GetFormatVersion()) << 8 // nolint:gomnd
+	timestampAndFlags |= uint64(len(packBlobID))
+	binary.BigEndian.PutUint64(entryTimestampAndFlags, timestampAndFlags)
+
+	return nil
+}
diff --git a/repo/content/info.go b/repo/content/info.go
index c768b0d9..6ab96032 100644
--- a/repo/content/info.go
+++ b/repo/content/info.go
@@ -24,8 +24,34 @@ func (i ID) HasPrefix() bool {
 }
 
 // Info is an information about a single piece of content managed by Manager.
-type Info struct {
-	ID               ID      `json:"contentID"`
+type Info interface {
+	GetContentID() ID
+	GetPackBlobID() blob.ID
+	GetTimestampSeconds() int64
+	Timestamp() time.Time
+	GetOriginalLength() uint32
+	GetPackedLength() uint32
+	GetPackOffset() uint32
+	GetDeleted() bool
+	GetFormatVersion() byte
+}
+
+type deletedInfo struct {
+	Info
+	deletedTime int64
+}
+
+func (d *deletedInfo) GetDeleted() bool {
+	return true
+}
+
+func (d *deletedInfo) GetTimestampSeconds() int64 {
+	return d.deletedTime
+}
+
+// InfoStruct is an implementation of Info based on a structure.
+type InfoStruct struct {
+	ContentID        ID      `json:"contentID"`
 	PackBlobID       blob.ID `json:"packFile,omitempty"`
 	TimestampSeconds int64   `json:"time"`
 	OriginalLength   uint32  `json:"originalLength"`
@@ -35,7 +61,49 @@ type Info struct {
 	FormatVersion    byte    `json:"formatVersion"`
 }
 
-// Timestamp returns the time when a content was created or deleted.
-func (i *Info) Timestamp() time.Time {
-	return time.Unix(i.TimestampSeconds, 0)
+// GetContentID implements the Info interface.
+func (i *InfoStruct) GetContentID() ID { return i.ContentID }
+
+// GetPackBlobID implements the Info interface.
+func (i *InfoStruct) GetPackBlobID() blob.ID { return i.PackBlobID }
+
+// GetTimestampSeconds implements the Info interface.
+func (i *InfoStruct) GetTimestampSeconds() int64 { return i.TimestampSeconds }
+
+// GetOriginalLength implements the Info interface.
+func (i *InfoStruct) GetOriginalLength() uint32 { return i.OriginalLength }
+
+// GetPackedLength implements the Info interface.
+func (i *InfoStruct) GetPackedLength() uint32 { return i.PackedLength }
+
+// GetPackOffset implements the Info interface.
+func (i *InfoStruct) GetPackOffset() uint32 { return i.PackOffset }
+
+// GetDeleted implements the Info interface.
+func (i *InfoStruct) GetDeleted() bool { return i.Deleted }
+
+// GetFormatVersion implements the Info interface.
+func (i *InfoStruct) GetFormatVersion() byte { return i.FormatVersion }
+
+// Timestamp implements the Info interface.
+func (i *InfoStruct) 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{
+		ContentID:        i.GetContentID(),
+		PackBlobID:       i.GetPackBlobID(),
+		TimestampSeconds: i.GetTimestampSeconds(),
+		OriginalLength:   i.GetOriginalLength(),
+		PackedLength:     i.GetPackedLength(),
+		PackOffset:       i.GetPackOffset(),
+		Deleted:          i.GetDeleted(),
+		FormatVersion:    i.GetFormatVersion(),
+	}
 }
diff --git a/repo/content/merged.go b/repo/content/merged.go
index 2ac7230f..72a5cd0d 100644
--- a/repo/content/merged.go
+++ b/repo/content/merged.go
@@ -33,8 +33,8 @@ func (m mergedIndex) Close() error {
 }
 
 // GetInfo returns information about a single content. If a content is not found, returns (nil,nil).
-func (m mergedIndex) GetInfo(id ID) (*Info, error) {
-	var best *Info
+func (m mergedIndex) GetInfo(id ID) (Info, error) {
+	var best Info
 
 	for _, ndx := range m {
 		i, err := ndx.GetInfo(id)
@@ -43,7 +43,7 @@ func (m mergedIndex) GetInfo(id ID) (*Info, error) {
 		}
 
 		if i != nil {
-			if best == nil || i.TimestampSeconds > best.TimestampSeconds || (i.TimestampSeconds == best.TimestampSeconds && !i.Deleted) {
+			if best == nil || i.GetTimestampSeconds() > best.GetTimestampSeconds() || (i.GetTimestampSeconds() == best.GetTimestampSeconds() && !i.GetDeleted()) {
 				best = i
 			}
 		}
@@ -61,15 +61,15 @@ type nextInfo struct {
 
 func (h nextInfoHeap) Len() int { return len(h) }
 func (h nextInfoHeap) Less(i, j int) bool {
-	if a, b := h[i].it.ID, h[j].it.ID; a != b {
+	if a, b := h[i].it.GetContentID(), h[j].it.GetContentID(); a != b {
 		return a < b
 	}
 
-	if a, b := h[i].it.TimestampSeconds, h[j].it.TimestampSeconds; a != b {
+	if a, b := h[i].it.GetTimestampSeconds(), h[j].it.GetTimestampSeconds(); a != b {
 		return a < b
 	}
 
-	return !h[i].it.Deleted
+	return !h[i].it.GetDeleted()
 }
 
 func (h nextInfoHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
@@ -127,15 +127,15 @@ func (m mergedIndex) Iterate(r IDRange, cb func(i Info) error) error {
 
 	for len(minHeap) > 0 {
 		min := heap.Pop(&minHeap).(*nextInfo)
-		if pendingItem.ID != min.it.ID {
-			if pendingItem.ID != "" {
+		if pendingItem == nil || pendingItem.GetContentID() != min.it.GetContentID() {
+			if pendingItem != nil {
 				if err := cb(pendingItem); err != nil {
 					return err
 				}
 			}
 
 			pendingItem = min.it
-		} else if min.it.TimestampSeconds > pendingItem.TimestampSeconds {
+		} else if min.it.GetTimestampSeconds() > pendingItem.GetTimestampSeconds() {
 			pendingItem = min.it
 		}
 
@@ -145,7 +145,7 @@ func (m mergedIndex) Iterate(r IDRange, cb func(i Info) error) error {
 		}
 	}
 
-	if pendingItem.ID != "" {
+	if pendingItem != nil {
 		return cb(pendingItem)
 	}
 
diff --git a/repo/grpc_repository_client.go b/repo/grpc_repository_client.go
index 78d5d209..5dcc4d58 100644
--- a/repo/grpc_repository_client.go
+++ b/repo/grpc_repository_client.go
@@ -438,7 +438,7 @@ func (r *grpcRepositoryClient) ContentInfo(ctx context.Context, contentID conten
 		return sess.contentInfo(ctx, contentID)
 	})
 	if err != nil {
-		return content.Info{}, err
+		return nil, err
 	}
 
 	return v.(content.Info), nil
@@ -454,8 +454,8 @@ func (r *grpcInnerSession) contentInfo(ctx context.Context, contentID content.ID
 	}) {
 		switch rr := resp.Response.(type) {
 		case *apipb.SessionResponse_GetContentInfo:
-			return content.Info{
-				ID:               content.ID(rr.GetContentInfo.GetInfo().GetId()),
+			return &content.InfoStruct{
+				ContentID:        content.ID(rr.GetContentInfo.GetInfo().GetId()),
 				PackedLength:     rr.GetContentInfo.GetInfo().GetPackedLength(),
 				TimestampSeconds: rr.GetContentInfo.GetInfo().GetTimestampSeconds(),
 				PackBlobID:       blob.ID(rr.GetContentInfo.GetInfo().GetPackBlobId()),
@@ -466,11 +466,11 @@ func (r *grpcInnerSession) contentInfo(ctx context.Context, contentID content.ID
 			}, nil
 
 		default:
-			return content.Info{}, unhandledSessionResponse(resp)
+			return nil, unhandledSessionResponse(resp)
 		}
 	}
 
-	return content.Info{}, errNoSessionResponse()
+	return nil, errNoSessionResponse()
 }
 
 func errorFromSessionResponse(rr *apipb.ErrorResponse) error {
diff --git a/repo/maintenance/content_rewrite.go b/repo/maintenance/content_rewrite.go
index 8a36b853..c3c5960b 100644
--- a/repo/maintenance/content_rewrite.go
+++ b/repo/maintenance/content_rewrite.go
@@ -77,27 +77,27 @@ func RewriteContents(ctx context.Context, rep repo.DirectRepositoryWriter, opt *
 				}
 
 				var optDeleted string
-				if c.Deleted {
+				if c.GetDeleted() {
 					optDeleted = " (deleted)"
 				}
 
 				age := rep.Time().Sub(c.Timestamp())
 				if age < safety.RewriteMinAge {
-					log(ctx).Debugf("Not rewriting content %v (%v bytes) from pack %v%v %v, because it's too new.", c.ID, c.PackedLength, c.PackBlobID, optDeleted, age)
+					log(ctx).Debugf("Not rewriting content %v (%v bytes) from pack %v%v %v, because it's too new.", c.GetContentID(), c.GetPackedLength(), c.GetPackBlobID(), optDeleted, age)
 					continue
 				}
 
-				log(ctx).Debugf("Rewriting content %v (%v bytes) from pack %v%v %v", c.ID, c.PackedLength, c.PackBlobID, optDeleted, age)
+				log(ctx).Debugf("Rewriting content %v (%v bytes) from pack %v%v %v", c.GetContentID(), c.GetPackedLength(), c.GetPackBlobID(), optDeleted, age)
 				mu.Lock()
-				totalBytes += int64(c.PackedLength)
+				totalBytes += int64(c.GetPackedLength())
 				mu.Unlock()
 
 				if opt.DryRun {
 					continue
 				}
 
-				if err := rep.ContentManager().RewriteContent(ctx, c.ID); err != nil {
-					log(ctx).Infof("unable to rewrite content %q: %v", c.ID, err)
+				if err := rep.ContentManager().RewriteContent(ctx, c.GetContentID()); err != nil {
+					log(ctx).Infof("unable to rewrite content %q: %v", c.GetContentID(), err)
 					mu.Lock()
 					failedCount++
 					mu.Unlock()
@@ -160,7 +160,7 @@ func findContentWithFormatVersion(ctx context.Context, rep repo.DirectRepository
 			IncludeDeleted: true,
 		},
 		func(b content.Info) error {
-			if int(b.FormatVersion) == opt.FormatVersion && strings.HasPrefix(string(b.PackBlobID), string(opt.PackPrefix)) {
+			if int(b.GetFormatVersion()) == opt.FormatVersion && strings.HasPrefix(string(b.GetPackBlobID()), string(opt.PackPrefix)) {
 				ch <- contentInfoOrError{Info: b}
 			}
 			return nil
diff --git a/repo/manifest/committed_manifest_manager.go b/repo/manifest/committed_manifest_manager.go
index 67e4186f..4ca4ea42 100644
--- a/repo/manifest/committed_manifest_manager.go
+++ b/repo/manifest/committed_manifest_manager.go
@@ -108,12 +108,12 @@ func (m *committedManifestManager) loadCommittedContentsLocked(ctx context.Conte
 			Range:    content.PrefixRange(ContentPrefix),
 			Parallel: manifestLoadParallelism,
 		}, func(ci content.Info) error {
-			man, err := loadManifestContent(ctx, m.b, ci.ID)
+			man, err := loadManifestContent(ctx, m.b, ci.GetContentID())
 			if err != nil {
 				return err
 			}
 			mu.Lock()
-			manifests[ci.ID] = man
+			manifests[ci.GetContentID()] = man
 			mu.Unlock()
 			return nil
 		})
diff --git a/snapshot/snapshotgc/gc.go b/snapshot/snapshotgc/gc.go
index 35e5baa4..778e20a4 100644
--- a/snapshot/snapshotgc/gc.go
+++ b/snapshot/snapshotgc/gc.go
@@ -100,34 +100,34 @@ func runInternal(ctx context.Context, rep repo.DirectRepositoryWriter, gcDelete
 	// Ensure that the iteration includes deleted contents, so those can be
 	// undeleted (recovered).
 	err := rep.ContentReader().IterateContents(ctx, content.IterateOptions{IncludeDeleted: true}, func(ci content.Info) error {
-		if manifest.ContentPrefix == ci.ID.Prefix() {
-			system.Add(int64(ci.PackedLength))
+		if manifest.ContentPrefix == ci.GetContentID().Prefix() {
+			system.Add(int64(ci.GetPackedLength()))
 			return nil
 		}
 
-		if _, ok := used.Load(ci.ID); ok {
-			if ci.Deleted {
-				if err := rep.ContentManager().UndeleteContent(ctx, ci.ID); err != nil {
+		if _, ok := used.Load(ci.GetContentID()); ok {
+			if ci.GetDeleted() {
+				if err := rep.ContentManager().UndeleteContent(ctx, ci.GetContentID()); err != nil {
 					return errors.Wrapf(err, "Could not undelete referenced content: %v", ci)
 				}
-				undeleted.Add(int64(ci.PackedLength))
+				undeleted.Add(int64(ci.GetPackedLength()))
 			}
 
-			inUse.Add(int64(ci.PackedLength))
+			inUse.Add(int64(ci.GetPackedLength()))
 			return nil
 		}
 
 		if rep.Time().Sub(ci.Timestamp()) < safety.MinContentAgeSubjectToGC {
-			log(ctx).Debugf("recent unreferenced content %v (%v bytes, modified %v)", ci.ID, ci.PackedLength, ci.Timestamp())
-			tooRecent.Add(int64(ci.PackedLength))
+			log(ctx).Debugf("recent unreferenced content %v (%v bytes, modified %v)", ci.GetContentID(), ci.GetPackedLength(), ci.Timestamp())
+			tooRecent.Add(int64(ci.GetPackedLength()))
 			return nil
 		}
 
-		log(ctx).Debugf("unreferenced %v (%v bytes, modified %v)", ci.ID, ci.PackedLength, ci.Timestamp())
-		cnt, totalSize := unused.Add(int64(ci.PackedLength))
+		log(ctx).Debugf("unreferenced %v (%v bytes, modified %v)", ci.GetContentID(), ci.GetPackedLength(), ci.Timestamp())
+		cnt, totalSize := unused.Add(int64(ci.GetPackedLength()))
 
 		if gcDelete {
-			if err := rep.ContentManager().DeleteContent(ctx, ci.ID); err != nil {
+			if err := rep.ContentManager().DeleteContent(ctx, ci.GetContentID()); err != nil {
 				return errors.Wrap(err, "error deleting content")
 			}
 		}


Test patch:
diff --git a/repo/content/committed_content_index_cache_test.go b/repo/content/committed_content_index_cache_test.go
index eb8bbeb5..949e6ea8 100644
--- a/repo/content/committed_content_index_cache_test.go
+++ b/repo/content/committed_content_index_cache_test.go
@@ -46,8 +46,8 @@ func testCache(t *testing.T, cache committedContentIndexCache, fakeTime *faketim
 	}
 
 	require.NoError(t, cache.addContentToCache(ctx, "ndx1", mustBuildPackIndex(t, packIndexBuilder{
-		"c1": &Info{PackBlobID: "p1234", ID: "c1"},
-		"c2": &Info{PackBlobID: "p1234", ID: "c2"},
+		"c1": &InfoStruct{PackBlobID: "p1234", ContentID: "c1"},
+		"c2": &InfoStruct{PackBlobID: "p1234", ContentID: "c2"},
 	})))
 
 	has, err = cache.hasIndexBlobID(ctx, "ndx1")
@@ -58,13 +58,13 @@ func testCache(t *testing.T, cache committedContentIndexCache, fakeTime *faketim
 	}
 
 	require.NoError(t, cache.addContentToCache(ctx, "ndx2", mustBuildPackIndex(t, packIndexBuilder{
-		"c3": &Info{PackBlobID: "p2345", ID: "c3"},
-		"c4": &Info{PackBlobID: "p2345", ID: "c4"},
+		"c3": &InfoStruct{PackBlobID: "p2345", ContentID: "c3"},
+		"c4": &InfoStruct{PackBlobID: "p2345", ContentID: "c4"},
 	})))
 
 	require.NoError(t, cache.addContentToCache(ctx, "ndx2", mustBuildPackIndex(t, packIndexBuilder{
-		"c3": &Info{PackBlobID: "p2345", ID: "c3"},
-		"c4": &Info{PackBlobID: "p2345", ID: "c4"},
+		"c3": &InfoStruct{PackBlobID: "p2345", ContentID: "c3"},
+		"c4": &InfoStruct{PackBlobID: "p2345", ContentID: "c4"},
 	})))
 
 	ndx1, err := cache.openIndex(ctx, "ndx1")
@@ -76,7 +76,7 @@ func testCache(t *testing.T, cache committedContentIndexCache, fakeTime *faketim
 	i, err := ndx1.GetInfo("c1")
 	require.NoError(t, err)
 
-	if got, want := i.PackBlobID, blob.ID("p1234"); got != want {
+	if got, want := i.GetPackBlobID(), blob.ID("p1234"); got != want {
 		t.Fatalf("unexpected pack blob ID: %v, want %v", got, want)
 	}
 
@@ -85,7 +85,7 @@ func testCache(t *testing.T, cache committedContentIndexCache, fakeTime *faketim
 	i, err = ndx2.GetInfo("c3")
 	require.NoError(t, err)
 
-	if got, want := i.PackBlobID, blob.ID("p2345"); got != want {
+	if got, want := i.GetPackBlobID(), blob.ID("p2345"); got != want {
 		t.Fatalf("unexpected pack blob ID: %v, want %v", got, want)
 	}
 
diff --git a/repo/content/content_manager_test.go b/repo/content/content_manager_test.go
index 770c5454..4409f024 100644
--- a/repo/content/content_manager_test.go
+++ b/repo/content/content_manager_test.go
@@ -649,24 +649,21 @@ func TestUndeleteContentSimple(t *testing.T) {
 
 		got, want := getContentInfo(t, bm, tc.cid), tc.info
 
-		if got.Deleted {
+		if got.GetDeleted() {
 			t.Error("Content marked as deleted:", got)
 		}
 
-		if got.PackBlobID == "" {
+		if got.GetPackBlobID() == "" {
 			t.Error("Empty pack id for undeleted content:", tc.cid)
 		}
 
-		if got.PackOffset == 0 {
+		if got.GetPackOffset() == 0 {
 			t.Error("0 offset for undeleted content:", tc.cid)
 		}
 
-		// ignore different timestamps, pack id and pack offset
-		got.TimestampSeconds = want.TimestampSeconds
-		got.PackBlobID = want.PackBlobID
-		got.PackOffset = want.PackOffset
-
-		require.Equal(t, want, got)
+		if diff := infoDiff(want, got, "GetTimestampSeconds", "GetPackBlobID", "GetPackOffset"); len(diff) > 0 {
+			t.Fatalf("diff: %v", diff)
+		}
 	}
 
 	// ensure content is still there after flushing
@@ -700,25 +697,21 @@ func TestUndeleteContentSimple(t *testing.T) {
 		t.Log("case name:", tc.name)
 		got := getContentInfo(t, bm, tc.cid)
 
-		if got.Deleted {
+		if got.GetDeleted() {
 			t.Error("Content marked as deleted:", got)
 		}
 
-		if got.PackBlobID == "" {
+		if got.GetPackBlobID() == "" {
 			t.Error("Empty pack id for undeleted content:", tc.cid)
 		}
 
-		if got.PackOffset == 0 {
+		if got.GetPackOffset() == 0 {
 			t.Error("0 offset for undeleted content:", tc.cid)
 		}
 
 		// ignore different timestamps, pack id and pack offset
-		got.TimestampSeconds = tc.want.TimestampSeconds
-		got.PackBlobID = tc.want.PackBlobID
-		got.PackOffset = tc.want.PackOffset
-
-		if got != tc.want {
-			t.Errorf("content info does not match.\nwant: %#v\ngot:  %#v", tc.want, got)
+		if diff := infoDiff(tc.want, got, "GetPackBlobID", "GetTimestampSeconds"); len(diff) > 0 {
+			t.Errorf("content info does not match. diff: %v", diff)
 		}
 	}
 }
@@ -806,7 +799,7 @@ func TestUndeleteContent(t *testing.T) {
 			t.Fatalf("unable to get content info for %v: %v", id, err)
 		}
 
-		if got, want := ci.Deleted, false; got != want {
+		if got, want := ci.GetDeleted(), false; got != want {
 			t.Fatalf("content %v was not undeleted: %v", id, ci)
 		}
 	}
@@ -822,7 +815,7 @@ func TestUndeleteContent(t *testing.T) {
 			t.Fatalf("unable to get content info for %v: %v", id, err)
 		}
 
-		if got, want := ci.Deleted, false; got != want {
+		if got, want := ci.GetDeleted(), false; got != want {
 			t.Fatalf("content %v was not undeleted: %v", id, ci)
 		}
 	}
@@ -837,7 +830,7 @@ func TestUndeleteContent(t *testing.T) {
 			t.Fatalf("unable to get content info for %v: %v", id, err)
 		}
 
-		if got, want := ci.Deleted, false; got != want {
+		if got, want := ci.GetDeleted(), false; got != want {
 			t.Fatalf("content %v was not undeleted: %v", id, ci)
 		}
 	}
@@ -878,7 +871,6 @@ func TestDeleteAfterUndelete(t *testing.T) {
 	}
 
 	c2Want := getContentInfo(t, bm, content2)
-	c2Want.Deleted = true
 
 	// delete content1 before flushing
 	deleteContentAfterUndeleteAndCheck(ctx, t, bm, content1, c1Want)
@@ -888,6 +880,7 @@ func TestDeleteAfterUndelete(t *testing.T) {
 		t.Fatal("error while flushing:", err)
 	}
 
+	c2Want = withDeleted{c2Want, true}
 	deleteContentAfterUndeleteAndCheck(ctx, t, bm, content2, c2Want)
 }
 
@@ -896,15 +889,12 @@ func deleteContentAfterUndeleteAndCheck(ctx context.Context, t *testing.T, bm *W
 	deleteContent(ctx, t, bm, id)
 
 	got := getContentInfo(t, bm, id)
-	if !got.Deleted {
+	if !got.GetDeleted() {
 		t.Fatalf("Expected content %q to be deleted, got: %#v", id, got)
 	}
 
-	// ignore timestamp
-	got.TimestampSeconds = want.TimestampSeconds
-
-	if want != got {
-		t.Fatalf("Content %q info does not match\nwant: %#v\ngot:  %#v", id, want, got)
+	if diff := infoDiff(want, got, "GetTimestampSeconds"); len(diff) != 0 {
+		t.Fatalf("Content %q info does not match\ndiff: %v", id, diff)
 	}
 
 	if err := bm.Flush(ctx); err != nil {
@@ -913,15 +903,13 @@ func deleteContentAfterUndeleteAndCheck(ctx context.Context, t *testing.T, bm *W
 
 	// check c1 again
 	got = getContentInfo(t, bm, id)
-	if !got.Deleted {
+	if !got.GetDeleted() {
 		t.Fatal("Expected content to be deleted, got: ", got)
 	}
 
 	// ignore timestamp
-	got.TimestampSeconds = want.TimestampSeconds
-
-	if !reflect.DeepEqual(want, got) {
-		t.Fatalf("Content info does not match\nwant: %#v\ngot:  %#v", want, got)
+	if diff := infoDiff(want, got, "GetTimestampSeconds"); len(diff) != 0 {
+		t.Fatalf("Content info does not match\ndiff: %v", diff)
 	}
 }
 
@@ -1159,7 +1147,7 @@ func verifyAllDataPresent(ctx context.Context, t *testing.T, data map[blob.ID][]
 	bm := newTestContentManager(t, data, nil, nil)
 	defer bm.Close(ctx)
 	_ = bm.IterateContents(ctx, IterateOptions{}, func(ci Info) error {
-		delete(contentIDs, ci.ID)
+		delete(contentIDs, ci.GetContentID())
 		return nil
 	})
 
@@ -1587,7 +1575,7 @@ func TestIterateContents(t *testing.T) {
 				}
 
 				mu.Lock()
-				got[ci.ID] = true
+				got[ci.GetContentID()] = true
 				mu.Unlock()
 				return nil
 			})
@@ -2021,7 +2009,7 @@ func verifyDeletedContentRead(ctx context.Context, t *testing.T, bm *WriteManage
 		return
 	}
 
-	if !ci.Deleted {
+	if !ci.GetDeleted() {
 		t.Errorf("Expected content to be deleted but it is not: %#v", ci)
 	}
 }
diff --git a/repo/content/merged_test.go b/repo/content/merged_test.go
index 3904950e..ca0f18c9 100644
--- a/repo/content/merged_test.go
+++ b/repo/content/merged_test.go
@@ -10,29 +10,29 @@
 
 func TestMerged(t *testing.T) {
 	i1, err := indexWithItems(
-		Info{ID: "aabbcc", TimestampSeconds: 1, PackBlobID: "xx", PackOffset: 11},
-		Info{ID: "ddeeff", TimestampSeconds: 1, PackBlobID: "xx", PackOffset: 111},
-		Info{ID: "z010203", TimestampSeconds: 1, PackBlobID: "xx", PackOffset: 111},
-		Info{ID: "de1e1e", TimestampSeconds: 4, PackBlobID: "xx", PackOffset: 111},
+		&InfoStruct{ContentID: "aabbcc", TimestampSeconds: 1, PackBlobID: "xx", PackOffset: 11},
+		&InfoStruct{ContentID: "ddeeff", TimestampSeconds: 1, PackBlobID: "xx", PackOffset: 111},
+		&InfoStruct{ContentID: "z010203", TimestampSeconds: 1, PackBlobID: "xx", PackOffset: 111},
+		&InfoStruct{ContentID: "de1e1e", TimestampSeconds: 4, PackBlobID: "xx", PackOffset: 111},
 	)
 	if err != nil {
 		t.Fatalf("can't create index: %v", err)
 	}
 
 	i2, err := indexWithItems(
-		Info{ID: "aabbcc", TimestampSeconds: 3, PackBlobID: "yy", PackOffset: 33},
-		Info{ID: "xaabbcc", TimestampSeconds: 1, PackBlobID: "xx", PackOffset: 111},
-		Info{ID: "de1e1e", TimestampSeconds: 4, PackBlobID: "xx", PackOffset: 222, Deleted: true},
+		&InfoStruct{ContentID: "aabbcc", TimestampSeconds: 3, PackBlobID: "yy", PackOffset: 33},
+		&InfoStruct{ContentID: "xaabbcc", TimestampSeconds: 1, PackBlobID: "xx", PackOffset: 111},
+		&InfoStruct{ContentID: "de1e1e", TimestampSeconds: 4, PackBlobID: "xx", PackOffset: 222, Deleted: true},
 	)
 	if err != nil {
 		t.Fatalf("can't create index: %v", err)
 	}
 
 	i3, err := indexWithItems(
-		Info{ID: "aabbcc", TimestampSeconds: 2, PackBlobID: "zz", PackOffset: 22},
-		Info{ID: "ddeeff", TimestampSeconds: 1, PackBlobID: "zz", PackOffset: 222},
-		Info{ID: "k010203", TimestampSeconds: 1, PackBlobID: "xx", PackOffset: 111},
-		Info{ID: "k020304", TimestampSeconds: 1, PackBlobID: "xx", PackOffset: 111},
+		&InfoStruct{ContentID: "aabbcc", TimestampSeconds: 2, PackBlobID: "zz", PackOffset: 22},
+		&InfoStruct{ContentID: "ddeeff", TimestampSeconds: 1, PackBlobID: "zz", PackOffset: 222},
+		&InfoStruct{ContentID: "k010203", TimestampSeconds: 1, PackBlobID: "xx", PackOffset: 111},
+		&InfoStruct{ContentID: "k020304", TimestampSeconds: 1, PackBlobID: "xx", PackOffset: 111},
 	)
 	if err != nil {
 		t.Fatalf("can't create index: %v", err)
@@ -45,13 +45,13 @@ func TestMerged(t *testing.T) {
 		t.Fatalf("unable to get info: %v", err)
 	}
 
-	if got, want := i.PackOffset, uint32(33); got != want {
+	if got, want := i.GetPackOffset(), uint32(33); got != want {
 		t.Errorf("invalid pack offset %v, wanted %v", got, want)
 	}
 
 	assertNoError(t, m.Iterate(AllIDs, func(i Info) error {
-		if i.ID == "de1e1e" {
-			if i.Deleted {
+		if i.GetContentID() == "de1e1e" {
+			if i.GetDeleted() {
 				t.Errorf("iteration preferred deleted content over non-deleted")
 			}
 		}
@@ -60,7 +60,7 @@ func TestMerged(t *testing.T) {
 
 	if i, err := m.GetInfo("de1e1e"); err != nil {
 		t.Errorf("error getting deleted content info: %v", err)
-	} else if i.Deleted {
+	} else if i.GetDeleted() {
 		t.Errorf("GetInfo preferred deleted content over non-deleted")
 	}
 
@@ -139,7 +139,7 @@ func iterateIDRange(t *testing.T, m packIndex, r IDRange) []ID {
 	var inOrder []ID
 
 	assertNoError(t, m.Iterate(r, func(i Info) error {
-		inOrder = append(inOrder, i.ID)
+		inOrder = append(inOrder, i.GetContentID())
 		return nil
 	}))
 
diff --git a/repo/content/packindex_test.go b/repo/content/packindex_test.go
index bef09824..623ff2c8 100644
--- a/repo/content/packindex_test.go
+++ b/repo/content/packindex_test.go
@@ -6,7 +6,6 @@
 	"encoding/hex"
 	"fmt"
 	"math/rand"
-	"reflect"
 	"strings"
 	"testing"
 
@@ -70,10 +69,10 @@ func TestPackIndex(t *testing.T) {
 
 	// deleted contents with all information
 	for i := 0; i < 100; i++ {
-		infos = append(infos, Info{
+		infos = append(infos, &InfoStruct{
 			TimestampSeconds: randomUnixTime(),
 			Deleted:          true,
-			ID:               deterministicContentID("deleted-packed", i),
+			ContentID:        deterministicContentID("deleted-packed", i),
 			PackBlobID:       deterministicPackBlobID(i),
 			PackOffset:       deterministicPackedOffset(i),
 			PackedLength:     deterministicPackedLength(i),
@@ -82,9 +81,9 @@ func TestPackIndex(t *testing.T) {
 	}
 	// non-deleted content
 	for i := 0; i < 100; i++ {
-		infos = append(infos, Info{
+		infos = append(infos, &InfoStruct{
 			TimestampSeconds: randomUnixTime(),
-			ID:               deterministicContentID("packed", i),
+			ContentID:        deterministicContentID("packed", i),
 			PackBlobID:       deterministicPackBlobID(i),
 			PackOffset:       deterministicPackedOffset(i),
 			PackedLength:     deterministicPackedLength(i),
@@ -98,7 +97,7 @@ func TestPackIndex(t *testing.T) {
 	b3 := make(packIndexBuilder)
 
 	for _, info := range infos {
-		infoMap[info.ID] = info
+		infoMap[info.GetContentID()] = info
 		b1.Add(info)
 		b2.Add(info)
 		b3.Add(info)
@@ -150,27 +149,27 @@ func TestPackIndex(t *testing.T) {
 	defer ndx.Close()
 
 	for _, want := range infos {
-		info2, err := ndx.GetInfo(want.ID)
+		info2, err := ndx.GetInfo(want.GetContentID())
 		if err != nil {
-			t.Errorf("unable to find %v", want.ID)
+			t.Errorf("unable to find %v", want.GetContentID())
 			continue
 		}
 
-		want.OriginalLength = want.PackedLength - fakeEncryptionOverhead
+		want = withOriginalLength{want, want.GetPackedLength() - fakeEncryptionOverhead}
 
-		if !reflect.DeepEqual(want, *info2) {
-			t.Errorf("invalid value retrieved: %+v, wanted %+v", info2, want)
+		if diff := infoDiff(want, info2); len(diff) != 0 {
+			t.Errorf("invalid value retrieved: diff: %v", diff)
 		}
 	}
 
 	cnt := 0
 
 	assertNoError(t, ndx.Iterate(AllIDs, func(info2 Info) error {
-		want := infoMap[info2.ID]
-		want.OriginalLength = want.PackedLength - fakeEncryptionOverhead
+		want := infoMap[info2.GetContentID()]
+		want = withOriginalLength{want, want.GetPackedLength() - fakeEncryptionOverhead}
 
-		if !reflect.DeepEqual(want, info2) {
-			t.Errorf("invalid value retrieved: %+v, wanted %+v", info2, want)
+		if diff := infoDiff(want, info2); len(diff) != 0 {
+			t.Errorf("invalid value retrieved: %v", diff)
 		}
 		cnt++
 		return nil
@@ -200,8 +199,8 @@ func TestPackIndex(t *testing.T) {
 		prefix := prefix
 		assertNoError(t, ndx.Iterate(PrefixRange(prefix), func(info2 Info) error {
 			cnt2++
-			if !strings.HasPrefix(string(info2.ID), string(prefix)) {
-				t.Errorf("unexpected item %v when iterating prefix %v", info2.ID, prefix)
+			if !strings.HasPrefix(string(info2.GetContentID()), string(prefix)) {
+				t.Errorf("unexpected item %v when iterating prefix %v", info2.GetContentID(), prefix)
 			}
 			return nil
 		}))
@@ -222,7 +221,7 @@ func fuzzTestIndexOpen(originalData []byte) {
 		cnt := 0
 		_ = ndx.Iterate(AllIDs, func(cb Info) error {
 			if cnt < 10 {
-				_, _ = ndx.GetInfo(cb.ID)
+				_, _ = ndx.GetInfo(cb.GetContentID())
 			}
 			cnt++
 			return nil
@@ -266,3 +265,75 @@ func fuzzTest(rnd *rand.Rand, originalData []byte, rounds int, callback func(d [
 		callback(data)
 	}
 }
+
+type withOriginalLength struct {
+	Info
+	originalLength uint32
+}
+
+func (o withOriginalLength) GetOriginalLength() uint32 {
+	return o.originalLength
+}
+
+type withDeleted struct {
+	Info
+	deleted bool
+}
+
+func (o withDeleted) GetDeleted() bool {
+	return o.deleted
+}
+
+func infoDiff(i1, i2 Info, ignore ...string) []string {
+	var diffs []string
+
+	if l, r := i1.GetContentID(), i2.GetContentID(); l != r {
+		diffs = append(diffs, fmt.Sprintf("GetContentID %v != %v", l, r))
+	}
+
+	if l, r := i1.GetPackBlobID(), i2.GetPackBlobID(); l != r {
+		diffs = append(diffs, fmt.Sprintf("GetPackBlobID %v != %v", l, r))
+	}
+
+	if l, r := i1.GetDeleted(), i2.GetDeleted(); l != r {
+		diffs = append(diffs, fmt.Sprintf("GetDeleted %v != %v", l, r))
+	}
+
+	if l, r := i1.GetFormatVersion(), i2.GetFormatVersion(); l != r {
+		diffs = append(diffs, fmt.Sprintf("GetFormatVersion %v != %v", l, r))
+	}
+
+	if l, r := i1.GetOriginalLength(), i2.GetOriginalLength(); l != r {
+		diffs = append(diffs, fmt.Sprintf("GetOriginalLength %v != %v", l, r))
+	}
+
+	if l, r := i1.GetPackOffset(), i2.GetPackOffset(); l != r {
+		diffs = append(diffs, fmt.Sprintf("GetPackOffset %v != %v", l, r))
+	}
+
+	if l, r := i1.GetPackedLength(), i2.GetPackedLength(); l != r {
+		diffs = append(diffs, fmt.Sprintf("GetPackedLength %v != %v", l, r))
+	}
+
+	if l, r := i1.GetTimestampSeconds(), i2.GetTimestampSeconds(); l != r {
+		diffs = append(diffs, fmt.Sprintf("GetTimestampSeconds %v != %v", l, r))
+	}
+
+	var result []string
+
+	for _, v := range diffs {
+		ignored := false
+
+		for _, ign := range ignore {
+			if strings.HasPrefix(v, ign) {
+				ignored = true
+			}
+		}
+
+		if !ignored {
+			result = append(result, v)
+		}
+	}
+
+	return result
+}
diff --git a/repo/maintenance/maintenance_safety_test.go b/repo/maintenance/maintenance_safety_test.go
index 2546337d..8b3f86e9 100644
--- a/repo/maintenance/maintenance_safety_test.go
+++ b/repo/maintenance/maintenance_safety_test.go
@@ -115,7 +115,7 @@ func verifyContentDeletedState(ctx context.Context, t *testing.T, rep repo.Repos
 	require.NoError(t, repo.DirectWriteSession(ctx, rep.(repo.DirectRepository), repo.WriteSessionOptions{}, func(dw repo.DirectRepositoryWriter) error {
 		info, err := dw.ContentReader().ContentInfo(ctx, cid)
 		require.NoError(t, err)
-		require.Equal(t, want, info.Deleted)
+		require.Equal(t, want, info.GetDeleted())
 		return nil
 	}))
 }
diff --git a/repo/object/object_manager_test.go b/repo/object/object_manager_test.go
index be5cc46c..0253f243 100644
--- a/repo/object/object_manager_test.go
+++ b/repo/object/object_manager_test.go
@@ -62,10 +62,10 @@ func (f *fakeContentManager) ContentInfo(ctx context.Context, contentID content.
 	defer f.mu.Unlock()
 
 	if d, ok := f.data[contentID]; ok {
-		return content.Info{ID: contentID, PackedLength: uint32(len(d))}, nil
+		return &content.InfoStruct{ContentID: contentID, PackedLength: uint32(len(d))}, nil
 	}
 
-	return content.Info{}, blob.ErrBlobNotFound
+	return nil, blob.ErrBlobNotFound
 }
 
 func (f *fakeContentManager) Flush(ctx context.Context) error {
diff --git a/snapshot/snapshotmaintenance/snapshotmaintenance_test.go b/snapshot/snapshotmaintenance/snapshotmaintenance_test.go
index e05b6297..07575305 100644
--- a/snapshot/snapshotmaintenance/snapshotmaintenance_test.go
+++ b/snapshot/snapshotmaintenance/snapshotmaintenance_test.go
@@ -131,7 +131,7 @@ func TestMaintenanceReuseDirManifest(t *testing.T) {
 
 	info, err := r2.(repo.DirectRepository).ContentReader().ContentInfo(ctx, content.ID(s2.RootObjectID()))
 	require.NoError(t, err)
-	require.False(t, info.Deleted, "content must not be deleted")
+	require.False(t, info.GetDeleted(), "content must not be deleted")
 
 	_, err = r2.VerifyObject(ctx, s2.RootObjectID())
 	require.NoError(t, err)
@@ -145,7 +145,7 @@ func TestMaintenanceReuseDirManifest(t *testing.T) {
 
 	info, err = th.RepositoryWriter.ContentReader().ContentInfo(ctx, content.ID(s2.RootObjectID()))
 	require.NoError(t, err)
-	require.True(t, info.Deleted, "content must be deleted")
+	require.True(t, info.GetDeleted(), "content must be deleted")
 
 	_, err = th.RepositoryWriter.VerifyObject(ctx, s2.RootObjectID())
 	require.NoError(t, err)
@@ -159,7 +159,7 @@ func TestMaintenanceReuseDirManifest(t *testing.T) {
 	// Was the previous root undeleted
 	info, err = th.RepositoryWriter.ContentReader().ContentInfo(ctx, content.ID(s2.RootObjectID()))
 	require.NoError(t, err)
-	require.False(t, info.Deleted, "content must not be deleted")
+	require.False(t, info.GetDeleted(), "content must not be deleted")
 
 	_, err = th.RepositoryWriter.VerifyObject(ctx, s2.RootObjectID())
 	require.NoError(t, err)
diff --git a/tests/end_to_end_test/policy_test.go b/tests/end_to_end_test/policy_test.go
index 57f63f78..a0416fb2 100644
--- a/tests/end_to_end_test/policy_test.go
+++ b/tests/end_to_end_test/policy_test.go
@@ -21,7 +21,7 @@ func TestDefaultGlobalPolicy(t *testing.T) {
 
 	// verify we created global policy entry
 
-	var contents []content.Info
+	var contents []content.InfoStruct
 
 	mustParseJSONLines(t, e.RunAndExpectSuccess(t, "content", "ls", "--json"), &contents)
 
@@ -29,7 +29,7 @@ func TestDefaultGlobalPolicy(t *testing.T) {
 		t.Fatalf("unexpected number of contents %v, want %v", got, want)
 	}
 
-	globalPolicyContentID := contents[0].ID
+	globalPolicyContentID := contents[0].GetContentID()
 	e.RunAndExpectSuccess(t, "content", "show", "-jz", string(globalPolicyContentID))
 
 	// make sure the policy is visible in the manifest list
diff --git a/tests/end_to_end_test/snapshot_gc_test.go b/tests/end_to_end_test/snapshot_gc_test.go
index b49affc2..49dc402b 100644
--- a/tests/end_to_end_test/snapshot_gc_test.go
+++ b/tests/end_to_end_test/snapshot_gc_test.go
@@ -61,7 +61,7 @@ func TestSnapshotGC(t *testing.T) {
 	e.RunAndExpectFailure(t, "snapshot", "gc", "--safety=none")
 
 	// data block + directory block + manifest block + manifest block from manifest deletion
-	var contentInfo []content.Info
+	var contentInfo []content.InfoStruct
 
 	mustParseJSONLines(t, e.RunAndExpectSuccess(t, "content", "list", "--json"), &contentInfo)
 
diff --git a/tests/repository_stress_test/repository_stress_test.go b/tests/repository_stress_test/repository_stress_test.go
index 20929ee6..04f0cc4b 100644
--- a/tests/repository_stress_test/repository_stress_test.go
+++ b/tests/repository_stress_test/repository_stress_test.go
@@ -285,7 +285,7 @@ func listAndReadAllContents(ctx context.Context, t *testing.T, r repo.DirectRepo
 		ctx,
 		content.IterateOptions{},
 		func(ci content.Info) error {
-			cid := ci.ID
+			cid := ci.GetContentID()
 			_, err := r.ContentReader().GetContent(ctx, cid)
 			if err != nil {
 				if errors.Is(err, content.ErrContentNotFound) && strings.HasPrefix(string(cid), "m") {