NAME

Sys::Export::VFAT - Write minimal FAT12/16/32 filesystems with control over stored file extents

SYNOPSIS

my $dst= Sys::Export::VFAT->new(
  filename => $path,
  volume_label => 'ESP'
  volume_offset => 2048*512, # inform VFAT of your partition layout
);
# Basic files and directories
$dst->add([ file => "README.TXT", "Hello World\r\n" ]);
$dst->add([ file => 'EFI/BOOT/BOOTIA32.EFI', { data_path => $loader }]);

# Request file 'initrd' have its bytes stored at exactly disk offset 0x110000
$dst->add([ file => 'initrd', { data_path => $initrd, device_offset => 0x110000 }]);

# Request file 'vmlinuz' have its bytes stored aligned to 2048 disk address,
#  and capture the location that was chosen into $kernel_ofs
$dst->add(
  name => 'vmlinuz',
  mode => S_IFREG, # stat constant
  data_path => $path_to_kernel,
  device_align => 2048,
  device_offset => \my $kernel_ofs,
);

$dst->finish;

DESCRIPTION

This module can be used as an export destination to build a FAT32/16/12 filesystem by directly encoding your files into a very compact VFAT layout. The generated filesystem has no fragmentation and no free space (unless you specified device_offset/device_align in a way that created "holes" in your cluster array).

This implementation caches all files in memory, and then chooses FAT parameters that result in the smallest image.

This implementation also has some fun features intended to work together with the ISOHybrid module, which can (on the assumption that the filesystem will never be written) encode hardlinks, encode symlinks as hard-linked directories, and place files at specific offsets within the generated image.

FAT Geometry

This was complicated enough it became its own module. See Sys::Export::VFAT::Geometry.

Algorithm

This module chooses the optimal cluster size and count for the files you provide. That choice affects the starting offset of the first cluster, so this module buffers all directories into memory until the decisions are made, and then writes the whole filesystem in one pass during "finish".

The cluster size/count are chosen by scanning over all the files and directories you have added and total up the number of clusters required, under each of the possible cluster sizes. It also adds in the size of the FAT, which is based on the cluster count. It also calculates a complete assignment of files to clusters, to verify it can meet requests for device_offset or device_align. It then selects whichever successful configuration had the smallest overall size. The calculated cluster assignment is then applied to the files and directory entries, and then the directories get encoded now that the cluster refs are resolved. Finally, each component of the filesystem is written to the destination filename or filehandle.

CONSTRUCTORS

new

$fat= Sys::Export::VFAT->new($filename_or_handle);
$fat= Sys::Export::VFAT->new(%attrs);
$fat= Sys::Export::VFAT->new(\%attrs);

This takes a list of attributes as a hashref or key/value list. If there is exactly one argument, it is treated as the filename attribute.

ATTRIBUTES

filename

