blob: 3e4ce89267295a82c9b321a1f7b26c105d7461c6 [file] [log] [blame]
Lorenz Brun378a4452021-01-26 13:47:41 +01001// 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
17package erofs
18
19import (
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.
30type Writer struct {
31 w io.WriteSeeker
Serge Bazanski216fe7b2021-05-21 18:36:16 +020032 // 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 Brun378a4452021-01-26 13:47:41 +010037 fixDirectoryEntry map[string][]direntFixupLocation
38 pathInodeMeta map[string]*uncompressedInodeMeta
Serge Bazanski216fe7b2021-05-21 18:36:16 +020039 // legacyInodeIndex stores the next legacy (32-bit) inode to be allocated.
40 // 64 bit inodes are automatically calculated by EROFS on mount.
Lorenz Brun378a4452021-01-26 13:47:41 +010041 legacyInodeIndex uint32
42 blockAllocatorIndex uint32
43 metadataBlocksFree metadataBlocksMeta
44}
45
Serge Bazanski216fe7b2021-05-21 18:36:16 +020046// NewWriter creates a new EROFS filesystem writer. The given WriteSeeker needs
47// to be at the start.
Lorenz Brun378a4452021-01-26 13:47:41 +010048func 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 Bazanski216fe7b2021-05-21 18:36:16 +020062 Magic: Magic,
63 BlockSizeBits: blockSizeBits,
64 // 1024 (padding) + 128 (superblock) / 32, not eligible for fixup as
65 // different int size
66 RootNodeNumber: 36,
Lorenz Brun378a4452021-01-26 13:47:41 +010067 }); err != nil {
68 return nil, fmt.Errorf("failed to write superblock: %w", err)
69 }
70 return erofsWriter, nil
71}
72
Serge Bazanski216fe7b2021-05-21 18:36:16 +020073// 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 Brun378a4452021-01-26 13:47:41 +010076func (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 Bazanski216fe7b2021-05-21 18:36:16 +020099// 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 Brun378a4452021-01-26 13:47:41 +0100103func (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
112func (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 Bazanski216fe7b2021-05-21 18:36:16 +0200123// 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 Brun378a4452021-01-26 13:47:41 +0100128func (w *Writer) CreateFile(pathname string, meta *FileMeta) io.WriteCloser {
129 return w.create(pathname, meta)
130}
131
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200132// 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 Brun378a4452021-01-26 13:47:41 +0100137func (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 Bazanski216fe7b2021-05-21 18:36:16 +0200152// 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 Brun378a4452021-01-26 13:47:41 +0100155func (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 Bazanski216fe7b2021-05-21 18:36:16 +0200170// 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 Brun378a4452021-01-26 13:47:41 +0100173type 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
187func (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
200func (a *uncompressedInodeMeta) Write(p []byte) (int, error) {
201 if a.currentOffset < a.blockLength {
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200202 // 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 Brun378a4452021-01-26 13:47:41 +0100205 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
214type direntFixupLocation struct {
215 path string
216 entryIndex uint16
217}
218
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200219// 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 Brun378a4452021-01-26 13:47:41 +0100222func 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
238type metadataBlockMeta struct {
239 blockNumber uint32
240 freeBytes uint16
241}
242
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200243// 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 Brun378a4452021-01-26 13:47:41 +0100246type metadataBlocksMeta []metadataBlockMeta
247
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200248// 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 Brun378a4452021-01-26 13:47:41 +0100251func (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
266var 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.
277func unixModeToFT(mode uint16) uint8 {
278 return unixModeToFTMap[mode&unix.S_IFMT]
279}