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).
|
||||
@@ -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 => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[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('</textarea>'));
|
||||
assert.ok(html.includes('" onfocus="bad'));
|
||||
assert.ok(html.includes('registry.0.data'));
|
||||
assert.ok(html.includes('data-action="registry-remove:0" disabled'));
|
||||
});
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user