xref: /aosp_15_r20/external/avb/tools/transparency/verify/internal/tiles/reader_test.go (revision d289c2ba6de359471b23d594623b906876bc48a0)
1package tiles
2
3import (
4	"bytes"
5	"context"
6	"encoding/hex"
7	"io"
8	"log"
9	"net/http"
10	"net/http/httptest"
11	"testing"
12
13	"github.com/google/go-cmp/cmp"
14	"golang.org/x/mod/sumdb/tlog"
15)
16
17const (
18	tileHeight = 1
19)
20
21// mustHexDecode decodes its input string from hex and panics if this fails.
22func mustHexDecode(b string) []byte {
23	r, err := hex.DecodeString(b)
24	if err != nil {
25		log.Fatalf("unable to decode string %v", err)
26	}
27	return r
28}
29
30// nodeHashes is a structured slice of node hashes for all complete subtrees of a Merkle tree built from test data using the RFC 6962 hashing strategy. The first index in the slice is the tree level (zero being the leaves level), the second is the horizontal index within a level.
31var nodeHashes = [][][]byte{{
32	mustHexDecode("6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d"),
33	mustHexDecode("96a296d224f285c67bee93c30f8a309157f0daa35dc5b87e410b78630a09cfc7"),
34	mustHexDecode("0298d122906dcfc10892cb53a73992fc5b9f493ea4c9badb27b791b4127a7fe7"),
35	mustHexDecode("07506a85fd9dd2f120eb694f86011e5bb4662e5c415a62917033d4a9624487e7"),
36	mustHexDecode("bc1a0643b12e4d2d7c77918f44e0f4f79a838b6cf9ec5b5c283e1f4d88599e6b"),
37	mustHexDecode("4271a26be0d8a84f0bd54c8c302e7cb3a3b5d1fa6780a40bcce2873477dab658"),
38	mustHexDecode("b08693ec2e721597130641e8211e7eedccb4c26413963eee6c1e2ed16ffb1a5f"),
39	mustHexDecode("46f6ffadd3d06a09ff3c5860d2755c8b9819db7df44251788c7d8e3180de8eb1"),
40}, {
41	mustHexDecode("fac54203e7cc696cf0dfcb42c92a1d9dbaf70ad9e621f4bd8d98662f00e3c125"),
42	mustHexDecode("5f083f0a1a33ca076a95279832580db3e0ef4584bdff1f54c8a360f50de3031e"),
43	mustHexDecode("0ebc5d3437fbe2db158b9f126a1d118e308181031d0a949f8dededebc558ef6a"),
44	mustHexDecode("ca854ea128ed050b41b35ffc1b87b8eb2bde461e9e3b5596ece6b9d5975a0ae0"),
45}, {
46	mustHexDecode("d37ee418976dd95753c1c73862b9398fa2a2cf9b4ff0fdfe8b30cd95209614b7"),
47	mustHexDecode("6b47aaf29ee3c2af9af889bc1fb9254dabd31177f16232dd6aab035ca39bf6e4"),
48}, {
49	mustHexDecode("5dc9da79a70659a9ad559cb701ded9a2ab9d823aad2f4960cfe370eff4604328"),
50}}
51
52// testServer serves a tile based log of height 1, using the test data in
53// nodeHashes.
54func testServer(ctx context.Context, t *testing.T) *httptest.Server {
55	t.Helper()
56	return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
57
58		// Parse the tile data out of r.URL.
59		// Strip the leading `/` to get a valid tile path.
60		tile, err := tlog.ParseTilePath(r.URL.String()[1:])
61		if err != nil {
62			t.Fatalf("ParseTilePath(%s): %v", r.URL.String(), err)
63		}
64		// Fill the response with the test nodeHashes ...
65		io.Copy(w, bytes.NewReader(nodeHashes[tile.L][2*tile.N]))
66		if tile.W == 2 {
67			// ... with special handling when the width is 2
68			io.Copy(w, bytes.NewReader(nodeHashes[tile.L][2*tile.N+1]))
69		}
70	}))
71}
72
73func TestReadHashesWithReadTileData(t *testing.T) {
74	ctx := context.Background()
75	s := testServer(ctx, t)
76	defer s.Close()
77
78	for _, tc := range []struct {
79		desc string
80		size uint64
81		want [][]byte
82	}{
83		{desc: "empty-0", size: 0},
84		{
85			desc: "size-3",
86			size: 3,
87			want: [][]byte{
88				nodeHashes[0][0],
89				append(nodeHashes[0][0], nodeHashes[0][1]...),
90				nodeHashes[1][0],
91				nodeHashes[0][2],
92			},
93		},
94	} {
95		t.Run(tc.desc, func(t *testing.T) {
96			r := HashReader{URL: s.URL}
97
98			// Read hashes.
99			for i, want := range tc.want {
100				tile := tlog.TileForIndex(tileHeight, int64(i))
101				got, err := tlog.ReadTileData(tile, r)
102				if err != nil {
103					t.Fatalf("ReadTileData: %v", err)
104				}
105				if !cmp.Equal(got, want) {
106					t.Errorf("tile %+v: got %X, want %X", tile, got, want)
107				}
108			}
109		})
110	}
111}
112
113func TestReadHashesCachedTile(t *testing.T) {
114	ctx := context.Background()
115	s := testServer(ctx, t)
116	defer s.Close()
117
118	wantHash := nodeHashes[0][0]
119	r := HashReader{URL: s.URL}
120
121	// Read hash at index 0 twice, to exercise the caching of tiles.
122	// On the first pass, the read is fresh and readFromURL is called.
123	// On the second pass, the tile is cached, so we skip readFromURL.
124	// We don't explicitly check that readFromURL is only called once,
125	// but we do check ReadHashes returns the correct values.
126	indices := []int64{0, 0}
127	hashes, err := r.ReadHashes(indices)
128	if err != nil {
129		t.Fatalf("ReadHashes: %v", err)
130	}
131
132	got := make([][]byte, 0, len(indices))
133	for _, hash := range hashes {
134		got = append(got, hash[:])
135	}
136
137	if !bytes.Equal(got[0], got[1]) {
138		t.Errorf("expected the same hash: got %X, want %X", got[0], got[1])
139	}
140	if !bytes.Equal(got[0], wantHash) {
141		t.Errorf("wrong ReadHashes result: got %X, want %X", got[0], wantHash)
142	}
143}
144
145func TestParseImageInfosIndex(t *testing.T) {
146	for _, tc := range []struct {
147		desc       string
148		imageInfos string
149		want       map[string]int64
150		wantErr    bool
151	}{
152		{
153			desc:       "size 2",
154			imageInfos: "0\nbuild_fingerprint0\nimage_digest0\n\n1\nbuild_fingerprint1\nimage_digest1\n",
155			wantErr:    false,
156			want: map[string]int64{
157				"build_fingerprint0\nimage_digest0\n": 0,
158				"build_fingerprint1\nimage_digest1\n": 1,
159			},
160		},
161		{
162			desc:       "invalid log entry (no newlines)",
163			imageInfos: "0build_fingerprintimage_digest",
164			wantErr:    true,
165		},
166	} {
167		t.Run(tc.desc, func(t *testing.T) {
168			got, err := parseBinaryInfosIndex(tc.imageInfos, "image_info.txt")
169			if err != nil && !tc.wantErr {
170				t.Fatalf("parseBinaryInfosIndex(%s) received unexpected err %q", tc.imageInfos, err)
171			}
172
173			if err == nil && tc.wantErr {
174				t.Fatalf("parseBinaryInfosIndex(%s) did not return err, expected err", tc.imageInfos)
175			}
176
177			if diff := cmp.Diff(tc.want, got); diff != "" {
178				t.Errorf("parseBinaryInfosIndex returned unexpected diff (-want +got):\n%s", diff)
179			}
180		})
181	}
182}
183
184func TestParsePackageInfosIndex(t *testing.T) {
185	for _, tc := range []struct {
186		desc         string
187		packageInfos string
188		want         map[string]int64
189		wantErr      bool
190	}{
191		{
192			desc:         "size 2",
193			packageInfos: "0\nhash0\nhash_desc0\npackage_name0\npackage_version0\n\n1\nhash1\nhash_desc1\npackage_name1\npackage_version1\n",
194			wantErr:      false,
195			want: map[string]int64{
196				"hash0\nhash_desc0\npackage_name0\npackage_version0\n": 0,
197				"hash1\nhash_desc1\npackage_name1\npackage_version1\n": 1,
198			},
199		},
200		{
201			desc:         "invalid log entry (no newlines)",
202			packageInfos: "0hashhash_descpackage_namepackage_version",
203			wantErr:      true,
204		},
205	} {
206		t.Run(tc.desc, func(t *testing.T) {
207			got, err := parseBinaryInfosIndex(tc.packageInfos, "package_info.txt")
208			if err != nil && !tc.wantErr {
209				t.Fatalf("parseBinaryInfosIndex(%s) received unexpected err %q", tc.packageInfos, err)
210			}
211
212			if err == nil && tc.wantErr {
213				t.Fatalf("parseBinaryInfosIndex(%s) did not return err, expected err", tc.packageInfos)
214			}
215
216			if diff := cmp.Diff(tc.want, got); diff != "" {
217				t.Errorf("parseBinaryInfosIndex returned unexpected diff (-want +got):\n%s", diff)
218			}
219		})
220	}
221}
222