blob: af6ad1cf0ff273786bee5b9e615a5449e6eb843f [file] [log] [blame]
// 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 (
"encoding/binary"
"errors"
"fmt"
"io"
"path"
"golang.org/x/sys/unix"
)
// Writer writes a new EROFS filesystem.
type Writer struct {
w io.WriteSeeker
// fixDirectoryEntry contains for each referenced path where it is referenced from. Since self-references
// are required anyways (for the "." and ".." entries) we let the user write files in any order and just
// point the directory entries to the right target nid and file type on Close().
fixDirectoryEntry map[string][]direntFixupLocation
pathInodeMeta map[string]*uncompressedInodeMeta
// legacyInodeIndex stores the next legacy (32-bit) inode to be allocated. 64 bit inodes are automatically
// calculated by EROFS on mount.
legacyInodeIndex uint32
blockAllocatorIndex uint32
metadataBlocksFree metadataBlocksMeta
}
// NewWriter creates a new EROFS filesystem writer. The given WriteSeeker needs to be at the start.
func NewWriter(w io.WriteSeeker) (*Writer, error) {
erofsWriter := &Writer{
w: w,
fixDirectoryEntry: make(map[string][]direntFixupLocation),
pathInodeMeta: make(map[string]*uncompressedInodeMeta),
}
_, err := erofsWriter.allocateMetadata(1024+binary.Size(&superblock{}), 0)
if err != nil {
return nil, fmt.Errorf("cannot allocate first metadata block: %w", err)
}
if _, err := erofsWriter.w.Write(make([]byte, 1024)); err != nil { // Padding
return nil, fmt.Errorf("failed to write initial padding: %w", err)
}
if err := binary.Write(erofsWriter.w, binary.LittleEndian, &superblock{
Magic: Magic,
BlockSizeBits: blockSizeBits,
RootNodeNumber: 36, // 1024 (padding) + 128 (superblock) / 32, not eligible for fixup as different int size
}); err != nil {
return nil, fmt.Errorf("failed to write superblock: %w", err)
}
return erofsWriter, nil
}
// allocateMetadata allocates metadata space of size bytes with a given alignment and seeks to the first byte of the
// newly-allocated metadata space. It also returns the position of that first byte.
func (w *Writer) allocateMetadata(size int, alignment uint16) (int64, error) {
if size > BlockSize {
panic("cannot allocate a metadata object bigger than BlockSize bytes")
}
sizeU16 := uint16(size)
pos, ok := w.metadataBlocksFree.findBlock(sizeU16, 32)
if !ok {
blockNumber, err := w.allocateBlocks(1)
if err != nil {
return 0, fmt.Errorf("failed to allocate additional metadata space: %w", err)
}
w.metadataBlocksFree = append(w.metadataBlocksFree, metadataBlockMeta{blockNumber: blockNumber, freeBytes: BlockSize - sizeU16})
if _, err := w.w.Write(make([]byte, BlockSize)); err != nil {
return 0, fmt.Errorf("failed to write metadata: %w", err)
}
pos = int64(blockNumber) * BlockSize // Always aligned to BlockSize, bigger alignments are unsupported anyways
}
if _, err := w.w.Seek(pos, io.SeekStart); err != nil {
return 0, fmt.Errorf("cannot seek to existing metadata nid, likely misaligned meta write")
}
return pos, nil
}
// allocateBlocks allocates n new BlockSize-sized block and seeks to the beginning of the first newly-allocated block.
// It also returns the first newly-allocated block number. The caller is expected to write these blocks completely
// before calling allocateBlocks again.
func (w *Writer) allocateBlocks(n uint32) (uint32, error) {
if _, err := w.w.Seek(int64(w.blockAllocatorIndex)*BlockSize, io.SeekStart); err != nil {
return 0, fmt.Errorf("cannot seek to end of last block, check write alignment: %w", err)
}
firstBlock := w.blockAllocatorIndex
w.blockAllocatorIndex += n
return firstBlock, nil
}
func (w *Writer) create(pathname string, inode Inode) *uncompressedInodeWriter {
i := &uncompressedInodeWriter{
writer: w,
inode: *inode.inode(),
legacyInodeNumber: w.legacyInodeIndex,
pathname: path.Clean(pathname),
}
w.legacyInodeIndex++
return i
}
// CreateFile adds a new file to the EROFS. It returns a WriteCloser to which the file contents should be written and
// which then needs to be closed. The last writer obtained by calling CreateFile() needs to be closed first before
// opening a new one. The given pathname needs to be referenced by a directory created using Create(), otherwise it will
// not be accessible.
func (w *Writer) CreateFile(pathname string, meta *FileMeta) io.WriteCloser {
return w.create(pathname, meta)
}
// Create adds a new non-file inode to the EROFS. This includes directories, device nodes, symlinks and FIFOs.
// The first call to Create() needs to be with pathname "." and a directory inode.
// The given pathname needs to be referenced by a directory, otherwise it will not be accessible (with the exception of
// the directory ".").
func (w *Writer) Create(pathname string, inode Inode) error {
iw := w.create(pathname, inode)
switch i := inode.(type) {
case *Directory:
if err := i.writeTo(iw); err != nil {
return fmt.Errorf("failed to write directory contents: %w", err)
}
case *SymbolicLink:
if err := i.writeTo(iw); err != nil {
return fmt.Errorf("failed to write symbolic link contents: %w", err)
}
}
return iw.Close()
}
// Close finishes writing an EROFS filesystem. Errors by this function need to be handled as they indicate if the
// written filesystem is consistent (i.e. there are no directory entries pointing to nonexistent inodes).
func (w *Writer) Close() error {
for targetPath, entries := range w.fixDirectoryEntry {
for _, entry := range entries {
targetMeta, ok := w.pathInodeMeta[targetPath]
if !ok {
return fmt.Errorf("failed to link filesystem tree: dangling reference to %v", targetPath)
}
if err := direntFixup(w.pathInodeMeta[entry.path], int64(entry.entryIndex), targetMeta); err != nil {
return err
}
}
}
return nil
}
// uncompressedInodeMeta tracks enough metadata about a written inode to be able to point dirents to it and to provide
// a WriteSeeker into the inode itself.
type uncompressedInodeMeta struct {
nid uint64
ftype uint8
// Physical placement metdata
blockStart int64
blockLength int64
inlineStart int64
inlineLength int64
writer *Writer
currentOffset int64
}
func (a *uncompressedInodeMeta) Seek(offset int64, whence int) (int64, error) {
switch whence {
case io.SeekCurrent:
break
case io.SeekStart:
a.currentOffset = 0
case io.SeekEnd:
a.currentOffset = a.blockLength + a.inlineLength
}
a.currentOffset += offset
return a.currentOffset, nil
}
func (a *uncompressedInodeMeta) Write(p []byte) (int, error) {
if a.currentOffset < a.blockLength {
// TODO(lorenz): Handle the special case where a directory inode is spread across multiple
// blocks (depending on other factors this occurs around ~200 direct children).
return 0, errors.New("relocating dirents in multi-block directory inodes is unimplemented")
}
if _, err := a.writer.w.Seek(a.inlineStart+a.currentOffset, io.SeekStart); err != nil {
return 0, err
}
a.currentOffset += int64(len(p))
return a.writer.w.Write(p)
}
type direntFixupLocation struct {
path string
entryIndex uint16
}
// direntFixup overrides nid and file type from the path the dirent is pointing to. The given iw is expected to be at
// the start of the dirent inode to be fixed up.
func direntFixup(iw io.WriteSeeker, entryIndex int64, meta *uncompressedInodeMeta) error {
if _, err := iw.Seek(entryIndex*12, io.SeekStart); err != nil {
return fmt.Errorf("failed to seek to dirent: %w", err)
}
if err := binary.Write(iw, binary.LittleEndian, meta.nid); err != nil {
return fmt.Errorf("failed to write nid: %w", err)
}
if _, err := iw.Seek(2, io.SeekCurrent); err != nil { // Skip NameStartOffset
return fmt.Errorf("failed to seek to dirent: %w", err)
}
if err := binary.Write(iw, binary.LittleEndian, meta.ftype); err != nil {
return fmt.Errorf("failed to write ftype: %w", err)
}
return nil
}
type metadataBlockMeta struct {
blockNumber uint32
freeBytes uint16
}
// metadataBlocksMeta contains metadata about all metadata blocks, most importantly the amount of free
// bytes in each block. This is not a map for reproducibility (map ordering).
type metadataBlocksMeta []metadataBlockMeta
// findBlock returns the absolute position where `size` bytes with the specified alignment can still fit.
// If there is not enough space in any metadata block it returns false as the second return value.
func (m metadataBlocksMeta) findBlock(size uint16, alignment uint16) (int64, bool) {
for i, blockMeta := range m {
freeBytesAligned := blockMeta.freeBytes - (blockMeta.freeBytes % alignment)
if freeBytesAligned > size {
m[i] = metadataBlockMeta{
blockNumber: blockMeta.blockNumber,
freeBytes: freeBytesAligned - size,
}
pos := int64(blockMeta.blockNumber+1)*BlockSize - int64(freeBytesAligned)
return pos, true
}
}
return 0, false
}
var unixModeToFTMap = map[uint16]uint8{
unix.S_IFREG: fileTypeRegularFile,
unix.S_IFDIR: fileTypeDirectory,
unix.S_IFCHR: fileTypeCharacterDevice,
unix.S_IFBLK: fileTypeBlockDevice,
unix.S_IFIFO: fileTypeFIFO,
unix.S_IFSOCK: fileTypeSocket,
unix.S_IFLNK: fileTypeSymbolicLink,
}
// unixModeToFT maps a Unix file type to an EROFS file type.
func unixModeToFT(mode uint16) uint8 {
return unixModeToFTMap[mode&unix.S_IFMT]
}