// DCR — Cargo-like C/C++ project manager. // // Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . use crate::core::build_config::ArchiveConfig; use fatfs::{FileSystem, FormatVolumeOptions, format_volume}; use glob::glob; use std::fs::{self, File, OpenOptions}; use std::io::{self, Read, Seek, SeekFrom, Write}; use std::path::Path; /// File wrapper that shifts all seek offsets (used to skip the boot sector on a FAT image). struct OffsetFile { inner: T, offset: u64, } impl Read for OffsetFile { fn read(&mut self, buf: &mut [u8]) -> io::Result { self.inner.read(buf) } } impl Write for OffsetFile { fn write(&mut self, buf: &[u8]) -> io::Result { self.inner.write(buf) } fn flush(&mut self) -> io::Result<()> { self.inner.flush() } } impl Seek for OffsetFile { fn seek(&mut self, pos: SeekFrom) -> io::Result { match pos { SeekFrom::Start(n) => { let actual = self.inner.seek(SeekFrom::Start(n + self.offset))?; if actual < self.offset { Ok(0) } else { Ok(actual - self.offset) } } SeekFrom::Current(n) => { let actual = self.inner.seek(SeekFrom::Current(n))?; if actual < self.offset { Ok(0) } else { Ok(actual - self.offset) } } SeekFrom::End(n) => { let actual = self.inner.seek(SeekFrom::End(n))?; if actual < self.offset { Ok(0) } else { Ok(actual - self.offset) } } } } } /// Parses a size string like `1.5m` or `500k` into bytes (1024-based; empty → 0). fn parse_size(s: &str) -> Result { let s = s.trim(); if s.is_empty() { return Ok(0); } let mut num_str = String::new(); let mut unit = String::new(); for c in s.chars() { if c.is_ascii_digit() || c == '.' { num_str.push(c); } else { unit.push(c); } } let val: f64 = num_str .parse() .map_err(|_| format!("invalid size number: {num_str}"))?; let unit = unit.trim().to_lowercase(); let multiplier = match unit.as_str() { "k" | "kb" => 1024.0, "m" | "mb" => 1024.0 * 1024.0, "g" | "gb" => 1024.0 * 1024.0 * 1024.0, "" => 1.0, _ => return Err(format!("unknown unit: {unit}")), }; Ok((val * multiplier) as u64) } /// Creates a FAT image from layout config, substituting `{profile}` and optional boot sector. pub fn pack_archive( project_root: &Path, config: &ArchiveConfig, profile: &str, ) -> Result<(), String> { let output_substituted = config.output.replace("{profile}", profile); let output_path = project_root.join(&output_substituted); if let Some(parent) = output_path.parent() { fs::create_dir_all(parent) .map_err(|e| format!("failed to create output parent dir: {e}"))?; } let size = if let Some(sz_str) = &config.size { parse_size(sz_str)? } else { 1474560 }; let offset = if let Some(off_str) = &config.offset { parse_size(off_str)? } else { 0 }; let mut file = OpenOptions::new() .read(true) .write(true) .create(true) .truncate(false) .open(&output_path) .map_err(|e| format!("failed to open output file: {e}"))?; let current_len = file.metadata().map(|m| m.len()).unwrap_or(0); if current_len < size { file.set_len(size) .map_err(|e| format!("failed to set image file size: {e}"))?; } if offset == 0 && let Some(boot_rel) = &config.bootsector { // Load and write bootsector if present let boot_rel_substituted = boot_rel.replace("{profile}", profile); let boot_path = project_root.join(&boot_rel_substituted); if boot_path.is_file() { let mut boot_file = File::open(&boot_path).map_err(|e| format!("failed to open bootsector: {e}"))?; let mut boot_bytes = vec![0; 512]; let _ = boot_file .read(&mut boot_bytes) .map_err(|e| format!("failed to read bootsector: {e}"))?; file.seek(SeekFrom::Start(0)) .map_err(|e| format!("failed to seek to start: {e}"))?; file.write_all(&boot_bytes) .map_err(|e| format!("failed to write bootsector: {e}"))?; } } file.seek(SeekFrom::Start(0)) .map_err(|e| format!("failed to seek to start: {e}"))?; let mut wrapper = OffsetFile { inner: file, offset, }; let label = config.label.as_deref().unwrap_or("VOLUME"); let mut label_bytes = [b' '; 11]; let bytes_to_copy = label.len().min(11); label_bytes[..bytes_to_copy].copy_from_slice(&label.as_bytes()[..bytes_to_copy]); let fat_type = match config.format.to_lowercase().as_str() { "fat12" => Some(fatfs::FatType::Fat12), "fat16" => Some(fatfs::FatType::Fat16), "fat32" => Some(fatfs::FatType::Fat32), _ => None, }; let mut options = FormatVolumeOptions::new().volume_label(label_bytes); if let Some(t) = fat_type { options = options.fat_type(t); } format_volume(&mut wrapper, options) .map_err(|e| format!("failed to format FAT volume: {e}"))?; wrapper .seek(SeekFrom::Start(0)) .map_err(|e| format!("failed to seek to start after format: {e}"))?; let fs = FileSystem::new(wrapper, fatfs::FsOptions::new()) .map_err(|e| format!("failed to mount FAT fs: {e}"))?; let root_dir = fs.root_dir(); for layout in &config.layout { let from_substituted = layout.from.replace("{profile}", profile); let from_pattern = project_root .join(&from_substituted) .to_string_lossy() .to_string(); let paths = if from_pattern.contains('*') || from_pattern.contains('?') { let mut matched = Vec::new(); if let Ok(entries) = glob(&from_pattern) { for path in entries.flatten() { matched.push(path); } } matched } else { let p = project_root.join(&from_substituted); if p.exists() { vec![p] } else { Vec::new() } }; for path in paths { if !path.is_file() { continue; } let filename = path.file_name().and_then(|n| n.to_str()).unwrap_or(""); if filename.is_empty() { continue; } let mut target_filename = filename.to_string(); let mut target_dir_str = layout.to.trim_matches('/'); let is_glob = from_pattern.contains('*') || from_pattern.contains('?'); if !is_glob && !layout.to.ends_with('/') && let Some(last_part) = Path::new(&layout.to).file_name().and_then(|n| n.to_str()) && last_part.contains('.') { target_filename = last_part.to_string(); let parent_path = Path::new(&layout.to).parent().unwrap_or(Path::new("")); target_dir_str = parent_path.to_str().unwrap_or("").trim_matches('/'); } let mut current_dir = root_dir.clone(); if !target_dir_str.is_empty() { let parts = target_dir_str.split('/'); for part in parts { if part.is_empty() { continue; } current_dir = match current_dir.open_dir(part) { Ok(dir) => dir, Err(_) => current_dir .create_dir(part) .map_err(|e| format!("failed to create dir {part}: {e}"))?, }; } } let mut out_file = current_dir .create_file(&target_filename) .map_err(|e| format!("failed to create file {target_filename}: {e}"))?; let mut in_file = File::open(&path).map_err(|e| format!("failed to open input file: {e}"))?; let mut buffer = Vec::new(); in_file .read_to_end(&mut buffer) .map_err(|e| format!("failed to read input file: {e}"))?; out_file .write_all(&buffer) .map_err(|e| format!("failed to write to FAT file {target_filename}: {e}"))?; } } Ok(()) }