Lorenz Brun | 378a445 | 2021-01-26 13:47:41 +0100 | [diff] [blame] | 1 | // Copyright 2020 The Monogon Project Authors. |
| 2 | // |
| 3 | // SPDX-License-Identifier: Apache-2.0 |
| 4 | // |
| 5 | // Licensed under the Apache License, Version 2.0 (the "License"); |
| 6 | // you may not use this file except in compliance with the License. |
| 7 | // You may obtain a copy of the License at |
| 8 | // |
| 9 | // http://www.apache.org/licenses/LICENSE-2.0 |
| 10 | // |
| 11 | // Unless required by applicable law or agreed to in writing, software |
| 12 | // distributed under the License is distributed on an "AS IS" BASIS, |
| 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 14 | // See the License for the specific language governing permissions and |
| 15 | // limitations under the License. |
| 16 | |
| 17 | package erofs |
| 18 | |
| 19 | import ( |
| 20 | "encoding/binary" |
| 21 | "errors" |
| 22 | "fmt" |
| 23 | "io" |
| 24 | "path" |
| 25 | |
| 26 | "golang.org/x/sys/unix" |
| 27 | ) |
| 28 | |
| 29 | // Writer writes a new EROFS filesystem. |
| 30 | type Writer struct { |
| 31 | w io.WriteSeeker |
Serge Bazanski | 216fe7b | 2021-05-21 18:36:16 +0200 | [diff] [blame^] | 32 | // fixDirectoryEntry contains for each referenced path where it is |
| 33 | // referenced from. Since self-references are required anyways (for the "." |
| 34 | // and ".." entries) we let the user write files in any order and just |
| 35 | // point the directory entries to the right target nid and file type on |
| 36 | // Close(). |
Lorenz Brun | 378a445 | 2021-01-26 13:47:41 +0100 | [diff] [blame] | 37 | fixDirectoryEntry map[string][]direntFixupLocation |
| 38 | pathInodeMeta map[string]*uncompressedInodeMeta |
Serge Bazanski | 216fe7b | 2021-05-21 18:36:16 +0200 | [diff] [blame^] | 39 | // legacyInodeIndex stores the next legacy (32-bit) inode to be allocated. |
| 40 | // 64 bit inodes are automatically calculated by EROFS on mount. |
Lorenz Brun | 378a445 | 2021-01-26 13:47:41 +0100 | [diff] [blame] | 41 | legacyInodeIndex uint32 |
| 42 | blockAllocatorIndex uint32 |
| 43 | metadataBlocksFree metadataBlocksMeta |
| 44 | } |
| 45 | |
Serge Bazanski | 216fe7b | 2021-05-21 18:36:16 +0200 | [diff] [blame^] | 46 | // NewWriter creates a new EROFS filesystem writer. The given WriteSeeker needs |
| 47 | // to be at the start. |
Lorenz Brun | 378a445 | 2021-01-26 13:47:41 +0100 | [diff] [blame] | 48 | func NewWriter(w io.WriteSeeker) (*Writer, error) { |
| 49 | erofsWriter := &Writer{ |
| 50 | w: w, |
| 51 | fixDirectoryEntry: make(map[string][]direntFixupLocation), |
| 52 | pathInodeMeta: make(map[string]*uncompressedInodeMeta), |
| 53 | } |
| 54 | _, err := erofsWriter.allocateMetadata(1024+binary.Size(&superblock{}), 0) |
| 55 | if err != nil { |
| 56 | return nil, fmt.Errorf("cannot allocate first metadata block: %w", err) |
| 57 | } |
| 58 | if _, err := erofsWriter.w.Write(make([]byte, 1024)); err != nil { // Padding |
| 59 | return nil, fmt.Errorf("failed to write initial padding: %w", err) |
| 60 | } |
| 61 | if err := binary.Write(erofsWriter.w, binary.LittleEndian, &superblock{ |
Serge Bazanski | 216fe7b | 2021-05-21 18:36:16 +0200 | [diff] [blame^] | 62 | Magic: Magic, |
| 63 | BlockSizeBits: blockSizeBits, |
| 64 | // 1024 (padding) + 128 (superblock) / 32, not eligible for fixup as |
| 65 | // different int size |
| 66 | RootNodeNumber: 36, |
Lorenz Brun | 378a445 | 2021-01-26 13:47:41 +0100 | [diff] [blame] | 67 | }); err != nil { |
| 68 | return nil, fmt.Errorf("failed to write superblock: %w", err) |
| 69 | } |
| 70 | return erofsWriter, nil |
| 71 | } |
| 72 | |
Serge Bazanski | 216fe7b | 2021-05-21 18:36:16 +0200 | [diff] [blame^] | 73 | // allocateMetadata allocates metadata space of size bytes with a given |
| 74 | // alignment and seeks to the first byte of the newly-allocated metadata space. |
| 75 | // It also returns the position of that first byte. |
Lorenz Brun | 378a445 | 2021-01-26 13:47:41 +0100 | [diff] [blame] | 76 | func (w *Writer) allocateMetadata(size int, alignment uint16) (int64, error) { |
| 77 | if size > BlockSize { |
| 78 | panic("cannot allocate a metadata object bigger than BlockSize bytes") |
| 79 | } |
| 80 | sizeU16 := uint16(size) |
| 81 | pos, ok := w.metadataBlocksFree.findBlock(sizeU16, 32) |
| 82 | if !ok { |
| 83 | blockNumber, err := w.allocateBlocks(1) |
| 84 | if err != nil { |
| 85 | return 0, fmt.Errorf("failed to allocate additional metadata space: %w", err) |
| 86 | } |
| 87 | w.metadataBlocksFree = append(w.metadataBlocksFree, metadataBlockMeta{blockNumber: blockNumber, freeBytes: BlockSize - sizeU16}) |
| 88 | if _, err := w.w.Write(make([]byte, BlockSize)); err != nil { |
| 89 | return 0, fmt.Errorf("failed to write metadata: %w", err) |
| 90 | } |
| 91 | pos = int64(blockNumber) * BlockSize // Always aligned to BlockSize, bigger alignments are unsupported anyways |
| 92 | } |
| 93 | if _, err := w.w.Seek(pos, io.SeekStart); err != nil { |
| 94 | return 0, fmt.Errorf("cannot seek to existing metadata nid, likely misaligned meta write") |
| 95 | } |
| 96 | return pos, nil |
| 97 | } |
| 98 | |
Serge Bazanski | 216fe7b | 2021-05-21 18:36:16 +0200 | [diff] [blame^] | 99 | // allocateBlocks allocates n new BlockSize-sized block and seeks to the |
| 100 | // beginning of the first newly-allocated block. It also returns the first |
| 101 | // newly-allocated block number. The caller is expected to write these blocks |
| 102 | // completely before calling allocateBlocks again. |
Lorenz Brun | 378a445 | 2021-01-26 13:47:41 +0100 | [diff] [blame] | 103 | func (w *Writer) allocateBlocks(n uint32) (uint32, error) { |
| 104 | if _, err := w.w.Seek(int64(w.blockAllocatorIndex)*BlockSize, io.SeekStart); err != nil { |
| 105 | return 0, fmt.Errorf("cannot seek to end of last block, check write alignment: %w", err) |
| 106 | } |
| 107 | firstBlock := w.blockAllocatorIndex |
| 108 | w.blockAllocatorIndex += n |
| 109 | return firstBlock, nil |
| 110 | } |
| 111 | |
| 112 | func (w *Writer) create(pathname string, inode Inode) *uncompressedInodeWriter { |
| 113 | i := &uncompressedInodeWriter{ |
| 114 | writer: w, |
| 115 | inode: *inode.inode(), |
| 116 | legacyInodeNumber: w.legacyInodeIndex, |
| 117 | pathname: path.Clean(pathname), |
| 118 | } |
| 119 | w.legacyInodeIndex++ |
| 120 | return i |
| 121 | } |
| 122 | |
Serge Bazanski | 216fe7b | 2021-05-21 18:36:16 +0200 | [diff] [blame^] | 123 | // CreateFile adds a new file to the EROFS. It returns a WriteCloser to which |
| 124 | // the file contents should be written and which then needs to be closed. The |
| 125 | // last writer obtained by calling CreateFile() needs to be closed first before |
| 126 | // opening a new one. The given pathname needs to be referenced by a directory |
| 127 | // created using Create(), otherwise it will not be accessible. |
Lorenz Brun | 378a445 | 2021-01-26 13:47:41 +0100 | [diff] [blame] | 128 | func (w *Writer) CreateFile(pathname string, meta *FileMeta) io.WriteCloser { |
| 129 | return w.create(pathname, meta) |
| 130 | } |
| 131 | |
Serge Bazanski | 216fe7b | 2021-05-21 18:36:16 +0200 | [diff] [blame^] | 132 | // Create adds a new non-file inode to the EROFS. This includes directories, |
| 133 | // device nodes, symlinks and FIFOs. The first call to Create() needs to be |
| 134 | // with pathname "." and a directory inode. The given pathname needs to be |
| 135 | // referenced by a directory, otherwise it will not be accessible (with the |
| 136 | // exception of the directory "."). |
Lorenz Brun | 378a445 | 2021-01-26 13:47:41 +0100 | [diff] [blame] | 137 | func (w *Writer) Create(pathname string, inode Inode) error { |
| 138 | iw := w.create(pathname, inode) |
| 139 | switch i := inode.(type) { |
| 140 | case *Directory: |
| 141 | if err := i.writeTo(iw); err != nil { |
| 142 | return fmt.Errorf("failed to write directory contents: %w", err) |
| 143 | } |
| 144 | case *SymbolicLink: |
| 145 | if err := i.writeTo(iw); err != nil { |
| 146 | return fmt.Errorf("failed to write symbolic link contents: %w", err) |
| 147 | } |
| 148 | } |
| 149 | return iw.Close() |
| 150 | } |
| 151 | |
Serge Bazanski | 216fe7b | 2021-05-21 18:36:16 +0200 | [diff] [blame^] | 152 | // Close finishes writing an EROFS filesystem. Errors by this function need to |
| 153 | // be handled as they indicate if the written filesystem is consistent (i.e. |
| 154 | // there are no directory entries pointing to nonexistent inodes). |
Lorenz Brun | 378a445 | 2021-01-26 13:47:41 +0100 | [diff] [blame] | 155 | func (w *Writer) Close() error { |
| 156 | for targetPath, entries := range w.fixDirectoryEntry { |
| 157 | for _, entry := range entries { |
| 158 | targetMeta, ok := w.pathInodeMeta[targetPath] |
| 159 | if !ok { |
| 160 | return fmt.Errorf("failed to link filesystem tree: dangling reference to %v", targetPath) |
| 161 | } |
| 162 | if err := direntFixup(w.pathInodeMeta[entry.path], int64(entry.entryIndex), targetMeta); err != nil { |
| 163 | return err |
| 164 | } |
| 165 | } |
| 166 | } |
| 167 | return nil |
| 168 | } |
| 169 | |
Serge Bazanski | 216fe7b | 2021-05-21 18:36:16 +0200 | [diff] [blame^] | 170 | // uncompressedInodeMeta tracks enough metadata about a written inode to be |
| 171 | // able to point dirents to it and to provide a WriteSeeker into the inode |
| 172 | // itself. |
Lorenz Brun | 378a445 | 2021-01-26 13:47:41 +0100 | [diff] [blame] | 173 | type uncompressedInodeMeta struct { |
| 174 | nid uint64 |
| 175 | ftype uint8 |
| 176 | |
| 177 | // Physical placement metdata |
| 178 | blockStart int64 |
| 179 | blockLength int64 |
| 180 | inlineStart int64 |
| 181 | inlineLength int64 |
| 182 | |
| 183 | writer *Writer |
| 184 | currentOffset int64 |
| 185 | } |
| 186 | |
| 187 | func (a *uncompressedInodeMeta) Seek(offset int64, whence int) (int64, error) { |
| 188 | switch whence { |
| 189 | case io.SeekCurrent: |
| 190 | break |
| 191 | case io.SeekStart: |
| 192 | a.currentOffset = 0 |
| 193 | case io.SeekEnd: |
| 194 | a.currentOffset = a.blockLength + a.inlineLength |
| 195 | } |
| 196 | a.currentOffset += offset |
| 197 | return a.currentOffset, nil |
| 198 | } |
| 199 | |
| 200 | func (a *uncompressedInodeMeta) Write(p []byte) (int, error) { |
| 201 | if a.currentOffset < a.blockLength { |
Serge Bazanski | 216fe7b | 2021-05-21 18:36:16 +0200 | [diff] [blame^] | 202 | // TODO(lorenz): Handle the special case where a directory inode is |
| 203 | // spread across multiple blocks (depending on other factors this |
| 204 | // occurs around ~200 direct children). |
Lorenz Brun | 378a445 | 2021-01-26 13:47:41 +0100 | [diff] [blame] | 205 | return 0, errors.New("relocating dirents in multi-block directory inodes is unimplemented") |
| 206 | } |
| 207 | if _, err := a.writer.w.Seek(a.inlineStart+a.currentOffset, io.SeekStart); err != nil { |
| 208 | return 0, err |
| 209 | } |
| 210 | a.currentOffset += int64(len(p)) |
| 211 | return a.writer.w.Write(p) |
| 212 | } |
| 213 | |
| 214 | type direntFixupLocation struct { |
| 215 | path string |
| 216 | entryIndex uint16 |
| 217 | } |
| 218 | |
Serge Bazanski | 216fe7b | 2021-05-21 18:36:16 +0200 | [diff] [blame^] | 219 | // direntFixup overrides nid and file type from the path the dirent is pointing |
| 220 | // to. The given iw is expected to be at the start of the dirent inode to be |
| 221 | // fixed up. |
Lorenz Brun | 378a445 | 2021-01-26 13:47:41 +0100 | [diff] [blame] | 222 | func direntFixup(iw io.WriteSeeker, entryIndex int64, meta *uncompressedInodeMeta) error { |
| 223 | if _, err := iw.Seek(entryIndex*12, io.SeekStart); err != nil { |
| 224 | return fmt.Errorf("failed to seek to dirent: %w", err) |
| 225 | } |
| 226 | if err := binary.Write(iw, binary.LittleEndian, meta.nid); err != nil { |
| 227 | return fmt.Errorf("failed to write nid: %w", err) |
| 228 | } |
| 229 | if _, err := iw.Seek(2, io.SeekCurrent); err != nil { // Skip NameStartOffset |
| 230 | return fmt.Errorf("failed to seek to dirent: %w", err) |
| 231 | } |
| 232 | if err := binary.Write(iw, binary.LittleEndian, meta.ftype); err != nil { |
| 233 | return fmt.Errorf("failed to write ftype: %w", err) |
| 234 | } |
| 235 | return nil |
| 236 | } |
| 237 | |
| 238 | type metadataBlockMeta struct { |
| 239 | blockNumber uint32 |
| 240 | freeBytes uint16 |
| 241 | } |
| 242 | |
Serge Bazanski | 216fe7b | 2021-05-21 18:36:16 +0200 | [diff] [blame^] | 243 | // metadataBlocksMeta contains metadata about all metadata blocks, most |
| 244 | // importantly the amount of free bytes in each block. This is not a map for |
| 245 | // reproducibility (map ordering). |
Lorenz Brun | 378a445 | 2021-01-26 13:47:41 +0100 | [diff] [blame] | 246 | type metadataBlocksMeta []metadataBlockMeta |
| 247 | |
Serge Bazanski | 216fe7b | 2021-05-21 18:36:16 +0200 | [diff] [blame^] | 248 | // findBlock returns the absolute position where `size` bytes with the |
| 249 | // specified alignment can still fit. If there is not enough space in any |
| 250 | // metadata block it returns false as the second return value. |
Lorenz Brun | 378a445 | 2021-01-26 13:47:41 +0100 | [diff] [blame] | 251 | func (m metadataBlocksMeta) findBlock(size uint16, alignment uint16) (int64, bool) { |
| 252 | for i, blockMeta := range m { |
| 253 | freeBytesAligned := blockMeta.freeBytes - (blockMeta.freeBytes % alignment) |
| 254 | if freeBytesAligned > size { |
| 255 | m[i] = metadataBlockMeta{ |
| 256 | blockNumber: blockMeta.blockNumber, |
| 257 | freeBytes: freeBytesAligned - size, |
| 258 | } |
| 259 | pos := int64(blockMeta.blockNumber+1)*BlockSize - int64(freeBytesAligned) |
| 260 | return pos, true |
| 261 | } |
| 262 | } |
| 263 | return 0, false |
| 264 | } |
| 265 | |
| 266 | var unixModeToFTMap = map[uint16]uint8{ |
| 267 | unix.S_IFREG: fileTypeRegularFile, |
| 268 | unix.S_IFDIR: fileTypeDirectory, |
| 269 | unix.S_IFCHR: fileTypeCharacterDevice, |
| 270 | unix.S_IFBLK: fileTypeBlockDevice, |
| 271 | unix.S_IFIFO: fileTypeFIFO, |
| 272 | unix.S_IFSOCK: fileTypeSocket, |
| 273 | unix.S_IFLNK: fileTypeSymbolicLink, |
| 274 | } |
| 275 | |
| 276 | // unixModeToFT maps a Unix file type to an EROFS file type. |
| 277 | func unixModeToFT(mode uint16) uint8 { |
| 278 | return unixModeToFTMap[mode&unix.S_IFMT] |
| 279 | } |