| // 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] |
| } |