Skip to content

Commit 9b77951

Browse files
sunnylqmclaude
andcommitted
fix: clear compressType label when single-format diff stores raw payload
HDiffPatch v5's single-format writer keeps the lzma2 compressType label even when compression would expand the payload and it falls back to raw storage (compressedSize==0). Old deployed decoders (HDiffPatch ~4.x, e.g. react-native-update's vendored hpatch) decide whether to decompress from the label alone, so they lzma2-decode raw bytes and fail with patch error -4 — any tiny diff between near-identical bundles bricked the diff channel on every fielded client. diff()/diffWindow()/diffSingleStream() now post-normalize their output: when the parsed header says compressedSize==0 with a non-empty label, the label bytes are spliced out of the header (payload and all other fields are byte-identical), restoring 1.x-compatible output. A shared helper validates the HDIFFSF20& prefix and computes splice offsets for both the in-memory and file paths. diffStream (HDIFF13) is unaffected: that format carries per-stream sizes and old decoders already judge by compressedSize. Tests: tiny raw-stored diffs assert an empty-label header and round-trip via patch()/patchSingleStream() across all three writers; a compressible case asserts the lzma2 label is kept. Cross-verified externally against react-native-update's unpatched vendored decoder. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 4f42a80 commit 9b77951

2 files changed

Lines changed: 224 additions & 2 deletions

File tree

src/hdiff.cpp

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
#include "hdiff.h"
22
#include "../HDiffPatch/libHDiffPatch/HDiff/diff.h"
3+
#include "../HDiffPatch/libHDiffPatch/HPatch/patch.h"
34
#include "../HDiffPatch/file_for_patch.h"
5+
#include <cstring>
46
#include <stdexcept>
57

