WIP HWP ABI 판 확인 (관리자 지시 10/20, 28 지적 - 옛 .so 에 새 인수를 넘기면 힙 손상): Rust jj_rhwp_abi_version() = 2 · Dart 는 처음 HWP 를 열 때 꼭 있어야 할 함수 7개와 판을 보고 다르면 앱 안 보기를 쓰지 않고 다른 앱으로 · check_apk 가 APK 안 libjj_rhwp.so 의 내보낸 기호를 NDK llvm-nm 으로 보고 없으면 실패 (986 의 옛 .so 를 잡음) · 시험 hwp_abi_test (검토 대기)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
hwp2
RyuJongJin 2026-10-20 19:19:05 +09:00
parent 0d2b14ce68
commit 76fd2f3cbf
4 changed files with 90 additions and 6 deletions

View File

@ -16,11 +16,33 @@ class RhwpEngine implements HwpPdfEngine {
/// 관리자 판단 (10/20): 한글 글꼴 컴포넌트를 받으면 그 폴더를 앞에 더한다 (아직 없음)
static const fontDirs = ['/system/fonts'];
/// 라이브러리를 읽을 수 있는지 (없으면 다른 앱으로 연다)
static bool get available {
/// 이 Dart 가 기대하는 C ABI 의 판 (tool/rhwp_ffi 의 JJ_RHWP_ABI_VERSION 과 같아야 함)
static const abiVersion = 2;
/// 꼭 있어야 하는 C 함수
static const requiredSymbols = [
'jj_rhwp_abi_version', 'jj_rhwp_open', 'jj_rhwp_page_count', 'jj_rhwp_export_pdf', 'jj_rhwp_cancel', 'jj_rhwp_close', 'jj_rhwp_free', //
];
static bool? _available;
/// 라이브러리를 읽을 수 있고 ABI 가 맞는지 (아니면 다른 앱으로 연다). 처음 HWP 를 열 때 한 번 본다.
/// 관리자 지시 (10/20, 28 지적): 옛 .so (판 1 - code 칸 · cancel 없음) 에 새 인수를 넘기면 힙이 망가지므로, 기호와 판이 맞을 때만
static bool get available => _available ??= () {
try {
final lib = DynamicLibrary.open(libName);
return abiCompatible(
has: lib.providesSymbol, abi: () => lib.lookupFunction<Int32 Function(), int Function()>('jj_rhwp_abi_version')());
} catch (_) {
return false;
}
}();
/// [requiredSymbols] 가 모두 있고 [abi] 가 [abiVersion] 인지 (시험용으로 따로 - 기호를 찾다 · 부르다 실패하면 false)
static bool abiCompatible({required bool Function(String name) has, required int Function() abi}) {
try {
DynamicLibrary.open(libName);
return true;
if (!requiredSymbols.every(has)) return false;
return abi() == abiVersion;
} catch (_) {
return false;
}

28
test/hwp_abi_test.dart Normal file
View File

@ -0,0 +1,28 @@
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:jj_mkvmaker/platform/android/hwp_pdf_ffi.dart';
/// 관리자 지시 (10/20, 28 지적): 옛 libjj_rhwp.so (ABI 판 1 - jj_rhwp_cancel · jj_rhwp_abi_version 없음, 오류 code 칸 없음) 이면
/// 앱 안 보기를 쓰지 않고 다른 앱으로 연다 (새 인수를 넘기면 힙이 망가진다)
void main() {
const v1 = {'jj_rhwp_open', 'jj_rhwp_page_count', 'jj_rhwp_export_pdf', 'jj_rhwp_close', 'jj_rhwp_free', 'jj_rhwp_version'};
final v2 = {...v1, 'jj_rhwp_cancel', 'jj_rhwp_abi_version'};
test('옛 .so (기호 없음) · 판이 다름 · 부르다 실패 → 쓰지 않음, 맞으면 씀', () {
expect(RhwpEngine.abiCompatible(has: v1.contains, abi: () => throw ArgumentError('no symbol')), isFalse, reason: '판 1');
expect(RhwpEngine.abiCompatible(has: {...v1, 'jj_rhwp_abi_version'}.contains, abi: () => 2), isFalse, reason: 'cancel 없음');
expect(RhwpEngine.abiCompatible(has: v2.contains, abi: () => 1), isFalse, reason: '판이 다름');
expect(RhwpEngine.abiCompatible(has: v2.contains, abi: () => 3), isFalse, reason: '더 새 판도');
expect(RhwpEngine.abiCompatible(has: (_) => throw StateError('x'), abi: () => 2), isFalse);
expect(RhwpEngine.abiCompatible(has: v2.contains, abi: () => RhwpEngine.abiVersion), isTrue);
});
test('Dart 의 판 = tool/rhwp_ffi 의 JJ_RHWP_ABI_VERSION · 꼭 있어야 하는 함수는 lib.rs 에 모두', () {
final rs = File('tool/rhwp_ffi/src/lib.rs').readAsStringSync();
expect(rs, contains('pub const JJ_RHWP_ABI_VERSION: c_int = ${RhwpEngine.abiVersion};'));
for (final s in RhwpEngine.requiredSymbols) {
expect(rs, contains('pub extern "C" fn $s('), reason: s);
}
});
}

View File

@ -8,6 +8,8 @@
# 새 버전 확인도 "최신과 같음" 이 된다. 시험판은 flutter build apk --build-number 9xx 로 빌드한다. 정식 판 APK 는 -Release.
# - 순번 900 이상의 v 태그 (시험판 Pre-release, 예: v2026.10.18_977) 는 정식 판으로 보지 않는다 (관리자 지시 10/19, 28 지적:
# 시험판에 태그를 단 뒤 같은 APK 를 다시 검사하면 "정식 판 번호와 같음" 으로 거짓 실패). 정식 판 (-Release) 인데 순번이 900 이상이면 실패.
# - HWP 보기 (관리자 지시 10/20, 28 지적): APK 안 libjj_rhwp.so 가 앱의 Dart 와 같은 C ABI 인지 - 내보낸 기호 (NDK llvm-nm) 에
# jj_rhwp_cancel · jj_rhwp_abi_version 등이 모두 있어야 한다 (옛 .so 는 이름 · 목록만으로는 못 가린다).
# 사용: powershell -ExecutionPolicy Bypass -File tool\check_apk.ps1 -Apk build\app\outputs\flutter-apk\app-release.apk [-Release]
param(
[Parameter(Mandatory = $true)][string]$Apk,
@ -41,7 +43,7 @@ if (-not $Libs) { $Libs = Join-Path $app 'android\app\third_party_libs.txt' }
$listed = @(Get-Content $Libs | ForEach-Object { $_.Trim() } | Where-Object { $_ -and -not $_.StartsWith('#') })
# third_party 에 있는 파일 (목록 밖이면 APK 에 없어야 함)
$tp = @()
foreach ($d in 'third_party\rsync\android', 'third_party\sdcpp\android', 'third_party\tor\android', 'third_party\7zip\android') {
foreach ($d in 'third_party\rsync\android', 'third_party\sdcpp\android', 'third_party\tor\android', 'third_party\7zip\android', 'third_party\rhwp\android') {
$dir = Join-Path $app $d
if (Test-Path $dir) { $tp += @(Get-ChildItem $dir -Recurse -Filter *.so | ForEach-Object { $_.Name }) }
}
@ -50,6 +52,13 @@ try {
$libsIn = @($zip.Entries | Where-Object { $_.FullName -like 'lib/arm64-v8a/*.so' } | ForEach-Object { $_.Name })
$dexes = @($zip.Entries | Where-Object { $_.FullName -match '^classes\d*\.dex$' })
if ($dexes.Count -eq 0) { throw "dex 가 없습니다: $Apk" }
# HWP 보기 라이브러리는 꺼내 두고 아래에서 기호를 본다
$rhwpSo = $null
$rhwpEntry = $zip.Entries | Where-Object { $_.FullName -eq 'lib/arm64-v8a/libjj_rhwp.so' } | Select-Object -First 1
if ($rhwpEntry) {
$rhwpSo = Join-Path ([System.IO.Path]::GetTempPath()) ("jj_check_rhwp_{0}.so" -f [guid]::NewGuid().ToString('N'))
[System.IO.Compression.ZipFileExtensions]::ExtractToFile($rhwpEntry, $rhwpSo)
}
$found = $null
foreach ($e in $dexes) {
$s = $e.Open()
@ -61,6 +70,21 @@ try {
$zip.Dispose()
}
$bad = @()
# HWP 보기: libjj_rhwp.so 의 내보낸 기호 (lib/platform/android/hwp_pdf_ffi.dart 의 requiredSymbols 와 같게)
$rhwpNote = ''
if ($rhwpSo) {
try {
$nm = Get-ChildItem (Join-Path $app '..\tools\android-sdk\ndk') -Recurse -Filter llvm-nm.exe -ErrorAction SilentlyContinue | Select-Object -Last 1
if (-not $nm) { throw 'llvm-nm.exe 를 찾지 못했습니다 (tools\android-sdk\ndk)' }
$syms = @(& $nm.FullName -D --defined-only $rhwpSo 2>$null | ForEach-Object { ($_ -split '\s+')[-1] })
$need = 'jj_rhwp_abi_version', 'jj_rhwp_open', 'jj_rhwp_page_count', 'jj_rhwp_export_pdf', 'jj_rhwp_cancel', 'jj_rhwp_close', 'jj_rhwp_free'
$miss = @($need | Where-Object { $syms -notcontains $_ })
if ($miss) { $bad += "libjj_rhwp.so 가 옛 C ABI 입니다 - 없는 함수: $($miss -join ', ') (sh tool/build_rhwp_android.sh 로 다시 빌드)" }
else { $rhwpNote = " · libjj_rhwp.so 함수 $($need.Count)개 있음" }
} finally {
Remove-Item $rhwpSo -Force -ErrorAction SilentlyContinue
}
}
# 시험판 번호: aapt2 로 versionCode · versionName 을 읽어 정식 판 태그의 번호와 견준다 (build.gradle.kts 와 같은 셈: 년%100 · 월 · 일 · 번호)
$aapt = Get-ChildItem (Join-Path $app '..\tools\android-sdk\build-tools') -Recurse -Filter aapt2.exe -ErrorAction SilentlyContinue | Select-Object -Last 1
if (-not $aapt -and $env:ANDROID_HOME) { $aapt = Get-ChildItem (Join-Path $env:ANDROID_HOME 'build-tools') -Recurse -Filter aapt2.exe -ErrorAction SilentlyContinue | Select-Object -Last 1 }
@ -90,4 +114,4 @@ if (-not $found) {
Write-Host "실패: GeneratedPluginRegistrant 가 dex 에 없습니다 (켜자마자 꺼지는 APK). flutter pub get 뒤 --no-pub 없이 다시 빌드하세요." -ForegroundColor Red
exit 1
}
Write-Host "통과: GeneratedPluginRegistrant 있음 ($found) · third_party 실행 파일 $($listed.Count)개 목록과 같음 · versionCode $code ($vname)$(if ($Release) { ' (정식 판)' } else { ' (시험판 - 정식 판 번호와 다름)' })"
Write-Host "통과: GeneratedPluginRegistrant 있음 ($found) · third_party 실행 파일 $($listed.Count)개 목록과 같음$rhwpNote · versionCode $code ($vname)$(if ($Release) { ' (정식 판)' } else { ' (시험판 - 정식 판 번호와 다름)' })"

View File

@ -100,6 +100,16 @@ fn open_kind(msg: &str, with_password: bool) -> c_int {
}
}
/// 이 C ABI 의 판 - Dart (lib/platform/android/hwp_pdf_ffi.dart 의 RhwpEngine.abiVersion) 와 같아야 앱 안 보기를 쓴다
/// (관리자 지시 10/20, 28 지적: 옛 .so (판 1 - code 칸 · cancel 없음) 에 새 인수를 넘기면 힙이 망가진다).
/// 함수의 인수 · 오류 코드를 바꾸면 올린다
pub const JJ_RHWP_ABI_VERSION: c_int = 2;
#[no_mangle]
pub extern "C" fn jj_rhwp_abi_version() -> c_int {
JJ_RHWP_ABI_VERSION
}
/// 판 (예: "0.8.7") - 라이선스 화면 · 작업 기록용. jj_rhwp_free 로 푼다
#[no_mangle]
pub extern "C" fn jj_rhwp_version() -> *mut c_char {