diff --git a/ostool/src/artifact/elf_metadata.rs b/ostool/src/artifact/elf_metadata.rs index a07c77c..6479a72 100644 --- a/ostool/src/artifact/elf_metadata.rs +++ b/ostool/src/artifact/elf_metadata.rs @@ -1,16 +1,19 @@ use anyhow::{Context, bail}; use object::{ - Object, ObjectSegment, ObjectSymbol, - read::elf::{ElfFile, FileHeader, ProgramHeader}, + Object, ObjectSection, ObjectSegment, ObjectSymbol, + read::elf::{ElfFile, FileHeader, ProgramHeader, SectionHeader}, }; pub(crate) struct ElfMetadata { pub(crate) arch: object::Architecture, pub(crate) entry: u64, pub(crate) load_segments: Vec, + /// Allocated sections with file bytes that can appear in a raw binary image. + pub(crate) load_sections: Vec, pub(crate) executable_start: Option, } +#[derive(Debug, PartialEq, Eq)] pub(crate) struct LoadSegment { pub(crate) virtual_address: u64, pub(crate) physical_address: u64, @@ -21,6 +24,14 @@ pub(crate) struct LoadSegment { pub(crate) flags: u32, } +/// A file-backed allocated section, represented in both ELF address spaces. +#[derive(Debug, PartialEq, Eq)] +pub(crate) struct LoadSection { + pub(crate) virtual_address: u64, + pub(crate) file_offset: u64, + pub(crate) size: u64, +} + impl ElfMetadata { pub(crate) fn parse(data: &[u8]) -> anyhow::Result { let file = object::File::parse(data).context("failed to parse ELF")?; @@ -53,6 +64,26 @@ impl ElfMetadata { }) .collect(); + let load_sections = file + .sections() + .filter_map(|section| { + let header = section.elf_section_header(); + let flags: u64 = header.sh_flags(endian).into(); + if flags & u64::from(object::elf::SHF_ALLOC) == 0 + || header.sh_type(endian) == object::elf::SHT_NOBITS + { + return None; + } + + let (file_offset, size) = section.file_range()?; + (size != 0).then_some(LoadSection { + virtual_address: section.address(), + file_offset, + size, + }) + }) + .collect(); + let mut executable_start = None; for symbol in file.symbols() { if symbol.name()? == "__executable_start" && symbol.is_definition() { @@ -65,6 +96,7 @@ impl ElfMetadata { arch: file.architecture(), entry: file.entry(), load_segments, + load_sections, executable_start, }) } diff --git a/ostool/src/boot/fit.rs b/ostool/src/boot/fit.rs index 922576d..a08ac2a 100644 --- a/ostool/src/boot/fit.rs +++ b/ostool/src/boot/fit.rs @@ -10,7 +10,15 @@ use log::{info, warn}; use object::Architecture; use tokio::fs; -use crate::utils::PathResultExt; +use crate::{ + artifact::{ + elf_metadata::ElfMetadata, + runtime::{PreparedRuntimeArtifacts, RuntimeArtifactOptions, prepare_runtime_artifacts}, + }, + boot::fit_config::{FitAddress, FitConfig, FitFormat, FitOs}, + process::ProcessContext, + utils::PathResultExt, +}; const KERNEL_COMPONENT_NAME: &str = "kernel"; const FDT_COMPONENT_NAME: &str = "fdt"; @@ -53,7 +61,191 @@ impl GeneratedFitImage { } } -pub(crate) async fn generate_fit_image(input: FitInput) -> anyhow::Result { +/// Builds a configured payload from one prepared ELF selection. Paths in the +/// configuration are already resolved by the caller; this does not run a guest. +/// Symbols come from the original ELF, even when the runtime copy is stripped. +#[allow(dead_code)] // Consumed by the boot-prepare entry point in the next change. +pub(crate) async fn generate_configured_fit( + context: &ProcessContext, + prepared: &PreparedRuntimeArtifacts, + config: &FitConfig, +) -> anyhow::Result { + let source_data = fs::read(prepared.source_elf()) + .await + .with_path("failed to read source ELF", prepared.source_elf())?; + let metadata = ElfMetadata::parse(&source_data) + .with_context(|| format!("invalid source ELF: {}", prepared.source_elf().display()))?; + validate_elf_load_data(&metadata, source_data.len())?; + fit_arch_name(metadata.arch)?; + + let runtime_data = fs::read(prepared.elf()) + .await + .with_path("failed to read runtime ELF", prepared.elf())?; + let runtime_metadata = ElfMetadata::parse(&runtime_data) + .with_context(|| format!("invalid runtime ELF: {}", prepared.elf().display()))?; + validate_elf_load_data(&runtime_metadata, runtime_data.len())?; + if metadata.arch != runtime_metadata.arch + || metadata.entry != runtime_metadata.entry + || metadata.load_segments != runtime_metadata.load_segments + || metadata.load_sections != runtime_metadata.load_sections + { + anyhow::bail!("source and runtime ELF load layouts differ; prepare the selected ELF again"); + } + let load = config.resolve_load(&metadata)?; + let output = match &config.output { + Some(path) => path.clone(), + None => default_output_path(prepared.elf())?, + }; + // An output alias must not destroy an input needed by subsequent preparation. + if let Ok(destination) = output.canonicalize() { + for input in [ + Some(prepared.source_elf()), + Some(prepared.elf()), + config.fdt.as_ref().map(|fdt| fdt.path.as_path()), + ] + .into_iter() + .flatten() + { + if input.canonicalize().ok().as_ref() == Some(&destination) { + anyhow::bail!("FIT output would overwrite an input: {}", output.display()); + } + } + } + let kernel_data = match config.format { + FitFormat::Elf => runtime_data, + FitFormat::Bin => { + // Derive from this runtime ELF, never reuse an unrelated/stale BIN. + let temporary = tempfile::tempdir().context("failed to stage FIT BIN payload")?; + let binary = prepare_runtime_artifacts( + context, + RuntimeArtifactOptions { + elf_path: prepared.elf().to_path_buf(), + to_bin: true, + bin_dir: Some(temporary.path().to_path_buf()), + debug: false, + cargo_artifact_dir: None, + strip_elf: false, + }, + ) + .with_context(|| { + format!( + "failed to derive FIT BIN payload with llvm-objcopy from {}", + prepared.elf().display() + ) + })?; + fs::read(binary.bin().context("BIN conversion produced no path")?) + .await + .context("failed to read derived FIT BIN payload")? + } + }; + if kernel_data.is_empty() { + anyhow::bail!("cannot build FIT with an empty kernel payload"); + } + // The Linux image header takes precedence over metadata-only inference. + // These branches are exclusive: an explicit entry never reads the header. + let entry = if config.entry == FitAddress::Auto + && config.format == FitFormat::Bin + && config.os == FitOs::Linux + && metadata.arch == Architecture::LoongArch64 + && kernel_data.starts_with(LOONGARCH_IMAGE_MAGIC) + { + resolve_kernel_entry_addr(metadata.arch, &kernel_data, load, metadata.entry)? + } else { + config.resolve_entry(&metadata, load)? + }; + // Replace the output directory entry only after success. In particular, a + // hard link to an input must not truncate that input's shared inode. + let output_dir = output + .parent() + .filter(|parent| !parent.as_os_str().is_empty()) + .unwrap_or(Path::new(".")); + #[cfg(unix)] + let output_permissions = match fs::metadata(&output).await { + Ok(metadata) => Some(metadata.permissions()), + Err(error) if error.kind() == std::io::ErrorKind::NotFound => None, + Err(error) => { + return Err(error).with_path("failed to read FIT output permissions", &output); + } + }; + let builder = tempfile::Builder::new(); + #[cfg(unix)] + let builder = { + use std::os::unix::fs::PermissionsExt; + let mut builder = builder; + // New outputs follow normal file creation (0666 masked by umask). + // Keep replacements private until their existing mode is restored. + if output_permissions.is_none() { + builder.permissions(std::fs::Permissions::from_mode(0o666)); + } + builder + }; + let temporary = builder + .tempfile_in(output_dir) + .with_path("failed to stage FIT output", &output)?; + write_fit_image( + FitInput { + kernel_path: prepared.elf().to_path_buf(), + dtb_path: config.fdt.as_ref().map(|fdt| fdt.path.clone()), + initramfs_path: None, + arch: metadata.arch, + kernel_load_addr: load, + kernel_entry_addr: entry, + fdt_load_addr: config.fdt.as_ref().and_then(|fdt| fdt.load), + output_path: Some(temporary.path().to_path_buf()), + }, + kernel_data, + config.os, + ) + .await?; + #[cfg(unix)] + if let Some(permissions) = output_permissions { + temporary + .as_file() + .set_permissions(permissions) + .with_path("failed to preserve FIT output permissions", &output)?; + } + temporary + .persist(&output) + .map_err(|error| error.error) + .with_path(errors::FIT_SAVE_ERROR, &output)?; + Ok(GeneratedFitImage { path: output }) +} + +// Metadata parsing reads descriptions, not the bytes they describe. Explicit +// addresses bypass inference, but cannot make truncated load data valid. +fn validate_elf_load_data(metadata: &ElfMetadata, file_size: usize) -> anyhow::Result<()> { + let in_file = |offset: u64, size: u64| { + offset + .checked_add(size) + .is_some_and(|end| end <= file_size as u64) + }; + for segment in &metadata.load_segments { + if segment.file_size > segment.memory_size + || !in_file(segment.file_offset, segment.file_size) + || segment + .virtual_address + .checked_add(segment.memory_size) + .is_none() + || segment + .physical_address + .checked_add(segment.memory_size) + .is_none() + { + anyhow::bail!("invalid ELF PT_LOAD data range"); + } + } + for section in &metadata.load_sections { + if !in_file(section.file_offset, section.size) + || section.virtual_address.checked_add(section.size).is_none() + { + anyhow::bail!("invalid ELF allocated section data range"); + } + } + Ok(()) +} + +/// Existing U-Boot path: Linux payload with its historical header handling. +pub(crate) async fn generate_fit_image(mut input: FitInput) -> anyhow::Result { info!("Making FIT image..."); let kernel_data = fs::read(&input.kernel_path) @@ -76,6 +268,15 @@ pub(crate) async fn generate_fit_image(input: FitInput) -> anyhow::Result, + os: FitOs, +) -> anyhow::Result { let arch_name = fit_arch_name(input.arch)?; let dtb_data = if let Some(dtb_path) = &input.dtb_path { let data = fs::read(dtb_path) @@ -105,13 +306,14 @@ pub(crate) async fn generate_fit_image(input: FitInput) -> anyhow::Result None, }; - let config = build_default_fit_config( + let config = build_fit_config( arch_name, + os, kernel_data, dtb_data, initramfs_data, input.kernel_load_addr, - kernel_entry_addr, + input.kernel_entry_addr, input.fdt_load_addr, ); @@ -223,8 +425,10 @@ fn default_output_path(kernel_path: &Path) -> anyhow::Result { Ok(Path::new(output_dir).join(FIT_IMAGE_NAME)) } -fn build_default_fit_config( +#[allow(clippy::too_many_arguments)] +fn build_fit_config( arch_name: &'static str, + os: FitOs, kernel_data: Vec, dtb_data: Option>, initramfs_data: Option>, @@ -237,7 +441,7 @@ fn build_default_fit_config( .with_description("This kernel") .with_type("kernel") .with_arch(arch_name) - .with_os("linux") + .with_os(os.as_str()) .with_compression(false) .with_load_address(kernel_load_addr) .with_entry_point(kernel_entry_addr), @@ -289,7 +493,7 @@ fn build_default_fit_config( #[cfg(test)] mod tests { use super::{ - FitInput, build_default_fit_config, default_output_path, fit_arch_name, + FitInput, FitOs, build_fit_config, default_output_path, fit_arch_name, resolve_kernel_entry_addr, }; use fdt_edit::{Fdt, Node, Property}; @@ -343,8 +547,16 @@ mod tests { #[test] fn default_fit_config_keeps_linux_kernel_defaults_without_dtb() { - let config = - build_default_fit_config("arm64", vec![1, 2, 3], None, None, 0x80000, 0x80000, None); + let config = build_fit_config( + "arm64", + FitOs::Linux, + vec![1, 2, 3], + None, + None, + 0x80000, + 0x80000, + None, + ); let kernel = config.kernel.as_ref().unwrap(); assert_eq!( @@ -368,8 +580,9 @@ mod tests { #[test] fn default_fit_config_adds_optional_fdt_component() { - let config = build_default_fit_config( + let config = build_fit_config( "riscv", + FitOs::Linux, vec![1, 2, 3], Some(vec![4, 5, 6]), None, @@ -389,8 +602,9 @@ mod tests { #[test] fn fit_links_initramfs_without_relocation_for_target_architecture() { - let config = build_default_fit_config( + let config = build_fit_config( "riscv", + FitOs::Linux, vec![1, 2, 3], None, Some(vec![4, 5, 6]), diff --git a/ostool/src/boot/fit_config.rs b/ostool/src/boot/fit_config.rs new file mode 100644 index 0000000..17313a1 --- /dev/null +++ b/ostool/src/boot/fit_config.rs @@ -0,0 +1,383 @@ +//! Typed internal configuration and address resolution for FIT images. + +use std::path::PathBuf; + +use anyhow::{Result, anyhow, bail}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::artifact::elf_metadata::{ElfMetadata, LoadSection, LoadSegment}; + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub(crate) enum FitFormat { + Elf, + #[default] + Bin, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub(crate) enum FitOs { + #[default] + Linux, + Elf, +} + +impl FitOs { + pub(crate) fn as_str(self) -> &'static str { + match self { + Self::Linux => "linux", + Self::Elf => "elf", + } + } +} + +/// An address supplied directly or derived from ELF metadata. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub(crate) enum FitAddress { + #[default] + Auto, + Explicit(u64), +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(deny_unknown_fields)] +pub(crate) struct FitFdt { + pub(crate) path: PathBuf, + pub(crate) load: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(default, deny_unknown_fields)] +pub(crate) struct FitConfig { + pub(crate) format: FitFormat, + pub(crate) os: FitOs, + pub(crate) load: FitAddress, + pub(crate) entry: FitAddress, + pub(crate) fdt: Option, + pub(crate) output: Option, +} + +impl Default for FitConfig { + fn default() -> Self { + Self { + format: FitFormat::Bin, + os: FitOs::Linux, + load: FitAddress::Auto, + entry: FitAddress::Auto, + fdt: None, + output: None, + } + } +} + +impl FitConfig { + /// Resolves the payload load address from the original, unstripped ELF. + pub(crate) fn resolve_load(&self, metadata: &ElfMetadata) -> Result { + if self.os == FitOs::Elf && self.format != FitFormat::Elf { + bail!("FIT OS elf requires format elf and U-Boot CONFIG_BOOTM_ELF"); + } + match self.load { + FitAddress::Explicit(address) => Ok(address), + // An ELF container needs staging memory, independent of its runtime + // sections. Metadata cannot establish a safe region on the board. + FitAddress::Auto if self.os == FitOs::Elf => { + bail!("FIT OS elf requires an explicit load address in safe staging memory") + } + FitAddress::Auto if self.format == FitFormat::Bin => { + let base = bin_load_base(metadata)?; + if metadata + .executable_start + .is_some_and(|address| address != base) + { + bail!( + "cannot derive BIN FIT load address: __executable_start differs from the first payload LMA; set load explicitly" + ); + } + Ok(base) + } + FitAddress::Auto => { + let base = elf_load_base(metadata)?; + if metadata + .executable_start + .is_some_and(|address| address != base) + { + bail!( + "cannot derive ELF FIT load address: __executable_start differs from the ELF container base; set load explicitly" + ); + } + Ok(base) + } + } + } + + /// FIT OS elf passes the ELF container address to bootelf, which reads e_entry + /// itself. Other OS handlers consume an execution address. + pub(crate) fn resolve_entry(&self, metadata: &ElfMetadata, load: u64) -> Result { + if self.os == FitOs::Elf { + if let FitAddress::Explicit(address) = self.entry + && address != load + { + bail!("FIT OS elf entry must equal the ELF payload load address; use entry auto"); + } + return Ok(load); + } + match self.entry { + FitAddress::Explicit(address) => Ok(address), + FitAddress::Auto => direct_entry(metadata, load, self.format), + } + } +} + +fn direct_entry(metadata: &ElfMetadata, load: u64, format: FitFormat) -> Result { + // Raw bytes carry no relocation or address-mapping contract. Only preserve + // e_entry when it still addresses its original bytes without either. + let base = match format { + FitFormat::Bin => bin_load_base(metadata)?, + FitFormat::Elf => elf_load_base(metadata)?, + }; + if load != base { + bail!("cannot derive FIT entry for a relocated payload; set entry explicitly"); + } + let mut found = false; + for section in &metadata.load_sections { + if metadata.entry >= section.virtual_address + && metadata.entry + < checked_end( + section.virtual_address, + section.size, + "section virtual range", + )? + { + if section_lma(section, &metadata.load_segments)? != section.virtual_address { + bail!( + "cannot derive FIT entry: ELF entry VMA differs from its LMA; set entry explicitly" + ); + } + found = true; + } + } + if !found { + bail!( + "cannot derive FIT entry: ELF entry is outside the file-backed payload; set entry explicitly" + ); + } + Ok(metadata.entry) +} + +fn elf_load_base(metadata: &ElfMetadata) -> Result { + let mut bases = metadata + .load_segments + .iter() + .filter(|segment| segment.file_size != 0) + .map(elf_container_base); + let first = bases.next().ok_or_else(|| { + anyhow!("cannot derive ELF FIT load address: no file-backed PT_LOAD segment") + })??; + for base in bases { + let base = base?; + if base != first { + bail!( + "cannot derive ELF FIT load address: PT_LOAD segments have different file bases; set load explicitly" + ); + } + } + Ok(first) +} + +fn elf_container_base(segment: &LoadSegment) -> Result { + valid_segment_range(segment)?; + segment.physical_address.checked_sub(segment.file_offset).ok_or_else(|| { + anyhow!( + "cannot derive ELF FIT load address: PT_LOAD physical address {:#x} precedes file offset {:#x}; set load explicitly", + segment.physical_address, + segment.file_offset + ) + }) +} + +fn bin_load_base(metadata: &ElfMetadata) -> Result { + metadata + .load_sections + .iter() + .map(|section| section_lma(section, &metadata.load_segments)) + .try_fold(None::, |minimum, address| { + let address = address?; + Ok::<_, anyhow::Error>(Some(minimum.map_or(address, |current| current.min(address)))) + })? + .ok_or_else(|| { + anyhow!( + "cannot derive BIN FIT load address: no file-backed allocated section; set load explicitly" + ) + }) +} + +fn section_lma(section: &LoadSection, segments: &[LoadSegment]) -> Result { + let section_file_end = checked_end(section.file_offset, section.size, "section file range")?; + let section_virtual_end = checked_end( + section.virtual_address, + section.size, + "section virtual range", + )?; + let mut candidates = Vec::new(); + + for segment in segments.iter().filter(|segment| segment.file_size != 0) { + valid_segment_range(segment)?; + let segment_file_end = + checked_end(segment.file_offset, segment.file_size, "PT_LOAD file range")?; + let segment_virtual_end = checked_end( + segment.virtual_address, + segment.file_size, + "PT_LOAD virtual range", + )?; + if section.file_offset < segment.file_offset + || section_file_end > segment_file_end + || section.virtual_address < segment.virtual_address + || section_virtual_end > segment_virtual_end + { + continue; + } + + let file_delta = section.file_offset - segment.file_offset; + let virtual_delta = section.virtual_address - segment.virtual_address; + if file_delta != virtual_delta { + continue; + } + candidates.push( + segment + .physical_address + .checked_add(file_delta) + .ok_or_else(|| { + anyhow!("cannot derive BIN FIT load address: PT_LOAD LMA overflow") + })?, + ); + } + + let first = candidates.first().copied().ok_or_else(|| { + anyhow!( + "cannot derive BIN FIT load address: an allocated file-backed section has no matching PT_LOAD segment; set load explicitly" + ) + })?; + if candidates.iter().any(|candidate| *candidate != first) { + bail!( + "cannot derive BIN FIT load address: an allocated file-backed section has ambiguous PT_LOAD LMA mappings; set load explicitly" + ); + } + Ok(first) +} + +fn valid_segment_range(segment: &LoadSegment) -> Result<()> { + checked_end(segment.file_offset, segment.file_size, "PT_LOAD file range")?; + checked_end( + segment.virtual_address, + segment.file_size, + "PT_LOAD virtual range", + )?; + checked_end( + segment.physical_address, + segment.file_size, + "PT_LOAD physical range", + )?; + Ok(()) +} + +fn checked_end(start: u64, size: u64, kind: &str) -> Result { + start + .checked_add(size) + .ok_or_else(|| anyhow!("cannot derive FIT load address: {kind} overflows")) +} + +#[cfg(test)] +mod tests { + use super::*; + use object::Architecture; + + #[test] + fn resolves_payload_addresses_and_rejects_ambiguous_auto_values() { + let mut metadata = ElfMetadata { + arch: Architecture::Riscv64, + entry: 0x8000_0100, + executable_start: None, + load_segments: vec![LoadSegment { + virtual_address: 0x8000_0000, + physical_address: 0x8000_0000, + file_offset: 0x100, + file_size: 0x200, + memory_size: 0x200, + alignment: 1, + flags: 0, + }], + load_sections: vec![LoadSection { + virtual_address: 0x8000_0100, + file_offset: 0x200, + size: 0x20, + }], + }; + let mut config = FitConfig::default(); + let load = config.resolve_load(&metadata).unwrap(); + assert_eq!(load, 0x8000_0100); + assert_eq!( + config.resolve_entry(&metadata, load).unwrap(), + metadata.entry + ); + metadata.executable_start = Some(0x9000_0100); + assert!(config.resolve_load(&metadata).is_err()); + metadata.load_segments[0].virtual_address += 0x1000_0000; + metadata.load_sections[0].virtual_address += 0x1000_0000; + metadata.entry += 0x1000_0000; + assert!(config.resolve_entry(&metadata, load).is_err()); + config.entry = FitAddress::Explicit(0); + config.load = FitAddress::Explicit(0); + assert_eq!(config.resolve_load(&metadata).unwrap(), 0); + assert_eq!(config.resolve_entry(&metadata, 0).unwrap(), 0); + config.load = FitAddress::Auto; + metadata.executable_start = None; + config.format = FitFormat::Elf; + let load = config.resolve_load(&metadata).unwrap(); + assert_eq!(load, 0x7fff_ff00); + // A VMA symbol cannot override the file base, even if it names entry. + metadata.executable_start = Some(metadata.entry); + assert!(config.resolve_load(&metadata).is_err()); + metadata.executable_start = Some(load); + assert_eq!(config.resolve_load(&metadata).unwrap(), load); + config.entry = FitAddress::Auto; + assert!(config.resolve_entry(&metadata, load).is_err()); + config.entry = FitAddress::Explicit(metadata.entry); + assert_eq!( + config.resolve_entry(&metadata, load).unwrap(), + metadata.entry + ); + config.os = FitOs::Elf; + assert!(config.resolve_load(&metadata).is_err()); + config.load = FitAddress::Explicit(load); + config.entry = FitAddress::Auto; + assert_eq!(config.resolve_entry(&metadata, load).unwrap(), load); + config.entry = FitAddress::Explicit(metadata.entry); + assert!(config.resolve_entry(&metadata, load).is_err()); + config.format = FitFormat::Bin; + assert!(config.resolve_load(&metadata).is_err()); + } + + #[test] + fn config_round_trips_json_and_toml_with_explicit_zero_and_high_addresses() { + let config = FitConfig { + format: FitFormat::Elf, + os: FitOs::Elf, + load: FitAddress::Explicit(0), + entry: FitAddress::Explicit(0x9000_0000_8000_0000), + fdt: Some(FitFdt { + path: PathBuf::from("board.dtb"), + load: Some(0), + }), + output: Some(PathBuf::from("boot.fit")), + }; + + let json = serde_json::to_string(&config).unwrap(); + assert_eq!(serde_json::from_str::(&json).unwrap(), config); + + let toml = toml::to_string(&config).unwrap(); + assert_eq!(toml::from_str::(&toml).unwrap(), config); + } +} diff --git a/ostool/src/boot/mod.rs b/ostool/src/boot/mod.rs index 11778a6..b8d4d74 100644 --- a/ostool/src/boot/mod.rs +++ b/ostool/src/boot/mod.rs @@ -2,4 +2,7 @@ pub(crate) mod artifacts; pub(crate) mod fit; +// The configuration entry point is internal until boot preparation consumes it. +#[allow(dead_code)] +pub(crate) mod fit_config; pub mod payload;