68
#define _CompressPlugin_lzma2
@@ -28,6 +30,8 @@ namespace {
2830
// 显式固定为旧值,保证与既有版本产出行为连续。
2931
const int kSingleMatchScore = 3;
3032
const size_t kPatchStepMemSize = 1024 * 256;
33+
const char kSingleDiffPrefix[] = "HDIFFSF20&";
34+
const size_t kSingleDiffPrefixSize = sizeof(kSingleDiffPrefix) - 1;
3135

3236
struct FileStreamGuard {
3337
hpatch_TFileStreamInput oldStream{};
@@ -99,6 +103,155 @@ namespace {
99103
}
100104
}
101105
};
106+
107+
struct FileRewriteGuard {
108+
hpatch_TFileStreamOutput stream{};
109+
bool opened = false;
110+
111+
FileRewriteGuard() {
112+
hpatch_TFileStreamOutput_init(&stream);
113+
}
114+
~FileRewriteGuard() {
115+
if (opened) hpatch_TFileStreamOutput_close(&stream);
116+
}
117+
void open(const char* path) {
118+
if (!hpatch_TFileStreamOutput_reopen(&stream, path, ~(hpatch_StreamPos_t)0)) {
119+
throw std::runtime_error("open diff file for rewrite failed.");
120+
}
121+
opened = true;
122+
hpatch_TFileStreamOutput_setRandomOut(&stream, hpatch_TRUE);
123+
}
124+
void close() {
125+
opened = false;
126+
if (!hpatch_TFileStreamOutput_close(&stream)) {
127+
throw std::runtime_error("close diff file failed.");
128+
}
129+
}
130+
};
131+
132+
bool should_clear_single_raw_compress_type(const hpatch_singleCompressedDiffInfo& diffInfo,
133+
size_t* outTypeLen) {
134+
if ((diffInfo.compressedSize != 0) || (diffInfo.compressType[0] == '\0')) {
135+
return false;
136+
}
137+
*outTypeLen = std::strlen(diffInfo.compressType);
138+
if (*outTypeLen == 0) {
139+
return false;
140+
}
141+
return true;
142+
}
143+
144+
struct SingleRawCompressTypeSplice {
145+
bool shouldSplice = false;
146+
hpatch_StreamPos_t readPos = 0;
147+
hpatch_StreamPos_t writePos = 0;
148+
hpatch_StreamPos_t newSize = 0;
149+
};
150+
151+
SingleRawCompressTypeSplice get_single_raw_compress_type_splice(
152+
const hpatch_singleCompressedDiffInfo& diffInfo,
153+
hpatch_StreamPos_t diffSize,
154+
const uint8_t* prefixBytes) {
155+
size_t typeLen = 0;
156+
if (!should_clear_single_raw_compress_type(diffInfo, &typeLen)) {
157+
return {};
158+
}
159+
if ((diffSize < kSingleDiffPrefixSize + typeLen + 1) ||
160+
(0 != std::memcmp(prefixBytes, kSingleDiffPrefix, kSingleDiffPrefixSize))) {
161+
throw std::runtime_error("single diff header layout error.");
162+
}
163+
164+
SingleRawCompressTypeSplice splice;
165+
splice.shouldSplice = true;
166+
splice.readPos = kSingleDiffPrefixSize + typeLen;
167+
splice.writePos = kSingleDiffPrefixSize;
168+
splice.newSize = diffSize - typeLen;
169+
return splice;
170+
}
171+
172+
void normalize_single_raw_compress_type(std::vector<uint8_t>& diff) {
173+
hpatch_singleCompressedDiffInfo diffInfo;
174+
if (!getSingleCompressedDiffInfo_mem(&diffInfo, diff.data(), diff.data() + diff.size())) {
175+
throw std::runtime_error("getSingleCompressedDiffInfo_mem() failed, invalid diff data!");
176+
}
177+
178+
SingleRawCompressTypeSplice splice = get_single_raw_compress_type_splice(
179+
diffInfo, diff.size(), diff.data());
180+
if (!splice.shouldSplice) {
181+
return;
182+
}
183+
diff.erase(diff.begin() + (size_t)splice.writePos,
184+
diff.begin() + (size_t)splice.readPos);
185+
}
186+
187+
void normalize_single_raw_compress_type(const char* diffPath) {
188+
hpatch_TFileStreamInput diffIn;
189+
hpatch_TFileStreamInput_init(&diffIn);
190+
bool diffInOpened = false;
191+
192+
hpatch_singleCompressedDiffInfo diffInfo;
193+
hpatch_StreamPos_t oldDiffSize = 0;
194+
uint8_t prefixBytes[kSingleDiffPrefixSize];
195+
try {
196+
if (!hpatch_TFileStreamInput_open(&diffIn, diffPath)) {
197+
throw std::runtime_error("open diff file for read failed.");
198+
}
199+
diffInOpened = true;
200+
oldDiffSize = diffIn.base.streamSize;
201+
if ((oldDiffSize < kSingleDiffPrefixSize) ||
202+
!diffIn.base.read(&diffIn.base, 0, prefixBytes,
203+
prefixBytes + kSingleDiffPrefixSize)) {
204+
throw std::runtime_error("single diff header layout error.");
205+
}
206+
if (!getSingleCompressedDiffInfo(&diffInfo, &diffIn.base, 0)) {
207+
throw std::runtime_error("getSingleCompressedDiffInfo() failed, invalid diff data!");
208+
}
209+
} catch (...) {
210+
if (diffInOpened) hpatch_TFileStreamInput_close(&diffIn);
211+
throw;
212+
}
213+
diffInOpened = false;
214+
if (!hpatch_TFileStreamInput_close(&diffIn)) {
215+
throw std::runtime_error("close diff file failed.");
216+
}
217+
218+
SingleRawCompressTypeSplice splice = get_single_raw_compress_type_splice(
219+
diffInfo, oldDiffSize, prefixBytes);
220+
if (!splice.shouldSplice) {
221+
return;
222+
}
223+
224+
FileRewriteGuard diffOut;
225+
diffOut.open(diffPath);
226+
227+
const size_t kBufSize = hpatch_kFileIOBufBetterSize;
228+
std::vector<uint8_t> buf(kBufSize);
229+
hpatch_StreamPos_t readPos = splice.readPos;
230+
hpatch_StreamPos_t writePos = splice.writePos;
231+
while (readPos < oldDiffSize) {
232+
hpatch_StreamPos_t readLen = oldDiffSize - readPos;
233+
if (readLen > (hpatch_StreamPos_t)buf.size()) {
234+
readLen = (hpatch_StreamPos_t)buf.size();
235+
}
236+
if (!diffOut.stream.base.read_writed(&diffOut.stream.base, readPos,
237+
buf.data(), buf.data() + (size_t)readLen)) {
238+
throw std::runtime_error("read diff file for rewrite failed.");
239+
}
240+
if (!diffOut.stream.base.write(&diffOut.stream.base, writePos,
241+
buf.data(), buf.data() + (size_t)readLen)) {
242+
throw std::runtime_error("rewrite diff file failed.");
243+
}
244+
readPos += readLen;
245+
writePos += readLen;
246+
}
247+
if (!hpatch_TFileStreamOutput_flush(&diffOut.stream)) {
248+
throw std::runtime_error("flush diff file failed.");
249+
}
250+
if (!hpatch_TFileStreamOutput_truncate(&diffOut.stream, splice.newSize)) {
251+
throw std::runtime_error("truncate diff file failed.");
252+
}
253+
diffOut.close();
254+
}
102255
}
103256

