feat: generate and verify MSIX registry hives

Closes #3
This commit is contained in:
jychoi4 committed 2026-11-03 16:34:00 +09:00
1 parent 6213fec7d2
commit fdbc9a9d1b
7 files changed
+598 -2

No files matched your search

+18 -2
View File
@@ -1,5 +1,6 @@
mod files;
mod process;
mod registry;
mod store;
pub use store::*;
@@ -28,7 +29,9 @@ impl PackageBackend for MsixBackend {
local: &LocalConfig,
root: &Path,
) -> Result<Vec<ResolvedFile>> {
ensure!(project.registry.is_empty(), "UnsupportedRegistry: registry packaging is not enabled yet");
if !project.registry.is_empty() {
registry::available()?;
}
files::resolve(project, local, root)
}
fn tool(&self, local: &LocalConfig) -> Result<Tool> {
@@ -70,7 +73,13 @@ impl PackageBackend for MsixBackend {
file.target
);
}
let registry_hash: Option<String> = None;
let registry_path = stage.join("registry.dat");
let registry_hash = if plan.project.registry.is_empty() {
None
} else {
registry::create(&plan.project.registry, &registry_path, cancel)?;
Some(hash_file(&registry_path)?)
};
let manifest = package_domain::manifest(&plan.project);
fs::write(stage.join("AppxManifest.xml"), &manifest)?;
fs::write(work.join("AppxManifest.xml"), &manifest)?;
@@ -118,6 +127,13 @@ impl PackageBackend for MsixBackend {
file.target
);
}
if let Some(expected) = &registry_hash {
ensure!(
&hash_file(&unpack.join("registry.dat"))? == expected,
"RegistryMismatch: packed registry.dat hash"
);
registry::verify(&plan.project.registry, &unpack.join("registry.dat"), cancel)?;
}
atomic_json(
&work.join("registry-report.json"),
&serde_json::json!({
+323
View File
@@ -0,0 +1,323 @@
//! Offline-only MSIX registry generation. Never mounts or writes a live registry hive.
use anyhow::{ensure, Result};
use package_domain::RegistryEntry;
use std::{path::Path, sync::atomic::AtomicBool};
pub fn available() -> Result<()> {
#[cfg(windows)]
{
native::Api::load()?;
Ok(())
}
#[cfg(not(windows))]
{
anyhow::bail!(
"RegistryPlatform: registry.dat generation requires Windows and System32/offreg.dll"
)
}
}
pub fn create(entries: &[RegistryEntry], path: &Path, cancel: &AtomicBool) -> Result<()> {
ensure!(
package_domain::validate_registry(entries).is_empty(),
"InvalidRegistry: invalid registry entries"
);
ensure!(!path.exists(), "RegistryOutputExists");
#[cfg(windows)]
{
native::create(entries, path, cancel)
}
#[cfg(not(windows))]
{
let _ = cancel;
available()
}
}
pub fn verify(entries: &[RegistryEntry], path: &Path, cancel: &AtomicBool) -> Result<()> {
#[cfg(windows)]
{
native::verify(entries, path, cancel)
}
#[cfg(not(windows))]
{
let _ = (entries, path, cancel);
available()
}
}
#[cfg(windows)]
mod native {
use super::*;
use anyhow::Context;
use std::{ffi::c_void, os::windows::ffi::OsStrExt, ptr::null_mut};
use windows_sys::Win32::{
Foundation::{FreeLibrary, HMODULE},
System::LibraryLoader::{GetProcAddress, LoadLibraryExW, LOAD_LIBRARY_SEARCH_SYSTEM32},
};
type Raw = *mut c_void;
type CreateHive = unsafe extern "system" fn(*mut Raw) -> u32;
type OpenHive = unsafe extern "system" fn(*const u16, *mut Raw) -> u32;
type Close = unsafe extern "system" fn(Raw) -> u32;
type CreateKey =
unsafe extern "system" fn(Raw, *const u16, *mut u16, u32, Raw, *mut Raw, *mut u32) -> u32;
type SetValue = unsafe extern "system" fn(Raw, *const u16, u32, *const u8, u32) -> u32;
type GetValue =
unsafe extern "system" fn(Raw, *const u16, *const u16, *mut u32, Raw, *mut u32) -> u32;
type SaveHive = unsafe extern "system" fn(Raw, *const u16, u32, u32) -> u32;
pub(super) struct Api {
module: HMODULE,
create_hive: CreateHive,
open_hive: OpenHive,
close_hive: Close,
create_key: CreateKey,
close_key: Close,
set_value: SetValue,
get_value: GetValue,
save_hive: SaveHive,
}
struct Library(HMODULE);
impl Drop for Library {
fn drop(&mut self) {
unsafe {
FreeLibrary(self.0);
}
}
}
impl Api {
pub(super) fn load() -> Result<Self> {
// Restrict the loader to the Windows system directory, never the project/CWD/PATH.
let module = unsafe {
LoadLibraryExW(
wide("offreg.dll").as_ptr(),
null_mut(),
LOAD_LIBRARY_SEARCH_SYSTEM32,
)
};
ensure!(
!module.is_null(),
"MissingRegistryTool: Windows System32/offreg.dll is unavailable: {}",
std::io::Error::last_os_error()
);
let guard = Library(module);
macro_rules! symbol {
($name:literal, $ty:ty) => {{
let address = unsafe { GetProcAddress(module, concat!($name, "\0").as_ptr()) }
.context(concat!("MissingRegistryApi: ", $name))?;
unsafe {
std::mem::transmute::<unsafe extern "system" fn() -> isize, $ty>(address)
}
}};
}
let api = Self {
module,
create_hive: symbol!("ORCreateHive", CreateHive),
open_hive: symbol!("OROpenHive", OpenHive),
close_hive: symbol!("ORCloseHive", Close),
create_key: symbol!("ORCreateKey", CreateKey),
close_key: symbol!("ORCloseKey", Close),
set_value: symbol!("ORSetValue", SetValue),
get_value: symbol!("ORGetValue", GetValue),
save_hive: symbol!("ORSaveHive", SaveHive),
};
std::mem::forget(guard);
Ok(api)
}
}
impl Drop for Api {
fn drop(&mut self) {
unsafe {
FreeLibrary(self.module);
}
}
}
struct Handle<'a> {
raw: Raw,
api: &'a Api,
root: bool,
}
impl Drop for Handle<'_> {
fn drop(&mut self) {
unsafe {
if self.root {
(self.api.close_hive)(self.raw);
} else {
(self.api.close_key)(self.raw);
}
}
}
}
fn wide(value: &str) -> Vec<u16> {
value.encode_utf16().chain(Some(0)).collect()
}
fn path_wide(path: &Path) -> Vec<u16> {
path.as_os_str().encode_wide().chain(Some(0)).collect()
}
fn check(code: u32, operation: &str) -> Result<()> {
ensure!(
code == 0,
"RegistryApi: {operation}: {} ({code})",
std::io::Error::from_raw_os_error(code as i32)
);
Ok(())
}
pub(super) fn create(
entries: &[RegistryEntry],
path: &Path,
cancel: &AtomicBool,
) -> Result<()> {
let api = Api::load()?;
let mut raw = null_mut();
unsafe {
check((api.create_hive)(&mut raw), "ORCreateHive")?;
}
let hive = Handle {
raw,
api: &api,
root: true,
};
for entry in entries {
package_engine::cancelled(cancel)?;
let mut key: Option<Handle<'_>> = None;
// Unlike RegCreateKeyEx, ORCreateKey requires each parent to exist.
for component in package_domain::registry_path(entry).split('\\') {
let mut raw = null_mut();
unsafe {
check(
(api.create_key)(
key.as_ref().map_or(hive.raw, |key| key.raw),
wide(component).as_ptr(),
null_mut(),
0,
null_mut(),
&mut raw,
null_mut(),
),
"ORCreateKey",
)?;
}
key = Some(Handle {
raw,
api: &api,
root: false,
});
}
let key = key.context("InvalidRegistryKey")?;
let (kind, data) = package_domain::registry_data(entry).map_err(anyhow::Error::msg)?;
unsafe {
check(
(api.set_value)(
key.raw,
wide(&entry.name).as_ptr(),
kind,
data.as_ptr(),
data.len().try_into()?,
),
"ORSetValue",
)?;
}
}
package_engine::cancelled(cancel)?;
// 6.1 selects the documented modern hive format, also readable on Windows 10/11.
unsafe {
check(
(api.save_hive)(hive.raw, path_wide(path).as_ptr(), 6, 1),
"ORSaveHive",
)?;
}
drop(hive);
verify(entries, path, cancel)
}
pub(super) fn verify(
entries: &[RegistryEntry],
path: &Path,
cancel: &AtomicBool,
) -> Result<()> {
let api = Api::load()?;
let mut raw = null_mut();
unsafe {
check(
(api.open_hive)(path_wide(path).as_ptr(), &mut raw),
"OROpenHive",
)?;
}
let hive = Handle {
raw,
api: &api,
root: true,
};
for entry in entries {
package_engine::cancelled(cancel)?;
let (kind, expected) =
package_domain::registry_data(entry).map_err(anyhow::Error::msg)?;
let mut actual = vec![0u8; expected.len().max(1)];
let mut size = actual.len() as u32;
let mut actual_kind = 0;
unsafe {
check(
(api.get_value)(
hive.raw,
wide(&package_domain::registry_path(entry)).as_ptr(),
wide(&entry.name).as_ptr(),
&mut actual_kind,
actual.as_mut_ptr().cast(),
&mut size,
),
"ORGetValue",
)?;
}
actual.truncate(size as usize);
ensure!(
kind == actual_kind && expected == actual,
"RegistryMismatch: {}",
entry.id
);
}
Ok(())
}
}
#[cfg(all(test, windows))]
mod tests {
use super::*;
#[test]
fn offline_hive_round_trip_and_mismatch() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().join("registry.dat");
let cancel = AtomicBool::new(false);
let mut entries: Vec<_> = [
("REG_SZ", "한글"),
("REG_EXPAND_SZ", "%LOCALAPPDATA%\\Baron"),
("REG_DWORD", "4294967295"),
("REG_QWORD", "18446744073709551615"),
("REG_MULTI_SZ", "가\n나"),
("REG_BINARY", "00 ff 10"),
("REG_BINARY", ""),
]
.iter()
.enumerate()
.map(|(i, (kind, data))| RegistryEntry {
id: format!("entry-{i}"),
hive: "HKLM".into(),
key: "Software\\MSIXStudioTest".into(),
name: if i == 0 {
"".into()
} else {
format!("value-{i}")
},
view: if i % 2 == 0 { "32".into() } else { "64".into() },
value_type: (*kind).into(),
data: (*data).into(),
})
.collect();
create(&entries, &path, &cancel).unwrap();
assert_eq!(&std::fs::read(&path).unwrap()[..4], b"regf");
verify(&entries, &path, &cancel).unwrap();
entries[0].data = "changed".into();
assert!(verify(&entries, &path, &cancel).is_err());
assert!(create(&entries, &path, &cancel).is_err());
let cancelled_path = temp.path().join("cancelled.dat");
assert!(create(&entries, &cancelled_path, &AtomicBool::new(true)).is_err());
assert!(!cancelled_path.exists());
}
}