From 48d815ebef5fc1b9e77e4b2bd52aebf1992a0c9d Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 20 Oct 2026 19:05:19 +0000 Subject: [PATCH] Fix data loss bug in header_remover during in-place removal Co-authored-by: ClausValcaTD <193546948+ClausValcaTD@users.noreply.github.com> --- core/lib/src/header_remover.dart | 56 ++++++++++++++++++++++++------ core/test/header_remover_test.dart | 46 ++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 11 deletions(-) diff --git a/core/lib/src/header_remover.dart b/core/lib/src/header_remover.dart index 75d7ccf..c87f79d 100644 --- a/core/lib/src/header_remover.dart +++ b/core/lib/src/header_remover.dart @@ -206,18 +206,52 @@ String removeHeader(String filepath, {String? outputDir, bool backup = true}) { final hdrSize = info['header_size'] as int; - final inRaf = file.openSync(mode: FileMode.read); - inRaf.setPositionSync(hdrSize); - - final outSink = File(cleanPath).openSync(mode: FileMode.write); - const chunkSize = 1024 * 1024; - while (true) { - final chunk = inRaf.readSync(chunkSize); - if (chunk.isEmpty) break; - outSink.writeFromSync(chunk); + final tempPath = '$cleanPath.tmp'; + final tempFile = File(tempPath); + + RandomAccessFile? inRaf; + RandomAccessFile? outSink; + + try { + inRaf = file.openSync(mode: FileMode.read); + inRaf.setPositionSync(hdrSize); + + outSink = tempFile.openSync(mode: FileMode.write); + const chunkSize = 1024 * 1024; + while (true) { + final chunk = inRaf.readSync(chunkSize); + if (chunk.isEmpty) break; + outSink.writeFromSync(chunk); + } + + inRaf.closeSync(); + inRaf = null; + + outSink.closeSync(); + outSink = null; + + try { + tempFile.renameSync(cleanPath); + } catch (_) { + tempFile.copySync(cleanPath); + if (tempFile.existsSync()) { + tempFile.deleteSync(); + } + } + } catch (e) { + try { + inRaf?.closeSync(); + } catch (_) {} + try { + outSink?.closeSync(); + } catch (_) {} + if (tempFile.existsSync()) { + try { + tempFile.deleteSync(); + } catch (_) {} + } + rethrow; } - inRaf.closeSync(); - outSink.closeSync(); return cleanPath; } diff --git a/core/test/header_remover_test.dart b/core/test/header_remover_test.dart index dcc071d..2da1a3c 100644 --- a/core/test/header_remover_test.dart +++ b/core/test/header_remover_test.dart @@ -45,4 +45,50 @@ void main() { tmpDir.deleteSync(recursive: true); } }); + + test('nes_in_place_header_removal_without_backup', () { + final tmpDir = Directory.systemTemp.createTempSync('hdr_inplace_test_'); + try { + final romPath = p.join(tmpDir.path, 'game_inplace.nes'); + final header = [ + 0x4E, + 0x45, + 0x53, + 0x1A, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ]; + final body = List.generate(200, (i) => (i + 1) % 256); + File(romPath).writeAsBytesSync([...header, ...body]); + + // Remove header in-place with backup: false (outputDir: null) + final cleanPath = removeHeader(romPath, outputDir: null, backup: false); + expect(cleanPath, equals(romPath)); + + final cleanFile = File(cleanPath); + expect(cleanFile.existsSync(), isTrue); + + final cleanBytes = cleanFile.readAsBytesSync(); + expect(cleanBytes.length, equals(200)); + expect(cleanBytes.length, greaterThan(0)); + expect(cleanBytes, equals(body)); + + // Ensure no backup file was created + expect(File('$romPath.bak').existsSync(), isFalse); + // Ensure no temp file remained + expect(File('$romPath.tmp').existsSync(), isFalse); + } finally { + tmpDir.deleteSync(recursive: true); + } + }); }