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
+}