blob: e5c2e586fa79152548903ca8f36efcf77358ad58 [file] [log] [blame]
Tim Windelschmidt6d33a432025-02-04 14:34:25 +01001// Copyright The Monogon Project Authors.
2// SPDX-License-Identifier: Apache-2.0
3
Lorenz Brunbd2ce6d2022-07-22 00:00:13 +00004// Package fat32 implements a writer for the FAT32 filesystem.
5package fat32
6
7import (
8 "crypto/rand"
9 "encoding/binary"
10 "errors"
11 "fmt"
12 "io"
13 "io/fs"
14 "math"
15 "math/bits"
16 "strings"
17 "time"
18 "unicode/utf16"
19)
20
21// This package contains multiple references to the FAT32 specification, called
22// Microsoft Extensible Firmware Initiative FAT32 File System Specification
23// version 1.03 (just called the spec from now on). You can get it at
24// https://download.microsoft.com/download/0/8/4/\
25// 084c452b-b772-4fe5-89bb-a0cbf082286a/fatgen103.doc
26
27type Options struct {
28 // Size of a logical block on the block device. Needs to be a power of two
29 // equal or bigger than 512. If left at zero, defaults to 512.
30 BlockSize uint16
31
32 // Number of blocks the filesystem should span. If zero, it will be exactly
33 // as large as it needs to be.
34 BlockCount uint32
35
36 // Human-readable filesystem label. Maximum 10 bytes (gets cut off), should
37 // be uppercase alphanumeric.
38 Label string
39
40 // Filesystem identifier. If unset (i.e. left at zero) a random value will
41 // be assigned by WriteFS.
42 ID uint32
43}
44
45// SizedReader is an io.Reader with a known size
46type SizedReader interface {
47 io.Reader
48 Size() int64
49}
50
51// Attribute is a bitset of flags set on an inode.
52// See also the spec page 24
53type Attribute uint8
54
55const (
56 // AttrReadOnly marks a file as read-only
57 AttrReadOnly Attribute = 0x01
58 // AttrHidden indicates that directory listings should not show this file.
59 AttrHidden Attribute = 0x02
60 // AttrSystem indicates that this is an operating system file.
61 AttrSystem Attribute = 0x04
62 // AttrDirectory indicates that this is a directory and not a file.
63 AttrDirectory Attribute = 0x10
64 // AttrArchive canonically indicates that a file has been created/modified
65 // since the last backup. Its use in practice is inconsistent.
66 AttrArchive Attribute = 0x20
67)
68
69// Inode is file or directory on the FAT32 filesystem. Note that the concept
70// of an inode doesn't really exist on FAT32, its directories are just special
71// files.
72type Inode struct {
73 // Name of the file or directory (not including its path)
74 Name string
75 // Time the file or directory was last modified
76 ModTime time.Time
77 // Time the file or directory was created
78 CreateTime time.Time
79 // Attributes
80 Attrs Attribute
81 // Children of this directory (only valid when Attrs has AttrDirectory set)
82 Children []*Inode
83 // Content of this file
84 // Only valid when Attrs doesn't have AttrDirectory set.
85 Content SizedReader
86
87 // Filled out on placement and write-out
88 startCluster int
89 parent *Inode
90 dosName [11]byte
91}
92
93// Number of LFN entries + normal entry (all 32 bytes)
94func (i Inode) metaSize() (int64, error) {
95 fileNameUTF16 := utf16.Encode([]rune(i.Name))
96 // VFAT file names are null-terminated
97 fileNameUTF16 = append(fileNameUTF16, 0x00)
98 if len(fileNameUTF16) > 255 {
99 return 0, errors.New("file name too long, maximum is 255 UTF-16 code points")
100 }
101
102 // ⌈len(fileNameUTF16)/codepointsPerEntry⌉
103 numEntries := (len(fileNameUTF16) + codepointsPerEntry - 1) / codepointsPerEntry
104 return (int64(numEntries) + 1) * 32, nil
105}
106
107func lfnChecksum(dosName [11]byte) uint8 {
108 var sum uint8
109 for _, b := range dosName {
110 sum = ((sum & 1) << 7) + (sum >> 1) + b
111 }
112 return sum
113}
114
115// writeMeta writes information about this inode into the contents of the parent
116// inode.
117func (i Inode) writeMeta(w io.Writer) error {
118 fileNameUTF16 := utf16.Encode([]rune(i.Name))
119 // VFAT file names are null-terminated
120 fileNameUTF16 = append(fileNameUTF16, 0x00)
121 if len(fileNameUTF16) > 255 {
122 return errors.New("file name too long, maximum is 255 UTF-16 code points")
123 }
124
125 // ⌈len(fileNameUTF16)/codepointsPerEntry⌉
126 numEntries := (len(fileNameUTF16) + codepointsPerEntry - 1) / codepointsPerEntry
127 // Fill up to space in given number of entries with fill code point 0xffff
128 fillCodePoints := (numEntries * codepointsPerEntry) - len(fileNameUTF16)
129 for j := 0; j < fillCodePoints; j++ {
130 fileNameUTF16 = append(fileNameUTF16, 0xffff)
131 }
132
133 // Write entries in reverse order
134 for j := numEntries; j > 0; j-- {
135 // Index of the code point being processed
136 cpIdx := (j - 1) * codepointsPerEntry
137 var entry lfnEntry
138 entry.Checksum = lfnChecksum(i.dosName)
139 // Downcast is safe as i <= numEntries <= ⌈255/codepointsPerEntry⌉
140 entry.SequenceNumber = uint8(j)
141 if j == numEntries {
142 entry.SequenceNumber |= lastSequenceNumberFlag
143 }
144 entry.Attributes = 0x0F
145 copy(entry.NamePart1[:], fileNameUTF16[cpIdx:])
146 cpIdx += len(entry.NamePart1)
147 copy(entry.NamePart2[:], fileNameUTF16[cpIdx:])
148 cpIdx += len(entry.NamePart2)
149 copy(entry.NamePart3[:], fileNameUTF16[cpIdx:])
150 cpIdx += len(entry.NamePart3)
151
152 if err := binary.Write(w, binary.LittleEndian, entry); err != nil {
153 return err
154 }
155 }
156 selfSize, err := i.dataSize()
157 if err != nil {
158 return err
159 }
160 if selfSize >= 4*1024*1024*1024 {
161 return errors.New("single file size exceeds 4GiB which is prohibited in FAT32")
162 }
163 if i.Attrs&AttrDirectory != 0 {
164 selfSize = 0 // Directories don't have an explicit size
165 }
166 date, t, _ := timeToMsDosTime(i.ModTime)
Jan Schär79595192024-11-11 14:55:56 +0100167 cdate, ctime, ctens := timeToMsDosTime(i.CreateTime)
Lorenz Brunbd2ce6d2022-07-22 00:00:13 +0000168 if err := binary.Write(w, binary.LittleEndian, &dirEntry{
169 DOSName: i.dosName,
170 Attributes: uint8(i.Attrs),
Jan Schär79595192024-11-11 14:55:56 +0100171 CreationTenMilli: ctens,
172 CreationTime: ctime,
173 CreationDate: cdate,
Lorenz Brunbd2ce6d2022-07-22 00:00:13 +0000174 FirstClusterHigh: uint16(i.startCluster >> 16),
175 LastWrittenToTime: t,
176 LastWrittenToDate: date,
177 FirstClusterLow: uint16(i.startCluster & 0xffff),
178 FileSize: uint32(selfSize),
179 }); err != nil {
180 return err
181 }
182 return nil
183}
184
185// writeData writes the contents of this inode (including possible metadata
186// of its children, but not its children's data)
187func (i Inode) writeData(w io.Writer, volumeLabel [11]byte) error {
188 if i.Attrs&AttrDirectory != 0 {
189 if i.parent == nil {
190 if err := binary.Write(w, binary.LittleEndian, &dirEntry{
191 DOSName: volumeLabel,
192 Attributes: 0x08, // Volume ID, internal use only
193 }); err != nil {
194 return err
195 }
196 } else {
197 date, t, _ := timeToMsDosTime(i.ModTime)
198 cdate, ctime, ctens := timeToMsDosTime(i.CreateTime)
199 if err := binary.Write(w, binary.LittleEndian, &dirEntry{
200 DOSName: [11]byte{'.', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '},
201 CreationDate: cdate,
202 CreationTime: ctime,
203 CreationTenMilli: ctens,
204 LastWrittenToTime: t,
205 LastWrittenToDate: date,
206 Attributes: uint8(i.Attrs),
207 FirstClusterHigh: uint16(i.startCluster >> 16),
208 FirstClusterLow: uint16(i.startCluster & 0xffff),
209 }); err != nil {
210 return err
211 }
212 startCluster := i.parent.startCluster
213 if i.parent.parent == nil {
214 // Special case: When the dotdot directory points to the root
215 // directory, the start cluster is defined to be zero even if
216 // it isn't.
217 startCluster = 0
218 }
219 // Time is intentionally taken from this directory, not the parent
220 if err := binary.Write(w, binary.LittleEndian, &dirEntry{
221 DOSName: [11]byte{'.', '.', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '},
Jan Schär79595192024-11-11 14:55:56 +0100222 CreationDate: cdate,
223 CreationTime: ctime,
224 CreationTenMilli: ctens,
Lorenz Brunbd2ce6d2022-07-22 00:00:13 +0000225 LastWrittenToTime: t,
226 LastWrittenToDate: date,
227 Attributes: uint8(AttrDirectory),
228 FirstClusterHigh: uint16(startCluster >> 16),
229 FirstClusterLow: uint16(startCluster & 0xffff),
230 }); err != nil {
231 return err
232 }
233 }
234 err := makeUniqueDOSNames(i.Children)
235 if err != nil {
236 return err
237 }
238 for _, c := range i.Children {
239 if err := c.writeMeta(w); err != nil {
240 return err
241 }
242 }
243 } else {
244 if _, err := io.CopyN(w, i.Content, i.Content.Size()); err != nil {
245 return err
246 }
247 }
248 return nil
249}
250
251func (i Inode) dataSize() (int64, error) {
252 if i.Attrs&AttrDirectory != 0 {
253 var size int64
254 if i.parent != nil {
255 // Dot and dotdot directories
256 size += 2 * 32
257 } else {
258 // Volume ID
259 size += 1 * 32
260 }
261 for _, c := range i.Children {
262 cs, err := c.metaSize()
263 if err != nil {
264 return 0, err
265 }
266 size += cs
267 }
268 if size > 2*1024*1024 {
269 return 0, errors.New("directory contains > 2MiB of metadata which is prohibited in FAT32")
270 }
271 return size, nil
272 } else {
273 return i.Content.Size(), nil
274 }
275}
276
277func (i *Inode) PlaceFile(path string, reader SizedReader) error {
278 pathParts := strings.Split(path, "/")
279 inodeRef := i
280 for j, part := range pathParts {
281 var childExists bool
282 for _, child := range inodeRef.Children {
Tim Windelschmidtc4ac94e2024-04-18 23:12:38 +0200283 if strings.EqualFold(child.Name, part) {
Lorenz Brunbd2ce6d2022-07-22 00:00:13 +0000284 inodeRef = child
285 childExists = true
286 break
287 }
288 }
289 if j == len(pathParts)-1 { // Is last path part (i.e. file name)
290 if childExists {
291 return &fs.PathError{Path: path, Err: fs.ErrExist, Op: "create"}
292 }
293 newInode := &Inode{
294 Name: part,
295 Content: reader,
296 }
297 inodeRef.Children = append(inodeRef.Children, newInode)
298 return nil
299 } else if !childExists {
300 newInode := &Inode{
301 Name: part,
302 Attrs: AttrDirectory,
303 }
304 inodeRef.Children = append(inodeRef.Children, newInode)
305 inodeRef = newInode
306 }
307 }
308 panic("unreachable")
309}
310
311type planningState struct {
312 // List of inodes in filesystem layout order
313 orderedInodes []*Inode
314 // File Allocation Table
315 fat []uint32
316 // Size of a single cluster in the FAT in bytes
317 clusterSize int64
318}
319
320// Allocates clusters capable of holding at least b bytes and returns the
321// starting cluster index
322func (p *planningState) allocBytes(b int64) int {
323 // Zero-byte data entries are located at the cluster zero by definition
324 // No actual allocation is performed
325 if b == 0 {
326 return 0
327 }
328 // Calculate the number of clusters to be allocated
329 n := (b + p.clusterSize - 1) / p.clusterSize
330 allocStartCluster := len(p.fat)
331 for i := int64(0); i < n-1; i++ {
332 p.fat = append(p.fat, uint32(len(p.fat)+1))
333 }
334 p.fat = append(p.fat, fatEOF)
335 return allocStartCluster
336}
337
338func (i *Inode) placeRecursively(p *planningState) error {
339 selfDataSize, err := i.dataSize()
340 if err != nil {
341 return fmt.Errorf("%s: %w", i.Name, err)
342 }
343 i.startCluster = p.allocBytes(selfDataSize)
344 p.orderedInodes = append(p.orderedInodes, i)
345 for _, c := range i.Children {
346 c.parent = i
347 err = c.placeRecursively(p)
348 if err != nil {
349 return fmt.Errorf("%s/%w", i.Name, err)
350 }
351 }
352 return nil
353}
354
355// WriteFS writes a filesystem described by a root inode and its children to a
356// given io.Writer.
357func WriteFS(w io.Writer, rootInode Inode, opts Options) error {
Tim Windelschmidtd7e34c42024-07-09 17:32:13 +0200358 bs, fsi, p, err := prepareFS(&opts, rootInode)
359 if err != nil {
360 return err
361 }
362
363 wb := newBlockWriter(w)
364
365 // Write superblock
366 if err := binary.Write(wb, binary.LittleEndian, bs); err != nil {
367 return err
368 }
369 if err := wb.FinishBlock(int64(opts.BlockSize), true); err != nil {
370 return err
371 }
372 if err := binary.Write(wb, binary.LittleEndian, fsi); err != nil {
373 return err
374 }
375 if err := wb.FinishBlock(int64(opts.BlockSize), true); err != nil {
376 return err
377 }
378
379 block := make([]byte, opts.BlockSize)
380 for i := 0; i < 4; i++ {
381 if _, err := wb.Write(block); err != nil {
382 return err
383 }
384 }
385 // Backup of superblock at block 6
386 if err := binary.Write(wb, binary.LittleEndian, bs); err != nil {
387 return err
388 }
389 if err := wb.FinishBlock(int64(opts.BlockSize), true); err != nil {
390 return err
391 }
392 if err := binary.Write(wb, binary.LittleEndian, fsi); err != nil {
393 return err
394 }
395 if err := wb.FinishBlock(int64(opts.BlockSize), true); err != nil {
396 return err
397 }
398
399 for i := uint8(0); i < bs.NumFATs; i++ {
400 if err := binary.Write(wb, binary.LittleEndian, p.fat); err != nil {
401 return err
402 }
403 if err := wb.FinishBlock(int64(opts.BlockSize), true); err != nil {
404 return err
405 }
406 }
407
408 for _, i := range p.orderedInodes {
409 if err := i.writeData(wb, bs.Label); err != nil {
Tim Windelschmidt5f1a7de2024-09-19 02:00:14 +0200410 return fmt.Errorf("failed to write inode %q: %w", i.Name, err)
Tim Windelschmidtd7e34c42024-07-09 17:32:13 +0200411 }
Jan Schärd589b6a2024-11-11 14:55:38 +0100412 if err := wb.FinishBlock(int64(opts.BlockSize)*int64(bs.BlocksPerCluster), true); err != nil {
Tim Windelschmidtd7e34c42024-07-09 17:32:13 +0200413 return err
414 }
415 }
416 // Creatively use block writer to write out all empty data at the end
417 if err := wb.FinishBlock(int64(opts.BlockSize)*int64(bs.TotalBlocks), false); err != nil {
418 return err
419 }
420 return nil
421}
422
423func prepareFS(opts *Options, rootInode Inode) (*bootSector, *fsinfo, *planningState, error) {
Lorenz Brunbd2ce6d2022-07-22 00:00:13 +0000424 if opts.BlockSize == 0 {
425 opts.BlockSize = 512
426 }
427 if bits.OnesCount16(opts.BlockSize) != 1 {
Tim Windelschmidtd7e34c42024-07-09 17:32:13 +0200428 return nil, nil, nil, fmt.Errorf("option BlockSize is not a power of two")
Lorenz Brunbd2ce6d2022-07-22 00:00:13 +0000429 }
430 if opts.BlockSize < 512 {
Tim Windelschmidtd7e34c42024-07-09 17:32:13 +0200431 return nil, nil, nil, fmt.Errorf("option BlockSize must be at least 512 bytes")
Lorenz Brunbd2ce6d2022-07-22 00:00:13 +0000432 }
433 if opts.ID == 0 {
434 var buf [4]byte
435 if _, err := rand.Read(buf[:]); err != nil {
Tim Windelschmidt5f1a7de2024-09-19 02:00:14 +0200436 return nil, nil, nil, fmt.Errorf("failed to assign random FAT ID: %w", err)
Lorenz Brunbd2ce6d2022-07-22 00:00:13 +0000437 }
438 opts.ID = binary.BigEndian.Uint32(buf[:])
439 }
440 if rootInode.Attrs&AttrDirectory == 0 {
Tim Windelschmidtd7e34c42024-07-09 17:32:13 +0200441 return nil, nil, nil, errors.New("root inode must be a directory (i.e. have AttrDirectory set)")
Lorenz Brunbd2ce6d2022-07-22 00:00:13 +0000442 }
Lorenz Brunbd2ce6d2022-07-22 00:00:13 +0000443 bs := bootSector{
444 // Assembled x86_32 machine code corresponding to
445 // jmp $
446 // nop
447 // i.e. an infinite loop doing nothing. Nothing created in the last 35
448 // years should boot this anyway.
449 // TODO(q3k): write a stub
450 JmpInstruction: [3]byte{0xEB, 0xFE, 0x90},
451 // Identification
452 OEMName: [8]byte{'M', 'O', 'N', 'O', 'G', 'O', 'N'},
453 ID: opts.ID,
454 // Block geometry
455 BlockSize: opts.BlockSize,
456 TotalBlocks: opts.BlockCount,
457 // BootSector block + FSInfo Block, backup copy at blocks 6 and 7
458 ReservedBlocks: 8,
459 // FSInfo block is always in block 1, right after this block
460 FSInfoBlock: 1,
461 // Start block of the backup of the boot block and FSInfo block
462 // De facto this must be 6 as it is only used when the primary
463 // boot block is damaged at which point this field can no longer be
464 // read.
465 BackupStartBlock: 6,
466 // A lot of implementations only work with 2, so use that
467 NumFATs: 2,
468 BlocksPerCluster: 1,
469 // Flags and signatures
470 MediaCode: 0xf8,
471 BootSignature: 0x29,
472 Label: [11]byte{' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '},
473 Type: [8]byte{'F', 'A', 'T', '3', '2', ' ', ' ', ' '},
474 Signature: [2]byte{0x55, 0xaa},
475 }
476
477 copy(bs.Label[:], opts.Label)
478
Tim Windelschmidtd7e34c42024-07-09 17:32:13 +0200479 fsi := fsinfo{
Lorenz Brunbd2ce6d2022-07-22 00:00:13 +0000480 // Signatures
481 LeadSignature: [4]byte{0x52, 0x52, 0x61, 0x41},
482 StructSignature: [4]byte{0x72, 0x72, 0x41, 0x61},
483 TrailingSignature: [2]byte{0x55, 0xAA},
484
485 // This is the unset value which is always legal
486 NextFreeCluster: 0xFFFFFFFF,
487 }
488
489 p := planningState{
490 clusterSize: int64(bs.BlocksPerCluster) * int64(bs.BlockSize),
491 }
492 if opts.BlockCount != 0 {
493 // Preallocate FAT if we know how big it needs to be
494 p.fat = make([]uint32, 0, opts.BlockCount/uint32(bs.BlocksPerCluster))
495 } else {
496 // Preallocate minimum size FAT
497 // See the spec page 15 for the origin of this calculation.
498 p.fat = make([]uint32, 0, 65525+2)
499 }
500 // First two clusters are special
501 p.fat = append(p.fat, 0x0fffff00|uint32(bs.MediaCode), 0x0fffffff)
502 err := rootInode.placeRecursively(&p)
503 if err != nil {
Tim Windelschmidtd7e34c42024-07-09 17:32:13 +0200504 return nil, nil, nil, err
Lorenz Brunbd2ce6d2022-07-22 00:00:13 +0000505 }
506
507 allocClusters := len(p.fat)
508 if allocClusters >= fatMask&math.MaxUint32 {
Tim Windelschmidtd7e34c42024-07-09 17:32:13 +0200509 return nil, nil, nil, fmt.Errorf("filesystem contains more than 2^28 FAT entries, this is unsupported. Note that this package currently always creates minimal clusters")
Lorenz Brunbd2ce6d2022-07-22 00:00:13 +0000510 }
511
512 // Fill out FAT to minimum size for FAT32
513 for len(p.fat) < 65525+2 {
514 p.fat = append(p.fat, fatFree)
515 }
516
517 bs.RootClusterNumber = uint32(rootInode.startCluster)
518
519 bs.BlocksPerFAT = uint32(binary.Size(p.fat)+int(opts.BlockSize)-1) / uint32(opts.BlockSize)
520 occupiedBlocks := uint32(bs.ReservedBlocks) + (uint32(len(p.fat)-2) * uint32(bs.BlocksPerCluster)) + bs.BlocksPerFAT*uint32(bs.NumFATs)
521 if bs.TotalBlocks == 0 {
522 bs.TotalBlocks = occupiedBlocks
523 } else if bs.TotalBlocks < occupiedBlocks {
Tim Windelschmidtd7e34c42024-07-09 17:32:13 +0200524 return nil, nil, nil, fmt.Errorf("content (minimum %d blocks) would exceed number of blocks specified (%d blocks)", occupiedBlocks, bs.TotalBlocks)
Lorenz Brunbd2ce6d2022-07-22 00:00:13 +0000525 } else { // Fixed-size file system with enough space
526 blocksToDistribute := bs.TotalBlocks - uint32(bs.ReservedBlocks)
527 // Number of data blocks which can be described by one metadata/FAT
528 // block. Always an integer because 4 (bytes per uint32) is a divisor of
529 // all powers of two equal or bigger than 8 and FAT32 requires a minimum
530 // of 512.
531 dataBlocksPerFATBlock := (uint32(bs.BlocksPerCluster) * uint32(bs.BlockSize)) / (uint32(binary.Size(p.fat[0])))
532 // Split blocksToDistribute between metadata and data so that exactly as
533 // much metadata (FAT) exists for describing the amount of data blocks
534 // while respecting alignment.
535 divisor := dataBlocksPerFATBlock + uint32(bs.NumFATs)
536 // 2*blocksPerCluster compensates for the first two "magic" FAT entries
537 // which do not have corresponding data.
538 bs.BlocksPerFAT = (bs.TotalBlocks + 2*uint32(bs.BlocksPerCluster) + (divisor - 1)) / divisor
539 dataBlocks := blocksToDistribute - (uint32(bs.NumFATs) * bs.BlocksPerFAT)
540 // Align to full clusters
541 dataBlocks -= dataBlocks % uint32(bs.BlocksPerCluster)
542 // Magic +2 as the first two entries do not describe data
543 for len(p.fat) < (int(dataBlocks)/int(bs.BlocksPerCluster))+2 {
544 p.fat = append(p.fat, fatFree)
545 }
546 }
Tim Windelschmidtd7e34c42024-07-09 17:32:13 +0200547 fsi.FreeCount = uint32(len(p.fat) - allocClusters)
548 if fsi.FreeCount > 1 {
549 fsi.NextFreeCluster = uint32(allocClusters) + 1
550 }
551 return &bs, &fsi, &p, nil
552}
553
554// SizeFS returns the number of blocks required to hold the filesystem defined
555// by rootInode and opts. This can be used for sizing calculations before
556// calling WriteFS.
557func SizeFS(rootInode Inode, opts Options) (int64, error) {
558 bs, _, _, err := prepareFS(&opts, rootInode)
559 if err != nil {
560 return 0, err
Lorenz Brunbd2ce6d2022-07-22 00:00:13 +0000561 }
562
Tim Windelschmidtd7e34c42024-07-09 17:32:13 +0200563 return int64(bs.TotalBlocks), nil
Lorenz Brunbd2ce6d2022-07-22 00:00:13 +0000564}