Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions core/bin/openrom.dart
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ ArgParser buildParser() {
help: 'output format: CHD, CSO, ECM, ISO, BIN, BIN/CUE, XISO, ZIP, 7Z',
)
..addOption('force-platform', help: 'override automatic platform detection')
..addOption(
'media',
help: 'media type override for CHD creation: cd or dvd',
)
..addFlag(
'list-dats',
negatable: false,
Expand Down Expand Up @@ -760,6 +764,7 @@ Future<int> main(List<String> args) async {

final targetFmt = results['format'] as String?;
final forcePlat = results['force-platform'] as String?;
final mediaVal = results['media'] as String?;
final compLevel = results['compression'] as String;
final verifyVal = results['verify'] == true;
final outputDir = results['output'] as String?;
Expand All @@ -774,6 +779,7 @@ Future<int> main(List<String> args) async {
compression: compLevel,
verify: verifyVal,
forcePlatform: forcePlat,
media: mediaVal,
);
jobs.add(job);
} else if (folder != null) {
Expand All @@ -786,6 +792,7 @@ Future<int> main(List<String> args) async {
compression: compLevel,
verify: verifyVal,
forcePlatform: forcePlat,
media: mediaVal,
);
jobs.add(job);
}
Expand Down
28 changes: 21 additions & 7 deletions core/lib/src/converter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class ConversionJob {
final String compression;
final bool verify;
final String? forcePlatform;
final String? media;
String status;
double progress;
final List<String> logLines;
Expand All @@ -54,6 +55,7 @@ class ConversionJob {
this.compression = 'Normal',
this.verify = false,
this.forcePlatform,
this.media,
this.status = 'Queued',
this.progress = 0.0,
List<String>? logLines,
Expand Down Expand Up @@ -207,16 +209,28 @@ class Converter {
subCmd = 'createcd';
} else if (fmt == 'ISO' || fmt == 'IMG') {
final platform = info['platform'] as String? ?? 'UNKNOWN';
if (platform == 'UNKNOWN' ||
final mediaOverride = job.media?.trim().toLowerCase();

if (mediaOverride == 'cd') {
subCmd = 'createcd';
} else if (mediaOverride == 'dvd') {
subCmd = 'createdvd';
} else if (platform.toUpperCase() == 'CD') {
subCmd = 'createcd';
} else if (platform.toUpperCase() == 'DVD') {
subCmd = 'createdvd';
} else if (cdPlatformKeywords.any((kw) => platform.contains(kw))) {
subCmd = 'createcd';
} else if (platform == 'UNKNOWN' ||
platform == 'Unknown Disc' ||
platform.isEmpty) {
_log(
'[WARN] Platform is UNKNOWN for . Defaulting safely to createdvd.');
subCmd = 'createdvd';
final err =
'Error: Platform could not be detected. Please specify --media cd or --media dvd.';
_log('[ERROR] $err');
job.error = err;
return false;
} else {
subCmd = cdPlatformKeywords.any((kw) => platform.contains(kw))
? 'createcd'
: 'createdvd';
subCmd = 'createdvd';
}
} else {
subCmd = 'createcd';
Expand Down
37 changes: 34 additions & 3 deletions core/lib/src/rom_renamer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -220,15 +220,21 @@ Future<Map<String, Map<String, dynamic>>> loadDatIndex(String datPath) async {
}

if (crc.isNotEmpty) {
index[crc] = {
final item = {
'name': currentGameName,
'description':
currentDesc.isNotEmpty ? currentDesc : currentGameName,
'rom_name': romName,
'size': size,
'md5': md5,
'sha1': sha1,
'crc': crc,
};
final compositeKey = '${crc}_$size';
index[compositeKey] = item;
if (!index.containsKey(crc)) {
index[crc] = item;
}
}
}
} else if (event is XmlTextEvent) {
Expand Down Expand Up @@ -272,6 +278,29 @@ class RomScanResult {
}
}

Map<String, dynamic>? _findInIndex(
Map<String, Map<String, dynamic>> indexMap,
String crc32,
int fileSize,
) {
final compositeKey = '${crc32}_$fileSize';
if (indexMap.containsKey(compositeKey)) {
return indexMap[compositeKey];
}
final zeroKey = '${crc32}_0';
if (indexMap.containsKey(zeroKey)) {
return indexMap[zeroKey];
}
if (indexMap.containsKey(crc32)) {
final item = indexMap[crc32]!;
final expectedSize = item['size'] as int? ?? 0;
if (expectedSize == 0 || expectedSize == fileSize) {
return item;
}
}
return null;
}

Future<List<RomScanResult>> scanFolder(
String folder,
List<String> datPaths, {
Expand Down Expand Up @@ -373,8 +402,10 @@ Future<List<RomScanResult>> scanFolder(
},
);

if (globalIndex.containsKey(result.crc32)) {
final item = globalIndex[result.crc32]!;
final fileSize = File(fpath).lengthSync();
final item = _findInIndex(globalIndex, result.crc32, fileSize);

if (item != null) {
result.matched = true;
result.canonicalName = item['name'] as String;
result.romName = item['rom_name'] as String;
Expand Down
62 changes: 62 additions & 0 deletions core/test/detector_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -184,4 +184,66 @@ void main() {
final xgdtoolPath = getToolPath('xgdtool');
expect(xgdtoolPath, isNotEmpty);
});
test('unknown_iso_conversion_without_media_fails_safely', () {
final tmpDir = Directory.systemTemp.createTempSync('unknown_iso_conv_');
try {
final isoPath = p.join(tmpDir.path, 'unidentified.iso');
File(isoPath).writeAsBytesSync(List<int>.filled(2048, 0));

final job = ConversionJob(
filepath: isoPath,
outputDir: tmpDir.path,
targetFormat: 'CHD',
);

final converter = Converter();
final success = converter.convert(job);

expect(success, isFalse);
expect(job.status, equals('Failed'));
expect(
job.error,
equals(
'Error: Platform could not be detected. Please specify --media cd or --media dvd.',
),
);
} finally {
tmpDir.deleteSync(recursive: true);
}
});

test('unknown_iso_conversion_with_media_override_routes_correctly', () {
final tmpDir = Directory.systemTemp.createTempSync('unknown_iso_override_');
try {
final isoPath = p.join(tmpDir.path, 'unidentified.iso');
File(isoPath).writeAsBytesSync(List<int>.filled(2048, 0));

saveConfig({'chdman': p.join(tmpDir.path, 'missing_chdman')});

final jobCd = ConversionJob(
filepath: isoPath,
outputDir: tmpDir.path,
targetFormat: 'CHD',
media: 'cd',
);

final converter = Converter();
converter.convert(jobCd);
expect(jobCd.error, contains('Tool not found'));
expect(jobCd.error, isNot(contains('Platform could not be detected')));

final jobDvd = ConversionJob(
filepath: isoPath,
outputDir: tmpDir.path,
targetFormat: 'CHD',
media: 'dvd',
);
converter.convert(jobDvd);
expect(jobDvd.error, contains('Tool not found'));
expect(jobDvd.error, isNot(contains('Platform could not be detected')));
} finally {
saveConfig({});
tmpDir.deleteSync(recursive: true);
}
});
}
45 changes: 44 additions & 1 deletion core/test/rom_renamer_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ void main() {
</header>
<game name="Super Mario Land">
<description>Super Mario Land</description>
<rom name="Super Mario Land.gb" size="65536" crc="cbf43926"/>
<rom name="Super Mario Land.gb" size="9" crc="cbf43926"/>
</game>
</datafile>''');

Expand Down Expand Up @@ -99,4 +99,47 @@ void main() {
tmpDir.deleteSync(recursive: true);
}
});
test('composite_dat_lookup_prevents_crc32_collisions', () async {
final tmpDir = Directory.systemTemp.createTempSync('collision_test_');
try {
final datPath = p.join(tmpDir.path, 'collision.dat');
final xmlContent = '''<?xml version="1.0"?>
<datafile>
<header>
<name>Collision DAT</name>
<url>http://www.no-intro.org</url>
</header>
<game name="Game A">
<description>Game A</description>
<rom name="Game A.gb" size="9" crc="cbf43926"/>
</game>
<game name="Game B Collision">
<description>Game B Collision</description>
<rom name="Game B.gb" size="100" crc="cbf43926"/>
</game>
</datafile>''';
File(datPath).writeAsStringSync(xmlContent);

final romDir = p.join(tmpDir.path, 'roms');
Directory(romDir).createSync();
final romPath = p.join(romDir, 'sample.gb');
File(romPath).writeAsStringSync('123456789');

final results = await scanFolder(romDir, [datPath]);
expect(results.length, equals(1));
expect(results.first.matched, isTrue);
expect(results.first.canonicalName, equals('Game A'));
expect(results.first.suggestedFilename, equals('Game A.gb'));

final mismatchPath = p.join(romDir, 'mismatch.gb');
File(mismatchPath).writeAsBytesSync(List<int>.filled(50, 0x31));

final mismatchResults = await scanFolder(romDir, [datPath]);
final mismatchRes =
mismatchResults.firstWhere((r) => r.filename == 'mismatch.gb');
expect(mismatchRes.matched, isFalse);
} finally {
tmpDir.deleteSync(recursive: true);
}
});
}
21 changes: 21 additions & 0 deletions gui/lib/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,27 @@ class _OpenROMAppState extends State<OpenROMApp> {
compression: _compression,
verify: _verify,
outputDir: _outputDir,
onPromptMedia: (job) async {
return await showDialog<String>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Unrecognized Media Type'),
content: Text(
'Unrecognized Media Type: Is this game a CD or DVD?\n\nFile: ${job.romFile.filename}',
),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop('cd'),
child: const Text('CD'),
),
ElevatedButton(
onPressed: () => Navigator.of(ctx).pop('dvd'),
child: const Text('DVD'),
),
],
),
);
},
);
}

Expand Down
22 changes: 22 additions & 0 deletions gui/lib/controllers/home_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ class HomeController extends ChangeNotifier {
String compression = 'Normal',
bool verify = false,
String outputDir = '',
Future<String?> Function(ConversionJob job)? onPromptMedia,
void Function(OpenROMException e)? onError,
}) async {
if (_jobs.isEmpty || _isConverting) return;
Expand All @@ -100,6 +101,27 @@ class HomeController extends ChangeNotifier {
job.compression,
);

if (job.targetFormat.toUpperCase() == 'CHD' &&
(job.romFile.platform == 'UNKNOWN' ||
job.romFile.platform == 'Unknown Disc' ||
job.romFile.platform == 'ROM File') &&
(job.media == null || job.media!.isEmpty)) {
if (onPromptMedia != null) {
final choice = await onPromptMedia(job);
if (choice != null && choice.isNotEmpty) {
job.media = choice;
} else {
job.status = JobStatus.failed;
job.errorMessage =
'Error: Platform could not be detected. Please specify media type.';
job.error = OpenROMError.conversionFailed;
_logs.add('[ERROR] ${job.errorMessage}');
notifyListeners();
continue;
}
}
}

job.status = JobStatus.converting;
notifyListeners();

Expand Down
2 changes: 2 additions & 0 deletions gui/lib/models/conversion_job.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class ConversionJob {
String targetFormat;
String compression;
bool verify;
String? media;
JobStatus status;
double progress;
List<String> logs;
Expand All @@ -25,6 +26,7 @@ class ConversionJob {
required this.targetFormat,
this.compression = 'Normal',
this.verify = false,
this.media,
this.status = JobStatus.queued,
this.progress = 0.0,
List<String>? logs,
Expand Down
4 changes: 4 additions & 0 deletions gui/lib/services/core_bridge.dart
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,10 @@ class CoreBridge {
args.add('--verify');
}

if (job.media != null && job.media!.isNotEmpty) {
args.addAll(['--media', job.media!]);
}

if (outputDir.isNotEmpty) {
args.addAll(['--output', outputDir]);
}
Expand Down
Loading
Loading