203 lines
7.2 KiB
Go
203 lines
7.2 KiB
Go
package aseprite
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/zlib"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestReaderBoundsAndText(t *testing.T) {
|
|
r := NewReader([]byte{0xff, 0xfe, 0xfd, 0xfc, 0xfb, 2, 0, 'o', 'k'})
|
|
if r.U8() != 255 || r.I16() != -514 || r.I16() != -1028 || r.Text() != "ok" || r.Offset() != 9 || r.Err() != nil {
|
|
t.Fatalf("unexpected decoded fields: offset=%d error=%v", r.Offset(), r.Err())
|
|
}
|
|
if r.U32() != 0 || !errors.Is(r.Err(), io.ErrUnexpectedEOF) || r.Offset() != 9 {
|
|
t.Fatalf("short read: offset=%d error=%v", r.Offset(), r.Err())
|
|
}
|
|
if r.Take(0) != nil || !errors.Is(r.Err(), io.ErrUnexpectedEOF) {
|
|
t.Fatal("read error must remain sticky")
|
|
}
|
|
for _, tc := range []struct {
|
|
name string
|
|
data []byte
|
|
want string
|
|
}{
|
|
{"bad UTF-8", []byte{1, 0, 0xff}, "invalid UTF-8"},
|
|
{"truncated text", []byte{2, 0, 'x'}, "unexpected EOF"},
|
|
{"truncated length", []byte{1}, "unexpected EOF"},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
r := NewReader(tc.data)
|
|
_ = r.Text()
|
|
if r.Err() == nil || r.Err().Error() != tc.want {
|
|
t.Fatalf("error = %v, want %q", r.Err(), tc.want)
|
|
}
|
|
})
|
|
}
|
|
r = NewReader([]byte{1})
|
|
if r.Take(-1) != nil || !errors.Is(r.Err(), io.ErrUnexpectedEOF) || r.Offset() != 0 {
|
|
t.Fatal("negative read was not rejected")
|
|
}
|
|
}
|
|
|
|
func testFrame(duration, oldCount, newCount int, chunks ...[]byte) []byte {
|
|
frame := make([]byte, 16)
|
|
binary.LittleEndian.PutUint16(frame[4:], 0xf1fa)
|
|
binary.LittleEndian.PutUint16(frame[6:], uint16(oldCount))
|
|
binary.LittleEndian.PutUint16(frame[8:], uint16(duration))
|
|
binary.LittleEndian.PutUint32(frame[12:], uint32(newCount))
|
|
for _, chunk := range chunks {
|
|
frame = append(frame, chunk...)
|
|
}
|
|
binary.LittleEndian.PutUint32(frame, uint32(len(frame)))
|
|
return frame
|
|
}
|
|
|
|
func testChunk(kind uint16, payload ...byte) []byte {
|
|
b := make([]byte, 6, 6+len(payload))
|
|
binary.LittleEndian.PutUint32(b, uint32(6+len(payload)))
|
|
binary.LittleEndian.PutUint16(b[4:], kind)
|
|
return append(b, payload...)
|
|
}
|
|
|
|
func testFile(frames ...[]byte) []byte {
|
|
b := make([]byte, 128)
|
|
for _, frame := range frames {
|
|
b = append(b, frame...)
|
|
}
|
|
return b
|
|
}
|
|
|
|
func TestReadFramesOrderAndDurations(t *testing.T) {
|
|
// The nonzero new count overrides oldCount. The final frame falls back to
|
|
// the default 100 ms when both duration and header speed are zero.
|
|
data := testFile(testFrame(42, 1, 2, testChunk(0x2004, 1), testChunk(0x2005, 2, 3)), testFrame(0, 0, 0))
|
|
var calls []string
|
|
err := ReadFrames(data, 2, 0, func(d int) { calls = append(calls, fmt.Sprintf("frame:%d", d)) },
|
|
func(frame int, kind uint16, b []byte) error {
|
|
calls = append(calls, fmt.Sprintf("chunk:%d:%x:%x", frame, kind, b))
|
|
return nil
|
|
})
|
|
if err != nil || strings.Join(calls, ",") != "frame:42,chunk:0:2004:01,chunk:0:2005:0203,frame:100" {
|
|
t.Fatalf("calls=%v error=%v", calls, err)
|
|
}
|
|
var duration int
|
|
if err := ReadFrames(testFile(testFrame(0, 0, 0)), 1, 77, func(d int) { duration = d }, func(int, uint16, []byte) error { return nil }); err != nil || duration != 77 {
|
|
t.Fatalf("header speed fallback=%d error=%v", duration, err)
|
|
}
|
|
}
|
|
|
|
func TestReadFramesErrors(t *testing.T) {
|
|
invalidMagic := testFrame(1, 0, 0)
|
|
invalidMagic[4] = 0
|
|
invalidSize := testFrame(1, 0, 0)
|
|
binary.LittleEndian.PutUint32(invalidSize, 15)
|
|
invalidCount := testFrame(1, 1, 0)
|
|
invalidChunk := testChunk(0x2004)
|
|
binary.LittleEndian.PutUint32(invalidChunk, 5)
|
|
trailingFrame := testFrame(1, 0, 0, []byte{1})
|
|
tests := []struct {
|
|
name, want string
|
|
data []byte
|
|
}{
|
|
{"short frame header", "unexpected EOF", make([]byte, 128)},
|
|
{"bad frame magic", "invalid frame 0", testFile(invalidMagic)},
|
|
{"bad frame length", "invalid frame 0", testFile(invalidSize)},
|
|
{"bad chunk count", "invalid chunk count", testFile(invalidCount)},
|
|
{"bad chunk length", "invalid chunk size at frame 0", testFile(testFrame(1, 1, 0, invalidChunk))},
|
|
{"trailing frame", "trailing frame bytes", testFile(trailingFrame)},
|
|
{"trailing file", "trailing file bytes", append(testFile(testFrame(1, 0, 0)), 0)},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
err := ReadFrames(tc.data, 1, 0, func(int) {}, func(int, uint16, []byte) error { return nil })
|
|
if err == nil || err.Error() != tc.want {
|
|
t.Fatalf("error=%v want=%q", err, tc.want)
|
|
}
|
|
})
|
|
}
|
|
marker := errors.New("chunk failed")
|
|
err := ReadFrames(testFile(testFrame(1, 1, 0, testChunk(0x2004))), 1, 0, func(int) {}, func(int, uint16, []byte) error { return marker })
|
|
if !errors.Is(err, marker) || err.Error() != "frame 0 chunk 0x2004: chunk failed" {
|
|
t.Fatalf("chunk context lost: %v", err)
|
|
}
|
|
// Count is rejected before visiting chunks even when enough frame bytes exist.
|
|
frame := testFrame(1, 0, 65537, make([]byte, 6*65537))
|
|
err = ReadFrames(testFile(frame), 1, 0, func(int) { t.Fatal("frame visited") }, func(int, uint16, []byte) error { t.Fatal("chunk visited"); return nil })
|
|
if err == nil || err.Error() != "invalid chunk count" {
|
|
t.Fatalf("chunk limit: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestInflateExactAndDeflateRows(t *testing.T) {
|
|
var encoded bytes.Buffer
|
|
rows := [][]byte{[]byte("abc"), []byte("def")}
|
|
if err := DeflateRows(&encoded, len(rows), func(y int) ([]byte, error) { return rows[y], nil }); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Compare against the old direct zlib write sequence, not against our reader.
|
|
var reference bytes.Buffer
|
|
z := zlib.NewWriter(&reference)
|
|
for _, row := range rows {
|
|
if _, err := z.Write(row); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if err := z.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !bytes.Equal(encoded.Bytes(), reference.Bytes()) {
|
|
t.Fatal("compressed stream differs from direct zlib writes")
|
|
}
|
|
if got, err := InflateExact(encoded.Bytes(), 6); err != nil || string(got) != "abcdef" {
|
|
t.Fatalf("inflate=%q error=%v", got, err)
|
|
}
|
|
for _, tc := range []struct {
|
|
name string
|
|
raw []byte
|
|
size int64
|
|
}{
|
|
{"short decoded", encoded.Bytes(), 7},
|
|
{"excess decoded", encoded.Bytes(), 5},
|
|
{"trailing bytes", append(bytes.Clone(encoded.Bytes()), 0), 6},
|
|
{"truncated stream", encoded.Bytes()[:encoded.Len()-1], 6},
|
|
{"invalid stream", []byte("invalid"), 6},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if _, err := InflateExact(tc.raw, tc.size); err == nil {
|
|
t.Fatal("accepted invalid compressed data")
|
|
}
|
|
})
|
|
}
|
|
marker := errors.New("row failed")
|
|
if err := DeflateRows(io.Discard, 2, func(int) ([]byte, error) { return nil, marker }); !errors.Is(err, marker) {
|
|
t.Fatalf("row error lost: %v", err)
|
|
}
|
|
limit := errors.New("limit")
|
|
out := NewOutput(0, limit)
|
|
if err := DeflateRows(out, 1, func(int) ([]byte, error) { return []byte{1}, nil }); !errors.Is(err, limit) || !errors.Is(out.Err(), limit) {
|
|
t.Fatalf("compressed write limit lost: %v / %v", err, out.Err())
|
|
}
|
|
}
|
|
|
|
func TestOutputChunkFramingAndLimit(t *testing.T) {
|
|
limit := errors.New("limit")
|
|
w := NewOutput(20, limit)
|
|
w.Chunk(0x2004, func() { w.U8(7); w.Text("ok") })
|
|
if w.Err() != nil || !bytes.Equal(w.Bytes(), []byte{11, 0, 0, 0, 4, 0x20, 7, 2, 0, 'o', 'k'}) {
|
|
t.Fatalf("chunk=%x error=%v", w.Bytes(), w.Err())
|
|
}
|
|
before := bytes.Clone(w.Bytes())
|
|
w.Zeros(10)
|
|
w.U8(1)
|
|
w.PatchSize(0)
|
|
if !errors.Is(w.Err(), limit) || !bytes.Equal(before, w.Bytes()) {
|
|
t.Fatalf("limit must be sticky and leave bytes intact: %x / %v", w.Bytes(), w.Err())
|
|
}
|
|
}
|