Skip to content

Commit ffd9dfa

Browse files
committed
fix: validate rollout input and add comprehensive test coverage for core utilities and CLI logic
1 parent 123102c commit ffd9dfa

16 files changed

+1072
-594
lines changed

src/api.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,13 @@ async function query(url: string, options: fetch.RequestInit) {
9898
let json: any;
9999
try {
100100
json = JSON.parse(text);
101-
} catch (e) {}
101+
} catch (e) {
102+
if (resp.status === 200 && text) {
103+
console.warn(
104+
`Warning: API returned 200 with non-JSON body (${text.length} bytes)`,
105+
);
106+
}
107+
}
102108

103109
if (resp.status !== 200) {
104110
const message = json?.message || resp.statusText || `HTTP ${resp.status}`;

src/app.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ export const appCommands = {
112112
options: { platform: Platform };
113113
}) => {
114114
const { platform } = options;
115-
const id = args[0] || chooseApp(platform);
115+
const id = args[0] || (await chooseApp(platform)).id;
116116
if (!id) {
117117
console.log(t('cancelled'));
118118
}
@@ -139,7 +139,9 @@ export const appCommands = {
139139
Record<Platform, { appId: number; appKey: string }>
140140
> = {};
141141
try {
142-
updateInfo = JSON.parse(await fs.promises.readFile('update.json', 'utf8'));
142+
updateInfo = JSON.parse(
143+
await fs.promises.readFile('update.json', 'utf8'),
144+
);
143145
} catch (e: any) {
144146
if (e.code !== 'ENOENT') {
145147
console.error(t('failedToParseUpdateJson'));

src/diff.ts

Lines changed: 1 addition & 155 deletions
Original file line numberDiff line numberDiff line change
@@ -19,41 +19,12 @@ import {
1919
} from './utils/zip-options';
2020

2121
type Diff = (oldSource?: Buffer, newSource?: Buffer) => Buffer;
22-
type HpatchCover = {
23-
oldPos: number | string | bigint;
24-
newPos: number | string | bigint;
25-
len: number | string | bigint;
26-
};
27-
type HpatchCompatiblePlan = {
28-
covers?: HpatchCover[];
29-
};
30-
type HdiffWithCoversOptions = {
31-
mode?: 'replace' | 'merge' | 'native-coalesce';
32-
};
3322
type HdiffModule = {
3423
diff?: Diff;
35-
diffWithCovers?: (
36-
oldSource: Buffer,
37-
newSource: Buffer,
38-
covers: HpatchCover[],
39-
options?: HdiffWithCoversOptions,
40-
) => { diff?: Buffer };
4124
};
4225
type BsdiffModule = {
4326
diff?: Diff;
4427
};
45-
type ChiffModule = {
46-
hpatchCompatiblePlanResult?: (
47-
oldSource: Buffer,
48-
newSource: Buffer,
49-
) => HpatchCompatiblePlan;
50-
hpatchApproximatePlanResult?: (
51-
oldSource: Buffer,
52-
newSource: Buffer,
53-
) => HpatchCompatiblePlan;
54-
};
55-
type ChiffHpatchPolicy = 'off' | 'costed';
56-
type ChiffHpatchExactPolicy = 'off' | 'on';
5728
type EntryMap = Record<string, { crc32: number; fileName: string }>;
5829
type CrcMap = Record<number, string>;
5930
type CopyMap = Record<string, string>;
@@ -86,121 +57,6 @@ const loadModule = <T>(pkgName: string): T | undefined => {
8657

8758
const hdiff = loadModule<HdiffModule>('node-hdiffpatch');
8859
const bsdiff = loadModule<BsdiffModule>('node-bsdiff');
89-
const chiff = loadModule<ChiffModule>('@chiff/node');
90-
91-
// Structured covers are experimental and can be expensive on real Hermes input.
92-
// Keep native hdiff as the default unless the server explicitly opts in.
93-
function resolveChiffHpatchPolicy(policy?: unknown): ChiffHpatchPolicy {
94-
const value = String(
95-
policy ?? process.env.RNU_CHIFF_HPATCH_POLICY ?? 'off',
96-
).toLowerCase();
97-
if (
98-
value === 'costed' ||
99-
value === 'on' ||
100-
value === 'true' ||
101-
value === '1'
102-
) {
103-
return 'costed';
104-
}
105-
return 'off';
106-
}
107-
108-
function resolveChiffHpatchMinNativeBytes(value?: unknown): number {
109-
const raw = value ?? process.env.RNU_CHIFF_HPATCH_MIN_NATIVE_BYTES ?? 4096;
110-
const parsed = Number(raw);
111-
if (!Number.isFinite(parsed) || parsed < 0) {
112-
return 4096;
113-
}
114-
return Math.floor(parsed);
115-
}
116-
117-
function resolveChiffHpatchExactPolicy(policy?: unknown): ChiffHpatchExactPolicy {
118-
const value = String(
119-
policy ?? process.env.RNU_CHIFF_HPATCH_EXACT_COVERS ?? 'off',
120-
).toLowerCase();
121-
if (value === 'on' || value === 'true' || value === '1') {
122-
return 'on';
123-
}
124-
return 'off';
125-
}
126-
127-
function createChiffAwareHdiff(
128-
hdiffModule: HdiffModule,
129-
chiffModule: ChiffModule | undefined,
130-
policy: ChiffHpatchPolicy,
131-
minNativeBytes: number,
132-
exactPolicy: ChiffHpatchExactPolicy,
133-
): Diff {
134-
const baseDiff = hdiffModule.diff;
135-
if (!baseDiff) {
136-
throw new Error(t('nodeHdiffpatchRequired', { scriptName }));
137-
}
138-
139-
if (policy === 'off') {
140-
return baseDiff;
141-
}
142-
143-
return (oldSource?: Buffer, newSource?: Buffer) => {
144-
const nativeDiff = baseDiff(oldSource, newSource);
145-
if (!oldSource || !newSource || !hdiffModule.diffWithCovers) {
146-
return nativeDiff;
147-
}
148-
149-
let bestDiff = nativeDiff;
150-
const tryDiffWithCovers = (
151-
covers: HpatchCover[],
152-
mode: 'replace' | 'merge' | 'native-coalesce',
153-
) => {
154-
try {
155-
const result = hdiffModule.diffWithCovers?.(
156-
oldSource,
157-
newSource,
158-
covers,
159-
{ mode },
160-
);
161-
if (
162-
Buffer.isBuffer(result?.diff) &&
163-
result.diff.length < bestDiff.length
164-
) {
165-
bestDiff = result.diff;
166-
}
167-
} catch {}
168-
};
169-
170-
tryDiffWithCovers([], 'native-coalesce');
171-
172-
if (nativeDiff.length < minNativeBytes) {
173-
return bestDiff;
174-
}
175-
176-
try {
177-
const approximatePlan = chiffModule?.hpatchApproximatePlanResult?.(
178-
oldSource,
179-
newSource,
180-
);
181-
if (Array.isArray(approximatePlan?.covers)) {
182-
tryDiffWithCovers(approximatePlan.covers, 'merge');
183-
}
184-
} catch {}
185-
186-
if (
187-
exactPolicy === 'off' ||
188-
!chiffModule?.hpatchCompatiblePlanResult
189-
) {
190-
return bestDiff;
191-
}
192-
193-
try {
194-
const plan = chiffModule.hpatchCompatiblePlanResult(oldSource, newSource);
195-
if (Array.isArray(plan.covers)) {
196-
tryDiffWithCovers(plan.covers, 'replace');
197-
tryDiffWithCovers(plan.covers, 'merge');
198-
}
199-
} catch {}
200-
201-
return bestDiff;
202-
};
203-
}
20460

20561
function basename(fn: string): string | undefined {
20662
const m = /^(.+\/)[^\/]+\/?$/.exec(fn);
@@ -473,10 +329,6 @@ async function diffFromPackage(
473329
type DiffCommandOptions = {
474330
customDiff?: Diff;
475331
customHdiffModule?: HdiffModule;
476-
customChiffModule?: ChiffModule;
477-
chiffHpatchPolicy?: ChiffHpatchPolicy;
478-
chiffHpatchMinNativeBytes?: number | string;
479-
chiffHpatchExactCovers?: ChiffHpatchExactPolicy | boolean | string | number;
480332
[key: string]: any;
481333
};
482334

@@ -493,13 +345,7 @@ function resolveDiffImplementation(
493345
if (!hdiffModule?.diff) {
494346
throw new Error(t('nodeHdiffpatchRequired', { scriptName }));
495347
}
496-
return createChiffAwareHdiff(
497-
hdiffModule,
498-
options.customChiffModule ?? chiff,
499-
resolveChiffHpatchPolicy(options.chiffHpatchPolicy),
500-
resolveChiffHpatchMinNativeBytes(options.chiffHpatchMinNativeBytes),
501-
resolveChiffHpatchExactPolicy(options.chiffHpatchExactCovers),
502-
);
348+
return hdiffModule.diff;
503349
}
504350

505351
if (!bsdiff?.diff) {

src/utils/zip-entries.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ export function readEntry(
1515
const buffers: Buffer[] = [];
1616
return new Promise((resolve, reject) => {
1717
zipFile.openReadStream(entry, (err, stream) => {
18+
if (err) {
19+
return reject(err);
20+
}
21+
if (!stream) {
22+
return reject(new Error(`Unable to read zip entry: ${entry.fileName}`));
23+
}
1824
stream.on('data', (chunk: Buffer) => {
1925
buffers.push(chunk);
2026
});

src/utils/zip-options.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import path from 'path';
21
import * as fs from 'fs';
2+
import path from 'path';
33

44
export type ZipEntryOptions = {
55
compress?: boolean;
@@ -108,11 +108,7 @@ function hasAlreadyCompressedMagic(bytes: Buffer): boolean {
108108
if (startsWith(bytes, Buffer.from('ID3', 'ascii'))) {
109109
return true;
110110
}
111-
if (
112-
bytes[0] === 0xff &&
113-
bytes.length >= 2 &&
114-
(bytes[1] & 0xe0) === 0xe0
115-
) {
111+
if (bytes[0] === 0xff && bytes.length >= 2 && (bytes[1] & 0xe0) === 0xe0) {
116112
return true;
117113
}
118114
if (

src/versions.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -485,7 +485,8 @@ export const versionCommands = {
485485
update: async ({ options }: { options: VersionCommandOptions }) => {
486486
const platform = await getPlatform(options.platform);
487487
const appId = options.appId || (await getSelectedApp(platform)).appId;
488-
let versionId = options.versionId || (await chooseVersion(String(appId))).id;
488+
let versionId =
489+
options.versionId || (await chooseVersion(String(appId))).id;
489490
if (versionId === 'null') {
490491
versionId = undefined;
491492
}
@@ -498,12 +499,8 @@ export const versionCommands = {
498499
let rollout: number | undefined = undefined;
499500

500501
if (options.rollout !== undefined) {
501-
try {
502-
rollout = Number.parseInt(options.rollout);
503-
} catch (e) {
504-
throw new Error(t('rolloutRangeError'));
505-
}
506-
if (rollout < 1 || rollout > 100) {
502+
rollout = Number.parseInt(options.rollout, 10);
503+
if (Number.isNaN(rollout) || rollout < 1 || rollout > 100) {
507504
throw new Error(t('rolloutRangeError'));
508505
}
509506
}
@@ -596,7 +593,8 @@ export const versionCommands = {
596593
}) => {
597594
const platform = await getPlatform(options.platform);
598595
const { appId } = await getSelectedApp(platform);
599-
const versionId = options.versionId || (await chooseVersion(String(appId))).id;
596+
const versionId =
597+
options.versionId || (await chooseVersion(String(appId))).id;
600598

601599
const updateParams: Record<string, string> = {};
602600
if (options.name) updateParams.name = options.name;

0 commit comments

Comments
 (0)