From c9a9be8182cc7177c18007b7928079f6cd289284 Mon Sep 17 00:00:00 2001 From: DrDeano Date: Sun, 27 Sep 2020 20:53:11 +0100 Subject: [PATCH] Create a blank FAT32 image This will be used for testing the FAT32 driver for the kernel and will be integrated into the OS as a mkfs.fat32 program. Plus typos Fixed dependencies Removed `fat32_` in options Plus fixed doc comment Removed the DefaultOrValue Also reordered some stuff Removed the serial time for more parameters Moved writer() and seekableStream() to variables Refactored mkFAT32 --- build.zig | 70 ++++- mkfat32.zig | 580 ++++++++++++++++++++++++++++++++++++++++++ test/runtime_test.zig | 2 +- 3 files changed, 645 insertions(+), 7 deletions(-) create mode 100644 mkfat32.zig diff --git a/build.zig b/build.zig index 8724d62..3383cbd 100644 --- a/build.zig +++ b/build.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const logger = std.log.scoped(.builder); const builtin = @import("builtin"); const rt = @import("test/runtime_test.zig"); const RuntimeStep = rt.RuntimeStep; @@ -12,6 +13,7 @@ const File = fs.File; const Mode = builtin.Mode; const TestMode = rt.TestMode; const ArrayList = std.ArrayList; +const Fat32 = @import("mkfat32.zig").Fat32; const x86_i686 = CrossTarget{ .cpu_arch = .i386, @@ -28,6 +30,7 @@ pub fn build(b: *Builder) !void { const fmt_step = b.addFmt(&[_][]const u8{ "build.zig", + "mkfat32.zig", "src", "test", }); @@ -42,6 +45,7 @@ pub fn build(b: *Builder) !void { const boot_path = try fs.path.join(b.allocator, &[_][]const u8{ b.exe_dir, "iso", "boot" }); const modules_path = try fs.path.join(b.allocator, &[_][]const u8{ b.exe_dir, "iso", "modules" }); const ramdisk_path = try fs.path.join(b.allocator, &[_][]const u8{ b.install_path, "initrd.ramdisk" }); + const fat32_image_path = try fs.path.join(b.allocator, &[_][]const u8{ b.install_path, "fat32.img" }); const build_mode = b.standardReleaseOptions(); comptime var test_mode_desc: []const u8 = "\n "; @@ -68,6 +72,9 @@ pub fn build(b: *Builder) !void { }; make_iso.step.dependOn(&exec.step); + var fat32_builder_step = Fat32BuilderStep.create(b, .{}, fat32_image_path); + make_iso.step.dependOn(&fat32_builder_step.step); + var ramdisk_files_al = ArrayList([]const u8).init(b.allocator); defer ramdisk_files_al.deinit(); @@ -166,6 +173,58 @@ pub fn build(b: *Builder) !void { debug_step.dependOn(&debug_cmd.step); } +/// The FAT32 step for creating a FAT32 image. +const Fat32BuilderStep = struct { + /// The Step, that is all you need to know + step: Step, + + /// The builder pointer, also all you need to know + builder: *Builder, + + /// The path to where the ramdisk will be written to. + out_file_path: []const u8, + + /// Options for creating the FAT32 image. + options: Fat32.Options, + + /// + /// The make function that is called by the builder. + /// + /// Arguments: + /// IN step: *Step - The step of this step. + /// + /// Error: error{EndOfStream} || File.OpenError || File.ReadError || File.WriteError || File.SeekError || Allocator.Error || Fat32.Error || Error + /// error{EndOfStream} || File.OpenError || File.ReadError || File.WriteError || File.SeekError - Error related to file operations. See std.fs.File. + /// Allocator.Error - If there isn't enough memory to allocate for the make step. + /// Fat32.Error - If there was an error creating the FAT image. This will be invalid options. + /// + fn make(step: *Step) (error{EndOfStream} || File.OpenError || File.ReadError || File.WriteError || File.SeekError || Fat32.Error)!void { + const self = @fieldParentPtr(Fat32BuilderStep, "step", step); + try Fat32.make(self.options, self.out_file_path); + } + + /// + /// Create a FAT32 builder step. + /// + /// Argument: + /// IN builder: *Builder - The build builder. + /// IN options: Options - Options for creating FAT32 image. + /// + /// Return: *Fat32BuilderStep + /// The FAT32 builder step pointer to add to the build process. + /// + pub fn create(builder: *Builder, options: Fat32.Options, out_file_path: []const u8) *Fat32BuilderStep { + const fat32_builder_step = builder.allocator.create(Fat32BuilderStep) catch unreachable; + fat32_builder_step.* = .{ + .step = Step.init(.Custom, builder.fmt("Fat32BuilderStep", .{}), builder.allocator, make), + .builder = builder, + .options = options, + .out_file_path = out_file_path, + }; + return fat32_builder_step; + } +}; + /// The ramdisk make step for creating the initial ramdisk. const RamdiskStep = struct { /// The Step, that is all you need to know @@ -198,7 +257,7 @@ const RamdiskStep = struct { /// Errors for opening, reading and writing to and from files and for allocating memory. /// fn writeRamdisk(comptime Usize: type, self: *RamdiskStep) Error!void { - // 1MB, don't think the ram disk should be very big + // 1GB, don't think the ram disk should be very big const max_file_size = 1024 * 1024 * 1024; // Open the out file @@ -240,9 +299,8 @@ const RamdiskStep = struct { } /// - /// The make function that is called by the builder. This will create the qemu process with the - /// stdout as a Pipe. Then create the read thread to read the logs from the qemu stdout. Then - /// will call the test function to test a specifics part of the OS defined by the test mode. + /// The make function that is called by the builder. This will switch on the target to get the + /// correct usize length for the target. /// /// Arguments: /// IN step: *Step - The step of this step. @@ -262,8 +320,8 @@ const RamdiskStep = struct { /// Create a ramdisk step. /// /// Argument: - /// IN builder: *Builder - The build builder. - /// IN target: CrossTarget - The target for the build. + /// IN builder: *Builder - The build builder. + /// IN target: CrossTarget - The target for the build. /// IN files: []const []const u8 - The file names to be added to the ramdisk. /// IN out_file_path: []const u8 - The output file path. /// diff --git a/mkfat32.zig b/mkfat32.zig new file mode 100644 index 0000000..d36fc9c --- /dev/null +++ b/mkfat32.zig @@ -0,0 +1,580 @@ +const std = @import("std"); +const File = std.fs.File; + +// This is the assembly for the FAT bootleader. +// [bits 16] +// [org 0x7C00] +// +// jmp short _start +// nop +// +// times 87 db 0xAA +// +// _start: +// jmp long 0x0000:start_16bit +// +// start_16bit: +// cli +// mov ax, cs +// mov ds, ax +// mov es, ax +// mov ss, ax +// mov sp, 0x7C00 +// lea si, [message] +// .print_string_with_new_line: +// mov ah, 0x0E +// xor bx, bx +// .print_string_loop: +// lodsb +// cmp al, 0 +// je .print_string_done +// int 0x10 +// jmp short .print_string_loop +// .print_string_done: +// mov al, 0x0A +// int 0x10 +// mov al, 0x0D +// int 0x10 +// +// .reboot: +// xor ah, ah +// int 0x16 +// int 0x19 +// +// .loop_forever: +// hlt +// jmp .loop_forever + +// message db "This is not a bootable disk. Please insert a bootable floppy and press any key to try again", 0 +// times 510 - ($ - $$) db 0 +// dw 0xAA55 + +/// A FAT32 static structure for creating a empty FAT32 image. This contains helper functions +/// for creating a empty FAT32 image. +/// For more information, see: https://en.wikipedia.org/wiki/Design_of_the_FAT_file_system +pub const Fat32 = struct { + /// The FAT32 boot sector header (without the jmp and boot code) including the extended + /// parameter block for FAT32 with the extended signature 0x29. + const Header = struct { + /// The OEM. + oem: [8]u8 = "ZystemOS".*, + + /// The number of bytes per sector. This needs to be initialised by the user or use the + /// default value at runtime. Use `getDefaultSectorSize` for the default. + bytes_per_sector: u16, + + /// The number of sectors per cluster. This needs to be initialised by the user or use the + /// default value at runtime. Use `getDefaultSectorsPerCluster` for the default. + sectors_per_cluster: u8, + + /// The number of reserved sectors at the beginning of the image. This is where the fat + /// header, FSInfo and FAT is stored. The default is 32 sectors. + /// TODO: Investigate if this can be reduced if not all sectors are needed. + reserved_sectors: u16 = getReservedSectors(), + + /// The number of FAT's. This is always 2. + fat_count: u8 = 2, + + /// The size in bytes of the root directory. This is only used by FAT12 and FAT16, so is + /// always 0 for FAT32. + zero_root_directory_size: u16 = 0, + + /// The total number of sectors. This is only used for FAT12 and FAT16, so is always 0 for + /// FAT32. + zero_total_sectors_16: u16 = 0, + + /// The media type. This is used to identify the type of media this image is. As all FAT's + /// that are created as a fixed disk, non of that old school floppy, this is always 0xF8. + media_descriptor_type: u8 = 0xF8, + + /// The total number of sectors of the FAT. This is only used for FAT12 and FAT16, so + /// always 0 for FAT32. + zero_sectors_per_fat: u16 = 0, + + /// The number of sectors per track for Int 13h. This isn't really needed as we are + /// creating a fixed disk. An example used 63. + sectors_per_track: u16 = 63, + + /// The number of heads for Int 13h. This isn't really needed as we are creating a fixed + /// disk. An example used 255. + head_count: u16 = 255, + + /// The number of hidden sectors. As there is no support for partitions, this is set ot zero. + hidden_sectors: u32 = 0, + + /// The total number of sectors of the image. This is determined at runtime by the size of + /// image and the sector size. + total_sectors: u32, + + /// The number of sectors the FAT takes up. This is set based on the size of the image. + sectors_per_fat: u32, + + /// Mirror flags. If bit 7 is set, then bits 0-3 represent the number of active FAT entries + /// (zero based) and if clear, all FAT's are mirrored. Always mirror, so zero. + mirror_flags: u16 = 0x0000, + + /// The version. This is usually always zero (0.0). If version is 1.2, this will be stored + /// as 0x0201. + version_number: u16 = 0x0000, + + /// The start cluster of the root directory. This is usually always 2 unless there is a bad + /// sector, 0 or 1 are invalid. + root_directory_cluster: u32 = 2, + + /// The sector where is the FS information sector is located. A value of 0x0000 or 0xFFFF + /// indicates that there isn't a FSInfo structure. A value of 0x0000 should not be treated + /// as a valid FSInfo sector. Without a FS Information Sector, the minimum allowed logical + /// sector size of FAT32 volumes can be reduced down to 128 bytes for special purposes. + /// This is usually sector 1. + fsinfo_sector: u16 = 1, + + /// The sector for the backup boot record where the first 3 sectors are copied. A value of + /// 0x000 or 0xFFFF indicate there is no backup. This usually 6. + backup_boot_sector: u16 = 6, + + /// Reserved,. All zero. + reserved0: u96 = 0x000000000000, + + /// The physical drive number for Int 13h in the BIOS. 0x80 is for fixed disks, but as this + /// is used for bootloaders, this isn't really needed. + drive_number: u8 = 0x80, + + /// Reserved. All zero. + reserved1: u8 = 0x00, + + /// The extended boot signature. 0x29, can be 0x28, but will only accept 0x29. If 0x28, + /// then the below (serial_number, volume_label and filesystem_type) will be unavailable. + signature: u8 = 0x29, + + /// The serial number of the FAT image at format. This is a function of the current + /// timestamp of creation. + serial_number: u32, + + /// The partitioned volume label. This can be set by the user else the default name will be + /// give: "NO NAME ". + volume_label: [11]u8, + + /// The file system type, "FAT32 " for FAT32. + filesystem_type: [8]u8 = "FAT32 ".*, + }; + + /// Options used for initialising a new FAT image. + pub const Options = struct { + /// The FAT32 image size in bytes. This is not the formatted size. + /// The default (recommenced smallest) is 34090496 bytes (~32.5KB). This can be reduced to + /// the lowest value of 17920 bytes (17.5KB). This image size is the smallest possible size + /// where you can only create files, but not write to them. This image size can be mounted + /// in Linux and ZystemOS. + image_size: u32 = getDefaultImageSize(), + + /// The sector size in bytes. The minimum value for this is 512, and a maximum of 4096. + /// This also must be a multiple of 512. Default 512. + sector_size: u16 = getDefaultSectorSize(), + + /// The number of sectors per clusters. A default is chosen depending on the image size and + /// sector size. Valid value are: 1, 2, 4, 8, 16, 32, 64 and 128. Default 1. + cluster_size: u8 = getDefaultSectorsPerCluster(getDefaultImageSize(), getDefaultSectorSize()) catch unreachable, + + /// The formatted volume name. Volume names shorter than 11 characters must have trailing + /// spaces. Default NO MANE. + volume_name: [11]u8 = getDefaultVolumeName(), + }; + + /// The error set for the static functions for creating a FAT32 image. + pub const Error = error{ + /// If the FAT image that the user want's to create is too small: < 17.5KB. + TooSmall, + + /// If the FAT image that the user want's to create is too large: > 2TB. + TooLarge, + + /// The value in a option provided from the user is invalid. + InvalidOptionValue, + }; + + /// The logger function for printing errors when creating a FAT32 image. + const logger = std.log.scoped(.mkfat32); + + /// The bootloader code for the FAT32 boot sector. + const bootsector_boot_code = [512]u8{ + 0xEB, 0x58, 0x90, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, + 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, + 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, + 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, + 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, + 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0x66, 0xEA, 0x62, 0x7C, 0x00, 0x00, + 0x00, 0x00, 0xFA, 0x8C, 0xC8, 0x8E, 0xD8, 0x8E, 0xC0, 0x8E, 0xD0, 0xBC, 0x00, 0x7C, 0x8D, 0x36, + 0x8F, 0x7C, 0xB4, 0x0E, 0x31, 0xDB, 0xAC, 0x3C, 0x00, 0x74, 0x04, 0xCD, 0x10, 0xEB, 0xF7, 0xB0, + 0x0A, 0xCD, 0x10, 0xB0, 0x0D, 0xCD, 0x10, 0x30, 0xE4, 0xCD, 0x16, 0xCD, 0x19, 0xEB, 0xFE, 0x54, + 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 0x20, 0x6E, 0x6F, 0x74, 0x20, 0x61, 0x20, 0x62, 0x6F, 0x6F, + 0x74, 0x61, 0x62, 0x6C, 0x65, 0x20, 0x64, 0x69, 0x73, 0x6B, 0x2E, 0x20, 0x50, 0x6C, 0x65, 0x61, + 0x73, 0x65, 0x20, 0x69, 0x6E, 0x73, 0x65, 0x72, 0x74, 0x20, 0x61, 0x20, 0x62, 0x6F, 0x6F, 0x74, + 0x61, 0x62, 0x6C, 0x65, 0x20, 0x66, 0x6C, 0x6F, 0x70, 0x70, 0x79, 0x20, 0x61, 0x6E, 0x64, 0x20, + 0x70, 0x72, 0x65, 0x73, 0x73, 0x20, 0x61, 0x6E, 0x79, 0x20, 0x6B, 0x65, 0x79, 0x20, 0x74, 0x6F, + 0x20, 0x74, 0x72, 0x79, 0x20, 0x61, 0x67, 0x61, 0x69, 0x6E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0xAA, + }; + + /// + /// Get the number of reserved sectors. The number of reserved sectors doesn't have to be 32, + /// but this is a commonly used value. + /// + /// Return: u16 + /// The number of reserved sectors. + /// + fn getReservedSectors() u16 { + return 32; + } + + /// + /// Get the default sector size for a FAT32 image. (512) + /// + /// Return: u16 + /// The default sector size (512). + /// + fn getDefaultSectorSize() u16 { + // TODO: Look into if this could be different for different scenarios + return 512; + } + + /// + /// Based on the image size, the sector size per cluster can vary. See + /// https://support.microsoft.com/en-us/help/140365/default-cluster-size-for-ntfs-fat-and-exfat + /// but with some modifications to allow for smaller size images (The smallest image size possible is 17.5KB): + /// 17.5KB -> 64MB: 512 bytes + /// 64MB -> 128MB: 1024 bytes (1KB) + /// 128MB -> 256MB: 2048 bytes (2KB) + /// 256MB -> 8GB: 4096 bytes (4KB) + /// 8GB -> 16GB: 8192 bytes (8KB) + /// 16GB -> 32GB: 16384 bytes (16KB) + /// 32GB -> 2TB: 32768 bytes (32KB) + /// else: Error + /// + /// The smallest size is calculated as follows: + /// (reserved_sectors + num_of_fats + root_directory) * smallest_sector_size = (32 + 2 + 1) * 512 = 35 * 512 + /// + /// Arguments: + /// IN image_size: u64 - The image size the user wants to create. + /// IN bytes_per_sector: u16 - The bytes per sector the user defined, or the default value. + /// + /// Return: u16 + /// The default sectors per cluster based on the image size and sector size. + /// + /// Error: Error + /// error.TooSmall - If the image size is too small (below 35 * 512 (17.5KB)) + /// error.TooLarge - If the image size is too large (above 2TB) + /// + fn getDefaultSectorsPerCluster(image_size: u64, bytes_per_sector: u16) Error!u8 { + // A couple of constants to help + const KB = 1024; + const MB = 1024 * KB; + const GB = 1024 * MB; + const TB = 1024 * GB; + + return switch (image_size) { + 0...35 * 512 - 1 => Error.TooSmall, + 35 * 512...64 * MB - 1 => @intCast(u8, std.math.max(512, bytes_per_sector) / bytes_per_sector), + 64 * MB...128 * MB - 1 => @intCast(u8, std.math.max(1024, bytes_per_sector) / bytes_per_sector), + 128 * MB...256 * MB - 1 => @intCast(u8, std.math.max(2048, bytes_per_sector) / bytes_per_sector), + 256 * MB...8 * GB - 1 => @intCast(u8, std.math.max(4096, bytes_per_sector) / bytes_per_sector), + 8 * GB...16 * GB - 1 => @intCast(u8, std.math.max(8192, bytes_per_sector) / bytes_per_sector), + 16 * GB...32 * GB - 1 => @intCast(u8, std.math.max(16384, bytes_per_sector) / bytes_per_sector), + 32 * GB...2 * TB - 1 => @intCast(u8, std.math.max(32768, bytes_per_sector) / bytes_per_sector), + else => Error.TooLarge, + }; + } + + /// + /// Get the default volume name of a FAT image: "NO NAME ". + /// + /// Return: [11]u8 + /// The default volume name. + /// + fn getDefaultVolumeName() [11]u8 { + return "NO NAME ".*; + } + + /// + /// Get the default image size: 34090496 (~32.5KB). This is the recommended minimum image size + /// for FAT32. (Valid cluster values + (sectors per FAT * 2) + reserved sectors) * bytes per sector. + /// + /// Return: u32 + /// The smallest recommended FAT32 image size. + /// + fn getDefaultImageSize() u32 { + // The 513*2 was pre calculated + return (0xFFF5 + 1026 + @as(u32, getReservedSectors())) * @as(u32, getDefaultSectorSize()); + } + + /// + /// Create the FAT32 serial number. This is generated from the date. Reference: + /// https://www.digital-detective.net/documents/Volume%20Serial%20Numbers.pdf + /// + /// Return: u32 + /// Serial number for a new FAT32 image. + /// + fn createSerialNumber() u32 { + // TODO: Get the actual date. Currently there is no std lib for human readable date. + const year = 2020; + const month = 09; + const day = 27; + const hour = 13; + const minute = 46; + const second = 53; + const millisecond_10 = 54; + + const p1 = (@as(u16, month) << 8) | day; + const p2 = (@as(u16, second) << 8) | millisecond_10; + const p3 = (@as(u16, hour) << 8) | minute; + + const p4 = p1 + p2; + const p5 = p3 + year; + + return (@as(u32, p4) << 16) + p5; + } + + /// + /// Write the FSInfo and backup FSInfo sector to a image file. This uses a valid FAT32 boot + /// sector header for creating the FSInfo sector. + /// + /// Argument: + /// IN/OUT image: File - The image file to write the FSInfo to. + /// IN fat32_header: Header - A valid FAT32 boot header for creating the FSInfo sector. + /// IN free_cluster_num: u32 - The number of free data clusters on the image. + /// IN next_free_cluster: u32 - The next free data cluster to start looking for when writing + /// files. + /// + /// Error File.WriteError || File.SeekError + /// File.WriteError - If there is an error when writing to the image. See File.WriteError + /// File.SeekError - If there is an error when seeking the image. See File.SeekError + /// + fn writeFSInfo(image: File, fat32_header: Header, free_cluster_num: u32, next_free_cluster: u32) (File.WriteError || File.SeekError)!void { + const seekable_stream = image.seekableStream(); + const writer = image.writer(); + + // Seek to the correct location + try seekable_stream.seekTo(fat32_header.fsinfo_sector * fat32_header.bytes_per_sector); + + // First signature + try writer.writeIntLittle(u32, 0x41615252); + + // These next bytes are reserved and unused + try seekable_stream.seekBy(480); + + // Second signature + try writer.writeIntLittle(u32, 0x61417272); + + // Last know free cluster count + try writer.writeIntLittle(u32, free_cluster_num); + + // Cluster number to start looking for available clusters + try writer.writeIntLittle(u32, next_free_cluster); + + // These next bytes are reserved and unused + try seekable_stream.seekBy(12); + + // Trail signature + try writer.writeIntLittle(u32, 0xAA550000); + + // Repeat again for the backup + try seekable_stream.seekTo((fat32_header.backup_boot_sector + fat32_header.fsinfo_sector) * fat32_header.bytes_per_sector); + try writer.writeIntLittle(u32, 0x41615252); + try seekable_stream.seekBy(480); + try writer.writeIntLittle(u32, 0x61417272); + try writer.writeIntLittle(u32, free_cluster_num); + try writer.writeIntLittle(u32, next_free_cluster); + try seekable_stream.seekBy(12); + try writer.writeIntLittle(u32, 0xAA550000); + } + + /// + /// Write the FAT to the image. This sets up a blank FAT with end marker of 0x0FFFFFFF and root + /// directory cluster of 0x0FFFFFFF (one cluster chain). + /// + /// Argument: + /// IN/OUT image: File - The image file to write the FSInfo to. + /// IN fat32_header: Header - A valid FAT32 boot header for creating the FAT. + /// + /// Error File.WriteError || File.SeekError + /// File.WriteError - If there is an error when writing to the image. See File.WriteError + /// File.SeekError - If there is an error when seeking the image. See File.SeekError + /// + fn writeFAT(image: File, fat32_header: Header) (File.WriteError || File.SeekError)!void { + const seekable_stream = image.seekableStream(); + const writer = image.writer(); + + // This FAT is below the reserved sectors + try seekable_stream.seekTo(fat32_header.reserved_sectors * fat32_header.bytes_per_sector); + + // Last byte is the same as the media descriptor, the rest are 1's. + try writer.writeIntLittle(u32, 0xFFFFFF00 | @as(u32, fat32_header.media_descriptor_type)); + + // End of chain indicator. This can be 0x0FFFFFF8 - 0x0FFFFFFF, but 0x0FFFFFFF is better supported. + try writer.writeIntLittle(u32, 0x0FFFFFFF); + + // Root director cluster, 0x0FFFFFFF = initially only one cluster for root directory + try writer.writeIntLittle(u32, 0x0FFFFFFF); + + // Write the second FAT, same as the first + try seekable_stream.seekTo(fat32_header.reserved_sectors * fat32_header.bytes_per_sector + (fat32_header.sectors_per_fat * fat32_header.bytes_per_sector)); + try writer.writeIntLittle(u32, 0xFFFFFF00 | @as(u32, fat32_header.media_descriptor_type)); + try writer.writeIntLittle(u32, 0x0FFFFFFF); + try writer.writeIntLittle(u32, 0x0FFFFFFF); + } + + /// + /// Write the FAT boot sector with the boot code and FAT32 header to the image. + /// + /// Argument: + /// IN/OUT image: File - The image file to write the FSInfo to. + /// IN fat32_header: Header - A valid FAT32 boot header for creating the FAT. + /// + /// Error: File.WriteError || File.SeekError + /// File.WriteError - If there is an error when writing to the image. See File.WriteError. + /// File.SeekError - If there is an error when seeking the image. See File.SeekError. + /// + fn writeBootSector(image: File, fat32_header: Header) (File.WriteError || File.SeekError)!void { + const seekable_stream = image.seekableStream(); + const writer = image.writer(); + + var boot_sector: [512]u8 = undefined; + std.mem.copy(u8, &boot_sector, &bootsector_boot_code); + + // Write the header into the boot sector variable + var fat32_header_stream = std.io.fixedBufferStream(boot_sector[3..90]); + inline for (std.meta.fields(Header)) |item| { + switch (@typeInfo(item.field_type)) { + .Array => |info| switch (info.child) { + u8 => try fat32_header_stream.writer().writeAll(&@field(fat32_header, item.name)), + else => @compileError("Unexpected field type: " ++ @typeName(info.child)), + }, + .Int => try fat32_header_stream.writer().writeIntLittle(item.field_type, @field(fat32_header, item.name)), + else => @compileError("Unexpected field type: " ++ @typeName(info.child)), + } + } + + // Write the bootstrap and header sector to the image + try seekable_stream.seekTo(0); + try writer.writeAll(&boot_sector); + + // Also write the backup sector to the image + try seekable_stream.seekTo(fat32_header.backup_boot_sector * fat32_header.bytes_per_sector); + try writer.writeAll(&boot_sector); + } + + /// + /// Create a valid FAT32 header with out the bootstrap code with either user defined parameters + /// or default FAT32 parameters. + /// + /// Argument: + /// IN options: Options - The values provided by the user or default values. + /// + /// Return: Fat32.Header + /// A valid FAT32 header without the bootstrap code. + /// + /// Error: Error + /// Error.InvalidOptionValue - If the values provided by the user are invalid and/or out of + /// bounds. A log message will be printed to display the valid + /// ranges. + /// + fn createFATHeader(options: Options) Error!Header { + // 512 is the smallest sector size for FAT32 + if (options.sector_size < 512 or options.sector_size > 4096 or options.sector_size % 512 != 0) { + logger.err("Bytes per sector is invalid. Must be greater then 512 and a multiple of 512. Found: {}", .{options.sector_size}); + return Error.InvalidOptionValue; + } + + // Valid clusters are 1, 2, 4, 8, 16, 32, 64 and 128 + if (options.cluster_size < 0 or options.cluster_size > 128 or !std.math.isPowerOfTwo(options.cluster_size)) { + logger.err("Sectors per cluster is invalid. Must be less then 32 and a power of 2. Found: {}", .{options.cluster_size}); + return Error.InvalidOptionValue; + } + + // Ensure the image is aligned to the bytes per sector + // Backwards as if being imaged to a device, we can't can't go over + const image_size = std.mem.alignBackward(options.image_size, options.sector_size); + + // This is the bare minimum. It wont hold any data in a file, but can create some files. But it still can be mounted in Linux + // +1 for the root director sector, 2 TB + // FAT count for FAT32 is always 2 + if (image_size < ((getReservedSectors() + 2 + 1) * options.sector_size) or image_size >= 2 * 1024 * 1024 * 1024 * 1024) { + logger.err("Image size is invalid. Must be greater then 17919 (~18KB). Found: {}", .{image_size}); + return Error.InvalidOptionValue; + } + + // See: https://board.flatassembler.net/topic.php?t=12680 + var sectors_per_fat = @intCast(u32, (image_size - getReservedSectors() + (2 * options.cluster_size)) / ((options.cluster_size * (options.sector_size / 4)) + 2)); + // round up sectors + sectors_per_fat = (sectors_per_fat + options.sector_size - 1) / options.sector_size; + + return Header{ + .bytes_per_sector = options.sector_size, + .sectors_per_cluster = options.cluster_size, + .total_sectors = @intCast(u32, @divExact(image_size, options.sector_size)), + .sectors_per_fat = sectors_per_fat, + .serial_number = createSerialNumber(), + .volume_label = options.volume_name, + }; + } + + /// + /// Make a FAT32 image. This will either use the default options or modified defaults from the + /// user. The file will be saved to the path specified. + /// + /// Argument: + /// IN options: Options - The FAT32 options that the user can provide to change + /// the parameters of a FAT32 image. + /// IN out_file_path: []const u8 - The location for which the FAT32 image will be created. + /// + /// Error: File.OpenError || File.WriteError || File.SeekError || Error + /// File.OpenError || File.WriteError || File.SeekError - Error relating to file operations. See std.fs.File. + /// Error.InvalidOptionValue - In the user has provided invalid options. + /// Error.TooLarge - The image size is too small. < 17.5KB. + /// Error.TooSmall - The image size is to large. > 2TB. + /// + pub fn make(options: Options, out_file_path: []const u8) (File.OpenError || File.WriteError || File.SeekError || Error)!void { + // First set up the header + const fat32_header = try Fat32.createFATHeader(options); + // Get the total image size again. As the above has a check for the image size, we don't need one here again + const image_size = std.mem.alignBackward(options.image_size, fat32_header.bytes_per_sector); + + // Open the out file + const image = try std.fs.cwd().createFile(out_file_path, .{ .read = true }); + + // If there was an error, delete the image as this will be invalid + errdefer (std.fs.cwd().deleteFile(out_file_path) catch unreachable); + defer image.close(); + + // Initialise the image with all zeros + try image.writer().writeByteNTimes(0x00, image_size); + + // Write the boot sector with the bootstrap code and header and the backup boot sector. + try Fat32.writeBootSector(image, fat32_header); + + // Write the FAT and second FAT + try Fat32.writeFAT(image, fat32_header); + + // Calculate the usable clusters. + const usable_sectors = fat32_header.total_sectors - fat32_header.reserved_sectors - (fat32_header.fat_count * fat32_header.sectors_per_fat); + const usable_clusters = @divFloor(usable_sectors, fat32_header.sectors_per_cluster) - 1; + + // Write the FSInfo and backup FSInfo sectors + try Fat32.writeFSInfo(image, fat32_header, usable_clusters, 2); + } +}; diff --git a/test/runtime_test.zig b/test/runtime_test.zig index b1d4fe0..9640e11 100644 --- a/test/runtime_test.zig +++ b/test/runtime_test.zig @@ -179,7 +179,7 @@ pub const RuntimeStep = struct { } /// - /// The make function that is called by the builder. This will create the qemu process with the + /// The make function that is called by the builder. This will create a qemu process with the /// stdout as a Pipe. Then create the read thread to read the logs from the qemu stdout. Then /// will call the test function to test a specifics part of the OS defined by the test mode. ///