Name of file (or device) to write. If the file exists it will be truncated before writing. If you want to write the filesystem amid existing data (like a partition table0, pass a file handle as filehandle.

filehandle

Output filehandle to write. The file will be enlarged if it is not big enough.

root

The root Directory object.

geometry

An instance of Sys::Export::VFAT::Geometry that describes the size and location of VFAT structures. This is generated during "finish", but if you have very rigid ideas about how the filesystem should be laid out, you can pass it to the constructor.

allocation_table

An instance of Sys::Export::VFAT::AllocationTable used to track which clusters have been allocated.

volume_offset

This value causes the entire volume to be written at an offset from the start of the file or device. This value is part of the calculation for methods like "get_file_device_extent" and alignment of files to device addresses. If you are writing this filesystem within a partition of a larger device, set this attribute to get correct device alignments.

If not set, it defaults to 0, so alignments will be performed relative to the start of the volume, and methods like "get_file_device_extent" will return ofsets relative to the volume.

min_bits

Setting this to 32 enforces the generated filesystem will be FAT32 rather than FAT16 or FAT12. FAT32 has a minimum disk size of about 32MiB, so this has the side effect of forcing a minimum number of clusters, which may result in a lot of unused space in the generated filesystem. But, FAT32 is structurally different from FAT12/16 (such as having arbitrary number of reserved sectors at the start of the image, used for things like boot loaders) and you might require that even at the expense of wasted space.

bytes_per_sector

Force a sector size other than the default 512.

sectors_per_cluster

Force a number of sectors per cluster. The default is to try different sizes to see which results in the smallest filesystem.

fat_count

Force a number of allocation tables. Two is standard (for redundancy in case of disk errors) but setting this to 1 saves some space.

free_space

By default, the filesystem is created with zero free clusters. Specify this (in bytes) to add some free space to the generated filesystem.

volume_label

Volume label of the generated filesystem

METHODS

add

$fat->add(\%file_attrs);
# Attributes:
# {
#   name               => $path_bytes,
#   uname              => $path_unicode_string,
#   FAT_shortname      => "8CHARATR.EXT",
#   mode               => $unix_stat_mode,
#   FAT_flags          => ATTR_READONLY|ATTR_HIDDEN|ATTR_SYSTEM|ATTR_ARCHIVE,
#   atime              => $unix_epoch,
#   mtime              => $unix_epoch,
#   btime              => $unix_epoch,
#   size               => $data_size,
#   data               => $literal_data_or_scalarref,
#   device_offset      => $desired_byte_offset,
#   device_align       => $desired_byte_alignment_pow2,
# }

This add method takes the same file objects as used by Sys::Export, but with some optional extras:

FAT_shortname

Any file name not conforming to the 8.3 name limitation of FAT will get an auto-generated "short" filename, in addition to its "long" filename. If you want control over what short name is generated, you can specify it with FAT_shortname.

FAT_attrs

An ORed combination of "ATTR_READONLY", "ATTR_HIDDEN", "ATTR_SYSTEM", or "ATTR_ARCHIVE". If this value is defined, this module will *not* use the mode user-write bit to determine ATTR_READONLY and will not use leading "." to determine ATTR_HIDDEN.

device_offset

For integration with ISOHybrid, you may specify device_offset to request the file be placed at an exact location, and as a single un-fragmented extent. This accounts for the "device_offset" of the whole filesystem. If you did not set that attribute, this becomes a byte offset from the start of this filesystem.

This offset must fall on the address of one of the clusters of the data region, and will generate an exception if it can't be honored. It must also agree with any device_align you requested on other files. Unfortunately you won't get that exception until "finish" is called, as this module looks for workable cluster layouts.

You may also set this to a scalar-ref which will receive the device_offset once the file's location is decided.

device_align

Like device_offset, but if you just want to request the file be aligned to the device rather than needing it to exist at a specific offset. This is a power of 2 in bytes, such as '2048'. This takes attribute "volume_offset" into account, possibly providing alignment that is a multiple of a power-of-2 from the start of the device but not from the start of the volume.

finish

This method performs all the actual work of building the filesystem. This module writes the entire filesystem in one pass after deciding the best geometry and minimal number of clusters to hold the data you've supplied.

You may get exceptions during this call if there isn't a way to write your files as requested.

EXPORTS

is_valid_longname

$bool= is_valid_longname($name)

$name should be a unicode string

is_valid_shortname

$bool= is_valid_shortname($name)

$name should be encoded as platform-native bytes, with no codepoints above 0xFF. Allows space characters in the name, even though most DOS tools can't handle that.

is_valid_volume_label

$bool= is_valid_volume_label($name)

$name should be encoded as platform-native bytes, with no codepoints above 0xFF.

remove_invalid_shortname_chars

$name= remove_invalid_shortname_chars($name, $replacement='_')

Coerce an arbitrary string to characters valid as a FAT short name, uppercasing any lowercase ASCII and replacing illegal characters with '_' or the character of your choice.

VERSION

version 0.004

AUTHOR

Michael Conrad <mike@nrdvana.net>

COPYRIGHT AND LICENSE

This software is copyright (c) 2026 by Michael Conrad.

This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.