blob: d3d4513ba25d58b29acd55a6093534f53222d686 [file] [edit]
// Copyright 2026 The Wuffs Authors.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
//
// SPDX-License-Identifier: Apache-2.0 OR MIT
package suitar
import (
"archive/tar"
"bytes"
"errors"
"fmt"
"hash/crc32"
"io"
"os"
"reflect"
"testing"
"time"
)
type crcWriter uint32
func (c *crcWriter) Write(b []byte) (int, error) {
state := uint32(*c)
state = crc32.Update(state, crc32.IEEETable, b)
*c = crcWriter(state)
return len(b), nil
}
func testWriter(tt *testing.T, sparse bool) {
f, err := os.Open("../../test/data/archive.tar")
if err != nil {
tt.Fatalf("os.Open: %v", err)
}
defer f.Close()
// Convert from t (tar, using the standard library) to s (suitar, using
// this package).
buf := bytes.Buffer{}
sWriter := NewWriter(&buf)
for tReader := tar.NewReader(f); ; {
tHeader, err := tReader.Next()
if err == io.EOF {
break
} else if err != nil {
tt.Fatalf("Next: %v", err)
}
sHeader := &Header{
Typeflag: tHeader.Typeflag,
Name: tHeader.Name,
Size: tHeader.Size,
Mode: tHeader.Mode,
ModTime: tHeader.ModTime,
}
if sparse && (sHeader.Typeflag == TypeReg) {
sHeader.Typeflag = TypeGNUSparse
}
if err := sWriter.WriteHeader(sHeader); err != nil {
tt.Fatalf("WriteHeader: %v", err)
}
dstWriter := (io.Writer)(sWriter)
if sparse {
dstWriter = io.Discard
}
if _, err := io.Copy(dstWriter, tReader); err != nil {
tt.Fatalf("io.Copy: %v", err)
}
}
if err := sWriter.Close(); err != nil {
tt.Fatalf("Close: %v", err)
}
got := buf.Bytes()
wantFilename := "../../test/data/archive"
if sparse {
wantFilename += ".sparse.suitar"
} else {
wantFilename += ".dense.suitar"
}
want, err := os.ReadFile(wantFilename)
if err != nil {
tt.Fatalf("os.ReadFile: %v", err)
}
if !bytes.Equal(got, want) {
tt.Fatalf("did not recreate golden test file")
}
}
func testReader(tt *testing.T, sparse bool, ignore bool) {
filename, wantTypeflag := "../../test/data/archive.dense.suitar", " T:'0'"
if sparse {
filename, wantTypeflag = "../../test/data/archive.sparse.suitar", " T:'S'"
}
wantChecksums := []string(nil)
if ignore {
wantChecksums = []string{
"C:0x00000000",
"C:0x00000000",
"C:0x00000000",
"C:0x00000000",
"C:0x00000000",
"C:0x00000000",
"C:0x00000000",
"C:0x00000000",
}
} else if sparse {
wantChecksums = []string{
"C:0x00000000",
"C:0xB69F8E37",
"C:0x73FF3CAE",
"C:0xD71F022F",
"C:0xC446EAB8",
"C:0x5F228EB9",
"C:0x7BA7D011",
"C:0x48792A7F",
}
} else {
wantChecksums = []string{
"C:0x00000000",
"C:0xFEDD8F35",
"C:0x87EE5E05",
"C:0x703E9270",
"C:0xC37CB538",
"C:0x2B0B23B0",
"C:0xABE507EF",
"C:0x67FABE9C",
}
}
want := "" +
wantChecksums[0] + wantTypeflag + " S:0x0000 M:644 MT:0x5E3A5C50 N:artificial/0.bytes\n" +
wantChecksums[1] + wantTypeflag + " S:0x0355 M:644 MT:0x5F33F6E6 N:github-tags.json\n" +
wantChecksums[2] + wantTypeflag + " S:0x02B5 M:755 MT:0x608F960B N:hello.sh\n" +
wantChecksums[3] + wantTypeflag + " S:0x0068 M:644 MT:0x608F954D N:non-ascii/αβ.txt\n" +
wantChecksums[4] + wantTypeflag + " S:0x0097 M:644 MT:0x608F96C7 N:non-ascii/😻.txt\n" +
wantChecksums[5] + wantTypeflag + " S:0x00D0 M:644 MT:0x5E3A5C50 N:pjw-thumbnail.png\n" +
wantChecksums[6] + wantTypeflag + " S:0x03AE M:644 MT:0x5E3A5C50 N:romeo.txt\n" +
wantChecksums[7] + wantTypeflag + " S:0x022E M:644 MT:0x5E3A5C50 N:romeo.txt.gz\n" +
""
f, err := os.Open(filename)
if err != nil {
tt.Fatalf("os.Open: %v", err)
}
defer f.Close()
buf := bytes.Buffer{}
for r := NewReader(f); ; {
h, err := r.Next()
if err == io.EOF {
break
} else if err != nil {
tt.Fatalf("Next: %v", err)
}
checksum := crcWriter(0)
if !ignore {
if _, err := io.Copy(&checksum, r); err != nil {
tt.Fatalf("io.Copy: %v", err)
}
}
fmt.Fprintf(&buf, "C:0x%08X T:'%c' S:0x%04X M:%3o MT:0x%08X N:%s\n",
checksum, h.Typeflag, h.Size, h.Mode, h.ModTime.Unix(), h.Name)
}
if got := buf.String(); got != want {
tt.Fatalf("\ngot:\n%s\nwant:\n%s", got, want)
}
}
func TestWriterDense(tt *testing.T) { testWriter(tt, false) }
func TestWriterSparse(tt *testing.T) { testWriter(tt, true) }
func TestReaderDenseCheck(tt *testing.T) { testReader(tt, false, false) }
func TestReaderDenseIgnore(tt *testing.T) { testReader(tt, false, true) }
func TestReaderSparseCheck(tt *testing.T) { testReader(tt, true, false) }
func TestReaderSparseIgnore(tt *testing.T) { testReader(tt, true, true) }
// cappedWriter returns an error once the total number of bytes written would
// exceed limit, so that a buggy Writer (which would otherwise loop forever)
// instead fails the surrounding test.
type cappedWriter struct {
buf bytes.Buffer
limit int
}
func (w *cappedWriter) Write(b []byte) (int, error) {
if w.buf.Len()+len(b) > w.limit {
return 0, errCappedWriter
}
return w.buf.Write(b)
}
var errCappedWriter = errors.New("suitar_test: capped writer")
// writeOneFileChunked returns a SUITAR archive holding a single file whose
// contents are size bytes long, written in chunks of chunk bytes (or all at
// once if chunk is non-positive).
func writeOneFileChunked(tt *testing.T, size int, chunk int) []byte {
tt.Helper()
cw := &cappedWriter{limit: 0x10000}
w := NewWriter(cw)
err := w.WriteHeader(&Header{
Typeflag: TypeReg,
Name: "file.bin",
Size: int64(size),
Mode: Mode644,
ModTime: time.Unix(12345678, 0),
})
if err != nil {
tt.Fatalf("size=%d chunk=%d: WriteHeader: %v", size, chunk, err)
}
contents := make([]byte, size)
for i := range contents {
contents[i] = byte(i)
}
if chunk <= 0 {
if _, err := w.Write(contents); err != nil {
tt.Fatalf("size=%d chunk=%d: Write: %v", size, chunk, err)
}
} else {
for i := 0; i < size; i += chunk {
j := min(i+chunk, size)
if _, err := w.Write(contents[i:j]); err != nil {
tt.Fatalf("size=%d chunk=%d: Write[%d:%d]: %v", size, chunk, i, j, err)
}
}
}
if err := w.Close(); err != nil {
tt.Fatalf("size=%d chunk=%d: Close: %v", size, chunk, err)
}
return cw.buf.Bytes()
}
// TestWriterChunked checks that how the file contents are split up over Write
// calls doesn't matter, as long as the total is Header.Size bytes long.
func TestWriterChunked(tt *testing.T) {
for _, size := range []int{0, 1, 300, 511, 512, 513, 700, 1000, 1024, 5000} {
want := writeOneFileChunked(tt, size, 0)
for _, chunk := range []int{1, 100, 300, 511, 512, 700, 1024} {
if got := writeOneFileChunked(tt, size, chunk); !bytes.Equal(got, want) {
tt.Fatalf("size=%d chunk=%d: chunk size changed the archive bytes", size, chunk)
}
}
r := NewReader(bytes.NewReader(want))
h, err := r.Next()
if err != nil {
tt.Fatalf("size=%d: Next: %v", size, err)
} else if (h.Typeflag != TypeReg) || (h.Size != int64(size)) {
tt.Fatalf("size=%d: got T:'%c' S:%d", size, h.Typeflag, h.Size)
}
got, err := io.ReadAll(r)
if err != nil {
tt.Fatalf("size=%d: ReadAll: %v", size, err)
} else if len(got) != size {
tt.Fatalf("size=%d: read back %d bytes", size, len(got))
} else {
for i, c := range got {
if c != byte(i) {
tt.Fatalf("size=%d: contents differ at %d", size, i)
break
}
}
}
if _, err := r.Next(); err != io.EOF {
tt.Fatalf("size=%d: Next after the last entry: %v", size, err)
}
}
}
// TestWriteNonRegular checks that entries (including TypeDir entries which
// have no contents and TypeGNUSparse entries which have all-NUL contents) can
// be written and read back.
func TestWriteNonRegular(tt *testing.T) {
headers := []Header{{
Typeflag: TypeDir,
Name: "a/b",
Mode: Mode755,
ModTime: time.Unix(12345601, 0),
}, {
Typeflag: TypeReg,
Name: "a/b/c.txt",
Size: 5,
Mode: Mode644,
ModTime: time.Unix(12345602, 0),
}, {
Typeflag: TypeGNUSparse,
Name: "sparse0.bin",
Size: 3,
Mode: Mode644,
ModTime: time.Unix(123000, 0),
}, {
Typeflag: TypeGNUSparse,
Name: "sparse1.bin",
Size: 3,
Mode: Mode644,
ModTime: time.Unix(123001, 0),
}, {
Typeflag: TypeGNUSparse,
Name: "sparse3.bin",
Size: 3,
Mode: Mode644,
ModTime: time.Unix(123002, 0),
}}
contents := map[string]string{
"a/b/c.txt": "hello",
"sparse0.bin": "\x00\x00\x00",
"sparse1.bin": "\x00\x00\x00",
"sparse3.bin": "\x00\x00\x00",
}
buf := bytes.Buffer{}
w := NewWriter(&buf)
for _, h := range headers {
if err := w.WriteHeader(&h); err != nil {
tt.Fatalf("WriteHeader(%q): %v", h.Name, err)
}
content, ok := contents[h.Name]
if !ok {
continue
} else if h.Name == "sparse0.bin" {
// It's OK, for TypeGNUSparse, to Write no bytes.
continue
} else if h.Name == "sparse1.bin" {
// It's OK, for TypeGNUSparse, to Write some but not all of the
// Size bytes, provided that what you're writing are NUL bytes.
content = content[:1]
} else if h.Name == "sparse3.bin" {
// No-op, going on to Write all 3 explicit NUL bytes.
}
if _, err := w.Write([]byte(content)); err != nil {
tt.Fatalf("Write(%q): %v", h.Name, err)
}
}
if err := w.Close(); err != nil {
tt.Fatalf("Close: %v", err)
}
r := NewReader(&buf)
for _, hWant := range headers {
hGot, err := r.Next()
if err != nil {
tt.Fatalf("Next: %v", err)
} else if !reflect.DeepEqual(hGot, hWant) {
tt.Fatalf("got vs want\n%#v\n%#v", hGot, hWant)
}
if readAll, err := io.ReadAll(r); err != nil {
tt.Fatalf("ReadAll: %v", err)
} else if rGot, rWant := string(readAll), contents[hGot.Name]; rGot != rWant {
tt.Fatalf("N:%s: got %q, want %q", hGot.Name, rGot, rWant)
}
}
if _, err := r.Next(); err != io.EOF {
tt.Fatalf("Next after the last entry: %v", err)
}
}
func TestWriteANonNUL(tt *testing.T) {
buf := bytes.Buffer{}
w := NewWriter(&buf)
if err := w.WriteHeader(&Header{
Typeflag: TypeGNUSparse,
Name: "example.dat",
Size: 5,
Mode: Mode644,
ModTime: time.Unix(0, 0),
}); err != nil {
tt.Fatalf("WriteHeader: %v", err)
}
if _, err := w.Write([]byte("Lorem")); err != errWriteANonNul {
tt.Fatalf("Write: got %v, want %v", err, errWriteANonNul)
}
}
func testWriteTooMuch(tt *testing.T, typeflag byte) {
for i := 8; i <= 12; i++ {
buf := bytes.Buffer{}
w := NewWriter(&buf)
if err := w.WriteHeader(&Header{
Typeflag: typeflag,
Name: "example.dat",
Size: 10,
Mode: Mode644,
ModTime: time.Unix(0, 0),
}); err != nil {
tt.Fatalf("WriteHeader: %v", err)
}
errWant := error(nil)
if i > 10 {
errWant = errHeaderSize
}
if _, errGot := w.Write(make([]byte, i)); errGot != errWant {
tt.Fatalf("i=%d: Write: got %v, want %v", i, errGot, errWant)
}
}
}
func TestWriteTooMuchRegular(tt *testing.T) { testWriteTooMuch(tt, TypeReg) }
func TestWriteTooMuchSparse(tt *testing.T) { testWriteTooMuch(tt, TypeGNUSparse) }