| // Package fat32 implements a writer for the FAT32 filesystem. | 
 | package fat32 | 
 |  | 
 | import ( | 
 | 	"crypto/rand" | 
 | 	"encoding/binary" | 
 | 	"errors" | 
 | 	"fmt" | 
 | 	"io" | 
 | 	"io/fs" | 
 | 	"math" | 
 | 	"math/bits" | 
 | 	"strings" | 
 | 	"time" | 
 | 	"unicode/utf16" | 
 | ) | 
 |  | 
 | // This package contains multiple references to the FAT32 specification, called | 
 | // Microsoft Extensible Firmware Initiative FAT32 File System Specification | 
 | // version 1.03 (just called the spec from now on). You can get it at | 
 | // https://download.microsoft.com/download/0/8/4/\ | 
 | // 084c452b-b772-4fe5-89bb-a0cbf082286a/fatgen103.doc | 
 |  | 
 | type Options struct { | 
 | 	// Size of a logical block on the block device. Needs to be a power of two | 
 | 	// equal or bigger than 512. If left at zero, defaults to 512. | 
 | 	BlockSize uint16 | 
 |  | 
 | 	// Number of blocks the filesystem should span. If zero, it will be exactly | 
 | 	// as large as it needs to be. | 
 | 	BlockCount uint32 | 
 |  | 
 | 	// Human-readable filesystem label. Maximum 10 bytes (gets cut off), should | 
 | 	// be uppercase alphanumeric. | 
 | 	Label string | 
 |  | 
 | 	// Filesystem identifier. If unset (i.e. left at zero) a random value will | 
 | 	// be assigned by WriteFS. | 
 | 	ID uint32 | 
 | } | 
 |  | 
 | // SizedReader is an io.Reader with a known size | 
 | type SizedReader interface { | 
 | 	io.Reader | 
 | 	Size() int64 | 
 | } | 
 |  | 
 | // Attribute is a bitset of flags set on an inode. | 
 | // See also the spec page 24 | 
 | type Attribute uint8 | 
 |  | 
 | const ( | 
 | 	// AttrReadOnly marks a file as read-only | 
 | 	AttrReadOnly Attribute = 0x01 | 
 | 	// AttrHidden indicates that directory listings should not show this file. | 
 | 	AttrHidden Attribute = 0x02 | 
 | 	// AttrSystem indicates that this is an operating system file. | 
 | 	AttrSystem Attribute = 0x04 | 
 | 	// AttrDirectory indicates that this is a directory and not a file. | 
 | 	AttrDirectory Attribute = 0x10 | 
 | 	// AttrArchive canonically indicates that a file has been created/modified | 
 | 	// since the last backup. Its use in practice is inconsistent. | 
 | 	AttrArchive Attribute = 0x20 | 
 | ) | 
 |  | 
 | // Inode is file or directory on the FAT32 filesystem. Note that the concept | 
 | // of an inode doesn't really exist on FAT32, its directories are just special | 
 | // files. | 
 | type Inode struct { | 
 | 	// Name of the file or directory (not including its path) | 
 | 	Name string | 
 | 	// Time the file or directory was last modified | 
 | 	ModTime time.Time | 
 | 	// Time the file or directory was created | 
 | 	CreateTime time.Time | 
 | 	// Attributes | 
 | 	Attrs Attribute | 
 | 	// Children of this directory (only valid when Attrs has AttrDirectory set) | 
 | 	Children []*Inode | 
 | 	// Content of this file | 
 | 	// Only valid when Attrs doesn't have AttrDirectory set. | 
 | 	Content SizedReader | 
 |  | 
 | 	// Filled out on placement and write-out | 
 | 	startCluster int | 
 | 	parent       *Inode | 
 | 	dosName      [11]byte | 
 | } | 
 |  | 
 | // Number of LFN entries + normal entry (all 32 bytes) | 
 | func (i Inode) metaSize() (int64, error) { | 
 | 	fileNameUTF16 := utf16.Encode([]rune(i.Name)) | 
 | 	// VFAT file names are null-terminated | 
 | 	fileNameUTF16 = append(fileNameUTF16, 0x00) | 
 | 	if len(fileNameUTF16) > 255 { | 
 | 		return 0, errors.New("file name too long, maximum is 255 UTF-16 code points") | 
 | 	} | 
 |  | 
 | 	// ⌈len(fileNameUTF16)/codepointsPerEntry⌉ | 
 | 	numEntries := (len(fileNameUTF16) + codepointsPerEntry - 1) / codepointsPerEntry | 
 | 	return (int64(numEntries) + 1) * 32, nil | 
 | } | 
 |  | 
 | func lfnChecksum(dosName [11]byte) uint8 { | 
 | 	var sum uint8 | 
 | 	for _, b := range dosName { | 
 | 		sum = ((sum & 1) << 7) + (sum >> 1) + b | 
 | 	} | 
 | 	return sum | 
 | } | 
 |  | 
 | // writeMeta writes information about this inode into the contents of the parent | 
 | // inode. | 
 | func (i Inode) writeMeta(w io.Writer) error { | 
 | 	fileNameUTF16 := utf16.Encode([]rune(i.Name)) | 
 | 	// VFAT file names are null-terminated | 
 | 	fileNameUTF16 = append(fileNameUTF16, 0x00) | 
 | 	if len(fileNameUTF16) > 255 { | 
 | 		return errors.New("file name too long, maximum is 255 UTF-16 code points") | 
 | 	} | 
 |  | 
 | 	// ⌈len(fileNameUTF16)/codepointsPerEntry⌉ | 
 | 	numEntries := (len(fileNameUTF16) + codepointsPerEntry - 1) / codepointsPerEntry | 
 | 	// Fill up to space in given number of entries with fill code point 0xffff | 
 | 	fillCodePoints := (numEntries * codepointsPerEntry) - len(fileNameUTF16) | 
 | 	for j := 0; j < fillCodePoints; j++ { | 
 | 		fileNameUTF16 = append(fileNameUTF16, 0xffff) | 
 | 	} | 
 |  | 
 | 	// Write entries in reverse order | 
 | 	for j := numEntries; j > 0; j-- { | 
 | 		// Index of the code point being processed | 
 | 		cpIdx := (j - 1) * codepointsPerEntry | 
 | 		var entry lfnEntry | 
 | 		entry.Checksum = lfnChecksum(i.dosName) | 
 | 		// Downcast is safe as i <= numEntries <= ⌈255/codepointsPerEntry⌉ | 
 | 		entry.SequenceNumber = uint8(j) | 
 | 		if j == numEntries { | 
 | 			entry.SequenceNumber |= lastSequenceNumberFlag | 
 | 		} | 
 | 		entry.Attributes = 0x0F | 
 | 		copy(entry.NamePart1[:], fileNameUTF16[cpIdx:]) | 
 | 		cpIdx += len(entry.NamePart1) | 
 | 		copy(entry.NamePart2[:], fileNameUTF16[cpIdx:]) | 
 | 		cpIdx += len(entry.NamePart2) | 
 | 		copy(entry.NamePart3[:], fileNameUTF16[cpIdx:]) | 
 | 		cpIdx += len(entry.NamePart3) | 
 |  | 
 | 		if err := binary.Write(w, binary.LittleEndian, entry); err != nil { | 
 | 			return err | 
 | 		} | 
 | 	} | 
 | 	selfSize, err := i.dataSize() | 
 | 	if err != nil { | 
 | 		return err | 
 | 	} | 
 | 	if selfSize >= 4*1024*1024*1024 { | 
 | 		return errors.New("single file size exceeds 4GiB which is prohibited in FAT32") | 
 | 	} | 
 | 	if i.Attrs&AttrDirectory != 0 { | 
 | 		selfSize = 0 // Directories don't have an explicit size | 
 | 	} | 
 | 	date, t, _ := timeToMsDosTime(i.ModTime) | 
 | 	if err := binary.Write(w, binary.LittleEndian, &dirEntry{ | 
 | 		DOSName:           i.dosName, | 
 | 		Attributes:        uint8(i.Attrs), | 
 | 		FirstClusterHigh:  uint16(i.startCluster >> 16), | 
 | 		LastWrittenToTime: t, | 
 | 		LastWrittenToDate: date, | 
 | 		FirstClusterLow:   uint16(i.startCluster & 0xffff), | 
 | 		FileSize:          uint32(selfSize), | 
 | 	}); err != nil { | 
 | 		return err | 
 | 	} | 
 | 	return nil | 
 | } | 
 |  | 
 | // writeData writes the contents of this inode (including possible metadata | 
 | // of its children, but not its children's data) | 
 | func (i Inode) writeData(w io.Writer, volumeLabel [11]byte) error { | 
 | 	if i.Attrs&AttrDirectory != 0 { | 
 | 		if i.parent == nil { | 
 | 			if err := binary.Write(w, binary.LittleEndian, &dirEntry{ | 
 | 				DOSName:    volumeLabel, | 
 | 				Attributes: 0x08, // Volume ID, internal use only | 
 | 			}); err != nil { | 
 | 				return err | 
 | 			} | 
 | 		} else { | 
 | 			date, t, _ := timeToMsDosTime(i.ModTime) | 
 | 			cdate, ctime, ctens := timeToMsDosTime(i.CreateTime) | 
 | 			if err := binary.Write(w, binary.LittleEndian, &dirEntry{ | 
 | 				DOSName:           [11]byte{'.', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '}, | 
 | 				CreationDate:      cdate, | 
 | 				CreationTime:      ctime, | 
 | 				CreationTenMilli:  ctens, | 
 | 				LastWrittenToTime: t, | 
 | 				LastWrittenToDate: date, | 
 | 				Attributes:        uint8(i.Attrs), | 
 | 				FirstClusterHigh:  uint16(i.startCluster >> 16), | 
 | 				FirstClusterLow:   uint16(i.startCluster & 0xffff), | 
 | 			}); err != nil { | 
 | 				return err | 
 | 			} | 
 | 			startCluster := i.parent.startCluster | 
 | 			if i.parent.parent == nil { | 
 | 				// Special case: When the dotdot directory points to the root | 
 | 				// directory, the start cluster is defined to be zero even if | 
 | 				// it isn't. | 
 | 				startCluster = 0 | 
 | 			} | 
 | 			// Time is intentionally taken from this directory, not the parent | 
 | 			if err := binary.Write(w, binary.LittleEndian, &dirEntry{ | 
 | 				DOSName:           [11]byte{'.', '.', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '}, | 
 | 				LastWrittenToTime: t, | 
 | 				LastWrittenToDate: date, | 
 | 				Attributes:        uint8(AttrDirectory), | 
 | 				FirstClusterHigh:  uint16(startCluster >> 16), | 
 | 				FirstClusterLow:   uint16(startCluster & 0xffff), | 
 | 			}); err != nil { | 
 | 				return err | 
 | 			} | 
 | 		} | 
 | 		err := makeUniqueDOSNames(i.Children) | 
 | 		if err != nil { | 
 | 			return err | 
 | 		} | 
 | 		for _, c := range i.Children { | 
 | 			if err := c.writeMeta(w); err != nil { | 
 | 				return err | 
 | 			} | 
 | 		} | 
 | 	} else { | 
 | 		if _, err := io.CopyN(w, i.Content, i.Content.Size()); err != nil { | 
 | 			return err | 
 | 		} | 
 | 	} | 
 | 	return nil | 
 | } | 
 |  | 
 | func (i Inode) dataSize() (int64, error) { | 
 | 	if i.Attrs&AttrDirectory != 0 { | 
 | 		var size int64 | 
 | 		if i.parent != nil { | 
 | 			// Dot and dotdot directories | 
 | 			size += 2 * 32 | 
 | 		} else { | 
 | 			// Volume ID | 
 | 			size += 1 * 32 | 
 | 		} | 
 | 		for _, c := range i.Children { | 
 | 			cs, err := c.metaSize() | 
 | 			if err != nil { | 
 | 				return 0, err | 
 | 			} | 
 | 			size += cs | 
 | 		} | 
 | 		if size > 2*1024*1024 { | 
 | 			return 0, errors.New("directory contains > 2MiB of metadata which is prohibited in FAT32") | 
 | 		} | 
 | 		return size, nil | 
 | 	} else { | 
 | 		return i.Content.Size(), nil | 
 | 	} | 
 | } | 
 |  | 
 | func (i *Inode) PlaceFile(path string, reader SizedReader) error { | 
 | 	pathParts := strings.Split(path, "/") | 
 | 	inodeRef := i | 
 | 	for j, part := range pathParts { | 
 | 		var childExists bool | 
 | 		for _, child := range inodeRef.Children { | 
 | 			if strings.ToLower(child.Name) == strings.ToLower(part) { | 
 | 				inodeRef = child | 
 | 				childExists = true | 
 | 				break | 
 | 			} | 
 | 		} | 
 | 		if j == len(pathParts)-1 { // Is last path part (i.e. file name) | 
 | 			if childExists { | 
 | 				return &fs.PathError{Path: path, Err: fs.ErrExist, Op: "create"} | 
 | 			} | 
 | 			newInode := &Inode{ | 
 | 				Name:    part, | 
 | 				Content: reader, | 
 | 			} | 
 | 			inodeRef.Children = append(inodeRef.Children, newInode) | 
 | 			return nil | 
 | 		} else if !childExists { | 
 | 			newInode := &Inode{ | 
 | 				Name:  part, | 
 | 				Attrs: AttrDirectory, | 
 | 			} | 
 | 			inodeRef.Children = append(inodeRef.Children, newInode) | 
 | 			inodeRef = newInode | 
 | 		} | 
 | 	} | 
 | 	panic("unreachable") | 
 | } | 
 |  | 
 | type planningState struct { | 
 | 	// List of inodes in filesystem layout order | 
 | 	orderedInodes []*Inode | 
 | 	// File Allocation Table | 
 | 	fat []uint32 | 
 | 	// Size of a single cluster in the FAT in bytes | 
 | 	clusterSize int64 | 
 | } | 
 |  | 
 | // Allocates clusters capable of holding at least b bytes and returns the | 
 | // starting cluster index | 
 | func (p *planningState) allocBytes(b int64) int { | 
 | 	// Zero-byte data entries are located at the cluster zero by definition | 
 | 	// No actual allocation is performed | 
 | 	if b == 0 { | 
 | 		return 0 | 
 | 	} | 
 | 	// Calculate the number of clusters to be allocated | 
 | 	n := (b + p.clusterSize - 1) / p.clusterSize | 
 | 	allocStartCluster := len(p.fat) | 
 | 	for i := int64(0); i < n-1; i++ { | 
 | 		p.fat = append(p.fat, uint32(len(p.fat)+1)) | 
 | 	} | 
 | 	p.fat = append(p.fat, fatEOF) | 
 | 	return allocStartCluster | 
 | } | 
 |  | 
 | func (i *Inode) placeRecursively(p *planningState) error { | 
 | 	selfDataSize, err := i.dataSize() | 
 | 	if err != nil { | 
 | 		return fmt.Errorf("%s: %w", i.Name, err) | 
 | 	} | 
 | 	i.startCluster = p.allocBytes(selfDataSize) | 
 | 	p.orderedInodes = append(p.orderedInodes, i) | 
 | 	for _, c := range i.Children { | 
 | 		c.parent = i | 
 | 		err = c.placeRecursively(p) | 
 | 		if err != nil { | 
 | 			return fmt.Errorf("%s/%w", i.Name, err) | 
 | 		} | 
 | 	} | 
 | 	return nil | 
 | } | 
 |  | 
 | // WriteFS writes a filesystem described by a root inode and its children to a | 
 | // given io.Writer. | 
 | func WriteFS(w io.Writer, rootInode Inode, opts Options) error { | 
 | 	if opts.BlockSize == 0 { | 
 | 		opts.BlockSize = 512 | 
 | 	} | 
 | 	if bits.OnesCount16(opts.BlockSize) != 1 { | 
 | 		return fmt.Errorf("option BlockSize is not a power of two") | 
 | 	} | 
 | 	if opts.BlockSize < 512 { | 
 | 		return fmt.Errorf("option BlockSize must be at least 512 bytes") | 
 | 	} | 
 | 	if opts.ID == 0 { | 
 | 		var buf [4]byte | 
 | 		if _, err := rand.Read(buf[:]); err != nil { | 
 | 			return fmt.Errorf("failed to assign random FAT ID: %v", err) | 
 | 		} | 
 | 		opts.ID = binary.BigEndian.Uint32(buf[:]) | 
 | 	} | 
 | 	if rootInode.Attrs&AttrDirectory == 0 { | 
 | 		return errors.New("root inode must be a directory (i.e. have AttrDirectory set)") | 
 | 	} | 
 | 	wb := newBlockWriter(w) | 
 | 	bs := bootSector{ | 
 | 		// Assembled x86_32 machine code corresponding to | 
 | 		// jmp $ | 
 | 		// nop | 
 | 		// i.e. an infinite loop doing nothing. Nothing created in the last 35 | 
 | 		// years should boot this anyway. | 
 | 		// TODO(q3k): write a stub | 
 | 		JmpInstruction: [3]byte{0xEB, 0xFE, 0x90}, | 
 | 		// Identification | 
 | 		OEMName: [8]byte{'M', 'O', 'N', 'O', 'G', 'O', 'N'}, | 
 | 		ID:      opts.ID, | 
 | 		// Block geometry | 
 | 		BlockSize:   opts.BlockSize, | 
 | 		TotalBlocks: opts.BlockCount, | 
 | 		// BootSector block + FSInfo Block, backup copy at blocks 6 and 7 | 
 | 		ReservedBlocks: 8, | 
 | 		// FSInfo block is always in block 1, right after this block | 
 | 		FSInfoBlock: 1, | 
 | 		// Start block of the backup of the boot block and FSInfo block | 
 | 		// De facto this must be 6 as it is only used when the primary | 
 | 		// boot block is damaged at which point this field can no longer be | 
 | 		// read. | 
 | 		BackupStartBlock: 6, | 
 | 		// A lot of implementations only work with 2, so use that | 
 | 		NumFATs:          2, | 
 | 		BlocksPerCluster: 1, | 
 | 		// Flags and signatures | 
 | 		MediaCode:     0xf8, | 
 | 		BootSignature: 0x29, | 
 | 		Label:         [11]byte{' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '}, | 
 | 		Type:          [8]byte{'F', 'A', 'T', '3', '2', ' ', ' ', ' '}, | 
 | 		Signature:     [2]byte{0x55, 0xaa}, | 
 | 	} | 
 |  | 
 | 	copy(bs.Label[:], opts.Label) | 
 |  | 
 | 	fs := fsinfo{ | 
 | 		// Signatures | 
 | 		LeadSignature:     [4]byte{0x52, 0x52, 0x61, 0x41}, | 
 | 		StructSignature:   [4]byte{0x72, 0x72, 0x41, 0x61}, | 
 | 		TrailingSignature: [2]byte{0x55, 0xAA}, | 
 |  | 
 | 		// This is the unset value which is always legal | 
 | 		NextFreeCluster: 0xFFFFFFFF, | 
 | 	} | 
 |  | 
 | 	p := planningState{ | 
 | 		clusterSize: int64(bs.BlocksPerCluster) * int64(bs.BlockSize), | 
 | 	} | 
 | 	if opts.BlockCount != 0 { | 
 | 		// Preallocate FAT if we know how big it needs to be | 
 | 		p.fat = make([]uint32, 0, opts.BlockCount/uint32(bs.BlocksPerCluster)) | 
 | 	} else { | 
 | 		// Preallocate minimum size FAT | 
 | 		// See the spec page 15 for the origin of this calculation. | 
 | 		p.fat = make([]uint32, 0, 65525+2) | 
 | 	} | 
 | 	// First two clusters are special | 
 | 	p.fat = append(p.fat, 0x0fffff00|uint32(bs.MediaCode), 0x0fffffff) | 
 | 	err := rootInode.placeRecursively(&p) | 
 | 	if err != nil { | 
 | 		return err | 
 | 	} | 
 |  | 
 | 	allocClusters := len(p.fat) | 
 | 	if allocClusters >= fatMask&math.MaxUint32 { | 
 | 		return fmt.Errorf("filesystem contains more than 2^28 FAT entries, this is unsupported. Note that this package currently always creates minimal clusters.") | 
 | 	} | 
 |  | 
 | 	// Fill out FAT to minimum size for FAT32 | 
 | 	for len(p.fat) < 65525+2 { | 
 | 		p.fat = append(p.fat, fatFree) | 
 | 	} | 
 |  | 
 | 	bs.RootClusterNumber = uint32(rootInode.startCluster) | 
 |  | 
 | 	bs.BlocksPerFAT = uint32(binary.Size(p.fat)+int(opts.BlockSize)-1) / uint32(opts.BlockSize) | 
 | 	occupiedBlocks := uint32(bs.ReservedBlocks) + (uint32(len(p.fat)-2) * uint32(bs.BlocksPerCluster)) + bs.BlocksPerFAT*uint32(bs.NumFATs) | 
 | 	if bs.TotalBlocks == 0 { | 
 | 		bs.TotalBlocks = occupiedBlocks | 
 | 	} else if bs.TotalBlocks < occupiedBlocks { | 
 | 		return fmt.Errorf("content (minimum %d blocks) would exceed number of blocks specified (%d blocks)", occupiedBlocks, bs.TotalBlocks) | 
 | 	} else { // Fixed-size file system with enough space | 
 | 		blocksToDistribute := bs.TotalBlocks - uint32(bs.ReservedBlocks) | 
 | 		// Number of data blocks which can be described by one metadata/FAT | 
 | 		// block. Always an integer because 4 (bytes per uint32) is a divisor of | 
 | 		// all powers of two equal or bigger than 8 and FAT32 requires a minimum | 
 | 		// of 512. | 
 | 		dataBlocksPerFATBlock := (uint32(bs.BlocksPerCluster) * uint32(bs.BlockSize)) / (uint32(binary.Size(p.fat[0]))) | 
 | 		// Split blocksToDistribute between metadata and data so that exactly as | 
 | 		// much metadata (FAT) exists for describing the amount of data blocks | 
 | 		// while respecting alignment. | 
 | 		divisor := dataBlocksPerFATBlock + uint32(bs.NumFATs) | 
 | 		// 2*blocksPerCluster compensates for the first two "magic" FAT entries | 
 | 		// which do not have corresponding data. | 
 | 		bs.BlocksPerFAT = (bs.TotalBlocks + 2*uint32(bs.BlocksPerCluster) + (divisor - 1)) / divisor | 
 | 		dataBlocks := blocksToDistribute - (uint32(bs.NumFATs) * bs.BlocksPerFAT) | 
 | 		// Align to full clusters | 
 | 		dataBlocks -= dataBlocks % uint32(bs.BlocksPerCluster) | 
 | 		// Magic +2 as the first two entries do not describe data | 
 | 		for len(p.fat) < (int(dataBlocks)/int(bs.BlocksPerCluster))+2 { | 
 | 			p.fat = append(p.fat, fatFree) | 
 | 		} | 
 | 	} | 
 | 	fs.FreeCount = uint32(len(p.fat) - allocClusters) | 
 | 	if fs.FreeCount > 1 { | 
 | 		fs.NextFreeCluster = uint32(allocClusters) + 1 | 
 | 	} | 
 |  | 
 | 	// Write superblock | 
 | 	if err := binary.Write(wb, binary.LittleEndian, bs); err != nil { | 
 | 		return err | 
 | 	} | 
 | 	if err := wb.FinishBlock(int64(opts.BlockSize), true); err != nil { | 
 | 		return err | 
 | 	} | 
 | 	if err := binary.Write(wb, binary.LittleEndian, fs); err != nil { | 
 | 		return err | 
 | 	} | 
 | 	if err := wb.FinishBlock(int64(opts.BlockSize), true); err != nil { | 
 | 		return err | 
 | 	} | 
 |  | 
 | 	block := make([]byte, opts.BlockSize) | 
 | 	for i := 0; i < 4; i++ { | 
 | 		if _, err := wb.Write(block); err != nil { | 
 | 			return err | 
 | 		} | 
 | 	} | 
 | 	// Backup of superblock at block 6 | 
 | 	if err := binary.Write(wb, binary.LittleEndian, bs); err != nil { | 
 | 		return err | 
 | 	} | 
 | 	if err := wb.FinishBlock(int64(opts.BlockSize), true); err != nil { | 
 | 		return err | 
 | 	} | 
 | 	if err := binary.Write(wb, binary.LittleEndian, fs); err != nil { | 
 | 		return err | 
 | 	} | 
 | 	if err := wb.FinishBlock(int64(opts.BlockSize), true); err != nil { | 
 | 		return err | 
 | 	} | 
 |  | 
 | 	for i := uint8(0); i < bs.NumFATs; i++ { | 
 | 		if err := binary.Write(wb, binary.LittleEndian, p.fat); err != nil { | 
 | 			return err | 
 | 		} | 
 | 		if err := wb.FinishBlock(int64(opts.BlockSize), true); err != nil { | 
 | 			return err | 
 | 		} | 
 | 	} | 
 |  | 
 | 	for _, i := range p.orderedInodes { | 
 | 		if err := i.writeData(wb, bs.Label); err != nil { | 
 | 			return fmt.Errorf("failed to write inode %q: %v", i.Name, err) | 
 | 		} | 
 | 		if err := wb.FinishBlock(int64(opts.BlockSize)*int64(bs.BlocksPerCluster), false); err != nil { | 
 | 			return err | 
 | 		} | 
 | 	} | 
 | 	// Creatively use block writer to write out all empty data at the end | 
 | 	if err := wb.FinishBlock(int64(opts.BlockSize)*int64(bs.TotalBlocks), false); err != nil { | 
 | 		return err | 
 | 	} | 
 | 	return nil | 
 | } |