From fdbc9a9d1b465a04f1462c70055b741215ab701c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EC=A4=80=EC=98=81?= Date: Tue, 3 Nov 2026 16:34:00 +0900 Subject: [PATCH] feat: generate and verify MSIX registry hives Closes #3 --- .../2026-10-22-msix-registry-editor.md | 75 ++++ src/apps/desktop/src/registry-editor.ts | 24 ++ .../desktop/tests/registry-editor.test.mjs | 24 ++ src/crates/package-infra/src/lib.rs | 20 +- src/crates/package-infra/src/registry.rs | 323 ++++++++++++++++++ src/fixtures/registry-probe.rs | 106 ++++++ .../verification-2026-10-22-registry.json | 28 ++ 7 files changed, 598 insertions(+), 2 deletions(-) create mode 100644 docs/changes/2026-10-22-msix-registry-editor.md create mode 100644 src/apps/desktop/src/registry-editor.ts create mode 100644 src/apps/desktop/tests/registry-editor.test.mjs create mode 100644 src/crates/package-infra/src/registry.rs create mode 100644 src/fixtures/registry-probe.rs create mode 100644 src/tests/verification-2026-10-22-registry.json diff --git a/docs/changes/2026-10-22-msix-registry-editor.md b/docs/changes/2026-10-22-msix-registry-editor.md new file mode 100644 index 0000000..987bc34 --- /dev/null +++ b/docs/changes/2026-10-22-msix-registry-editor.md @@ -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). diff --git a/src/apps/desktop/src/registry-editor.ts b/src/apps/desktop/src/registry-editor.ts new file mode 100644 index 0000000..768dc27 --- /dev/null +++ b/src/apps/desktop/src/registry-editor.ts @@ -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 `
MSIX 앱 전용 레지스트리
HKLM\\Software 아래의 설정을 패키지에 포함합니다. 빌드 PC의 레지스트리는 변경하지 않습니다. HKCU 초기화와 시스템 전체 COM·파일 연결 등록은 아직 지원하지 않습니다.
+
${entries.length}개 설정
+ ${entries.length ? '' : '

등록된 값이 없습니다. 필요한 키, 값 이름, 형식과 데이터를 추가하세요.

'} + ${entries.map((entry, index) => { + const path = `registry.${index}`; + const input = (label: string, key: keyof RegistryEntry, help = '') => ``; + const select = (label: string, key: keyof RegistryEntry, choices: string[]) => ``; + return `
항목 ${index + 1}
+ ${input('항목 ID', 'id', '프로젝트 안에서 고유한 ID')}${select('루트', 'hive', ['HKLM'])} + ${input('키 경로', 'key', '예: Software\\Baron\\EG-BIM')}${select('레지스트리 뷰', 'view', ['64', '32'])} + ${input('값 이름', 'name', '비워 두면 해당 키의 기본값')}${select('값 형식', 'valueType', registryTypes)} +
`; + }).join('')}`; +} diff --git a/src/apps/desktop/tests/registry-editor.test.mjs b/src/apps/desktop/tests/registry-editor.test.mjs new file mode 100644 index 0000000..d0c1a03 --- /dev/null +++ b/src/apps/desktop/tests/registry-editor.test.mjs @@ -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 = ''; + second.name = '" onfocus="bad'; + const entries = [first, second]; entries.splice(0, 1); + const html = renderRegistryEditor(entries, true); + assert.ok(!html.includes('