m/p/fat32: add fat32 package

The fat32 package is a write-only implementation of the FAT32
filesystem. It works quite unlike a normal file system by first
determining the entire disk layout and then sequentially writing
out everything. This allows it to have a fully streaming output without
needing to seek at all.
Because all IO is sequential the implementation is extremely fast and
can potentially even leverage things like the copy_file_range syscall.
This means however that all files and readers need to be prepared ahead
of time, it is not possible to make decisions during the writing
process.
It is also possible to generate "right-sized" filesystems by not
specifying an explicit block count. In that case the resulting image
will contain exactly as many clusters as needed.

Change-Id: I49bf2ce09b26a7d628a39a0dd0745bca61c1c4da
Reviewed-on: https://review.monogon.dev/c/monogon/+/841
Tested-by: Jenkins CI
Reviewed-by: Sergiusz Bazanski <serge@monogon.tech>
diff --git a/metropolis/pkg/fat32/structs_test.go b/metropolis/pkg/fat32/structs_test.go
new file mode 100644
index 0000000..77a7df0
--- /dev/null
+++ b/metropolis/pkg/fat32/structs_test.go
@@ -0,0 +1,27 @@
+package fat32
+
+import (
+	"encoding/binary"
+	"reflect"
+	"testing"
+)
+
+func TestStructureSizes(t *testing.T) {
+	cases := []struct {
+		StructInstance interface{}
+		ExpectedSize   int
+	}{
+		{bootSector{}, 512},
+		{fsinfo{}, 512},
+		{dirEntry{}, 32},
+		{lfnEntry{}, 32},
+	}
+	for _, c := range cases {
+		t.Run(reflect.TypeOf(c.StructInstance).String(), func(t *testing.T) {
+			actualSize := binary.Size(c.StructInstance)
+			if actualSize != c.ExpectedSize {
+				t.Errorf("Expected %d bytes, got %d", c.ExpectedSize, actualSize)
+			}
+		})
+	}
+}