github.com/quay/claircore@v1.5.28/java/jar/jar_test.go (about) 1 package jar 2 3 import ( 4 "archive/tar" 5 "archive/zip" 6 "bytes" 7 "compress/gzip" 8 "context" 9 "crypto/sha256" 10 "encoding/binary" 11 "encoding/hex" 12 "errors" 13 "io" 14 "io/fs" 15 "net/http" 16 "net/url" 17 "os" 18 "path" 19 "path/filepath" 20 "testing" 21 22 "github.com/google/go-cmp/cmp" 23 "github.com/quay/zlog" 24 25 "github.com/quay/claircore/test" 26 "github.com/quay/claircore/test/integration" 27 ) 28 29 //go:generate go run fetch_testdata.go 30 31 func TestParse(t *testing.T) { 32 t.Parallel() 33 ctx := zlog.Test(context.Background(), t) 34 const url = `https://archive.apache.org/dist/cassandra/4.0.0/apache-cassandra-4.0.0-bin.tar.gz` 35 const sha = `2ff17bda7126c50a2d4b26fe6169807f35d2db9e308dc2851109e1c7438ac2f1` 36 name := fetch(t, url, sha) 37 38 f, err := os.Open(name) 39 if err != nil { 40 t.Fatal(err) 41 } 42 defer f.Close() 43 gz, err := gzip.NewReader(f) 44 if err != nil { 45 t.Fatal(err) 46 } 47 defer gz.Close() 48 tr := tar.NewReader(gz) 49 var h *tar.Header 50 var buf bytes.Buffer 51 for h, err = tr.Next(); err == nil; h, err = tr.Next() { 52 if !ValidExt(h.Name) { 53 continue 54 } 55 t.Log("found jar:", h.Name) 56 t.Run(filepath.Base(h.Name), func(t *testing.T) { 57 ctx := zlog.Test(ctx, t) 58 buf.Reset() 59 buf.Grow(int(h.Size)) 60 n, err := io.Copy(&buf, tr) 61 if err != nil { 62 t.Fatal(err) 63 } 64 t.Logf("read: %d bytes", n) 65 z, err := zip.NewReader(bytes.NewReader(buf.Bytes()), int64(buf.Len())) 66 if err != nil { 67 t.Fatal(err) 68 } 69 ps, err := Parse(ctx, h.Name, z) 70 switch { 71 case errors.Is(err, nil): 72 t.Log(ps) 73 case errors.Is(err, ErrUnidentified): 74 t.Log(err) 75 case filepath.Base(h.Name) == "javax.inject-1.jar" && errors.Is(err, ErrNotAJar): 76 // This is an odd one, it has no metadata. 77 t.Log(err) 78 default: 79 t.Errorf("unexpected: %v", err) 80 } 81 }) 82 } 83 if err != io.EOF { 84 t.Error(err) 85 } 86 } 87 88 func TestWAR(t *testing.T) { 89 t.Parallel() 90 ctx := zlog.Test(context.Background(), t) 91 const url = `https://get.jenkins.io/war/2.311/jenkins.war` 92 const sha = `fe21501800c769279699ecf511fd9b495b1cb3ebd226452e01553ff06820910a` 93 name := fetch(t, url, sha) 94 95 f, err := os.Open(name) 96 if err != nil { 97 t.Fatal(err) 98 } 99 defer f.Close() 100 fi, err := f.Stat() 101 if err != nil { 102 t.Fatal(err) 103 } 104 z, err := zip.NewReader(f, fi.Size()) 105 if err != nil { 106 t.Error(err) 107 } 108 ps, err := Parse(ctx, filepath.Base(name), z) 109 switch { 110 case errors.Is(err, nil): 111 for _, p := range ps { 112 t.Log(p.String()) 113 } 114 case errors.Is(err, ErrUnidentified): 115 t.Error(err) 116 default: 117 t.Errorf("unexpected: %v", err) 118 } 119 } 120 121 func fetch(t testing.TB, u string, ck string) (name string) { 122 t.Helper() 123 uri, err := url.Parse(u) 124 if err != nil { 125 t.Fatal(err) 126 } 127 name = filepath.Join(integration.PackageCacheDir(t), path.Base(uri.Path)) 128 ckb, err := hex.DecodeString(ck) 129 if err != nil { 130 t.Fatal(err) 131 } 132 133 switch _, err := os.Stat(name); { 134 case errors.Is(err, nil): 135 t.Logf("file %q found", name) 136 case errors.Is(err, os.ErrNotExist): 137 t.Logf("file %q missing", name) 138 integration.Skip(t) 139 res, err := http.Get(uri.String()) // Use of http.DefaultClient guarded by integration.Skip call. 140 if err != nil { 141 t.Error(err) 142 break 143 } 144 defer res.Body.Close() 145 if res.StatusCode != http.StatusOK { 146 t.Errorf("unexpected HTTP status: %v", res.Status) 147 break 148 } 149 o, err := os.Create(name) 150 if err != nil { 151 t.Error(err) 152 break 153 } 154 defer o.Close() 155 h := sha256.New() 156 if _, err := io.Copy(o, io.TeeReader(res.Body, h)); err != nil { 157 t.Error(err) 158 } 159 o.Sync() 160 if got, want := h.Sum(nil), ckb; !bytes.Equal(got, want) { 161 t.Errorf("checksum mismatch; got: %x, want: %x", got, want) 162 } 163 default: 164 t.Error(err) 165 } 166 if t.Failed() { 167 if err := os.Remove(name); err != nil { 168 t.Error(err) 169 } 170 t.FailNow() 171 } 172 t.Log("🆗") 173 return name 174 } 175 176 func TestJAR(t *testing.T) { 177 ctx := context.Background() 178 td := os.DirFS("testdata/jar") 179 ls, err := fs.ReadDir(td, ".") 180 if err != nil { 181 t.Fatal(err) 182 } 183 if len(ls) == 0 { 184 t.Skip(`no jars found in "testdata" directory`) 185 } 186 187 var buf bytes.Buffer 188 for _, ent := range ls { 189 if !ent.Type().IsRegular() { 190 continue 191 } 192 n := path.Base(ent.Name()) 193 if ok, _ := filepath.Match(".?ar", path.Ext(n)); !ok { 194 continue 195 } 196 t.Run(n, func(t *testing.T) { 197 ctx := zlog.Test(ctx, t) 198 f, err := td.Open(ent.Name()) 199 if err != nil { 200 t.Error(err) 201 return 202 } 203 defer f.Close() 204 fi, err := ent.Info() 205 if err != nil { 206 t.Error(err) 207 return 208 } 209 sz := fi.Size() 210 buf.Reset() 211 buf.Grow(int(sz)) 212 if _, err := buf.ReadFrom(f); err != nil { 213 t.Error(err) 214 return 215 } 216 217 z, err := zip.NewReader(bytes.NewReader(buf.Bytes()), fi.Size()) 218 if err != nil { 219 t.Fatal(err) 220 return 221 } 222 i, err := Parse(ctx, n, z) 223 if err != nil { 224 t.Error(err) 225 return 226 } 227 for _, i := range i { 228 t.Log(i.String()) 229 } 230 }) 231 } 232 } 233 234 func TestJARBadManifest(t *testing.T) { 235 ctx := context.Background() 236 path := "testdata/malformed-manifests" 237 d := os.DirFS(path) 238 ls, err := fs.ReadDir(d, ".") 239 if err != nil { 240 t.Fatal(err) 241 } 242 if len(ls) == 0 { 243 t.Skip(`no jars found in "testdata" directory`) 244 } 245 246 for _, n := range ls { 247 t.Log(n) 248 t.Run(n.Name(), func(t *testing.T) { 249 ctx := zlog.Test(ctx, t) 250 f, err := os.Open(filepath.Join(path, n.Name())) 251 if err != nil { 252 t.Fatal(err) 253 } 254 defer f.Close() 255 i := &Info{} 256 err = i.parseManifest(ctx, f) 257 if err != nil && !errors.Is(err, errInsaneManifest) { 258 t.Fatal(err) 259 } 260 }) 261 } 262 } 263 264 // TestMalformed creates malformed zips, then makes sure the package handles 265 // them gracefully. 266 func TestMalformed(t *testing.T) { 267 t.Parallel() 268 ctx := zlog.Test(context.Background(), t) 269 270 t.Run("BadOffset", func(t *testing.T) { 271 const ( 272 jarName = `malformed_zip.jar` 273 manifest = `testdata/malformed_zip.MF` 274 ) 275 fn := test.GenerateFixture(t, jarName, test.Modtime(t, "jar_test.go"), func(t testing.TB, f *os.File) { 276 // Create the jar-like. 277 w := zip.NewWriter(f) 278 if _, err := w.Create(`META-INF/`); err != nil { 279 t.Fatal(err) 280 } 281 fw, err := w.Create(`META-INF/MANIFEST.MF`) 282 if err != nil { 283 t.Fatal(err) 284 } 285 mf, err := os.ReadFile(manifest) 286 if err != nil { 287 t.Fatal(err) 288 } 289 if _, err := io.Copy(fw, bytes.NewReader(mf)); err != nil { 290 t.Fatal(err) 291 } 292 if err := w.Close(); err != nil { 293 t.Fatal(err) 294 } 295 296 // Then, corrupt it. 297 // Seek to the central directory footer: 298 pos, err := f.Seek(-0x16+0x10 /* sizeof(footer) + offset(dir_offset)*/, io.SeekEnd) 299 if err != nil { 300 t.Fatal(err) 301 } 302 b := make([]byte, 4) 303 if _, err := io.ReadFull(f, b); err != nil { 304 t.Fatal(err) 305 } 306 // Offset everything so the reader slowly descends into madness. 307 b[0] -= 7 308 if _, err := f.WriteAt(b, pos); err != nil { 309 t.Fatal(err) 310 } 311 312 if err := f.Sync(); err != nil { 313 t.Error(err) 314 } 315 }) 316 317 f, err := os.Open(fn) 318 if err != nil { 319 t.Fatal(err) 320 } 321 defer f.Close() 322 fi, err := f.Stat() 323 if err != nil { 324 t.Fatal(err) 325 } 326 z, err := zip.NewReader(f, fi.Size()) 327 if err != nil { 328 t.Fatal(err) 329 } 330 infos, err := Parse(ctx, jarName, z) 331 t.Logf("returned error: %v", err) 332 switch { 333 case errors.Is(err, ErrNotAJar): 334 default: 335 t.Fail() 336 } 337 if len(infos) != 0 { 338 t.Errorf("returned infos: %#v", infos) 339 } 340 }) 341 342 t.Run("Cursed", func(t *testing.T) { 343 // Why is the footer corrupted like that? 344 // No idea, we just found a jar in the wild that looked like this. 345 fn := test.GenerateFixture(t, `plantar.jar`, test.Modtime(t, "jar_test.go"), func(t testing.TB, f *os.File) { 346 const comment = "\x00" 347 // Create the jar-like. 348 w := zip.NewWriter(f) 349 fw, err := w.Create(`META-INF/MANIFEST.MF`) 350 if err != nil { 351 t.Fatal(err) 352 } 353 mf, err := os.Open("testdata/manifest/HdrHistogram-2.1.9.jar") 354 if err != nil { 355 t.Fatal(err) 356 } 357 defer mf.Close() 358 if _, err := io.Copy(fw, mf); err != nil { 359 t.Fatal(err) 360 } 361 w.SetComment(comment) 362 if err := w.Close(); err != nil { 363 t.Fatal(err) 364 } 365 f.Write([]byte{0x00}) // Bonus! 366 367 // Then, corrupt it. 368 fi, err := f.Stat() 369 if err != nil { 370 t.Fatal(err) 371 } 372 373 ft := make([]byte, 0x16+int64(len(comment))+1) 374 ftOff := fi.Size() - int64(len(ft)) 375 ptrOff := ftOff + 16 376 szOff := ftOff + 12 377 378 info := func() { 379 if _, err := f.ReadAt(ft, ftOff); err != nil { 380 t.Error(err) 381 } 382 t.Logf("footer:\n%s", hex.Dump(ft)) 383 b := make([]byte, 4) 384 if _, err := f.ReadAt(b, ptrOff); err != nil { 385 t.Fatal(err) 386 } 387 ptr := binary.LittleEndian.Uint32(b) 388 t.Logf("Central Directory pointer: 0x%08x", ptr) 389 b = b[:2] 390 if _, err := f.ReadAt(b, szOff); err != nil { 391 t.Fatal(err) 392 } 393 sz := binary.LittleEndian.Uint16(b) 394 t.Logf("Central Directory size: %d", sz) 395 } 396 397 info() 398 if _, err := f.WriteAt([]byte{0xef, 0xbe, 0xad, 0xde}, ptrOff); err != nil { 399 t.Error(err) 400 } 401 if _, err := f.WriteAt([]byte{0x20, 0x00}, szOff); err != nil { 402 t.Error(err) 403 } 404 info() 405 406 if err := f.Sync(); err != nil { 407 t.Error(err) 408 } 409 }) 410 411 f, err := os.Open(fn) 412 if err != nil { 413 t.Fatal(err) 414 } 415 defer f.Close() 416 fi, err := f.Stat() 417 if err != nil { 418 t.Fatal(err) 419 } 420 _, err = zip.NewReader(f, fi.Size()) 421 t.Logf("returned error: %v", err) 422 switch { 423 case errors.Is(err, io.EOF): // <= go1.20 424 case errors.Is(err, zip.ErrFormat): 425 default: 426 t.Fail() 427 } 428 }) 429 430 } 431 432 func TestManifestSectionReader(t *testing.T) { 433 var ms []string 434 d := os.DirFS("testdata") 435 for _, p := range []string{"manifest", "manifestSection"} { 436 ents, err := fs.ReadDir(d, p) 437 if err != nil { 438 t.Error(err) 439 return 440 } 441 for _, e := range ents { 442 if filepath.Ext(e.Name()) == ".want" { 443 continue 444 } 445 ms = append(ms, filepath.Join("testdata", p, e.Name())) 446 } 447 } 448 449 for _, n := range ms { 450 n := n 451 t.Run(filepath.Base(n), func(t *testing.T) { 452 wantF, err := os.Open(n + ".want") 453 if err != nil { 454 t.Error(err) 455 } 456 var want bytes.Buffer 457 _, err = want.ReadFrom(wantF) 458 wantF.Close() 459 if err != nil { 460 t.Error(err) 461 } 462 inF, err := os.Open(n) 463 if err != nil { 464 t.Error(err) 465 } 466 defer inF.Close() 467 var out bytes.Buffer 468 if _, err := io.Copy(&out, newMainSectionReader(inF)); err != nil { 469 t.Error(err) 470 } 471 // Can't use iotest.TestReader because we disallow tiny reads. 472 if got, want := out.String(), want.String(); !cmp.Equal(got, want) { 473 t.Error(cmp.Diff(got, want)) 474 } 475 }) 476 } 477 }