aboutsummaryrefslogtreecommitdiff
path: root/hotline/flattened_file_object_fuzz_test.go
blob: 574fed3793cab3ce7b831ed44b7f67fd6605bf96 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
package hotline

import (
	"bytes"
	"io"
	"testing"

	"github.com/stretchr/testify/require"
)

// sampleFFOBytes encodes a small flattened file object (FILP header, INFO fork header,
// information fork, DATA fork header) — the layout that ReadFrom parses off the wire.
func sampleFFOBytes(t interface{ Fatal(...any) }) []byte {
	ffo := flattenedFileObject{
		FlatFileHeader: FlatFileHeader{
			Format:    [4]byte{'F', 'I', 'L', 'P'},
			Version:   [2]byte{0, 1},
			ForkCount: [2]byte{0, 2},
		},
		FlatFileInformationFork: NewFlatFileInformationFork("testfile.txt", [8]byte{}, "TEXT", "TTXT"),
		FlatFileDataForkHeader: FlatFileForkHeader{
			ForkType: [4]byte{'D', 'A', 'T', 'A'},
			DataSize: [4]byte{0, 0, 0, 5},
		},
	}
	b, err := io.ReadAll(&ffo)
	if err != nil {
		t.Fatal(err)
	}
	return b
}

// FuzzFlatFileInformationForkUnmarshal feeds raw untrusted bytes to the information fork
// decoder, which parses the metadata section of client file uploads.
func FuzzFlatFileInformationForkUnmarshal(f *testing.F) {
	fork := NewFlatFileInformationFork("testfile.txt", [8]byte{}, "TEXT", "TTXT")
	forkBytes, err := io.ReadAll(&fork)
	if err != nil {
		f.Fatal(err)
	}
	f.Add(forkBytes)
	f.Add(forkBytes[:flatFileInfoForkMinLen]) // fixed-size section only
	f.Add([]byte{})                           // empty

	f.Fuzz(func(t *testing.T, data []byte) {
		var ffif FlatFileInformationFork
		if err := ffif.UnmarshalBinary(data); err != nil {
			return
		}
		// A successfully decoded fork must re-encode without panicking.
		_, err := io.ReadAll(&ffif)
		require.NoError(t, err)
	})
}

// FuzzFlattenedFileObjectReadFrom feeds raw untrusted bytes to the flattened file object
// parser — the entry point for decoding client file uploads on the transfer port.
func FuzzFlattenedFileObjectReadFrom(f *testing.F) {
	sample := sampleFFOBytes(f)
	f.Add(sample)
	f.Add(sample[:24]) // FILP header only
	// Information fork header declaring an absurd size (previously triggered an unbounded allocation).
	huge := bytes.Clone(sample)
	copy(huge[36:40], []byte{0xff, 0xff, 0xff, 0xff})
	f.Add(huge)

	f.Fuzz(func(t *testing.T, data []byte) {
		var ffo flattenedFileObject
		_, _ = ffo.ReadFrom(bytes.NewReader(data)) // must not panic or over-allocate
	})
}

// TestFlattenedFileObject_ReadFrom_RoundTrip pins that ReadFrom can parse what Read encodes.
func TestFlattenedFileObject_ReadFrom_RoundTrip(t *testing.T) {
	sample := sampleFFOBytes(t)

	var ffo flattenedFileObject
	_, err := ffo.ReadFrom(bytes.NewReader(sample))
	require.NoError(t, err)

	require.Equal(t, [4]byte{'F', 'I', 'L', 'P'}, ffo.FlatFileHeader.Format)
	require.Equal(t, []byte("testfile.txt"), ffo.FlatFileInformationFork.Name)
	require.Equal(t, ForkType{'D', 'A', 'T', 'A'}, ffo.FlatFileDataForkHeader.ForkType)
	require.Equal(t, int64(5), ffo.dataSize())
}

// TestFlattenedFileObject_ReadFrom_RejectsOversizedInfoFork pins the untrusted-size guard.
func TestFlattenedFileObject_ReadFrom_RejectsOversizedInfoFork(t *testing.T) {
	sample := sampleFFOBytes(t)
	copy(sample[36:40], []byte{0xff, 0xff, 0xff, 0xff})

	var ffo flattenedFileObject
	_, err := ffo.ReadFrom(bytes.NewReader(sample))
	require.ErrorContains(t, err, "exceeds maximum")
}