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  }