netget/server/usb/msc/
fat16.rs1#[cfg(all(unix, feature = "libusb"))]
40use anyhow::{bail, Result};
41
42#[cfg(all(unix, feature = "libusb"))]
44pub const BYTES_PER_SECTOR: usize = 512;
45
46#[cfg(all(unix, feature = "libusb"))]
47const SECTORS_PER_CLUSTER: u8 = 1;
48#[cfg(all(unix, feature = "libusb"))]
49const RESERVED_SECTORS: u16 = 1;
50#[cfg(all(unix, feature = "libusb"))]
51const NUM_FATS: u8 = 2;
52#[cfg(all(unix, feature = "libusb"))]
53const ROOT_ENTRIES: u16 = 512;
54#[cfg(all(unix, feature = "libusb"))]
55const FAT_SECTORS: u16 = 32;
56
57#[cfg(all(unix, feature = "libusb"))]
59pub const DEFAULT_TOTAL_SECTORS: u16 = 8192;
60
61#[cfg(all(unix, feature = "libusb"))]
63const ROOT_DIR_SECTORS: u32 = (ROOT_ENTRIES as u32 * 32).div_ceil(BYTES_PER_SECTOR as u32);
64
65#[cfg(all(unix, feature = "libusb"))]
66const ROOT_DIR_LBA: u32 = RESERVED_SECTORS as u32 + (NUM_FATS as u32 * FAT_SECTORS as u32);
67
68#[cfg(all(unix, feature = "libusb"))]
69const DATA_START_LBA: u32 = ROOT_DIR_LBA + ROOT_DIR_SECTORS;
70
71#[cfg(all(unix, feature = "libusb"))]
73#[derive(Debug, Clone)]
74pub struct FileSpec {
75 pub name: String,
77 pub content: String,
80}
81
82#[cfg(all(unix, feature = "libusb"))]
84pub fn cluster_lba(cluster: u16) -> u32 {
85 DATA_START_LBA + (cluster as u32 - 2) * SECTORS_PER_CLUSTER as u32
86}
87
88#[cfg(all(unix, feature = "libusb"))]
93pub fn build_volume(files: &[FileSpec], total_sectors: u16, volume_label: &str) -> Result<Vec<u8>> {
94 if files.len() > ROOT_ENTRIES as usize {
95 bail!(
96 "{} files do not fit in a {}-entry root directory",
97 files.len(),
98 ROOT_ENTRIES
99 );
100 }
101
102 let total = total_sectors as usize;
103 if (total as u32) <= DATA_START_LBA {
104 bail!(
105 "{} sectors is smaller than the {} sectors of metadata a FAT16 volume needs",
106 total,
107 DATA_START_LBA
108 );
109 }
110
111 let data_clusters = total as u32 - DATA_START_LBA;
112 if !(4085..65525).contains(&data_clusters) {
113 bail!(
114 "{} sectors gives {} data clusters, outside the FAT16 range 4085..65525; a host \
115 would read the FAT with the wrong entry width",
116 total,
117 data_clusters
118 );
119 }
120
121 let mut image = vec![0u8; total * BYTES_PER_SECTOR];
122 write_boot_sector(&mut image, total_sectors, volume_label);
123
124 let mut fat = vec![0u16; (FAT_SECTORS as usize * BYTES_PER_SECTOR) / 2];
126 fat[0] = 0xFFF8;
127 fat[1] = 0xFFFF;
128
129 let mut next_cluster: u16 = 2;
130 for (index, file) in files.iter().enumerate() {
131 let name = encode_8_3(&file.name)?;
132 let data = file.content.as_bytes();
133
134 let clusters_needed = if data.is_empty() {
135 0u32
136 } else {
137 (data.len() as u32).div_ceil(BYTES_PER_SECTOR as u32 * SECTORS_PER_CLUSTER as u32)
138 };
139 if clusters_needed as u64 + next_cluster as u64 > data_clusters as u64 + 2 {
140 bail!(
141 "'{}' does not fit in the remaining {} clusters",
142 file.name,
143 data_clusters as u64 + 2 - next_cluster as u64
144 );
145 }
146
147 let first_cluster = if clusters_needed == 0 {
149 0
150 } else {
151 next_cluster
152 };
153
154 for n in 0..clusters_needed {
155 let cluster = next_cluster;
156 let offset = cluster_lba(cluster) as usize * BYTES_PER_SECTOR;
157 let start = (n as usize) * BYTES_PER_SECTOR * SECTORS_PER_CLUSTER as usize;
158 let end = (start + BYTES_PER_SECTOR * SECTORS_PER_CLUSTER as usize).min(data.len());
159 let chunk = &data[start..end];
160 image
161 .get_mut(offset..offset + chunk.len())
162 .ok_or_else(|| anyhow::anyhow!("'{}' runs past the end of the volume", file.name))?
163 .copy_from_slice(chunk);
164
165 *fat.get_mut(cluster as usize)
166 .ok_or_else(|| anyhow::anyhow!("cluster {} is outside the FAT", cluster))? =
167 if n + 1 == clusters_needed {
168 0xFFFF } else {
170 cluster + 1
171 };
172 next_cluster += 1;
173 }
174
175 let entry_offset = ROOT_DIR_LBA as usize * BYTES_PER_SECTOR + index * 32;
177 let entry = image
178 .get_mut(entry_offset..entry_offset + 32)
179 .ok_or_else(|| anyhow::anyhow!("root directory entry is outside the volume"))?;
180 entry[0..11].copy_from_slice(&name);
181 entry[11] = 0x20; entry[22..24].copy_from_slice(&0u16.to_le_bytes()); entry[24..26].copy_from_slice(&0x5821u16.to_le_bytes()); entry[26..28].copy_from_slice(&first_cluster.to_le_bytes());
185 entry[28..32].copy_from_slice(&(data.len() as u32).to_le_bytes());
186 }
187
188 let fat_bytes: Vec<u8> = fat.iter().flat_map(|e| e.to_le_bytes()).collect();
191 for copy in 0..NUM_FATS as usize {
192 let offset = (RESERVED_SECTORS as usize + copy * FAT_SECTORS as usize) * BYTES_PER_SECTOR;
193 image
194 .get_mut(offset..offset + fat_bytes.len())
195 .ok_or_else(|| anyhow::anyhow!("FAT does not fit in the volume"))?
196 .copy_from_slice(&fat_bytes);
197 }
198
199 Ok(image)
200}
201
202#[cfg(all(unix, feature = "libusb"))]
203fn write_boot_sector(image: &mut [u8], total_sectors: u16, volume_label: &str) {
204 let bs = &mut image[..BYTES_PER_SECTOR];
205
206 bs[0..3].copy_from_slice(&[0xEB, 0x3C, 0x90]); bs[3..11].copy_from_slice(b"MSWIN4.1"); bs[11..13].copy_from_slice(&(BYTES_PER_SECTOR as u16).to_le_bytes());
211 bs[13] = SECTORS_PER_CLUSTER;
212 bs[14..16].copy_from_slice(&RESERVED_SECTORS.to_le_bytes());
213 bs[16] = NUM_FATS;
214 bs[17..19].copy_from_slice(&ROOT_ENTRIES.to_le_bytes());
215 bs[19..21].copy_from_slice(&total_sectors.to_le_bytes());
216 bs[21] = 0xF8; bs[22..24].copy_from_slice(&FAT_SECTORS.to_le_bytes());
218 bs[24..26].copy_from_slice(&32u16.to_le_bytes()); bs[26..28].copy_from_slice(&2u16.to_le_bytes()); bs[28..32].copy_from_slice(&0u32.to_le_bytes()); bs[32..36].copy_from_slice(&0u32.to_le_bytes()); bs[36] = 0x80; bs[37] = 0x00; bs[38] = 0x29; bs[39..43].copy_from_slice(&0x1234_5678u32.to_le_bytes()); bs[43..54].copy_from_slice(&encode_label(volume_label));
229 bs[54..62].copy_from_slice(b"FAT16 "); bs[510] = 0x55;
232 bs[511] = 0xAA;
233}
234
235#[cfg(all(unix, feature = "libusb"))]
237fn encode_label(label: &str) -> [u8; 11] {
238 let mut out = [b' '; 11];
239 for (slot, byte) in out.iter_mut().zip(
240 label
241 .to_ascii_uppercase()
242 .bytes()
243 .filter(|b| b.is_ascii_graphic() || *b == b' '),
244 ) {
245 *slot = byte;
246 }
247 out
248}
249
250#[cfg(all(unix, feature = "libusb"))]
255fn encode_8_3(name: &str) -> Result<[u8; 11]> {
256 let upper = name.trim().to_ascii_uppercase();
257 let (base, ext) = match upper.rsplit_once('.') {
258 Some((b, e)) => (b, e),
259 None => (upper.as_str(), ""),
260 };
261 if base.is_empty() || base.len() > 8 || ext.len() > 3 {
262 bail!(
263 "'{}' is not a valid FAT 8.3 name: at most 8 characters, a dot, and at most 3 more",
264 name
265 );
266 }
267 if !base
268 .bytes()
269 .chain(ext.bytes())
270 .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || b"$%'-_@~`!(){}^#&".contains(&c))
271 {
272 bail!(
273 "'{}' contains characters a FAT 8.3 name cannot hold (letters, digits and \
274 $%'-_@~`!(){{}}^#& only)",
275 name
276 );
277 }
278
279 let mut out = [b' '; 11];
280 out[..base.len()].copy_from_slice(base.as_bytes());
281 out[8..8 + ext.len()].copy_from_slice(ext.as_bytes());
282 Ok(out)
283}