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
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,19 @@ static File stagingDirectoryFor(File versionDir) {
return new File(versionDir.getPath() + STAGING_SUFFIX);
}

/**
* bundleSha256 is mandatory for schema 1: a version whose bundle could
* not be hashed must fail its install, never be recorded unverifiable.
*/
static String build(String versionHash, String bundleSha256, String artifactSha256)
throws JSONException {
throws IOException, JSONException {
if (bundleSha256 == null || bundleSha256.isEmpty()) {
throw new IOException("Bundle version " + versionHash + " has no bundle digest.");
}
JSONObject record = new JSONObject();
record.put("schema", SCHEMA);
record.put("versionHash", versionHash);
if (bundleSha256 != null && !bundleSha256.isEmpty()) {
record.put("bundleSha256", bundleSha256);
}
record.put("bundleSha256", bundleSha256);
if (artifactSha256 != null && !artifactSha256.isEmpty()) {
record.put("artifactSha256", artifactSha256);
}
Expand Down Expand Up @@ -123,7 +128,8 @@ static boolean isComplete(File versionDir, String versionHash) {
}

/**
* Activation check: re-hashes the bundle when the record carries a digest.
* Activation check: re-hashes the bundle against the record's digest,
* which schema 1 makes mandatory (only the legacy empty marker has none).
* Throws with the reason when the directory must not be activated.
*/
static void verifyForActivation(File versionDir, String versionHash, File bundleFile)
Expand All @@ -139,9 +145,10 @@ static void verifyForActivation(File versionDir, String versionHash, File bundle
|| !versionHash.equals(record.optString("versionHash", ""))) {
throw new IOException("Bundle version " + versionHash + " completion record mismatch.");
}
String expected = record.optString("bundleSha256", "");
Object digest = record.opt("bundleSha256");
String expected = digest instanceof String ? (String) digest : "";
if (expected.isEmpty()) {
return;
throw new IOException("Bundle version " + versionHash + " completion record mismatch.");
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
String actual = UpdateFileUtils.sha256Hex(bundleFile);
if (!expected.equalsIgnoreCase(actual)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -533,9 +533,10 @@ private void verifySwitchTarget(String hash) {
StateCoreResult currentState = getStateSnapshot();
boolean isLegacyActivatedVersion = hash.equals(currentState.currentVersion)
|| hash.equals(currentState.lastVersion);
if (isLegacyActivatedVersion) {
if (isLegacyActivatedVersion && !new File(versionDir, InstallRecord.FILE_NAME).exists()) {
// Versions activated before completion markers were introduced are
// explicitly grandfathered through current/last state.
// explicitly grandfathered through current/last state. A directory
// that does carry a record is verified like any other.
return;
}
if (!InstallRecord.isComplete(versionDir, hash)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Arrays;
import org.json.JSONException;
import org.json.JSONObject;
import org.junit.Rule;
import org.junit.Test;
Expand Down Expand Up @@ -41,17 +40,29 @@ private static String sha256Hex(byte[] bytes) throws Exception {
}

@Test
public void buildWritesSchemaAndOnlyPresentDigests() throws JSONException {
public void buildWritesSchemaAndDigests() throws Exception {
JSONObject full = new JSONObject(InstallRecord.build(HASH, "bb", "aa"));
assertEquals(InstallRecord.SCHEMA, full.getInt("schema"));
assertEquals(HASH, full.getString("versionHash"));
assertEquals("bb", full.getString("bundleSha256"));
assertEquals("aa", full.getString("artifactSha256"));

JSONObject bare = new JSONObject(InstallRecord.build(HASH, "", null));
assertEquals(HASH, bare.getString("versionHash"));
assertFalse(bare.has("bundleSha256"));
assertFalse(bare.has("artifactSha256"));
// artifactSha256 is optional diagnostics.
JSONObject noArtifact = new JSONObject(InstallRecord.build(HASH, "bb", null));
assertEquals("bb", noArtifact.getString("bundleSha256"));
assertFalse(noArtifact.has("artifactSha256"));
}

@Test
public void buildRefusesARecordWithoutBundleDigest() throws Exception {
for (String missing : new String[] {"", null}) {
try {
InstallRecord.build(HASH, missing, "aa");
fail("a record without bundleSha256 must not be built");
} catch (IOException expected) {
assertTrue(expected.getMessage().contains("no bundle digest"));
}
}
}

@Test
Expand Down Expand Up @@ -128,18 +139,42 @@ public void verifyForActivationChecksTheBundleDigest() throws Exception {
}

@Test
public void verifyForActivationAcceptsRecordsWithoutDigestAndLegacyMarkers() throws Exception {
public void verifyForActivationTrustsLegacyEmptyMarkers() throws Exception {
File dir = temp.newFolder();
File bundle = new File(dir, "index.bundlejs");
writeFile(bundle, "x".getBytes(StandardCharsets.UTF_8));

InstallRecord.write(dir, InstallRecord.build(HASH, "", ""));
InstallRecord.verifyForActivation(dir, HASH, bundle);

writeFile(new File(dir, InstallRecord.FILE_NAME), new byte[0]);
InstallRecord.verifyForActivation(dir, HASH, bundle);
}

@Test
public void verifyForActivationRejectsSchemaOneRecordsWithoutDigest() throws Exception {
File dir = temp.newFolder();
File bundle = new File(dir, "index.bundlejs");
writeFile(bundle, "x".getBytes(StandardCharsets.UTF_8));
File marker = new File(dir, InstallRecord.FILE_NAME);
String prefix = "{\"schema\":" + InstallRecord.SCHEMA + ",\"versionHash\":\"" + HASH + "\"";
String[] records = {
prefix + "}",
prefix + ",\"bundleSha256\":\"\"}",
prefix + ",\"bundleSha256\":null}",
prefix + ",\"bundleSha256\":123}",
prefix + ",\"artifactSha256\":\"aa\"}",
};
for (String json : records) {
writeFile(marker, json.getBytes(StandardCharsets.UTF_8));
// Still present for launch / dedup: only activation is refused.
assertTrue(InstallRecord.isComplete(dir, HASH));
try {
InstallRecord.verifyForActivation(dir, HASH, bundle);
fail("digest-less record must not activate: " + json);
} catch (IOException expected) {
assertTrue(expected.getMessage().contains("record mismatch"));
}
}
}

@Test
public void verifyForActivationRejectsMissingOrForeignRecords() throws Exception {
File dir = temp.newFolder();
Expand All @@ -152,7 +187,7 @@ public void verifyForActivationRejectsMissingOrForeignRecords() throws Exception
assertTrue(expected.getMessage().contains("no valid completion record"));
}

InstallRecord.write(dir, InstallRecord.build("anotherhash", "", ""));
InstallRecord.write(dir, InstallRecord.build("anotherhash", "bb", ""));
try {
InstallRecord.verifyForActivation(dir, HASH, bundle);
fail("record for another version must not activate");
Expand Down
6 changes: 5 additions & 1 deletion cpp/patch_core/install_record.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@
// - versionHash must equal the directory name; bundleSha256 is the digest of
// the final index.bundlejs (bundle.harmony.js on Harmony) and is
// re-verified at switchVersion time; artifactSha256 is the digest of the
// downloaded archive (diagnostics / CDN corruption attribution).
// downloaded archive (diagnostics / CDN corruption attribution, optional).
// - bundleSha256 is MANDATORY for schema 1. A writer whose bundle digest is
// empty or fails must fail the install (staging dropped, no record); a
// verifier must reject a non-empty record without one (record mismatch),
// never activate the version unverified.
// - Legacy: an EMPTY file is a completed install written by SDK < 10.53
// (no digests); it stays trusted for presence, nothing to verify.
//
Expand Down
34 changes: 26 additions & 8 deletions harmony/pushy/src/main/ets/InstallRecord.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import fileIo from '@ohos.file.fs';
import { util } from '@kit.ArkTS';
import NativePatchCore from './NativePatchCore';
import { ERROR_SWITCH_VERSION_FAILED, createPushyError } from './ErrorCodes';
import {
ERROR_DELTA_FAILED,
ERROR_SWITCH_VERSION_FAILED,
createPushyError,
} from './ErrorCodes';

// ArkTS mirror of cpp/patch_core/install_record.h — keep in sync by hand
// (harmony/pushy/src/test/check-constant-parity.js asserts the values match).
Expand Down Expand Up @@ -36,13 +40,19 @@ export function buildInstallRecord(
bundleSha256: string,
artifactSha256: string,
): string {
// schema 1 的 bundleSha256 是必填:算不出摘要的版本必须安装失败,绝不能
// 写成一条无法复核的记录。
if (!bundleSha256) {
throw createPushyError(
ERROR_DELTA_FAILED,
`Bundle version ${versionHash} has no bundle digest.`,
);
}
const record: InstallRecordData = {
schema: INSTALL_RECORD_SCHEMA,
versionHash,
bundleSha256,
};
if (bundleSha256) {
record.bundleSha256 = bundleSha256;
}
if (artifactSha256) {
record.artifactSha256 = artifactSha256;
}
Expand Down Expand Up @@ -133,8 +143,9 @@ export function isInstallComplete(versionDir: string, versionHash: string): bool
}

/**
* 激活前的记录校验(不含摘要):返回需要复核的 bundle 摘要,历史空记录或
* 无摘要记录返回空串;记录缺失/不符抛 SWITCH_VERSION_FAILED。纯函数。
* 激活前的记录校验(不含摘要):返回需要复核的 bundle 摘要,仅历史空记录
* 返回空串;记录缺失/不符或缺摘要(schema 1 必填)抛 SWITCH_VERSION_FAILED。
* 纯函数。
*/
export function expectedBundleSha256ForActivation(
record: InstallRecordData | null,
Expand All @@ -158,11 +169,18 @@ export function expectedBundleSha256ForActivation(
`Bundle version ${versionHash} completion record mismatch.`,
);
}
return record.bundleSha256 ?? '';
const expected = record.bundleSha256;
if (typeof expected !== 'string' || expected.length === 0) {
throw createPushyError(
ERROR_SWITCH_VERSION_FAILED,
`Bundle version ${versionHash} completion record mismatch.`,
);
}
return expected;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/**
* 激活前校验:记录带摘要时在 native 工作线程重新计算 bundle 摘要(整包
* 激活前校验:非历史空记录时在 native 工作线程重新计算 bundle 摘要(整包
* 几十 MB,不能在 UI 线程同步算);不通过则抛错说明原因。
*/
export async function verifyInstallForActivation(
Expand Down
13 changes: 8 additions & 5 deletions harmony/pushy/src/main/ets/UpdateContext.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import preferences from '@ohos.data.preferences';
import fileIo from '@ohos.file.fs';
import {
INSTALL_RECORD_FILE_NAME,
isInstallComplete,
readInstallRecord,
verifyInstallForActivation,
Expand Down Expand Up @@ -842,9 +843,10 @@ export class UpdateContext {

/**
* 激活前置校验(同步,不写任何东西):bundle 在盘上,且要么是本 SDK 记录为
* 完整安装的版本(bundle + 记录),要么是标记机制之前就激活过的历史版本
* (current/last 里有它)。其他无记录的目录可能是崩溃留下的半成品。
* 返回是否还需复核安装记录里的 bundle 摘要(历史版本无记录可核)。
* 完整安装的版本(bundle + 记录),要么是标记机制之前就激活过、目录里没有
* 记录文件的历史版本(current/last 里有它)。其他无记录的目录可能是崩溃
* 留下的半成品;有记录文件的目录即便是 current/last 也照常复核。
* 返回是否还需复核安装记录(无记录的历史版本无可核)。
*/
private assertActivatable(safeHash: string): boolean {
if (!fileIo.accessSync(this.getBundlePath(safeHash))) {
Expand All @@ -854,8 +856,9 @@ export class UpdateContext {
);
}
const legacyActivated =
this.readString('currentVersion') === safeHash ||
this.readString('lastVersion') === safeHash;
!fileIo.accessSync(`${this.rootDir}/${safeHash}/${INSTALL_RECORD_FILE_NAME}`) &&
(this.readString('currentVersion') === safeHash ||
this.readString('lastVersion') === safeHash);
if (!this.hasDownloadedVersion(safeHash) && !legacyActivated) {
throw createPushyError(
ERROR_SWITCH_VERSION_FAILED,
Expand Down
53 changes: 41 additions & 12 deletions harmony/pushy/src/test/InstallRecord.test.ets
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { describe, it, expect } from '@ohos/hypium';
import {
INSTALL_RECORD_FILE_NAME,
INSTALL_RECORD_SCHEMA,
InstallRecordData,
STAGING_SUFFIX,
buildInstallRecord,
expectedBundleSha256ForActivation,
Expand All @@ -10,7 +11,11 @@ import {
parseInstallRecord,
stagingDirectoryFor,
} from '../main/ets/InstallRecord';
import { ERROR_SWITCH_VERSION_FAILED, PushyError } from '../main/ets/ErrorCodes';
import {
ERROR_DELTA_FAILED,
ERROR_SWITCH_VERSION_FAILED,
PushyError,
} from '../main/ets/ErrorCodes';

// 两阶段安装的完成记录(cpp/patch_core/install_record.h 的 ArkTS 镜像):
// 纯逻辑部分,不碰磁盘。
Expand All @@ -27,13 +32,24 @@ export default function installRecordTest() {
expect(record!.artifactSha256).assertEqual('ARTIFACT');
});

it('omits empty digests', 0, () => {
const record = parseInstallRecord(buildInstallRecord('abc', '', ''));
it('omits an empty artifact digest', 0, () => {
const record = parseInstallRecord(buildInstallRecord('abc', 'BUNDLE', ''));
expect(record !== null).assertTrue();
expect(record!.bundleSha256 === undefined).assertTrue();
expect(record!.bundleSha256).assertEqual('BUNDLE');
expect(record!.artifactSha256 === undefined).assertTrue();
});

it('refuses to build a record without a bundle digest', 0, () => {
let caught: PushyError | null = null;
try {
buildInstallRecord('abc', '', 'ARTIFACT');
} catch (e) {
caught = e as PushyError;
}
expect(caught !== null).assertTrue();
expect(caught!.code).assertEqual(ERROR_DELTA_FAILED);
});

it('derives the staging directory from the version directory', 0, () => {
expect(stagingDirectoryFor('/data/_update/abc')).assertEqual(
`/data/_update/abc${STAGING_SUFFIX}`,
Expand Down Expand Up @@ -73,7 +89,7 @@ export default function installRecordTest() {
});

it('requires the schema and version to match', 0, () => {
const record = parseInstallRecord(buildInstallRecord('abc', '', ''));
const record = parseInstallRecord(buildInstallRecord('abc', 'DIGEST', ''));
expect(isRecordForVersion(record, 'abc')).assertTrue();
expect(isRecordForVersion(record, 'def')).assertFalse();
expect(
Expand All @@ -95,14 +111,27 @@ export default function installRecordTest() {
expect(caught!.message.startsWith(`[${ERROR_SWITCH_VERSION_FAILED}] `)).assertTrue();
});

it('has nothing to verify for legacy or digest-less records', 0, () => {
it('has nothing to verify for a legacy empty record', 0, () => {
expect(expectedBundleSha256ForActivation({}, 'abc')).assertEqual('');
expect(
expectedBundleSha256ForActivation(
parseInstallRecord(buildInstallRecord('abc', '', '')),
'abc',
),
).assertEqual('');
});

it('rejects a schema 1 record without a bundle digest', 0, () => {
const records: InstallRecordData[] = [
{ schema: INSTALL_RECORD_SCHEMA, versionHash: 'abc' },
{ schema: INSTALL_RECORD_SCHEMA, versionHash: 'abc', bundleSha256: '' },
{ schema: INSTALL_RECORD_SCHEMA, versionHash: 'abc', artifactSha256: 'A' },
];
for (const record of records) {
let caught: PushyError | null = null;
try {
expectedBundleSha256ForActivation(record, 'abc');
} catch (e) {
caught = e as PushyError;
}
expect(caught !== null).assertTrue();
expect(caught!.code).assertEqual(ERROR_SWITCH_VERSION_FAILED);
expect(caught!.message.includes('record mismatch')).assertTrue();
}
});

it('returns the recorded digest for a matching record', 0, () => {
Expand Down
Loading
Loading