From 5ca78c34d5634bf0e8245e0783a0a70667948d9d Mon Sep 17 00:00:00 2001 From: ClausValcaTD <1.93546948e+08+ClausValcaTD@users.noreply.github.com> Date: Sat, 3 Oct 2026 22:02:36 +0000 Subject: [PATCH 1/3] fix: harden DCP patching and add quality gate --- .github/workflows/quality.yml | 65 +++++++++++++++++++++++++++++ README.md | 2 +- VERSION | 2 +- core/lib/src/dcp_patcher.dart | 74 +++++++++++++++++++++++++++------ core/pubspec.yaml | 2 +- core/test/dcp_patcher_test.dart | 23 ++++++++++ gui/pubspec.yaml | 2 +- 7 files changed, 154 insertions(+), 16 deletions(-) create mode 100644 .github/workflows/quality.yml diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml new file mode 100644 index 0000000..7b49fb6 --- /dev/null +++ b/.github/workflows/quality.yml @@ -0,0 +1,65 @@ +name: Quality Gate + +on: + pull_request: + branches: [main] + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +jobs: + dart-and-flutter: + name: Dart and Flutter checks + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Check version consistency + shell: bash + run: | + version="$(tr -d '[:space:]' < VERSION)" + test -n "$version" + grep -q "^version: $version$" core/pubspec.yaml + grep -q "^version: $version+1$" gui/pubspec.yaml + + - name: Set up Flutter + uses: subosito/flutter-action@v2 + with: + channel: stable + cache: true + + - name: Install GUI dependencies + working-directory: gui + run: flutter pub get + + - name: Check GUI formatting + working-directory: gui + run: dart format --output=none --set-exit-if-changed . + + - name: Analyze GUI + working-directory: gui + run: flutter analyze + + - name: Test GUI + working-directory: gui + run: flutter test + + - name: Install core dependencies + working-directory: core + run: dart pub get + + - name: Check core formatting + working-directory: core + run: dart format --output=none --set-exit-if-changed . + + - name: Analyze core + working-directory: core + run: dart analyze + + - name: Test core + working-directory: core + run: dart test diff --git a/README.md b/README.md index 9948ee0..93cb8a0 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ Each release includes the **Flutter GUI** (`OpenROM`) and the **headless CLI** ( - **CRC32 matching** — Identifies ROMs by hash, not filename. - **Dry run mode** — Preview renames before applying. - **DAT library** — Import multiple DATs and OpenROM stores them locally for reuse. -- **Streaming XML parser** — Handles massive DAT files (hundreds of MB for PS2/MAME) without RAM spikes. +- **Event-based XML parser** — Parses DAT metadata without building a full DOM tree; a streaming file decoder is planned for extremely large DAT files. - **100% offline** — No network requests, no API keys. ### 📊 Queue & Preview diff --git a/VERSION b/VERSION index 1545d96..7c69a55 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.5.0 +3.7.0 diff --git a/core/lib/src/dcp_patcher.dart b/core/lib/src/dcp_patcher.dart index f243c9a..301fb7e 100644 --- a/core/lib/src/dcp_patcher.dart +++ b/core/lib/src/dcp_patcher.dart @@ -43,11 +43,38 @@ class DcpPatcher { return {'success': false, 'error': 'Disc directory not found: $discDir'}; } - Directory(outputDir).createSync(recursive: true); + final sourcePath = p.normalize(p.canonicalize(discDir)); + final requestedOutputPath = p.normalize(p.canonicalize(outputDir)); + if (requestedOutputPath == sourcePath || + requestedOutputPath.startsWith('$sourcePath${p.separator}')) { + return { + 'success': false, + 'error': 'Output directory must be outside the source disc directory.', + }; + } - _log('[DCP] Copying source disc to output directory...'); - _copyDisc(discDir, outputDir); - _updateProgress(10.0); + // Patch a sibling staging directory first so the requested output remains + // untouched if archive decoding or patching fails halfway through. + final outputPath = p.normalize(p.absolute(outputDir)); + final stagingDir = Directory( + '${outputPath}.openrom-staging-${DateTime.now().microsecondsSinceEpoch}', + ); + try { + stagingDir.createSync(recursive: true); + } catch (e) { + return {'success': false, 'error': 'Could not create staging directory: $e'}; + } + + try { + _log('[DCP] Copying source disc to output directory...'); + _copyDisc(discDir, stagingDir.path); + _updateProgress(10.0); + } catch (e) { + try { + stagingDir.deleteSync(recursive: true); + } catch (_) {} + return {'success': false, 'error': 'Could not stage source disc: $e'}; + } final result = { 'success': false, @@ -64,7 +91,7 @@ class DcpPatcher { final total = archive.length == 0 ? 1 : archive.length; _log('[DCP] Archive contains $total entries.'); - final basePath = p.normalize(p.canonicalize(outputDir)); + final basePath = p.normalize(p.canonicalize(stagingDir.path)); final xdeltaEntries = []; for (int i = 0; i < archive.length; i++) { @@ -81,7 +108,7 @@ class DcpPatcher { if (name.toLowerCase() == 'bootsector/ip.bin') { final ipbinData = file.content as List; - File(p.join(outputDir, 'IP.BIN')).writeAsBytesSync(ipbinData); + File(p.join(stagingDir.path, 'IP.BIN')).writeAsBytesSync(ipbinData); result['ipbin_replaced'] = true; _log('[DCP] Replaced IP.BIN from patch.'); continue; @@ -106,7 +133,7 @@ class DcpPatcher { if (xdeltaEntries.isNotEmpty) { final xdeltaApplied = _applyXdeltaPatches( xdeltaEntries, - outputDir, + stagingDir.path, ignoreChecksum, ); result['xdelta_applied'] = xdeltaApplied; @@ -114,6 +141,7 @@ class DcpPatcher { } _updateProgress(100.0); + _commitStaging(stagingDir, outputPath); result['success'] = true; _log('[DCP] Done. Files patched: ${result["files_patched"]}, ' 'xdelta: ${result["xdelta_applied"]}, ' @@ -123,6 +151,12 @@ class DcpPatcher { result['error'] = e.toString(); _log('[DCP ERROR] $e'); return result; + } finally { + if (stagingDir.existsSync()) { + try { + stagingDir.deleteSync(recursive: true); + } catch (_) {} + } } } @@ -139,12 +173,15 @@ class DcpPatcher { final basePath = p.normalize(p.canonicalize(outputDir)); for (final entry in xdeltaEntries) { - final basename = p.basename(entry.name); - String targetName = basename; + final archivePrefix = entry.name.startsWith('xdelta\\') + ? 'xdelta\\' + : 'xdelta/'; + final relativePatchName = entry.name.substring(archivePrefix.length); + String targetName = relativePatchName.replaceAll('\\', '/'); for (final ext in ['.xdelta3', '.xdelta', '.xd', '.vcdiff']) { - if (basename.toLowerCase().endsWith(ext)) { - targetName = basename.substring(0, basename.length - ext.length); + if (targetName.toLowerCase().endsWith(ext)) { + targetName = targetName.substring(0, targetName.length - ext.length); break; } } @@ -154,7 +191,7 @@ class DcpPatcher { throw FormatException('Malicious archive entry detected (Path Traversal): ${entry.name}'); } - final patchTmp = p.join(tempDir.path, basename); + final patchTmp = p.join(tempDir.path, p.basename(relativePatchName)); final sourcePath = destPath; final outputPath = '$destPath.patched'; @@ -207,4 +244,17 @@ class DcpPatcher { } } } + + void _commitStaging(Directory stagingDir, String outputDir) { + final target = Directory(outputDir); + target.createSync(recursive: true); + for (final entity in stagingDir.listSync(recursive: true)) { + if (entity is! File) continue; + final rel = p.relative(entity.path, from: stagingDir.path); + final dest = File(p.join(outputDir, rel)); + dest + ..createSync(recursive: true) + ..writeAsBytesSync(entity.readAsBytesSync()); + } + } } diff --git a/core/pubspec.yaml b/core/pubspec.yaml index 595c4b7..d24a6b3 100644 --- a/core/pubspec.yaml +++ b/core/pubspec.yaml @@ -1,6 +1,6 @@ name: openrom_core description: OpenROM Pure Dart Backend Core & Toolkit Library -version: 3.0.0 +version: 3.7.0 publish_to: 'none' environment: diff --git a/core/test/dcp_patcher_test.dart b/core/test/dcp_patcher_test.dart index 8cd4c04..62ba052 100644 --- a/core/test/dcp_patcher_test.dart +++ b/core/test/dcp_patcher_test.dart @@ -37,4 +37,27 @@ void main() { tmpDir.deleteSync(recursive: true); } }); + + test('rejects output directory inside source disc', () { + final tmpDir = Directory.systemTemp.createTempSync('dcp_output_test_'); + try { + final discDir = Directory(p.join(tmpDir.path, 'disc'))..createSync(); + final outputDir = p.join(discDir.path, 'patched'); + final dcpPath = p.join(tmpDir.path, 'empty.dcp'); + final encoder = ZipFileEncoder()..create(dcpPath); + encoder.close(); + + final result = DcpPatcher().apply( + dcpPath: dcpPath, + discDir: discDir.path, + outputDir: outputDir, + ); + + expect(result['success'], isFalse); + expect(result['error'], contains('outside the source disc')); + expect(Directory(outputDir).existsSync(), isFalse); + } finally { + tmpDir.deleteSync(recursive: true); + } + }); } diff --git a/gui/pubspec.yaml b/gui/pubspec.yaml index c66aae6..71bdf53 100644 --- a/gui/pubspec.yaml +++ b/gui/pubspec.yaml @@ -1,7 +1,7 @@ name: gui description: OpenROM — Universal ROM Compression Suite Desktop App publish_to: 'none' -version: 3.5.0+1 +version: 3.7.0+1 environment: sdk: '>=3.0.0 <4.0.0' From 36f1233c3029913c7a8d17a9c256a1ac2d48771f Mon Sep 17 00:00:00 2001 From: ClausValcaTD <1.93546948e+08+ClausValcaTD@users.noreply.github.com> Date: Sat, 3 Oct 2026 22:03:13 +0000 Subject: [PATCH 2/3] ci: pin bundled toolchain revision --- .github/workflows/Flatpak.yml | 1 + .github/workflows/Release.yml | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/.github/workflows/Flatpak.yml b/.github/workflows/Flatpak.yml index c40d9b1..c067012 100644 --- a/.github/workflows/Flatpak.yml +++ b/.github/workflows/Flatpak.yml @@ -66,6 +66,7 @@ jobs: - name: Clone OpenROM binaries repo run: | git clone --depth 1 https://github.com/M5Devs/OpenROM-bins.git bins + git -C bins checkout 586f7cc54df0facf9df9d3066c70e6b434b93753 - name: Set up Flutter uses: subosito/flutter-action@v2 diff --git a/.github/workflows/Release.yml b/.github/workflows/Release.yml index 826ffed..c895fd7 100644 --- a/.github/workflows/Release.yml +++ b/.github/workflows/Release.yml @@ -22,6 +22,7 @@ jobs: - name: Fetch binaries from OpenROM-bins run: | git clone --depth 1 https://github.com/M5Devs/OpenROM-bins.git bins + git -C bins checkout 586f7cc54df0facf9df9d3066c70e6b434b93753 New-Item -ItemType Directory -Force -Path assets\windows Copy-Item -Path "bins\windows\x86_64\*" -Destination "assets\windows\" -Recurse @@ -148,6 +149,7 @@ jobs: - name: Fetch binaries from OpenROM-bins run: | git clone --depth 1 https://github.com/M5Devs/OpenROM-bins.git bins + git -C bins checkout 586f7cc54df0facf9df9d3066c70e6b434b93753 mkdir -p assets/linux/x86_64 cp -r bins/linux/x86_64/. assets/linux/x86_64/ chmod +x assets/linux/x86_64/* @@ -271,6 +273,7 @@ jobs: - name: Fetch binaries from OpenROM-bins run: | git clone --depth 1 https://github.com/M5Devs/OpenROM-bins.git bins + git -C bins checkout 586f7cc54df0facf9df9d3066c70e6b434b93753 mkdir -p assets/linux/arm64 cp -r bins/linux/arm64/. assets/linux/arm64/ chmod +x assets/linux/arm64/* @@ -377,6 +380,7 @@ jobs: - name: Fetch binaries from OpenROM-bins run: | git clone --depth 1 https://github.com/M5Devs/OpenROM-bins.git bins + git -C bins checkout 586f7cc54df0facf9df9d3066c70e6b434b93753 mkdir -p assets/macos/arm64 assets/macos/x86_64 cp -r bins/macos/arm64/. assets/macos/arm64/ cp -r bins/macos/x86_64/. assets/macos/x86_64/ From 9412fa03daf4fe127e3177bdfe6d7d2131b1b9e1 Mon Sep 17 00:00:00 2001 From: ClausValcaTD <193546948+ClausValcaTD@users.noreply.github.com> Date: Sat, 3 Oct 2026 22:21:14 +0000 Subject: [PATCH 3/3] style: auto format dart files --- core/bin/openrom.dart | 256 ++++++++++++++++++----- core/lib/src/bin_merger.dart | 55 +++-- core/lib/src/compressor.dart | 25 ++- core/lib/src/config.dart | 13 +- core/lib/src/converter.dart | 74 +++++-- core/lib/src/cue_generator.dart | 14 +- core/lib/src/dcp_builder.dart | 82 +++++--- core/lib/src/dcp_patcher.dart | 37 +++- core/lib/src/detector.dart | 35 +++- core/lib/src/gdi_reader.dart | 35 ++-- core/lib/src/header_remover.dart | 41 ++-- core/lib/src/ipbin_editor.dart | 11 +- core/lib/src/logger.dart | 2 + core/lib/src/m3u_generator.dart | 5 +- core/lib/src/rom_renamer.dart | 61 ++++-- core/lib/src/ssp_builder.dart | 46 ++-- core/lib/src/validator.dart | 1 + core/test/dcp_builder_test.dart | 13 +- core/test/dcp_patcher_test.dart | 9 +- core/test/detector_test.dart | 80 +++++-- core/test/header_remover_test.dart | 20 +- core/test/rom_renamer_test.dart | 1 + core/test/ssp_builder_test.dart | 71 ++++--- gui/lib/l10n/app_localizations.dart | 47 +++-- gui/lib/l10n/app_localizations_ar.dart | 1 + gui/lib/l10n/app_localizations_de.dart | 1 + gui/lib/l10n/app_localizations_en.dart | 1 + gui/lib/l10n/app_localizations_es.dart | 1 + gui/lib/l10n/app_localizations_fr.dart | 1 + gui/lib/l10n/app_localizations_it.dart | 1 + gui/lib/l10n/app_localizations_ja.dart | 1 + gui/lib/l10n/app_localizations_pt.dart | 1 + gui/lib/l10n/app_localizations_ru.dart | 1 + gui/lib/l10n/app_localizations_tr.dart | 1 + gui/lib/l10n/app_localizations_zh.dart | 1 + gui/lib/models/conversion_job.dart | 4 +- gui/lib/models/rom_file.dart | 6 +- gui/lib/patcher/aps_gba_builder.dart | 8 +- gui/lib/patcher/aps_n64_patcher.dart | 9 +- gui/lib/patcher/aps_patcher.dart | 16 +- gui/lib/patcher/dcp_builder.dart | 2 + gui/lib/patcher/dcp_patcher.dart | 5 +- gui/lib/patcher/ips_builder.dart | 4 +- gui/lib/patcher/ups_patcher.dart | 5 +- gui/lib/screens/dreamcast_screen.dart | 36 ++-- gui/lib/screens/home_screen.dart | 5 +- gui/lib/screens/patcher_screen.dart | 47 +++-- gui/lib/screens/settings_screen.dart | 7 +- gui/lib/screens/theme_editor_screen.dart | 227 ++++++++++++++++---- gui/lib/screens/tools_screen.dart | 79 +++---- gui/lib/services/core_bridge.dart | 206 +++++++++--------- gui/lib/services/patcher_service.dart | 3 +- gui/lib/services/theme_service.dart | 13 +- gui/lib/services/tools_service.dart | 152 +++++++------- gui/lib/widgets/rom_card.dart | 8 +- gui/lib/widgets/sidebar.dart | 5 +- gui/lib/widgets/top_bar.dart | 5 +- gui/test/patcher_builder_test.dart | 41 +++- gui/test/theme_config_test.dart | 5 +- gui/test/tools_test.dart | 4 +- 60 files changed, 1311 insertions(+), 636 deletions(-) diff --git a/core/bin/openrom.dart b/core/bin/openrom.dart index 5202fb8..46a8a46 100644 --- a/core/bin/openrom.dart +++ b/core/bin/openrom.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'dart:io'; + import 'package:args/args.dart'; import 'package:openrom_core/openrom_core.dart'; import 'package:path/path.dart' as p; @@ -12,7 +13,8 @@ final _progressStdout = stdout; void _renderProgress(String label, double pct, {int width = 28}) { final filled = (width * pct / 100).toInt(); final bar = '█' * filled + '░' * (width - filled); - final line = '\r \x1B[36m$bar\x1B[0m \x1B[33m${pct.toStringAsFixed(1).padLeft(5)}%\x1B[0m \x1B[90m$label\x1B[0m'; + final line = + '\r \x1B[36m$bar\x1B[0m \x1B[33m${pct.toStringAsFixed(1).padLeft(5)}%\x1B[0m \x1B[90m$label\x1B[0m'; _progressStdout.write(line); } @@ -27,50 +29,142 @@ void _jsonPrint(Map obj) { ArgParser buildParser() { final parser = ArgParser() ..addOption('input', abbr: 'i', help: 'single input ROM file') - ..addOption('folder', abbr: 'f', help: 'convert all supported ROMs in a folder') + ..addOption( + 'folder', + abbr: 'f', + help: 'convert all supported ROMs in a folder', + ) ..addOption('detect', help: 'detect file format and info') ..addOption('convert', help: 'single file to convert (alias for --input)') - ..addMultiOption('compress', help: 'file(s) to compress into ZIP/7Z', splitCommas: false) - ..addMultiOption('extract', help: 'file(s) to extract from ZIP/7Z', splitCommas: false) - ..addMultiOption('m3u', help: 'disc file(s) to generate an M3U playlist for', splitCommas: false) + ..addMultiOption( + 'compress', + help: 'file(s) to compress into ZIP/7Z', + splitCommas: false, + ) + ..addMultiOption( + 'extract', + help: 'file(s) to extract from ZIP/7Z', + splitCommas: false, + ) + ..addMultiOption( + 'm3u', + help: 'disc file(s) to generate an M3U playlist for', + splitCommas: false, + ) ..addOption('generate-cue', help: 'generate CUE file for a BIN file') - ..addOption('merge-bins', help: 'merge multi-track BIN files referenced by CUE') + ..addOption( + 'merge-bins', + help: 'merge multi-track BIN files referenced by CUE', + ) ..addOption('detect-header', help: 'detect copier header on ROM file') ..addOption('tool-path', help: 'get resolved path for a tool') ..addOption('remove-header', help: 'remove copier header from ROM file') - ..addOption('import-dat', help: 'import a No-Intro or Redump DAT file into OpenROM') - ..addOption('scan-roms', help: 'scan a ROM folder against imported DATs and show matches') - ..addOption('rename-roms', help: 'rename ROMs in a folder to canonical DAT names') - + ..addOption( + 'import-dat', + help: 'import a No-Intro or Redump DAT file into OpenROM', + ) + ..addOption( + 'scan-roms', + help: 'scan a ROM folder against imported DATs and show matches', + ) + ..addOption( + 'rename-roms', + help: 'rename ROMs in a folder to canonical DAT names', + ) // Dreamcast DCP patch - ..addOption('dcp', help: 'apply a Dreamcast DCP patch (.dcp) to an extracted disc directory') - ..addOption('disc-dir', help: 'extracted Dreamcast disc directory (used with --dcp)') - + ..addOption( + 'dcp', + help: 'apply a Dreamcast DCP patch (.dcp) to an extracted disc directory', + ) + ..addOption( + 'disc-dir', + help: 'extracted Dreamcast disc directory (used with --dcp)', + ) // IP.BIN editor - ..addOption('read-ipbin', help: 'read and display IP.BIN fields from a standalone IP.BIN or GDI') + ..addOption( + 'read-ipbin', + help: 'read and display IP.BIN fields from a standalone IP.BIN or GDI', + ) ..addOption('read-gdi', help: 'read and display GDI tracks') ..addOption('write-ipbin', help: 'IP.BIN file to modify') ..addOption('set-title', help: 'set game title in IP.BIN') ..addOption('set-region', help: 'set region code (e.g. JUE)') ..addFlag('set-vga', negatable: false, help: 'enable VGA in IP.BIN') - ..addFlag('region-free', negatable: false, help: 'enable all regions in IP.BIN') - + ..addFlag( + 'region-free', + negatable: false, + help: 'enable all regions in IP.BIN', + ) // Options - ..addOption('format', abbr: 'F', help: 'output format: CHD, CSO, ECM, ISO, BIN, BIN/CUE, XISO, ZIP, 7Z') + ..addOption( + 'format', + abbr: 'F', + help: 'output format: CHD, CSO, ECM, ISO, BIN, BIN/CUE, XISO, ZIP, 7Z', + ) ..addOption('force-platform', help: 'override automatic platform detection') - ..addFlag('list-dats', negatable: false, help: 'list all imported DAT files') - ..addFlag('dry-run', negatable: false, help: 'simulate rename without actually renaming files') - ..addFlag('json', negatable: false, help: 'output results/progress in JSON format') + ..addFlag( + 'list-dats', + negatable: false, + help: 'list all imported DAT files', + ) + ..addFlag( + 'dry-run', + negatable: false, + help: 'simulate rename without actually renaming files', + ) + ..addFlag( + 'json', + negatable: false, + help: 'output results/progress in JSON format', + ) ..addOption('output', abbr: 'o', help: 'output directory or file path') - ..addOption('compression', abbr: 'c', defaultsTo: 'Normal', help: 'compression level: Normal | High | Max') - ..addOption('level', defaultsTo: 'normal', help: 'compression level for ZIP tools: fast | normal | ultra') - ..addFlag('delete-source', negatable: false, help: 'delete source file(s) after successful operation') - ..addFlag('no-backup', negatable: false, help: 'do not keep a .bak backup file when removing header') - ..addFlag('absolute', negatable: false, help: 'use absolute file paths in M3U playlist') - ..addFlag('verify', negatable: false, help: 'verify CHD integrity after conversion') - ..addFlag('verify-only', negatable: false, help: 'verify an existing CHD file without converting') - ..addFlag('list-formats', negatable: false, help: 'print supported conversion formats and exit') - ..addFlag('quiet', abbr: 'q', negatable: false, help: 'suppress per-job log output') + ..addOption( + 'compression', + abbr: 'c', + defaultsTo: 'Normal', + help: 'compression level: Normal | High | Max', + ) + ..addOption( + 'level', + defaultsTo: 'normal', + help: 'compression level for ZIP tools: fast | normal | ultra', + ) + ..addFlag( + 'delete-source', + negatable: false, + help: 'delete source file(s) after successful operation', + ) + ..addFlag( + 'no-backup', + negatable: false, + help: 'do not keep a .bak backup file when removing header', + ) + ..addFlag( + 'absolute', + negatable: false, + help: 'use absolute file paths in M3U playlist', + ) + ..addFlag( + 'verify', + negatable: false, + help: 'verify CHD integrity after conversion', + ) + ..addFlag( + 'verify-only', + negatable: false, + help: 'verify an existing CHD file without converting', + ) + ..addFlag( + 'list-formats', + negatable: false, + help: 'print supported conversion formats and exit', + ) + ..addFlag( + 'quiet', + abbr: 'q', + negatable: false, + help: 'suppress per-job log output', + ) ..addFlag('version', abbr: 'v', negatable: false, help: 'show version'); return parser; @@ -141,19 +235,25 @@ int main(List args) { final file = (results['input'] ?? results['convert']) as String?; if (file == null) { if (isJson) { - _jsonPrint({'type': 'error', 'message': '--verify-only requires --input FILE'}); + _jsonPrint({ + 'type': 'error', + 'message': '--verify-only requires --input FILE', + }); } else { print('Error: --verify-only requires --input FILE'); } return 2; } - final ok = verifyChd(file, onLog: (msg) { - if (isJson) { - _jsonPrint({'type': 'log', 'message': msg}); - } else { - print(' $msg'); - } - }); + final ok = verifyChd( + file, + onLog: (msg) { + if (isJson) { + _jsonPrint({'type': 'log', 'message': msg}); + } else { + print(' $msg'); + } + }, + ); if (isJson) { _jsonPrint({'type': 'done', 'success': ok}); } else { @@ -185,7 +285,11 @@ int main(List args) { }, onProgress: (j, pct) { if (isJson) { - _jsonPrint({'type': 'progress', 'file': p.basename(j.filepath), 'percent': pct}); + _jsonPrint({ + 'type': 'progress', + 'file': p.basename(j.filepath), + 'percent': pct, + }); } else { _renderProgress(p.basename(j.filepath), pct); } @@ -198,7 +302,8 @@ int main(List args) { if (isJson) _jsonPrint({'type': 'done', 'success': true}); } else { failed++; - if (isJson) _jsonPrint({'type': 'done', 'success': false, 'error': job.error}); + if (isJson) + _jsonPrint({'type': 'done', 'success': false, 'error': job.error}); } } return failed == 0 ? 0 : 1; @@ -224,7 +329,11 @@ int main(List args) { }, onProgress: (j, pct) { if (isJson) { - _jsonPrint({'type': 'progress', 'file': p.basename(j.filepath), 'percent': pct}); + _jsonPrint({ + 'type': 'progress', + 'file': p.basename(j.filepath), + 'percent': pct, + }); } else { _renderProgress(p.basename(j.filepath), pct); } @@ -237,7 +346,8 @@ int main(List args) { if (isJson) _jsonPrint({'type': 'done', 'success': true}); } else { failed++; - if (isJson) _jsonPrint({'type': 'done', 'success': false, 'error': job.error}); + if (isJson) + _jsonPrint({'type': 'done', 'success': false, 'error': job.error}); } } return failed == 0 ? 0 : 1; @@ -245,10 +355,16 @@ int main(List args) { if ((results['m3u'] as List).isNotEmpty) { final discFiles = results['m3u'] as List; - final out = results['output'] as String? ?? p.dirname(p.canonicalize(discFiles.first)); + final out = + results['output'] as String? ?? + p.dirname(p.canonicalize(discFiles.first)); final rel = results['absolute'] != true; try { - final m3uFile = generateM3u(discFiles: discFiles, outputPath: out, relative: rel); + final m3uFile = generateM3u( + discFiles: discFiles, + outputPath: out, + relative: rel, + ); if (isJson) { _jsonPrint({'type': 'done', 'success': true, 'output': m3uFile}); } else { @@ -303,7 +419,11 @@ int main(List args) { outputDir: out, onProgress: (pct) { if (isJson) { - _jsonPrint({'type': 'progress', 'file': p.basename(cue), 'percent': pct}); + _jsonPrint({ + 'type': 'progress', + 'file': p.basename(cue), + 'percent': pct, + }); } else { _renderProgress(p.basename(cue), pct); } @@ -417,7 +537,9 @@ int main(List args) { } else { print('Imported DATs:'); for (final d in dats) { - print(' ${d['name']} (Source: ${d['source']}, Games: ${d['game_count']})'); + print( + ' ${d['name']} (Source: ${d['source']}, Games: ${d['game_count']})', + ); } } } @@ -450,7 +572,12 @@ int main(List args) { if (!isJson) _clearProgress(); if (r.error.isNotEmpty) { if (isJson) { - _jsonPrint({'type': 'result', 'file': r.filename, 'matched': false, 'error': r.error}); + _jsonPrint({ + 'type': 'result', + 'file': r.filename, + 'matched': false, + 'error': r.error, + }); } else { print(' ✗ ${r.filename} (${r.error})'); } @@ -469,7 +596,12 @@ int main(List args) { } } else { if (isJson) { - _jsonPrint({'type': 'result', 'file': r.filename, 'matched': false, 'crc32': r.crc32}); + _jsonPrint({ + 'type': 'result', + 'file': r.filename, + 'matched': false, + 'crc32': r.crc32, + }); } else { print(' ? ${r.filename} [${r.crc32}]'); } @@ -518,7 +650,10 @@ int main(List args) { final discDir = results['disc-dir'] as String?; if (discDir == null) { if (isJson) { - _jsonPrint({'type': 'error', 'message': '--disc-dir is required with --dcp'}); + _jsonPrint({ + 'type': 'error', + 'message': '--disc-dir is required with --dcp', + }); } else { stderr.writeln('--disc-dir is required with --dcp'); } @@ -543,7 +678,11 @@ int main(List args) { 'error': res['error'], }); } else { - print(res['success'] == true ? '✅ DCP applied.' : '❌ DCP failed: ${res['error']}'); + print( + res['success'] == true + ? '✅ DCP applied.' + : '❌ DCP failed: ${res['error']}', + ); } return res['success'] == true ? 0 : 1; } @@ -552,7 +691,8 @@ int main(List args) { final file = results['read-ipbin'] as String; final fields = readIpbin(file); if (isJson) { - final clean = Map.from(fields)..removeWhere((k, v) => k.startsWith('_')); + final clean = Map.from(fields) + ..removeWhere((k, v) => k.startsWith('_')); _jsonPrint({'type': 'done', 'success': true, 'fields': clean}); } else { fields.forEach((k, v) { @@ -574,8 +714,12 @@ int main(List args) { var fields = readIpbin(file); if (results['set-title'] != null) { final title = results['set-title'] as String; - fields['product_name'] = title.length > 16 ? title.substring(0, 16) : title; - fields['product_name_2'] = title.length > 16 ? title.substring(16, title.length > 32 ? 32 : title.length) : ''; + fields['product_name'] = title.length > 16 + ? title.substring(0, 16) + : title; + fields['product_name_2'] = title.length > 16 + ? title.substring(16, title.length > 32 ? 32 : title.length) + : ''; } if (results['set-region'] != null) { final reg = (results['set-region'] as String).toUpperCase(); @@ -606,7 +750,10 @@ int main(List args) { if (inputFile == null && folder == null) { if (isJson) { - _jsonPrint({'type': 'error', 'message': 'Provide --input FILE or --folder DIR'}); + _jsonPrint({ + 'type': 'error', + 'message': 'Provide --input FILE or --folder DIR', + }); } else { print('Error: Provide --input FILE or --folder DIR'); } @@ -676,7 +823,8 @@ int main(List args) { if (isJson) _jsonPrint({'type': 'done', 'success': true}); } else { failed++; - if (isJson) _jsonPrint({'type': 'done', 'success': false, 'error': job.error}); + if (isJson) + _jsonPrint({'type': 'done', 'success': false, 'error': job.error}); } } diff --git a/core/lib/src/bin_merger.dart b/core/lib/src/bin_merger.dart index 87fec62..2b527f2 100644 --- a/core/lib/src/bin_merger.dart +++ b/core/lib/src/bin_merger.dart @@ -2,6 +2,7 @@ // M5 Dev | GPL v3 import 'dart:io'; + import 'package:path/path.dart' as p; String _sectorsToMsf(int sectors) { @@ -52,9 +53,18 @@ List parseCue(String cuePath) { final tracks = []; String? currentFile; - final fileRegex = RegExp('FILE\\s+["\']?([^"\']+)["\']?\\s+BINARY', caseSensitive: false); - final trackRegex = RegExp('TRACK\\s+(\\d+)\\s+([^\\s]+)', caseSensitive: false); - final indexRegex = RegExp('INDEX\\s+(\\d+)\\s+(\\d{2}:\\d{2}:\\d{2})', caseSensitive: false); + final fileRegex = RegExp( + 'FILE\\s+["\']?([^"\']+)["\']?\\s+BINARY', + caseSensitive: false, + ); + final trackRegex = RegExp( + 'TRACK\\s+(\\d+)\\s+([^\\s]+)', + caseSensitive: false, + ); + final indexRegex = RegExp( + 'INDEX\\s+(\\d+)\\s+(\\d{2}:\\d{2}:\\d{2})', + caseSensitive: false, + ); final lines = File(cuePath).readAsLinesSync(); for (final line in lines) { @@ -70,13 +80,15 @@ List parseCue(String cuePath) { if (tm != null) { final num = int.parse(tm.group(1)!); final mode = tm.group(2)!; - tracks.add(CueTrack( - trackNumber: num, - mode: mode, - file: currentFile, - relFile: currentFile != null ? p.basename(currentFile) : '', - indexes: [], - )); + tracks.add( + CueTrack( + trackNumber: num, + mode: mode, + file: currentFile, + relFile: currentFile != null ? p.basename(currentFile) : '', + indexes: [], + ), + ); continue; } @@ -108,7 +120,9 @@ BinMergeResult mergeBins( } final cueDir = p.dirname(p.canonicalize(cuePath)); - final targetDir = (outputDir != null && outputDir.isNotEmpty) ? outputDir : cueDir; + final targetDir = (outputDir != null && outputDir.isNotEmpty) + ? outputDir + : cueDir; Directory(targetDir).createSync(recursive: true); final tracks = parseCue(cuePath); @@ -119,7 +133,10 @@ BinMergeResult mergeBins( for (final t in tracks) { final binFile = t.file; if (binFile == null || !File(binFile).existsSync()) { - throw FileSystemException('Referenced BIN file not found: $binFile', binFile); + throw FileSystemException( + 'Referenced BIN file not found: $binFile', + binFile, + ); } } @@ -165,16 +182,20 @@ BinMergeResult mergeBins( break; } } - final idx01Offset = idx01Entry != null ? _msfToSectors(idx01Entry.msf) : 0; + final idx01Offset = idx01Entry != null + ? _msfToSectors(idx01Entry.msf) + : 0; computedIndexes = []; for (final idxItem in parsedIndexes) { final idxOffset = _msfToSectors(idxItem.msf); final idxSectors = accumulatedSectors + (idxOffset - idx01Offset); - computedIndexes.add(CueTrackIndex( - number: idxItem.number, - msf: _sectorsToMsf(idxSectors), - )); + computedIndexes.add( + CueTrackIndex( + number: idxItem.number, + msf: _sectorsToMsf(idxSectors), + ), + ); } } else { computedIndexes = [ diff --git a/core/lib/src/compressor.dart b/core/lib/src/compressor.dart index baa8c57..d88134d 100644 --- a/core/lib/src/compressor.dart +++ b/core/lib/src/compressor.dart @@ -2,6 +2,7 @@ // M5 Dev | GPL v3 import 'dart:io'; + import 'package:archive/archive_io.dart'; import 'package:path/path.dart' as p; @@ -14,7 +15,7 @@ const Set excludedExtensions = { '.gz', '.zst', '.csz', - '.zso' + '.zso', }; class CompressionJob { @@ -36,8 +37,8 @@ class CompressionJob { this.status = 'Queued', this.progress = 0.0, this.error, - }) : format = format.toLowerCase(), - level = level.toLowerCase(); + }) : format = format.toLowerCase(), + level = level.toLowerCase(); } class Compressor { @@ -79,7 +80,8 @@ class Compressor { return extract(job); } - if (!File(job.filepath).existsSync() && !Directory(job.filepath).existsSync()) { + if (!File(job.filepath).existsSync() && + !Directory(job.filepath).existsSync()) { job.status = 'Failed'; job.error = 'File or directory not found: ${job.filepath}'; _log('[ERROR] ${job.error}'); @@ -90,7 +92,9 @@ class Compressor { job.status = 'Done'; job.progress = 100.0; job.error = 'Skipped (already compressed)'; - _log('[SKIP] ${p.basename(job.filepath)} is already compressed. Skipped.'); + _log( + '[SKIP] ${p.basename(job.filepath)} is already compressed. Skipped.', + ); _updateProgress(job, 100.0); return true; } @@ -159,7 +163,9 @@ class Compressor { bool _compressZip(CompressionJob job) { final baseName = p.basenameWithoutExtension(job.filepath); final outPath = p.join(job.outputDir, '$baseName.zip'); - _log('[ZIP] Compressing ${p.basename(job.filepath)} → ${p.basename(outPath)}'); + _log( + '[ZIP] Compressing ${p.basename(job.filepath)} → ${p.basename(outPath)}', + ); final encoder = ZipFileEncoder(); encoder.create(outPath); @@ -192,8 +198,11 @@ class Compressor { final destPath = p.normalize(p.join(basePath, file.name)); // Zip-Slip security check - if (!destPath.startsWith(basePath + p.separator) && destPath != basePath) { - throw FormatException('Malicious archive entry (Zip Slip): ${file.name}'); + if (!destPath.startsWith(basePath + p.separator) && + destPath != basePath) { + throw FormatException( + 'Malicious archive entry (Zip Slip): ${file.name}', + ); } if (file.isFile) { diff --git a/core/lib/src/config.dart b/core/lib/src/config.dart index 9cf59f8..e3872f7 100644 --- a/core/lib/src/config.dart +++ b/core/lib/src/config.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'dart:io'; + import 'package:path/path.dart' as p; const String appName = 'OpenROM'; @@ -10,7 +11,10 @@ const String appName = 'OpenROM'; String getDirectoryConfig() { String path; if (Platform.isWindows) { - final base = Platform.environment['APPDATA'] ?? Platform.environment['USERPROFILE'] ?? ''; + final base = + Platform.environment['APPDATA'] ?? + Platform.environment['USERPROFILE'] ?? + ''; path = p.join(base, appName); } else if (Platform.isMacOS) { final home = Platform.environment['HOME'] ?? ''; @@ -60,7 +64,8 @@ String getDefaultBundledPath(String tool) { } final String archFolder; - final archStr = '${Platform.version} ${Platform.operatingSystem}'.toLowerCase(); + final archStr = '${Platform.version} ${Platform.operatingSystem}' + .toLowerCase(); if (archStr.contains('aarch64') || archStr.contains('arm64')) { archFolder = 'arm64'; } else { @@ -174,7 +179,9 @@ Map loadConfig() { bool saveConfig(Map config) { try { final file = File(configFile); - file.writeAsStringSync(const JsonEncoder.withIndent(' ').convert(config)); + file.writeAsStringSync( + const JsonEncoder.withIndent(' ').convert(config), + ); _configCache = null; return true; } catch (e) { diff --git a/core/lib/src/converter.dart b/core/lib/src/converter.dart index 1fe7c34..dca9dc0 100644 --- a/core/lib/src/converter.dart +++ b/core/lib/src/converter.dart @@ -4,7 +4,9 @@ import 'config.dart' as config; import 'dart:convert'; import 'dart:io'; + import 'package:path/path.dart' as p; + import 'cue_generator.dart'; import 'detector.dart' as detector; import 'logger.dart' as logger; @@ -16,7 +18,7 @@ const List cdPlatformKeywords = [ 'Saturn', 'Sega CD', 'PC-Engine CD', - 'Neo Geo CD' + 'Neo Geo CD', ]; const Map chdCdCompression = { @@ -57,8 +59,8 @@ class ConversionJob { List? logLines, this.error, List? tempFiles, - }) : logLines = logLines ?? [], - tempFiles = tempFiles ?? []; + }) : logLines = logLines ?? [], + tempFiles = tempFiles ?? []; Map getFileInfo() { if (_fileInfo == null) { @@ -112,8 +114,10 @@ class Converter { final src = job.filepath; if (job.forcePlatform != null && job.forcePlatform!.trim().isNotEmpty) { - _log("[INFO] Platform override: using '${job.forcePlatform}' " - "(auto-detected: ${info['platform'] ?? 'UNKNOWN'})"); + _log( + "[INFO] Platform override: using '${job.forcePlatform}' " + "(auto-detected: ${info['platform'] ?? 'UNKNOWN'})", + ); } if (info.containsKey('error')) { @@ -131,7 +135,8 @@ class Converter { final valid = detector.getValidTargets(fmt); if (!valid.contains(tgt) && tgt != 'BIN/CUE' && tgt != 'FILES') { - final err = 'Cannot convert $fmt → $tgt. ' + final err = + 'Cannot convert $fmt → $tgt. ' 'Supported targets for $fmt: ${valid.isNotEmpty ? valid.join(', ') : 'none'}'; _log('[ERROR] $err'); job.error = err; @@ -190,7 +195,11 @@ class Converter { } bool _toChd( - ConversionJob job, String src, String fmt, Map info) { + ConversionJob job, + String src, + String fmt, + Map info, + ) { final chdman = detector.getChdmanPath(); final out = _outPath(job, src, '.chd'); @@ -206,8 +215,9 @@ class Converter { subCmd = 'createcd'; } - final compressionMap = - subCmd == 'createcd' ? chdCdCompression : chdDvdCompression; + final compressionMap = subCmd == 'createcd' + ? chdCdCompression + : chdDvdCompression; final defaultCodec = subCmd == 'createcd' ? 'cdlz' : 'zlib'; final cmd = [ chdman, @@ -217,7 +227,7 @@ class Converter { '-o', out, '--compression', - compressionMap[job.compression] ?? defaultCodec + compressionMap[job.compression] ?? defaultCodec, ]; if (fmt == 'BIN') { @@ -239,7 +249,11 @@ class Converter { } bool _fromChd( - ConversionJob job, String src, String tgt, Map info) { + ConversionJob job, + String src, + String tgt, + Map info, + ) { final chdman = detector.getChdmanPath(); final chdType = info['chd_type'] as String? ?? 'cd'; @@ -252,7 +266,16 @@ class Converter { final out = _outPath(job, src, '.iso'); cmd = [chdman, 'extractdvd', '-i', src, '-o', out]; } else { - cmd = [chdman, 'extractcd', '-i', src, '-o', cueOut, '--outputbin', binOut]; + cmd = [ + chdman, + 'extractcd', + '-i', + src, + '-o', + cueOut, + '--outputbin', + binOut, + ]; } } else { final out = _outPath(job, src, '.iso'); @@ -337,7 +360,9 @@ class Converter { Directory(outDir).createSync(recursive: true); final cmd = [xiso, '-x', src, '-d', outDir]; _log('[XISO→FILES] Extracting ${p.basename(src)} → $outDir/'); - _log('[XISO→FILES] Output is a folder of extracted files, not a single ISO.'); + _log( + '[XISO→FILES] Output is a folder of extracted files, not a single ISO.', + ); return _run(cmd, job); } @@ -396,7 +421,9 @@ class Converter { return false; } - _log(' cmd: ${cmd.asMap().entries.map((e) => e.key == 0 ? p.basename(e.value) : e.value).join(' ')}'); + _log( + ' cmd: ${cmd.asMap().entries.map((e) => e.key == 0 ? p.basename(e.value) : e.value).join(' ')}', + ); String lastLine = ''; try { @@ -500,20 +527,25 @@ class Converter { } double? _parseProgress(String line) { - if (RegExp(r'\b(?:cpu|memory|ram|disk|swap|usage|load)\b', caseSensitive: false) - .hasMatch(line)) { + if (RegExp( + r'\b(?:cpu|memory|ram|disk|swap|usage|load)\b', + caseSensitive: false, + ).hasMatch(line)) { return null; } - final m = RegExp(r'(?:progress|complete|done)?\s*(\d+(?:\.\d+)?)\s*%', - caseSensitive: false) - .firstMatch(line); + final m = RegExp( + r'(?:progress|complete|done)?\s*(\d+(?:\.\d+)?)\s*%', + caseSensitive: false, + ).firstMatch(line); if (m != null) { return double.tryParse(m.group(1)!); } - final mBytes = RegExp(r'Wrote\s+(\d+)\s+of\s+(\d+)\s+bytes', caseSensitive: false) - .firstMatch(line); + final mBytes = RegExp( + r'Wrote\s+(\d+)\s+of\s+(\d+)\s+bytes', + caseSensitive: false, + ).firstMatch(line); if (mBytes != null) { final written = int.parse(mBytes.group(1)!); final total = int.parse(mBytes.group(2)!); diff --git a/core/lib/src/cue_generator.dart b/core/lib/src/cue_generator.dart index 1eb4acd..913f9fe 100644 --- a/core/lib/src/cue_generator.dart +++ b/core/lib/src/cue_generator.dart @@ -3,10 +3,22 @@ import 'dart:io'; import 'dart:typed_data'; + import 'package:path/path.dart' as p; final Uint8List _cdSync = Uint8List.fromList([ - 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00 + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, ]); bool _sublistEquals(Uint8List list, int offset, Uint8List sublist) { diff --git a/core/lib/src/dcp_builder.dart b/core/lib/src/dcp_builder.dart index 0324612..c4ebabc 100644 --- a/core/lib/src/dcp_builder.dart +++ b/core/lib/src/dcp_builder.dart @@ -2,16 +2,18 @@ // M5 Dev | GPL v3 import 'dart:io'; + import 'package:archive/archive_io.dart'; import 'package:crypto/crypto.dart'; import 'package:path/path.dart' as p; + import 'config.dart' as config; import 'logger.dart' as logger; class _PendingEntry { final String archivePath; final File file; - _PendingEntry({required this.archivePath, required this.file}); + _PendingEntry({required this.archivePath, required this.file}); } /// Universal Dreamcast Patcher (DCP) builder. @@ -53,10 +55,14 @@ class DcpBuilder { Future build() async { if (!originalDir.existsSync()) { - throw FormatException('Original disc directory not found: ${originalDir.path}'); + throw FormatException( + 'Original disc directory not found: ${originalDir.path}', + ); } if (!modifiedDir.existsSync()) { - throw FormatException('Modified disc directory not found: ${modifiedDir.path}'); + throw FormatException( + 'Modified disc directory not found: ${modifiedDir.path}', + ); } _log('[DCP Builder] Scanning directory differences...'); @@ -76,7 +82,9 @@ class DcpBuilder { final bool canUseXdelta = useXdelta && File(xdelta3Path).existsSync(); if (useXdelta && !canUseXdelta) { - _log('[DCP Builder WARN] xdelta3 tool not found at "$xdelta3Path". Falling back to verbatim files.'); + _log( + '[DCP Builder WARN] xdelta3 tool not found at "$xdelta3Path". Falling back to verbatim files.', + ); } for (int i = 0; i < modFiles.length; i++) { @@ -84,12 +92,18 @@ class DcpBuilder { _updateProgress(pct); final modFile = modFiles[i]; - final relPath = p.relative(modFile.path, from: modifiedDir.path).replaceAll('\\', '/'); + final relPath = p + .relative(modFile.path, from: modifiedDir.path) + .replaceAll('\\', '/'); final normRelLower = relPath.toLowerCase(); // Bootsector / IP.BIN handling if (normRelLower == 'bootsector/ip.bin' || normRelLower == 'ip.bin') { - final origIpBinPath1 = p.join(originalDir.path, 'bootsector', 'IP.BIN'); + final origIpBinPath1 = p.join( + originalDir.path, + 'bootsector', + 'IP.BIN', + ); final origIpBinPath2 = p.join(originalDir.path, 'IP.BIN'); File? origIpBin; if (File(origIpBinPath1).existsSync()) { @@ -100,10 +114,9 @@ class DcpBuilder { if (origIpBin == null || !_filesEqual(origIpBin, modFile)) { _log('[DCP Builder] Adding bootsector/IP.BIN'); - pendingEntries.add(_PendingEntry( - archivePath: 'bootsector/IP.BIN', - file: modFile, - )); + pendingEntries.add( + _PendingEntry(archivePath: 'bootsector/IP.BIN', file: modFile), + ); ipbinAdded = true; } continue; @@ -113,15 +126,16 @@ class DcpBuilder { continue; } - final origFile = File(p.join(originalDir.path, relPath.replaceAll('/', p.separator))); + final origFile = File( + p.join(originalDir.path, relPath.replaceAll('/', p.separator)), + ); if (!origFile.existsSync()) { // New file added verbatim _log('[DCP Builder] Adding verbatim: $relPath'); - pendingEntries.add(_PendingEntry( - archivePath: relPath, - file: modFile, - )); + pendingEntries.add( + _PendingEntry(archivePath: relPath, file: modFile), + ); added++; } else { if (_filesEqual(origFile, modFile)) { @@ -143,25 +157,27 @@ class DcpBuilder { ); if (success && File(patchTmpPath).existsSync()) { - pendingEntries.add(_PendingEntry( - archivePath: xdeltaRelPath, - file: File(patchTmpPath), - )); + pendingEntries.add( + _PendingEntry( + archivePath: xdeltaRelPath, + file: File(patchTmpPath), + ), + ); diffed++; } else { - _log('[DCP Builder WARN] xdelta encoding failed for $relPath. Including verbatim.'); - pendingEntries.add(_PendingEntry( - archivePath: relPath, - file: modFile, - )); + _log( + '[DCP Builder WARN] xdelta encoding failed for $relPath. Including verbatim.', + ); + pendingEntries.add( + _PendingEntry(archivePath: relPath, file: modFile), + ); added++; } } else { _log('[DCP Builder] Adding changed file verbatim: $relPath'); - pendingEntries.add(_PendingEntry( - archivePath: relPath, - file: modFile, - )); + pendingEntries.add( + _PendingEntry(archivePath: relPath, file: modFile), + ); added++; } } @@ -173,11 +189,7 @@ class DcpBuilder { final archive = Archive(); for (final entry in pendingEntries) { final data = entry.file.readAsBytesSync(); - archive.addFile(ArchiveFile( - entry.archivePath, - data.length, - data, - )); + archive.addFile(ArchiveFile(entry.archivePath, data.length, data)); } _log('[DCP Builder] Writing DCP archive to ${outputFile.path}...'); @@ -191,7 +203,9 @@ class DcpBuilder { outputFile.writeAsBytesSync(zipData); _updateProgress(100.0); - _log('[DCP Builder] Complete. Added verbatim: $added, xdelta diffs: $diffed, IP.BIN included: $ipbinAdded'); + _log( + '[DCP Builder] Complete. Added verbatim: $added, xdelta diffs: $diffed, IP.BIN included: $ipbinAdded', + ); } finally { try { tempDir.deleteSync(recursive: true); diff --git a/core/lib/src/dcp_patcher.dart b/core/lib/src/dcp_patcher.dart index 301fb7e..885fbcb 100644 --- a/core/lib/src/dcp_patcher.dart +++ b/core/lib/src/dcp_patcher.dart @@ -2,8 +2,10 @@ // M5 Dev | GPL v3 import 'dart:io'; + import 'package:archive/archive_io.dart'; import 'package:path/path.dart' as p; + import 'config.dart' as config; import 'logger.dart' as logger; @@ -62,7 +64,10 @@ class DcpPatcher { try { stagingDir.createSync(recursive: true); } catch (e) { - return {'success': false, 'error': 'Could not create staging directory: $e'}; + return { + 'success': false, + 'error': 'Could not create staging directory: $e', + }; } try { @@ -102,8 +107,11 @@ class DcpPatcher { final name = file.name; final destPath = p.normalize(p.join(basePath, name)); - if (!destPath.startsWith(basePath + p.separator) && destPath != basePath) { - throw FormatException('Malicious archive entry detected (Path Traversal): $name'); + if (!destPath.startsWith(basePath + p.separator) && + destPath != basePath) { + throw FormatException( + 'Malicious archive entry detected (Path Traversal): $name', + ); } if (name.toLowerCase() == 'bootsector/ip.bin') { @@ -137,15 +145,18 @@ class DcpPatcher { ignoreChecksum, ); result['xdelta_applied'] = xdeltaApplied; - result['files_patched'] = (result['files_patched'] as int) + xdeltaApplied; + result['files_patched'] = + (result['files_patched'] as int) + xdeltaApplied; } _updateProgress(100.0); _commitStaging(stagingDir, outputPath); result['success'] = true; - _log('[DCP] Done. Files patched: ${result["files_patched"]}, ' - 'xdelta: ${result["xdelta_applied"]}, ' - 'IP.BIN replaced: ${result["ipbin_replaced"]}'); + _log( + '[DCP] Done. Files patched: ${result["files_patched"]}, ' + 'xdelta: ${result["xdelta_applied"]}, ' + 'IP.BIN replaced: ${result["ipbin_replaced"]}', + ); return result; } catch (e) { result['error'] = e.toString(); @@ -181,14 +192,20 @@ class DcpPatcher { for (final ext in ['.xdelta3', '.xdelta', '.xd', '.vcdiff']) { if (targetName.toLowerCase().endsWith(ext)) { - targetName = targetName.substring(0, targetName.length - ext.length); + targetName = targetName.substring( + 0, + targetName.length - ext.length, + ); break; } } final destPath = p.normalize(p.join(basePath, targetName)); - if (!destPath.startsWith(basePath + p.separator) && destPath != basePath) { - throw FormatException('Malicious archive entry detected (Path Traversal): ${entry.name}'); + if (!destPath.startsWith(basePath + p.separator) && + destPath != basePath) { + throw FormatException( + 'Malicious archive entry detected (Path Traversal): ${entry.name}', + ); } final patchTmp = p.join(tempDir.path, p.basename(relativePatchName)); diff --git a/core/lib/src/detector.dart b/core/lib/src/detector.dart index 7ffa57a..b8f02a2 100644 --- a/core/lib/src/detector.dart +++ b/core/lib/src/detector.dart @@ -3,7 +3,9 @@ import 'dart:io'; import 'dart:typed_data'; + import 'package:path/path.dart' as p; + import 'config.dart' as config; const Map supportedInput = { @@ -139,7 +141,16 @@ const Map commandTemplates = { }; // Magic bytes -final Uint8List chdMagic = Uint8List.fromList([0x4D, 0x43, 0x6F, 0x6D, 0x70, 0x72, 0x48, 0x44]); // MComprHD +final Uint8List chdMagic = Uint8List.fromList([ + 0x4D, + 0x43, + 0x6F, + 0x6D, + 0x70, + 0x72, + 0x48, + 0x44, +]); // MComprHD final Uint8List ps2Magic = Uint8List.fromList('PLAYSTATION'.codeUnits); final Uint8List ps1Magic = Uint8List.fromList('PlayStation'.codeUnits); const int gcMagic = 0xC2339F3D; @@ -149,10 +160,14 @@ final Uint8List saturnMagic1 = Uint8List.fromList('SEGA SEGASATURN'.codeUnits); final Uint8List saturnMagic2 = Uint8List.fromList('SEGA SATURN '.codeUnits); final Uint8List segacdMagic1 = Uint8List.fromList('SEGADISCSYSTEM'.codeUnits); final Uint8List segacdMagic2 = Uint8List.fromList('SEGA_CD'.codeUnits); -final Uint8List pcecdMagic = Uint8List.fromList('PC Engine CD-ROM SYSTEM'.codeUnits); +final Uint8List pcecdMagic = Uint8List.fromList( + 'PC Engine CD-ROM SYSTEM'.codeUnits, +); final Uint8List neogeocdMagic = Uint8List.fromList('NEO-GEO CD'.codeUnits); -final Uint8List xboxMagic = Uint8List.fromList('MICROSOFT*XBOX*MEDIA'.codeUnits); +final Uint8List xboxMagic = Uint8List.fromList( + 'MICROSOFT*XBOX*MEDIA'.codeUnits, +); const int xboxOffset1 = 0x10000; const int xboxOffset2 = 0x2090000; @@ -188,13 +203,20 @@ List getValidTargets(String fmt) { return conversionMap[fmt.toUpperCase()] ?? []; } -String getCommandPreview(String fmt, String target, [String filename = 'game.iso']) { +String getCommandPreview( + String fmt, + String target, [ + String filename = 'game.iso', +]) { final key = '${fmt.toUpperCase()}->${target.toUpperCase()}'; final template = commandTemplates[key]; if (template == null) { return '$fmt -> $target'; } - final outName = getOutputName(filename, target.toLowerCase().replaceAll('/cue', '')); + final outName = getOutputName( + filename, + target.toLowerCase().replaceAll('/cue', ''), + ); final idx = filename.lastIndexOf('.'); final base = idx != -1 ? filename.substring(0, idx) : filename; final cueName = '$base.cue'; @@ -483,7 +505,8 @@ String _chdTypeBySize(int size) { String? _findPair(String filepath, String targetExt) { final idx = filepath.lastIndexOf('.'); final base = idx != -1 ? filepath.substring(0, idx) : filepath; - final candidate = base + (targetExt.startsWith('.') ? targetExt : '.$targetExt'); + final candidate = + base + (targetExt.startsWith('.') ? targetExt : '.$targetExt'); return File(candidate).existsSync() ? candidate : null; } diff --git a/core/lib/src/gdi_reader.dart b/core/lib/src/gdi_reader.dart index ca11238..75df8a8 100644 --- a/core/lib/src/gdi_reader.dart +++ b/core/lib/src/gdi_reader.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'dart:typed_data'; + import 'package:path/path.dart' as p; class GdiTrack { @@ -43,7 +44,10 @@ List parseGdi(String gdiPath) { final gdiDir = p.dirname(p.canonicalize(gdiPath)); final tracks = []; - final lines = file.readAsLinesSync().where((l) => l.trim().isNotEmpty).toList(); + final lines = file + .readAsLinesSync() + .where((l) => l.trim().isNotEmpty) + .toList(); if (lines.isEmpty) return tracks; for (final line in lines.skip(1)) { @@ -60,16 +64,20 @@ List parseGdi(String gdiPath) { filename = filename.substring(1, filename.length - 1); } final filepath = p.join(gdiDir, filename); - final filesize = File(filepath).existsSync() ? File(filepath).lengthSync() : 0; - - tracks.add(GdiTrack( - number: num, - lba: lba, - trackType: ttype, - sectorSize: sectorSize, - filename: filepath, - filesize: filesize, - )); + final filesize = File(filepath).existsSync() + ? File(filepath).lengthSync() + : 0; + + tracks.add( + GdiTrack( + number: num, + lba: lba, + trackType: ttype, + sectorSize: sectorSize, + filename: filepath, + filesize: filesize, + ), + ); } catch (_) {} } @@ -92,7 +100,10 @@ Uint8List extractIpbinFromGdi(String gdiPath) { final track = hdTracks.first; final trackFile = File(track.filename); if (!trackFile.existsSync()) { - throw FileSystemException('Track file not found: ${track.filename}', track.filename); + throw FileSystemException( + 'Track file not found: ${track.filename}', + track.filename, + ); } final raf = trackFile.openSync(mode: FileMode.read); diff --git a/core/lib/src/header_remover.dart b/core/lib/src/header_remover.dart index dabe34c..1b610da 100644 --- a/core/lib/src/header_remover.dart +++ b/core/lib/src/header_remover.dart @@ -3,25 +3,26 @@ import 'dart:io'; import 'dart:typed_data'; + import 'package:path/path.dart' as p; const Map> supportedHeaders = { 'NES': { 'magic': [0x4E, 0x45, 0x53, 0x1A], 'size': 16, - 'ext': ['.nes'] + 'ext': ['.nes'], }, 'SNES': { 'size': 512, - 'ext': ['.smc', '.sfc', '.fig', '.swc'] + 'ext': ['.smc', '.sfc', '.fig', '.swc'], }, 'GB/GBC': { 'size': 512, - 'ext': ['.gb', '.gbc'] + 'ext': ['.gb', '.gbc'], }, 'GBA': { 'size': 0, - 'ext': ['.gba'] + 'ext': ['.gba'], }, }; @@ -71,14 +72,14 @@ Map? detectHeader(String filepath) { 'system': 'NES', 'header_size': 16, 'has_header': true, - 'confidence': 'certain' + 'confidence': 'certain', }; } else { return { 'system': 'NES', 'header_size': 0, 'has_header': false, - 'confidence': 'certain' + 'confidence': 'certain', }; } } catch (_) { @@ -93,24 +94,24 @@ Map? detectHeader(String filepath) { final hasCopierHeaderInternal = _verifySnesInternalHeader(headerData, 0x81C0) || - _verifySnesInternalHeader(headerData, 0x101C0); + _verifySnesInternalHeader(headerData, 0x101C0); final hasCleanInternal = _verifySnesInternalHeader(headerData, 0x7FC0) || - _verifySnesInternalHeader(headerData, 0xFFC0); + _verifySnesInternalHeader(headerData, 0xFFC0); if (hasCopierHeaderInternal) { return { 'system': 'SNES', 'header_size': 512, 'has_header': true, - 'confidence': 'certain' + 'confidence': 'certain', }; } else if (hasCleanInternal) { return { 'system': 'SNES', 'header_size': 0, 'has_header': false, - 'confidence': 'certain' + 'confidence': 'certain', }; } } catch (_) {} @@ -120,21 +121,21 @@ Map? detectHeader(String filepath) { 'system': 'SNES', 'header_size': 512, 'has_header': true, - 'confidence': 'likely' + 'confidence': 'likely', }; } else if (rem == 0) { return { 'system': 'SNES', 'header_size': 0, 'has_header': false, - 'confidence': 'certain' + 'confidence': 'certain', }; } else { return { 'system': 'SNES', 'header_size': 0, 'has_header': false, - 'confidence': 'unlikely' + 'confidence': 'unlikely', }; } } else if (system == 'GB/GBC') { @@ -144,21 +145,21 @@ Map? detectHeader(String filepath) { 'system': 'GB/GBC', 'header_size': 512, 'has_header': true, - 'confidence': 'certain' + 'confidence': 'certain', }; } else if (rem == 0) { return { 'system': 'GB/GBC', 'header_size': 0, 'has_header': false, - 'confidence': 'certain' + 'confidence': 'certain', }; } else { return { 'system': 'GB/GBC', 'header_size': 0, 'has_header': false, - 'confidence': 'likely' + 'confidence': 'likely', }; } } else if (system == 'GBA') { @@ -166,18 +167,14 @@ Map? detectHeader(String filepath) { 'system': 'GBA', 'header_size': 0, 'has_header': false, - 'confidence': 'certain' + 'confidence': 'certain', }; } return null; } -String removeHeader( - String filepath, { - String? outputDir, - bool backup = true, -}) { +String removeHeader(String filepath, {String? outputDir, bool backup = true}) { final file = File(filepath); if (!file.existsSync()) { throw FileSystemException('ROM file not found: $filepath', filepath); diff --git a/core/lib/src/ipbin_editor.dart b/core/lib/src/ipbin_editor.dart index e07ba37..d2aeabf 100644 --- a/core/lib/src/ipbin_editor.dart +++ b/core/lib/src/ipbin_editor.dart @@ -44,13 +44,16 @@ Map readIpbin(String path) { final data = file.readAsBytesSync(); if (data.length < ipbinSize) { throw FormatException( - 'File too small to be IP.BIN: ${data.length} bytes (need $ipbinSize)'); + 'File too small to be IP.BIN: ${data.length} bytes (need $ipbinSize)', + ); } final hardwareIdBytes = data.sublist(0x00, 0x10); final hardwareId = ascii.decode(hardwareIdBytes, allowInvalid: true).trim(); if (!hardwareId.contains('SEGA')) { - throw FormatException('Not a valid Dreamcast IP.BIN (hardware_id: "$hardwareId")'); + throw FormatException( + 'Not a valid Dreamcast IP.BIN (hardware_id: "$hardwareId")', + ); } final result = { @@ -88,7 +91,9 @@ Map readIpbin(String path) { void writeIpbin(Map fields, String outputPath) { final raw = fields['_raw'] as Uint8List?; if (raw == null || raw.length < ipbinSize) { - throw ArgumentError('fields must contain valid _raw Uint8List buffer of 2048 bytes'); + throw ArgumentError( + 'fields must contain valid _raw Uint8List buffer of 2048 bytes', + ); } final data = Uint8List.fromList(raw); diff --git a/core/lib/src/logger.dart b/core/lib/src/logger.dart index 5ff1cd4..8572b9a 100644 --- a/core/lib/src/logger.dart +++ b/core/lib/src/logger.dart @@ -2,7 +2,9 @@ // M5 Dev | GPL v3 import 'dart:io'; + import 'package:path/path.dart' as p; + import 'config.dart'; void log(String message) { diff --git a/core/lib/src/m3u_generator.dart b/core/lib/src/m3u_generator.dart index a686b52..09f4f9e 100644 --- a/core/lib/src/m3u_generator.dart +++ b/core/lib/src/m3u_generator.dart @@ -2,6 +2,7 @@ // M5 Dev | GPL v3 import 'dart:io'; + import 'package:path/path.dart' as p; String generateM3u({ @@ -19,7 +20,9 @@ String generateM3u({ } } - final targetDir = Directory(outputPath).existsSync() || !outputPath.toLowerCase().endsWith('.m3u') + final targetDir = + Directory(outputPath).existsSync() || + !outputPath.toLowerCase().endsWith('.m3u') ? outputPath : p.dirname(outputPath); diff --git a/core/lib/src/rom_renamer.dart b/core/lib/src/rom_renamer.dart index 726ea75..97fb64f 100644 --- a/core/lib/src/rom_renamer.dart +++ b/core/lib/src/rom_renamer.dart @@ -3,8 +3,10 @@ import 'dart:io'; import 'dart:typed_data'; + import 'package:path/path.dart' as p; import 'package:xml/xml_events.dart'; + import 'config.dart' as config; final Uint32List _crc32Table = () { @@ -162,7 +164,10 @@ String calcCrc32(String filepath, {void Function(double pct)? onProgress}) { raf.closeSync(); } - final hex = (crc & 0xFFFFFFFF).toRadixString(16).toLowerCase().padLeft(8, '0'); + final hex = (crc & 0xFFFFFFFF) + .toRadixString(16) + .toLowerCase() + .padLeft(8, '0'); return hex; } @@ -211,7 +216,9 @@ Map> loadDatIndex(String datPath) { if (crc.isNotEmpty) { index[crc] = { 'name': currentGameName, - 'description': currentDesc.isNotEmpty ? currentDesc : currentGameName, + 'description': currentDesc.isNotEmpty + ? currentDesc + : currentGameName, 'rom_name': romName, 'size': size, 'md5': md5, @@ -290,17 +297,44 @@ List scanFolder( for (final sk in skippedDats) { if (onProgress != null) { - onProgress('[WARN] Skipped unreadable DAT: ${sk['name']} (${sk['reason']})', 0.0); + onProgress( + '[WARN] Skipped unreadable DAT: ${sk['name']} (${sk['reason']})', + 0.0, + ); } } const extensions = { - '.smc', '.sfc', '.nes', '.gba', '.gb', '.gbc', - '.n64', '.z64', '.v64', '.ndd', - '.md', '.gen', '.sms', '.gg', '.32x', - '.pce', '.iso', '.bin', '.img', '.chd', - '.ws', '.wsc', '.ngp', '.ngc', - '.lnx', '.vb', '.vec', '.a26', '.a52', '.j64', + '.smc', + '.sfc', + '.nes', + '.gba', + '.gb', + '.gbc', + '.n64', + '.z64', + '.v64', + '.ndd', + '.md', + '.gen', + '.sms', + '.gg', + '.32x', + '.pce', + '.iso', + '.bin', + '.img', + '.chd', + '.ws', + '.wsc', + '.ngp', + '.ngc', + '.lnx', + '.vb', + '.vec', + '.a26', + '.a52', + '.j64', }; final entries = dir.listSync(); @@ -322,9 +356,12 @@ List scanFolder( final result = RomScanResult(filepath: fpath, filename: fname); try { - result.crc32 = calcCrc32(fpath, onProgress: (pct) { - if (onProgress != null) onProgress(fname, pct); - }); + result.crc32 = calcCrc32( + fpath, + onProgress: (pct) { + if (onProgress != null) onProgress(fname, pct); + }, + ); for (final datEntry in datIndexes) { final index = datEntry['index'] as Map>; diff --git a/core/lib/src/ssp_builder.dart b/core/lib/src/ssp_builder.dart index fe15347..dd9420e 100644 --- a/core/lib/src/ssp_builder.dart +++ b/core/lib/src/ssp_builder.dart @@ -4,8 +4,10 @@ import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; + import 'package:archive/archive.dart'; import 'package:crypto/crypto.dart'; + import 'logger.dart' as logger; /// Mode-1 EDC and ECC P/Q recalculation primitives (ported from ecmlib / Neill Corlett). @@ -214,7 +216,9 @@ class BsdiffEncoder { } for (int i = 0; i < lenf; i++) { - diffBuf.addByte((newData[lastscan + i] - oldData[lastpos + i]) & 0xFF); + diffBuf.addByte( + (newData[lastscan + i] - oldData[lastpos + i]) & 0xFF, + ); } for (int i = 0; i < (scan - lenb) - (lastscan + lenf); i++) { @@ -337,11 +341,7 @@ class IsoDirEntry { final int lba; final int size; - IsoDirEntry({ - required this.filename, - required this.lba, - required this.size, - }); + IsoDirEntry({required this.filename, required this.lba, required this.size}); } /// Helper class for reading ISO9660 root directory from raw 2352-byte sector BIN tracks. @@ -361,7 +361,9 @@ class Iso9660Reader { // Check CD001 magic final magic = utf8.decode(pvdPayload.sublist(1, 6), allowMalformed: true); if (magic != 'CD001' || pvdPayload[0] != 0x01) { - throw FormatException('Not a valid ISO9660 image (missing CD001 PVD at LBA 16).'); + throw FormatException( + 'Not a valid ISO9660 image (missing CD001 PVD at LBA 16).', + ); } final rootRec = pvdPayload.sublist(156, 156 + 34); @@ -531,7 +533,9 @@ class SspBuilder { _updateProgress(pct); if (!origDirMap.containsKey(filename)) { - _log('[SSP Builder WARN] File $filename present in modified BIN but missing in original. Skipping.'); + _log( + '[SSP Builder WARN] File $filename present in modified BIN but missing in original. Skipping.', + ); continue; } @@ -559,7 +563,9 @@ class SspBuilder { ); } - _log('[SSP Builder] Encoding bsdiff diff for $filename (${origBytes.length} bytes)...'); + _log( + '[SSP Builder] Encoding bsdiff diff for $filename (${origBytes.length} bytes)...', + ); final origMd5 = md5.convert(origBytes).toString().toUpperCase(); md5Lines.add('$filename: $origMd5'); @@ -567,26 +573,28 @@ class SspBuilder { final patchData = bsdiff.encode(origBytes, modBytes); final dfrFilename = '$filename._DFR'; - archive.addFile(ArchiveFile( - dfrFilename, - patchData.length, - patchData, - )); + archive.addFile(ArchiveFile(dfrFilename, patchData.length, patchData)); changedCount++; } - _log('[SSP Builder] Packaging SSP archive with $changedCount patch entries...'); + _log( + '[SSP Builder] Packaging SSP archive with $changedCount patch entries...', + ); _updateProgress(92.0); // changes.md5 final changesMd5Content = '${md5Lines.join('\n')}\n'; final changesBytes = utf8.encode(changesMd5Content); - archive.addFile(ArchiveFile('changes.md5', changesBytes.length, changesBytes)); + archive.addFile( + ArchiveFile('changes.md5', changesBytes.length, changesBytes), + ); // version.txt final versionBytes = utf8.encode('$version\n'); - archive.addFile(ArchiveFile('version.txt', versionBytes.length, versionBytes)); + archive.addFile( + ArchiveFile('version.txt', versionBytes.length, versionBytes), + ); outputFile.parent.createSync(recursive: true); final zipData = ZipEncoder().encode(archive); @@ -597,7 +605,9 @@ class SspBuilder { outputFile.writeAsBytesSync(zipData); _updateProgress(100.0); - _log('[SSP Builder] Complete. Generated .ssp with $changedCount modified files.'); + _log( + '[SSP Builder] Complete. Generated .ssp with $changedCount modified files.', + ); } bool _bytesEqual(List a, List b) { diff --git a/core/lib/src/validator.dart b/core/lib/src/validator.dart index 4701a73..1f2f76a 100644 --- a/core/lib/src/validator.dart +++ b/core/lib/src/validator.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'dart:io'; + import 'detector.dart' as detector; import 'logger.dart' as logger; diff --git a/core/test/dcp_builder_test.dart b/core/test/dcp_builder_test.dart index bee7a99..83081c6 100644 --- a/core/test/dcp_builder_test.dart +++ b/core/test/dcp_builder_test.dart @@ -1,4 +1,5 @@ import 'dart:io'; + import 'package:archive/archive_io.dart'; import 'package:openrom_core/openrom_core.dart'; import 'package:path/path.dart' as p; @@ -90,10 +91,14 @@ void main() { expect(result['success'], isTrue); - final patchedIpBin = File(p.join(outputDir.path, 'IP.BIN')).readAsStringSync(); - final patched1stRead = File(p.join(outputDir.path, '1ST_READ.BIN')).readAsStringSync(); - final patchedConfig = File(p.join(outputDir.path, 'DATA', 'CONFIG.TXT')).readAsStringSync(); - final patchedNewFile = File(p.join(outputDir.path, 'DATA', 'NEWFILE.BIN')).readAsStringSync(); + final patchedIpBin = File(p.join(outputDir.path, 'IP.BIN')) + .readAsStringSync(); + final patched1stRead = File(p.join(outputDir.path, '1ST_READ.BIN')) + .readAsStringSync(); + final patchedConfig = File(p.join(outputDir.path, 'DATA', 'CONFIG.TXT')) + .readAsStringSync(); + final patchedNewFile = File(p.join(outputDir.path, 'DATA', 'NEWFILE.BIN')) + .readAsStringSync(); expect(patchedIpBin, equals('MODIFIED_BOOTSECTOR_IPBIN_HEADER_67890')); expect(patched1stRead, equals('MODIFIED_GAME_CODE_XYZ123')); diff --git a/core/test/dcp_patcher_test.dart b/core/test/dcp_patcher_test.dart index 62ba052..39a014a 100644 --- a/core/test/dcp_patcher_test.dart +++ b/core/test/dcp_patcher_test.dart @@ -1,4 +1,5 @@ import 'dart:io'; + import 'package:archive/archive_io.dart'; import 'package:openrom_core/openrom_core.dart'; import 'package:path/path.dart' as p; @@ -13,7 +14,8 @@ void main() { Directory(outputDir).createSync(); Directory(discDir).createSync(); - File(p.join(discDir, 'track01.bin')).writeAsStringSync('dummy track data'); + File(p.join(discDir, 'track01.bin')) + .writeAsStringSync('dummy track data'); final encoder = ZipFileEncoder(); final maliciousDcp = p.join(tmpDir.path, 'malicious.dcp'); @@ -32,7 +34,10 @@ void main() { ); expect(res['success'], isFalse); - expect(res['error'].toString(), contains('Malicious archive entry detected')); + expect( + res['error'].toString(), + contains('Malicious archive entry detected'), + ); } finally { tmpDir.deleteSync(recursive: true); } diff --git a/core/test/detector_test.dart b/core/test/detector_test.dart index 4738388..4fac3a3 100644 --- a/core/test/detector_test.dart +++ b/core/test/detector_test.dart @@ -1,4 +1,5 @@ import 'dart:io'; + import 'package:openrom_core/openrom_core.dart'; import 'package:path/path.dart' as p; import 'package:test/test.dart'; @@ -21,17 +22,61 @@ void main() { test('detect_file_cases', () { final cases = [ - ['tom_jerry.bin.ecm', 'ECM', ['ISO', 'BIN']], - ['game.iso', 'ISO', ['CHD', 'CSO', 'ECM', 'XISO', 'RVZ', 'NKIT']], - ['disc.bin', 'BIN', ['CHD', 'ECM']], - ['disc.cue', 'CUE', ['CHD']], - ['sonic.gdi', 'GDI', ['CHD']], - ['halo.chd', 'CHD', ['ISO', 'BIN/CUE']], - ['psp_game.cso', 'CSO', ['ISO']], - ['psp_game.zso', 'ZSO', ['ISO']], - ['xbox_iso.iso', 'ISO', ['CHD', 'CSO', 'ECM', 'XISO', 'RVZ', 'NKIT']], - ['game360.cci', 'CCI', ['ISO']], - ['game360.zar', 'ZAR', ['ISO']], + [ + 'tom_jerry.bin.ecm', + 'ECM', + ['ISO', 'BIN'], + ], + [ + 'game.iso', + 'ISO', + ['CHD', 'CSO', 'ECM', 'XISO', 'RVZ', 'NKIT'], + ], + [ + 'disc.bin', + 'BIN', + ['CHD', 'ECM'], + ], + [ + 'disc.cue', + 'CUE', + ['CHD'], + ], + [ + 'sonic.gdi', + 'GDI', + ['CHD'], + ], + [ + 'halo.chd', + 'CHD', + ['ISO', 'BIN/CUE'], + ], + [ + 'psp_game.cso', + 'CSO', + ['ISO'], + ], + [ + 'psp_game.zso', + 'ZSO', + ['ISO'], + ], + [ + 'xbox_iso.iso', + 'ISO', + ['CHD', 'CSO', 'ECM', 'XISO', 'RVZ', 'NKIT'], + ], + [ + 'game360.cci', + 'CCI', + ['ISO'], + ], + [ + 'game360.zar', + 'ZAR', + ['ISO'], + ], ]; final tmpDir = Directory.systemTemp.createTempSync('detector_test_'); @@ -47,8 +92,16 @@ void main() { final res = detectFile(filepath); expect(res.containsKey('error'), isFalse, reason: 'Error in $filename'); - expect(res['format'], equals(expectedFmt), reason: 'Format mismatch for $filename'); - expect(res['valid_targets'], equals(expectedTargets), reason: 'Targets mismatch for $filename'); + expect( + res['format'], + equals(expectedFmt), + reason: 'Format mismatch for $filename', + ); + expect( + res['valid_targets'], + equals(expectedTargets), + reason: 'Targets mismatch for $filename', + ); } } finally { tmpDir.deleteSync(recursive: true); @@ -84,7 +137,6 @@ void main() { } }); - test('missing_tool_preflight_check', () { final tmpDir = Directory.systemTemp.createTempSync('missing_tool_test_'); try { diff --git a/core/test/header_remover_test.dart b/core/test/header_remover_test.dart index 50c7dad..dcc071d 100644 --- a/core/test/header_remover_test.dart +++ b/core/test/header_remover_test.dart @@ -1,4 +1,5 @@ import 'dart:io'; + import 'package:openrom_core/openrom_core.dart'; import 'package:path/path.dart' as p; import 'package:test/test.dart'; @@ -8,7 +9,24 @@ void main() { final tmpDir = Directory.systemTemp.createTempSync('hdr_test_'); try { final romPath = p.join(tmpDir.path, 'game.nes'); - final header = [0x4E, 0x45, 0x53, 0x1A, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + final header = [ + 0x4E, + 0x45, + 0x53, + 0x1A, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ]; final body = List.filled(100, 0xFF); File(romPath).writeAsBytesSync([...header, ...body]); diff --git a/core/test/rom_renamer_test.dart b/core/test/rom_renamer_test.dart index d996e3a..7b4d744 100644 --- a/core/test/rom_renamer_test.dart +++ b/core/test/rom_renamer_test.dart @@ -1,4 +1,5 @@ import 'dart:io'; + import 'package:openrom_core/openrom_core.dart'; import 'package:path/path.dart' as p; import 'package:test/test.dart'; diff --git a/core/test/ssp_builder_test.dart b/core/test/ssp_builder_test.dart index 7f36e0b..9ebd178 100644 --- a/core/test/ssp_builder_test.dart +++ b/core/test/ssp_builder_test.dart @@ -38,7 +38,8 @@ void main() { ecc.recalculateSector(sector); // Verify EDC non-zero - final edcVal = sector[0x810] | + final edcVal = + sector[0x810] | (sector[0x811] << 8) | (sector[0x812] << 16) | (sector[0x813] << 24); @@ -86,38 +87,44 @@ void main() { } catch (_) {} }); - test('SspBuilder generates valid .ssp ZIP archive with expected structure', () async { - final builder = SspBuilder( - originalBin: origBin, - modifiedBin: modBin, - outputFile: sspOutput, - version: '1.2.3', - ); + test( + 'SspBuilder generates valid .ssp ZIP archive with expected structure', + () async { + final builder = SspBuilder( + originalBin: origBin, + modifiedBin: modBin, + outputFile: sspOutput, + version: '1.2.3', + ); - await builder.build(); + await builder.build(); - expect(sspOutput.existsSync(), isTrue); + expect(sspOutput.existsSync(), isTrue); - final archiveBytes = sspOutput.readAsBytesSync(); - final archive = ZipDecoder().decodeBytes(archiveBytes); + final archiveBytes = sspOutput.readAsBytesSync(); + final archive = ZipDecoder().decodeBytes(archiveBytes); - final fileNames = archive.map((f) => f.name).toList(); - expect(fileNames, contains('changes.md5')); - expect(fileNames, contains('version.txt')); - expect(fileNames, contains('TEST.BIN._DFR')); + final fileNames = archive.map((f) => f.name).toList(); + expect(fileNames, contains('changes.md5')); + expect(fileNames, contains('version.txt')); + expect(fileNames, contains('TEST.BIN._DFR')); - final changesFile = archive.firstWhere((f) => f.name == 'changes.md5'); - final changesTxt = utf8.decode(changesFile.content as List); + final changesFile = archive.firstWhere((f) => f.name == 'changes.md5'); + final changesTxt = utf8.decode(changesFile.content as List); - final origIsoContent = Iso9660Reader.readIsoFile(origBin, 18, 39); - final expectedMd5 = md5.convert(origIsoContent).toString().toUpperCase(); + final origIsoContent = Iso9660Reader.readIsoFile(origBin, 18, 39); + final expectedMd5 = md5 + .convert(origIsoContent) + .toString() + .toUpperCase(); - expect(changesTxt, contains('TEST.BIN: $expectedMd5')); + expect(changesTxt, contains('TEST.BIN: $expectedMd5')); - final versionFile = archive.firstWhere((f) => f.name == 'version.txt'); - final versionTxt = utf8.decode(versionFile.content as List).trim(); - expect(versionTxt, equals('1.2.3')); - }); + final versionFile = archive.firstWhere((f) => f.name == 'version.txt'); + final versionTxt = utf8.decode(versionFile.content as List).trim(); + expect(versionTxt, equals('1.2.3')); + }, + ); test('SspBuilder generated SSP can be applied with saturn-patcher CLI binary if available', () async { final builder = SspBuilder( @@ -127,7 +134,9 @@ void main() { ); await builder.build(); - final saturnPatcherCli = File('/tmp/saturn-patcher-egui/target/debug/saturn-patcher'); + final saturnPatcherCli = File( + '/tmp/saturn-patcher-egui/target/debug/saturn-patcher', + ); if (!saturnPatcherCli.existsSync()) { return; // Skip Rust CLI test if binary isn't built } @@ -140,10 +149,16 @@ void main() { sspOutput.path, ]); - expect(res.exitCode, equals(0), reason: 'saturn-patcher stderr: ${res.stderr}'); + expect( + res.exitCode, + equals(0), + reason: 'saturn-patcher stderr: ${res.stderr}', + ); final patchedIsoContent = Iso9660Reader.readIsoFile(targetBin, 18, 39); - final expectedModContent = ascii.encode('MODIFIED SATURN FILE CONTENT 1234567890'); + final expectedModContent = ascii.encode( + 'MODIFIED SATURN FILE CONTENT 1234567890', + ); expect(patchedIsoContent, equals(expectedModContent)); }); }); diff --git a/gui/lib/l10n/app_localizations.dart b/gui/lib/l10n/app_localizations.dart index e3e8192..ef13b4b 100644 --- a/gui/lib/l10n/app_localizations.dart +++ b/gui/lib/l10n/app_localizations.dart @@ -72,7 +72,7 @@ import 'app_localizations_zh.dart'; /// property. abstract class AppLocalizations { AppLocalizations(String locale) - : localeName = intl.Intl.canonicalizedLocale(locale.toString()); + : localeName = intl.Intl.canonicalizedLocale(locale.toString()); final String localeName; @@ -95,11 +95,11 @@ abstract class AppLocalizations { /// of delegates is preferred or required. static const List> localizationsDelegates = >[ - delegate, - GlobalMaterialLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - ]; + delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ]; /// A list of this localizations delegate's supported locales. static const List supportedLocales = [ @@ -113,7 +113,7 @@ abstract class AppLocalizations { Locale('pt'), Locale('ru'), Locale('tr'), - Locale('zh') + Locale('zh'), ]; /// No description provided for @appTitle. @@ -878,18 +878,18 @@ class _AppLocalizationsDelegate @override bool isSupported(Locale locale) => [ - 'ar', - 'de', - 'en', - 'es', - 'fr', - 'it', - 'ja', - 'pt', - 'ru', - 'tr', - 'zh' - ].contains(locale.languageCode); + 'ar', + 'de', + 'en', + 'es', + 'fr', + 'it', + 'ja', + 'pt', + 'ru', + 'tr', + 'zh', + ].contains(locale.languageCode); @override bool shouldReload(_AppLocalizationsDelegate old) => false; @@ -923,8 +923,9 @@ AppLocalizations lookupAppLocalizations(Locale locale) { } throw FlutterError( - 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' - 'an issue with the localizations generation tool. Please file an issue ' - 'on GitHub with a reproducible sample app and the gen-l10n configuration ' - 'that was used.'); + 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' + 'an issue with the localizations generation tool. Please file an issue ' + 'on GitHub with a reproducible sample app and the gen-l10n configuration ' + 'that was used.', + ); } diff --git a/gui/lib/l10n/app_localizations_ar.dart b/gui/lib/l10n/app_localizations_ar.dart index c874632..ba03841 100644 --- a/gui/lib/l10n/app_localizations_ar.dart +++ b/gui/lib/l10n/app_localizations_ar.dart @@ -1,5 +1,6 @@ // ignore: unused_import import 'package:intl/intl.dart' as intl; + import 'app_localizations.dart'; // ignore_for_file: type=lint diff --git a/gui/lib/l10n/app_localizations_de.dart b/gui/lib/l10n/app_localizations_de.dart index be78c67..13a01cb 100644 --- a/gui/lib/l10n/app_localizations_de.dart +++ b/gui/lib/l10n/app_localizations_de.dart @@ -1,5 +1,6 @@ // ignore: unused_import import 'package:intl/intl.dart' as intl; + import 'app_localizations.dart'; // ignore_for_file: type=lint diff --git a/gui/lib/l10n/app_localizations_en.dart b/gui/lib/l10n/app_localizations_en.dart index 84c693e..5db4036 100644 --- a/gui/lib/l10n/app_localizations_en.dart +++ b/gui/lib/l10n/app_localizations_en.dart @@ -1,5 +1,6 @@ // ignore: unused_import import 'package:intl/intl.dart' as intl; + import 'app_localizations.dart'; // ignore_for_file: type=lint diff --git a/gui/lib/l10n/app_localizations_es.dart b/gui/lib/l10n/app_localizations_es.dart index aca36a3..ecd9158 100644 --- a/gui/lib/l10n/app_localizations_es.dart +++ b/gui/lib/l10n/app_localizations_es.dart @@ -1,5 +1,6 @@ // ignore: unused_import import 'package:intl/intl.dart' as intl; + import 'app_localizations.dart'; // ignore_for_file: type=lint diff --git a/gui/lib/l10n/app_localizations_fr.dart b/gui/lib/l10n/app_localizations_fr.dart index 833ea84..003e89b 100644 --- a/gui/lib/l10n/app_localizations_fr.dart +++ b/gui/lib/l10n/app_localizations_fr.dart @@ -1,5 +1,6 @@ // ignore: unused_import import 'package:intl/intl.dart' as intl; + import 'app_localizations.dart'; // ignore_for_file: type=lint diff --git a/gui/lib/l10n/app_localizations_it.dart b/gui/lib/l10n/app_localizations_it.dart index 9eefc61..c2ffdf3 100644 --- a/gui/lib/l10n/app_localizations_it.dart +++ b/gui/lib/l10n/app_localizations_it.dart @@ -1,5 +1,6 @@ // ignore: unused_import import 'package:intl/intl.dart' as intl; + import 'app_localizations.dart'; // ignore_for_file: type=lint diff --git a/gui/lib/l10n/app_localizations_ja.dart b/gui/lib/l10n/app_localizations_ja.dart index fa65621..8a23797 100644 --- a/gui/lib/l10n/app_localizations_ja.dart +++ b/gui/lib/l10n/app_localizations_ja.dart @@ -1,5 +1,6 @@ // ignore: unused_import import 'package:intl/intl.dart' as intl; + import 'app_localizations.dart'; // ignore_for_file: type=lint diff --git a/gui/lib/l10n/app_localizations_pt.dart b/gui/lib/l10n/app_localizations_pt.dart index fbd9495..2da0b95 100644 --- a/gui/lib/l10n/app_localizations_pt.dart +++ b/gui/lib/l10n/app_localizations_pt.dart @@ -1,5 +1,6 @@ // ignore: unused_import import 'package:intl/intl.dart' as intl; + import 'app_localizations.dart'; // ignore_for_file: type=lint diff --git a/gui/lib/l10n/app_localizations_ru.dart b/gui/lib/l10n/app_localizations_ru.dart index 1520c7b..d6e17a8 100644 --- a/gui/lib/l10n/app_localizations_ru.dart +++ b/gui/lib/l10n/app_localizations_ru.dart @@ -1,5 +1,6 @@ // ignore: unused_import import 'package:intl/intl.dart' as intl; + import 'app_localizations.dart'; // ignore_for_file: type=lint diff --git a/gui/lib/l10n/app_localizations_tr.dart b/gui/lib/l10n/app_localizations_tr.dart index 13124a3..c7fa616 100644 --- a/gui/lib/l10n/app_localizations_tr.dart +++ b/gui/lib/l10n/app_localizations_tr.dart @@ -1,5 +1,6 @@ // ignore: unused_import import 'package:intl/intl.dart' as intl; + import 'app_localizations.dart'; // ignore_for_file: type=lint diff --git a/gui/lib/l10n/app_localizations_zh.dart b/gui/lib/l10n/app_localizations_zh.dart index 8ce7bb8..ea87b43 100644 --- a/gui/lib/l10n/app_localizations_zh.dart +++ b/gui/lib/l10n/app_localizations_zh.dart @@ -1,5 +1,6 @@ // ignore: unused_import import 'package:intl/intl.dart' as intl; + import 'app_localizations.dart'; // ignore_for_file: type=lint diff --git a/gui/lib/models/conversion_job.dart b/gui/lib/models/conversion_job.dart index 333d77d..698d522 100644 --- a/gui/lib/models/conversion_job.dart +++ b/gui/lib/models/conversion_job.dart @@ -47,8 +47,8 @@ class ConversionJob { ratio = compression == 'Max' ? 0.60 : compression == 'Fast' - ? 0.80 - : 0.70; + ? 0.80 + : 0.70; break; case 'RVZ': ratio = compression == 'Max' ? 0.50 : 0.55; diff --git a/gui/lib/models/rom_file.dart b/gui/lib/models/rom_file.dart index 6d23f06..8ff4c8d 100644 --- a/gui/lib/models/rom_file.dart +++ b/gui/lib/models/rom_file.dart @@ -35,13 +35,15 @@ class RomFile { factory RomFile.fromJson(Map json) { return RomFile( filepath: json['filepath'] ?? '', - filename: json['filename'] ?? + filename: + json['filename'] ?? (json['filepath'] != null ? json['filepath'].split('/')['last'] : ''), format: json['format'] ?? 'UNKNOWN', platform: json['platform'] ?? 'ROM File', sizeBytes: json['size_bytes'] ?? 0, sizeStr: json['size_str'] ?? '0 B', - validTargets: (json['valid_targets'] as List?) + validTargets: + (json['valid_targets'] as List?) ?.map((e) => e.toString()) .toList() ?? [], diff --git a/gui/lib/patcher/aps_gba_builder.dart b/gui/lib/patcher/aps_gba_builder.dart index ee35cee..c46979f 100644 --- a/gui/lib/patcher/aps_gba_builder.dart +++ b/gui/lib/patcher/aps_gba_builder.dart @@ -45,12 +45,16 @@ class ApsGbaBuilder { final modChunk = Uint8List(_chunkSize); if (offset < origLen) { - final end = (offset + _chunkSize) < origLen ? (offset + _chunkSize) : origLen; + final end = (offset + _chunkSize) < origLen + ? (offset + _chunkSize) + : origLen; origChunk.setRange(0, end - offset, origBytes.sublist(offset, end)); } if (offset < modLen) { - final end = (offset + _chunkSize) < modLen ? (offset + _chunkSize) : modLen; + final end = (offset + _chunkSize) < modLen + ? (offset + _chunkSize) + : modLen; modChunk.setRange(0, end - offset, modBytes.sublist(offset, end)); } diff --git a/gui/lib/patcher/aps_n64_patcher.dart b/gui/lib/patcher/aps_n64_patcher.dart index 9718d68..b32fdb2 100644 --- a/gui/lib/patcher/aps_n64_patcher.dart +++ b/gui/lib/patcher/aps_n64_patcher.dart @@ -67,7 +67,8 @@ class ApsN64Patcher extends RomPatcher { if (patchType == _typeN64Patch) { validatesRomHeader = true; final endianness = await patch.readByte(); - final cardId = ((await patch.readByte() & 0xff) << 8) + + final cardId = + ((await patch.readByte() & 0xff) << 8) + (await patch.readByte() & 0xff); final country = await patch.readByte(); final crc = await patch.read(8); @@ -180,10 +181,12 @@ class ApsN64Patcher extends RomPatcher { // Check cartridge ID. await rom.setPosition(0x3c); if (endianness == 1) { - val = ((await rom.readByte() & 0xff) << 8) + + val = + ((await rom.readByte() & 0xff) << 8) + (await rom.readByte() & 0xff); } else { - val = (await rom.readByte() & 0xff) + + val = + (await rom.readByte() & 0xff) + ((await rom.readByte() & 0xff) << 8); } if (cartId != val) return false; diff --git a/gui/lib/patcher/aps_patcher.dart b/gui/lib/patcher/aps_patcher.dart index 7a7f7bd..e328518 100644 --- a/gui/lib/patcher/aps_patcher.dart +++ b/gui/lib/patcher/aps_patcher.dart @@ -25,15 +25,15 @@ class ApsPatcher extends RomPatcher { Future apply({bool ignoreChecksum = false}) async { final RomPatcher delegate = switch (await _detectType()) { _ApsType.n64 => ApsN64Patcher( - patchFile: patchFile, - romFile: romFile, - outputFile: outputFile, - ), + patchFile: patchFile, + romFile: romFile, + outputFile: outputFile, + ), _ApsType.gba => ApsGbaPatcher( - patchFile: patchFile, - romFile: romFile, - outputFile: outputFile, - ), + patchFile: patchFile, + romFile: romFile, + outputFile: outputFile, + ), _ApsType.unknown => throw PatchException("Not a valid APS patch."), }; return delegate.apply(ignoreChecksum: ignoreChecksum); diff --git a/gui/lib/patcher/dcp_builder.dart b/gui/lib/patcher/dcp_builder.dart index bd860df..137c171 100644 --- a/gui/lib/patcher/dcp_builder.dart +++ b/gui/lib/patcher/dcp_builder.dart @@ -3,7 +3,9 @@ // M5 Dev | GPL v3 import 'dart:io'; + import 'package:openrom_core/openrom_core.dart' as core; + import 'patcher.dart'; /// DCP (Universal Dreamcast Patcher) patch builder. diff --git a/gui/lib/patcher/dcp_patcher.dart b/gui/lib/patcher/dcp_patcher.dart index 5a662c6..c494248 100644 --- a/gui/lib/patcher/dcp_patcher.dart +++ b/gui/lib/patcher/dcp_patcher.dart @@ -40,9 +40,8 @@ class DcpPatcher { int filesPatched = 0; bool ipbinReplaced = false; try { - final lines = LineSplitter.split( - result.stdout, - ).where((l) => l.trim().isNotEmpty); + final lines = LineSplitter.split(result.stdout) + .where((l) => l.trim().isNotEmpty); for (final line in lines.toList().reversed) { final decoded = jsonDecode(line); if (decoded is Map) { diff --git a/gui/lib/patcher/ips_builder.dart b/gui/lib/patcher/ips_builder.dart index 9160776..85ae462 100644 --- a/gui/lib/patcher/ips_builder.dart +++ b/gui/lib/patcher/ips_builder.dart @@ -23,7 +23,9 @@ class IpsBuilder { final modLen = await modifiedFile.length(); if (origLen > _maxSize || modLen > _maxSize) { - throw PatchException('File too large for IPS format. Use xdelta instead.'); + throw PatchException( + 'File too large for IPS format. Use xdelta instead.', + ); } final origBytes = await originalFile.readAsBytes(); diff --git a/gui/lib/patcher/ups_patcher.dart b/gui/lib/patcher/ups_patcher.dart index 7a3c3d5..c492f51 100644 --- a/gui/lib/patcher/ups_patcher.dart +++ b/gui/lib/patcher/ups_patcher.dart @@ -103,8 +103,9 @@ class UpsPatcher extends RomPatcher { final copyLen = offset - outPos; if (copyLen > 0) { final available = romLen - romCursor; - final n = - copyLen <= available ? copyLen : (available > 0 ? available : 0); + final n = copyLen <= available + ? copyLen + : (available > 0 ? available : 0); if (n > 0) { out.setRange(outPos, outPos + n, rom, romCursor); romCursor += n; diff --git a/gui/lib/screens/dreamcast_screen.dart b/gui/lib/screens/dreamcast_screen.dart index 887e033..dda7e24 100644 --- a/gui/lib/screens/dreamcast_screen.dart +++ b/gui/lib/screens/dreamcast_screen.dart @@ -558,9 +558,9 @@ class _IpBinEditorTabState extends State<_IpBinEditorTab> { ]); if (res.exitCode == 0) { - final lines = LineSplitter.split( - res.stdout.toString(), - ).where((l) => l.trim().isNotEmpty).toList(); + final lines = LineSplitter.split(res.stdout.toString()) + .where((l) => l.trim().isNotEmpty) + .toList(); Map? jsonMap; for (final line in lines.reversed) { @@ -582,9 +582,10 @@ class _IpBinEditorTabState extends State<_IpBinEditorTab> { final regions = (fields['regions'] as List?)?.map((e) => e.toString()).toList() ?? - []; - final peripheralsStr = - (fields['peripherals'] ?? '').toString().trim(); + []; + final peripheralsStr = (fields['peripherals'] ?? '') + .toString() + .trim(); int peripFlags = 0; try { peripFlags = int.parse(peripheralsStr, radix: 16); @@ -592,12 +593,12 @@ class _IpBinEditorTabState extends State<_IpBinEditorTab> { setState(() { _titleController.text = fullTitle; - _productNumberController.text = - (fields['product_number'] ?? '').toString(); - _versionController.text = - (fields['product_version'] ?? '').toString(); - _releaseDateController.text = - (fields['release_date'] ?? '').toString(); + _productNumberController.text = (fields['product_number'] ?? '') + .toString(); + _versionController.text = (fields['product_version'] ?? '') + .toString(); + _releaseDateController.text = (fields['release_date'] ?? '') + .toString(); _japan = regions.contains('Japan'); _usa = regions.contains('USA'); @@ -1066,9 +1067,9 @@ class _GdiInfoTabState extends State<_GdiInfoTab> { final res = await CoreBridge.runCore(['--read-gdi', _gdiPath]); if (res.exitCode == 0) { - final lines = LineSplitter.split( - res.stdout.toString(), - ).where((l) => l.trim().isNotEmpty).toList(); + final lines = LineSplitter.split(res.stdout.toString()) + .where((l) => l.trim().isNotEmpty) + .toList(); List? jsonList; for (final line in lines.reversed) { @@ -1189,8 +1190,9 @@ class _GdiInfoTabState extends State<_GdiInfoTab> { vertical: 14, ), ), - onPressed: - (_gdiPath.isNotEmpty && !_isLoading) ? _loadGdi : null, + onPressed: (_gdiPath.isNotEmpty && !_isLoading) + ? _loadGdi + : null, child: _isLoading ? SizedBox( width: 18, diff --git a/gui/lib/screens/home_screen.dart b/gui/lib/screens/home_screen.dart index 2d175ce..2432933 100644 --- a/gui/lib/screens/home_screen.dart +++ b/gui/lib/screens/home_screen.dart @@ -58,8 +58,9 @@ class HomeScreenState extends State { final roms = await FileDetector.detectPaths(paths); for (final rom in roms) { if (!_jobs.any((j) => j.romFile.filepath == rom.filepath)) { - final defaultTarget = - rom.validTargets.isNotEmpty ? rom.validTargets.first : 'CHD'; + final defaultTarget = rom.validTargets.isNotEmpty + ? rom.validTargets.first + : 'CHD'; setState(() { _jobs.add( ConversionJob( diff --git a/gui/lib/screens/patcher_screen.dart b/gui/lib/screens/patcher_screen.dart index c4ee6f2..f09d704 100644 --- a/gui/lib/screens/patcher_screen.dart +++ b/gui/lib/screens/patcher_screen.dart @@ -50,14 +50,8 @@ class _PatcherScreenState extends State { labelColor: theme.accent, unselectedLabelColor: theme.textSecondary, tabs: const [ - Tab( - icon: Icon(Icons.healing_outlined), - text: 'Apply Patch', - ), - Tab( - icon: Icon(Icons.build_outlined), - text: 'Build Patch', - ), + Tab(icon: Icon(Icons.healing_outlined), text: 'Apply Patch'), + Tab(icon: Icon(Icons.build_outlined), text: 'Build Patch'), ], ), ), @@ -272,7 +266,8 @@ class _ApplyPatchTabState extends State<_ApplyPatchTab> { final l10n = AppLocalizations.of(context); final theme = widget.theme; final formatBadge = PatcherFactory.formatName(_patchPath); - final canApply = !_isPatching && + final canApply = + !_isPatching && _romPath.isNotEmpty && _patchPath.isNotEmpty && (_isSspPatch || _outputPath.isNotEmpty); @@ -643,8 +638,9 @@ class _ApplyPatchTabState extends State<_ApplyPatchTab> { child: Text( path.isEmpty ? '...' : path, style: TextStyle( - color: - path.isEmpty ? theme.textSecondary : theme.textPrimary, + color: path.isEmpty + ? theme.textSecondary + : theme.textPrimary, fontSize: 13, ), maxLines: 1, @@ -697,13 +693,23 @@ class _BuildPatchTabState extends State<_BuildPatchTab> { bool _isBuilding = false; PatchBuildReport? _report; - final List _formats = ['xdelta', 'IPS', 'IPS32', 'BPS', 'UPS', 'EBP', 'PPF3', 'APS (GBA)', 'APS (N64)', 'DCP']; + final List _formats = [ + 'xdelta', + 'IPS', + 'IPS32', + 'BPS', + 'UPS', + 'EBP', + 'PPF3', + 'APS (GBA)', + 'APS (N64)', + 'DCP', + ]; @override void initState() { super.initState(); - _patchBuilderService = - widget.patchBuilderService ?? PatchBuilderService(); + _patchBuilderService = widget.patchBuilderService ?? PatchBuilderService(); } String _getFormatExtension(String format) { @@ -836,7 +842,8 @@ class _BuildPatchTabState extends State<_BuildPatchTab> { Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); final theme = widget.theme; - final canBuild = !_isBuilding && + final canBuild = + !_isBuilding && _originalPath.isNotEmpty && _modifiedPath.isNotEmpty && _outputPath.isNotEmpty; @@ -910,10 +917,7 @@ class _BuildPatchTabState extends State<_BuildPatchTab> { dropdownColor: theme.surface, style: TextStyle(color: theme.textPrimary, fontSize: 14), items: _formats.map((f) { - return DropdownMenuItem( - value: f, - child: Text(f), - ); + return DropdownMenuItem(value: f, child: Text(f)); }).toList(), onChanged: (val) { if (val != null) { @@ -1080,8 +1084,9 @@ class _BuildPatchTabState extends State<_BuildPatchTab> { child: Text( path.isEmpty ? '...' : path, style: TextStyle( - color: - path.isEmpty ? theme.textSecondary : theme.textPrimary, + color: path.isEmpty + ? theme.textSecondary + : theme.textPrimary, fontSize: 13, ), maxLines: 1, diff --git a/gui/lib/screens/settings_screen.dart b/gui/lib/screens/settings_screen.dart index 92eece1..cfe442e 100644 --- a/gui/lib/screens/settings_screen.dart +++ b/gui/lib/screens/settings_screen.dart @@ -20,7 +20,8 @@ class SettingsScreen extends StatefulWidget { bool verify, String outputDir, bool sameFolder, - ) onSettingsChanged; + ) + onSettingsChanged; const SettingsScreen({ super.key, @@ -83,8 +84,8 @@ class _SettingsScreenState extends State { } void _pickOutputDirectory() async { - final String? selectedDirectory = - await FilePicker.platform.getDirectoryPath(); + final String? selectedDirectory = await FilePicker.platform + .getDirectoryPath(); if (selectedDirectory != null) { setState(() { _outputDestination = selectedDirectory; diff --git a/gui/lib/screens/theme_editor_screen.dart b/gui/lib/screens/theme_editor_screen.dart index b1d2d17..570e537 100644 --- a/gui/lib/screens/theme_editor_screen.dart +++ b/gui/lib/screens/theme_editor_screen.dart @@ -2,6 +2,7 @@ // M5 Dev | GPL v3 import 'package:flutter/material.dart'; + import '../models/theme_config.dart'; import '../services/theme_service.dart'; @@ -43,7 +44,10 @@ class _ThemeEditorScreenState extends State { context: context, builder: (ctx) => AlertDialog( backgroundColor: widget.theme.surface, - title: Text('Pick Color', style: TextStyle(color: widget.theme.textPrimary)), + title: Text( + 'Pick Color', + style: TextStyle(color: widget.theme.textPrimary), + ), content: SizedBox( width: 300, child: StatefulBuilder( @@ -56,12 +60,23 @@ class _ThemeEditorScreenState extends State { temp = HSVColor.fromColor(temp).withHue(v).toColor(); }); }, temp), - _buildSlider('Saturation', HSVColor.fromColor(temp).saturation, 0, 1, (v) { - setInner(() { - temp = HSVColor.fromColor(temp).withSaturation(v).toColor(); - }); - }, temp), - _buildSlider('Value', HSVColor.fromColor(temp).value, 0, 1, (v) { + _buildSlider( + 'Saturation', + HSVColor.fromColor(temp).saturation, + 0, + 1, + (v) { + setInner(() { + temp = HSVColor.fromColor(temp) + .withSaturation(v) + .toColor(); + }); + }, + temp, + ), + _buildSlider('Value', HSVColor.fromColor(temp).value, 0, 1, ( + v, + ) { setInner(() { temp = HSVColor.fromColor(temp).withValue(v).toColor(); }); @@ -82,10 +97,15 @@ class _ThemeEditorScreenState extends State { actions: [ TextButton( onPressed: () => Navigator.pop(ctx), - child: Text('Cancel', style: TextStyle(color: widget.theme.textSecondary)), + child: Text( + 'Cancel', + style: TextStyle(color: widget.theme.textSecondary), + ), ), ElevatedButton( - style: ElevatedButton.styleFrom(backgroundColor: widget.theme.accent), + style: ElevatedButton.styleFrom( + backgroundColor: widget.theme.accent, + ), onPressed: () { Navigator.pop(ctx); onPicked(temp); @@ -97,11 +117,23 @@ class _ThemeEditorScreenState extends State { ); } - Widget _buildSlider(String label, double value, double min, double max, - void Function(double) onChanged, Color previewColor) { + Widget _buildSlider( + String label, + double value, + double min, + double max, + void Function(double) onChanged, + Color previewColor, + ) { return Row( children: [ - SizedBox(width: 90, child: Text(label, style: TextStyle(color: widget.theme.textPrimary, fontSize: 12))), + SizedBox( + width: 90, + child: Text( + label, + style: TextStyle(color: widget.theme.textPrimary, fontSize: 12), + ), + ), Expanded( child: Slider( value: value, @@ -116,14 +148,21 @@ class _ThemeEditorScreenState extends State { } // One color row: label + swatch button - Widget _buildColorRow(String label, Color current, void Function(Color) onPicked) { + Widget _buildColorRow( + String label, + Color current, + void Function(Color) onPicked, + ) { return Padding( padding: const EdgeInsets.symmetric(vertical: 6), child: Row( children: [ SizedBox( width: 160, - child: Text(label, style: TextStyle(color: _edited.textPrimary, fontSize: 14)), + child: Text( + label, + style: TextStyle(color: _edited.textPrimary, fontSize: 14), + ), ), GestureDetector( onTap: () => _pickColor(current, (c) { @@ -142,7 +181,11 @@ class _ThemeEditorScreenState extends State { const SizedBox(width: 12), Text( '#${(current.toARGB32() & 0xFFFFFF).toRadixString(16).padLeft(6, '0').toUpperCase()}', - style: TextStyle(color: _edited.textSecondary, fontFamily: 'monospace', fontSize: 12), + style: TextStyle( + color: _edited.textSecondary, + fontFamily: 'monospace', + fontSize: 12, + ), ), ], ), @@ -194,8 +237,21 @@ class _ThemeEditorScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('game.iso', style: TextStyle(color: t.textPrimary, fontSize: 11, fontWeight: FontWeight.bold)), - Text('→ CHD · 700 MB', style: TextStyle(color: t.textSecondary, fontSize: 10)), + Text( + 'game.iso', + style: TextStyle( + color: t.textPrimary, + fontSize: 11, + fontWeight: FontWeight.bold, + ), + ), + Text( + '→ CHD · 700 MB', + style: TextStyle( + color: t.textSecondary, + fontSize: 10, + ), + ), ], ), ), @@ -209,18 +265,32 @@ class _ThemeEditorScreenState extends State { ), child: Text( '> chdman createcd ...\n✓ Done in 3.2s', - style: TextStyle(color: t.terminalText, fontSize: 9, fontFamily: 'monospace'), + style: TextStyle( + color: t.terminalText, + fontSize: 9, + fontFamily: 'monospace', + ), ), ), const SizedBox(height: 10), // Accent button Container( - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6), + padding: const EdgeInsets.symmetric( + horizontal: 14, + vertical: 6, + ), decoration: BoxDecoration( color: t.accent, borderRadius: BorderRadius.circular(t.borderRadius / 2), ), - child: Text('Convert', style: TextStyle(color: Colors.white, fontSize: 11, fontWeight: FontWeight.bold)), + child: Text( + 'Convert', + style: TextStyle( + color: Colors.white, + fontSize: 11, + fontWeight: FontWeight.bold, + ), + ), ), ], ), @@ -240,7 +310,14 @@ class _ThemeEditorScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('Theme Editor', style: TextStyle(color: t.textPrimary, fontSize: 24, fontWeight: FontWeight.bold)), + Text( + 'Theme Editor', + style: TextStyle( + color: t.textPrimary, + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), const SizedBox(height: 24), Row( crossAxisAlignment: CrossAxisAlignment.start, @@ -251,7 +328,14 @@ class _ThemeEditorScreenState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ // Theme name field - Text('Theme Name', style: TextStyle(color: t.accent, fontSize: 14, fontWeight: FontWeight.bold)), + Text( + 'Theme Name', + style: TextStyle( + color: t.accent, + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), const SizedBox(height: 8), TextField( controller: _nameController, @@ -261,11 +345,15 @@ class _ThemeEditorScreenState extends State { fillColor: t.surface, border: OutlineInputBorder( borderRadius: BorderRadius.circular(t.borderRadius), - borderSide: BorderSide(color: t.accent.withValues(alpha: 0.3)), + borderSide: BorderSide( + color: t.accent.withValues(alpha: 0.3), + ), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(t.borderRadius), - borderSide: BorderSide(color: t.accent.withValues(alpha: 0.3)), + borderSide: BorderSide( + color: t.accent.withValues(alpha: 0.3), + ), ), ), onChanged: (v) => setState(() { @@ -273,29 +361,83 @@ class _ThemeEditorScreenState extends State { }), ), const SizedBox(height: 20), - Text('Colors', style: TextStyle(color: t.accent, fontSize: 14, fontWeight: FontWeight.bold)), + Text( + 'Colors', + style: TextStyle( + color: t.accent, + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), const SizedBox(height: 8), - _buildColorRow('Background', _edited.background, (c) => _edited = _edited.copyWith(background: c)), - _buildColorRow('Surface', _edited.surface, (c) => _edited = _edited.copyWith(surface: c)), - _buildColorRow('Accent', _edited.accent, (c) => _edited = _edited.copyWith(accent: c)), - _buildColorRow('Text Primary', _edited.textPrimary, (c) => _edited = _edited.copyWith(textPrimary: c)), - _buildColorRow('Text Secondary', _edited.textSecondary, (c) => _edited = _edited.copyWith(textSecondary: c)), - _buildColorRow('Sidebar Background', _edited.sidebarBg, (c) => _edited = _edited.copyWith(sidebarBg: c)), - _buildColorRow('Card Background', _edited.cardBg, (c) => _edited = _edited.copyWith(cardBg: c)), - _buildColorRow('Terminal Background', _edited.terminalBg, (c) => _edited = _edited.copyWith(terminalBg: c)), - _buildColorRow('Terminal Text', _edited.terminalText, (c) => _edited = _edited.copyWith(terminalText: c)), + _buildColorRow( + 'Background', + _edited.background, + (c) => _edited = _edited.copyWith(background: c), + ), + _buildColorRow( + 'Surface', + _edited.surface, + (c) => _edited = _edited.copyWith(surface: c), + ), + _buildColorRow( + 'Accent', + _edited.accent, + (c) => _edited = _edited.copyWith(accent: c), + ), + _buildColorRow( + 'Text Primary', + _edited.textPrimary, + (c) => _edited = _edited.copyWith(textPrimary: c), + ), + _buildColorRow( + 'Text Secondary', + _edited.textSecondary, + (c) => _edited = _edited.copyWith(textSecondary: c), + ), + _buildColorRow( + 'Sidebar Background', + _edited.sidebarBg, + (c) => _edited = _edited.copyWith(sidebarBg: c), + ), + _buildColorRow( + 'Card Background', + _edited.cardBg, + (c) => _edited = _edited.copyWith(cardBg: c), + ), + _buildColorRow( + 'Terminal Background', + _edited.terminalBg, + (c) => _edited = _edited.copyWith(terminalBg: c), + ), + _buildColorRow( + 'Terminal Text', + _edited.terminalText, + (c) => _edited = _edited.copyWith(terminalText: c), + ), const SizedBox(height: 24), ElevatedButton.icon( icon: const Icon(Icons.save, color: Colors.white), - label: const Text('Save Theme', style: TextStyle(color: Colors.white)), - style: ElevatedButton.styleFrom(backgroundColor: t.accent), + label: const Text( + 'Save Theme', + style: TextStyle(color: Colors.white), + ), + style: ElevatedButton.styleFrom( + backgroundColor: t.accent, + ), onPressed: () async { - final saved = _edited.copyWith(name: _nameController.text.trim().isEmpty ? 'Custom Theme' : _nameController.text.trim()); + final saved = _edited.copyWith( + name: _nameController.text.trim().isEmpty + ? 'Custom Theme' + : _nameController.text.trim(), + ); await widget.themeService.saveCustomTheme(saved); await widget.themeService.setTheme(saved); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Theme "${saved.name}" saved!')), + SnackBar( + content: Text('Theme "${saved.name}" saved!'), + ), ); } }, @@ -308,7 +450,14 @@ class _ThemeEditorScreenState extends State { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('Live Preview', style: TextStyle(color: t.accent, fontSize: 14, fontWeight: FontWeight.bold)), + Text( + 'Live Preview', + style: TextStyle( + color: t.accent, + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), const SizedBox(height: 12), SizedBox(height: 280, child: _buildPreview()), ], diff --git a/gui/lib/screens/tools_screen.dart b/gui/lib/screens/tools_screen.dart index b1ad926..76f0821 100644 --- a/gui/lib/screens/tools_screen.dart +++ b/gui/lib/screens/tools_screen.dart @@ -112,7 +112,7 @@ class _ToolsScreenState extends State class _CompressorQueueItem { final String filepath; String - status; // "Queued", "Compressing", "Extracting", "Done", "Failed", "Skipped" + status; // "Queued", "Compressing", "Extracting", "Done", "Failed", "Skipped" double progress; String? error; @@ -394,8 +394,9 @@ class _CompressorTabState extends State<_CompressorTab> { backgroundColor: theme.surface, labelStyle: TextStyle( color: selected ? Colors.black : theme.textPrimary, - fontWeight: - selected ? FontWeight.bold : FontWeight.normal, + fontWeight: selected + ? FontWeight.bold + : FontWeight.normal, ), onSelected: (_) { setState(() { @@ -403,12 +404,14 @@ class _CompressorTabState extends State<_CompressorTab> { for (final item in _queue) { if (item.status == 'Skipped' || item.status == 'Queued') { - final isSkipped = (_format != 'Extract') && + final isSkipped = + (_format != 'Extract') && _isAlreadyCompressed(item.filepath); item.status = isSkipped ? 'Skipped' : 'Queued'; item.progress = isSkipped ? 100.0 : 0.0; - item.error = - isSkipped ? l10n.compressorSkipped : null; + item.error = isSkipped + ? l10n.compressorSkipped + : null; } } }); @@ -431,30 +434,32 @@ class _CompressorTabState extends State<_CompressorTab> { ), const SizedBox(height: 8), Row( - children: [ - MapEntry('Fast', l10n.compressorLevelFast), - MapEntry('Normal', l10n.compressorLevelNormal), - MapEntry('Ultra', l10n.compressorLevelUltra), - ].map((entry) { - final key = entry.key; - final label = entry.value; - final selected = _level == key; - return Padding( - padding: const EdgeInsets.only(right: 10), - child: ChoiceChip( - label: Text(label), - selected: selected, - selectedColor: theme.accent, - backgroundColor: theme.surface, - labelStyle: TextStyle( - color: selected ? Colors.black : theme.textPrimary, - fontWeight: - selected ? FontWeight.bold : FontWeight.normal, - ), - onSelected: (_) => setState(() => _level = key), - ), - ); - }).toList(), + children: + [ + MapEntry('Fast', l10n.compressorLevelFast), + MapEntry('Normal', l10n.compressorLevelNormal), + MapEntry('Ultra', l10n.compressorLevelUltra), + ].map((entry) { + final key = entry.key; + final label = entry.value; + final selected = _level == key; + return Padding( + padding: const EdgeInsets.only(right: 10), + child: ChoiceChip( + label: Text(label), + selected: selected, + selectedColor: theme.accent, + backgroundColor: theme.surface, + labelStyle: TextStyle( + color: selected ? Colors.black : theme.textPrimary, + fontWeight: selected + ? FontWeight.bold + : FontWeight.normal, + ), + onSelected: (_) => setState(() => _level = key), + ), + ); + }).toList(), ), const SizedBox(height: 16), ], @@ -1607,8 +1612,9 @@ class _BinMergerTabState extends State<_BinMergerTab> { width: double.infinity, height: 46, child: ElevatedButton( - onPressed: - (_cuePath.isNotEmpty && !_isProcessing) ? _mergeBins : null, + onPressed: (_cuePath.isNotEmpty && !_isProcessing) + ? _mergeBins + : null, style: ElevatedButton.styleFrom( backgroundColor: theme.accent, foregroundColor: Colors.black, @@ -2127,8 +2133,9 @@ class _CueEditorTabState extends State<_CueEditorTab> { decoration: InputDecoration( contentPadding: const EdgeInsets.all(12), border: InputBorder.none, - hintText: - _cuePath == null ? 'Open a .cue file to edit...' : null, + hintText: _cuePath == null + ? 'Open a .cue file to edit...' + : null, hintStyle: TextStyle(color: widget.theme.textSecondary), ), ), @@ -2366,8 +2373,8 @@ class _RomRenamerTabState extends State<_RomRenamerTab> { ElevatedButton.icon( onPressed: (_isRenaming || _scanResults.isEmpty || _romFolder == null) - ? null - : _rename, + ? null + : _rename, icon: _isRenaming ? const SizedBox( width: 14, diff --git a/gui/lib/services/core_bridge.dart b/gui/lib/services/core_bridge.dart index 697291b..c96486f 100644 --- a/gui/lib/services/core_bridge.dart +++ b/gui/lib/services/core_bridge.dart @@ -33,8 +33,9 @@ class CoreBridge { } final String exeDir = File(Platform.resolvedExecutable).parent.path; - final String binaryName = - Platform.isWindows ? 'openrom-core.exe' : 'openrom-core'; + final String binaryName = Platform.isWindows + ? 'openrom-core.exe' + : 'openrom-core'; final String sameDirBinary = '$exeDir${Platform.pathSeparator}$binaryName'; if (File(sameDirBinary).existsSync()) { @@ -87,9 +88,9 @@ class CoreBridge { final result = await Process.run(corePath, args); if (result.exitCode == 0) { - final lines = LineSplitter.split( - result.stdout.toString(), - ).where((l) => l.trim().isNotEmpty).toList(); + final lines = LineSplitter.split(result.stdout.toString()) + .where((l) => l.trim().isNotEmpty) + .toList(); if (lines.isNotEmpty) { final Map jsonMap = jsonDecode(lines.last); return RomFile.fromJson(jsonMap); @@ -106,7 +107,8 @@ class CoreBridge { ); } } on FileSystemException catch (e) { - final err = (e.osError?.errorCode == 13 || + final err = + (e.osError?.errorCode == 13 || e.message.toLowerCase().contains('permission')) ? OpenROMError.permissionDenied : OpenROMError.fileNotFound; @@ -181,39 +183,39 @@ class CoreBridge { .transform(utf8.decoder) .transform(const LineSplitter()) .listen((line) { - if (line.trim().isEmpty) return; - try { - final Map event = jsonDecode(line); - final type = event['type']; - if (type == 'progress') { - final double pct = (event['percent'] as num).toDouble(); - onProgress(pct); - } else if (type == 'log') { - final String msg = event['message'] ?? ''; - onLog(msg); - } else if (type == 'done') { - final bool success = event['success'] ?? false; - final String? err = event['error']; - onDone(success, err); - } else if (type == 'error') { - final String msg = event['message'] ?? 'Unknown error'; - onLog('[ERROR] $msg'); - stderrLog.writeln(msg); - } - } catch (_) { - onLog(line); - } - }); + if (line.trim().isEmpty) return; + try { + final Map event = jsonDecode(line); + final type = event['type']; + if (type == 'progress') { + final double pct = (event['percent'] as num).toDouble(); + onProgress(pct); + } else if (type == 'log') { + final String msg = event['message'] ?? ''; + onLog(msg); + } else if (type == 'done') { + final bool success = event['success'] ?? false; + final String? err = event['error']; + onDone(success, err); + } else if (type == 'error') { + final String msg = event['message'] ?? 'Unknown error'; + onLog('[ERROR] $msg'); + stderrLog.writeln(msg); + } + } catch (_) { + onLog(line); + } + }); process.stderr .transform(utf8.decoder) .transform(const LineSplitter()) .listen((line) { - if (line.trim().isNotEmpty) { - onLog('[STDERR] $line'); - stderrLog.writeln(line); - } - }); + if (line.trim().isNotEmpty) { + onLog('[STDERR] $line'); + stderrLog.writeln(line); + } + }); final exitCode = await process.exitCode; if (exitCode != 0) { @@ -236,7 +238,8 @@ class CoreBridge { } } } on FileSystemException catch (e) { - final err = (e.osError?.errorCode == 13 || + final err = + (e.osError?.errorCode == 13 || e.message.toLowerCase().contains('permission')) ? OpenROMError.permissionDenied : OpenROMError.fileNotFound; @@ -300,39 +303,39 @@ class CoreBridge { .transform(utf8.decoder) .transform(const LineSplitter()) .listen((line) { - if (line.trim().isEmpty) return; - try { - final Map event = jsonDecode(line); - final type = event['type']; - if (type == 'progress') { - final double pct = (event['percent'] as num).toDouble(); - onProgress?.call(pct); - } else if (type == 'log') { - final String msg = event['message'] ?? ''; - onLog?.call(msg); - } else if (type == 'done') { - final bool success = event['success'] ?? false; - final String? err = event['error']; - onDone?.call(success, err); - } else if (type == 'error') { - final String msg = event['message'] ?? 'Unknown error'; - onLog?.call('[ERROR] $msg'); - stderrLog.writeln(msg); - } - } catch (_) { - onLog?.call(line); - } - }); + if (line.trim().isEmpty) return; + try { + final Map event = jsonDecode(line); + final type = event['type']; + if (type == 'progress') { + final double pct = (event['percent'] as num).toDouble(); + onProgress?.call(pct); + } else if (type == 'log') { + final String msg = event['message'] ?? ''; + onLog?.call(msg); + } else if (type == 'done') { + final bool success = event['success'] ?? false; + final String? err = event['error']; + onDone?.call(success, err); + } else if (type == 'error') { + final String msg = event['message'] ?? 'Unknown error'; + onLog?.call('[ERROR] $msg'); + stderrLog.writeln(msg); + } + } catch (_) { + onLog?.call(line); + } + }); process.stderr .transform(utf8.decoder) .transform(const LineSplitter()) .listen((line) { - if (line.trim().isNotEmpty) { - onLog?.call('[STDERR] $line'); - stderrLog.writeln(line); - } - }); + if (line.trim().isNotEmpty) { + onLog?.call('[STDERR] $line'); + stderrLog.writeln(line); + } + }); final exitCode = await process.exitCode; if (exitCode != 0) { @@ -343,7 +346,8 @@ class CoreBridge { throw OpenROMException(OpenROMError.conversionFailed, details: details); } } on FileSystemException catch (e) { - final err = (e.osError?.errorCode == 13 || + final err = + (e.osError?.errorCode == 13 || e.message.toLowerCase().contains('permission')) ? OpenROMError.permissionDenied : OpenROMError.fileNotFound; @@ -397,39 +401,39 @@ class CoreBridge { .transform(utf8.decoder) .transform(const LineSplitter()) .listen((line) { - if (line.trim().isEmpty) return; - try { - final Map event = jsonDecode(line); - final type = event['type']; - if (type == 'progress') { - final double pct = (event['percent'] as num).toDouble(); - onProgress?.call(pct); - } else if (type == 'log') { - final String msg = event['message'] ?? ''; - onLog?.call(msg); - } else if (type == 'done') { - final bool success = event['success'] ?? false; - final String? err = event['error']; - onDone?.call(success, err); - } else if (type == 'error') { - final String msg = event['message'] ?? 'Unknown error'; - onLog?.call('[ERROR] $msg'); - stderrLog.writeln(msg); - } - } catch (_) { - onLog?.call(line); - } - }); + if (line.trim().isEmpty) return; + try { + final Map event = jsonDecode(line); + final type = event['type']; + if (type == 'progress') { + final double pct = (event['percent'] as num).toDouble(); + onProgress?.call(pct); + } else if (type == 'log') { + final String msg = event['message'] ?? ''; + onLog?.call(msg); + } else if (type == 'done') { + final bool success = event['success'] ?? false; + final String? err = event['error']; + onDone?.call(success, err); + } else if (type == 'error') { + final String msg = event['message'] ?? 'Unknown error'; + onLog?.call('[ERROR] $msg'); + stderrLog.writeln(msg); + } + } catch (_) { + onLog?.call(line); + } + }); process.stderr .transform(utf8.decoder) .transform(const LineSplitter()) .listen((line) { - if (line.trim().isNotEmpty) { - onLog?.call('[STDERR] $line'); - stderrLog.writeln(line); - } - }); + if (line.trim().isNotEmpty) { + onLog?.call('[STDERR] $line'); + stderrLog.writeln(line); + } + }); final exitCode = await process.exitCode; if (exitCode != 0) { @@ -440,7 +444,8 @@ class CoreBridge { throw OpenROMException(OpenROMError.conversionFailed, details: details); } } on FileSystemException catch (e) { - final err = (e.osError?.errorCode == 13 || + final err = + (e.osError?.errorCode == 13 || e.message.toLowerCase().contains('permission')) ? OpenROMError.permissionDenied : OpenROMError.fileNotFound; @@ -481,9 +486,9 @@ class CoreBridge { final result = await Process.run(corePath, args); if (result.exitCode == 0) { - final lines = LineSplitter.split( - result.stdout.toString(), - ).where((l) => l.trim().isNotEmpty).toList(); + final lines = LineSplitter.split(result.stdout.toString()) + .where((l) => l.trim().isNotEmpty) + .toList(); for (final line in lines.reversed) { try { final Map event = jsonDecode(line); @@ -499,7 +504,8 @@ class CoreBridge { details: err.isNotEmpty ? err : 'M3U generation failed', ); } on FileSystemException catch (e) { - final err = (e.osError?.errorCode == 13 || + final err = + (e.osError?.errorCode == 13 || e.message.toLowerCase().contains('permission')) ? OpenROMError.permissionDenied : OpenROMError.fileNotFound; @@ -524,9 +530,9 @@ class CoreBridge { } final result = await Process.run(corePath, args); if (result.exitCode == 0) { - final lines = LineSplitter.split( - result.stdout.toString(), - ).where((l) => l.trim().isNotEmpty).toList(); + final lines = LineSplitter.split(result.stdout.toString()) + .where((l) => l.trim().isNotEmpty) + .toList(); if (lines.isNotEmpty) { final Map jsonMap = jsonDecode(lines.last); final p = jsonMap['path'] as String?; diff --git a/gui/lib/services/patcher_service.dart b/gui/lib/services/patcher_service.dart index 40de517..819688d 100644 --- a/gui/lib/services/patcher_service.dart +++ b/gui/lib/services/patcher_service.dart @@ -61,8 +61,7 @@ class PatcherService { if (saturnPatcher == null || saturnPatcher.isEmpty) { throw OpenROMException( OpenROMError.toolFailed, - details: - 'saturn-patcher binary not found. Please check your OpenROM installation.', + details: 'saturn-patcher binary not found. Please check your OpenROM installation.', ); } diff --git a/gui/lib/services/theme_service.dart b/gui/lib/services/theme_service.dart index 14a3bc2..ad8db06 100644 --- a/gui/lib/services/theme_service.dart +++ b/gui/lib/services/theme_service.dart @@ -60,7 +60,11 @@ class ThemeService extends ChangeNotifier { final customFiles = Directory(dir.path) .listSync() .whereType() - .where((f) => f.path.endsWith('.json') && f.uri.pathSegments.last.startsWith('custom_')); + .where( + (f) => + f.path.endsWith('.json') && + f.uri.pathSegments.last.startsWith('custom_'), + ); for (final file in customFiles) { try { final content = await file.readAsString(); @@ -90,9 +94,10 @@ class ThemeService extends ChangeNotifier { Future saveCustomTheme(ThemeConfig theme) async { try { final dir = await getApplicationSupportDirectory(); - final safeName = theme.name - .toLowerCase() - .replaceAll(RegExp(r'[^a-z0-9_]'), '_'); + final safeName = theme.name.toLowerCase().replaceAll( + RegExp(r'[^a-z0-9_]'), + '_', + ); final file = File('${dir.path}/custom_$safeName.json'); await file.writeAsString(jsonEncode(theme.toJson())); diff --git a/gui/lib/services/tools_service.dart b/gui/lib/services/tools_service.dart index 67187b3..54b1737 100644 --- a/gui/lib/services/tools_service.dart +++ b/gui/lib/services/tools_service.dart @@ -81,9 +81,9 @@ class ToolsService { final result = await Process.run(corePath, args); if (result.exitCode == 0) { - final lines = LineSplitter.split( - result.stdout.toString(), - ).where((l) => l.trim().isNotEmpty).toList(); + final lines = LineSplitter.split(result.stdout.toString()) + .where((l) => l.trim().isNotEmpty) + .toList(); for (final line in lines.reversed) { try { final Map event = jsonDecode(line); @@ -102,7 +102,8 @@ class ToolsService { details: err.isNotEmpty ? err : 'CUE generation failed', ); } on FileSystemException catch (e) { - final err = (e.osError?.errorCode == 13 || + final err = + (e.osError?.errorCode == 13 || e.message.toLowerCase().contains('permission')) ? OpenROMError.permissionDenied : OpenROMError.fileNotFound; @@ -152,33 +153,33 @@ class ToolsService { .transform(utf8.decoder) .transform(const LineSplitter()) .listen((line) { - if (line.trim().isEmpty) return; - try { - final Map event = jsonDecode(line); - final type = event['type']; - if (type == 'progress') { - final double pct = (event['percent'] as num).toDouble(); - onProgress?.call(pct); - } else if (type == 'done' && event['success'] == true) { - mergeResult = BinMergeResult( - mergedBinPath: event['merged_bin'] as String, - mergedCuePath: event['merged_cue'] as String, - tracksCount: (event['tracks_count'] as num?)?.toInt() ?? 0, - ); - } else if (type == 'error') { - stderrLog.writeln(event['message'] ?? ''); - } - } catch (_) {} - }); + if (line.trim().isEmpty) return; + try { + final Map event = jsonDecode(line); + final type = event['type']; + if (type == 'progress') { + final double pct = (event['percent'] as num).toDouble(); + onProgress?.call(pct); + } else if (type == 'done' && event['success'] == true) { + mergeResult = BinMergeResult( + mergedBinPath: event['merged_bin'] as String, + mergedCuePath: event['merged_cue'] as String, + tracksCount: (event['tracks_count'] as num?)?.toInt() ?? 0, + ); + } else if (type == 'error') { + stderrLog.writeln(event['message'] ?? ''); + } + } catch (_) {} + }); process.stderr .transform(utf8.decoder) .transform(const LineSplitter()) .listen((line) { - if (line.trim().isNotEmpty) { - stderrLog.writeln(line); - } - }); + if (line.trim().isNotEmpty) { + stderrLog.writeln(line); + } + }); final exitCode = await process.exitCode; if (exitCode == 0 && mergeResult != null) { @@ -190,7 +191,8 @@ class ToolsService { : 'Process exited with code $exitCode'; throw OpenROMException(OpenROMError.conversionFailed, details: details); } on FileSystemException catch (e) { - final err = (e.osError?.errorCode == 13 || + final err = + (e.osError?.errorCode == 13 || e.message.toLowerCase().contains('permission')) ? OpenROMError.permissionDenied : OpenROMError.fileNotFound; @@ -226,9 +228,9 @@ class ToolsService { final result = await Process.run(corePath, args); if (result.exitCode == 0) { - final lines = LineSplitter.split( - result.stdout.toString(), - ).where((l) => l.trim().isNotEmpty).toList(); + final lines = LineSplitter.split(result.stdout.toString()) + .where((l) => l.trim().isNotEmpty) + .toList(); for (final line in lines.reversed) { try { final Map event = jsonDecode(line); @@ -240,7 +242,8 @@ class ToolsService { } return null; } on FileSystemException catch (e) { - final err = (e.osError?.errorCode == 13 || + final err = + (e.osError?.errorCode == 13 || e.message.toLowerCase().contains('permission')) ? OpenROMError.permissionDenied : OpenROMError.fileNotFound; @@ -288,9 +291,9 @@ class ToolsService { final result = await Process.run(corePath, args); if (result.exitCode == 0) { - final lines = LineSplitter.split( - result.stdout.toString(), - ).where((l) => l.trim().isNotEmpty).toList(); + final lines = LineSplitter.split(result.stdout.toString()) + .where((l) => l.trim().isNotEmpty) + .toList(); for (final line in lines.reversed) { try { final Map event = jsonDecode(line); @@ -306,7 +309,8 @@ class ToolsService { details: err.isNotEmpty ? err : 'Header removal failed', ); } on FileSystemException catch (e) { - final err = (e.osError?.errorCode == 13 || + final err = + (e.osError?.errorCode == 13 || e.message.toLowerCase().contains('permission')) ? OpenROMError.permissionDenied : OpenROMError.fileNotFound; @@ -333,9 +337,9 @@ class ToolsService { final result = await Process.run(corePath, args); if (result.exitCode == 0) { - final lines = LineSplitter.split( - result.stdout.toString(), - ).where((l) => l.trim().isNotEmpty).toList(); + final lines = LineSplitter.split(result.stdout.toString()) + .where((l) => l.trim().isNotEmpty) + .toList(); for (final line in lines.reversed) { try { final Map event = jsonDecode(line); @@ -375,9 +379,9 @@ class ToolsService { final result = await Process.run(corePath, args); if (result.exitCode == 0) { - final lines = LineSplitter.split( - result.stdout.toString(), - ).where((l) => l.trim().isNotEmpty).toList(); + final lines = LineSplitter.split(result.stdout.toString()) + .where((l) => l.trim().isNotEmpty) + .toList(); for (final line in lines.reversed) { try { final Map event = jsonDecode(line); @@ -422,30 +426,30 @@ class ToolsService { .transform(utf8.decoder) .transform(const LineSplitter()) .listen((line) { - if (line.trim().isEmpty) return; - try { - final Map event = jsonDecode(line); - if (event['type'] == 'result') { - results.add(Map.from(event)); - } else if (event['type'] == 'progress' && onProgress != null) { - onProgress( - event['file'] as String? ?? '', - (event['percent'] as num?)?.toDouble() ?? 0, - ); - } else if (event['type'] == 'error') { - stderrLog.writeln(event['message'] ?? ''); - } - } catch (_) {} - }); + if (line.trim().isEmpty) return; + try { + final Map event = jsonDecode(line); + if (event['type'] == 'result') { + results.add(Map.from(event)); + } else if (event['type'] == 'progress' && onProgress != null) { + onProgress( + event['file'] as String? ?? '', + (event['percent'] as num?)?.toDouble() ?? 0, + ); + } else if (event['type'] == 'error') { + stderrLog.writeln(event['message'] ?? ''); + } + } catch (_) {} + }); process.stderr .transform(utf8.decoder) .transform(const LineSplitter()) .listen((line) { - if (line.trim().isNotEmpty) { - stderrLog.writeln(line); - } - }); + if (line.trim().isNotEmpty) { + stderrLog.writeln(line); + } + }); final exitCode = await process.exitCode; if (exitCode != 0 && results.isEmpty) { @@ -483,25 +487,25 @@ class ToolsService { .transform(utf8.decoder) .transform(const LineSplitter()) .listen((line) { - if (line.trim().isEmpty) return; - try { - final Map event = jsonDecode(line); - if (event['type'] == 'rename') { - results.add(Map.from(event)); - } else if (event['type'] == 'error') { - stderrLog.writeln(event['message'] ?? ''); - } - } catch (_) {} - }); + if (line.trim().isEmpty) return; + try { + final Map event = jsonDecode(line); + if (event['type'] == 'rename') { + results.add(Map.from(event)); + } else if (event['type'] == 'error') { + stderrLog.writeln(event['message'] ?? ''); + } + } catch (_) {} + }); process.stderr .transform(utf8.decoder) .transform(const LineSplitter()) .listen((line) { - if (line.trim().isNotEmpty) { - stderrLog.writeln(line); - } - }); + if (line.trim().isNotEmpty) { + stderrLog.writeln(line); + } + }); final exitCode = await process.exitCode; if (exitCode != 0 && results.isEmpty) { diff --git a/gui/lib/widgets/rom_card.dart b/gui/lib/widgets/rom_card.dart index 83fc02c..6ff8143 100644 --- a/gui/lib/widgets/rom_card.dart +++ b/gui/lib/widgets/rom_card.dart @@ -86,8 +86,8 @@ class _RomCardState extends State { final err = widget.job.error ?? OpenROMError.conversionFailed; final details = (widget.job.errorMessage != null && widget.job.errorMessage!.isNotEmpty) - ? widget.job.errorMessage - : widget.job.logs.join('\n'); + ? widget.job.errorMessage + : widget.job.logs.join('\n'); showOpenROMError(context, err, details: details); } @@ -184,8 +184,8 @@ class _RomCardState extends State { color: isFailed ? Colors.redAccent : (widget.job.status == JobStatus.done - ? Colors.greenAccent - : widget.theme.textSecondary), + ? Colors.greenAccent + : widget.theme.textSecondary), fontSize: 13, ), ), diff --git a/gui/lib/widgets/sidebar.dart b/gui/lib/widgets/sidebar.dart index e7c28dc..ddea03d 100644 --- a/gui/lib/widgets/sidebar.dart +++ b/gui/lib/widgets/sidebar.dart @@ -63,8 +63,9 @@ class Sidebar extends StatelessWidget { decoration: BoxDecoration( border: Border( left: BorderSide( - color: - isSelected ? theme.accent : Colors.transparent, + color: isSelected + ? theme.accent + : Colors.transparent, width: 3, ), ), diff --git a/gui/lib/widgets/top_bar.dart b/gui/lib/widgets/top_bar.dart index a938b72..75e1a11 100644 --- a/gui/lib/widgets/top_bar.dart +++ b/gui/lib/widgets/top_bar.dart @@ -70,8 +70,9 @@ class TopBar extends StatelessWidget { ), const SizedBox(width: 12), ElevatedButton( - onPressed: - (fileCount > 0 && !isConverting) ? onConvertPressed : null, + onPressed: (fileCount > 0 && !isConverting) + ? onConvertPressed + : null, style: ElevatedButton.styleFrom( backgroundColor: theme.accent, foregroundColor: Colors.white, diff --git a/gui/test/patcher_builder_test.dart b/gui/test/patcher_builder_test.dart index 7214064..c9f4f76 100644 --- a/gui/test/patcher_builder_test.dart +++ b/gui/test/patcher_builder_test.dart @@ -70,7 +70,10 @@ void main() { // Verify header magic "PATCH" and footer "EOF" final patchBytes = await patchFile.readAsBytes(); - expect(patchBytes.sublist(0, 5), equals([0x50, 0x41, 0x54, 0x43, 0x48])); // PATCH + expect( + patchBytes.sublist(0, 5), + equals([0x50, 0x41, 0x54, 0x43, 0x48]), + ); // PATCH expect( patchBytes.sublist(patchBytes.length - 3), equals([0x45, 0x4F, 0x46]), // EOF @@ -127,7 +130,9 @@ void main() { final patchFile = File('${tempDir.path}/patch.ips32'); final outFile = File('${tempDir.path}/out.bin'); - final origData = Uint8List.fromList(List.generate(1500, (i) => (i * 7) % 256)); + final origData = Uint8List.fromList( + List.generate(1500, (i) => (i * 7) % 256), + ); final modData = Uint8List.fromList(List.from(origData)); // Modify 15 bytes at offset 300 for (int i = 0; i < 15; i++) { @@ -148,7 +153,10 @@ void main() { // Verify header magic "IPS32" and footer "EEOF" final patchBytes = await patchFile.readAsBytes(); - expect(patchBytes.sublist(0, 5), equals([0x49, 0x50, 0x53, 0x33, 0x32])); // IPS32 + expect( + patchBytes.sublist(0, 5), + equals([0x49, 0x50, 0x53, 0x33, 0x32]), + ); // IPS32 expect( patchBytes.sublist(patchBytes.length - 4), equals([0x45, 0x45, 0x4F, 0x46]), // EEOF @@ -173,7 +181,9 @@ void main() { final patchFile = File('${tempDir.path}/patch.ebp'); final outFile = File('${tempDir.path}/out.bin'); - final origData = Uint8List.fromList(List.generate(800, (i) => (i * 11) % 256)); + final origData = Uint8List.fromList( + List.generate(800, (i) => (i * 11) % 256), + ); final modData = Uint8List.fromList(List.from(origData)); // Modify 8 bytes at offset 200 for (int i = 0; i < 8; i++) { @@ -214,7 +224,9 @@ void main() { final forwardOutFile = File('${tempDir.path}/forward_out.bin'); final reverseOutFile = File('${tempDir.path}/reverse_out.bin'); - final origData = Uint8List.fromList(List.generate(3000, (i) => (i * 13) % 256)); + final origData = Uint8List.fromList( + List.generate(3000, (i) => (i * 13) % 256), + ); final modData = Uint8List.fromList(List.from(origData)); // Modify 25 bytes at offset 1200 for (int i = 0; i < 25; i++) { @@ -266,7 +278,9 @@ void main() { final patchFile = File('${tempDir.path}/patch.bps'); final outFile = File('${tempDir.path}/out.bin'); - final origData = Uint8List.fromList(List.generate(2000, (i) => (i * 3) % 256)); + final origData = Uint8List.fromList( + List.generate(2000, (i) => (i * 3) % 256), + ); final modData = Uint8List.fromList(List.from(origData)); // Modify 20 bytes at offset 500 for (int i = 0; i < 20; i++) { @@ -390,7 +404,9 @@ void main() { final patchFile = File('${tempDir.path}/patch_gba.aps'); final outFile = File('${tempDir.path}/out_aps_gba.bin'); - final origData = Uint8List.fromList(List.generate(70000, (i) => (i * 3) % 256)); + final origData = Uint8List.fromList( + List.generate(70000, (i) => (i * 3) % 256), + ); final modData = Uint8List.fromList(List.from(origData)); for (int i = 0; i < 30; i++) { modData[1000 + i] = (modData[1000 + i] + 20) % 256; @@ -428,7 +444,9 @@ void main() { final patchFile = File('${tempDir.path}/patch_n64.aps'); final outFile = File('${tempDir.path}/out_aps_n64.z64'); - final origData = Uint8List.fromList(List.generate(1024, (i) => (i * 17) % 256)); + final origData = Uint8List.fromList( + List.generate(1024, (i) => (i * 17) % 256), + ); // Set mock N64 header origData[0] = 0x80; // Big endian origData[0x3c] = 0x4e; // Cart ID @@ -469,20 +487,21 @@ void main() { test('DCP Round-trip test', () async { final origDir = Directory(p.join(tempDir.path, 'dcp_orig'))..createSync(); final modDir = Directory(p.join(tempDir.path, 'dcp_mod'))..createSync(); - final outputDir = Directory(p.join(tempDir.path, 'dcp_out'))..createSync(); + final outputDir = Directory(p.join(tempDir.path, 'dcp_out')) + ..createSync(); final dcpFile = File(p.join(tempDir.path, 'patch.dcp')); File(p.join(origDir.path, 'bootsector', 'IP.BIN')) ..createSync(recursive: true) ..writeAsStringSync('ORIG_IPBIN_123'); File(p.join(origDir.path, '1ST_READ.BIN')) - .writeAsStringSync('ORIG_1ST_READ'); + .writeAsStringSync('ORIG_1ST_READ'); File(p.join(modDir.path, 'bootsector', 'IP.BIN')) ..createSync(recursive: true) ..writeAsStringSync('MOD_IPBIN_456'); File(p.join(modDir.path, '1ST_READ.BIN')) - .writeAsStringSync('MOD_1ST_READ'); + .writeAsStringSync('MOD_1ST_READ'); final builder = DcpBuilder( originalDir: origDir, diff --git a/gui/test/theme_config_test.dart b/gui/test/theme_config_test.dart index cfc0500..7d0788b 100644 --- a/gui/test/theme_config_test.dart +++ b/gui/test/theme_config_test.dart @@ -8,7 +8,10 @@ void main() { final theme = ThemeConfig.defaultTheme(); expect(theme.name, 'Gaming Dashboard'); - final updated = theme.copyWith(name: 'New Custom Theme', accent: const Color(0xff00ff00)); + final updated = theme.copyWith( + name: 'New Custom Theme', + accent: const Color(0xff00ff00), + ); expect(updated.name, 'New Custom Theme'); expect(updated.accent, const Color(0xff00ff00)); expect(updated.background, theme.background); diff --git a/gui/test/tools_test.dart b/gui/test/tools_test.dart index 61082b2..9ac7f1c 100644 --- a/gui/test/tools_test.dart +++ b/gui/test/tools_test.dart @@ -19,7 +19,9 @@ void main() { ); } - testWidgets('Renders Tools tabs and compressor UI', (WidgetTester tester) async { + testWidgets('Renders Tools tabs and compressor UI', ( + WidgetTester tester, + ) async { await tester.pumpWidget(createToolsScreen()); await tester.pumpAndSettle();