mirror of
https://github.com/signalapp/libsignal.git
synced 2024-09-19 19:42:19 +02:00
backup: bridge canonical serialization as ComparableBackup
This commit is contained in:
parent
854343294d
commit
66cd3f0133
3
.gitattributes
vendored
3
.gitattributes
vendored
@ -5,3 +5,6 @@ acknowledgments/acknowledgments.*.hbs merge text=auto
|
|||||||
# Treat encrypted and unencrypted message backup files as binary
|
# Treat encrypted and unencrypted message backup files as binary
|
||||||
**/*.binproto binary
|
**/*.binproto binary
|
||||||
**/*.binproto.encrypted binary
|
**/*.binproto.encrypted binary
|
||||||
|
|
||||||
|
# Avoid Windows line-endings for files compared literally.
|
||||||
|
**/*.expected.json text eol=lf
|
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -2011,6 +2011,7 @@ dependencies = [
|
|||||||
name = "libsignal-bridge-testing"
|
name = "libsignal-bridge-testing"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"displaydoc",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"jni 0.21.1",
|
"jni 0.21.1",
|
||||||
"libsignal-bridge-macros",
|
"libsignal-bridge-macros",
|
||||||
@ -2023,6 +2024,7 @@ dependencies = [
|
|||||||
"signal-neon-futures",
|
"signal-neon-futures",
|
||||||
"strum",
|
"strum",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -0,0 +1,106 @@
|
|||||||
|
//
|
||||||
|
// Copyright 2024 Signal Messenger, LLC.
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
//
|
||||||
|
|
||||||
|
package org.signal.libsignal.messagebackup;
|
||||||
|
|
||||||
|
import static org.signal.libsignal.internal.FilterExceptions.filterExceptions;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import org.signal.libsignal.internal.CalledFromNative;
|
||||||
|
import org.signal.libsignal.internal.NativeHandleGuard;
|
||||||
|
import org.signal.libsignal.internal.NativeTesting;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An in-memory representation of a backup file used to compare contents.
|
||||||
|
*
|
||||||
|
* <p>When comparing the contents of two backups:
|
||||||
|
*
|
||||||
|
* <ol>
|
||||||
|
* <li>Create a `ComparableBackup` instance for each of the inputs.
|
||||||
|
* <li>Check the `unknownFields()` value; if it's not empty, some parts of the backup weren't
|
||||||
|
* parsed and won't be compared.
|
||||||
|
* <li>Produce a canonical string for each backup with `comparableString()`.
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* Compare the canonical string representations. The diff of the canonical strings (which may be
|
||||||
|
* rather large) will show the differences between the logical content of the input backup files.
|
||||||
|
*/
|
||||||
|
public class ComparableBackup implements NativeHandleGuard.Owner {
|
||||||
|
/**
|
||||||
|
* Reads an unencrypted message backup bundle into memory for comparison.
|
||||||
|
*
|
||||||
|
* <p>Returns an error if the input cannot be read or if validation fails.
|
||||||
|
*
|
||||||
|
* @param purpose whether the input was created for device-to-device transfer or remote backup
|
||||||
|
* @param input an <code>InputStream</code> that produces the input
|
||||||
|
* @param streamLength the number of bytes each <code>InputStream</code> will produce
|
||||||
|
* @throws ValidationError with an error message if the input is invalid
|
||||||
|
* @throws IOException if the input could not be read
|
||||||
|
*/
|
||||||
|
public static ComparableBackup readUnencrypted(
|
||||||
|
MessageBackup.Purpose purpose, InputStream input, long streamLength)
|
||||||
|
throws ValidationError, IOException {
|
||||||
|
|
||||||
|
long handle =
|
||||||
|
filterExceptions(
|
||||||
|
IOException.class,
|
||||||
|
ValidationError.class,
|
||||||
|
() ->
|
||||||
|
NativeTesting.ComparableBackup_ReadUnencrypted(
|
||||||
|
input, streamLength, purpose.ordinal()));
|
||||||
|
|
||||||
|
return new ComparableBackup(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Produces a string representation of the contents.
|
||||||
|
*
|
||||||
|
* <p>The returned strings for two backups will be equal if the backups contain the same logical
|
||||||
|
* content. If two backups' strings are not equal, the diff will show what is different between
|
||||||
|
* them.
|
||||||
|
*
|
||||||
|
* @return a canonical string representation of the backup
|
||||||
|
*/
|
||||||
|
public String getComparableString() {
|
||||||
|
try (NativeHandleGuard guard = new NativeHandleGuard(this)) {
|
||||||
|
return filterExceptions(
|
||||||
|
() -> NativeTesting.ComparableBackup_GetComparableString(guard.nativeHandle()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the unrecognized protobuf fields present in the backup.
|
||||||
|
*
|
||||||
|
* <p>If the returned array is not empty, some parts of the backup were not recognized and won't
|
||||||
|
* be present in the string representation.
|
||||||
|
*
|
||||||
|
* @return information about each unknown field found in the backup
|
||||||
|
*/
|
||||||
|
public String[] getUnknownFieldMessages() {
|
||||||
|
try (NativeHandleGuard guard = new NativeHandleGuard(this)) {
|
||||||
|
return (String[])
|
||||||
|
filterExceptions(
|
||||||
|
() -> NativeTesting.ComparableBackup_GetUnknownFields(guard.nativeHandle()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@CalledFromNative
|
||||||
|
private ComparableBackup(long unsafeHandle) {
|
||||||
|
this.unsafeHandle = unsafeHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final long unsafeHandle;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
protected void finalize() {
|
||||||
|
NativeTesting.ComparableBackup_Destroy(this.unsafeHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long unsafeNativeHandleWithoutGuard() {
|
||||||
|
return this.unsafeHandle;
|
||||||
|
}
|
||||||
|
}
|
@ -33,14 +33,8 @@ public class MessageBackup {
|
|||||||
|
|
||||||
public static enum Purpose {
|
public static enum Purpose {
|
||||||
// This needs to be kept in sync with the corresponding Rust enum.
|
// This needs to be kept in sync with the corresponding Rust enum.
|
||||||
DEVICE_TRANSFER(0),
|
DEVICE_TRANSFER,
|
||||||
REMOTE_BACKUP(1);
|
REMOTE_BACKUP,
|
||||||
|
|
||||||
private final int value;
|
|
||||||
|
|
||||||
private Purpose(int value) {
|
|
||||||
this.value = value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -71,7 +65,7 @@ public class MessageBackup {
|
|||||||
ValidationError.class,
|
ValidationError.class,
|
||||||
() ->
|
() ->
|
||||||
Native.MessageBackupValidator_Validate(
|
Native.MessageBackupValidator_Validate(
|
||||||
keyGuard.nativeHandle(), first, second, streamLength, purpose.value));
|
keyGuard.nativeHandle(), first, second, streamLength, purpose.ordinal()));
|
||||||
|
|
||||||
// Rust conversion code is generating an instance of this class.
|
// Rust conversion code is generating an instance of this class.
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
|
@ -0,0 +1,44 @@
|
|||||||
|
//
|
||||||
|
// Copyright 2023 Signal Messenger, LLC.
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
//
|
||||||
|
|
||||||
|
package org.signal.libsignal.messagebackup;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertArrayEquals;
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.signal.libsignal.util.ResourceReader;
|
||||||
|
|
||||||
|
public class ComparableBackupTest {
|
||||||
|
|
||||||
|
static final MessageBackup.Purpose BACKUP_PURPOSE = MessageBackup.Purpose.REMOTE_BACKUP;
|
||||||
|
static final String CANONICAL_BACKUP_PROTO_NAME = "canonical-backup.binproto";
|
||||||
|
static final String CANONICAL_BACKUP_STRING_NAME = "canonical-backup.expected.json";
|
||||||
|
|
||||||
|
private static InputStream getCanonicalBackupInputStream() {
|
||||||
|
return ComparableBackupTest.class.getResourceAsStream(CANONICAL_BACKUP_PROTO_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void canonicalBackupString() throws IOException, ValidationError {
|
||||||
|
final long length;
|
||||||
|
try (InputStream input = getCanonicalBackupInputStream()) {
|
||||||
|
length = ResourceReader.readAll(input).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
ComparableBackup backup =
|
||||||
|
ComparableBackup.readUnencrypted(BACKUP_PURPOSE, getCanonicalBackupInputStream(), length);
|
||||||
|
assertArrayEquals(backup.getUnknownFieldMessages(), new String[] {});
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
backup.getComparableString(),
|
||||||
|
new String(
|
||||||
|
ComparableBackupTest.class
|
||||||
|
.getResourceAsStream(CANONICAL_BACKUP_STRING_NAME)
|
||||||
|
.readAllBytes()));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
../../../../../../../../../rust/message-backup/tests/res/test-cases/valid/account-data.binproto
|
@ -0,0 +1,61 @@
|
|||||||
|
{
|
||||||
|
"meta": {
|
||||||
|
"version": 1,
|
||||||
|
"backup_time": {
|
||||||
|
"secs_since_epoch": 1715636551,
|
||||||
|
"nanos_since_epoch": 0
|
||||||
|
},
|
||||||
|
"purpose": "RemoteBackup"
|
||||||
|
},
|
||||||
|
"account_data": {
|
||||||
|
"profile_key": "610291abedc34249489da39a31c9a5cd99cdd26ff58732e268e357ee0075d9d8",
|
||||||
|
"username": {
|
||||||
|
"username": "boba_fett.66",
|
||||||
|
"link": {
|
||||||
|
"color": "OLIVE",
|
||||||
|
"entropy": "65675c73d00eb01005e3bb7c4a47f296cb6554f78981238815e915d824fd2e93",
|
||||||
|
"server_id": "61c101a2-00d5-4217-89c2-0518d8497af0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"given_name": "Boba",
|
||||||
|
"family_name": "Fett",
|
||||||
|
"account_settings": {
|
||||||
|
"phone_number_sharing": "WithNobody",
|
||||||
|
"read_receipts": true,
|
||||||
|
"sealed_sender_indicators": true,
|
||||||
|
"typing_indicators": true,
|
||||||
|
"link_previews": false,
|
||||||
|
"not_discoverable_by_phone_number": true,
|
||||||
|
"prefer_contact_avatars": true,
|
||||||
|
"display_badges_on_profile": true,
|
||||||
|
"keep_muted_chats_archived": true,
|
||||||
|
"has_set_my_stories_privacy": true,
|
||||||
|
"has_viewed_onboarding_story": true,
|
||||||
|
"stories_disabled": true,
|
||||||
|
"story_view_receipts_enabled": true,
|
||||||
|
"has_seen_group_story_education_sheet": true,
|
||||||
|
"has_completed_username_onboarding": true,
|
||||||
|
"universal_expire_timer": {
|
||||||
|
"secs": 3,
|
||||||
|
"nanos": 600000000
|
||||||
|
},
|
||||||
|
"preferred_reaction_emoji": [
|
||||||
|
"🏎️"
|
||||||
|
],
|
||||||
|
"default_chat_style": null,
|
||||||
|
"custom_chat_colors": []
|
||||||
|
},
|
||||||
|
"avatar_url_path": "",
|
||||||
|
"donation_subscription": {
|
||||||
|
"subscriber_id": "ecbb68c734331a2ea333cda747c98c4553652261582b4fce5ae0dea84dce6519",
|
||||||
|
"currency_code": "USD",
|
||||||
|
"manually_canceled": true
|
||||||
|
},
|
||||||
|
"backup_subscription": null
|
||||||
|
},
|
||||||
|
"recipients": [],
|
||||||
|
"chats": [],
|
||||||
|
"ad_hoc_calls": [],
|
||||||
|
"pinned_chats": [],
|
||||||
|
"sticker_packs": []
|
||||||
|
}
|
@ -44,5 +44,10 @@ public final class NativeTesting {
|
|||||||
|
|
||||||
private NativeTesting() {}
|
private NativeTesting() {}
|
||||||
|
|
||||||
|
public static native void ComparableBackup_Destroy(long handle);
|
||||||
|
public static native String ComparableBackup_GetComparableString(long backup);
|
||||||
|
public static native Object[] ComparableBackup_GetUnknownFields(long backup);
|
||||||
|
public static native long ComparableBackup_ReadUnencrypted(InputStream stream, long len, int purpose) throws Exception;
|
||||||
|
|
||||||
public static native int test_only_fn_returns_123();
|
public static native int test_only_fn_returns_123();
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
build/**
|
build/**
|
||||||
dist/**
|
dist/**
|
||||||
Native.d.ts
|
Native.d.ts
|
||||||
|
ts/test/canonical-backup.expected.json
|
5
node/Native.d.ts
vendored
5
node/Native.d.ts
vendored
@ -192,6 +192,9 @@ export function ChatService_unauth_send_and_debug(asyncRuntime: Wrapper<TokioAsy
|
|||||||
export function CiphertextMessage_FromPlaintextContent(m: Wrapper<PlaintextContent>): CiphertextMessage;
|
export function CiphertextMessage_FromPlaintextContent(m: Wrapper<PlaintextContent>): CiphertextMessage;
|
||||||
export function CiphertextMessage_Serialize(obj: Wrapper<CiphertextMessage>): Buffer;
|
export function CiphertextMessage_Serialize(obj: Wrapper<CiphertextMessage>): Buffer;
|
||||||
export function CiphertextMessage_Type(msg: Wrapper<CiphertextMessage>): number;
|
export function CiphertextMessage_Type(msg: Wrapper<CiphertextMessage>): number;
|
||||||
|
export function ComparableBackup_GetComparableString(backup: Wrapper<ComparableBackup>): string;
|
||||||
|
export function ComparableBackup_GetUnknownFields(backup: Wrapper<ComparableBackup>): string[];
|
||||||
|
export function ComparableBackup_ReadUnencrypted(stream: InputStream, len: bigint, purpose: number): Promise<ComparableBackup>;
|
||||||
export function ConnectionManager_clear_proxy(connectionManager: Wrapper<ConnectionManager>): void;
|
export function ConnectionManager_clear_proxy(connectionManager: Wrapper<ConnectionManager>): void;
|
||||||
export function ConnectionManager_new(environment: number, userAgent: string): ConnectionManager;
|
export function ConnectionManager_new(environment: number, userAgent: string): ConnectionManager;
|
||||||
export function ConnectionManager_set_ipv6_enabled(connectionManager: Wrapper<ConnectionManager>, ipv6Enabled: boolean): void;
|
export function ConnectionManager_set_ipv6_enabled(connectionManager: Wrapper<ConnectionManager>, ipv6Enabled: boolean): void;
|
||||||
@ -553,6 +556,8 @@ interface Aes256GcmSiv { readonly __type: unique symbol; }
|
|||||||
interface CdsiLookup { readonly __type: unique symbol; }
|
interface CdsiLookup { readonly __type: unique symbol; }
|
||||||
interface Chat { readonly __type: unique symbol; }
|
interface Chat { readonly __type: unique symbol; }
|
||||||
interface CiphertextMessage { readonly __type: unique symbol; }
|
interface CiphertextMessage { readonly __type: unique symbol; }
|
||||||
|
interface ComparableBackup { readonly __type: unique symbol; }
|
||||||
|
interface ComparableBackup { readonly __type: unique symbol; }
|
||||||
interface ConnectionManager { readonly __type: unique symbol; }
|
interface ConnectionManager { readonly __type: unique symbol; }
|
||||||
interface DecryptionErrorMessage { readonly __type: unique symbol; }
|
interface DecryptionErrorMessage { readonly __type: unique symbol; }
|
||||||
interface ExpiringProfileKeyCredential { readonly __type: unique symbol; }
|
interface ExpiringProfileKeyCredential { readonly __type: unique symbol; }
|
||||||
|
@ -51,6 +51,8 @@ export enum ErrorCode {
|
|||||||
AppExpired,
|
AppExpired,
|
||||||
DeviceDelinked,
|
DeviceDelinked,
|
||||||
|
|
||||||
|
BackupValidation,
|
||||||
|
|
||||||
Cancelled,
|
Cancelled,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -240,6 +242,11 @@ export type SvrRestoreFailedError = LibSignalErrorCommon & {
|
|||||||
readonly triesRemaining: number;
|
readonly triesRemaining: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type BackupValidationError = LibSignalErrorCommon & {
|
||||||
|
code: ErrorCode.BackupValidation;
|
||||||
|
readonly unknownFields: ReadonlyArray<string>;
|
||||||
|
};
|
||||||
|
|
||||||
export type CancellationError = LibSignalErrorCommon & {
|
export type CancellationError = LibSignalErrorCommon & {
|
||||||
code: ErrorCode.Cancelled;
|
code: ErrorCode.Cancelled;
|
||||||
};
|
};
|
||||||
|
@ -99,3 +99,68 @@ export async function validate(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An in-memory representation of a backup file used to compare contents.
|
||||||
|
*
|
||||||
|
* When comparing the contents of two backups:
|
||||||
|
* 1. Create a `ComparableBackup` instance for each of the inputs.
|
||||||
|
* 2. Check the `unknownFields()` value; if it's not empty, some parts of the
|
||||||
|
* backup weren't parsed and won't be compared.
|
||||||
|
* 3. Produce a canonical string for each backup with `comparableString()`.
|
||||||
|
* 4. Compare the canonical string representations.
|
||||||
|
*
|
||||||
|
* The diff of the canonical strings (which may be rather large) will show the
|
||||||
|
* differences between the logical content of the input backup files.
|
||||||
|
*/
|
||||||
|
export class ComparableBackup {
|
||||||
|
readonly _nativeHandle: Native.ComparableBackup;
|
||||||
|
constructor(handle: Native.ComparableBackup) {
|
||||||
|
this._nativeHandle = handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read an unencrypted backup file into memory for comparison.
|
||||||
|
*
|
||||||
|
* @param purpose Whether the backup is intended for device-to-device transfer or remote storage.
|
||||||
|
* @param input An input stream that reads the backup contents.
|
||||||
|
* @param length The exact length of the input stream.
|
||||||
|
* @returns The in-memory representation.
|
||||||
|
* @throws BackupValidationError If an IO error occurs or the input is invalid.
|
||||||
|
*/
|
||||||
|
public static async fromUnencrypted(
|
||||||
|
purpose: Purpose,
|
||||||
|
input: InputStream,
|
||||||
|
length: bigint
|
||||||
|
): Promise<ComparableBackup> {
|
||||||
|
const handle = await Native.ComparableBackup_ReadUnencrypted(
|
||||||
|
input,
|
||||||
|
length,
|
||||||
|
purpose
|
||||||
|
);
|
||||||
|
return new ComparableBackup(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Produces a string representation of the contents.
|
||||||
|
*
|
||||||
|
* The returned strings for two backups will be equal if the backups contain
|
||||||
|
* the same logical content. If two backups' strings are not equal, the diff
|
||||||
|
* will show what is different between them.
|
||||||
|
*
|
||||||
|
* @returns a canonical string representation of the backup
|
||||||
|
*/
|
||||||
|
public comparableString(): string {
|
||||||
|
return Native.ComparableBackup_GetComparableString(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unrecognized protobuf fields present in the backup.
|
||||||
|
*
|
||||||
|
* If this is not empty, some parts of the backup were not recognized and
|
||||||
|
* won't be present in the string representation.
|
||||||
|
*/
|
||||||
|
public get unknownFields(): Array<string> {
|
||||||
|
return Native.ComparableBackup_GetUnknownFields(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -60,3 +60,25 @@ describe('MessageBackup', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('ComparableBackup', () => {
|
||||||
|
describe('exampleBackup', () => {
|
||||||
|
const input = fs.readFileSync(
|
||||||
|
path.join(__dirname, '../../ts/test/canonical-backup.binproto')
|
||||||
|
);
|
||||||
|
|
||||||
|
it('stringifies to the expected value', async () => {
|
||||||
|
const comparable = await MessageBackup.ComparableBackup.fromUnencrypted(
|
||||||
|
MessageBackup.Purpose.RemoteBackup,
|
||||||
|
new Uint8ArrayInputStream(input),
|
||||||
|
BigInt(input.length)
|
||||||
|
);
|
||||||
|
|
||||||
|
const expectedOutput = fs.readFileSync(
|
||||||
|
path.join(__dirname, '../../ts/test/canonical-backup.expected.json')
|
||||||
|
);
|
||||||
|
const output = comparable.comparableString();
|
||||||
|
assert.equal(output, new String(expectedOutput));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
1
node/ts/test/canonical-backup.binproto
Symbolic link
1
node/ts/test/canonical-backup.binproto
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../../../rust/message-backup/tests/res/test-cases/valid/account-data.binproto
|
1
node/ts/test/canonical-backup.expected.json
Symbolic link
1
node/ts/test/canonical-backup.expected.json
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../../../java/client/src/test/resources/org/signal/libsignal/messagebackup/canonical-backup.expected.json
|
@ -38,6 +38,8 @@ prefix = "Signal"
|
|||||||
renaming_overrides_prefixing = true
|
renaming_overrides_prefixing = true
|
||||||
|
|
||||||
[export.rename]
|
[export.rename]
|
||||||
|
"FfiInputStreamStruct" = "SignalInputStream"
|
||||||
|
|
||||||
# Avoid double-prefixing these
|
# Avoid double-prefixing these
|
||||||
"SignalFfiError" = "SignalFfiError"
|
"SignalFfiError" = "SignalFfiError"
|
||||||
"SignalErrorCode" = "SignalErrorCode"
|
"SignalErrorCode" = "SignalErrorCode"
|
||||||
@ -52,7 +54,7 @@ args = "horizontal"
|
|||||||
[parse]
|
[parse]
|
||||||
parse_deps = true
|
parse_deps = true
|
||||||
include = []
|
include = []
|
||||||
extra_bindings = ["libsignal-bridge-testing"]
|
extra_bindings = ["libsignal-bridge-testing", "libsignal-bridge-types"]
|
||||||
|
|
||||||
[parse.expand]
|
[parse.expand]
|
||||||
crates = ["libsignal-bridge-testing"]
|
crates = ["libsignal-bridge-testing"]
|
||||||
|
@ -153,6 +153,27 @@ pub unsafe extern "C" fn signal_error_get_tries_remaining(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn signal_error_get_unknown_fields(
|
||||||
|
err: *const SignalFfiError,
|
||||||
|
out: *mut StringArray,
|
||||||
|
) -> *mut SignalFfiError {
|
||||||
|
let err = AssertUnwindSafe(err);
|
||||||
|
run_ffi_safe(|| {
|
||||||
|
let err = err.as_ref().ok_or(NullPointerError)?;
|
||||||
|
let value = err
|
||||||
|
.provide_unknown_fields()
|
||||||
|
.map_err(|_| {
|
||||||
|
SignalProtocolError::InvalidArgument(format!(
|
||||||
|
"cannot get unknown_fields from error ({})",
|
||||||
|
err
|
||||||
|
))
|
||||||
|
})?
|
||||||
|
.into_boxed_slice();
|
||||||
|
write_result_to(out, value)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn signal_error_free(err: *mut SignalFfiError) {
|
pub unsafe extern "C" fn signal_error_free(err: *mut SignalFfiError) {
|
||||||
if !err.is_null() {
|
if !err.is_null() {
|
||||||
|
@ -13,12 +13,14 @@ license = "AGPL-3.0-only"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
libsignal-bridge-macros = { path = "../macros" }
|
libsignal-bridge-macros = { path = "../macros" }
|
||||||
libsignal-bridge-types = { path = "../types" }
|
libsignal-bridge-types = { path = "../types" }
|
||||||
libsignal-message-backup = { path = "../../../message-backup" }
|
libsignal-message-backup = { path = "../../../message-backup", features = ["json"] }
|
||||||
|
|
||||||
|
displaydoc = "0.2"
|
||||||
futures-util = "0.3.7"
|
futures-util = "0.3.7"
|
||||||
paste = "1.0"
|
paste = "1.0"
|
||||||
scopeguard = "1.0"
|
scopeguard = "1.0"
|
||||||
thiserror = "1.0.50"
|
thiserror = "1.0.50"
|
||||||
|
tokio = "1"
|
||||||
|
|
||||||
jni = { version = "0.21", package = "jni", optional = true }
|
jni = { version = "0.21", package = "jni", optional = true }
|
||||||
linkme = { version = "0.3.9", optional = true }
|
linkme = { version = "0.3.9", optional = true }
|
||||||
@ -29,5 +31,5 @@ strum = { version = "0.26", features = ["derive"] }
|
|||||||
[features]
|
[features]
|
||||||
ffi = ["libsignal-bridge-types/ffi"]
|
ffi = ["libsignal-bridge-types/ffi"]
|
||||||
jni = ["dep:jni", "libsignal-bridge-types/jni"]
|
jni = ["dep:jni", "libsignal-bridge-types/jni"]
|
||||||
node = ["dep:linkme", "libsignal-bridge-types/node"]
|
node = ["dep:linkme", "dep:neon", "libsignal-bridge-types/node"]
|
||||||
signal-media = ["libsignal-bridge-types/signal-media"]
|
signal-media = ["libsignal-bridge-types/signal-media"]
|
||||||
|
@ -17,3 +17,5 @@ pub use libsignal_bridge_types::node;
|
|||||||
pub fn test_only_fn_returns_123() -> u32 {
|
pub fn test_only_fn_returns_123() -> u32 {
|
||||||
123
|
123
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub mod message_backup;
|
||||||
|
58
rust/bridge/shared/testing/src/message_backup.rs
Normal file
58
rust/bridge/shared/testing/src/message_backup.rs
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
//
|
||||||
|
// Copyright 2024 Signal Messenger, LLC.
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
//
|
||||||
|
|
||||||
|
use libsignal_bridge_macros::*;
|
||||||
|
use libsignal_bridge_types::io::{AsyncInput, InputStream};
|
||||||
|
use libsignal_bridge_types::support::*;
|
||||||
|
use libsignal_bridge_types::*;
|
||||||
|
use libsignal_message_backup::backup::Purpose;
|
||||||
|
use libsignal_message_backup::{BackupReader, ReadError, ReadResult};
|
||||||
|
|
||||||
|
pub struct ComparableBackup {
|
||||||
|
pub backup: libsignal_message_backup::backup::serialize::Backup,
|
||||||
|
pub found_unknown_fields: Vec<libsignal_message_backup::FoundUnknownField>,
|
||||||
|
}
|
||||||
|
|
||||||
|
bridge_as_handle!(ComparableBackup);
|
||||||
|
bridge_handle_fns!(ComparableBackup, clone = false);
|
||||||
|
|
||||||
|
#[bridge_fn]
|
||||||
|
async fn ComparableBackup_ReadUnencrypted(
|
||||||
|
stream: &mut dyn InputStream,
|
||||||
|
len: u64,
|
||||||
|
purpose: AsType<Purpose, u8>,
|
||||||
|
) -> Result<ComparableBackup, ReadError> {
|
||||||
|
let reader = BackupReader::new_unencrypted(AsyncInput::new(stream, len), purpose.into_inner());
|
||||||
|
|
||||||
|
let ReadResult {
|
||||||
|
result,
|
||||||
|
found_unknown_fields,
|
||||||
|
} = reader.read_all().await;
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(backup) => Ok(ComparableBackup {
|
||||||
|
backup: backup.into(),
|
||||||
|
found_unknown_fields,
|
||||||
|
}),
|
||||||
|
Err(error) => Err(ReadError {
|
||||||
|
error,
|
||||||
|
found_unknown_fields,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[bridge_fn]
|
||||||
|
fn ComparableBackup_GetComparableString(backup: &ComparableBackup) -> String {
|
||||||
|
backup.backup.to_string_pretty()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[bridge_fn]
|
||||||
|
fn ComparableBackup_GetUnknownFields(backup: &ComparableBackup) -> Box<[String]> {
|
||||||
|
backup
|
||||||
|
.found_unknown_fields
|
||||||
|
.iter()
|
||||||
|
.map(ToString::to_string)
|
||||||
|
.collect()
|
||||||
|
}
|
@ -101,6 +101,8 @@ pub enum SignalErrorCode {
|
|||||||
|
|
||||||
AppExpired = 160,
|
AppExpired = 160,
|
||||||
DeviceDeregistered = 161,
|
DeviceDeregistered = 161,
|
||||||
|
|
||||||
|
BackupValidation = 170,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait UpcastAsAny {
|
pub trait UpcastAsAny {
|
||||||
@ -131,6 +133,9 @@ pub trait FfiError: UpcastAsAny + fmt::Debug + Send + 'static {
|
|||||||
fn provide_tries_remaining(&self) -> Result<u32, WrongErrorKind> {
|
fn provide_tries_remaining(&self) -> Result<u32, WrongErrorKind> {
|
||||||
Err(WrongErrorKind)
|
Err(WrongErrorKind)
|
||||||
}
|
}
|
||||||
|
fn provide_unknown_fields(&self) -> Result<Vec<String>, WrongErrorKind> {
|
||||||
|
Err(WrongErrorKind)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The top-level error type (opaquely) returned to C clients when something goes wrong.
|
/// The top-level error type (opaquely) returned to C clients when something goes wrong.
|
||||||
@ -627,6 +632,24 @@ impl FfiError for signal_media::sanitize::webp::Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl FfiError for libsignal_message_backup::ReadError {
|
||||||
|
fn describe(&self) -> String {
|
||||||
|
self.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn code(&self) -> SignalErrorCode {
|
||||||
|
SignalErrorCode::BackupValidation
|
||||||
|
}
|
||||||
|
|
||||||
|
fn provide_unknown_fields(&self) -> Result<Vec<String>, WrongErrorKind> {
|
||||||
|
Ok(self
|
||||||
|
.found_unknown_fields
|
||||||
|
.iter()
|
||||||
|
.map(ToString::to_string)
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl FfiError for NullPointerError {
|
impl FfiError for NullPointerError {
|
||||||
fn describe(&self) -> String {
|
fn describe(&self) -> String {
|
||||||
"null pointer".to_owned()
|
"null pointer".to_owned()
|
||||||
|
@ -1456,6 +1456,9 @@ macro_rules! jni_result_type {
|
|||||||
(MessageBackupValidationOutcome) => {
|
(MessageBackupValidationOutcome) => {
|
||||||
::jni::objects::JObject<'local>
|
::jni::objects::JObject<'local>
|
||||||
};
|
};
|
||||||
|
(MessageBackupReadOutcome) => {
|
||||||
|
::jni::objects::JObject<'local>
|
||||||
|
};
|
||||||
(LookupResponse) => {
|
(LookupResponse) => {
|
||||||
::jni::objects::JObject<'local>
|
::jni::objects::JObject<'local>
|
||||||
};
|
};
|
||||||
|
@ -50,6 +50,7 @@ pub enum SignalJniError {
|
|||||||
ChatService(ChatServiceError),
|
ChatService(ChatServiceError),
|
||||||
InvalidUri(InvalidUri),
|
InvalidUri(InvalidUri),
|
||||||
ConnectTimedOut,
|
ConnectTimedOut,
|
||||||
|
BackupValidation(#[from] libsignal_message_backup::ReadError),
|
||||||
Bridge(BridgeLayerError),
|
Bridge(BridgeLayerError),
|
||||||
TestingError {
|
TestingError {
|
||||||
exception_class: ClassName<'static>,
|
exception_class: ClassName<'static>,
|
||||||
@ -97,6 +98,7 @@ impl fmt::Display for SignalJniError {
|
|||||||
SignalJniError::InvalidUri(e) => write!(f, "{}", e),
|
SignalJniError::InvalidUri(e) => write!(f, "{}", e),
|
||||||
SignalJniError::WebSocket(e) => write!(f, "{e}"),
|
SignalJniError::WebSocket(e) => write!(f, "{e}"),
|
||||||
SignalJniError::ConnectTimedOut => write!(f, "connect timed out"),
|
SignalJniError::ConnectTimedOut => write!(f, "connect timed out"),
|
||||||
|
SignalJniError::BackupValidation(e) => write!(f, "{}", e),
|
||||||
SignalJniError::Svr3(e) => write!(f, "{}", e),
|
SignalJniError::Svr3(e) => write!(f, "{}", e),
|
||||||
SignalJniError::Bridge(e) => write!(f, "{}", e),
|
SignalJniError::Bridge(e) => write!(f, "{}", e),
|
||||||
SignalJniError::TestingError { exception_class } => {
|
SignalJniError::TestingError { exception_class } => {
|
||||||
|
@ -321,6 +321,33 @@ impl<'env> ConsumableException<'env> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SignalJniError::BackupValidation(ref err) => {
|
||||||
|
// TODO replace with try block once that is stabilized.
|
||||||
|
let throwable = (|| {
|
||||||
|
let libsignal_message_backup::ReadError {
|
||||||
|
error,
|
||||||
|
found_unknown_fields,
|
||||||
|
} = err;
|
||||||
|
|
||||||
|
let message = error.to_string().convert_into(env)?;
|
||||||
|
let found_unknown_fields = found_unknown_fields
|
||||||
|
.iter()
|
||||||
|
.map(|field| field.to_string())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.into_boxed_slice()
|
||||||
|
.convert_into(env)?;
|
||||||
|
new_instance(
|
||||||
|
env,
|
||||||
|
ClassName("org.signal.libsignal.messagebackup.ValidationError"),
|
||||||
|
jni_args!((message => java.lang.String, found_unknown_fields => [java.lang.String]) -> void),
|
||||||
|
)
|
||||||
|
})();
|
||||||
|
return ConsumableException {
|
||||||
|
throwable: throwable.map(Into::into),
|
||||||
|
error: error.into(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
SignalJniError::Bridge(BridgeLayerError::NullPointer(_)) => {
|
SignalJniError::Bridge(BridgeLayerError::NullPointer(_)) => {
|
||||||
(ClassName("java.lang.NullPointerException"), error)
|
(ClassName("java.lang.NullPointerException"), error)
|
||||||
}
|
}
|
||||||
|
@ -58,5 +58,11 @@ pub struct MessageBackupValidationOutcome {
|
|||||||
pub error_message: Option<String>,
|
pub error_message: Option<String>,
|
||||||
pub found_unknown_fields: Vec<FoundUnknownField>,
|
pub found_unknown_fields: Vec<FoundUnknownField>,
|
||||||
}
|
}
|
||||||
#[cfg(feature = "ffi")]
|
bridge_as_handle!(MessageBackupValidationOutcome, jni = false, node = false);
|
||||||
ffi_bridge_as_handle!(MessageBackupValidationOutcome);
|
|
||||||
|
pub struct ComparableBackup {
|
||||||
|
pub backup: libsignal_message_backup::backup::serialize::Backup,
|
||||||
|
pub found_unknown_fields: Vec<FoundUnknownField>,
|
||||||
|
}
|
||||||
|
|
||||||
|
bridge_as_handle!(ComparableBackup);
|
||||||
|
@ -16,6 +16,7 @@ use std::ops::{Deref, DerefMut, RangeInclusive};
|
|||||||
use std::slice;
|
use std::slice;
|
||||||
|
|
||||||
use crate::io::{InputStream, SyncInputStream};
|
use crate::io::{InputStream, SyncInputStream};
|
||||||
|
use crate::message_backup::MessageBackupValidationOutcome;
|
||||||
use crate::net::chat::{MakeChatListener, ResponseAndDebugInfo};
|
use crate::net::chat::{MakeChatListener, ResponseAndDebugInfo};
|
||||||
use crate::node::chat::NodeMakeChatListener;
|
use crate::node::chat::NodeMakeChatListener;
|
||||||
use crate::support::{extend_lifetime, Array, AsType, FixedLengthBincodeSerializable, Serialized};
|
use crate::support::{extend_lifetime, Array, AsType, FixedLengthBincodeSerializable, Serialized};
|
||||||
@ -889,7 +890,7 @@ impl<'a> ResultTypeInfo<'a> for () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> ResultTypeInfo<'a> for crate::message_backup::MessageBackupValidationOutcome {
|
impl<'a> ResultTypeInfo<'a> for MessageBackupValidationOutcome {
|
||||||
type ResultType = JsObject;
|
type ResultType = JsObject;
|
||||||
|
|
||||||
fn convert_into(self, cx: &mut impl Context<'a>) -> JsResult<'a, Self::ResultType> {
|
fn convert_into(self, cx: &mut impl Context<'a>) -> JsResult<'a, Self::ResultType> {
|
||||||
@ -898,8 +899,7 @@ impl<'a> ResultTypeInfo<'a> for crate::message_backup::MessageBackupValidationOu
|
|||||||
found_unknown_fields,
|
found_unknown_fields,
|
||||||
} = self;
|
} = self;
|
||||||
let error_message = error_message.convert_into(cx)?;
|
let error_message = error_message.convert_into(cx)?;
|
||||||
let unknown_field_messages =
|
let unknown_field_messages = found_unknown_fields.as_slice().convert_into(cx)?;
|
||||||
make_array(cx, found_unknown_fields.into_iter().map(|s| s.to_string()))?;
|
|
||||||
|
|
||||||
let obj = JsObject::new(cx);
|
let obj = JsObject::new(cx);
|
||||||
obj.set(cx, "errorMessage", error_message)?;
|
obj.set(cx, "errorMessage", error_message)?;
|
||||||
@ -909,6 +909,14 @@ impl<'a> ResultTypeInfo<'a> for crate::message_backup::MessageBackupValidationOu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'a> ResultTypeInfo<'a> for &[libsignal_message_backup::FoundUnknownField] {
|
||||||
|
type ResultType = JsArray;
|
||||||
|
|
||||||
|
fn convert_into(self, cx: &mut impl Context<'a>) -> JsResult<'a, Self::ResultType> {
|
||||||
|
make_array(cx, self.iter().map(ToString::to_string))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<'a, T: Value> ResultTypeInfo<'a> for Handle<'a, T> {
|
impl<'a, T: Value> ResultTypeInfo<'a> for Handle<'a, T> {
|
||||||
type ResultType = T;
|
type ResultType = T;
|
||||||
fn convert_into(self, _cx: &mut impl Context<'a>) -> NeonResult<Handle<'a, Self::ResultType>> {
|
fn convert_into(self, _cx: &mut impl Context<'a>) -> NeonResult<Handle<'a, Self::ResultType>> {
|
||||||
@ -1454,7 +1462,7 @@ pub fn clone_from_array_of_wrappers<'a, T: BridgeHandle<Strategy = Mutable<T>> +
|
|||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! node_bridge_as_handle {
|
macro_rules! node_bridge_as_handle {
|
||||||
( $typ:ty as false $(, $($_:tt)*)? ) => {};
|
( $typ:ty as false $(, $($_:tt)*)? ) => {};
|
||||||
( $typ:ty as $node_name:ident ) => {
|
( $typ:ty as $node_name:ident $(, mut = false)? ) => {
|
||||||
::paste::paste! {
|
::paste::paste! {
|
||||||
#[doc = "ts: interface " $typ " { readonly __type: unique symbol; }"]
|
#[doc = "ts: interface " $typ " { readonly __type: unique symbol; }"]
|
||||||
impl node::BridgeHandle for $typ {
|
impl node::BridgeHandle for $typ {
|
||||||
|
@ -542,6 +542,38 @@ impl SignalNodeError for CancellationError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl SignalNodeError for libsignal_message_backup::ReadError {
|
||||||
|
fn throw<'a>(
|
||||||
|
self,
|
||||||
|
cx: &mut impl Context<'a>,
|
||||||
|
module: Handle<'a, JsObject>,
|
||||||
|
operation_name: &str,
|
||||||
|
) -> JsResult<'a, JsValue> {
|
||||||
|
let libsignal_message_backup::ReadError {
|
||||||
|
error,
|
||||||
|
found_unknown_fields,
|
||||||
|
} = self;
|
||||||
|
let message = error.to_string();
|
||||||
|
let props = cx.empty_object();
|
||||||
|
let unknown_field_messages = found_unknown_fields.convert_into(cx)?;
|
||||||
|
props.set(cx, "unknownFields", unknown_field_messages)?;
|
||||||
|
match new_js_error(
|
||||||
|
cx,
|
||||||
|
module,
|
||||||
|
Some("BackupValidation"),
|
||||||
|
&message,
|
||||||
|
operation_name,
|
||||||
|
Some(props),
|
||||||
|
) {
|
||||||
|
Some(error) => cx.throw(error),
|
||||||
|
None => {
|
||||||
|
// Make sure we still throw something.
|
||||||
|
cx.throw_error(&message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Represents an error returned by a callback.
|
/// Represents an error returned by a callback.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct CallbackError {
|
struct CallbackError {
|
||||||
|
@ -57,6 +57,30 @@ pub struct ReadResult<B> {
|
|||||||
pub found_unknown_fields: Vec<FoundUnknownField>,
|
pub found_unknown_fields: Vec<FoundUnknownField>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
#[must_use]
|
||||||
|
pub struct ReadError {
|
||||||
|
pub error: Error,
|
||||||
|
pub found_unknown_fields: Vec<FoundUnknownField>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for ReadError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let Self {
|
||||||
|
error,
|
||||||
|
found_unknown_fields,
|
||||||
|
} = self;
|
||||||
|
write!(f, "{error} (with ")?;
|
||||||
|
if found_unknown_fields.is_empty() {
|
||||||
|
write!(f, "no unknown fields")?;
|
||||||
|
} else {
|
||||||
|
write!(f, "unknown fields: ")?;
|
||||||
|
f.debug_list().entries(found_unknown_fields).finish()?;
|
||||||
|
}
|
||||||
|
write!(f, ")")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub struct FoundUnknownField {
|
pub struct FoundUnknownField {
|
||||||
pub frame_index: usize,
|
pub frame_index: usize,
|
||||||
|
@ -61,6 +61,7 @@ public enum SignalError: Error {
|
|||||||
case chatServiceInactive(String)
|
case chatServiceInactive(String)
|
||||||
case appExpired(String)
|
case appExpired(String)
|
||||||
case deviceDeregistered(String)
|
case deviceDeregistered(String)
|
||||||
|
case backupValidation(unknownFields: [String], message: String)
|
||||||
|
|
||||||
case unknown(UInt32, String)
|
case unknown(UInt32, String)
|
||||||
}
|
}
|
||||||
@ -201,6 +202,11 @@ internal func checkError(_ error: SignalFfiErrorRef?) throws {
|
|||||||
throw SignalError.appExpired(errStr)
|
throw SignalError.appExpired(errStr)
|
||||||
case SignalErrorCodeDeviceDeregistered:
|
case SignalErrorCodeDeviceDeregistered:
|
||||||
throw SignalError.deviceDeregistered(errStr)
|
throw SignalError.deviceDeregistered(errStr)
|
||||||
|
case SignalErrorCodeBackupValidation:
|
||||||
|
let unknownFields = try invokeFnReturningStringArray {
|
||||||
|
signal_error_get_unknown_fields(error, $0)
|
||||||
|
}
|
||||||
|
throw SignalError.backupValidation(unknownFields: unknownFields, message: errStr)
|
||||||
default:
|
default:
|
||||||
throw SignalError.unknown(errType, errStr)
|
throw SignalError.unknown(errType, errStr)
|
||||||
}
|
}
|
||||||
|
@ -65,6 +65,75 @@ public func validateMessageBackup(
|
|||||||
return outcome.unknownFields
|
return outcome.unknownFields
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An in-memory representation of a backup file used to compare contents.
|
||||||
|
///
|
||||||
|
/// When comparing the contents of two backups:
|
||||||
|
/// 1. Create a `ComparableBackup` instance for each of the inputs.
|
||||||
|
/// 2. Check the `unknownFields()` value; if it's not empty, some parts of the
|
||||||
|
/// backup weren't parsed and won't be compared.
|
||||||
|
/// 3. Produce a canonical string for each backup with `comparableString()`.
|
||||||
|
/// 4. Compare the canonical string representations.
|
||||||
|
///
|
||||||
|
/// The diff of the canonical strings (which may be rather large) will show the
|
||||||
|
/// differences between the logical content of the input backup files.
|
||||||
|
public class ComparableBackup: NativeHandleOwner {
|
||||||
|
/// Reads an unencrypted backup file into memory for comparison.
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - purpose: Whether the backup is intended for transfer or remote storage.
|
||||||
|
/// - length: The exact length of the backup file, in bytes.
|
||||||
|
/// - stream: An InputStream that produces the backup contents.
|
||||||
|
///
|
||||||
|
/// - Throws:
|
||||||
|
/// - `SignalError.ioError`: If an IO error on the input occurs.
|
||||||
|
/// - `SignalError.backupValidation`: If validation of the input fails.
|
||||||
|
public convenience init(purpose: MessageBackupPurpose, length: UInt64, stream: SignalInputStream) throws {
|
||||||
|
var handle: OpaquePointer?
|
||||||
|
try checkError(
|
||||||
|
try withInputStream(stream) { stream in
|
||||||
|
signal_comparable_backup_read_unencrypted(&handle, stream, length, purpose.rawValue)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.init(owned: handle!)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unrecognized protobuf fields present in the backup.
|
||||||
|
///
|
||||||
|
/// If this is not empty, some parts of the backup were not recognized and
|
||||||
|
/// won't be present in the string representation.
|
||||||
|
public var unknownFields: MessageBackupUnknownFields {
|
||||||
|
let fields = failOnError {
|
||||||
|
try self.withNativeHandle { result in
|
||||||
|
try invokeFnReturningStringArray {
|
||||||
|
signal_comparable_backup_get_unknown_fields($0, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return MessageBackupUnknownFields(fields: fields)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Produces a string representation of the contents.
|
||||||
|
///
|
||||||
|
/// The returned strings for two backups will be equal if the backups
|
||||||
|
/// contain the same logical content. If two backups' strings are not equal,
|
||||||
|
/// the diff will show what is different between them.
|
||||||
|
///
|
||||||
|
/// - Returns: a canonical string representation of the backup.
|
||||||
|
public func comparableString() -> String {
|
||||||
|
return failOnError {
|
||||||
|
try self.withNativeHandle { result in
|
||||||
|
try invokeFnReturningString {
|
||||||
|
signal_comparable_backup_get_comparable_string($0, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override internal class func destroyNativeHandle(_ handle: OpaquePointer) -> SignalFfiErrorRef? {
|
||||||
|
signal_comparable_backup_destroy(handle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The outcome of a failed validation attempt.
|
/// The outcome of a failed validation attempt.
|
||||||
public struct MessageBackupValidationError: Error {
|
public struct MessageBackupValidationError: Error {
|
||||||
/// The human-readable error that caused validation to fail.
|
/// The human-readable error that caused validation to fail.
|
||||||
|
@ -196,6 +196,7 @@ typedef enum {
|
|||||||
SignalErrorCodeSvrRestoreFailed = 151,
|
SignalErrorCodeSvrRestoreFailed = 151,
|
||||||
SignalErrorCodeAppExpired = 160,
|
SignalErrorCodeAppExpired = 160,
|
||||||
SignalErrorCodeDeviceDeregistered = 161,
|
SignalErrorCodeDeviceDeregistered = 161,
|
||||||
|
SignalErrorCodeBackupValidation = 170,
|
||||||
} SignalErrorCode;
|
} SignalErrorCode;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -376,6 +377,8 @@ typedef struct {
|
|||||||
SignalOwnedBufferOfusize lengths;
|
SignalOwnedBufferOfusize lengths;
|
||||||
} SignalBytestringArray;
|
} SignalBytestringArray;
|
||||||
|
|
||||||
|
typedef SignalBytestringArray SignalStringArray;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
const unsigned char *base;
|
const unsigned char *base;
|
||||||
size_t length;
|
size_t length;
|
||||||
@ -660,8 +663,6 @@ typedef struct {
|
|||||||
|
|
||||||
typedef SignalFfiChatListenerStruct SignalFfiMakeChatListenerStruct;
|
typedef SignalFfiChatListenerStruct SignalFfiMakeChatListenerStruct;
|
||||||
|
|
||||||
typedef SignalBytestringArray SignalStringArray;
|
|
||||||
|
|
||||||
typedef int (*SignalRead)(void *ctx, uint8_t *buf, size_t buf_len, size_t *amount_read);
|
typedef int (*SignalRead)(void *ctx, uint8_t *buf, size_t buf_len, size_t *amount_read);
|
||||||
|
|
||||||
typedef int (*SignalSkip)(void *ctx, uint64_t amount);
|
typedef int (*SignalSkip)(void *ctx, uint64_t amount);
|
||||||
@ -760,6 +761,8 @@ SignalFfiError *signal_error_get_retry_after_seconds(const SignalFfiError *err,
|
|||||||
|
|
||||||
SignalFfiError *signal_error_get_tries_remaining(const SignalFfiError *err, uint32_t *out);
|
SignalFfiError *signal_error_get_tries_remaining(const SignalFfiError *err, uint32_t *out);
|
||||||
|
|
||||||
|
SignalFfiError *signal_error_get_unknown_fields(const SignalFfiError *err, SignalStringArray *out);
|
||||||
|
|
||||||
void signal_error_free(SignalFfiError *err);
|
void signal_error_free(SignalFfiError *err);
|
||||||
|
|
||||||
SignalFfiError *signal_identitykeypair_deserialize(SignalPrivateKey **private_key, SignalPublicKey **public_key, SignalBorrowedBuffer input);
|
SignalFfiError *signal_identitykeypair_deserialize(SignalPrivateKey **private_key, SignalPublicKey **public_key, SignalBorrowedBuffer input);
|
||||||
|
@ -16,6 +16,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include "signal_ffi.h"
|
#include "signal_ffi.h"
|
||||||
|
|
||||||
|
typedef struct SignalComparableBackup SignalComparableBackup;
|
||||||
|
|
||||||
SignalFfiError *signal_test_only_fn_returns_123(uint32_t *out);
|
SignalFfiError *signal_test_only_fn_returns_123(uint32_t *out);
|
||||||
|
|
||||||
|
SignalFfiError *signal_comparable_backup_destroy(SignalComparableBackup *p);
|
||||||
|
|
||||||
|
SignalFfiError *signal_comparable_backup_read_unencrypted(SignalComparableBackup **out, const SignalInputStream *stream, uint64_t len, uint8_t purpose);
|
||||||
|
|
||||||
|
SignalFfiError *signal_comparable_backup_get_comparable_string(const char **out, const SignalComparableBackup *backup);
|
||||||
|
|
||||||
|
SignalFfiError *signal_comparable_backup_get_unknown_fields(SignalStringArray *out, const SignalComparableBackup *backup);
|
||||||
|
|
||||||
#endif /* SIGNAL_FFI_TESTING_H_ */
|
#endif /* SIGNAL_FFI_TESTING_H_ */
|
||||||
|
@ -63,6 +63,15 @@ class MessageBackupTests: TestCaseBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testComparableBackup() throws {
|
||||||
|
let bytes = readResource(forName: "canonical-backup.binproto")
|
||||||
|
let backup = try ComparableBackup(purpose: .remoteBackup, length: UInt64(bytes.count), stream: SignalInputStreamAdapter(bytes))
|
||||||
|
let comparableString = backup.comparableString()
|
||||||
|
|
||||||
|
let expected = String(decoding: readResource(forName: "canonical-backup.expected.json"), as: UTF8.self)
|
||||||
|
XCTAssertEqual(comparableString, expected)
|
||||||
|
}
|
||||||
|
|
||||||
static func validateBackup(bytes: some Collection<UInt8>) throws -> MessageBackupUnknownFields {
|
static func validateBackup(bytes: some Collection<UInt8>) throws -> MessageBackupUnknownFields {
|
||||||
try validateMessageBackup(key: MessageBackupKey.testKey(), purpose: .remoteBackup, length: UInt64(bytes.count), makeStream: { SignalInputStreamAdapter(bytes) })
|
try validateMessageBackup(key: MessageBackupKey.testKey(), purpose: .remoteBackup, length: UInt64(bytes.count), makeStream: { SignalInputStreamAdapter(bytes) })
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
../../../../rust/message-backup/tests/res/test-cases/valid/account-data.binproto
|
@ -0,0 +1 @@
|
|||||||
|
../../../../java/client/src/test/resources/org/signal/libsignal/messagebackup/canonical-backup.expected.json
|
Loading…
Reference in New Issue
Block a user