blob: 796db98b6eaf7af141ac473a8956d362c19783cb [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"
Lorenz Brunbd2ce6d2022-07-22 00:00:13 +000016 "time"
17 "unicode/utf16"
Jan Schärc1b6df42025-03-20 08:52:18 +000018
19 "source.monogon.dev/osbase/structfs"
Lorenz Brunbd2ce6d2022-07-22 00:00:13 +000020)
21
22// This package contains multiple references to the FAT32 specification, called
23// Microsoft Extensible Firmware Initiative FAT32 File System Specification
24// version 1.03 (just called the spec from now on). You can get it at
25// https://download.microsoft.com/download/0/8/4/\
26// 084c452b-b772-4fe5-89bb-a0cbf082286a/fatgen103.doc
27
28type Options struct {
29 // Size of a logical block on the block device. Needs to be a power of two
30 // equal or bigger than 512. If left at zero, defaults to 512.
31 BlockSize uint16
32
33 // Number of blocks the filesystem should span. If zero, it will be exactly
34 // as large as it needs to be.
35 BlockCount uint32
36
Jan Schärc1b6df42025-03-20 08:52:18 +000037 // Human-readable filesystem label. Maximum 11 bytes (gets cut off), should
Lorenz Brunbd2ce6d2022-07-22 00:00:13 +000038 // be uppercase alphanumeric.
39 Label string
40
41 // Filesystem identifier. If unset (i.e. left at zero) a random value will
42 // be assigned by WriteFS.
43 ID uint32
44}
45
Jan Schärc1b6df42025-03-20 08:52:18 +000046// Attribute is a bitset of flags set on a directory entry.
Lorenz Brunbd2ce6d2022-07-22 00:00:13 +000047// See also the spec page 24
48type Attribute uint8
49
50const (
51 // AttrReadOnly marks a file as read-only
52 AttrReadOnly Attribute = 0x01
53 // AttrHidden indicates that directory listings should not show this file.
54 AttrHidden Attribute = 0x02
55 // AttrSystem indicates that this is an operating system file.
56 AttrSystem Attribute = 0x04
Jan Schärc1b6df42025-03-20 08:52:18 +000057 // attrVolumeID indicates that this is a special directory entry which
58 // contains the volume label.
59 attrVolumeID Attribute = 0x08
60 // attrDirectory indicates that this is a directory and not a file.
61 attrDirectory Attribute = 0x10
Lorenz Brunbd2ce6d2022-07-22 00:00:13 +000062 // AttrArchive canonically indicates that a file has been created/modified
63 // since the last backup. Its use in practice is inconsistent.
64 AttrArchive Attribute = 0x20
65)
66
Jan Schärc1b6df42025-03-20 08:52:18 +000067// DirEntrySys contains additional directory entry fields which are specific to
68// FAT32. To set these fields, the Sys field of a [structfs.Node] can be set to
69// a pointer to this or to a struct which embeds it.
70type DirEntrySys struct {
Lorenz Brunbd2ce6d2022-07-22 00:00:13 +000071 // Time the file or directory was created
72 CreateTime time.Time
73 // Attributes
74 Attrs Attribute
Jan Schärc1b6df42025-03-20 08:52:18 +000075}
Lorenz Brunbd2ce6d2022-07-22 00:00:13 +000076
Jan Schärc1b6df42025-03-20 08:52:18 +000077func (d *DirEntrySys) FAT32() *DirEntrySys {
78 return d
79}
80
81// DirEntrySysAccessor is used to access [DirEntrySys] instead of directly type
82// asserting the struct, to allow for embedding.
83type DirEntrySysAccessor interface {
84 FAT32() *DirEntrySys
85}
86
87// node is a file or directory on the FAT32 filesystem. It wraps a
88// [structfs.Node] and holds additional fields which are filled during planning.
89type node struct {
90 *structfs.Node
Lorenz Brunbd2ce6d2022-07-22 00:00:13 +000091 dosName [11]byte
Jan Schärc1b6df42025-03-20 08:52:18 +000092 createTime time.Time
93 attrs Attribute
94 parent *node
95 children []*node
96 size uint32
97 startCluster int
Lorenz Brunbd2ce6d2022-07-22 00:00:13 +000098}
99
100// Number of LFN entries + normal entry (all 32 bytes)
Jan Schärc1b6df42025-03-20 08:52:18 +0000101func (i node) metaSize() (int64, error) {
Lorenz Brunbd2ce6d2022-07-22 00:00:13 +0000102 fileNameUTF16 := utf16.Encode([]rune(i.Name))
103 // VFAT file names are null-terminated
104 fileNameUTF16 = append(fileNameUTF16, 0x00)
105 if len(fileNameUTF16) > 255 {
106 return 0, errors.New("file name too long, maximum is 255 UTF-16 code points")
107 }
108
109 // ⌈len(fileNameUTF16)/codepointsPerEntry⌉
110 numEntries := (len(fileNameUTF16) + codepointsPerEntry - 1) / codepointsPerEntry
111 return (int64(numEntries) + 1) * 32, nil
112}
113
114func lfnChecksum(dosName [11]byte) uint8 {
115 var sum uint8
116 for _, b := range dosName {
117 sum = ((sum & 1) << 7) + (sum >> 1) + b
118 }
119 return sum
120}
121
Jan Schärc1b6df42025-03-20 08:52:18 +0000122// writeMeta writes information about this node into the contents of the parent
123// node.
124func (i node) writeMeta(w io.Writer) error {
Lorenz Brunbd2ce6d2022-07-22 00:00:13 +0000125 fileNameUTF16 := utf16.Encode([]rune(i.Name))
126 // VFAT file names are null-terminated
127 fileNameUTF16 = append(fileNameUTF16, 0x00)
128 if len(fileNameUTF16) > 255 {
129 return errors.New("file name too long, maximum is 255 UTF-16 code points")
130 }
131
132 // ⌈len(fileNameUTF16)/codepointsPerEntry⌉
133 numEntries := (len(fileNameUTF16) + codepointsPerEntry - 1) / codepointsPerEntry
134 // Fill up to space in given number of entries with fill code point 0xffff
135 fillCodePoints := (numEntries * codepointsPerEntry) - len(fileNameUTF16)
136 for j := 0; j < fillCodePoints; j++ {
137 fileNameUTF16 = append(fileNameUTF16, 0xffff)
138 }
139
140 // Write entries in reverse order
141 for j := numEntries; j > 0; j-- {
142 // Index of the code point being processed
143 cpIdx := (j - 1) * codepointsPerEntry
144 var entry lfnEntry
145 entry.Checksum = lfnChecksum(i.dosName)
146 // Downcast is safe as i <= numEntries <= ⌈255/codepointsPerEntry⌉
147 entry.SequenceNumber = uint8(j)
148 if j == numEntries {
149 entry.SequenceNumber |= lastSequenceNumberFlag
150 }
151 entry.Attributes = 0x0F
152 copy(entry.NamePart1[:], fileNameUTF16[cpIdx:])
153 cpIdx += len(entry.NamePart1)
154 copy(entry.NamePart2[:], fileNameUTF16[cpIdx:])
155 cpIdx += len(entry.NamePart2)
156 copy(entry.NamePart3[:], fileNameUTF16[cpIdx:])
157 cpIdx += len(entry.NamePart3)
158
159 if err := binary.Write(w, binary.LittleEndian, entry); err != nil {
160 return err
161 }
162 }
Jan Schärc1b6df42025-03-20 08:52:18 +0000163 selfSize := i.size
164 if i.attrs&attrDirectory != 0 {
Lorenz Brunbd2ce6d2022-07-22 00:00:13 +0000165 selfSize = 0 // Directories don't have an explicit size
166 }
167 date, t, _ := timeToMsDosTime(i.ModTime)
Jan Schärc1b6df42025-03-20 08:52:18 +0000168 cdate, ctime, ctens := timeToMsDosTime(i.createTime)
Lorenz Brunbd2ce6d2022-07-22 00:00:13 +0000169 if err := binary.Write(w, binary.LittleEndian, &dirEntry{
170 DOSName: i.dosName,
Jan Schärc1b6df42025-03-20 08:52:18 +0000171 Attributes: uint8(i.attrs),
Jan Schär79595192024-11-11 14:55:56 +0100172 CreationTenMilli: ctens,
173 CreationTime: ctime,
174 CreationDate: cdate,
Lorenz Brunbd2ce6d2022-07-22 00:00:13 +0000175 FirstClusterHigh: uint16(i.startCluster >> 16),
176 LastWrittenToTime: t,
177 LastWrittenToDate: date,
178 FirstClusterLow: uint16(i.startCluster & 0xffff),
Jan Schärc1b6df42025-03-20 08:52:18 +0000179 FileSize: selfSize,
Lorenz Brunbd2ce6d2022-07-22 00:00:13 +0000180 }); err != nil {
181 return err
182 }
183 return nil
184}
185
Jan Schärc1b6df42025-03-20 08:52:18 +0000186// writeData writes the contents of this node (including possible metadata
Lorenz Brunbd2ce6d2022-07-22 00:00:13 +0000187// of its children, but not its children's data)
Jan Schärc1b6df42025-03-20 08:52:18 +0000188func (i node) writeData(w io.Writer, volumeLabel [11]byte) error {
189 if i.attrs&attrDirectory != 0 {
Lorenz Brunbd2ce6d2022-07-22 00:00:13 +0000190 if i.parent == nil {
191 if err := binary.Write(w, binary.LittleEndian, &dirEntry{
192 DOSName: volumeLabel,
Jan Schärc1b6df42025-03-20 08:52:18 +0000193 Attributes: uint8(attrVolumeID),
Lorenz Brunbd2ce6d2022-07-22 00:00:13 +0000194 }); err != nil {
195 return err
196 }
197 } else {
198 date, t, _ := timeToMsDosTime(i.ModTime)
Jan Schärc1b6df42025-03-20 08:52:18 +0000199 cdate, ctime, ctens := timeToMsDosTime(i.createTime)
Lorenz Brunbd2ce6d2022-07-22 00:00:13 +0000200 if err := binary.Write(w, binary.LittleEndian, &dirEntry{
201 DOSName: [11]byte{'.', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '},
202 CreationDate: cdate,
203 CreationTime: ctime,
204 CreationTenMilli: ctens,
205 LastWrittenToTime: t,
206 LastWrittenToDate: date,
Jan Schärc1b6df42025-03-20 08:52:18 +0000207 Attributes: uint8(i.attrs),
Lorenz Brunbd2ce6d2022-07-22 00:00:13 +0000208 FirstClusterHigh: uint16(i.startCluster >> 16),
209 FirstClusterLow: uint16(i.startCluster & 0xffff),
210 }); err != nil {
211 return err
212 }
213 startCluster := i.parent.startCluster
214 if i.parent.parent == nil {
215 // Special case: When the dotdot directory points to the root
216 // directory, the start cluster is defined to be zero even if
217 // it isn't.
218 startCluster = 0
219 }
220 // Time is intentionally taken from this directory, not the parent
221 if err := binary.Write(w, binary.LittleEndian, &dirEntry{
222 DOSName: [11]byte{'.', '.', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '},
Jan Schär79595192024-11-11 14:55:56 +0100223 CreationDate: cdate,
224 CreationTime: ctime,
225 CreationTenMilli: ctens,
Lorenz Brunbd2ce6d2022-07-22 00:00:13 +0000226 LastWrittenToTime: t,
227 LastWrittenToDate: date,
Jan Schärc1b6df42025-03-20 08:52:18 +0000228 Attributes: uint8(attrDirectory),
Lorenz Brunbd2ce6d2022-07-22 00:00:13 +0000229 FirstClusterHigh: uint16(startCluster >> 16),
230 FirstClusterLow: uint16(startCluster & 0xffff),
231 }); err != nil {
232 return err
233 }
234 }
Jan Schärc1b6df42025-03-20 08:52:18 +0000235 for _, c := range i.children {
Lorenz Brunbd2ce6d2022-07-22 00:00:13 +0000236 if err := c.writeMeta(w); err != nil {
237 return err
238 }
239 }
240 } else {
Jan Schärc1b6df42025-03-20 08:52:18 +0000241 content, err := i.Content.Open()
242 if err != nil {
243 return err
244 }
245 defer content.Close()
246 if _, err := io.CopyN(w, content, int64(i.size)); err != nil {
Lorenz Brunbd2ce6d2022-07-22 00:00:13 +0000247 return err
248 }
249 }
250 return nil
251}
252
Jan Schärc1b6df42025-03-20 08:52:18 +0000253func (i node) dirSize() (uint32, error) {
254 var size int64
255 if i.parent != nil {
256 // Dot and dotdot directories
257 size += 2 * 32
Lorenz Brunbd2ce6d2022-07-22 00:00:13 +0000258 } else {
Jan Schärc1b6df42025-03-20 08:52:18 +0000259 // Volume ID
260 size += 1 * 32
Lorenz Brunbd2ce6d2022-07-22 00:00:13 +0000261 }
Jan Schärc1b6df42025-03-20 08:52:18 +0000262 for _, c := range i.children {
263 cs, err := c.metaSize()
264 if err != nil {
265 return 0, err
Lorenz Brunbd2ce6d2022-07-22 00:00:13 +0000266 }
Jan Schärc1b6df42025-03-20 08:52:18 +0000267 size += cs
Lorenz Brunbd2ce6d2022-07-22 00:00:13 +0000268 }
Jan Schärc1b6df42025-03-20 08:52:18 +0000269 if size > 2*1024*1024 {
270 return 0, errors.New("directory contains > 2MiB of metadata which is prohibited in FAT32")
271 }
272 return uint32(size), nil
Lorenz Brunbd2ce6d2022-07-22 00:00:13 +0000273}
274
275type planningState struct {
Jan Schärc1b6df42025-03-20 08:52:18 +0000276 // List of nodes in filesystem layout order
277 orderedNodes []*node
Lorenz Brunbd2ce6d2022-07-22 00:00:13 +0000278 // File Allocation Table
279 fat []uint32
280 // Size of a single cluster in the FAT in bytes
281 clusterSize int64
282}
283
284// Allocates clusters capable of holding at least b bytes and returns the
285// starting cluster index
286func (p *planningState) allocBytes(b int64) int {
287 // Zero-byte data entries are located at the cluster zero by definition
288 // No actual allocation is performed
289 if b == 0 {
290 return 0
291 }
292 // Calculate the number of clusters to be allocated
293 n := (b + p.clusterSize - 1) / p.clusterSize
294 allocStartCluster := len(p.fat)
295 for i := int64(0); i < n-1; i++ {
296 p.fat = append(p.fat, uint32(len(p.fat)+1))
297 }
298 p.fat = append(p.fat, fatEOF)
299 return allocStartCluster
300}
301
Jan Schärc1b6df42025-03-20 08:52:18 +0000302func (i *node) placeRecursively(p *planningState) error {
303 if i.Mode.IsDir() {
304 for _, c := range i.Node.Children {
305 node := &node{
306 Node: c,
307 createTime: c.ModTime,
308 parent: i,
309 }
310 if sys, ok := c.Sys.(DirEntrySysAccessor); ok {
311 sys := sys.FAT32()
312 node.attrs = sys.Attrs & (AttrReadOnly | AttrHidden | AttrSystem | AttrArchive)
313 if !sys.CreateTime.IsZero() {
314 node.createTime = sys.CreateTime
315 }
316 }
317 switch {
318 case c.Mode.IsRegular():
319 size := c.Content.Size()
320 if size < 0 {
321 return fmt.Errorf("%s: negative file size", c.Name)
322 }
323 if size >= 4*1024*1024*1024 {
324 return fmt.Errorf("%s: single file size exceeds 4GiB which is prohibited in FAT32", c.Name)
325 }
326 node.size = uint32(size)
327 if len(c.Children) != 0 {
328 return fmt.Errorf("%s: file cannot have children", c.Name)
329 }
330 case c.Mode.IsDir():
331 node.attrs |= attrDirectory
332 default:
333 return fmt.Errorf("%s: unsupported file type %s", c.Name, c.Mode.Type().String())
334 }
335 i.children = append(i.children, node)
336 }
337 err := makeUniqueDOSNames(i.children)
338 if err != nil {
339 return err
340 }
341 i.size, err = i.dirSize()
342 if err != nil {
343 return fmt.Errorf("%s: %w", i.Name, err)
344 }
Lorenz Brunbd2ce6d2022-07-22 00:00:13 +0000345 }
Jan Schärc1b6df42025-03-20 08:52:18 +0000346 i.startCluster = p.allocBytes(int64(i.size))
347 p.orderedNodes = append(p.orderedNodes, i)
348 for _, c := range i.children {
349 err := c.placeRecursively(p)
Lorenz Brunbd2ce6d2022-07-22 00:00:13 +0000350 if err != nil {
351 return fmt.Errorf("%s/%w", i.Name, err)
352 }
353 }
354 return nil
355}
356
Jan Schärc1b6df42025-03-20 08:52:18 +0000357// WriteFS writes a filesystem described by a tree to a given io.Writer.
358func WriteFS(w io.Writer, root structfs.Tree, opts Options) error {
359 bs, fsi, p, err := prepareFS(&opts, root)
Tim Windelschmidtd7e34c42024-07-09 17:32:13 +0200360 if err != nil {
361 return err
362 }
363
364 wb := newBlockWriter(w)
365
366 // Write superblock
367 if err := binary.Write(wb, binary.LittleEndian, bs); err != nil {
368 return err
369 }
370 if err := wb.FinishBlock(int64(opts.BlockSize), true); err != nil {
371 return err
372 }
373 if err := binary.Write(wb, binary.LittleEndian, fsi); err != nil {
374 return err
375 }
376 if err := wb.FinishBlock(int64(opts.BlockSize), true); err != nil {
377 return err
378 }
379
380 block := make([]byte, opts.BlockSize)
381 for i := 0; i < 4; i++ {
382 if _, err := wb.Write(block); err != nil {
383 return err
384 }
385 }
386 // Backup of superblock at block 6
387 if err := binary.Write(wb, binary.LittleEndian, bs); err != nil {
388 return err
389 }
390 if err := wb.FinishBlock(int64(opts.BlockSize), true); err != nil {
391 return err
392 }
393 if err := binary.Write(wb, binary.LittleEndian, fsi); err != nil {
394 return err
395 }
396 if err := wb.FinishBlock(int64(opts.BlockSize), true); err != nil {
397 return err
398 }
399
400 for i := uint8(0); i < bs.NumFATs; i++ {
401 if err := binary.Write(wb, binary.LittleEndian, p.fat); err != nil {
402 return err
403 }
404 if err := wb.FinishBlock(int64(opts.BlockSize), true); err != nil {
405 return err
406 }
407 }
408
Jan Schärc1b6df42025-03-20 08:52:18 +0000409 for _, i := range p.orderedNodes {
Tim Windelschmidtd7e34c42024-07-09 17:32:13 +0200410 if err := i.writeData(wb, bs.Label); err != nil {
Jan Schärc1b6df42025-03-20 08:52:18 +0000411 return fmt.Errorf("failed to write contents of %q: %w", i.Name, err)
Tim Windelschmidtd7e34c42024-07-09 17:32:13 +0200412 }
Jan Schärd589b6a2024-11-11 14:55:38 +0100413 if err := wb.FinishBlock(int64(opts.BlockSize)*int64(bs.BlocksPerCluster), true); err != nil {
Tim Windelschmidtd7e34c42024-07-09 17:32:13 +0200414 return err
415 }
416 }
417 // Creatively use block writer to write out all empty data at the end
418 if err := wb.FinishBlock(int64(opts.BlockSize)*int64(bs.TotalBlocks), false); err != nil {
419 return err
420 }
421 return nil
422}
423
Jan Schärc1b6df42025-03-20 08:52:18 +0000424func prepareFS(opts *Options, root structfs.Tree) (*bootSector, *fsinfo, *planningState, error) {
Lorenz Brunbd2ce6d2022-07-22 00:00:13 +0000425 if opts.BlockSize == 0 {
426 opts.BlockSize = 512
427 }
428 if bits.OnesCount16(opts.BlockSize) != 1 {
Tim Windelschmidtd7e34c42024-07-09 17:32:13 +0200429 return nil, nil, nil, fmt.Errorf("option BlockSize is not a power of two")
Lorenz Brunbd2ce6d2022-07-22 00:00:13 +0000430 }
431 if opts.BlockSize < 512 {
Tim Windelschmidtd7e34c42024-07-09 17:32:13 +0200432 return nil, nil, nil, fmt.Errorf("option BlockSize must be at least 512 bytes")
Lorenz Brunbd2ce6d2022-07-22 00:00:13 +0000433 }
434 if opts.ID == 0 {
435 var buf [4]byte
436 if _, err := rand.Read(buf[:]); err != nil {
Tim Windelschmidt5f1a7de2024-09-19 02:00:14 +0200437 return nil, nil, nil, fmt.Errorf("failed to assign random FAT ID: %w", err)
Lorenz Brunbd2ce6d2022-07-22 00:00:13 +0000438 }
439 opts.ID = binary.BigEndian.Uint32(buf[:])
440 }
Lorenz Brunbd2ce6d2022-07-22 00:00:13 +0000441 bs := bootSector{
442 // Assembled x86_32 machine code corresponding to
443 // jmp $
444 // nop
445 // i.e. an infinite loop doing nothing. Nothing created in the last 35
446 // years should boot this anyway.
Lorenz Brunbd2ce6d2022-07-22 00:00:13 +0000447 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)
Jan Schärc1b6df42025-03-20 08:52:18 +0000499 rootNode := &node{
500 Node: &structfs.Node{
501 Mode: fs.ModeDir,
502 Children: root,
503 },
504 attrs: attrDirectory,
505 }
506 err := rootNode.placeRecursively(&p)
Lorenz Brunbd2ce6d2022-07-22 00:00:13 +0000507 if err != nil {
Tim Windelschmidtd7e34c42024-07-09 17:32:13 +0200508 return nil, nil, nil, err
Lorenz Brunbd2ce6d2022-07-22 00:00:13 +0000509 }
510
511 allocClusters := len(p.fat)
512 if allocClusters >= fatMask&math.MaxUint32 {
Tim Windelschmidtd7e34c42024-07-09 17:32:13 +0200513 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 +0000514 }
515
516 // Fill out FAT to minimum size for FAT32
517 for len(p.fat) < 65525+2 {
518 p.fat = append(p.fat, fatFree)
519 }
520
Jan Schärc1b6df42025-03-20 08:52:18 +0000521 bs.RootClusterNumber = uint32(rootNode.startCluster)
Lorenz Brunbd2ce6d2022-07-22 00:00:13 +0000522
523 bs.BlocksPerFAT = uint32(binary.Size(p.fat)+int(opts.BlockSize)-1) / uint32(opts.BlockSize)
524 occupiedBlocks := uint32(bs.ReservedBlocks) + (uint32(len(p.fat)-2) * uint32(bs.BlocksPerCluster)) + bs.BlocksPerFAT*uint32(bs.NumFATs)
525 if bs.TotalBlocks == 0 {
526 bs.TotalBlocks = occupiedBlocks
527 } else if bs.TotalBlocks < occupiedBlocks {
Tim Windelschmidtd7e34c42024-07-09 17:32:13 +0200528 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 +0000529 } else { // Fixed-size file system with enough space
530 blocksToDistribute := bs.TotalBlocks - uint32(bs.ReservedBlocks)
531 // Number of data blocks which can be described by one metadata/FAT
532 // block. Always an integer because 4 (bytes per uint32) is a divisor of
533 // all powers of two equal or bigger than 8 and FAT32 requires a minimum
534 // of 512.
535 dataBlocksPerFATBlock := (uint32(bs.BlocksPerCluster) * uint32(bs.BlockSize)) / (uint32(binary.Size(p.fat[0])))
536 // Split blocksToDistribute between metadata and data so that exactly as
537 // much metadata (FAT) exists for describing the amount of data blocks
538 // while respecting alignment.
539 divisor := dataBlocksPerFATBlock + uint32(bs.NumFATs)
540 // 2*blocksPerCluster compensates for the first two "magic" FAT entries
541 // which do not have corresponding data.
542 bs.BlocksPerFAT = (bs.TotalBlocks + 2*uint32(bs.BlocksPerCluster) + (divisor - 1)) / divisor
543 dataBlocks := blocksToDistribute - (uint32(bs.NumFATs) * bs.BlocksPerFAT)
544 // Align to full clusters
545 dataBlocks -= dataBlocks % uint32(bs.BlocksPerCluster)
546 // Magic +2 as the first two entries do not describe data
547 for len(p.fat) < (int(dataBlocks)/int(bs.BlocksPerCluster))+2 {
548 p.fat = append(p.fat, fatFree)
549 }
550 }
Tim Windelschmidtd7e34c42024-07-09 17:32:13 +0200551 fsi.FreeCount = uint32(len(p.fat) - allocClusters)
552 if fsi.FreeCount > 1 {
553 fsi.NextFreeCluster = uint32(allocClusters) + 1
554 }
555 return &bs, &fsi, &p, nil
556}
557
558// SizeFS returns the number of blocks required to hold the filesystem defined
Jan Schärc1b6df42025-03-20 08:52:18 +0000559// by root and opts. This can be used for sizing calculations before calling
560// WriteFS.
561func SizeFS(root structfs.Tree, opts Options) (int64, error) {
562 bs, _, _, err := prepareFS(&opts, root)
Tim Windelschmidtd7e34c42024-07-09 17:32:13 +0200563 if err != nil {
564 return 0, err
Lorenz Brunbd2ce6d2022-07-22 00:00:13 +0000565 }
566
Tim Windelschmidtd7e34c42024-07-09 17:32:13 +0200567 return int64(bs.TotalBlocks), nil
Lorenz Brunbd2ce6d2022-07-22 00:00:13 +0000568}