104257
void hdiff(const uint8_t* old, size_t oldsize, const uint8_t* _new, size_t newsize,
@@ -110,6 +263,7 @@ void hdiff(const uint8_t* old, size_t oldsize, const uint8_t* _new, size_t newsi
110263
create_single_compressed_diff(_new, _new + newsize, old, old + oldsize, out_codeBuf,
111264
&compressPlugin.base, kPatchStepMemSize,
112265
kSingleMatchScore);
266+
normalize_single_raw_compress_type(out_codeBuf);
113267

114268
if (!check_single_compressed_diff(_new, _new + newsize, old, old + oldsize,
115269
out_codeBuf.data(),
@@ -173,6 +327,7 @@ void hdiff_window(const char* oldPath,const char* newPath,const char* outDiffPat
173327
kSingleMatchScore);
174328

175329
streams.closeDiffOut();
330+
normalize_single_raw_compress_type(outDiffPath);
176331
streams.openDiffIn(outDiffPath);
177332
if (!check_single_compressed_diff(&streams.newStream.base, &streams.oldStream.base,
178333
&streams.diffInStream.base, decompressPlugin)) {
@@ -200,6 +355,7 @@ void hdiff_single_stream(const char* oldPath,const char* newPath,const char* out
200355
kMatchBlockSize_default);
201356

202357
streams.closeDiffOut();
358+
normalize_single_raw_compress_type(outDiffPath);
203359
streams.openDiffIn(outDiffPath);
204360
if (!check_single_compressed_diff(&streams.newStream.base, &streams.oldStream.base,
205361
&streams.diffInStream.base, decompressPlugin)) {

test/test.js

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,23 @@ var fs = require("fs");
55
var path = require("path");
66
var os = require("os");
77

8+
function deterministicBytes(size, seed) {
9+
var out = Buffer.allocUnsafe(size);
10+
var x = seed >>> 0;
11+
for (var i = 0; i < out.length; ++i) {
12+
x ^= x << 13;
13+
x ^= x >>> 17;
14+
x ^= x << 5;
15+
out[i] = x & 0xff;
16+
}
17+
return out;
18+
}
19+
20+
function assertHeaderPrefix(diffData, prefix) {
21+
var expected = Buffer.from(prefix, "latin1");
22+
assert.deepStrictEqual(diffData.subarray(0, expected.length), expected);
23+
}
24+
825
console.log("Test 1: diff + patch = original (sync)...");
926
var oldData = crypto.randomBytes(40960);
1027
var newData = Buffer.concat([
@@ -52,7 +69,56 @@ var uint8Patched = hdiffpatch.patch(uint8Old, uint8Diff);
5269
assert.deepStrictEqual(Buffer.from(uint8Patched), newData);
5370
console.log(" ✓ Uint8Array works");
5471

55-
console.log("\nTest 5: diffSingleStream (single format, low-memory generation)...");
72+
console.log("\nTest 5: raw single-format diffs clear compressType...");
73+
var tinyOld = deterministicBytes(280 * 1024, 0x12345678);
74+
var tinyNew = Buffer.from(tinyOld);
75+
for (var i = 0; i < 17; ++i) {
76+
tinyNew[Math.floor(tinyNew.length / 2) + i] ^= i + 1;
77+
}
78+
var tinyDiff = hdiffpatch.diff(tinyOld, tinyNew);
79+
assertHeaderPrefix(tinyDiff, "HDIFFSF20&\x00");
80+
assert.deepStrictEqual(hdiffpatch.patch(tinyOld, tinyDiff), tinyNew);
81+
82+
var tinyDir = fs.mkdtempSync(path.join(os.tmpdir(), "hdiffpatch-raw-"));
83+
var tinyOldPath = path.join(tinyDir, "old.bin");
84+
var tinyNewPath = path.join(tinyDir, "new.bin");
85+
fs.writeFileSync(tinyOldPath, tinyOld);
86+
fs.writeFileSync(tinyNewPath, tinyNew);
87+
88+
function assertRawLabelClearedForFileDiff(diffFn, name) {
89+
var tinyDiffPath = path.join(tinyDir, name + ".diff");
90+
var tinyOutPath = path.join(tinyDir, name + "-out.bin");
91+
diffFn(tinyOldPath, tinyNewPath, tinyDiffPath);
92+
var tinyFileDiff = fs.readFileSync(tinyDiffPath);
93+
assertHeaderPrefix(tinyFileDiff, "HDIFFSF20&\x00");
94+
assert.deepStrictEqual(hdiffpatch.patch(tinyOld, tinyFileDiff), tinyNew);
95+
hdiffpatch.patchSingleStream(tinyOldPath, tinyDiffPath, tinyOutPath);
96+
assert.deepStrictEqual(fs.readFileSync(tinyOutPath), tinyNew);
97+
}
98+
99+
assertRawLabelClearedForFileDiff(
100+
(oldPath, newPath, diffPath) =>
101+
hdiffpatch.diffWindow(oldPath, newPath, diffPath, 8 * 1024 * 1024),
102+
"window"
103+
);
104+
assertRawLabelClearedForFileDiff(hdiffpatch.diffSingleStream, "single");
105+
fs.rmSync(tinyDir, { recursive: true, force: true });
106+
console.log(" ✓ diff(), diffWindow(), and diffSingleStream() clear raw payload labels");
107+
108+
console.log("\nTest 5a: compressed single-format diffs keep compressType...");
109+
var compressibleOld = deterministicBytes(280 * 1024, 0x9abcdef0);
110+
var repeated = Buffer.alloc(64 * 1024, "A");
111+
var compressibleNew = Buffer.concat([
112+
compressibleOld.subarray(0, 128 * 1024),
113+
repeated,
114+
compressibleOld.subarray(128 * 1024)
115+
]);
116+
var compressibleDiff = hdiffpatch.diff(compressibleOld, compressibleNew);
117+
assertHeaderPrefix(compressibleDiff, "HDIFFSF20&lzma2\x00");
118+
assert.deepStrictEqual(hdiffpatch.patch(compressibleOld, compressibleDiff), compressibleNew);
119+
console.log(" ✓ lzma2 label remains when compression is used");
120+
121+
console.log("\nTest 5b: diffSingleStream (single format, low-memory generation)...");
56122
var ssDir = fs.mkdtempSync(path.join(os.tmpdir(), "hdiffpatch-ss-"));
57123
var ssOldPath = path.join(ssDir, "old.bin");
58124
var ssNewPath = path.join(ssDir, "new.bin");
@@ -70,7 +136,7 @@ hdiffpatch.patchSingleStream(ssOldPath, ssDiffPath, ssOutPath);
70136
assert.deepStrictEqual(fs.readFileSync(ssOutPath), newData);
71137
console.log(" ✓ diffSingleStream output applies via patch() and patchSingleStream()");
72138

73-
console.log("\nTest 5b: diffWindow (single format, window/fast-match generation)...");
139+
console.log("\nTest 5c: diffWindow (single format, window/fast-match generation)...");
74140
var winDiffPath = path.join(ssDir, "win.diff");
75141
var winOutPath = path.join(ssDir, "win-out.bin");
76142
var winDiffOut = hdiffpatch.diffWindow(ssOldPath, ssNewPath, winDiffPath);

0 commit comments

Comments
 (0)