├── .gitignore ├── apple-nvram ├── LICENSE ├── Cargo.toml └── src │ ├── mtd.rs │ ├── lib.rs │ ├── v1v2.rs │ └── v3.rs ├── asahi-bless ├── LICENSE ├── Cargo.toml ├── offsets.c └── src │ ├── main.rs │ └── lib.rs ├── asahi-btsync ├── LICENSE ├── etc │ ├── udev │ │ └── rules.d │ │ │ └── 96-asahi-btsync.rules │ └── systemd │ │ └── system │ │ └── asahi-btsync.service ├── Cargo.toml └── src │ ├── dbus.rs │ └── main.rs ├── asahi-nvram ├── LICENSE ├── Cargo.toml └── src │ └── main.rs ├── asahi-wifisync ├── LICENSE ├── Cargo.toml └── src │ └── main.rs ├── README.md ├── Cargo.toml ├── LICENSE └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /apple-nvram/LICENSE: -------------------------------------------------------------------------------- 1 | ../LICENSE -------------------------------------------------------------------------------- /asahi-bless/LICENSE: -------------------------------------------------------------------------------- 1 | ../LICENSE -------------------------------------------------------------------------------- /asahi-btsync/LICENSE: -------------------------------------------------------------------------------- 1 | ../LICENSE -------------------------------------------------------------------------------- /asahi-nvram/LICENSE: -------------------------------------------------------------------------------- 1 | ../LICENSE -------------------------------------------------------------------------------- /asahi-wifisync/LICENSE: -------------------------------------------------------------------------------- 1 | ../LICENSE -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # asahi-nvram 2 | Nvram reader/writer for arm macs. May or may not result in you having to perform a dfu restore. -------------------------------------------------------------------------------- /asahi-btsync/etc/udev/rules.d/96-asahi-btsync.rules: -------------------------------------------------------------------------------- 1 | SUBSYSTEM=="mtd", ATTR{name}=="nvram", DRIVERS=="apple-spi", TAG+="systemd", ENV{SYSTEMD_WANTS}="asahi-btsync.service" 2 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | members = [ 4 | "apple-nvram", 5 | "asahi-nvram", 6 | "asahi-btsync", 7 | "asahi-bless", 8 | "asahi-wifisync" 9 | ] 10 | resolver = "2" 11 | 12 | 13 | [profile.release] 14 | lto = true 15 | panic = "abort" 16 | -------------------------------------------------------------------------------- /asahi-btsync/etc/systemd/system/asahi-btsync.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Apple silicon Bluetooth device sync 3 | After=bluetooth.service 4 | Requisite=bluetooth.service 5 | 6 | [Service] 7 | Type=oneshot 8 | ExecStartPre=/usr/bin/sleep 2s 9 | ExecStart=/usr/bin/asahi-btsync sync 10 | 11 | -------------------------------------------------------------------------------- /apple-nvram/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "apple-nvram" 3 | version = "0.3.0" 4 | edition = "2021" 5 | license = "MIT" 6 | description = "A library to parse and write apple-formatted nvram entries" 7 | homepage = "https://github.com/WhatAmISupposedToPutHere/asahi-nvram" 8 | repository = "https://github.com/WhatAmISupposedToPutHere/asahi-nvram" 9 | 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [dependencies] 14 | adler32 = "1" 15 | crc32fast = "1.3.2" 16 | nix = "0.26" 17 | -------------------------------------------------------------------------------- /asahi-nvram/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "asahi-nvram" 3 | version = "0.2.3" 4 | edition = "2021" 5 | license = "MIT" 6 | description = "A tool to read and write nvram variables on ARM Macs" 7 | homepage = "https://github.com/WhatAmISupposedToPutHere/asahi-nvram" 8 | repository = "https://github.com/WhatAmISupposedToPutHere/asahi-nvram" 9 | 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [dependencies.apple-nvram] 14 | path = "../apple-nvram" 15 | version = "0.3" 16 | 17 | [dependencies.clap] 18 | version = "3" 19 | features = ["cargo"] 20 | 21 | -------------------------------------------------------------------------------- /asahi-bless/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "asahi-bless" 3 | version = "0.4.2" 4 | edition = "2021" 5 | license = "MIT" 6 | description = "A tool to select active boot partition on ARM Macs" 7 | homepage = "https://github.com/WhatAmISupposedToPutHere/asahi-nvram" 8 | repository = "https://github.com/WhatAmISupposedToPutHere/asahi-nvram" 9 | 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [dependencies] 14 | uuid = "1" 15 | gpt = "3" 16 | clap = { version = "4.4.11", features = ["derive"] } 17 | 18 | [dependencies.apple-nvram] 19 | path = "../apple-nvram" 20 | version = "0.3" 21 | -------------------------------------------------------------------------------- /asahi-wifisync/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "asahi-wifisync" 3 | license = "MIT" 4 | version = "0.2.2" 5 | edition = "2021" 6 | 7 | description = "A tool to sync Wifi passwords with macos on ARM Macs" 8 | homepage = "https://github.com/WhatAmISupposedToPutHere/asahi-nvram" 9 | repository = "https://github.com/WhatAmISupposedToPutHere/asahi-nvram" 10 | 11 | [dependencies] 12 | rust-ini = "0.21" 13 | 14 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 15 | 16 | [dependencies.apple-nvram] 17 | path = "../apple-nvram" 18 | version = "0.3" 19 | 20 | [dependencies.clap] 21 | version = "3" 22 | features = ["cargo"] 23 | 24 | -------------------------------------------------------------------------------- /asahi-btsync/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "asahi-btsync" 3 | license = "MIT" 4 | version = "0.2.4" 5 | edition = "2021" 6 | description = "A tool to sync Bluetooth pairing keys with macos on ARM Macs" 7 | homepage = "https://github.com/WhatAmISupposedToPutHere/asahi-nvram" 8 | repository = "https://github.com/WhatAmISupposedToPutHere/asahi-nvram" 9 | 10 | 11 | [dependencies] 12 | dbus = "0.9.7" 13 | regex = "1.11.1" 14 | rust-ini = "0.21" 15 | 16 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 17 | 18 | [dependencies.apple-nvram] 19 | path = "../apple-nvram" 20 | version = "0.3" 21 | 22 | [dependencies.clap] 23 | version = "3" 24 | features = ["cargo"] 25 | 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 The Asahi Linux Contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /apple-nvram/src/mtd.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io::{Seek, SeekFrom, Write}, 3 | os::unix::io::AsRawFd, 4 | }; 5 | 6 | use crate::NvramWriter; 7 | 8 | impl NvramWriter for T 9 | where 10 | T: Seek + Write + AsRawFd, 11 | { 12 | fn erase_if_needed(&mut self, offset: u32, size: usize) { 13 | if unsafe { mtd_mem_get_info(self.as_raw_fd(), &mut MtdInfoUser::default()) }.is_err() { 14 | return; 15 | } 16 | let erase_info = EraseInfoUser { 17 | start: offset, 18 | length: size as u32, 19 | }; 20 | unsafe { 21 | mtd_mem_erase(self.as_raw_fd(), &erase_info).unwrap(); 22 | } 23 | } 24 | 25 | fn write_all(&mut self, offset: u32, buf: &[u8]) -> std::io::Result<()> { 26 | self.seek(SeekFrom::Start(offset as u64))?; 27 | self.write_all(buf)?; 28 | 29 | Ok(()) 30 | } 31 | } 32 | 33 | #[repr(C)] 34 | pub struct EraseInfoUser { 35 | start: u32, 36 | length: u32, 37 | } 38 | 39 | #[repr(C)] 40 | #[derive(Default)] 41 | pub struct MtdInfoUser { 42 | ty: u8, 43 | flags: u32, 44 | size: u32, 45 | erasesize: u32, 46 | writesize: u32, 47 | oobsize: u32, 48 | padding: u64, 49 | } 50 | 51 | nix::ioctl_write_ptr!(mtd_mem_erase, b'M', 2, EraseInfoUser); 52 | nix::ioctl_read!(mtd_mem_get_info, b'M', 1, MtdInfoUser); 53 | -------------------------------------------------------------------------------- /apple-nvram/src/lib.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | use std::{ 3 | borrow::Cow, 4 | fmt::{Debug, Display, Formatter}, 5 | }; 6 | 7 | pub mod mtd; 8 | 9 | pub mod v1v2; 10 | pub mod v3; 11 | 12 | fn chrp_checksum_add(lhs: u8, rhs: u8) -> u8 { 13 | let (out, carry) = lhs.overflowing_add(rhs); 14 | if carry { 15 | out + 1 16 | } else { 17 | out 18 | } 19 | } 20 | 21 | fn slice_rstrip<'a, T: PartialEq>(mut ts: &'a [T], t: &T) -> &'a [T] { 22 | while let Some(last) = ts.last() { 23 | if last == t { 24 | ts = ts.split_last().unwrap().1; 25 | } else { 26 | break; 27 | } 28 | } 29 | ts 30 | } 31 | 32 | fn slice_find>(ts: &[T], t: &T) -> Option { 33 | let mut ret = None; 34 | for (i, v) in ts.iter().enumerate() { 35 | if v == t { 36 | ret = Some(i); 37 | break; 38 | } 39 | } 40 | ret 41 | } 42 | 43 | #[derive(Debug)] 44 | pub enum Error { 45 | ParseError, 46 | SectionTooBig, 47 | ApplyError(std::io::Error), 48 | } 49 | 50 | type Result = std::result::Result; 51 | 52 | #[derive(Clone, Copy, PartialEq)] 53 | pub enum VarType { 54 | Common, 55 | System, 56 | } 57 | 58 | impl Display for VarType { 59 | fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { 60 | match self { 61 | &VarType::Common => write!(f, "common"), 62 | &VarType::System => write!(f, "system"), 63 | } 64 | } 65 | } 66 | 67 | pub fn nvram_parse<'a>(nvr: &'a [u8]) -> Result + 'a>> { 68 | match (v3::Nvram::parse(nvr), v1v2::Nvram::parse(nvr)) { 69 | (Ok(nvram_v3), Err(_)) => Ok(Box::new(nvram_v3)), 70 | (Err(_), Ok(nvram_v1v2)) => Ok(Box::new(nvram_v1v2)), 71 | _ => Err(Error::ParseError), 72 | } 73 | } 74 | 75 | pub trait NvramWriter { 76 | fn erase_if_needed(&mut self, offset: u32, size: usize); 77 | fn write_all(&mut self, offset: u32, buf: &[u8]) -> std::io::Result<()>; 78 | } 79 | 80 | pub trait Nvram<'a> { 81 | fn prepare_for_write(&mut self); 82 | fn active_part_mut(&mut self) -> &mut dyn Partition<'a>; 83 | fn partitions(&self) -> Box> + '_>; 84 | fn serialize(&self) -> Result>; 85 | fn apply(&mut self, w: &mut dyn NvramWriter) -> Result<()>; 86 | } 87 | 88 | pub trait Partition<'a>: Display { 89 | fn variables(&self) -> Box> + '_>; 90 | fn get_variable(&self, key: &[u8], typ: VarType) -> Option<&dyn Variable<'a>>; 91 | fn insert_variable(&mut self, key: &[u8], value: Cow<'a, [u8]>, typ: VarType); 92 | fn remove_variable(&mut self, key: &[u8], typ: VarType); 93 | } 94 | 95 | pub trait Variable<'a>: Display { 96 | fn value(&self) -> Cow<'a, [u8]>; 97 | } 98 | -------------------------------------------------------------------------------- /asahi-btsync/src/dbus.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | // TODO: not architecture independent but correct for arm/ppc/x86 4 | const SIGUSR1: i32 = 10; 5 | 6 | // busctl call org.freedesktop.systemd1 /org/freedesktop/systemd1 org.freedesktop.systemd1.Manager KillUnit ssi bluetooth.service main 10 7 | 8 | pub fn systemd_reload_bt_config() -> Result<(), Box> { 9 | let con = dbus::blocking::Connection::new_system()?; 10 | 11 | let systemd1 = con.with_proxy( 12 | "org.freedesktop.systemd1", 13 | "/org/freedesktop/systemd1", 14 | Duration::from_millis(1000), 15 | ); 16 | 17 | // send USR1 signal to bluetoothd service to trigger config reload 18 | let r: Result<(), _> = systemd1.method_call( 19 | "org.freedesktop.systemd1.Manager", 20 | "KillUnit", 21 | ("bluetooth.service", "main", SIGUSR1), 22 | ); 23 | r?; 24 | Ok(()) 25 | } 26 | 27 | fn bluez_connect_device( 28 | con: &dbus::blocking::Connection, 29 | adapter_path: &str, 30 | mac: &[u8; 6], 31 | ) -> Result<(), Box> { 32 | let dev_mac = format!( 33 | "{:02X}_{:02X}_{:02X}_{:02X}_{:02X}_{:02X}", 34 | mac[0], mac[1], mac[2], mac[3], mac[4], mac[5] 35 | ); 36 | let dev_path = format!("{}/dev_{}", adapter_path, dev_mac); 37 | println!("connect BT dev {}", dev_path); 38 | 39 | let device1 = con.with_proxy("org.bluez", dev_path, Duration::from_secs(10)); 40 | 41 | // busctl call org.bluez /org/bluez/hci0/dev_$BTADDR org.bluez.Device1 Connect 42 | let r: Result<(), _> = device1.method_call("org.bluez.Device1", "Connect", ()); 43 | r?; 44 | Ok(()) 45 | } 46 | 47 | pub fn bluez_connect(info: &crate::BtInfo) -> Result<(), Box> { 48 | let con = dbus::blocking::Connection::new_system()?; 49 | 50 | let adapter_mac = format!( 51 | "{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}", 52 | info.mac[0], info.mac[1], info.mac[2], info.mac[3], info.mac[4], info.mac[5] 53 | ); 54 | 55 | let bluez1 = con.with_proxy("org.bluez", "/org/bluez", Duration::from_secs(2)); 56 | let introspect = bluez1.introspect()?; 57 | use dbus::blocking::stdintf::org_freedesktop_dbus::Introspectable; 58 | 59 | // println!("bluez introspect:\n{}", introspect); 60 | 61 | use regex::Regex; 62 | let re = Regex::new(r#""#).unwrap(); 63 | 64 | for (_, [adapter]) in re.captures_iter(introspect.as_str()).map(|c| c.extract()) { 65 | let adapter_path = format!("/org/bluez/{}", adapter); 66 | 67 | let adapter1 = con.with_proxy("org.bluez", &adapter_path, Duration::from_secs(2)); 68 | use dbus::blocking::stdintf::org_freedesktop_dbus::Properties; 69 | 70 | // busctl get-property org.bluez /org/bluez/hci0 org.bluez.Adapter1 Address 71 | let bt_addr: String = adapter1.get("org.bluez.Adapter1", "Address")?; 72 | 73 | if bt_addr.eq(adapter_mac.as_str()) { 74 | for dev in &info.devices { 75 | bluez_connect_device(&con, &adapter_path, &dev.mac)?; 76 | } 77 | return Ok(()); 78 | } 79 | } 80 | Ok(()) 81 | } 82 | -------------------------------------------------------------------------------- /asahi-wifisync/src/main.rs: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: MIT */ 2 | 3 | use std::{ 4 | env, 5 | fmt::Debug, 6 | fs::OpenOptions, 7 | io::{self, Read}, 8 | path::Path, 9 | }; 10 | 11 | use apple_nvram::{nvram_parse, VarType, Variable}; 12 | 13 | use ini::Ini; 14 | 15 | #[derive(Debug)] 16 | #[allow(dead_code)] 17 | enum Error { 18 | Parse, 19 | SectionTooBig, 20 | ApplyError(std::io::Error), 21 | VariableNotFound, 22 | FileIO, 23 | IWDConfigDirNotFound, 24 | } 25 | 26 | impl From for Error { 27 | fn from(e: apple_nvram::Error) -> Self { 28 | match e { 29 | apple_nvram::Error::ParseError => Error::Parse, 30 | apple_nvram::Error::SectionTooBig => Error::SectionTooBig, 31 | apple_nvram::Error::ApplyError(e) => Error::ApplyError(e), 32 | } 33 | } 34 | } 35 | 36 | impl From for Error { 37 | fn from(_e: io::Error) -> Self { 38 | Error::FileIO 39 | } 40 | } 41 | 42 | type Result = std::result::Result; 43 | 44 | fn main() { 45 | real_main().unwrap(); 46 | } 47 | 48 | fn real_main() -> Result<()> { 49 | let matches = clap::command!() 50 | .arg(clap::arg!(-d --device [DEVICE] "Path to the nvram device.")) 51 | .subcommand(clap::Command::new("list").about("Parse shared wlan keys from nvram")) 52 | .subcommand( 53 | clap::Command::new("sync") 54 | .about("Sync wlan information from nvram") 55 | .arg(clap::arg!(-c --config [CONFIG] "IWD config path.")), 56 | ) 57 | .get_matches(); 58 | 59 | let default_name = "/dev/mtd/by-name/nvram".to_owned(); 60 | let default_config = "/var/lib/iwd".to_owned(); 61 | let wlan_var = "preferred-networks"; 62 | 63 | let mut file = OpenOptions::new() 64 | .read(true) 65 | .open(matches.get_one::("device").unwrap_or(&default_name)) 66 | .unwrap(); 67 | let mut data = Vec::new(); 68 | file.read_to_end(&mut data).unwrap(); 69 | let mut nv = nvram_parse(&data)?; 70 | let active = nv.active_part_mut(); 71 | let wlan_devs = active 72 | .get_variable(wlan_var.as_bytes(), VarType::System) 73 | .ok_or(Error::VariableNotFound)?; 74 | 75 | match matches.subcommand() { 76 | Some(("list", _args)) => { 77 | print_wlankeys(wlan_devs).expect("Failed to parse wlan device info"); 78 | } 79 | Some(("sync", args)) => { 80 | sync_wlankeys( 81 | wlan_devs, 82 | args.get_one::("config").unwrap_or(&default_config), 83 | ) 84 | .expect("Failed to sync wlan device info"); 85 | } 86 | _ => { 87 | print_wlankeys(wlan_devs).expect("Failed to parse wlan device info"); 88 | } 89 | } 90 | Ok(()) 91 | } 92 | 93 | struct Network { 94 | ssid: String, 95 | psk: Option>, 96 | } 97 | 98 | const CHUNK_LEN: usize = 0xc0; 99 | 100 | fn parse_wlan_info(var: &dyn Variable) -> Vec { 101 | let mut nets = Vec::new(); 102 | let data = var.value(); 103 | for chunk in data.chunks(CHUNK_LEN) { 104 | let ssid_len = u32::from_le_bytes(chunk[0xc..0x10].try_into().unwrap()) as usize; 105 | let ssid = String::from_utf8_lossy(&chunk[0x10..0x10 + ssid_len]).to_string(); 106 | let secure = u32::from_le_bytes(chunk[0x8..0xc].try_into().unwrap()) != 0; 107 | let psk = if secure { 108 | Some(chunk[0xa0..0xc0].to_owned()) 109 | } else { 110 | None 111 | }; 112 | nets.push(Network { ssid, psk }); 113 | } 114 | 115 | nets 116 | } 117 | 118 | fn format_psk(psk: &[u8]) -> String { 119 | psk.iter() 120 | .map(|x| format!("{x:02x}")) 121 | .collect::>() 122 | .join("") 123 | } 124 | 125 | fn print_wlankeys(var: &dyn Variable) -> Result<()> { 126 | let info = parse_wlan_info(var); 127 | 128 | for network in info { 129 | let psk_str = if let Some(psk) = network.psk { 130 | format!("PSK {}", format_psk(&psk)) 131 | } else { 132 | "Open".to_owned() 133 | }; 134 | println!("SSID {}, {}", network.ssid, psk_str); 135 | } 136 | Ok(()) 137 | } 138 | 139 | fn sync_wlankeys(var: &dyn Variable, config: &String) -> Result<()> { 140 | let config_path = Path::new(config); 141 | 142 | if !config_path.is_dir() { 143 | return Err(Error::IWDConfigDirNotFound); 144 | } 145 | let nets = parse_wlan_info(var); 146 | 147 | for net in nets { 148 | let suffix = if net.psk.is_some() { ".psk" } else { ".open" }; 149 | let net_path = config_path.join(format!("{}{}", net.ssid, suffix)); 150 | 151 | if net_path.exists() { 152 | continue; 153 | } 154 | 155 | let mut info = Ini::new(); 156 | if let Some(psk) = net.psk { 157 | info.with_section(Some("Security")) 158 | .set("PreSharedKey", format_psk(&psk)); 159 | } 160 | info.write_to_file(net_path)?; 161 | } 162 | Ok(()) 163 | } 164 | -------------------------------------------------------------------------------- /asahi-nvram/src/main.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | use std::{borrow::Cow, fs::OpenOptions, io::Read, process::ExitCode}; 3 | 4 | use apple_nvram::{nvram_parse, VarType}; 5 | 6 | #[derive(Debug)] 7 | #[allow(dead_code)] 8 | enum Error { 9 | Parse, 10 | SectionTooBig, 11 | ApplyError(std::io::Error), 12 | MissingPartitionName, 13 | MissingValue, 14 | VariableNotFound, 15 | UnknownPartition, 16 | InvalidHex, 17 | } 18 | 19 | impl From for Error { 20 | fn from(e: apple_nvram::Error) -> Self { 21 | match e { 22 | apple_nvram::Error::ParseError => Error::Parse, 23 | apple_nvram::Error::SectionTooBig => Error::SectionTooBig, 24 | apple_nvram::Error::ApplyError(e) => Error::ApplyError(e), 25 | } 26 | } 27 | } 28 | 29 | type Result = std::result::Result; 30 | 31 | fn main() -> ExitCode { 32 | match real_main() { 33 | Ok(_) => ExitCode::SUCCESS, 34 | Err(e) => { 35 | eprintln!("{:?}", e); 36 | ExitCode::FAILURE 37 | } 38 | } 39 | } 40 | 41 | fn real_main() -> Result<()> { 42 | let matches = clap::command!() 43 | .arg(clap::arg!(-d --device [DEVICE] "Path to the nvram device.")) 44 | .subcommand( 45 | clap::Command::new("read") 46 | .about("Read nvram variables") 47 | .arg(clap::Arg::new("variable").multiple_values(true)), 48 | ) 49 | .subcommand( 50 | clap::Command::new("delete") 51 | .about("Delete nvram variables") 52 | .arg(clap::Arg::new("variable").multiple_values(true)), 53 | ) 54 | .subcommand( 55 | clap::Command::new("write") 56 | .about("Write nvram variables") 57 | .arg(clap::Arg::new("variable=value").multiple_values(true)), 58 | ) 59 | .get_matches(); 60 | let default_name = "/dev/mtd/by-name/nvram".to_owned(); 61 | let mut file = OpenOptions::new() 62 | .read(true) 63 | .write(true) 64 | .open(matches.get_one::("device").unwrap_or(&default_name)) 65 | .unwrap(); 66 | let mut data = Vec::new(); 67 | file.read_to_end(&mut data).unwrap(); 68 | let mut nv = nvram_parse(&data)?; 69 | match matches.subcommand() { 70 | Some(("read", args)) => { 71 | let active = nv.active_part_mut(); 72 | 73 | let vars = args.get_many::("variable"); 74 | if let Some(vars) = vars { 75 | for var in vars { 76 | let (part, name) = var.split_once(':').ok_or(Error::MissingPartitionName)?; 77 | let typ = part_by_name(part)?; 78 | let v = active 79 | .get_variable(name.as_bytes(), typ) 80 | .ok_or(Error::VariableNotFound)?; 81 | println!("{}", v); 82 | } 83 | } else { 84 | for var in active.variables() { 85 | println!("{}", var); 86 | } 87 | } 88 | } 89 | Some(("write", args)) => { 90 | let vars = args.get_many::("variable=value"); 91 | nv.prepare_for_write(); 92 | let active = nv.active_part_mut(); 93 | for var in vars.unwrap_or_default() { 94 | let (key, value) = var.split_once('=').ok_or(Error::MissingValue)?; 95 | let (part, name) = key.split_once(':').ok_or(Error::MissingPartitionName)?; 96 | let typ = part_by_name(part)?; 97 | active.insert_variable(name.as_bytes(), Cow::Owned(read_var(value)?), typ); 98 | } 99 | nv.apply(&mut file)?; 100 | } 101 | Some(("delete", args)) => { 102 | let vars = args.get_many::("variable"); 103 | nv.prepare_for_write(); 104 | let active = nv.active_part_mut(); 105 | for var in vars.unwrap_or_default() { 106 | let (part, name) = var.split_once(':').ok_or(Error::MissingPartitionName)?; 107 | let typ = part_by_name(part)?; 108 | active.remove_variable(name.as_bytes(), typ); 109 | } 110 | nv.apply(&mut file)?; 111 | } 112 | _ => {} 113 | } 114 | Ok(()) 115 | } 116 | 117 | fn part_by_name(name: &str) -> Result { 118 | match name { 119 | "common" => Ok(VarType::Common), 120 | "system" => Ok(VarType::System), 121 | _ => Err(Error::UnknownPartition), 122 | } 123 | } 124 | 125 | fn read_var(val: &str) -> Result> { 126 | let val = val.as_bytes(); 127 | let mut ret = Vec::new(); 128 | let mut i = 0; 129 | while i < val.len() { 130 | if val[i] == b'%' { 131 | ret.push( 132 | u8::from_str_radix( 133 | unsafe { std::str::from_utf8_unchecked(&val[i + 1..i + 3]) }, 134 | 16, 135 | ) 136 | .map_err(|_| Error::InvalidHex)?, 137 | ); 138 | i += 2; 139 | } else { 140 | ret.push(val[i]) 141 | } 142 | i += 1; 143 | } 144 | Ok(ret) 145 | } 146 | -------------------------------------------------------------------------------- /asahi-bless/offsets.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #define MAX_CKSUM_SIZE 8 6 | 7 | typedef uint64_t oid_t; 8 | typedef uint64_t xid_t; 9 | 10 | typedef struct obj_phys { 11 | uint8_t o_cksum[MAX_CKSUM_SIZE]; 12 | oid_t o_oid; 13 | xid_t o_xid; 14 | uint32_t o_type; 15 | uint32_t o_subtype; 16 | } obj_phys_t; 17 | 18 | typedef int64_t paddr_t; 19 | 20 | typedef struct prange { 21 | paddr_t pr_start_paddr; 22 | uint64_t pr_block_count; 23 | } prange_t; 24 | 25 | typedef unsigned char uuid_t[16]; 26 | 27 | #define NX_MAX_FILE_SYSTEMS 100 28 | 29 | typedef enum { 30 | NX_CNTR_OBJ_CKSUM_SET = 0, 31 | NX_CNTR_OBJ_CKSUM_FAIL = 1, 32 | NX_NUM_COUNTERS = 32 33 | } nx_counter_id_t; 34 | 35 | #define NX_EPH_INFO_COUNT 4 36 | 37 | typedef struct nx_superblock { 38 | obj_phys_t nx_o; 39 | uint32_t nx_magic; 40 | uint32_t nx_block_size; 41 | uint64_t nx_block_count; 42 | uint64_t nx_features; 43 | uint64_t nx_readonly_compatible_features; 44 | uint64_t nx_incompatible_features; 45 | uuid_t nx_uuid; 46 | oid_t nx_next_oid; 47 | xid_t nx_next_xid; 48 | uint32_t nx_xp_desc_blocks; 49 | uint32_t nx_xp_data_blocks; 50 | paddr_t nx_xp_desc_base; 51 | paddr_t nx_xp_data_base; 52 | uint32_t nx_xp_desc_next; 53 | uint32_t nx_xp_data_next; 54 | uint32_t nx_xp_desc_index; 55 | uint32_t nx_xp_desc_len; 56 | uint32_t nx_xp_data_index; 57 | uint32_t nx_xp_data_len; 58 | oid_t nx_spaceman_oid; 59 | oid_t nx_omap_oid; 60 | oid_t nx_reaper_oid; 61 | uint32_t nx_test_type; 62 | uint32_t nx_max_file_systems; 63 | oid_t nx_fs_oid[NX_MAX_FILE_SYSTEMS]; 64 | uint64_t nx_counters[NX_NUM_COUNTERS]; 65 | prange_t nx_blocked_out_prange; 66 | oid_t nx_evict_mapping_tree_oid; 67 | uint64_t nx_flags; 68 | paddr_t nx_efi_jumpstart; 69 | uuid_t nx_fusion_uuid; 70 | prange_t nx_keylocker; 71 | uint64_t nx_ephemeral_info[NX_EPH_INFO_COUNT]; 72 | oid_t nx_test_oid; 73 | oid_t nx_fusion_mt_oid; 74 | oid_t nx_fusion_wbc_oid; 75 | prange_t nx_fusion_wbc; 76 | uint64_t nx_newest_mounted_version; 77 | prange_t nx_mkb_locker; 78 | } nx_superblock_t; 79 | 80 | typedef struct omap_phys { 81 | obj_phys_t om_o; 82 | uint32_t om_flags; 83 | uint32_t om_snap_count; 84 | uint32_t om_tree_type; 85 | uint32_t om_snapshot_tree_type; 86 | oid_t om_tree_oid; 87 | oid_t om_snapshot_tree_oid; 88 | xid_t om_most_recent_snap; 89 | xid_t om_pending_revert_min; 90 | xid_t om_pending_revert_max; 91 | } omap_phys_t; 92 | 93 | typedef struct nloc { 94 | uint16_t off; 95 | uint16_t len; 96 | } nloc_t; 97 | 98 | typedef struct btree_node_phys { 99 | obj_phys_t btn_o; 100 | uint16_t btn_flags; 101 | uint16_t btn_level; 102 | uint32_t btn_nkeys; 103 | nloc_t btn_table_space; 104 | nloc_t btn_free_space; 105 | nloc_t btn_key_free_list; 106 | nloc_t btn_val_free_list; 107 | uint64_t btn_data[]; 108 | } btree_node_phys_t; 109 | 110 | typedef struct btree_info_fixed { 111 | uint32_t bt_flags; 112 | uint32_t bt_node_size; 113 | uint32_t bt_key_size; 114 | uint32_t bt_val_size; 115 | } btree_info_fixed_t; 116 | 117 | typedef struct btree_info { 118 | btree_info_fixed_t bt_fixed; 119 | uint32_t bt_longest_key; 120 | uint32_t bt_longest_val; 121 | uint64_t bt_key_count; 122 | uint64_t bt_node_count; 123 | } btree_info_t; 124 | 125 | typedef uint32_t crypto_flags_t; 126 | typedef uint32_t cp_key_class_t; 127 | typedef uint32_t cp_key_os_version_t; 128 | typedef uint16_t cp_key_revision_t; 129 | 130 | struct wrapped_meta_crypto_state { 131 | uint16_t major_version; 132 | uint16_t minor_version; 133 | crypto_flags_t cpflags; 134 | cp_key_class_t persistent_class; 135 | cp_key_os_version_t key_os_version; 136 | cp_key_revision_t key_revision; 137 | uint16_t unused; 138 | } __attribute__((aligned(2), packed)); 139 | typedef struct wrapped_meta_crypto_state wrapped_meta_crypto_state_t; 140 | 141 | #define APFS_MODIFIED_NAMELEN 32 142 | 143 | typedef struct apfs_modified_by { 144 | uint8_t id[APFS_MODIFIED_NAMELEN]; 145 | uint64_t timestamp; 146 | xid_t last_xid; 147 | } apfs_modified_by_t; 148 | 149 | #define APFS_MAX_HIST 8 150 | #define APFS_VOLNAME_LEN 256 151 | 152 | typedef struct apfs_superblock { 153 | obj_phys_t apfs_o; 154 | uint32_t apfs_magic; 155 | uint32_t apfs_fs_index; 156 | uint64_t apfs_features; 157 | uint64_t apfs_readonly_compatible_features; 158 | uint64_t apfs_incompatible_features; 159 | uint64_t apfs_unmount_time; 160 | uint64_t apfs_fs_reserve_block_count; 161 | uint64_t apfs_fs_quota_block_count; 162 | uint64_t apfs_fs_alloc_count; 163 | wrapped_meta_crypto_state_t apfs_meta_crypto; 164 | uint32_t apfs_root_tree_type; 165 | uint32_t apfs_extentref_tree_type; 166 | uint32_t apfs_snap_meta_tree_type; 167 | oid_t apfs_omap_oid; 168 | oid_t apfs_root_tree_oid; 169 | oid_t apfs_extentref_tree_oid; 170 | oid_t apfs_snap_meta_tree_oid; 171 | xid_t apfs_revert_to_xid; 172 | oid_t apfs_revert_to_sblock_oid; 173 | uint64_t apfs_next_obj_id; 174 | uint64_t apfs_num_files; 175 | uint64_t apfs_num_directories; 176 | uint64_t apfs_num_symlinks; 177 | uint64_t apfs_num_other_fsobjects; 178 | uint64_t apfs_num_snapshots; 179 | uint64_t apfs_total_blocks_alloced; 180 | uint64_t apfs_total_blocks_freed; 181 | uuid_t apfs_vol_uuid; 182 | uint64_t apfs_last_mod_time; 183 | uint64_t apfs_fs_flags; 184 | apfs_modified_by_t apfs_formatted_by; 185 | apfs_modified_by_t apfs_modified_by[APFS_MAX_HIST]; 186 | uint8_t apfs_volname[APFS_VOLNAME_LEN]; 187 | uint32_t apfs_next_doc_id; 188 | uint16_t apfs_role; 189 | uint16_t reserved; 190 | xid_t apfs_root_to_xid; 191 | oid_t apfs_er_state_oid; 192 | uint64_t apfs_cloneinfo_id_epoch; 193 | uint64_t apfs_cloneinfo_xid; 194 | oid_t apfs_snap_meta_ext_oid; 195 | uuid_t apfs_volume_group_id; 196 | oid_t apfs_integrity_meta_oid; 197 | oid_t apfs_fext_tree_oid; 198 | uint32_t apfs_fext_tree_type; 199 | uint32_t reserved_type; 200 | oid_t reserved_oid; 201 | } apfs_superblock_t; 202 | 203 | int main() { 204 | printf("%ld\n", offsetof(apfs_superblock_t, apfs_role)); 205 | } 206 | -------------------------------------------------------------------------------- /asahi-bless/src/main.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #![allow(dead_code)] 3 | use asahi_bless::{get_boot_candidates, get_boot_volume, set_boot_volume, clear_next_boot, BootCandidate, Error, Volume}; 4 | use clap::Parser; 5 | use std::{ 6 | io::{stdin, stdout, Write}, 7 | num::IntErrorKind, 8 | process::ExitCode, 9 | }; 10 | 11 | #[cfg(target_os = "macos")] 12 | compile_error!("asahi-bless will only work on linux, if you are on macos, use system `bless` instead"); 13 | 14 | type Result = std::result::Result; 15 | 16 | #[derive(Parser)] 17 | #[command(version)] 18 | struct Args { 19 | #[arg( 20 | short, 21 | long, 22 | help = "Path to the nvram device." 23 | )] 24 | device: Option, 25 | 26 | #[arg( 27 | short, 28 | long, 29 | help = "Set boot volume for next boot only" 30 | )] 31 | next: bool, 32 | 33 | #[arg( 34 | short, 35 | long, 36 | conflicts_with_all = &["set_boot", "set_boot_macos"], 37 | help = "List boot volume candidates" 38 | )] 39 | list_volumes: bool, 40 | 41 | #[arg(long, value_name = "name_or_index", help = "Set boot volume by name or index")] 42 | set_boot: Option, 43 | 44 | #[arg( 45 | long, 46 | conflicts_with = "set_boot", 47 | help = "Set boot volume to macOS if unambiguous" 48 | )] 49 | set_boot_macos: bool, 50 | 51 | #[arg(name = "yes", short, long, help = "Do not ask for confirmation")] 52 | autoconfirm: bool, 53 | 54 | #[arg(long, help = "Get currently selected boot target. May be combined with --next to show the next boot target.")] 55 | get_boot: bool, 56 | 57 | #[arg(long, help = "Clear the selected next boot target")] 58 | clear_next: bool, 59 | } 60 | 61 | fn error_to_string(e: Error) -> String { 62 | match e { 63 | Error::Ambiguous => "Unable to find the macos volume. Make sure you have exactly one volume that has a name staring with \"Macintosh\"".to_string(), 64 | Error::OutOfRange => "Index out of range".to_string(), 65 | Error::Parse => "Unable to parse current nvram contents".to_string(), 66 | Error::SectionTooBig => "Ran out of space on nvram".to_string(), 67 | Error::ApplyError(e) => format!("Failed to save new nvram contents, try running with sudo? Inner error: {:?}", e), 68 | Error::NvramReadError(e) => format!("Failed to read nvram contents, try running with sudo? Inner error: {:?}", e), 69 | Error::DiskReadError(e) => format!("Failed to collect boot candidates, try running with sudo? Inner error: {:?}", e), 70 | Error::VolumeNotFound => "Unable to find specified volume".to_string(), 71 | } 72 | } 73 | 74 | fn main() -> ExitCode { 75 | match real_main() { 76 | Ok(_) => ExitCode::SUCCESS, 77 | Err(e) => { 78 | eprintln!("Error: {}", error_to_string(e)); 79 | ExitCode::FAILURE 80 | } 81 | } 82 | } 83 | 84 | fn real_main() -> Result<()> { 85 | let args = Args::parse(); 86 | 87 | let device = match args.device { 88 | Some(ref dev) => dev, 89 | None => "/dev/mtd/by-name/nvram", 90 | }; 91 | 92 | if args.list_volumes { 93 | list_boot_volumes(&args, device)?; 94 | } else if args.get_boot { 95 | print_boot_target(&args, device)?; 96 | } else if args.clear_next { 97 | if clear_next_boot(device)? { 98 | println!("Cleared next boot target"); 99 | } else { 100 | println!("Next boot target was already empty"); 101 | } 102 | } else if let Some(spec) = &args.set_boot { 103 | let cands = get_boot_candidates()?; 104 | let lc_name = spec.to_lowercase(); 105 | for cand in &cands { 106 | if cand.volumes.iter().any(|n| n.name.to_lowercase() == lc_name) { 107 | set_boot_volume_by_ref(device, &cand, &args, false)?; 108 | return Ok(()); 109 | } 110 | } 111 | if let Ok(idx) = spec.parse::() { 112 | let cand = cands 113 | .into_iter() 114 | .nth(idx - 1) 115 | .ok_or(Error::OutOfRange)?; 116 | set_boot_volume_by_ref(device, &cand, &args, false)?; 117 | } else { 118 | return Err(Error::VolumeNotFound); 119 | } 120 | } else if args.set_boot_macos { 121 | let cands = get_boot_candidates()?; 122 | let macos_cands: Vec<_> = cands 123 | .iter() 124 | .filter(|c| { 125 | c.volumes 126 | .first() 127 | .map(|n| n.name.starts_with("Macintosh")) 128 | .unwrap_or(false) 129 | }) 130 | .collect(); 131 | if macos_cands.len() == 1 { 132 | set_boot_volume_by_ref(device, &macos_cands[0], &args, false)?; 133 | } else { 134 | return Err(Error::Ambiguous); 135 | } 136 | } else { 137 | interactive_main(&args, device)?; 138 | } 139 | 140 | Ok(()) 141 | } 142 | 143 | fn confirm() -> bool { 144 | print!("confirm? [y/N]: "); 145 | stdout().flush().unwrap(); 146 | let mut input = String::new(); 147 | stdin().read_line(&mut input).unwrap(); 148 | input.trim().to_lowercase() == "y" 149 | } 150 | 151 | fn get_vg_name(vg: &[Volume]) -> &str { 152 | for v in vg { 153 | if v.is_system { 154 | return &v.name; 155 | } 156 | } 157 | &vg[0].name 158 | } 159 | 160 | fn print_boot_target(args: &Args, device: &str) -> Result<()> { 161 | let cands = get_boot_candidates()?; 162 | let default_cand = get_boot_volume(device, args.next)?; 163 | for cand in cands { 164 | if (cand.part_uuid == default_cand.part_uuid) && (cand.vg_uuid == default_cand.vg_uuid) { 165 | println!("{}", get_vg_name(&cand.volumes)); 166 | return Ok(()); 167 | } 168 | } 169 | println!("No boot target set"); 170 | Ok(()) 171 | } 172 | 173 | fn list_boot_volumes(args: &Args, device: &str) -> Result> { 174 | let cands = get_boot_candidates()?; 175 | let default_cand = get_boot_volume(device, args.next)?; 176 | let mut is_default: &str; 177 | for (i, cand) in cands.iter().enumerate() { 178 | if (cand.part_uuid == default_cand.part_uuid) && (cand.vg_uuid == default_cand.vg_uuid) { 179 | is_default = "*"; 180 | } else { 181 | is_default = " "; 182 | } 183 | println!("{}{}) {}", is_default, i + 1, get_vg_name(&cand.volumes)); 184 | } 185 | Ok(cands) 186 | } 187 | 188 | fn set_boot_volume_by_ref( 189 | device: &str, 190 | cand: &BootCandidate, 191 | args: &Args, 192 | interactive: bool, 193 | ) -> Result<()> { 194 | if !interactive { 195 | let as_what = if !args.next { 196 | "default boot target" 197 | } else { 198 | "boot target for next boot only" 199 | }; 200 | println!("Will set volume {} as the {}", get_vg_name(&cand.volumes), as_what); 201 | } 202 | if !args.autoconfirm && !interactive { 203 | if !confirm() { 204 | return Ok(()); 205 | } 206 | } 207 | set_boot_volume(device, cand, args.next)?; 208 | Ok(()) 209 | } 210 | 211 | fn interactive_main(args: &Args, device: &str) -> Result<()> { 212 | let cands = list_boot_volumes(args, device)?; 213 | println!("\nEnter a number to select a boot volume:"); 214 | 215 | let mut input = String::new(); 216 | let index = loop { 217 | print!("==> "); 218 | stdout().flush().unwrap(); 219 | 220 | input.clear(); 221 | stdin().read_line(&mut input).unwrap(); 222 | 223 | match input.trim().parse::() { 224 | Ok(i @ 1..) if i <= cands.len() => break i - 1, 225 | Err(e) if e.kind() == &IntErrorKind::Empty => { 226 | eprintln!("No volume selected. Leaving unchanged."); 227 | return Ok(()); 228 | }, 229 | _ => eprintln!("Enter a number from 1 to {}", cands.len()), 230 | } 231 | }; 232 | 233 | set_boot_volume_by_ref(device, &cands[index], args, true) 234 | } 235 | -------------------------------------------------------------------------------- /asahi-btsync/src/main.rs: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: MIT */ 2 | 3 | use std::{ 4 | env, 5 | fmt::Debug, 6 | fs, 7 | fs::OpenOptions, 8 | io::{self, stdout, Read, Write}, 9 | path::Path, 10 | thread, 11 | time::Duration, 12 | }; 13 | 14 | use apple_nvram::{nvram_parse, VarType, Variable}; 15 | 16 | use ini::Ini; 17 | 18 | pub mod dbus; 19 | 20 | #[derive(Debug)] 21 | #[allow(dead_code)] 22 | enum Error { 23 | Parse, 24 | SectionTooBig, 25 | ApplyError(std::io::Error), 26 | VariableNotFound, 27 | FileIO, 28 | BluezConfigDirNotFound, 29 | SliceError, 30 | DbusSystemd, 31 | DbusBluez, 32 | } 33 | 34 | impl From for Error { 35 | fn from(e: apple_nvram::Error) -> Self { 36 | match e { 37 | apple_nvram::Error::ParseError => Error::Parse, 38 | apple_nvram::Error::SectionTooBig => Error::SectionTooBig, 39 | apple_nvram::Error::ApplyError(e) => Error::ApplyError(e), 40 | } 41 | } 42 | } 43 | 44 | impl From for Error { 45 | fn from(_e: io::Error) -> Self { 46 | Error::FileIO 47 | } 48 | } 49 | 50 | impl From for Error { 51 | fn from(_e: std::array::TryFromSliceError) -> Self { 52 | Error::SliceError 53 | } 54 | } 55 | 56 | type Result = std::result::Result; 57 | 58 | fn main() { 59 | real_main().unwrap(); 60 | } 61 | 62 | fn real_main() -> Result<()> { 63 | let matches = clap::command!() 64 | .arg(clap::arg!(-d --device [DEVICE] "Path to the nvram device.")) 65 | .subcommand(clap::Command::new("list").about("Parse shared Bluetooth keys from nvram")) 66 | .subcommand( 67 | clap::Command::new("sync") 68 | .about("Sync Bluetooth device information from nvram") 69 | .arg(clap::arg!(-c --config [CONFIG] "Bluez config path.")), 70 | ) 71 | .subcommand( 72 | clap::Command::new("dump").about("Dump binary Bluetooth device info from nvram"), 73 | ) 74 | .get_matches(); 75 | 76 | let default_name = "/dev/mtd/by-name/nvram".to_owned(); 77 | let default_config = "/var/lib/bluetooth".to_owned(); 78 | let bt_var = "BluetoothUHEDevices"; 79 | 80 | let mut file = OpenOptions::new() 81 | .read(true) 82 | .open(matches.get_one::("device").unwrap_or(&default_name)) 83 | .unwrap(); 84 | let mut data = Vec::new(); 85 | file.read_to_end(&mut data).unwrap(); 86 | let mut nv = nvram_parse(&data)?; 87 | let active = nv.active_part_mut(); 88 | let bt_devs = if let Some(var) = active.get_variable(bt_var.as_bytes(), VarType::System) { 89 | var 90 | } else { 91 | return Ok(()); 92 | }; 93 | 94 | match matches.subcommand() { 95 | Some(("list", _args)) => { 96 | print_btkeys(bt_devs).expect("Failed to parse bt device info"); 97 | } 98 | Some(("sync", args)) => { 99 | sync_btkeys( 100 | bt_devs, 101 | args.get_one::("config").unwrap_or(&default_config), 102 | ) 103 | .expect("Failed to sync bt device info"); 104 | } 105 | Some(("dump", _args)) => { 106 | dump(bt_devs).expect("Failed to dump bt device info"); 107 | } 108 | _ => { 109 | print_btkeys(bt_devs).expect("Failed to parse bt device info"); 110 | } 111 | } 112 | Ok(()) 113 | } 114 | 115 | fn dump(var: &dyn Variable) -> Result<()> { 116 | stdout().write_all(&var.value())?; 117 | Ok(()) 118 | } 119 | 120 | pub struct BtDevice { 121 | mac: [u8; 6], 122 | class: u16, 123 | name: String, 124 | vendor_id: u16, 125 | product_id: u16, 126 | pairing_key: [u8; 16], 127 | } 128 | 129 | pub struct BtInfo { 130 | mac: [u8; 6], 131 | devices: Vec, 132 | } 133 | 134 | fn read_le_u16(input: &mut &[u8]) -> Result { 135 | let (int_bytes, rest) = input.split_at(std::mem::size_of::()); 136 | *input = rest; 137 | Ok(u16::from_le_bytes(int_bytes.try_into()?)) 138 | } 139 | 140 | fn parse_bt_device(input: &mut &[u8]) -> Result { 141 | // parse MAC 142 | let (mac_bytes, remain) = input.split_at(6_usize); 143 | *input = remain; 144 | let mac: [u8; 6] = mac_bytes.try_into().expect("end of bytes"); 145 | 146 | let class = read_le_u16(input)?; 147 | 148 | // skip 2 bytes 149 | *input = &input[2..]; 150 | 151 | // parse device name (u16_le length + \0 terminated utf-8 string) 152 | let name_len = read_le_u16(input)? as usize; 153 | let (name_bytes, remain) = input.split_at(name_len); 154 | *input = remain; 155 | let name = String::from_utf8_lossy(&name_bytes[..name_len - 1]).to_string(); 156 | 157 | // parse pairing key 158 | let (key_bytes, remain) = input.split_at(16); 159 | *input = remain; 160 | let key: [u8; 16] = key_bytes.try_into().expect("end of bytes"); 161 | 162 | // parse product / vendor id 163 | let product_id = read_le_u16(input)?; 164 | let vendor_id = read_le_u16(input)?; 165 | 166 | // skip 2 unknown trailing bytes 167 | *input = &input[2..]; 168 | 169 | Ok(BtDevice { 170 | mac, 171 | class, 172 | name, 173 | vendor_id, 174 | product_id, 175 | pairing_key: key, 176 | }) 177 | } 178 | 179 | fn parse_bt_info(var: &dyn Variable) -> Result { 180 | let data = var.value(); 181 | 182 | assert!(data.len() >= 8); 183 | let adapter_mac: [u8; 6] = data[0..6].try_into()?; 184 | let num_devices = data[6]; 185 | assert!(data[7] == 0x04); 186 | 187 | let mut dev_data = &data[8..]; 188 | 189 | let mut devices: Vec = Vec::new(); 190 | for _n in 0..num_devices { 191 | devices.push(parse_bt_device(&mut dev_data)?); 192 | } 193 | 194 | Ok(BtInfo { 195 | mac: adapter_mac, 196 | devices, 197 | }) 198 | } 199 | 200 | fn format_mac(mac: &[u8; 6]) -> Result { 201 | Ok(mac 202 | .iter() 203 | .map(|x| format!("{x:02X}")) 204 | .collect::>() 205 | .join(":")) 206 | } 207 | 208 | fn format_key(key: &[u8; 16]) -> Result { 209 | Ok(key.iter().map(|x| format!("{x:02X}")).rev().collect()) 210 | } 211 | 212 | fn print_btkeys(var: &dyn Variable) -> Result<()> { 213 | let info = parse_bt_info(var)?; 214 | 215 | for dev in info.devices { 216 | println!( 217 | "ID {:04x}:{:04x} {} ({})", 218 | dev.vendor_id, 219 | dev.product_id, 220 | dev.name, 221 | format_mac(&dev.mac)? 222 | ); 223 | } 224 | Ok(()) 225 | } 226 | 227 | fn sync_btkeys(var: &dyn Variable, config: &String) -> Result<()> { 228 | let config_path = Path::new(config); 229 | 230 | if !config_path.is_dir() { 231 | return Err(Error::BluezConfigDirNotFound); 232 | } 233 | 234 | let info = parse_bt_info(var)?; 235 | 236 | let adapter_path = config_path.join(format_mac(&info.mac)?); 237 | 238 | if !adapter_path.is_dir() { 239 | fs::create_dir(adapter_path.clone())?; 240 | } 241 | let mut added_devs = 0; 242 | 243 | for dev in &info.devices { 244 | let dev_path = adapter_path.join(format_mac(&dev.mac)?); 245 | 246 | if !dev_path.is_dir() { 247 | fs::create_dir(dev_path.clone())?; 248 | } 249 | 250 | let info_file = dev_path.as_path().join("info"); 251 | 252 | if info_file.exists() { 253 | continue; 254 | } 255 | 256 | let mut info = Ini::new(); 257 | 258 | info.with_section(Some("General")) 259 | .set("Name", dev.name.clone()) 260 | .set("Class", format!("{:#08X}", dev.class)) 261 | .set("Trusted", "true") 262 | .set("Blocked", "false") 263 | .set("WakeAllowed", "true"); 264 | info.with_section(Some("LinkKey")) 265 | .set("Key", format_key(&dev.pairing_key)?); 266 | info.with_section(Some("DeviceID")) 267 | .set("Vendor", format!("{}", dev.vendor_id)) 268 | .set("Product", format!("{}", dev.product_id)); 269 | info.write_to_file(info_file)?; 270 | 271 | println!("{}", format_mac(&dev.mac)?); 272 | added_devs += 1; 273 | } 274 | if added_devs > 0 { 275 | if let Err(e) = dbus::systemd_reload_bt_config() { 276 | println!("Failed to reload bluetoothd config {}", e); 277 | return Err(Error::DbusSystemd); 278 | } 279 | // sleep 500 ms to let bluetoothd reload its config 280 | thread::sleep(Duration::from_millis(500)); 281 | if let Err(e) = dbus::bluez_connect(&info) { 282 | println!("Failed to connect bluetooth devices {}", e); 283 | return Err(Error::DbusBluez); 284 | } 285 | } 286 | Ok(()) 287 | } 288 | -------------------------------------------------------------------------------- /asahi-bless/src/lib.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | #![allow(dead_code)] 3 | use apple_nvram::{nvram_parse, VarType}; 4 | use gpt::{disk::LogicalBlockSize, GptConfig}; 5 | use std::{ 6 | borrow::Cow, 7 | collections::HashMap, 8 | fs::{File, OpenOptions}, 9 | io::{self, Read, Seek, SeekFrom}, 10 | ops::Deref, 11 | }; 12 | use uuid::Uuid; 13 | 14 | struct NxSuperblock([u8; NxSuperblock::SIZE]); 15 | 16 | impl NxSuperblock { 17 | const SIZE: usize = 1408; 18 | const MAGIC: u32 = 1112758350; //'BSXN' 19 | const MAX_FILE_SYSTEMS: usize = 100; 20 | fn get_buf(&mut self) -> &mut [u8] { 21 | &mut self.0 22 | } 23 | fn new() -> Self { 24 | NxSuperblock([0; NxSuperblock::SIZE]) 25 | } 26 | fn magic(&self) -> u32 { 27 | u32::from_le_bytes(self.0[32..32 + 4].try_into().unwrap()) 28 | } 29 | fn block_size(&self) -> u32 { 30 | u32::from_le_bytes(self.0[36..36 + 4].try_into().unwrap()) 31 | } 32 | fn xid(&self) -> u64 { 33 | u64::from_le_bytes(self.0[16..16 + 8].try_into().unwrap()) 34 | } 35 | fn omap_oid(&self) -> u64 { 36 | u64::from_le_bytes(self.0[160..160 + 8].try_into().unwrap()) 37 | } 38 | fn xp_desc_blocks(&self) -> u32 { 39 | u32::from_le_bytes(self.0[104..104 + 4].try_into().unwrap()) 40 | } 41 | fn xp_desc_base(&self) -> u64 { 42 | u64::from_le_bytes(self.0[112..112 + 8].try_into().unwrap()) 43 | } 44 | fn fs_oid(&self, i: usize) -> u64 { 45 | let at = 184 + 8 * i; 46 | u64::from_le_bytes(self.0[at..at + 8].try_into().unwrap()) 47 | } 48 | } 49 | 50 | struct OmapPhys<'a>(&'a [u8]); 51 | impl OmapPhys<'_> { 52 | const SIZE: usize = 88; 53 | fn tree_oid(&self) -> u64 { 54 | u64::from_le_bytes(self.0[48..48 + 8].try_into().unwrap()) 55 | } 56 | } 57 | 58 | struct NLoc<'a>(&'a [u8]); 59 | 60 | impl NLoc<'_> { 61 | fn off(&self) -> u16 { 62 | u16::from_le_bytes(self.0[0..2].try_into().unwrap()) 63 | } 64 | fn len(&self) -> u16 { 65 | u16::from_le_bytes(self.0[2..2 + 2].try_into().unwrap()) 66 | } 67 | } 68 | 69 | struct KVOff<'a>(&'a [u8]); 70 | impl KVOff<'_> { 71 | const SIZE: usize = 4; 72 | fn k(&self) -> u16 { 73 | u16::from_le_bytes(self.0[0..2].try_into().unwrap()) 74 | } 75 | fn v(&self) -> u16 { 76 | u16::from_le_bytes(self.0[2..2 + 2].try_into().unwrap()) 77 | } 78 | } 79 | 80 | struct OmapKey<'a>(&'a [u8]); 81 | impl OmapKey<'_> { 82 | fn oid(&self) -> u64 { 83 | u64::from_le_bytes(self.0[0..8].try_into().unwrap()) 84 | } 85 | fn xid(&self) -> u64 { 86 | u64::from_le_bytes(self.0[8..8 + 8].try_into().unwrap()) 87 | } 88 | } 89 | 90 | struct OmapVal<'a>(&'a [u8]); 91 | impl OmapVal<'_> { 92 | fn flags(&self) -> u32 { 93 | u32::from_le_bytes(self.0[0..4].try_into().unwrap()) 94 | } 95 | fn size(&self) -> u32 { 96 | u32::from_le_bytes(self.0[4..4 + 4].try_into().unwrap()) 97 | } 98 | fn paddr(&self) -> u64 { 99 | u64::from_le_bytes(self.0[8..8 + 8].try_into().unwrap()) 100 | } 101 | } 102 | 103 | struct BTreeInfo; 104 | impl BTreeInfo { 105 | const SIZE: usize = 40; 106 | } 107 | 108 | struct BTreeNodePhys<'a>(&'a [u8]); 109 | impl BTreeNodePhys<'_> { 110 | const FIXED_KV_SIZE: u16 = 0x4; 111 | const ROOT: u16 = 0x1; 112 | const SIZE: usize = 56; 113 | fn flags(&self) -> u16 { 114 | u16::from_le_bytes(self.0[32..32 + 2].try_into().unwrap()) 115 | } 116 | fn level(&self) -> u16 { 117 | u16::from_le_bytes(self.0[34..34 + 2].try_into().unwrap()) 118 | } 119 | fn table_space(&self) -> NLoc<'_> { 120 | NLoc(&self.0[40..]) 121 | } 122 | fn nkeys(&self) -> u32 { 123 | u32::from_le_bytes(self.0[36..36 + 4].try_into().unwrap()) 124 | } 125 | } 126 | 127 | struct ApfsSuperblock<'a>(&'a [u8]); 128 | impl ApfsSuperblock<'_> { 129 | fn volname(&self) -> &[u8] { 130 | &self.0[704..704 + 128] 131 | } 132 | fn vol_uuid(&self) -> Uuid { 133 | Uuid::from_slice(&self.0[240..240 + 16]).unwrap() 134 | } 135 | fn volume_group_id(&self) -> Uuid { 136 | Uuid::from_slice(&self.0[1008..1008 + 16]).unwrap() 137 | } 138 | fn role(&self) -> u16 { 139 | u16::from_le_bytes(self.0[964..964 + 2].try_into().unwrap()) 140 | } 141 | } 142 | 143 | const VOL_ROLE_SYSTEM: u16 = 1; 144 | 145 | fn pread(file: &mut T, pos: u64, target: &mut [u8]) -> io::Result<()> { 146 | file.seek(SeekFrom::Start(pos))?; 147 | file.read_exact(target) 148 | } 149 | 150 | // should probably fix xids here 151 | fn lookup(_disk: &mut File, cur_node: &BTreeNodePhys, key: u64) -> Option { 152 | if cur_node.level() != 0 { 153 | unimplemented!(); 154 | } 155 | if cur_node.flags() & BTreeNodePhys::FIXED_KV_SIZE != 0 { 156 | let toc_off = cur_node.table_space().off() as usize + BTreeNodePhys::SIZE; 157 | let key_start = toc_off + cur_node.table_space().len() as usize; 158 | let val_end = cur_node.0.len() 159 | - if cur_node.flags() & BTreeNodePhys::ROOT == 0 { 160 | 0 161 | } else { 162 | BTreeInfo::SIZE 163 | }; 164 | for i in 0..cur_node.nkeys() as usize { 165 | let entry = KVOff(&cur_node.0[(toc_off + i * KVOff::SIZE)..]); 166 | let key_off = entry.k() as usize + key_start; 167 | let map_key = OmapKey(&cur_node.0[key_off..]); 168 | if map_key.oid() == key { 169 | let val_off = val_end - entry.v() as usize; 170 | let val = OmapVal(&cur_node.0[val_off..]); 171 | return Some(val.paddr()); 172 | } 173 | } 174 | None 175 | } else { 176 | unimplemented!(); 177 | } 178 | } 179 | 180 | fn trim_zeroes(s: &[u8]) -> &[u8] { 181 | for i in 0..s.len() { 182 | if s[i] == 0 { 183 | return &s[..i]; 184 | } 185 | } 186 | s 187 | } 188 | 189 | fn scan_volume(disk: &mut File) -> io::Result>> { 190 | let mut sb = NxSuperblock::new(); 191 | disk.read_exact(sb.get_buf())?; 192 | if sb.magic() != NxSuperblock::MAGIC { 193 | return Ok(HashMap::new()); 194 | } 195 | let block_size = sb.block_size() as u64; 196 | for i in 0..sb.xp_desc_blocks() { 197 | let mut sbc = NxSuperblock::new(); 198 | pread( 199 | disk, 200 | (sb.xp_desc_base() + i as u64) * block_size, 201 | sbc.get_buf(), 202 | )?; 203 | if sbc.magic() == NxSuperblock::MAGIC { 204 | if sbc.xid() > sb.xid() { 205 | sb = sbc; 206 | } 207 | } 208 | } 209 | let mut omap_bytes = vec![0; OmapPhys::SIZE]; 210 | pread(disk, sb.omap_oid() * block_size, &mut omap_bytes)?; 211 | let omap = OmapPhys(&omap_bytes); 212 | let mut node_bytes = vec![0; sb.block_size() as usize]; 213 | pread(disk, omap.tree_oid() * block_size, &mut node_bytes)?; 214 | let node = BTreeNodePhys(&node_bytes); 215 | let mut vgs_found = HashMap::>::new(); 216 | for i in 0..NxSuperblock::MAX_FILE_SYSTEMS { 217 | let fs_id = sb.fs_oid(i); 218 | if fs_id == 0 { 219 | continue; 220 | } 221 | let vsb = lookup(disk, &node, fs_id); 222 | let mut asb_bytes = vec![0; sb.block_size() as usize]; 223 | if vsb.is_none() { 224 | continue; 225 | } 226 | pread(disk, vsb.unwrap() * sb.block_size() as u64, &mut asb_bytes)?; 227 | let asb = ApfsSuperblock(&asb_bytes); 228 | if asb.volume_group_id().is_nil() { 229 | continue; 230 | } 231 | if let Ok(name) = std::str::from_utf8(trim_zeroes(asb.volname())) { 232 | vgs_found 233 | .entry(asb.volume_group_id()) 234 | .or_default() 235 | .push(Volume { 236 | name: name.to_owned(), 237 | is_system: asb.role() == VOL_ROLE_SYSTEM, 238 | }); 239 | } 240 | } 241 | Ok(vgs_found) 242 | } 243 | 244 | #[derive(Debug)] 245 | pub struct Volume { 246 | pub name: String, 247 | pub is_system: bool, 248 | } 249 | 250 | #[derive(Debug)] 251 | pub struct BootCandidate { 252 | pub part_uuid: Uuid, 253 | pub vg_uuid: Uuid, 254 | pub volumes: Vec, 255 | } 256 | 257 | fn swap_uuid(u: &Uuid) -> Uuid { 258 | let (a, b, c, d) = u.as_fields(); 259 | Uuid::from_fields(a.swap_bytes(), b.swap_bytes(), c.swap_bytes(), d) 260 | } 261 | 262 | #[derive(Debug)] 263 | pub enum Error { 264 | Parse, 265 | SectionTooBig, 266 | ApplyError(std::io::Error), 267 | OutOfRange, 268 | Ambiguous, 269 | NvramReadError(std::io::Error), 270 | DiskReadError(std::io::Error), 271 | VolumeNotFound, 272 | } 273 | 274 | impl From for Error { 275 | fn from(e: apple_nvram::Error) -> Self { 276 | match e { 277 | apple_nvram::Error::ParseError => Error::Parse, 278 | apple_nvram::Error::SectionTooBig => Error::SectionTooBig, 279 | apple_nvram::Error::ApplyError(e) => Error::ApplyError(e), 280 | } 281 | } 282 | } 283 | 284 | type Result = std::result::Result; 285 | 286 | pub fn get_boot_candidates() -> Result> { 287 | let disk = GptConfig::new() 288 | .writable(false) 289 | .logical_block_size(LogicalBlockSize::Lb4096) 290 | .open("/dev/nvme0n1") 291 | .map_err(Error::DiskReadError)?; 292 | let mut cands = Vec::new(); 293 | for (i, v) in disk.partitions() { 294 | if v.part_type_guid.guid != "7C3457EF-0000-11AA-AA11-00306543ECAC" { 295 | continue; 296 | } 297 | let mut part = File::open(format!("/dev/nvme0n1p{i}")).map_err(Error::DiskReadError)?; 298 | for (vg_uuid, volumes) in scan_volume(&mut part).unwrap_or_default() { 299 | cands.push(BootCandidate { 300 | vg_uuid, 301 | volumes, 302 | part_uuid: swap_uuid(&v.part_guid), 303 | }); 304 | } 305 | } 306 | 307 | return Ok(cands); 308 | } 309 | 310 | const ALT_BOOT_VAR: &'static [u8] = b"alt-boot-volume"; 311 | 312 | pub fn get_boot_volume(device: &str, next: bool) -> Result { 313 | let mut file = OpenOptions::new() 314 | .read(true) 315 | .write(true) 316 | .open(device) 317 | .map_err(Error::NvramReadError)?; 318 | let mut data = Vec::new(); 319 | file.read_to_end(&mut data).map_err(Error::NvramReadError)?; 320 | let mut nv = nvram_parse(&data)?; 321 | 322 | let active = nv.active_part_mut(); 323 | let v; 324 | if next { 325 | v = active 326 | .get_variable(ALT_BOOT_VAR, VarType::System) 327 | .or(active.get_variable(b"boot-volume", VarType::System)) 328 | .ok_or(Error::Parse); 329 | } else { 330 | v = active 331 | .get_variable(b"boot-volume", VarType::System) 332 | .ok_or(Error::Parse); 333 | } 334 | let data = String::from_utf8(v?.value().deref().to_vec()).unwrap(); 335 | let [_, part_uuid, part_vg_uuid]: [&str; 3] = 336 | data.split(":").collect::>().try_into().unwrap(); 337 | 338 | Ok(BootCandidate { 339 | volumes: Vec::new(), 340 | part_uuid: Uuid::parse_str(part_uuid).unwrap(), 341 | vg_uuid: Uuid::parse_str(part_vg_uuid).unwrap(), 342 | }) 343 | } 344 | 345 | pub fn clear_next_boot(device: &str) -> Result { 346 | let mut file = OpenOptions::new() 347 | .read(true) 348 | .write(true) 349 | .open(device) 350 | .map_err(Error::ApplyError)?; 351 | let mut data = Vec::new(); 352 | file.read_to_end(&mut data).map_err(Error::ApplyError)?; 353 | let mut nv = nvram_parse(&data)?; 354 | nv.prepare_for_write(); 355 | if nv.active_part_mut().get_variable(ALT_BOOT_VAR, VarType::System).is_none() { 356 | return Ok(false); 357 | } 358 | nv.active_part_mut().remove_variable( 359 | ALT_BOOT_VAR, 360 | VarType::System, 361 | ); 362 | nv.apply(&mut file)?; 363 | Ok(true) 364 | } 365 | 366 | pub fn set_boot_volume(device: &str, cand: &BootCandidate, next: bool) -> Result<()> { 367 | let mut nvram_key: &[u8] = b"boot-volume".as_ref(); 368 | if next { 369 | nvram_key = ALT_BOOT_VAR.as_ref(); 370 | } 371 | 372 | let boot_str = format!( 373 | "EF57347C-0000-AA11-AA11-00306543ECAC:{}:{}", 374 | cand.part_uuid 375 | .hyphenated() 376 | .encode_upper(&mut Uuid::encode_buffer()), 377 | cand.vg_uuid 378 | .hyphenated() 379 | .encode_upper(&mut Uuid::encode_buffer()) 380 | ); 381 | let mut file = OpenOptions::new() 382 | .read(true) 383 | .write(true) 384 | .open(device) 385 | .map_err(Error::ApplyError)?; 386 | let mut data = Vec::new(); 387 | file.read_to_end(&mut data).map_err(Error::ApplyError)?; 388 | let mut nv = nvram_parse(&data)?; 389 | nv.prepare_for_write(); 390 | nv.active_part_mut().insert_variable( 391 | nvram_key, 392 | Cow::Owned(boot_str.into_bytes()), 393 | VarType::System, 394 | ); 395 | nv.apply(&mut file)?; 396 | Ok(()) 397 | } 398 | -------------------------------------------------------------------------------- /apple-nvram/src/v1v2.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | borrow::Cow, 3 | collections::HashMap, 4 | fmt::{Debug, Display, Formatter}, 5 | }; 6 | 7 | use crate::{chrp_checksum_add, slice_find, slice_rstrip, Error, Result, VarType}; 8 | 9 | pub struct UnescapeVal { 10 | inner: I, 11 | esc_out: u8, 12 | remaining: u8, 13 | } 14 | 15 | impl UnescapeVal 16 | where 17 | I: Iterator, 18 | { 19 | pub fn new(inner: I) -> Self { 20 | Self { 21 | inner, 22 | esc_out: 0, 23 | remaining: 0, 24 | } 25 | } 26 | } 27 | 28 | impl Iterator for UnescapeVal 29 | where 30 | I: Iterator, 31 | { 32 | type Item = u8; 33 | fn next(&mut self) -> Option { 34 | if self.remaining != 0 { 35 | self.remaining -= 1; 36 | return Some(self.esc_out); 37 | } 38 | if let Some(n) = self.inner.next() { 39 | if n != 0xFF { 40 | return Some(n); 41 | } 42 | let count = self.inner.next()?; 43 | self.esc_out = if count & 0x80 == 0 { 0 } else { 0xFF }; 44 | self.remaining = (count & 0x7F) - 1; 45 | Some(self.esc_out) 46 | } else { 47 | None 48 | } 49 | } 50 | } 51 | 52 | #[derive(Clone)] 53 | pub struct CHRPHeader<'a> { 54 | pub name: &'a [u8], 55 | pub size: u16, 56 | pub signature: u8, 57 | } 58 | 59 | impl Debug for CHRPHeader<'_> { 60 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 61 | f.debug_struct("CHRPHeader") 62 | .field("name", &String::from_utf8_lossy(self.name).into_owned()) 63 | .field("size", &self.size) 64 | .field("signature", &self.signature) 65 | .finish() 66 | } 67 | } 68 | 69 | impl CHRPHeader<'_> { 70 | pub fn parse(nvr: &[u8]) -> Result> { 71 | let signature = nvr[0]; 72 | let cksum = nvr[1]; 73 | let size = u16::from_le_bytes(nvr[2..4].try_into().unwrap()); 74 | let name = slice_rstrip(&nvr[4..16], &0); 75 | let cand = CHRPHeader { 76 | name, 77 | size, 78 | signature, 79 | }; 80 | if cand.checksum() != cksum { 81 | return Err(Error::ParseError); 82 | } 83 | Ok(cand) 84 | } 85 | fn checksum(&self) -> u8 { 86 | let mut cksum = 0; 87 | for &u in self.name { 88 | cksum = chrp_checksum_add(cksum, u); 89 | } 90 | cksum = chrp_checksum_add(cksum, self.signature); 91 | cksum = chrp_checksum_add(cksum, (self.size & 0xFF) as u8); 92 | chrp_checksum_add(cksum, (self.size >> 8) as u8) 93 | } 94 | 95 | pub fn serialize(&self, v: &mut Vec) { 96 | v.push(self.signature); 97 | v.push(self.checksum()); 98 | v.extend_from_slice(&self.size.to_le_bytes()); 99 | v.extend_from_slice(self.name); 100 | for _ in 0..(12 - self.name.len()) { 101 | v.push(0); 102 | } 103 | } 104 | } 105 | 106 | #[derive(Clone)] 107 | pub struct Variable<'a> { 108 | pub key: Cow<'a, [u8]>, 109 | pub value: Cow<'a, [u8]>, 110 | pub typ: VarType, 111 | } 112 | 113 | impl<'a> crate::Variable<'a> for Variable<'a> { 114 | fn value(&self) -> Cow<'a, [u8]> { 115 | Cow::Owned(UnescapeVal::new(self.value.iter().copied()).collect()) 116 | } 117 | } 118 | 119 | impl Display for Variable<'_> { 120 | fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { 121 | let key = String::from_utf8_lossy(&self.key); 122 | let mut value = String::new(); 123 | for c in UnescapeVal::new(self.value.iter().copied()) { 124 | if (c as char).is_ascii() && !(c as char).is_ascii_control() { 125 | value.push(c as char); 126 | } else { 127 | value.push_str(&format!("%{c:02x}")); 128 | } 129 | } 130 | 131 | let value: String = value.chars().take(128).collect(); 132 | write!(f, "{}:{}={}", self.typ, key, value) 133 | } 134 | } 135 | 136 | #[derive(Clone)] 137 | pub struct Section<'a> { 138 | pub header: CHRPHeader<'a>, 139 | pub values: HashMap, Variable<'a>>, 140 | } 141 | 142 | impl Section<'_> { 143 | pub fn parse(mut nvr: &[u8]) -> Result> { 144 | let header = CHRPHeader::parse(&nvr[..16])?; 145 | nvr = &nvr[16..]; 146 | let mut values = HashMap::new(); 147 | loop { 148 | let zero = slice_find(nvr, &0); 149 | if zero.is_none() { 150 | break; 151 | } 152 | let zero = zero.unwrap(); 153 | let cand = &nvr[..zero]; 154 | let eq = slice_find(cand, &b'='); 155 | if eq.is_none() { 156 | break; 157 | } 158 | let eq = eq.unwrap(); 159 | let key = &cand[..eq]; 160 | let typ = if header.name == b"common" { 161 | VarType::Common 162 | } else { 163 | VarType::System 164 | }; 165 | values.insert( 166 | Cow::Borrowed(key), 167 | Variable { 168 | key: Cow::Borrowed(key), 169 | value: Cow::Borrowed(&cand[(eq + 1)..]), 170 | typ, 171 | }, 172 | ); 173 | nvr = &nvr[(zero + 1)..] 174 | } 175 | Ok(Section { header, values }) 176 | } 177 | fn size_bytes(&self) -> usize { 178 | self.header.size as usize * 16 179 | } 180 | pub fn serialize(&self, v: &mut Vec) -> Result<()> { 181 | let start_size = v.len(); 182 | self.header.serialize(v); 183 | for val in self.values.values() { 184 | v.extend_from_slice(&val.key); 185 | v.push(b'='); 186 | v.extend_from_slice(&val.value); 187 | v.push(0); 188 | } 189 | let my_size = v.len() - start_size; 190 | if my_size > self.size_bytes() { 191 | return Err(Error::SectionTooBig); 192 | } 193 | for _ in 0..(self.size_bytes() - my_size) { 194 | v.push(0); 195 | } 196 | Ok(()) 197 | } 198 | } 199 | 200 | struct SectionDebug<'a, 'b>(&'a Section<'b>); 201 | impl Debug for SectionDebug<'_, '_> { 202 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 203 | let mut m = f.debug_map(); 204 | for v in self.0.values.values() { 205 | m.entry( 206 | &String::from_utf8_lossy(&v.key).into_owned(), 207 | &String::from_utf8_lossy( 208 | &UnescapeVal::new(v.value.iter().copied()).collect::>(), 209 | ) 210 | .into_owned(), 211 | ); 212 | } 213 | m.finish() 214 | } 215 | } 216 | 217 | impl Debug for Section<'_> { 218 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 219 | f.debug_struct("Section") 220 | .field("header", &self.header) 221 | .field("values", &SectionDebug(self)) 222 | .finish() 223 | } 224 | } 225 | 226 | #[derive(Debug, Clone)] 227 | pub struct Partition<'a> { 228 | pub header: CHRPHeader<'a>, 229 | pub generation: u32, 230 | pub common: Section<'a>, 231 | pub system: Section<'a>, 232 | } 233 | 234 | impl<'a> Partition<'a> { 235 | pub fn parse(nvr: &[u8]) -> Result> { 236 | let header = CHRPHeader::parse(&nvr[..16])?; 237 | if header.name != b"nvram" { 238 | return Err(Error::ParseError); 239 | } 240 | let adler = u32::from_le_bytes(nvr[16..20].try_into().unwrap()); 241 | let generation = u32::from_le_bytes(nvr[20..24].try_into().unwrap()); 242 | let sec1 = Section::parse(&nvr[32..])?; 243 | let sec2 = Section::parse(&nvr[(32 + sec1.size_bytes())..])?; 244 | let calc_adler = 245 | adler32::adler32(&nvr[20..(32 + sec1.size_bytes() + sec2.size_bytes())]).unwrap(); 246 | if adler != calc_adler { 247 | return Err(Error::ParseError); 248 | } 249 | let mut com = None; 250 | let mut sys = None; 251 | if sec1.header.name == b"common" { 252 | com = Some(sec1); 253 | } else if sec1.header.name == b"system" { 254 | sys = Some(sec1); 255 | } 256 | if sec2.header.name == b"common" { 257 | com = Some(sec2); 258 | } else if sec2.header.name == b"system" { 259 | sys = Some(sec2); 260 | } 261 | if com.is_none() || sys.is_none() { 262 | return Err(Error::ParseError); 263 | } 264 | Ok(Partition { 265 | header, 266 | generation, 267 | common: com.unwrap(), 268 | system: sys.unwrap(), 269 | }) 270 | } 271 | fn size_bytes(&self) -> usize { 272 | 32 + self.common.size_bytes() + self.system.size_bytes() 273 | } 274 | pub fn serialize(&self, v: &mut Vec) -> Result<()> { 275 | self.header.serialize(v); 276 | v.extend_from_slice(&[0; 4]); 277 | let adler_start = v.len(); 278 | v.extend_from_slice(&self.generation.to_le_bytes()); 279 | v.extend_from_slice(&[0; 8]); 280 | self.common.serialize(v)?; 281 | self.system.serialize(v)?; 282 | let adler_end = v.len(); 283 | let adler = adler32::adler32(&v[adler_start..adler_end]).unwrap(); 284 | v[(adler_start - 4)..adler_start].copy_from_slice(&adler.to_le_bytes()); 285 | Ok(()) 286 | } 287 | 288 | pub fn variables(&self) -> impl Iterator> { 289 | self.common 290 | .values 291 | .values() 292 | .chain(self.system.values.values()) 293 | } 294 | } 295 | 296 | impl<'a> crate::Partition<'a> for Partition<'a> { 297 | fn get_variable(&self, key: &[u8], typ: VarType) -> Option<&dyn crate::Variable<'a>> { 298 | match typ { 299 | VarType::Common => self 300 | .common 301 | .values 302 | .get(key) 303 | .map(|v| v as &dyn crate::Variable), 304 | VarType::System => self 305 | .system 306 | .values 307 | .get(key) 308 | .map(|v| v as &dyn crate::Variable), 309 | } 310 | } 311 | 312 | fn insert_variable(&mut self, key: &[u8], value: Cow<'a, [u8]>, typ: VarType) { 313 | match typ { 314 | VarType::Common => &mut self.common, 315 | VarType::System => &mut self.system, 316 | } 317 | .values 318 | .insert( 319 | Cow::Owned(key.into()), 320 | Variable { 321 | key: Cow::Owned(key.into()), 322 | value, 323 | typ, 324 | }, 325 | ); 326 | } 327 | 328 | fn remove_variable(&mut self, key: &[u8], typ: VarType) { 329 | match typ { 330 | VarType::Common => &mut self.common, 331 | VarType::System => &mut self.system, 332 | } 333 | .values 334 | .remove(key); 335 | } 336 | 337 | fn variables(&self) -> Box> + '_> { 338 | Box::new(self.variables().map(|e| e as &dyn crate::Variable<'a>)) 339 | } 340 | } 341 | 342 | impl Display for Partition<'_> { 343 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 344 | write!( 345 | f, 346 | "size: {}, generation: {}, count: {}", 347 | self.header.size, 348 | self.generation, 349 | self.common.values.len() + self.system.values.len(), 350 | ) 351 | } 352 | } 353 | 354 | #[derive(Debug)] 355 | pub struct Nvram<'a> { 356 | pub partitions: [Partition<'a>; 2], 357 | pub active: usize, 358 | } 359 | 360 | impl<'a> Nvram<'a> { 361 | pub fn parse(nvr: &[u8]) -> Result> { 362 | let p1; 363 | let p2; 364 | match (Partition::parse(nvr), Partition::parse(&nvr[0x10000..])) { 365 | (Err(err), Err(_)) => return Err(err), 366 | (Ok(p1r), Err(_)) => { 367 | p1 = p1r; 368 | p2 = p1.clone(); 369 | } 370 | (Err(_), Ok(p2r)) => { 371 | p2 = p2r; 372 | p1 = p2.clone(); 373 | } 374 | (Ok(p1r), Ok(p2r)) => { 375 | p1 = p1r; 376 | p2 = p2r; 377 | } 378 | } 379 | let active = if p1.generation > p2.generation { 0 } else { 1 }; 380 | let partitions = [p1, p2]; 381 | Ok(Nvram { partitions, active }) 382 | } 383 | 384 | pub fn partitions(&self) -> impl Iterator> { 385 | self.partitions.iter() 386 | } 387 | } 388 | 389 | impl<'a> crate::Nvram<'a> for Nvram<'a> { 390 | fn serialize(&self) -> Result> { 391 | let mut v = Vec::with_capacity(self.partitions[0].size_bytes() * 2); 392 | self.partitions[0].serialize(&mut v)?; 393 | self.partitions[1].serialize(&mut v)?; 394 | Ok(v) 395 | } 396 | fn prepare_for_write(&mut self) { 397 | let inactive = 1 - self.active; 398 | self.partitions[inactive] = self.partitions[self.active].clone(); 399 | self.partitions[inactive].generation += 1; 400 | self.active = inactive; 401 | } 402 | // fn active_part(&self) -> &Partition<'a> { 403 | // &self.partitions[self.active] 404 | // } 405 | fn active_part_mut(&mut self) -> &mut dyn crate::Partition<'a> { 406 | &mut self.partitions[self.active] as &mut dyn crate::Partition<'a> 407 | } 408 | 409 | fn partitions(&self) -> Box> + '_> { 410 | Box::new(self.partitions().map(|e| e as &dyn crate::Partition<'a>)) 411 | } 412 | 413 | fn apply(&mut self, w: &mut dyn crate::NvramWriter) -> Result<()> { 414 | let data = self.serialize()?; 415 | w.erase_if_needed(0, data.len()); 416 | w.write_all(0, &data).map_err(|e| Error::ApplyError(e))?; 417 | Ok(()) 418 | } 419 | } 420 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "adler32" 7 | version = "1.2.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" 10 | 11 | [[package]] 12 | name = "aho-corasick" 13 | version = "1.1.3" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 16 | dependencies = [ 17 | "memchr", 18 | ] 19 | 20 | [[package]] 21 | name = "anstream" 22 | version = "0.6.18" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 25 | dependencies = [ 26 | "anstyle", 27 | "anstyle-parse", 28 | "anstyle-query", 29 | "anstyle-wincon", 30 | "colorchoice", 31 | "is_terminal_polyfill", 32 | "utf8parse", 33 | ] 34 | 35 | [[package]] 36 | name = "anstyle" 37 | version = "1.0.10" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 40 | 41 | [[package]] 42 | name = "anstyle-parse" 43 | version = "0.2.6" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 46 | dependencies = [ 47 | "utf8parse", 48 | ] 49 | 50 | [[package]] 51 | name = "anstyle-query" 52 | version = "1.1.2" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 55 | dependencies = [ 56 | "windows-sys", 57 | ] 58 | 59 | [[package]] 60 | name = "anstyle-wincon" 61 | version = "3.0.6" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" 64 | dependencies = [ 65 | "anstyle", 66 | "windows-sys", 67 | ] 68 | 69 | [[package]] 70 | name = "apple-nvram" 71 | version = "0.3.0" 72 | dependencies = [ 73 | "adler32", 74 | "crc32fast", 75 | "nix", 76 | ] 77 | 78 | [[package]] 79 | name = "asahi-bless" 80 | version = "0.4.2" 81 | dependencies = [ 82 | "apple-nvram", 83 | "clap 4.5.21", 84 | "gpt", 85 | "uuid", 86 | ] 87 | 88 | [[package]] 89 | name = "asahi-btsync" 90 | version = "0.2.4" 91 | dependencies = [ 92 | "apple-nvram", 93 | "clap 3.2.25", 94 | "dbus", 95 | "regex", 96 | "rust-ini", 97 | ] 98 | 99 | [[package]] 100 | name = "asahi-nvram" 101 | version = "0.2.3" 102 | dependencies = [ 103 | "apple-nvram", 104 | "clap 3.2.25", 105 | ] 106 | 107 | [[package]] 108 | name = "asahi-wifisync" 109 | version = "0.2.2" 110 | dependencies = [ 111 | "apple-nvram", 112 | "clap 3.2.25", 113 | "rust-ini", 114 | ] 115 | 116 | [[package]] 117 | name = "atty" 118 | version = "0.2.14" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 121 | dependencies = [ 122 | "hermit-abi", 123 | "libc", 124 | "winapi", 125 | ] 126 | 127 | [[package]] 128 | name = "autocfg" 129 | version = "1.4.0" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 132 | 133 | [[package]] 134 | name = "bitflags" 135 | version = "1.3.2" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 138 | 139 | [[package]] 140 | name = "bitflags" 141 | version = "2.6.0" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 144 | 145 | [[package]] 146 | name = "cfg-if" 147 | version = "1.0.0" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 150 | 151 | [[package]] 152 | name = "clap" 153 | version = "3.2.25" 154 | source = "registry+https://github.com/rust-lang/crates.io-index" 155 | checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" 156 | dependencies = [ 157 | "atty", 158 | "bitflags 1.3.2", 159 | "clap_lex 0.2.4", 160 | "indexmap", 161 | "once_cell", 162 | "strsim 0.10.0", 163 | "termcolor", 164 | "textwrap", 165 | ] 166 | 167 | [[package]] 168 | name = "clap" 169 | version = "4.5.21" 170 | source = "registry+https://github.com/rust-lang/crates.io-index" 171 | checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f" 172 | dependencies = [ 173 | "clap_builder", 174 | "clap_derive", 175 | ] 176 | 177 | [[package]] 178 | name = "clap_builder" 179 | version = "4.5.21" 180 | source = "registry+https://github.com/rust-lang/crates.io-index" 181 | checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec" 182 | dependencies = [ 183 | "anstream", 184 | "anstyle", 185 | "clap_lex 0.7.3", 186 | "strsim 0.11.1", 187 | ] 188 | 189 | [[package]] 190 | name = "clap_derive" 191 | version = "4.5.18" 192 | source = "registry+https://github.com/rust-lang/crates.io-index" 193 | checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" 194 | dependencies = [ 195 | "heck", 196 | "proc-macro2", 197 | "quote", 198 | "syn", 199 | ] 200 | 201 | [[package]] 202 | name = "clap_lex" 203 | version = "0.2.4" 204 | source = "registry+https://github.com/rust-lang/crates.io-index" 205 | checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" 206 | dependencies = [ 207 | "os_str_bytes", 208 | ] 209 | 210 | [[package]] 211 | name = "clap_lex" 212 | version = "0.7.3" 213 | source = "registry+https://github.com/rust-lang/crates.io-index" 214 | checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7" 215 | 216 | [[package]] 217 | name = "colorchoice" 218 | version = "1.0.3" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 221 | 222 | [[package]] 223 | name = "const-random" 224 | version = "0.1.18" 225 | source = "registry+https://github.com/rust-lang/crates.io-index" 226 | checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" 227 | dependencies = [ 228 | "const-random-macro", 229 | ] 230 | 231 | [[package]] 232 | name = "const-random-macro" 233 | version = "0.1.16" 234 | source = "registry+https://github.com/rust-lang/crates.io-index" 235 | checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" 236 | dependencies = [ 237 | "getrandom", 238 | "once_cell", 239 | "tiny-keccak", 240 | ] 241 | 242 | [[package]] 243 | name = "crc" 244 | version = "3.2.1" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" 247 | dependencies = [ 248 | "crc-catalog", 249 | ] 250 | 251 | [[package]] 252 | name = "crc-catalog" 253 | version = "2.4.0" 254 | source = "registry+https://github.com/rust-lang/crates.io-index" 255 | checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" 256 | 257 | [[package]] 258 | name = "crc32fast" 259 | version = "1.4.2" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" 262 | dependencies = [ 263 | "cfg-if", 264 | ] 265 | 266 | [[package]] 267 | name = "crunchy" 268 | version = "0.2.3" 269 | source = "registry+https://github.com/rust-lang/crates.io-index" 270 | checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" 271 | 272 | [[package]] 273 | name = "dbus" 274 | version = "0.9.7" 275 | source = "registry+https://github.com/rust-lang/crates.io-index" 276 | checksum = "1bb21987b9fb1613058ba3843121dd18b163b254d8a6e797e144cbac14d96d1b" 277 | dependencies = [ 278 | "libc", 279 | "libdbus-sys", 280 | "winapi", 281 | ] 282 | 283 | [[package]] 284 | name = "dlv-list" 285 | version = "0.5.2" 286 | source = "registry+https://github.com/rust-lang/crates.io-index" 287 | checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" 288 | dependencies = [ 289 | "const-random", 290 | ] 291 | 292 | [[package]] 293 | name = "getrandom" 294 | version = "0.2.15" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 297 | dependencies = [ 298 | "cfg-if", 299 | "libc", 300 | "wasi", 301 | ] 302 | 303 | [[package]] 304 | name = "gpt" 305 | version = "3.1.0" 306 | source = "registry+https://github.com/rust-lang/crates.io-index" 307 | checksum = "8283e7331b8c93b9756e0cfdbcfb90312852f953c6faf9bf741e684cc3b6ad69" 308 | dependencies = [ 309 | "bitflags 2.6.0", 310 | "crc", 311 | "log", 312 | "uuid", 313 | ] 314 | 315 | [[package]] 316 | name = "hashbrown" 317 | version = "0.12.3" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 320 | 321 | [[package]] 322 | name = "hashbrown" 323 | version = "0.14.5" 324 | source = "registry+https://github.com/rust-lang/crates.io-index" 325 | checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 326 | 327 | [[package]] 328 | name = "heck" 329 | version = "0.5.0" 330 | source = "registry+https://github.com/rust-lang/crates.io-index" 331 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 332 | 333 | [[package]] 334 | name = "hermit-abi" 335 | version = "0.1.19" 336 | source = "registry+https://github.com/rust-lang/crates.io-index" 337 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 338 | dependencies = [ 339 | "libc", 340 | ] 341 | 342 | [[package]] 343 | name = "indexmap" 344 | version = "1.9.3" 345 | source = "registry+https://github.com/rust-lang/crates.io-index" 346 | checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" 347 | dependencies = [ 348 | "autocfg", 349 | "hashbrown 0.12.3", 350 | ] 351 | 352 | [[package]] 353 | name = "is_terminal_polyfill" 354 | version = "1.70.1" 355 | source = "registry+https://github.com/rust-lang/crates.io-index" 356 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 357 | 358 | [[package]] 359 | name = "libc" 360 | version = "0.2.164" 361 | source = "registry+https://github.com/rust-lang/crates.io-index" 362 | checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f" 363 | 364 | [[package]] 365 | name = "libdbus-sys" 366 | version = "0.2.5" 367 | source = "registry+https://github.com/rust-lang/crates.io-index" 368 | checksum = "06085512b750d640299b79be4bad3d2fa90a9c00b1fd9e1b46364f66f0485c72" 369 | dependencies = [ 370 | "pkg-config", 371 | ] 372 | 373 | [[package]] 374 | name = "log" 375 | version = "0.4.22" 376 | source = "registry+https://github.com/rust-lang/crates.io-index" 377 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 378 | 379 | [[package]] 380 | name = "memchr" 381 | version = "2.7.4" 382 | source = "registry+https://github.com/rust-lang/crates.io-index" 383 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 384 | 385 | [[package]] 386 | name = "memoffset" 387 | version = "0.7.1" 388 | source = "registry+https://github.com/rust-lang/crates.io-index" 389 | checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" 390 | dependencies = [ 391 | "autocfg", 392 | ] 393 | 394 | [[package]] 395 | name = "nix" 396 | version = "0.26.4" 397 | source = "registry+https://github.com/rust-lang/crates.io-index" 398 | checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" 399 | dependencies = [ 400 | "bitflags 1.3.2", 401 | "cfg-if", 402 | "libc", 403 | "memoffset", 404 | "pin-utils", 405 | ] 406 | 407 | [[package]] 408 | name = "once_cell" 409 | version = "1.20.2" 410 | source = "registry+https://github.com/rust-lang/crates.io-index" 411 | checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" 412 | 413 | [[package]] 414 | name = "ordered-multimap" 415 | version = "0.7.3" 416 | source = "registry+https://github.com/rust-lang/crates.io-index" 417 | checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" 418 | dependencies = [ 419 | "dlv-list", 420 | "hashbrown 0.14.5", 421 | ] 422 | 423 | [[package]] 424 | name = "os_str_bytes" 425 | version = "6.6.1" 426 | source = "registry+https://github.com/rust-lang/crates.io-index" 427 | checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" 428 | 429 | [[package]] 430 | name = "pin-utils" 431 | version = "0.1.0" 432 | source = "registry+https://github.com/rust-lang/crates.io-index" 433 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 434 | 435 | [[package]] 436 | name = "pkg-config" 437 | version = "0.3.31" 438 | source = "registry+https://github.com/rust-lang/crates.io-index" 439 | checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" 440 | 441 | [[package]] 442 | name = "proc-macro2" 443 | version = "1.0.92" 444 | source = "registry+https://github.com/rust-lang/crates.io-index" 445 | checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" 446 | dependencies = [ 447 | "unicode-ident", 448 | ] 449 | 450 | [[package]] 451 | name = "quote" 452 | version = "1.0.37" 453 | source = "registry+https://github.com/rust-lang/crates.io-index" 454 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 455 | dependencies = [ 456 | "proc-macro2", 457 | ] 458 | 459 | [[package]] 460 | name = "regex" 461 | version = "1.11.1" 462 | source = "registry+https://github.com/rust-lang/crates.io-index" 463 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 464 | dependencies = [ 465 | "aho-corasick", 466 | "memchr", 467 | "regex-automata", 468 | "regex-syntax", 469 | ] 470 | 471 | [[package]] 472 | name = "regex-automata" 473 | version = "0.4.9" 474 | source = "registry+https://github.com/rust-lang/crates.io-index" 475 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 476 | dependencies = [ 477 | "aho-corasick", 478 | "memchr", 479 | "regex-syntax", 480 | ] 481 | 482 | [[package]] 483 | name = "regex-syntax" 484 | version = "0.8.5" 485 | source = "registry+https://github.com/rust-lang/crates.io-index" 486 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 487 | 488 | [[package]] 489 | name = "rust-ini" 490 | version = "0.21.1" 491 | source = "registry+https://github.com/rust-lang/crates.io-index" 492 | checksum = "4e310ef0e1b6eeb79169a1171daf9abcb87a2e17c03bee2c4bb100b55c75409f" 493 | dependencies = [ 494 | "cfg-if", 495 | "ordered-multimap", 496 | "trim-in-place", 497 | ] 498 | 499 | [[package]] 500 | name = "strsim" 501 | version = "0.10.0" 502 | source = "registry+https://github.com/rust-lang/crates.io-index" 503 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 504 | 505 | [[package]] 506 | name = "strsim" 507 | version = "0.11.1" 508 | source = "registry+https://github.com/rust-lang/crates.io-index" 509 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 510 | 511 | [[package]] 512 | name = "syn" 513 | version = "2.0.89" 514 | source = "registry+https://github.com/rust-lang/crates.io-index" 515 | checksum = "44d46482f1c1c87acd84dea20c1bf5ebff4c757009ed6bf19cfd36fb10e92c4e" 516 | dependencies = [ 517 | "proc-macro2", 518 | "quote", 519 | "unicode-ident", 520 | ] 521 | 522 | [[package]] 523 | name = "termcolor" 524 | version = "1.4.1" 525 | source = "registry+https://github.com/rust-lang/crates.io-index" 526 | checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" 527 | dependencies = [ 528 | "winapi-util", 529 | ] 530 | 531 | [[package]] 532 | name = "textwrap" 533 | version = "0.16.1" 534 | source = "registry+https://github.com/rust-lang/crates.io-index" 535 | checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" 536 | 537 | [[package]] 538 | name = "tiny-keccak" 539 | version = "2.0.2" 540 | source = "registry+https://github.com/rust-lang/crates.io-index" 541 | checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" 542 | dependencies = [ 543 | "crunchy", 544 | ] 545 | 546 | [[package]] 547 | name = "trim-in-place" 548 | version = "0.1.7" 549 | source = "registry+https://github.com/rust-lang/crates.io-index" 550 | checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc" 551 | 552 | [[package]] 553 | name = "unicode-ident" 554 | version = "1.0.14" 555 | source = "registry+https://github.com/rust-lang/crates.io-index" 556 | checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" 557 | 558 | [[package]] 559 | name = "utf8parse" 560 | version = "0.2.2" 561 | source = "registry+https://github.com/rust-lang/crates.io-index" 562 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 563 | 564 | [[package]] 565 | name = "uuid" 566 | version = "1.11.0" 567 | source = "registry+https://github.com/rust-lang/crates.io-index" 568 | checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" 569 | dependencies = [ 570 | "getrandom", 571 | ] 572 | 573 | [[package]] 574 | name = "wasi" 575 | version = "0.11.0+wasi-snapshot-preview1" 576 | source = "registry+https://github.com/rust-lang/crates.io-index" 577 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 578 | 579 | [[package]] 580 | name = "winapi" 581 | version = "0.3.9" 582 | source = "registry+https://github.com/rust-lang/crates.io-index" 583 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 584 | dependencies = [ 585 | "winapi-i686-pc-windows-gnu", 586 | "winapi-x86_64-pc-windows-gnu", 587 | ] 588 | 589 | [[package]] 590 | name = "winapi-i686-pc-windows-gnu" 591 | version = "0.4.0" 592 | source = "registry+https://github.com/rust-lang/crates.io-index" 593 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 594 | 595 | [[package]] 596 | name = "winapi-util" 597 | version = "0.1.9" 598 | source = "registry+https://github.com/rust-lang/crates.io-index" 599 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 600 | dependencies = [ 601 | "windows-sys", 602 | ] 603 | 604 | [[package]] 605 | name = "winapi-x86_64-pc-windows-gnu" 606 | version = "0.4.0" 607 | source = "registry+https://github.com/rust-lang/crates.io-index" 608 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 609 | 610 | [[package]] 611 | name = "windows-sys" 612 | version = "0.59.0" 613 | source = "registry+https://github.com/rust-lang/crates.io-index" 614 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 615 | dependencies = [ 616 | "windows-targets", 617 | ] 618 | 619 | [[package]] 620 | name = "windows-targets" 621 | version = "0.52.6" 622 | source = "registry+https://github.com/rust-lang/crates.io-index" 623 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 624 | dependencies = [ 625 | "windows_aarch64_gnullvm", 626 | "windows_aarch64_msvc", 627 | "windows_i686_gnu", 628 | "windows_i686_gnullvm", 629 | "windows_i686_msvc", 630 | "windows_x86_64_gnu", 631 | "windows_x86_64_gnullvm", 632 | "windows_x86_64_msvc", 633 | ] 634 | 635 | [[package]] 636 | name = "windows_aarch64_gnullvm" 637 | version = "0.52.6" 638 | source = "registry+https://github.com/rust-lang/crates.io-index" 639 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 640 | 641 | [[package]] 642 | name = "windows_aarch64_msvc" 643 | version = "0.52.6" 644 | source = "registry+https://github.com/rust-lang/crates.io-index" 645 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 646 | 647 | [[package]] 648 | name = "windows_i686_gnu" 649 | version = "0.52.6" 650 | source = "registry+https://github.com/rust-lang/crates.io-index" 651 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 652 | 653 | [[package]] 654 | name = "windows_i686_gnullvm" 655 | version = "0.52.6" 656 | source = "registry+https://github.com/rust-lang/crates.io-index" 657 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 658 | 659 | [[package]] 660 | name = "windows_i686_msvc" 661 | version = "0.52.6" 662 | source = "registry+https://github.com/rust-lang/crates.io-index" 663 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 664 | 665 | [[package]] 666 | name = "windows_x86_64_gnu" 667 | version = "0.52.6" 668 | source = "registry+https://github.com/rust-lang/crates.io-index" 669 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 670 | 671 | [[package]] 672 | name = "windows_x86_64_gnullvm" 673 | version = "0.52.6" 674 | source = "registry+https://github.com/rust-lang/crates.io-index" 675 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 676 | 677 | [[package]] 678 | name = "windows_x86_64_msvc" 679 | version = "0.52.6" 680 | source = "registry+https://github.com/rust-lang/crates.io-index" 681 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 682 | -------------------------------------------------------------------------------- /apple-nvram/src/v3.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | borrow::Cow, 3 | fmt::{Display, Formatter}, 4 | ops::ControlFlow, 5 | }; 6 | 7 | use crate::{Error, VarType}; 8 | 9 | // https://github.com/apple-oss-distributions/xnu/blob/main/iokit/Kernel/IONVRAMV3Handler.cpp#L630 10 | 11 | const VARIABLE_STORE_SIGNATURE: &[u8; 4] = b"3VVN"; 12 | const VARIABLE_STORE_VERSION: u8 = 0x1; 13 | const VARIABLE_DATA: u16 = 0x55AA; 14 | 15 | const PARTITION_SIZE: usize = 0x10000; 16 | const STORE_HEADER_SIZE: usize = 24; 17 | const VAR_HEADER_SIZE: usize = 36; 18 | const VAR_ADDED: u8 = 0x7F; 19 | const VAR_IN_DELETED_TRANSITION: u8 = 0xFE; 20 | const VAR_DELETED: u8 = 0xFD; 21 | 22 | const APPLE_COMMON_VARIABLE_GUID: &[u8; 16] = &[ 23 | 0x7C, 0x43, 0x61, 0x10, 0xAB, 0x2A, 0x4B, 0xBB, 0xA8, 0x80, 0xFE, 0x41, 0x99, 0x5C, 0x9F, 0x82, 24 | ]; 25 | const APPLE_SYSTEM_VARIABLE_GUID: &[u8; 16] = &[ 26 | 0x40, 0xA0, 0xDD, 0xD2, 0x77, 0xF8, 0x43, 0x92, 0xB4, 0xA3, 0x1E, 0x73, 0x04, 0x20, 0x65, 0x16, 27 | ]; 28 | 29 | #[derive(Debug, Default)] 30 | enum Slot { 31 | Valid(T), 32 | Invalid, 33 | #[default] 34 | Empty, 35 | } 36 | 37 | impl Slot { 38 | pub const fn as_ref(&self) -> Slot<&T> { 39 | match self { 40 | Slot::Valid(v) => Slot::Valid(v), 41 | Slot::Invalid => Slot::Invalid, 42 | Slot::Empty => Slot::Empty, 43 | } 44 | } 45 | 46 | pub fn as_mut(&mut self) -> Slot<&mut T> { 47 | match self { 48 | Slot::Valid(v) => Slot::Valid(v), 49 | Slot::Invalid => Slot::Invalid, 50 | Slot::Empty => Slot::Empty, 51 | } 52 | } 53 | 54 | pub fn unwrap(self) -> T { 55 | match self { 56 | Slot::Valid(v) => v, 57 | Slot::Invalid => panic!("called `Slot::unwrap()` on an `Invalid` value"), 58 | Slot::Empty => panic!("called `Slot::unwrap()` on an `Empty` value"), 59 | } 60 | } 61 | 62 | pub fn empty(&self) -> bool { 63 | match self { 64 | Slot::Empty => true, 65 | _ => false, 66 | } 67 | } 68 | } 69 | 70 | #[derive(Debug)] 71 | pub struct Nvram<'a> { 72 | partitions: [Slot>; 16], 73 | partition_count: usize, 74 | active: usize, 75 | } 76 | 77 | impl<'a> Nvram<'a> { 78 | pub fn parse(nvr: &'a [u8]) -> crate::Result> { 79 | let partition_count = nvr.len() / PARTITION_SIZE; 80 | let mut partitions: [Slot>; 16] = Default::default(); 81 | let mut active = 0; 82 | let mut max_gen = 0; 83 | let mut valid_partitions = 0; 84 | 85 | for i in 0..partition_count { 86 | let offset = i * PARTITION_SIZE; 87 | if offset >= nvr.len() { 88 | break; 89 | } 90 | match Partition::parse(&nvr[offset..offset + PARTITION_SIZE]) { 91 | Ok(p) => { 92 | let p_gen = p.generation(); 93 | if p_gen > max_gen { 94 | active = i; 95 | max_gen = p_gen; 96 | } 97 | partitions[i] = Slot::Valid(p); 98 | valid_partitions += 1; 99 | } 100 | Err(V3Error::Empty) => { 101 | partitions[i] = Slot::Empty; 102 | } 103 | Err(_) => { 104 | partitions[i] = Slot::Invalid; 105 | } 106 | } 107 | } 108 | 109 | if valid_partitions == 0 { 110 | return Err(Error::ParseError); 111 | } 112 | 113 | Ok(Nvram { 114 | partitions, 115 | partition_count, 116 | active, 117 | }) 118 | } 119 | 120 | fn partitions(&self) -> impl Iterator> { 121 | self.partitions 122 | .iter() 123 | .take(self.partition_count) 124 | .filter_map(|x| match x { 125 | Slot::Valid(p) => Some(p), 126 | Slot::Invalid => None, 127 | Slot::Empty => None, 128 | }) 129 | } 130 | 131 | fn active_part(&self) -> &Partition<'a> { 132 | self.partitions[self.active].as_ref().unwrap() 133 | } 134 | 135 | #[cfg(test)] 136 | fn active_part_mut(&mut self) -> &mut Partition<'a> { 137 | self.partitions[self.active].as_mut().unwrap() 138 | } 139 | } 140 | 141 | impl<'a> crate::Nvram<'a> for Nvram<'a> { 142 | fn serialize(&self) -> crate::Result> { 143 | let mut v = Vec::with_capacity(self.partition_count * PARTITION_SIZE); 144 | for p in self.partitions() { 145 | p.serialize(&mut v); 146 | } 147 | Ok(v) 148 | } 149 | 150 | fn prepare_for_write(&mut self) { 151 | // nop 152 | } 153 | 154 | fn partitions(&self) -> Box> + '_> { 155 | Box::new(self.partitions().map(|p| p as &dyn crate::Partition<'a>)) 156 | } 157 | 158 | fn active_part_mut(&mut self) -> &mut dyn crate::Partition<'a> { 159 | self.partitions[self.active].as_mut().unwrap() 160 | } 161 | 162 | fn apply(&mut self, w: &mut dyn crate::NvramWriter) -> crate::Result<()> { 163 | let ap = self.active_part(); 164 | let offset; 165 | // there aren't really any sections in v3 but the store header still 166 | // specifies limits for maximum combined size of each kind of variable 167 | if ap.system_used() > ap.system_size() { 168 | return Err(Error::SectionTooBig); 169 | } 170 | if ap.common_used() > ap.common_size() { 171 | return Err(Error::SectionTooBig); 172 | } 173 | 174 | // if total size is too big, copy added variables to the next bank 175 | if ap.total_used() <= ap.usable_size() { 176 | offset = (self.active * PARTITION_SIZE) as u32; 177 | } else { 178 | let new_active = (self.active + 1) % self.partition_count; 179 | offset = (new_active * PARTITION_SIZE) as u32; 180 | if !self.partitions[new_active].empty() { 181 | w.erase_if_needed(offset, PARTITION_SIZE); 182 | } 183 | // must only clone 0x7F variables to the next partition 184 | self.partitions[new_active] = Slot::Valid( 185 | self.partitions[self.active] 186 | .as_ref() 187 | .unwrap() 188 | .clone_active(), 189 | ); 190 | self.active = new_active; 191 | // we could still have too many active variables 192 | if self.active_part().total_used() > PARTITION_SIZE { 193 | return Err(Error::SectionTooBig); 194 | } 195 | } 196 | 197 | let mut data = Vec::with_capacity(PARTITION_SIZE); 198 | self.active_part().serialize(&mut data); 199 | w.write_all(offset, &data) 200 | .map_err(|e| Error::ApplyError(e))?; 201 | Ok(()) 202 | } 203 | } 204 | 205 | #[derive(Debug, Clone)] 206 | pub struct Partition<'a> { 207 | pub header: StoreHeader<'a>, 208 | pub values: Vec>, 209 | empty_region_end: usize, 210 | } 211 | 212 | #[derive(Debug)] 213 | enum V3Error { 214 | ParseError, 215 | Empty, 216 | } 217 | 218 | type Result = std::result::Result; 219 | 220 | impl<'a> Partition<'a> { 221 | fn parse(nvr: &'a [u8]) -> Result> { 222 | if let Ok(header) = StoreHeader::parse(&nvr[..STORE_HEADER_SIZE]) { 223 | let mut offset = STORE_HEADER_SIZE; 224 | let mut values = Vec::new(); 225 | // one byte past the last 0xFF or the end of partition 226 | let mut empty_region_end = header.size(); 227 | 228 | while offset + VAR_HEADER_SIZE < header.size() { 229 | let mut empty = true; 230 | for i in 0..VAR_HEADER_SIZE { 231 | if nvr[offset + i] != 0 && nvr[offset + i] != 0xFF { 232 | empty = false; 233 | break; 234 | } 235 | } 236 | if empty { 237 | // check where exactly the "empty" space ends 238 | // (it might not be at the end of partition) 239 | offset += VAR_HEADER_SIZE; 240 | while offset < header.size() { 241 | if nvr[offset] != 0xFF { 242 | empty_region_end = offset; 243 | break; 244 | } 245 | offset += 1 246 | } 247 | break; 248 | } 249 | 250 | let Ok(v_header) = VarHeader::parse(&nvr[offset..]) else { 251 | // if there's no valid header, just end here and return values parsed so far 252 | // we also know there is no space for adding any new or updated variables 253 | empty_region_end = offset; 254 | break; 255 | }; 256 | 257 | let k_begin = offset + VAR_HEADER_SIZE; 258 | let k_end = k_begin + v_header.name_size as usize; 259 | let key = &nvr[k_begin..k_end - 1]; 260 | 261 | let v_begin = k_end; 262 | let v_end = v_begin + v_header.data_size as usize; 263 | let value = &nvr[v_begin..v_end]; 264 | 265 | let crc = crc32fast::hash(value); 266 | if crc != v_header.crc { 267 | return Err(V3Error::ParseError); 268 | } 269 | let v = Variable { 270 | header: v_header, 271 | key: Cow::Borrowed(key), 272 | value: Cow::Borrowed(value), 273 | }; 274 | 275 | offset += v.size(); 276 | values.push(v); 277 | } 278 | 279 | Ok(Partition { 280 | header, 281 | values, 282 | empty_region_end, 283 | }) 284 | } else { 285 | match nvr.iter().copied().try_for_each(|v| match v { 286 | 0xFF => ControlFlow::Continue(()), 287 | _ => ControlFlow::Break(()), 288 | }) { 289 | ControlFlow::Continue(_) => Err(V3Error::Empty), 290 | ControlFlow::Break(_) => Err(V3Error::ParseError), 291 | } 292 | } 293 | } 294 | 295 | fn generation(&self) -> u32 { 296 | self.header.generation 297 | } 298 | 299 | fn entries<'b, 'c>( 300 | &'b mut self, 301 | key: &'c [u8], 302 | typ: VarType, 303 | ) -> impl Iterator> 304 | where 305 | 'a: 'b, 306 | 'c: 'b, 307 | { 308 | self.values 309 | .iter_mut() 310 | .filter(move |e| e.key == key && e.typ() == typ) 311 | } 312 | 313 | fn entries_added<'b, 'c>( 314 | &'b mut self, 315 | key: &'c [u8], 316 | typ: VarType, 317 | ) -> impl Iterator> 318 | where 319 | 'a: 'b, 320 | 'c: 'b, 321 | { 322 | self.entries(key, typ) 323 | .filter(|v| v.header.state == VAR_ADDED) 324 | } 325 | 326 | // total size of store header + all variables including the inactive duplicates 327 | fn total_used(&self) -> usize { 328 | STORE_HEADER_SIZE + self.values.iter().fold(0, |acc, v| acc + v.size()) 329 | } 330 | 331 | // size of active system variables 332 | fn system_used(&self) -> usize { 333 | self.values 334 | .iter() 335 | .filter(|&v| v.header.state == VAR_ADDED && v.header.guid == APPLE_SYSTEM_VARIABLE_GUID) 336 | .fold(0, |acc, v| acc + v.size()) 337 | } 338 | 339 | // size of active common variables 340 | fn common_used(&self) -> usize { 341 | self.values 342 | .iter() 343 | .filter(|&v| v.header.state == VAR_ADDED && v.header.guid == APPLE_COMMON_VARIABLE_GUID) 344 | .fold(0, |acc, v| acc + v.size()) 345 | } 346 | 347 | fn system_size(&self) -> usize { 348 | self.header.system_size as usize 349 | } 350 | 351 | fn common_size(&self) -> usize { 352 | self.header.common_size as usize 353 | } 354 | 355 | // total usable size, usually equal to partition size 356 | // unless there are any non-0xFF bytes after last valid variable 357 | fn usable_size(&self) -> usize { 358 | self.empty_region_end 359 | } 360 | 361 | fn serialize(&self, v: &mut Vec) { 362 | let start_size = v.len(); 363 | self.header.serialize(v); 364 | // Here we actually want to iterate over all versions of variables so we use the struct field directly. 365 | for var in &self.values { 366 | var.serialize(v); 367 | } 368 | let my_size = v.len() - start_size; 369 | debug_assert!(v.len() == self.total_used()); 370 | 371 | // padding 372 | for _ in 0..(self.header.size() - my_size) { 373 | v.push(0xFF); 374 | } 375 | } 376 | 377 | fn variables(&self) -> impl Iterator> { 378 | self.values.iter().filter(|v| v.header.state == VAR_ADDED) 379 | } 380 | 381 | fn clone_active(&self) -> Partition<'a> { 382 | let mut header = self.header.clone(); 383 | header.generation += 1; 384 | Partition { 385 | header, 386 | values: self 387 | .values 388 | .iter() 389 | .filter_map(|v| { 390 | if v.header.state == VAR_ADDED { 391 | Some(v.clone()) 392 | } else { 393 | None 394 | } 395 | }) 396 | .collect(), 397 | empty_region_end: self.header.size(), 398 | } 399 | } 400 | } 401 | 402 | impl<'a> crate::Partition<'a> for Partition<'a> { 403 | fn get_variable(&self, key: &[u8], typ: VarType) -> Option<&dyn crate::Variable<'a>> { 404 | self.values.iter().find_map(|e| { 405 | if e.key == key && e.typ() == typ && e.header.state == VAR_ADDED { 406 | Some(e as &dyn crate::Variable<'a>) 407 | } else { 408 | None 409 | } 410 | }) 411 | } 412 | 413 | fn insert_variable(&mut self, key: &[u8], value: Cow<'a, [u8]>, typ: VarType) { 414 | // invalidate any previous variable instances 415 | for var in self.entries_added(key, typ) { 416 | var.header.state = var.header.state & VAR_DELETED & VAR_IN_DELETED_TRANSITION; 417 | } 418 | 419 | let guid = match typ { 420 | VarType::Common => APPLE_COMMON_VARIABLE_GUID, 421 | VarType::System => APPLE_SYSTEM_VARIABLE_GUID, 422 | }; 423 | let var = Variable { 424 | header: VarHeader { 425 | state: VAR_ADDED, 426 | attrs: 0, 427 | name_size: (key.len() + 1) as u32, 428 | data_size: value.len() as u32, 429 | guid, 430 | crc: crc32fast::hash(&value), 431 | }, 432 | key: Cow::Owned(key.into()), 433 | value, 434 | }; 435 | self.values.push(var); 436 | } 437 | 438 | fn remove_variable(&mut self, key: &[u8], typ: VarType) { 439 | // invalidate all previous variable instances 440 | for var in self.entries_added(key, typ) { 441 | var.header.state = var.header.state & VAR_DELETED & VAR_IN_DELETED_TRANSITION; 442 | } 443 | } 444 | 445 | fn variables(&self) -> Box> + '_> { 446 | Box::new(self.variables().map(|e| e as &dyn crate::Variable<'a>)) 447 | } 448 | } 449 | 450 | impl Display for Partition<'_> { 451 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 452 | write!( 453 | f, 454 | "size: {}, total_used: {}, system_used: {}, common_used: {}, generation: 0x{:02x}, state: 0x{:02x}, flags: 0x{:02x}, count: {}", 455 | self.header.size, 456 | self.total_used(), 457 | self.system_used(), 458 | self.common_used(), 459 | self.generation(), 460 | self.header.state, 461 | self.header.flags, 462 | self.values.len() 463 | ) 464 | } 465 | } 466 | 467 | #[derive(Debug, Clone)] 468 | pub struct StoreHeader<'a> { 469 | pub name: &'a [u8], 470 | pub size: u32, 471 | pub generation: u32, 472 | pub state: u8, 473 | pub flags: u8, 474 | pub version: u8, 475 | pub system_size: u32, 476 | pub common_size: u32, 477 | } 478 | 479 | impl<'a> StoreHeader<'a> { 480 | fn parse(nvr: &[u8]) -> Result> { 481 | let name = &nvr[..4]; 482 | let size = u32::from_le_bytes(nvr[4..8].try_into().unwrap()); 483 | let generation = u32::from_le_bytes(nvr[8..12].try_into().unwrap()); 484 | let state = nvr[12]; 485 | let flags = nvr[13]; 486 | let version = nvr[14]; 487 | let system_size = u32::from_le_bytes(nvr[16..20].try_into().unwrap()); 488 | let common_size = u32::from_le_bytes(nvr[20..24].try_into().unwrap()); 489 | 490 | if name != VARIABLE_STORE_SIGNATURE { 491 | return Err(V3Error::ParseError); 492 | } 493 | if version != VARIABLE_STORE_VERSION { 494 | return Err(V3Error::ParseError); 495 | } 496 | 497 | Ok(StoreHeader { 498 | name, 499 | size, 500 | generation, 501 | state, 502 | flags, 503 | version, 504 | system_size, 505 | common_size, 506 | }) 507 | } 508 | 509 | fn serialize(&self, v: &mut Vec) { 510 | v.extend_from_slice(VARIABLE_STORE_SIGNATURE); 511 | v.extend_from_slice(&self.size.to_le_bytes()); 512 | v.extend_from_slice(&self.generation.to_le_bytes()); 513 | v.push(self.state); 514 | v.push(self.flags); 515 | v.push(self.version); 516 | v.push(0); // reserved 517 | v.extend_from_slice(&self.system_size.to_le_bytes()); 518 | v.extend_from_slice(&self.common_size.to_le_bytes()); 519 | } 520 | 521 | fn size(&self) -> usize { 522 | self.size as usize 523 | } 524 | } 525 | 526 | #[derive(Debug, Default, Clone)] 527 | pub struct Variable<'a> { 528 | pub header: VarHeader<'a>, 529 | pub key: Cow<'a, [u8]>, 530 | pub value: Cow<'a, [u8]>, 531 | } 532 | 533 | impl<'a> Variable<'a> { 534 | fn size(&self) -> usize { 535 | VAR_HEADER_SIZE + (self.header.name_size + self.header.data_size) as usize 536 | } 537 | 538 | fn typ(&self) -> VarType { 539 | if self.header.guid == APPLE_SYSTEM_VARIABLE_GUID { 540 | return VarType::System; 541 | } 542 | VarType::Common 543 | } 544 | 545 | fn serialize(&self, v: &mut Vec) { 546 | self.header.serialize(v); 547 | v.extend_from_slice(&self.key); 548 | v.push(0); 549 | v.extend_from_slice(&self.value); 550 | } 551 | } 552 | 553 | impl<'a> crate::Variable<'a> for Variable<'a> { 554 | fn value(&self) -> Cow<'a, [u8]> { 555 | self.value.clone() 556 | } 557 | } 558 | 559 | impl Display for Variable<'_> { 560 | fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { 561 | let key = String::from_utf8_lossy(&self.key); 562 | let mut value = String::new(); 563 | for c in self.value.iter().copied() { 564 | if (c as char).is_ascii() && !(c as char).is_ascii_control() { 565 | value.push(c as char); 566 | } else { 567 | value.push_str(&format!("%{c:02x}")); 568 | } 569 | } 570 | 571 | write!(f, "{}:{}={}", self.typ(), key, value) 572 | } 573 | } 574 | 575 | #[derive(Debug, Default, Clone)] 576 | pub struct VarHeader<'a> { 577 | pub state: u8, 578 | pub attrs: u32, 579 | pub name_size: u32, 580 | pub data_size: u32, 581 | pub guid: &'a [u8], 582 | pub crc: u32, 583 | } 584 | 585 | impl<'a> VarHeader<'a> { 586 | fn parse(nvr: &[u8]) -> Result> { 587 | let start_id = u16::from_le_bytes(nvr[..2].try_into().unwrap()); 588 | if start_id != VARIABLE_DATA { 589 | return Err(V3Error::ParseError); 590 | } 591 | let state = nvr[2]; 592 | let attrs = u32::from_le_bytes(nvr[4..8].try_into().unwrap()); 593 | let name_size = u32::from_le_bytes(nvr[8..12].try_into().unwrap()); 594 | let data_size = u32::from_le_bytes(nvr[12..16].try_into().unwrap()); 595 | let guid = &nvr[16..32]; 596 | let crc = u32::from_le_bytes(nvr[32..36].try_into().unwrap()); 597 | 598 | if VAR_HEADER_SIZE + (name_size + data_size) as usize > nvr.len() { 599 | return Err(V3Error::ParseError); 600 | } 601 | 602 | Ok(VarHeader { 603 | state, 604 | attrs, 605 | name_size, 606 | data_size, 607 | guid, 608 | crc, 609 | }) 610 | } 611 | 612 | fn serialize(&self, v: &mut Vec) { 613 | v.extend_from_slice(&VARIABLE_DATA.to_le_bytes()); 614 | v.push(self.state); 615 | v.push(0); // reserved 616 | v.extend_from_slice(&self.attrs.to_le_bytes()); 617 | v.extend_from_slice(&self.name_size.to_le_bytes()); 618 | v.extend_from_slice(&self.data_size.to_le_bytes()); 619 | v.extend_from_slice(self.guid); 620 | v.extend_from_slice(&self.crc.to_le_bytes()); 621 | } 622 | } 623 | 624 | #[cfg(test)] 625 | mod tests { 626 | use super::*; 627 | use crate::{Nvram as NvramT, NvramWriter, Partition}; 628 | 629 | struct TestNvram { 630 | data: Vec, 631 | erase_count: usize, 632 | } 633 | 634 | impl TestNvram { 635 | fn new(data: Vec) -> TestNvram { 636 | Self { 637 | data, 638 | erase_count: 0, 639 | } 640 | } 641 | 642 | fn get_data(&self) -> &[u8] { 643 | &self.data 644 | } 645 | } 646 | 647 | impl NvramWriter for TestNvram { 648 | fn erase_if_needed(&mut self, offset: u32, size: usize) { 649 | for b in self.data.iter_mut().skip(offset as usize).take(size) { 650 | *b = 0xFF; 651 | } 652 | self.erase_count += 1; 653 | } 654 | 655 | fn write_all(&mut self, offset: u32, buf: &[u8]) -> std::io::Result<()> { 656 | for (d, s) in self 657 | .data 658 | .iter_mut() 659 | .skip(offset as usize) 660 | .take(buf.len()) 661 | .zip(buf.iter().copied()) 662 | { 663 | *d &= s; 664 | } 665 | Ok(()) 666 | } 667 | } 668 | 669 | #[rustfmt::skip] 670 | fn store_header() -> &'static [u8] { 671 | &[ 672 | 0x33, 0x56, 0x56, 0x4e, 0x00, 0x00, 0x01, 0x00, 673 | 0x01, 0x00, 0x00, 0x00, 0xfe, 0x5a, 0x01, 0x00, 674 | 0x00, 0x40, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 675 | ] 676 | } 677 | 678 | fn empty_nvram(bank_count: usize) -> Vec { 679 | let mut data = vec![0xFF; PARTITION_SIZE * bank_count]; 680 | data[0..STORE_HEADER_SIZE].copy_from_slice(store_header()); 681 | data 682 | } 683 | 684 | #[test] 685 | fn test_insert_variable() -> crate::Result<()> { 686 | let mut nvr = TestNvram::new(empty_nvram(2)); 687 | let data = nvr.get_data().to_owned(); 688 | let mut nv = Nvram::parse(&data)?; 689 | 690 | assert!(matches!(nv.partitions[0], Slot::Valid(_))); 691 | assert!(matches!(nv.partitions[1], Slot::Empty)); 692 | 693 | nv.active_part_mut().insert_variable( 694 | b"test-variable", 695 | Cow::Borrowed(b"test-value"), 696 | VarType::Common, 697 | ); 698 | 699 | // write changes 700 | nv.apply(&mut nvr)?; 701 | assert_eq!(nvr.erase_count, 0); 702 | 703 | // try to parse again 704 | let data_after = nvr.get_data().to_owned(); 705 | let mut nv_after = Nvram::parse(&data_after)?; 706 | 707 | let test_var = nv_after 708 | .active_part() 709 | .get_variable(b"test-variable", VarType::Common) 710 | .unwrap(); 711 | 712 | assert_eq!(test_var.value(), Cow::Borrowed(b"test-value")); 713 | 714 | let test_var_entries: Vec<_> = nv_after 715 | .active_part_mut() 716 | .entries(b"test-variable", VarType::Common) 717 | .collect(); 718 | 719 | assert_eq!(test_var_entries.len(), 1); 720 | assert_eq!(test_var_entries[0].header.state, VAR_ADDED); 721 | 722 | // update variable 723 | nv_after.active_part_mut().insert_variable( 724 | b"test-variable", 725 | Cow::Borrowed(b"test-value2"), 726 | VarType::Common, 727 | ); 728 | 729 | // write changes 730 | nv_after.apply(&mut nvr)?; 731 | assert_eq!(nvr.erase_count, 0); 732 | 733 | // try to parse again 734 | let data_after2 = nvr.get_data().to_owned(); 735 | let mut nv_after2 = Nvram::parse(&data_after2)?; 736 | 737 | let test_var2 = nv_after2 738 | .active_part() 739 | .get_variable(b"test-variable", VarType::Common) 740 | .unwrap(); 741 | 742 | assert_eq!(test_var2.value(), Cow::Borrowed(b"test-value2")); 743 | 744 | let test_var2_entries: Vec<_> = nv_after2 745 | .active_part_mut() 746 | .entries(b"test-variable", VarType::Common) 747 | .collect(); 748 | 749 | assert_eq!(test_var2_entries.len(), 2); 750 | assert_eq!( 751 | test_var2_entries[0].header.state, 752 | VAR_ADDED & VAR_DELETED & VAR_IN_DELETED_TRANSITION 753 | ); 754 | assert_eq!(test_var2_entries[1].header.state, VAR_ADDED); 755 | 756 | Ok(()) 757 | } 758 | 759 | #[test] 760 | fn test_write_to_next_bank() -> crate::Result<()> { 761 | let mut nvr = TestNvram::new(empty_nvram(2)); 762 | // write something to the second bank to force nvram erase later 763 | nvr.data[0x10000..0x10007].copy_from_slice(b"garbage"); 764 | 765 | let data = nvr.get_data().to_owned(); 766 | let mut nv = Nvram::parse(&data)?; 767 | 768 | assert!(matches!(nv.partitions[0], Slot::Valid(_))); 769 | assert!(matches!(nv.partitions[1], Slot::Invalid)); 770 | 771 | let orig_sys_val = vec![b'.'; 8192]; 772 | nv.active_part_mut().insert_variable( 773 | b"test-large-variable", 774 | Cow::Borrowed(&orig_sys_val), 775 | VarType::System, 776 | ); 777 | 778 | let orig_common_val = vec![b'.'; 24576]; 779 | nv.active_part_mut().insert_variable( 780 | b"test-large-variable", 781 | Cow::Borrowed(&orig_common_val), 782 | VarType::Common, 783 | ); 784 | 785 | // write changes 786 | nv.apply(&mut nvr)?; 787 | assert_eq!(nvr.erase_count, 0); 788 | assert_eq!(nv.active, 0); 789 | 790 | // try to parse again 791 | let data_after = nvr.get_data().to_owned(); 792 | let mut nv_after = Nvram::parse(&data_after)?; 793 | assert_eq!(nv_after.active_part().header.generation, 1); 794 | 795 | assert!(matches!(nv_after.partitions[0], Slot::Valid(_))); 796 | assert!(matches!(nv_after.partitions[1], Slot::Invalid)); 797 | 798 | // update variable 799 | let updated_sys_val = vec![b'.'; 9000]; 800 | nv_after.active_part_mut().insert_variable( 801 | b"test-large-variable", 802 | Cow::Borrowed(&updated_sys_val), 803 | VarType::System, 804 | ); 805 | 806 | let updated_common_val = vec![b'.'; 25000]; 807 | nv_after.active_part_mut().insert_variable( 808 | b"test-large-variable", 809 | Cow::Borrowed(&updated_common_val), 810 | VarType::Common, 811 | ); 812 | 813 | assert_eq!(nv_after.active_part().values.len(), 4); 814 | 815 | // write changes 816 | nv_after.apply(&mut nvr)?; 817 | assert_eq!(nvr.erase_count, 1); 818 | assert_eq!(nv_after.active, 1); 819 | assert_eq!(nv_after.active_part().values.len(), 2); 820 | 821 | // try to parse again 822 | let data_after2 = nvr.get_data().to_owned(); 823 | let nv_after2 = Nvram::parse(&data_after2)?; 824 | assert_eq!(nv_after2.active, 1); 825 | assert_eq!(nv_after2.active_part().values.len(), 2); 826 | assert_eq!(nv_after2.active_part().header.generation, 2); 827 | 828 | assert!(matches!(nv_after2.partitions[0], Slot::Valid(_))); 829 | assert!(matches!(nv_after2.partitions[1], Slot::Valid(_))); 830 | 831 | let test_sys_var2 = nv_after2 832 | .active_part() 833 | .get_variable(b"test-large-variable", VarType::System) 834 | .unwrap(); 835 | 836 | let test_common_var2 = nv_after2 837 | .active_part() 838 | .get_variable(b"test-large-variable", VarType::Common) 839 | .unwrap(); 840 | 841 | assert_eq!(test_sys_var2.value(), Cow::Borrowed(&updated_sys_val)); 842 | assert_eq!(test_common_var2.value(), Cow::Borrowed(&updated_common_val)); 843 | 844 | let test_old_sys_var = nv_after2.partitions[0] 845 | .as_ref() 846 | .unwrap() 847 | .get_variable(b"test-large-variable", VarType::System) 848 | .unwrap(); 849 | 850 | let test_old_common_var = nv_after2.partitions[0] 851 | .as_ref() 852 | .unwrap() 853 | .get_variable(b"test-large-variable", VarType::Common) 854 | .unwrap(); 855 | 856 | assert_eq!(test_old_sys_var.value(), Cow::Borrowed(&orig_sys_val)); 857 | assert_eq!(test_old_common_var.value(), Cow::Borrowed(&orig_common_val)); 858 | 859 | Ok(()) 860 | } 861 | 862 | #[test] 863 | fn test_insert_with_low_space() -> crate::Result<()> { 864 | let mut nvr = TestNvram::new(empty_nvram(2)); 865 | // this will shrink usable size to 100 bytes 866 | nvr.data[STORE_HEADER_SIZE + 100] = 0x42; 867 | 868 | let data = nvr.get_data().to_owned(); 869 | let mut nv = Nvram::parse(&data)?; 870 | 871 | assert!(matches!(nv.partitions[0], Slot::Valid(_))); 872 | assert!(matches!(nv.partitions[1], Slot::Empty)); 873 | 874 | nv.active_part_mut().insert_variable( 875 | b"test-variable", 876 | Cow::Borrowed(b"test-value"), 877 | VarType::Common, 878 | ); 879 | 880 | // write changes 881 | nv.apply(&mut nvr)?; 882 | assert_eq!(nvr.erase_count, 0); 883 | assert_eq!(nv.active, 0); 884 | 885 | // try to parse again 886 | let data_after = nvr.get_data().to_owned(); 887 | let mut nv_after = Nvram::parse(&data_after)?; 888 | 889 | let test_var = nv_after 890 | .active_part() 891 | .get_variable(b"test-variable", VarType::Common) 892 | .unwrap(); 893 | 894 | assert_eq!(test_var.value(), Cow::Borrowed(b"test-value")); 895 | 896 | let test_var_entries: Vec<_> = nv_after 897 | .active_part_mut() 898 | .entries(b"test-variable", VarType::Common) 899 | .collect(); 900 | 901 | assert_eq!(test_var_entries.len(), 1); 902 | assert_eq!(test_var_entries[0].header.state, VAR_ADDED); 903 | 904 | // update variable 905 | nv_after.active_part_mut().insert_variable( 906 | b"test-variable", 907 | Cow::Borrowed(b"test-value2"), 908 | VarType::Common, 909 | ); 910 | 911 | // write changes 912 | nv_after.apply(&mut nvr)?; 913 | assert_eq!(nvr.erase_count, 0); 914 | assert_eq!(nv_after.active, 1); 915 | 916 | Ok(()) 917 | } 918 | } 919 | --------------------------------------------------------------------------------