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