Add EROFS library

This adds a library to write EROFS filesystems. It supports most of the non-deprecated features the
filesystem supports other than extended inodes (which have no benefits for most use cases where EROFS would be
appropriate). EROFS's variable-length extent compression is partially implemented but it requires an LZ4
compressor with support for fixed-size output which Go's https://github.com/pierrec/lz4 doesn't have. This means
that VLE compression is currently not wired up.

This will be used later as a replacement for our current initramfs-based root filesystem.

Test Plan: Has both integration and some unit tests. Confirmed working for our whole rootfs.

X-Origin-Diff: phab/D692
GitOrigin-RevId: 8c52b45ea05c617c80047e99c04c2b63e1b60c7c
diff --git a/metropolis/pkg/erofs/uncompressed_inode_writer.go b/metropolis/pkg/erofs/uncompressed_inode_writer.go
new file mode 100644
index 0000000..df89fec
--- /dev/null
+++ b/metropolis/pkg/erofs/uncompressed_inode_writer.go
@@ -0,0 +1,125 @@
+// Copyright 2020 The Monogon Project Authors.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package erofs
+
+import (
+	"bytes"
+	"encoding/binary"
+	"errors"
+	"fmt"
+	"math"
+)
+
+// uncompressedInodeWriter exposes a io.Write-style interface for a single uncompressed inode. It splits the Write-calls
+// into blocks and writes both the blocks and inode metadata. It is required to call Close() to ensure everything is
+// properly written down before writing another inode.
+type uncompressedInodeWriter struct {
+	buf               bytes.Buffer
+	writer            *Writer
+	inode             inodeCompact
+	baseBlock         uint32 // baseBlock == 0 implies this inode didn't allocate a block (yet).
+	writtenBytes      int
+	legacyInodeNumber uint32
+	pathname          string
+}
+
+func (i *uncompressedInodeWriter) allocateBlock() error {
+	bb, err := i.writer.allocateBlocks(1)
+	if err != nil {
+		return err
+	}
+	if i.baseBlock == 0 {
+		i.baseBlock = bb
+	}
+	return nil
+}
+
+func (i *uncompressedInodeWriter) flush(n int) error {
+	if err := i.allocateBlock(); err != nil {
+		return err
+	}
+	slice := i.buf.Next(n)
+	if _, err := i.writer.w.Write(slice); err != nil {
+		return err
+	}
+	// Always pad to BlockSize.
+	_, err := i.writer.w.Write(make([]byte, BlockSize-len(slice)))
+	return err
+}
+
+func (i *uncompressedInodeWriter) Write(b []byte) (int, error) {
+	i.writtenBytes += len(b)
+	if _, err := i.buf.Write(b); err != nil {
+		return 0, err
+	}
+	for i.buf.Len() >= BlockSize {
+		if err := i.flush(BlockSize); err != nil {
+			return 0, err
+		}
+	}
+	return len(b), nil
+}
+
+func (i *uncompressedInodeWriter) Close() error {
+	if i.buf.Len() > BlockSize {
+		panic("programming error")
+	}
+	inodeSize := binary.Size(i.inode)
+	if i.buf.Len()+inodeSize > BlockSize {
+		// Can't fit last part of data inline, write it in its own block.
+		if err := i.flush(i.buf.Len()); err != nil {
+			return err
+		}
+	}
+	if i.buf.Len() == 0 {
+		i.inode.Format = inodeFlatPlain << 1
+	} else {
+		// Colocate last part of data with inode.
+		i.inode.Format = inodeFlatInline << 1
+	}
+	if i.writtenBytes > math.MaxUint32 {
+		return errors.New("inodes bigger than 2^32 need the extended inode format which is unsupported by this library")
+	}
+	i.inode.Size = uint32(i.writtenBytes)
+	if i.baseBlock != 0 {
+		i.inode.Union = i.baseBlock
+	}
+	i.inode.HardlinkCount = 1
+	i.inode.InodeNumCompat = i.legacyInodeNumber
+	basePos, err := i.writer.allocateMetadata(inodeSize+i.buf.Len(), 32)
+	if err != nil {
+		return fmt.Errorf("failed to allocate metadata: %w", err)
+	}
+	i.writer.pathInodeMeta[i.pathname] = &uncompressedInodeMeta{
+		nid:          uint64(basePos) / 32,
+		ftype:        unixModeToFT(i.inode.Mode),
+		blockStart:   int64(i.baseBlock),
+		blockLength:  (int64(i.writtenBytes) / BlockSize) * BlockSize,
+		inlineStart:  basePos + 32,
+		inlineLength: int64(i.buf.Len()),
+		writer:       i.writer,
+	}
+	if err := binary.Write(i.writer.w, binary.LittleEndian, &i.inode); err != nil {
+		return err
+	}
+	if i.inode.Format&(inodeFlatInline<<1) != 0 {
+		// Data colocated in inode, if any.
+		_, err := i.writer.w.Write(i.buf.Bytes())
+		return err
+	}
+	return nil
+}