1 parent
6213fec7d2
commit
fdbc9a9d1b
7 files changed
+598
-2
No files matched your search
@@ -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, ®istry_path, cancel)?;
|
||||
Some(hash_file(®istry_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) = ®istry_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!({
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user