diff --git a/EXAMPLES.md b/EXAMPLES.md index 420d06e8..a3a8d2ac 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -67,8 +67,6 @@ Options: Parse UEFI Capsule information from binary file --dump Dump extracted UX capsule bitmap image to a file - --h2o-capsule - Parse UEFI Capsule information from binary file --dump-ec-flash Dump EC flash contents --flash-ec diff --git a/README.md b/README.md index 2d87f6da..a4fcbb83 100644 --- a/README.md +++ b/README.md @@ -90,14 +90,11 @@ On UEFI and FreeBSD raw port I/O is used - on Linux this can also be used as a f - [x] CCG5 PD (11th Gen TigerLake) (`--pd-bin`) - [x] CCG6 PD (Intel systems, Framework Desktop) (`--pd-bin`) - [x] CCG8 PD (AMD Laptops) (`--pd-bin`) - - [x] H2O BIOS Capsule (`--h2o-capsule`) - - [x] BIOS Version - - [x] EC Version - - [x] CCG5/CCG6/CCG8 PD Version - [x] UEFI Capsule (`--capsule`) - - [x] Parse metadata from capsule binary - - [x] Determine type (GUID) of capsule binary - - [x] Extract bitmap image from winux capsule to file + - [x] Determine type (GUID) of capsule binary + - [x] Extract embedded BIOS, EC, and PD versions + - [x] Extract bitmap image from winux capsule to file + - [x] Fallback to raw H2O BIOS files without capsule header - [x] Get firmware version from system (`--versions`) - [x] BIOS - [x] EC diff --git a/completions/bash/framework_tool b/completions/bash/framework_tool index ade3813d..0dfcd869 100755 --- a/completions/bash/framework_tool +++ b/completions/bash/framework_tool @@ -23,7 +23,7 @@ _framework_tool() { case "${cmd}" in framework_tool) - opts="-v -q -t -f -h --flash-gpu-descriptor --verbose --quiet --versions --version --features --esrt --device --compare-version --power --thermal --sensors --fansetduty --fansetrpm --autofanctrl --pdports --info --meinfo --pd-info --pd-reset --pd-disable --pd-enable --dp-hdmi-info --dp-hdmi-update --audio-card-info --privacy --pd-bin --ec-bin --capsule --dump --h2o-capsule --dump-ec-flash --flash-ec --flash-ro-ec --flash-rw-ec --intrusion --inputdeck --inputdeck-mode --expansion-bay --charge-limit --charge-current-limit --charge-rate-limit --get-gpio --fp-led-level --fp-brightness --kblight --remap-key --rgbkbd --ps2-enable --tablet-mode --touchscreen-enable --stylus-battery --console --reboot-ec --ec-hib-delay --uptimeinfo --s0ix-counter --hash --driver --pd-addrs --pd-ports --test --test-retimer --boardid --force --dry-run --flash-gpu-descriptor-file --dump-gpu-descriptor-file --nvidia --host-command --generate-completions --help" + opts="-v -q -t -f -h --flash-gpu-descriptor --verbose --quiet --versions --version --features --esrt --device --compare-version --power --thermal --sensors --fansetduty --fansetrpm --autofanctrl --pdports --info --meinfo --pd-info --pd-reset --pd-disable --pd-enable --dp-hdmi-info --dp-hdmi-update --audio-card-info --privacy --pd-bin --ec-bin --capsule --dump --dump-ec-flash --flash-ec --flash-ro-ec --flash-rw-ec --intrusion --inputdeck --inputdeck-mode --expansion-bay --charge-limit --charge-current-limit --charge-rate-limit --get-gpio --fp-led-level --fp-brightness --kblight --remap-key --rgbkbd --ps2-enable --tablet-mode --touchscreen-enable --stylus-battery --console --reboot-ec --ec-hib-delay --uptimeinfo --s0ix-counter --hash --driver --pd-addrs --pd-ports --test --test-retimer --boardid --force --dry-run --flash-gpu-descriptor-file --dump-gpu-descriptor-file --nvidia --host-command --generate-completions --help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -89,10 +89,6 @@ _framework_tool() { COMPREPLY=($(compgen -f "${cur}")) return 0 ;; - --h2o-capsule) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; --dump-ec-flash) COMPREPLY=($(compgen -f "${cur}")) return 0 diff --git a/completions/fish/framework_tool.fish b/completions/fish/framework_tool.fish index 00a70a97..920ce553 100644 --- a/completions/fish/framework_tool.fish +++ b/completions/fish/framework_tool.fish @@ -20,7 +20,6 @@ complete -c framework_tool -l pd-bin -d 'Parse versions from PD firmware binary complete -c framework_tool -l ec-bin -d 'Parse versions from EC firmware binary file' -r -F complete -c framework_tool -l capsule -d 'Parse UEFI Capsule information from binary file' -r -F complete -c framework_tool -l dump -d 'Dump extracted UX capsule bitmap image to a file' -r -F -complete -c framework_tool -l h2o-capsule -d 'Parse UEFI Capsule information from binary file' -r -F complete -c framework_tool -l dump-ec-flash -d 'Dump EC flash contents' -r -F complete -c framework_tool -l flash-ec -d 'Flash EC (RO+RW) with new firmware from file - may render your hardware unbootable!' -r -F complete -c framework_tool -l flash-ro-ec -d 'Flash EC with new RO firmware from file - may render your hardware unbootable!' -r -F diff --git a/completions/zsh/_framework_tool b/completions/zsh/_framework_tool index 3b80dcdc..7c9631bb 100644 --- a/completions/zsh/_framework_tool +++ b/completions/zsh/_framework_tool @@ -30,7 +30,6 @@ _framework_tool() { '--ec-bin=[Parse versions from EC firmware binary file]:EC_BIN:_files' \ '--capsule=[Parse UEFI Capsule information from binary file]:CAPSULE:_files' \ '--dump=[Dump extracted UX capsule bitmap image to a file]:DUMP:_files' \ -'--h2o-capsule=[Parse UEFI Capsule information from binary file]:H2O_CAPSULE:_files' \ '--dump-ec-flash=[Dump EC flash contents]:DUMP_EC_FLASH:_files' \ '--flash-ec=[Flash EC (RO+RW) with new firmware from file - may render your hardware unbootable!]:FLASH_EC:_files' \ '--flash-ro-ec=[Flash EC with new RO firmware from file - may render your hardware unbootable!]:FLASH_RO_EC:_files' \ diff --git a/framework_lib/src/capsule_content.rs b/framework_lib/src/capsule_content.rs index 81199bda..0c642194 100644 --- a/framework_lib/src/capsule_content.rs +++ b/framework_lib/src/capsule_content.rs @@ -6,6 +6,7 @@ use crate::alloc::string::ToString; use alloc::string::String; +use alloc::vec::Vec; use core::convert::TryInto; use crate::ccgx::binary::{CCG5_PD_LEN, CCG6_PD_LEN, CCG8_PD_LEN}; @@ -51,19 +52,38 @@ pub fn find_ec_in_bios_cap(data: &[u8]) -> Option<&[u8]> { } pub fn find_pd_in_bios_cap(data: &[u8]) -> Option<&[u8]> { - // Just search for the first couple of bytes in PD binaries - // TODO: There's a second one but unless the capsule is bad, we can assume - // they're the same version - let ccg5_needle = &[0x00, 0x20, 0x00, 0x20, 0x11, 0x00]; - let ccg6_needle = &[0x00, 0x40, 0x00, 0x20, 0x11, 0x00]; - let ccg8_needle = &[0x00, 0x80, 0x00, 0x20, 0xAD, 0x0C]; - if let Some(found_pd1) = util::find_sequence(data, ccg5_needle) { - Some(&data[found_pd1..found_pd1 + CCG5_PD_LEN]) - } else if let Some(found_pd1) = util::find_sequence(data, ccg6_needle) { - Some(&data[found_pd1..found_pd1 + CCG6_PD_LEN]) - } else if let Some(found_pd1) = util::find_sequence(data, ccg8_needle) { - Some(&data[found_pd1..found_pd1 + CCG8_PD_LEN]) - } else { - None + find_all_pds_in_bios_cap(data).into_iter().next() +} + +/// PD binary signatures and their corresponding lengths +const CCG5_NEEDLE: &[u8] = &[0x00, 0x20, 0x00, 0x20, 0x11, 0x00]; +const CCG6_NEEDLE: &[u8] = &[0x00, 0x40, 0x00, 0x20, 0x11, 0x00]; +const CCG8_NEEDLE: &[u8] = &[0x00, 0x80, 0x00, 0x20, 0xAD, 0x0C]; + +/// Find all PD firmware binaries embedded in a BIOS capsule +pub fn find_all_pds_in_bios_cap(data: &[u8]) -> Vec<&[u8]> { + let mut results = Vec::new(); + + // Search for CCG5 PDs + for offset in util::find_all_sequences(data, CCG5_NEEDLE) { + if offset + CCG5_PD_LEN <= data.len() { + results.push(&data[offset..offset + CCG5_PD_LEN]); + } } + + // Search for CCG6 PDs + for offset in util::find_all_sequences(data, CCG6_NEEDLE) { + if offset + CCG6_PD_LEN <= data.len() { + results.push(&data[offset..offset + CCG6_PD_LEN]); + } + } + + // Search for CCG8 PDs + for offset in util::find_all_sequences(data, CCG8_NEEDLE) { + if offset + CCG8_PD_LEN <= data.len() { + results.push(&data[offset..offset + CCG8_PD_LEN]); + } + } + + results } diff --git a/framework_lib/src/commandline/clap_std.rs b/framework_lib/src/commandline/clap_std.rs index ba1cc43f..ff4fc53e 100644 --- a/framework_lib/src/commandline/clap_std.rs +++ b/framework_lib/src/commandline/clap_std.rs @@ -135,10 +135,6 @@ struct ClapCli { #[arg(long)] dump: Option, - /// Parse UEFI Capsule information from binary file - #[arg(long)] - h2o_capsule: Option, - /// Dump EC flash contents #[arg(long)] dump_ec_flash: Option, @@ -491,9 +487,6 @@ pub fn parse(args: &[String]) -> Cli { .capsule .map(|x| x.into_os_string().into_string().unwrap()), dump: args.dump.map(|x| x.into_os_string().into_string().unwrap()), - h2o_capsule: args - .h2o_capsule - .map(|x| x.into_os_string().into_string().unwrap()), dump_ec_flash: args .dump_ec_flash .map(|x| x.into_os_string().into_string().unwrap()), diff --git a/framework_lib/src/commandline/mod.rs b/framework_lib/src/commandline/mod.rs index 477f13a7..46a50529 100644 --- a/framework_lib/src/commandline/mod.rs +++ b/framework_lib/src/commandline/mod.rs @@ -28,7 +28,7 @@ use crate::built_info; use crate::camera::check_camera_version; use crate::capsule; use crate::capsule_content::{ - find_bios_version, find_ec_in_bios_cap, find_pd_in_bios_cap, find_retimer_version, + find_all_pds_in_bios_cap, find_bios_version, find_ec_in_bios_cap, find_retimer_version, }; use crate::ccgx::device::{FwMode, PdController, PdPort}; #[cfg(feature = "hidapi")] @@ -184,7 +184,6 @@ pub struct Cli { pub ec_bin: Option, pub capsule: Option, pub dump: Option, - pub h2o_capsule: Option, pub dump_ec_flash: Option, pub flash_ec: Option, pub flash_ro_ec: Option, @@ -272,7 +271,6 @@ pub fn parse(args: &[String]) -> Cli { ec_bin: cli.ec_bin, capsule: cli.capsule, dump: cli.dump, - h2o_capsule: cli.h2o_capsule, // dump_ec_flash // flash_ec // flash_ro_ec @@ -1678,41 +1676,30 @@ pub fn run_with_args(args: &Cli, _allupdate: bool) -> i32 { } } } else { - println!("Capsule is invalid."); - } - } - } else if let Some(capsule_path) = &args.h2o_capsule { - #[cfg(feature = "uefi")] - let data = crate::fw_uefi::fs::shell_read_file(capsule_path); - #[cfg(not(feature = "uefi"))] - let data = match fs::read(capsule_path) { - Ok(data) => Some(data), - // TODO: Perhaps a more user-friendly error - Err(e) => { - println!("Error {:?}", e); - None - } - }; - - if let Some(data) = data { - println!("File"); - println!(" Size: {:>20} B", data.len()); - println!(" Size: {:>20} KB", data.len() / 1024); - if let Some(cap) = find_bios_version(&data) { - println!(" BIOS Platform:{:>18}", cap.platform); - println!(" BIOS Version: {:>18}", cap.version); - } - if let Some(ec_bin) = find_ec_in_bios_cap(&data) { - debug!("Found EC binary in BIOS capsule"); - analyze_ec_fw(ec_bin); - } else { - debug!("Didn't find EC binary in BIOS capsule"); - } - if let Some(pd_bin) = find_pd_in_bios_cap(&data) { - debug!("Found PD binary in BIOS capsule"); - analyze_ccgx_pd_fw(pd_bin); - } else { - debug!("Didn't find PD binary in BIOS capsule"); + // No valid capsule header - try to extract embedded firmware directly + // This handles raw H2O BIOS files that aren't wrapped in a UEFI capsule + println!("No valid capsule header, searching for embedded firmware..."); + let mut found_any = false; + if let Some(cap) = find_bios_version(&data) { + found_any = true; + println!("BIOS"); + println!(" Platform: {:>18}", cap.platform); + println!(" Version: {:>18}", cap.version); + } + if let Some(ec_bin) = find_ec_in_bios_cap(&data) { + found_any = true; + println!("Embedded EC"); + analyze_ec_fw(ec_bin); + } + let pd_bins = find_all_pds_in_bios_cap(&data); + for (i, pd_bin) in pd_bins.iter().enumerate() { + found_any = true; + println!("Embedded PD {}", i + 1); + analyze_ccgx_pd_fw(pd_bin); + } + if !found_any { + println!("No embedded firmware found."); + } } } } else if let Some(dump_path) = &args.dump_ec_flash { @@ -1829,7 +1816,6 @@ Options: --ec-bin Parse versions from EC firmware binary file --capsule Parse UEFI Capsule information from binary file --dump Dump extracted UX capsule bitmap image to a file - --h2o-capsule Parse UEFI Capsule information from binary file --dump-ec-flash Dump EC flash contents --flash-ec Flash EC with new firmware from file --flash-ro-ec Flash EC with new firmware from file @@ -2333,53 +2319,128 @@ pub fn analyze_capsule(data: &[u8]) -> Option { let header = capsule::parse_capsule_header(data)?; capsule::print_capsule_header(&header); - match GUID::from(header.capsule_guid) { - esrt::TGL_BIOS_GUID => { - println!(" Type: Framework TGL Insyde BIOS"); + let guid_kind = esrt::match_guid_kind(&header.capsule_guid); + match guid_kind { + esrt::FrameworkGuidKind::TglBios => { + println!(" Type: Framework 13 TGL Insyde BIOS"); + } + esrt::FrameworkGuidKind::AdlBios => { + println!(" Type: Framework 13 ADL Insyde BIOS"); + } + esrt::FrameworkGuidKind::RplBios => { + println!(" Type: Framework 13 RPL Insyde BIOS"); + } + esrt::FrameworkGuidKind::MtlBios => { + println!(" Type: Framework 13 MTL Insyde BIOS"); + } + esrt::FrameworkGuidKind::Fw12RplBios => { + println!(" Type: Framework 12 RPL Insyde BIOS"); + } + esrt::FrameworkGuidKind::Fl16Bios => { + println!(" Type: Framework 16 AMD Insyde BIOS"); + } + esrt::FrameworkGuidKind::Amd16Ai300Bios => { + println!(" Type: Framework 16 AMD AI 300 Insyde BIOS"); + } + esrt::FrameworkGuidKind::Amd13Ryzen7040Bios => { + println!(" Type: Framework 13 AMD Ryzen 7040 Insyde BIOS"); + } + esrt::FrameworkGuidKind::Amd13Ai300Bios => { + println!(" Type: Framework 13 AMD AI 300 Insyde BIOS"); + } + esrt::FrameworkGuidKind::DesktopAmdAi300Bios => { + println!(" Type: Framework Desktop AMD AI 300 Insyde BIOS"); + } + esrt::FrameworkGuidKind::TglRetimer01 => { + println!(" Type: Framework TGL Retimer01 (Right)"); } - esrt::ADL_BIOS_GUID => { - println!(" Type: Framework ADL Insyde BIOS"); + esrt::FrameworkGuidKind::TglRetimer23 => { + println!(" Type: Framework TGL Retimer23 (Left)"); } - esrt::RPL_BIOS_GUID => { - println!(" Type: Framework RPL Insyde BIOS"); + esrt::FrameworkGuidKind::AdlRetimer01 => { + println!(" Type: Framework ADL Retimer01 (Right)"); } - esrt::TGL_RETIMER01_GUID => { - println!(" Type: Framework TGL Retimer01 (Right)"); + esrt::FrameworkGuidKind::AdlRetimer23 => { + println!(" Type: Framework ADL Retimer23 (Left)"); } - esrt::TGL_RETIMER23_GUID => { - println!(" Type: Framework TGL Retimer23 (Left)"); + esrt::FrameworkGuidKind::RplRetimer01 => { + println!(" Type: Framework RPL Retimer01 (Right)"); } - esrt::ADL_RETIMER01_GUID => { - println!(" Type: Framework ADL Retimer01 (Right)"); + esrt::FrameworkGuidKind::RplRetimer23 => { + println!(" Type: Framework RPL Retimer23 (Left)"); } - esrt::ADL_RETIMER23_GUID => { - println!(" Type: Framework ADL Retimer23 (Left)"); + esrt::FrameworkGuidKind::MtlRetimer01 => { + println!(" Type: Framework MTL Retimer01 (Right)"); } - esrt::RPL_RETIMER01_GUID => { - println!(" Type: Framework RPL Retimer01 (Right)"); + esrt::FrameworkGuidKind::MtlRetimer23 => { + println!(" Type: Framework MTL Retimer23 (Left)"); } - esrt::RPL_RETIMER23_GUID => { - println!(" Type: Framework RPL Retimer23 (Left)"); + esrt::FrameworkGuidKind::RplCsme => { + println!(" Type: Framework RPL CSME"); } - esrt::WINUX_GUID => { - println!(" Type: Windows UX capsule"); + esrt::FrameworkGuidKind::RplUCsme => { + println!(" Type: Framework RPL-U CSME"); + } + esrt::FrameworkGuidKind::MtlCsme => { + println!(" Type: Framework MTL CSME"); + } + esrt::FrameworkGuidKind::WinUx => { + println!(" Type: Windows UX capsule"); let ux_header = capsule::parse_ux_header(data); capsule::print_ux_header(&ux_header); } - _ => { + esrt::FrameworkGuidKind::Unknown => { println!(" Type: Unknown"); } } - match esrt::match_guid_kind(&header.capsule_guid) { + // Extract retimer version if this is a retimer capsule + match guid_kind { esrt::FrameworkGuidKind::TglRetimer01 | esrt::FrameworkGuidKind::TglRetimer23 | esrt::FrameworkGuidKind::AdlRetimer01 | esrt::FrameworkGuidKind::AdlRetimer23 | esrt::FrameworkGuidKind::RplRetimer01 - | esrt::FrameworkGuidKind::RplRetimer23 => { + | esrt::FrameworkGuidKind::RplRetimer23 + | esrt::FrameworkGuidKind::MtlRetimer01 + | esrt::FrameworkGuidKind::MtlRetimer23 => { if let Some(ver) = find_retimer_version(data) { - println!(" Version: {:>18?}", ver); + println!(" Retimer Version: {:>15}", ver); + } + } + _ => {} + } + + // Extract embedded firmware versions for BIOS capsules + match guid_kind { + esrt::FrameworkGuidKind::TglBios + | esrt::FrameworkGuidKind::AdlBios + | esrt::FrameworkGuidKind::RplBios + | esrt::FrameworkGuidKind::MtlBios + | esrt::FrameworkGuidKind::Fw12RplBios + | esrt::FrameworkGuidKind::Fl16Bios + | esrt::FrameworkGuidKind::Amd16Ai300Bios + | esrt::FrameworkGuidKind::Amd13Ryzen7040Bios + | esrt::FrameworkGuidKind::Amd13Ai300Bios + | esrt::FrameworkGuidKind::DesktopAmdAi300Bios => { + if let Some(cap) = find_bios_version(data) { + println!("BIOS"); + println!(" Platform: {:>18}", cap.platform); + println!(" Version: {:>18}", cap.version); + } + if let Some(ec_bin) = find_ec_in_bios_cap(data) { + println!("Embedded EC"); + if let Some(ver) = ec_binary::read_ec_version(ec_bin, true) { + println!(" RO Version: {:>18}", ver.version); + } + if let Some(ver) = ec_binary::read_ec_version(ec_bin, false) { + println!(" RW Version: {:>18}", ver.version); + } + } + let pd_bins = find_all_pds_in_bios_cap(data); + for (i, pd_bin) in pd_bins.iter().enumerate() { + println!("Embedded PD {}", i + 1); + analyze_ccgx_pd_fw(pd_bin); } } _ => {} diff --git a/framework_lib/src/commandline/uefi.rs b/framework_lib/src/commandline/uefi.rs index d08e6ddd..f7b6c2c4 100644 --- a/framework_lib/src/commandline/uefi.rs +++ b/framework_lib/src/commandline/uefi.rs @@ -62,7 +62,6 @@ pub fn parse(args: &[String]) -> Cli { flash_rw_ec: None, capsule: None, dump: None, - h2o_capsule: None, intrusion: false, inputdeck: false, inputdeck_mode: None, @@ -623,14 +622,6 @@ pub fn parse(args: &[String]) -> Cli { None }; found_an_option = true; - } else if arg == "--h2o-capsule" { - cli.h2o_capsule = if args.len() > i + 1 { - Some(args[i + 1].clone()) - } else { - println!("--h2o-capsule requires extra argument to denote input file"); - None - }; - found_an_option = true; } else if arg == "--dump-ec-flash" { cli.dump_ec_flash = if args.len() > i + 1 { Some(args[i + 1].clone()) diff --git a/framework_lib/src/util.rs b/framework_lib/src/util.rs index cdae531d..2ab91d29 100644 --- a/framework_lib/src/util.rs +++ b/framework_lib/src/util.rs @@ -258,6 +258,22 @@ pub fn find_sequence(haystack: &[u8], needle: &[u8]) -> Option { .position(|window| window == needle) } +/// Find all occurrences of a sequence of bytes in a long slice of bytes +pub fn find_all_sequences(haystack: &[u8], needle: &[u8]) -> Vec { + let mut results = Vec::new(); + let mut pos = 0; + while pos + needle.len() <= haystack.len() { + if let Some(offset) = find_sequence(&haystack[pos..], needle) { + let absolute_pos = pos + offset; + results.push(absolute_pos); + pos = absolute_pos + needle.len(); + } else { + break; + } + } + results +} + /// Assert length of an EC response from the windows driver /// It's always 20 more than expected. TODO: Figure out why pub fn assert_win_len(left: N, right: N) { diff --git a/framework_lib/test_bins/Framework_Laptop_13_Intel_Core_Ultra_Series1_CSME_Signed_9D6.cap b/framework_lib/test_bins/Framework_Laptop_13_Intel_Core_Ultra_Series1_CSME_Signed_9D6.cap new file mode 100644 index 00000000..ef7e4d4c Binary files /dev/null and b/framework_lib/test_bins/Framework_Laptop_13_Intel_Core_Ultra_Series1_CSME_Signed_9D6.cap differ diff --git a/framework_lib/test_bins/Framework_Laptop_13_Intel_Core_Ultra_Series1_Retimer_port01_624.cap b/framework_lib/test_bins/Framework_Laptop_13_Intel_Core_Ultra_Series1_Retimer_port01_624.cap new file mode 100644 index 00000000..cd391618 Binary files /dev/null and b/framework_lib/test_bins/Framework_Laptop_13_Intel_Core_Ultra_Series1_Retimer_port01_624.cap differ diff --git a/framework_lib/test_bins/Framework_Laptop_13_Intel_Core_Ultra_Series1_capsule_signed_allsku_3.06.cap b/framework_lib/test_bins/Framework_Laptop_13_Intel_Core_Ultra_Series1_capsule_signed_allsku_3.06.cap new file mode 100644 index 00000000..75bad779 Binary files /dev/null and b/framework_lib/test_bins/Framework_Laptop_13_Intel_Core_Ultra_Series1_capsule_signed_allsku_3.06.cap differ diff --git a/framework_lib/test_bins/Framework_Laptop_16_Ryzen7040_capsule_signed_allsku_4.02.cap b/framework_lib/test_bins/Framework_Laptop_16_Ryzen7040_capsule_signed_allsku_4.02.cap new file mode 100644 index 00000000..32de86a2 Binary files /dev/null and b/framework_lib/test_bins/Framework_Laptop_16_Ryzen7040_capsule_signed_allsku_4.02.cap differ diff --git a/framework_lib/test_bins/sparsify_capsule.py b/framework_lib/test_bins/sparsify_capsule.py new file mode 100755 index 00000000..32470c75 --- /dev/null +++ b/framework_lib/test_bins/sparsify_capsule.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python3 +""" +Sparsify UEFI capsule files for efficient git storage. + +This script zeroes out sections of capsule files that aren't needed for testing, +while preserving the file structure and all test-relevant data. The resulting +files compress extremely well with git's zlib compression (typically 99% reduction). + +Preserved sections: +- Capsule header (first 64 bytes) +- $BVDT section (BIOS version info) +- $_IFLASH_EC_IMG_ section + EC binary +- All embedded PD firmware (CCG5, CCG6, CCG8) +- $_RETIMER_PARAM_ section (retimer version) + +Usage: + ./sparsify_capsule.py input.cap # Creates input.sparse.cap + ./sparsify_capsule.py input.cap output.cap # Specify output path + ./sparsify_capsule.py *.cap # Process multiple files +""" + +import argparse +import gzip +import os +import sys + + +def find_all(data: bytes, needle: bytes) -> list[int]: + """Find all occurrences of needle in data.""" + results = [] + pos = 0 + while (found := data.find(needle, pos)) != -1: + results.append(found) + pos = found + len(needle) + return results + + +def sparsify_capsule(data: bytes) -> tuple[bytearray, list[tuple[int, int, str]]]: + """ + Zero out sections we don't need while preserving test-relevant data. + + Returns: + Tuple of (sparsified data, list of preserved regions) + """ + preserved = [] + + # Always preserve capsule header (64 bytes to be safe) + preserved.append((0, 64, "Capsule header")) + + # Find and preserve $BVDT section (BIOS version) + bvdt = data.find(b'$BVDT') + if bvdt != -1: + preserved.append((bvdt, 128, "$BVDT (BIOS version)")) + + # Find and preserve $_IFLASH_EC_IMG_ + EC binary (128KB) + ec_marker = data.find(b'$_IFLASH_EC_IMG_') + if ec_marker != -1: + # Marker + offset + EC binary (128KB) + preserved.append((ec_marker, 16 + 9 + 131072, "$_IFLASH_EC_IMG_ + EC")) + + # Find and preserve all CCG8 PD binaries (~262KB each) + CCG8_NEEDLE = bytes([0x00, 0x80, 0x00, 0x20, 0xAD, 0x0C]) + CCG8_SIZE = 262144 + for i, offset in enumerate(find_all(data, CCG8_NEEDLE), 1): + preserved.append((offset, CCG8_SIZE, f"CCG8 PD {i}")) + + # Find and preserve all CCG6 PD binaries (~64KB each) + CCG6_NEEDLE = bytes([0x00, 0x40, 0x00, 0x20, 0x11, 0x00]) + CCG6_SIZE = 65536 + for i, offset in enumerate(find_all(data, CCG6_NEEDLE), 1): + preserved.append((offset, CCG6_SIZE, f"CCG6 PD {i}")) + + # Find and preserve all CCG5 PD binaries (~32KB each) + CCG5_NEEDLE = bytes([0x00, 0x20, 0x00, 0x20, 0x11, 0x00]) + CCG5_SIZE = 32768 + for i, offset in enumerate(find_all(data, CCG5_NEEDLE), 1): + preserved.append((offset, CCG5_SIZE, f"CCG5 PD {i}")) + + # Find and preserve $_RETIMER_PARAM_ section + retimer = data.find(b'$_RETIMER_PARAM_') + if retimer != -1: + preserved.append((retimer, 64, "$_RETIMER_PARAM_")) + + # Sort by offset + preserved.sort(key=lambda x: x[0]) + + # Create zeroed version with only preserved regions + result = bytearray(len(data)) + for offset, length, _ in preserved: + end = min(offset + length, len(data)) + result[offset:end] = data[offset:end] + + return result, preserved + + +def process_file(input_path: str, output_path: str | None = None, verbose: bool = True) -> None: + """Process a single capsule file.""" + if output_path is None: + base, ext = os.path.splitext(input_path) + output_path = f"{base}.sparse{ext}" + + # Read input + with open(input_path, 'rb') as f: + data = f.read() + + original_size = len(data) + + # Sparsify + sparse_data, preserved = sparsify_capsule(data) + + # Write output + with open(output_path, 'wb') as f: + f.write(sparse_data) + + if verbose: + # Calculate compression stats + compressed = gzip.compress(bytes(sparse_data), compresslevel=9) + + print(f"{os.path.basename(input_path)}:") + print(f" Original size: {original_size:>12,} bytes") + print(f" Preserved regions: {len(preserved)}") + for offset, length, desc in preserved: + print(f" {offset:>10} - {offset+length:>10} ({length:>7} bytes): {desc}") + print(f" Compressed size: {len(compressed):>12,} bytes ({100*len(compressed)/original_size:.2f}%)") + print(f" Output: {output_path}") + print() + + +def main(): + parser = argparse.ArgumentParser( + description="Sparsify UEFI capsule files for efficient git storage.", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=__doc__ + ) + parser.add_argument( + 'input', nargs='+', + help='Input capsule file(s)' + ) + parser.add_argument( + '-o', '--output', + help='Output path (only valid with single input file)' + ) + parser.add_argument( + '-q', '--quiet', action='store_true', + help='Suppress verbose output' + ) + + args = parser.parse_args() + + if args.output and len(args.input) > 1: + print("Error: --output can only be used with a single input file", file=sys.stderr) + sys.exit(1) + + for input_path in args.input: + if not os.path.exists(input_path): + print(f"Error: {input_path} not found", file=sys.stderr) + continue + + output_path = args.output if len(args.input) == 1 else None + process_file(input_path, output_path, verbose=not args.quiet) + + +if __name__ == '__main__': + main()