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

@@ -0,0 +1,75 @@
# MSIX 레지스트리 설정 편집 및 빌드 반영
현재 MSIX Studio에 **레지스트리** 탭을 추가했다. 항목 추가·수정·삭제를 프로젝트 JSON에 저장하며, CLI와 GUI가 사용하는 공통 Rust 빌드 엔진에서 실제 `registry.dat`를 생성한다. 설정을 입력만 받고 빌드에서 무시하는 기능이 아니다.
## 사용 방법
1. 새로 빌드한 `src/target/debug/msix-studio.exe`를 실행하고 프로젝트를 연다.
2. **레지스트리 → 레지스트리 값 추가**를 누른다.
3. 키 경로, 값 이름, 32/64비트 뷰, 값 형식, 데이터를 입력한다. 값 이름을 비우면 해당 키의 기본값이다.
4. 저장한 뒤 **검증 · 빌드 → 입력 검사 → 빌드 계획 생성 → MSIX 빌드**를 실행한다.
5. 릴리스의 `registry-report.json`에서 항목 수, 패키지 내부 경로와 검증 결과를 확인한다.
현재 직접 빌드한 실행 파일에는 새 UI가 포함된다. 이미 실행 중이던 제작기는 종료 후 새 실행 파일로 다시 열어야 한다. GUI 클릭 시각 검사는 자동 수행하지 않았으며, 프런트엔드 빌드와 편집기 렌더링 테스트를 수행했다.
## 지원하는 설정
앱 전용 `HKLM\Software\회사\제품` 설정을 지원한다. `registry.dat`는 호스트 레지스트리에 가져오지 않으며 패키지 앱이 보는 레지스트리 뷰에 포함된다. 외부 앱이나 시스템 전체에 적용할 설정을 등록하는 방식이 아니다.
| 형식 | 데이터 입력 |
| --- | --- |
| `REG_SZ` | 문자열 그대로, 한글과 빈 문자열 지원 |
| `REG_EXPAND_SZ` | `%LOCALAPPDATA%` 등의 환경 변수 표기를 보존; 제작 PC에서 확장하지 않음 |
| `REG_DWORD` | 0~4294967295, 10진수 또는 `0x` 16진수 |
| `REG_QWORD` | 0~18446744073709551615, JSON 문자열로 저장하여 JavaScript 정밀도 손실 방지 |
| `REG_MULTI_SZ` | 한 줄에 한 문자열, 빈 줄 거부; 전체 빈 데이터는 빈 목록 |
| `REG_BINARY` | `00 FF 12` 또는 `00FF12`; 빈 데이터 지원 |
64비트 뷰는 hive 안의 `REGISTRY\MACHINE\SOFTWARE\...`, 32비트 뷰는 `REGISTRY\MACHINE\SOFTWARE\WOW6432Node\...`로 저장한다. 키 입력에서 WOW6432Node를 중복 지정할 수 없다. 이 뷰 선택은 앱 고유 Software 키용이며 Windows 공유 키·특수 리디렉션 규칙 전체를 자동 분석하지 않는다.
항목 ID, 키와 이름의 중복, 형식별 데이터, 숫자 범위, NUL, 잘못된 키 경로를 검사한다. 키와 값 이름의 중복은 대소문자를 구분하지 않는다. 항목은 10,000개, 입력 데이터는 값당 UTF-8 1 MiB까지다.
## 프로젝트 계약
v2 프로젝트에 선택 필드 `registry`를 추가했다. 기존 파일에 필드가 없으면 빈 목록으로 연다. 프로젝트 저장·백업·계획 revision 검사는 기존 흐름을 그대로 사용하므로 설정 변경 후에는 새 빌드 계획이 필요하다. JSON Schema도 갱신했다.
```json
{
"registry": [
{
"id": "baron-language",
"hive": "HKLM",
"key": "Software\\Baron\\EG-BIM",
"name": "Language",
"view": "64",
"valueType": "REG_SZ",
"data": "ko-KR"
}
]
}
```
위 내용은 프로젝트에 넣는 부분 예시다. 문자열은 literal이며 `[INSTALLDIR]`, `[APPDIR]` 같은 MSI 속성을 자동 치환하지 않는다. MSIX 설치 폴더는 버전별로 달라질 수 있으므로 기존 ISM의 경로 문자열을 그대로 가져오면 안 된다.
## 구현과 검증
- 도메인: `package-domain/src/registry.rs`에서 설정·검증·바이트 인코딩을 담당한다.
- 빌드: 시스템 디렉터리의 `offreg.dll`을 제한된 DLL 검색 방식으로 로드한다. Microsoft Offline Registry API로 메모리 hive를 생성하고 저장한다. 관리자 권한이나 호스트 레지스트리 마운트는 사용하지 않는다. DLL이 없으면 명시적으로 실패한다.
- 검증: 저장한 hive를 다시 열어 값 형식과 바이트를 비교하고, MakeAppx 역추출 후 hive 해시와 값들을 다시 확인한다.
- `registry.dat`, `User.dat`, `User.Classes.dat`를 파일 입력으로 덮어쓰는 경로는 거부한다.
- 보고서: `registry-report.json`을 작업·릴리스·내보내기에 포함한다. 이전 릴리스에 보고서가 없어도 내보내기는 가능하다. 빈 설정은 `notRequested`, 반영 후 비교 통과는 `verified`다.
- 앱에서 수정한 사용자별 값의 업데이트 병합이나 제거 정책은 이번 기능에서 별도 보장하지 않는다.
## 실제 시험 결과
- Rust workspace 단위/통합 테스트: 통과. 기존 프로젝트 호환, 저장·삭제, QWORD 정밀도, Unicode, 중복·경로·예약 파일 차단, 오프라인 hive 생성·읽기·불일치·취소 검사 포함.
- 프런트엔드 TypeScript/Vite 빌드 및 6개 테스트: 통과. 마크업 이스케이프와 삭제 후 행 주소를 확인했다.
- Windows SDK MakeAppx 실제 통합 테스트: 두 제품의 패키징·역추출·레지스트리 검증·보고서 내보내기 통과.
- 실제 설치/실행 시험: 독립 테스트 identity `Baron.RegistryFeatureProbe20261022`를 현 테스트 인증서로 서명하여 설치했다. 패키지 ID가 있는 프로세스에서 Win32 Registry API로 7개 값을 읽었고, 한글 기본값·32비트 뷰·6가지 형식 모두 기대한 타입/바이트와 일치했다. 이후 해당 테스트 패키지만 제거했다.
- 테스트 일반 레지스트리 키는 시험 전후 모두 없었다. 새 인증서를 추가하거나 기존 EG-BIM 앱을 설치·제거하지 않았다.
- 실행 시험 원본: `src/fixtures/registry-probe.rs`. 로컬 증빙: `output/samples/registry-feature-test/runtime-result.txt`, `cleanup-result.json`, 릴리스 `registry-report.json`.
- EG-BIM ISM의 379개 항목 자동 가져오기·변환 및 EG-BIM 재패키징은 수행하지 않았다. 기존 EG-BIM 서명 샘플에는 이번 설정이 소급 적용되지 않는다.
HKCU/User.dat, HKCR 별칭, COM/파일 연결 매니페스트, 레지스트리 삭제 작업, 빈 키만 생성, 조건부 설정, `.reg`/ISM 가져오기는 후속 범위다. 현재 단계에서 지원하지 않는 hive와 값 형식은 빌드 전에 거부한다.
근거: [Microsoft MSIX 레지스트리 동작](https://learn.microsoft.com/en-us/windows/msix/desktop/desktop-to-uwp-behind-the-scenes), [Offline Registry API](https://learn.microsoft.com/en-us/windows/win32/devnotes/offline-registry-library-functions), [ORSetValue](https://learn.microsoft.com/en-us/windows/win32/devnotes/orsetvalue), [ORSaveHive](https://learn.microsoft.com/en-us/windows/win32/devnotes/orsavehive).
+24
View File
@@ -0,0 +1,24 @@
export type RegistryEntry = {
id: string; hive: string; key: string; name: string; view: string; valueType: string; data: string;
};
export const registryTypes = ['REG_SZ', 'REG_EXPAND_SZ', 'REG_DWORD', 'REG_QWORD', 'REG_MULTI_SZ', 'REG_BINARY'];
const esc = (value: string) => value.replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c]!);
export function newRegistryEntry(architecture: string, id = `registry-${crypto.randomUUID()}`): RegistryEntry {
return { id, hive: 'HKLM', key: 'Software\\Baron\\Application', name: '', view: architecture === 'x86' ? '32' : '64', valueType: 'REG_SZ', data: '' };
}
export function renderRegistryEditor(entries: RegistryEntry[], disabled: boolean): string {
const lock = disabled ? 'disabled' : '';
return `<div class="callout"><strong>MSIX 앱 전용 레지스트리</strong><br>HKLM\\Software 아래의 설정을 패키지에 포함합니다. 빌드 PC의 레지스트리는 변경하지 않습니다. HKCU 초기화와 시스템 전체 COM·파일 연결 등록은 아직 지원하지 않습니다.</div>
<div class="registry-toolbar"><strong>${entries.length}개 설정</strong><button class="primary" data-action="registry-add" ${lock}>레지스트리 값 추가</button></div>
${entries.length ? '' : '<p class="muted">등록된 값이 없습니다. 필요한 키, 값 이름, 형식과 데이터를 추가하세요.</p>'}
${entries.map((entry, index) => {
const path = `registry.${index}`;
const input = (label: string, key: keyof RegistryEntry, help = '') => `<label class="field">${label}<input data-field="${path}.${key}" value="${esc(entry[key])}" ${lock}><small>${help}</small></label>`;
const select = (label: string, key: keyof RegistryEntry, choices: string[]) => `<label class="field">${label}<select data-field="${path}.${key}" ${lock}>${[...new Set([entry[key], ...choices])].map(value => `<option value="${esc(value)}" ${value === entry[key] ? 'selected' : ''}>${esc(value)}</option>`).join('')}</select></label>`;
return `<section class="registry-card" aria-label="레지스트리 항목 ${index + 1}"><div class="registry-toolbar"><strong>항목 ${index + 1}</strong><button data-action="registry-remove:${index}" ${lock} aria-label="레지스트리 항목 ${index + 1} 삭제">삭제</button></div><div class="form-grid">
${input('항목 ID', 'id', '프로젝트 안에서 고유한 ID')}${select('루트', 'hive', ['HKLM'])}
${input('키 경로', 'key', '예: Software\\Baron\\EG-BIM')}${select('레지스트리 뷰', 'view', ['64', '32'])}
${input('값 이름', 'name', '비워 두면 해당 키의 기본값')}${select('값 형식', 'valueType', registryTypes)}
</div><label class="field">데이터<textarea data-field="${path}.data" rows="3" spellcheck="false" ${lock}>${esc(entry.data)}</textarea><small>문자열은 그대로 저장합니다. 숫자는 10진수 또는 0x16진수, MULTI_SZ는 한 줄에 한 값, BINARY는 00 FF처럼 입력하세요. QWORD도 숫자 손실 없이 저장합니다. [INSTALLDIR] 같은 MSI 속성은 치환하지 않습니다.</small></label></section>`;
}).join('')}`;
}
@@ -0,0 +1,24 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { newRegistryEntry, renderRegistryEditor } from '../src/registry-editor.ts';
test('new entries select the app view and preserve lossless text data', () => {
assert.equal(newRegistryEntry('x86', 'one').view, '32');
const entry = newRegistryEntry('x64', 'two');
entry.valueType = 'REG_QWORD'; entry.data = '18446744073709551615';
assert.match(renderRegistryEditor([entry], false), /18446744073709551615/);
assert.equal(JSON.parse(JSON.stringify(entry)).data, entry.data);
});
test('registry editor escapes markup and addresses rows after deletion', () => {
const first = newRegistryEntry('x64', 'first');
const second = newRegistryEntry('x64', 'second');
second.data = '</textarea><script>alert(1)</script>';
second.name = '" onfocus="bad';
const entries = [first, second]; entries.splice(0, 1);
const html = renderRegistryEditor(entries, true);
assert.ok(!html.includes('<script>'));
assert.ok(html.includes('&lt;/textarea&gt;'));
assert.ok(html.includes('&quot; onfocus=&quot;bad'));
assert.ok(html.includes('registry.0.data'));
assert.ok(html.includes('data-action="registry-remove:0" disabled'));
});
+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());
}
}
+106
View File
@@ -0,0 +1,106 @@
//! Run only inside the dedicated registry integration-test MSIX.
#![windows_subsystem = "windows"]
use std::{ffi::c_void, ptr::null_mut};
#[link(name = "advapi32")]
extern "system" {
fn RegOpenKeyExW(
key: *mut c_void,
path: *const u16,
options: u32,
access: u32,
out: *mut *mut c_void,
) -> i32;
fn RegQueryValueExW(
key: *mut c_void,
name: *const u16,
reserved: *mut u32,
kind: *mut u32,
data: *mut u8,
size: *mut u32,
) -> i32;
fn RegCloseKey(key: *mut c_void) -> i32;
}
#[link(name = "kernel32")]
extern "system" {
fn GetCurrentPackageFullName(length: *mut u32, name: *mut u16) -> i32;
}
fn wide(s: &str) -> Vec<u16> {
s.encode_utf16().chain(Some(0)).collect()
}
fn string_bytes(s: &str) -> Vec<u8> {
s.encode_utf16().flat_map(u16::to_le_bytes).collect()
}
fn read(name: &str, view: u32) -> Result<(u32, Vec<u8>), i32> {
let mut key = null_mut();
let code = unsafe {
RegOpenKeyExW(
(-2147483646isize) as *mut c_void,
wide(env!("REGISTRY_PROBE_KEY")).as_ptr(),
0,
0x20019 | view,
&mut key,
)
};
if code != 0 {
return Err(code);
}
let mut size = 0;
let mut kind = 0;
let code = unsafe {
RegQueryValueExW(
key,
wide(name).as_ptr(),
null_mut(),
&mut kind,
null_mut(),
&mut size,
)
};
if code != 0 {
unsafe {
RegCloseKey(key);
}
return Err(code);
}
let mut data = vec![0; size as usize];
let code = unsafe {
RegQueryValueExW(
key,
wide(name).as_ptr(),
null_mut(),
&mut kind,
data.as_mut_ptr(),
&mut size,
)
};
unsafe {
RegCloseKey(key);
}
if code != 0 {
return Err(code);
}
data.truncate(size as usize);
Ok((kind, data))
}
fn main() {
let mut length = 0;
let packaged = unsafe { GetCurrentPackageFullName(&mut length, null_mut()) } == 122;
let mut output = format!("packaged={packaged}\n");
let mut passed = packaged;
for (name, view, kind, data) in [
("", 0x100, 1, string_bytes("한글 기본값\0")),
("Text32", 0x200, 1, string_bytes("32bit\0")),
("Expand", 0x100, 2, string_bytes("%LOCALAPPDATA%\\Baron\0")),
("Dword", 0x100, 4, u32::MAX.to_le_bytes().to_vec()),
("Qword", 0x100, 11, u64::MAX.to_le_bytes().to_vec()),
("Multi", 0x100, 7, string_bytes("가\0나\0\0")),
("Binary", 0x100, 3, vec![0, 255, 18]),
] {
let result = read(name, view);
let ok = result == Ok((kind, data));
passed &= ok;
output.push_str(&format!("{name}/{view}: {ok} {result:?}\n"));
}
output.push_str(&format!("passed={passed}\n"));
let _ = std::fs::write(env!("REGISTRY_PROBE_OUTPUT"), output);
}
@@ -0,0 +1,28 @@
{
"date": "2026-10-22",
"feature": "MSIX offline registry editor and packaging",
"rustWorkspace": { "command": "cargo test --locked --workspace", "passed": 20, "failed": 0, "ignoredSdkTestsRunSeparately": 1 },
"frontend": { "build": "passed", "testsPassed": 6, "testsFailed": 0, "nativeGuiClickTest": "notRun" },
"sdkIntegration": {
"command": "cargo test --locked -p package-infra --test workflow real_sdk_packages_two_products_and_rejects_stale_plans -- --ignored --nocapture",
"makeAppx": "C:\\Program Files (x86)\\Windows Kits\\10\\bin\\10.0.26100.0\\x64\\makeappx.exe",
"passed": true,
"productCount": 2
},
"runtimeProbe": {
"identity": "Baron.RegistryFeatureProbe20261022",
"buildId": "987b0d0f-e17b-43a7-8ad1-ffb7ea42d898",
"packagedProcess": true,
"valuesReadAndCompared": 7,
"types": ["REG_SZ", "REG_EXPAND_SZ", "REG_DWORD", "REG_QWORD", "REG_MULTI_SZ", "REG_BINARY"],
"views": ["64", "32"],
"passed": true,
"testPackageRemoved": true,
"hostRegistryTestKeyAbsent": true,
"evidenceRoot": "D:\\networkSetup\\output\\samples\\registry-feature-test",
"signedPackageSha256": "eff334f042528e8ab950f3b981896da42b86d0fa33312f09bdf9253406fc40aa"
},
"desktopExecutable": "D:\\networkSetup\\src\\target\\debug\\msix-studio.exe",
"egbimIsmRegistryImported": false,
"egbimSampleRebuilt": false
}