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