Add support for handling unknown protobuf fields.

master
Greyson Parrelli 2020-08-20 16:05:50 -04:00 committed by Alex Hart
parent ffcb90da52
commit 2cf9eb69eb
15 changed files with 631 additions and 47 deletions

View File

@ -120,6 +120,7 @@ public class RecipientDatabase extends Database {
private static final String PROFILE_FAMILY_NAME = "profile_family_name"; private static final String PROFILE_FAMILY_NAME = "profile_family_name";
private static final String PROFILE_JOINED_NAME = "profile_joined_name"; private static final String PROFILE_JOINED_NAME = "profile_joined_name";
private static final String MENTION_SETTING = "mention_setting"; private static final String MENTION_SETTING = "mention_setting";
private static final String STORAGE_PROTO = "storage_proto";
public static final String SEARCH_PROFILE_NAME = "search_signal_profile"; public static final String SEARCH_PROFILE_NAME = "search_signal_profile";
private static final String SORT_NAME = "sort_name"; private static final String SORT_NAME = "sort_name";
@ -150,7 +151,8 @@ public class RecipientDatabase extends Database {
private static final String[] MENTION_SEARCH_PROJECTION = new String[]{ID, removeWhitespace("COALESCE(" + nullIfEmpty(SYSTEM_DISPLAY_NAME) + ", " + nullIfEmpty(PROFILE_JOINED_NAME) + ", " + nullIfEmpty(PROFILE_GIVEN_NAME) + ", " + nullIfEmpty(USERNAME) + ", " + nullIfEmpty(PHONE) + ")") + " AS " + SORT_NAME}; private static final String[] MENTION_SEARCH_PROJECTION = new String[]{ID, removeWhitespace("COALESCE(" + nullIfEmpty(SYSTEM_DISPLAY_NAME) + ", " + nullIfEmpty(PROFILE_JOINED_NAME) + ", " + nullIfEmpty(PROFILE_GIVEN_NAME) + ", " + nullIfEmpty(USERNAME) + ", " + nullIfEmpty(PHONE) + ")") + " AS " + SORT_NAME};
private static final String[] RECIPIENT_FULL_PROJECTION = ArrayUtils.concat( private static final String[] RECIPIENT_FULL_PROJECTION = ArrayUtils.concat(
new String[] { TABLE_NAME + "." + ID }, new String[] { TABLE_NAME + "." + ID,
TABLE_NAME + "." + STORAGE_PROTO },
TYPED_RECIPIENT_PROJECTION, TYPED_RECIPIENT_PROJECTION,
new String[] { new String[] {
IdentityDatabase.TABLE_NAME + "." + IdentityDatabase.VERIFIED + " AS " + IDENTITY_STATUS, IdentityDatabase.TABLE_NAME + "." + IdentityDatabase.VERIFIED + " AS " + IDENTITY_STATUS,
@ -336,7 +338,8 @@ public class RecipientDatabase extends Database {
GROUPS_V2_CAPABILITY + " INTEGER DEFAULT " + Recipient.Capability.UNKNOWN.serialize() + ", " + GROUPS_V2_CAPABILITY + " INTEGER DEFAULT " + Recipient.Capability.UNKNOWN.serialize() + ", " +
STORAGE_SERVICE_ID + " TEXT UNIQUE DEFAULT NULL, " + STORAGE_SERVICE_ID + " TEXT UNIQUE DEFAULT NULL, " +
DIRTY + " INTEGER DEFAULT " + DirtyState.CLEAN.getId() + ", " + DIRTY + " INTEGER DEFAULT " + DirtyState.CLEAN.getId() + ", " +
MENTION_SETTING + " INTEGER DEFAULT " + MentionSetting.ALWAYS_NOTIFY.getId() + ");"; MENTION_SETTING + " INTEGER DEFAULT " + MentionSetting.ALWAYS_NOTIFY.getId() +
STORAGE_PROTO + " TEXT DEFAULT NULL);";
private static final String INSIGHTS_INVITEE_LIST = "SELECT " + TABLE_NAME + "." + ID + private static final String INSIGHTS_INVITEE_LIST = "SELECT " + TABLE_NAME + "." + ID +
" FROM " + TABLE_NAME + " FROM " + TABLE_NAME +
@ -907,6 +910,12 @@ public class RecipientDatabase extends Database {
values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(update.getId().getRaw())); values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(update.getId().getRaw()));
values.put(DIRTY, DirtyState.CLEAN.getId()); values.put(DIRTY, DirtyState.CLEAN.getId());
if (update.hasUnknownFields()) {
values.put(STORAGE_PROTO, Base64.encodeBytes(update.serializeUnknownFields()));
} else {
values.putNull(STORAGE_PROTO);
}
int updateCount = db.update(TABLE_NAME, values, STORAGE_SERVICE_ID + " = ?", new String[]{Base64.encodeBytes(storageId.getRaw())}); int updateCount = db.update(TABLE_NAME, values, STORAGE_SERVICE_ID + " = ?", new String[]{Base64.encodeBytes(storageId.getRaw())});
if (updateCount < 1) { if (updateCount < 1) {
throw new AssertionError("Account update didn't match any rows!"); throw new AssertionError("Account update didn't match any rows!");
@ -981,6 +990,12 @@ public class RecipientDatabase extends Database {
values.put(COLOR, ContactColors.generateFor(profileName.toString()).serialize()); values.put(COLOR, ContactColors.generateFor(profileName.toString()).serialize());
} }
if (contact.hasUnknownFields()) {
values.put(STORAGE_PROTO, Base64.encodeBytes(contact.serializeUnknownFields()));
} else {
values.putNull(STORAGE_PROTO);
}
return values; return values;
} }
@ -992,6 +1007,13 @@ public class RecipientDatabase extends Database {
values.put(BLOCKED, groupV1.isBlocked() ? "1" : "0"); values.put(BLOCKED, groupV1.isBlocked() ? "1" : "0");
values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(groupV1.getId().getRaw())); values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(groupV1.getId().getRaw()));
values.put(DIRTY, DirtyState.CLEAN.getId()); values.put(DIRTY, DirtyState.CLEAN.getId());
if (groupV1.hasUnknownFields()) {
values.put(STORAGE_PROTO, Base64.encodeBytes(groupV1.serializeUnknownFields()));
} else {
values.putNull(STORAGE_PROTO);
}
return values; return values;
} }
@ -1003,6 +1025,13 @@ public class RecipientDatabase extends Database {
values.put(BLOCKED, groupV2.isBlocked() ? "1" : "0"); values.put(BLOCKED, groupV2.isBlocked() ? "1" : "0");
values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(groupV2.getId().getRaw())); values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(groupV2.getId().getRaw()));
values.put(DIRTY, DirtyState.CLEAN.getId()); values.put(DIRTY, DirtyState.CLEAN.getId());
if (groupV2.hasUnknownFields()) {
values.put(STORAGE_PROTO, Base64.encodeBytes(groupV2.serializeUnknownFields()));
} else {
values.putNull(STORAGE_PROTO);
}
return values; return values;
} }
@ -1113,6 +1142,7 @@ public class RecipientDatabase extends Database {
int groupsV2CapabilityValue = CursorUtil.requireInt(cursor, GROUPS_V2_CAPABILITY); int groupsV2CapabilityValue = CursorUtil.requireInt(cursor, GROUPS_V2_CAPABILITY);
String storageKeyRaw = CursorUtil.requireString(cursor, STORAGE_SERVICE_ID); String storageKeyRaw = CursorUtil.requireString(cursor, STORAGE_SERVICE_ID);
int mentionSettingId = CursorUtil.requireInt(cursor, MENTION_SETTING); int mentionSettingId = CursorUtil.requireInt(cursor, MENTION_SETTING);
String storageProtoRaw = CursorUtil.getString(cursor, STORAGE_PROTO).orNull();
Optional<String> identityKeyRaw = CursorUtil.getString(cursor, IDENTITY_KEY); Optional<String> identityKeyRaw = CursorUtil.getString(cursor, IDENTITY_KEY);
Optional<Integer> identityStatusRaw = CursorUtil.getInt(cursor, IDENTITY_STATUS); Optional<Integer> identityStatusRaw = CursorUtil.getInt(cursor, IDENTITY_STATUS);
@ -1159,8 +1189,9 @@ public class RecipientDatabase extends Database {
} }
} }
byte[] storageKey = storageKeyRaw != null ? Base64.decodeOrThrow(storageKeyRaw) : null; byte[] storageKey = storageKeyRaw != null ? Base64.decodeOrThrow(storageKeyRaw) : null;
byte[] identityKey = identityKeyRaw.transform(Base64::decodeOrThrow).orNull(); byte[] identityKey = identityKeyRaw.transform(Base64::decodeOrThrow).orNull();
byte[] storageProto = storageProtoRaw != null ? Base64.decodeOrThrow(storageProtoRaw) : null;
IdentityDatabase.VerifiedStatus identityStatus = identityStatusRaw.transform(IdentityDatabase.VerifiedStatus::forState).or(IdentityDatabase.VerifiedStatus.DEFAULT); IdentityDatabase.VerifiedStatus identityStatus = identityStatusRaw.transform(IdentityDatabase.VerifiedStatus::forState).or(IdentityDatabase.VerifiedStatus.DEFAULT);
@ -1180,7 +1211,8 @@ public class RecipientDatabase extends Database {
Recipient.Capability.deserialize(uuidCapabilityValue), Recipient.Capability.deserialize(uuidCapabilityValue),
Recipient.Capability.deserialize(groupsV2CapabilityValue), Recipient.Capability.deserialize(groupsV2CapabilityValue),
InsightsBannerTier.fromId(insightsBannerTier), InsightsBannerTier.fromId(insightsBannerTier),
storageKey, identityKey, identityStatus, MentionSetting.fromId(mentionSettingId)); storageKey, identityKey, identityStatus, MentionSetting.fromId(mentionSettingId),
storageProto);
} }
public BulkOperationsHandle beginBulkSystemContactUpdate() { public BulkOperationsHandle beginBulkSystemContactUpdate() {
@ -2512,6 +2544,7 @@ public class RecipientDatabase extends Database {
private final byte[] identityKey; private final byte[] identityKey;
private final IdentityDatabase.VerifiedStatus identityStatus; private final IdentityDatabase.VerifiedStatus identityStatus;
private final MentionSetting mentionSetting; private final MentionSetting mentionSetting;
private final byte[] storageProto;
RecipientSettings(@NonNull RecipientId id, RecipientSettings(@NonNull RecipientId id,
@Nullable UUID uuid, @Nullable UUID uuid,
@ -2551,7 +2584,8 @@ public class RecipientDatabase extends Database {
@Nullable byte[] storageId, @Nullable byte[] storageId,
@Nullable byte[] identityKey, @Nullable byte[] identityKey,
@NonNull IdentityDatabase.VerifiedStatus identityStatus, @NonNull IdentityDatabase.VerifiedStatus identityStatus,
@NonNull MentionSetting mentionSetting) @NonNull MentionSetting mentionSetting,
@Nullable byte[] storageProto)
{ {
this.id = id; this.id = id;
this.uuid = uuid; this.uuid = uuid;
@ -2592,6 +2626,7 @@ public class RecipientDatabase extends Database {
this.identityKey = identityKey; this.identityKey = identityKey;
this.identityStatus = identityStatus; this.identityStatus = identityStatus;
this.mentionSetting = mentionSetting; this.mentionSetting = mentionSetting;
this.storageProto = storageProto;
} }
public RecipientId getId() { public RecipientId getId() {
@ -2752,6 +2787,10 @@ public class RecipientDatabase extends Database {
public @NonNull MentionSetting getMentionSetting() { public @NonNull MentionSetting getMentionSetting() {
return mentionSetting; return mentionSetting;
} }
public @Nullable byte[] getStorageProto() {
return storageProto;
}
} }
public static class RecipientReader implements Closeable { public static class RecipientReader implements Closeable {

View File

@ -143,8 +143,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
private static final int MENTIONS = 68; private static final int MENTIONS = 68;
private static final int PINNED_CONVERSATIONS = 69; private static final int PINNED_CONVERSATIONS = 69;
private static final int MENTION_GLOBAL_SETTING_MIGRATION = 70; private static final int MENTION_GLOBAL_SETTING_MIGRATION = 70;
private static final int UNKNOWN_STORAGE_FIELDS = 71;
private static final int DATABASE_VERSION = 70; private static final int DATABASE_VERSION = 71;
private static final String DATABASE_NAME = "signal.db"; private static final String DATABASE_NAME = "signal.db";
private final Context context; private final Context context;
@ -1008,6 +1009,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
db.update("recipient", updateNever, "mention_setting = 2", null); db.update("recipient", updateNever, "mention_setting = 2", null);
} }
if (oldVersion < UNKNOWN_STORAGE_FIELDS) {
db.execSQL("ALTER TABLE recipient ADD COLUMN storage_proto TEXT DEFAULT NULL");
}
db.setTransactionSuccessful(); db.setTransactionSuccessful();
} finally { } finally {
db.endTransaction(); db.endTransaction();

View File

@ -3,23 +3,15 @@ package org.thoughtcrime.securesms.storage;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.storage.SignalAccountRecord; import org.whispersystems.signalservice.api.storage.SignalAccountRecord;
import org.whispersystems.signalservice.api.storage.SignalContactRecord;
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord.IdentityState;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.UUID;
class AccountConflictMerger implements StorageSyncHelper.ConflictMerger<SignalAccountRecord> { class AccountConflictMerger implements StorageSyncHelper.ConflictMerger<SignalAccountRecord> {
@ -63,6 +55,7 @@ class AccountConflictMerger implements StorageSyncHelper.ConflictMerger<SignalAc
familyName = local.getFamilyName().or(""); familyName = local.getFamilyName().or("");
} }
byte[] unknownFields = remote.serializeUnknownFields();
String avatarUrlPath = remote.getAvatarUrlPath().or(local.getAvatarUrlPath()).or(""); String avatarUrlPath = remote.getAvatarUrlPath().or(local.getAvatarUrlPath()).or("");
byte[] profileKey = remote.getProfileKey().or(local.getProfileKey()).orNull(); byte[] profileKey = remote.getProfileKey().or(local.getProfileKey()).orNull();
boolean noteToSelfArchived = remote.isNoteToSelfArchived(); boolean noteToSelfArchived = remote.isNoteToSelfArchived();
@ -70,8 +63,8 @@ class AccountConflictMerger implements StorageSyncHelper.ConflictMerger<SignalAc
boolean typingIndicators = remote.isTypingIndicatorsEnabled(); boolean typingIndicators = remote.isTypingIndicatorsEnabled();
boolean sealedSenderIndicators = remote.isSealedSenderIndicatorsEnabled(); boolean sealedSenderIndicators = remote.isSealedSenderIndicatorsEnabled();
boolean linkPreviews = remote.isLinkPreviewsEnabled(); boolean linkPreviews = remote.isLinkPreviewsEnabled();
boolean matchesRemote = doParamsMatch(remote, givenName, familyName, avatarUrlPath, profileKey, noteToSelfArchived, readReceipts, typingIndicators, sealedSenderIndicators, linkPreviews); boolean matchesRemote = doParamsMatch(remote, unknownFields, givenName, familyName, avatarUrlPath, profileKey, noteToSelfArchived, readReceipts, typingIndicators, sealedSenderIndicators, linkPreviews);
boolean matchesLocal = doParamsMatch(local, givenName, familyName, avatarUrlPath, profileKey, noteToSelfArchived, readReceipts, typingIndicators, sealedSenderIndicators, linkPreviews); boolean matchesLocal = doParamsMatch(local, unknownFields, givenName, familyName, avatarUrlPath, profileKey, noteToSelfArchived, readReceipts, typingIndicators, sealedSenderIndicators, linkPreviews);
if (matchesRemote) { if (matchesRemote) {
return remote; return remote;
@ -79,6 +72,7 @@ class AccountConflictMerger implements StorageSyncHelper.ConflictMerger<SignalAc
return local; return local;
} else { } else {
return new SignalAccountRecord.Builder(keyGenerator.generate()) return new SignalAccountRecord.Builder(keyGenerator.generate())
.setUnknownFields(unknownFields)
.setGivenName(givenName) .setGivenName(givenName)
.setFamilyName(familyName) .setFamilyName(familyName)
.setAvatarUrlPath(avatarUrlPath) .setAvatarUrlPath(avatarUrlPath)
@ -93,6 +87,7 @@ class AccountConflictMerger implements StorageSyncHelper.ConflictMerger<SignalAc
} }
private static boolean doParamsMatch(@NonNull SignalAccountRecord contact, private static boolean doParamsMatch(@NonNull SignalAccountRecord contact,
@Nullable byte[] unknownFields,
@NonNull String givenName, @NonNull String givenName,
@NonNull String familyName, @NonNull String familyName,
@NonNull String avatarUrlPath, @NonNull String avatarUrlPath,
@ -103,7 +98,8 @@ class AccountConflictMerger implements StorageSyncHelper.ConflictMerger<SignalAc
boolean sealedSenderIndicators, boolean sealedSenderIndicators,
boolean linkPreviewsEnabled) boolean linkPreviewsEnabled)
{ {
return Objects.equals(contact.getGivenName().or(""), givenName) && return Arrays.equals(contact.serializeUnknownFields(), unknownFields) &&
Objects.equals(contact.getGivenName().or(""), givenName) &&
Objects.equals(contact.getFamilyName().or(""), familyName) && Objects.equals(contact.getFamilyName().or(""), familyName) &&
Objects.equals(contact.getAvatarUrlPath().or(""), avatarUrlPath) && Objects.equals(contact.getAvatarUrlPath().or(""), avatarUrlPath) &&
Arrays.equals(contact.getProfileKey().orNull(), profileKey) && Arrays.equals(contact.getProfileKey().orNull(), profileKey) &&

View File

@ -75,6 +75,7 @@ class ContactConflictMerger implements StorageSyncHelper.ConflictMerger<SignalCo
familyName = local.getFamilyName().or(""); familyName = local.getFamilyName().or("");
} }
byte[] unknownFields = remote.serializeUnknownFields();
UUID uuid = remote.getAddress().getUuid().or(local.getAddress().getUuid()).orNull(); UUID uuid = remote.getAddress().getUuid().or(local.getAddress().getUuid()).orNull();
String e164 = remote.getAddress().getNumber().or(local.getAddress().getNumber()).orNull(); String e164 = remote.getAddress().getNumber().or(local.getAddress().getNumber()).orNull();
SignalServiceAddress address = new SignalServiceAddress(uuid, e164); SignalServiceAddress address = new SignalServiceAddress(uuid, e164);
@ -85,8 +86,8 @@ class ContactConflictMerger implements StorageSyncHelper.ConflictMerger<SignalCo
boolean blocked = remote.isBlocked(); boolean blocked = remote.isBlocked();
boolean profileSharing = remote.isProfileSharingEnabled() || local.isProfileSharingEnabled(); boolean profileSharing = remote.isProfileSharingEnabled() || local.isProfileSharingEnabled();
boolean archived = remote.isArchived(); boolean archived = remote.isArchived();
boolean matchesRemote = doParamsMatch(remote, address, givenName, familyName, profileKey, username, identityState, identityKey, blocked, profileSharing, archived); boolean matchesRemote = doParamsMatch(remote, unknownFields, address, givenName, familyName, profileKey, username, identityState, identityKey, blocked, profileSharing, archived);
boolean matchesLocal = doParamsMatch(local, address, givenName, familyName, profileKey, username, identityState, identityKey, blocked, profileSharing, archived); boolean matchesLocal = doParamsMatch(local, unknownFields, address, givenName, familyName, profileKey, username, identityState, identityKey, blocked, profileSharing, archived);
if (matchesRemote) { if (matchesRemote) {
return remote; return remote;
@ -94,6 +95,7 @@ class ContactConflictMerger implements StorageSyncHelper.ConflictMerger<SignalCo
return local; return local;
} else { } else {
return new SignalContactRecord.Builder(keyGenerator.generate(), address) return new SignalContactRecord.Builder(keyGenerator.generate(), address)
.setUnknownFields(unknownFields)
.setGivenName(givenName) .setGivenName(givenName)
.setFamilyName(familyName) .setFamilyName(familyName)
.setProfileKey(profileKey) .setProfileKey(profileKey)
@ -107,6 +109,7 @@ class ContactConflictMerger implements StorageSyncHelper.ConflictMerger<SignalCo
} }
private static boolean doParamsMatch(@NonNull SignalContactRecord contact, private static boolean doParamsMatch(@NonNull SignalContactRecord contact,
@Nullable byte[] unknownFields,
@NonNull SignalServiceAddress address, @NonNull SignalServiceAddress address,
@NonNull String givenName, @NonNull String givenName,
@NonNull String familyName, @NonNull String familyName,
@ -118,15 +121,16 @@ class ContactConflictMerger implements StorageSyncHelper.ConflictMerger<SignalCo
boolean profileSharing, boolean profileSharing,
boolean archived) boolean archived)
{ {
return Objects.equals(contact.getAddress(), address) && return Arrays.equals(contact.serializeUnknownFields(), unknownFields) &&
Objects.equals(contact.getGivenName().or(""), givenName) && Objects.equals(contact.getAddress(), address) &&
Objects.equals(contact.getFamilyName().or(""), familyName) && Objects.equals(contact.getGivenName().or(""), givenName) &&
Arrays.equals(contact.getProfileKey().orNull(), profileKey) && Objects.equals(contact.getFamilyName().or(""), familyName) &&
Objects.equals(contact.getUsername().or(""), username) && Arrays.equals(contact.getProfileKey().orNull(), profileKey) &&
Objects.equals(contact.getIdentityState(), identityState) && Objects.equals(contact.getUsername().or(""), username) &&
Arrays.equals(contact.getIdentityKey().orNull(), identityKey) && Objects.equals(contact.getIdentityState(), identityState) &&
contact.isBlocked() == blocked && Arrays.equals(contact.getIdentityKey().orNull(), identityKey) &&
contact.isProfileSharingEnabled() == profileSharing && contact.isBlocked() == blocked &&
contact.isProfileSharingEnabled() == profileSharing &&
contact.isArchived() == archived; contact.isArchived() == archived;
} }
} }

View File

@ -9,6 +9,7 @@ import org.thoughtcrime.securesms.groups.GroupId;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.storage.SignalGroupV1Record; import org.whispersystems.signalservice.api.storage.SignalGroupV1Record;
import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.Map; import java.util.Map;
@ -33,12 +34,13 @@ class GroupV1ConflictMerger implements StorageSyncHelper.ConflictMerger<SignalGr
@Override @Override
public @NonNull SignalGroupV1Record merge(@NonNull SignalGroupV1Record remote, @NonNull SignalGroupV1Record local, @NonNull StorageSyncHelper.KeyGenerator keyGenerator) { public @NonNull SignalGroupV1Record merge(@NonNull SignalGroupV1Record remote, @NonNull SignalGroupV1Record local, @NonNull StorageSyncHelper.KeyGenerator keyGenerator) {
byte[] unknownFields = remote.serializeUnknownFields();
boolean blocked = remote.isBlocked(); boolean blocked = remote.isBlocked();
boolean profileSharing = remote.isProfileSharingEnabled() || local.isProfileSharingEnabled(); boolean profileSharing = remote.isProfileSharingEnabled() || local.isProfileSharingEnabled();
boolean archived = remote.isArchived(); boolean archived = remote.isArchived();
boolean matchesRemote = blocked == remote.isBlocked() && profileSharing == remote.isProfileSharingEnabled() && archived == remote.isArchived(); boolean matchesRemote = Arrays.equals(unknownFields, remote.serializeUnknownFields()) && blocked == remote.isBlocked() && profileSharing == remote.isProfileSharingEnabled() && archived == remote.isArchived();
boolean matchesLocal = blocked == local.isBlocked() && profileSharing == local.isProfileSharingEnabled() && archived == local.isArchived(); boolean matchesLocal = Arrays.equals(unknownFields, local.serializeUnknownFields()) && blocked == local.isBlocked() && profileSharing == local.isProfileSharingEnabled() && archived == local.isArchived();
if (matchesRemote) { if (matchesRemote) {
return remote; return remote;
@ -46,6 +48,7 @@ class GroupV1ConflictMerger implements StorageSyncHelper.ConflictMerger<SignalGr
return local; return local;
} else { } else {
return new SignalGroupV1Record.Builder(keyGenerator.generate(), remote.getGroupId()) return new SignalGroupV1Record.Builder(keyGenerator.generate(), remote.getGroupId())
.setUnknownFields(unknownFields)
.setBlocked(blocked) .setBlocked(blocked)
.setProfileSharingEnabled(blocked) .setProfileSharingEnabled(blocked)
.build(); .build();

View File

@ -9,6 +9,7 @@ import org.signal.zkgroup.groups.GroupMasterKey;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.storage.SignalGroupV2Record; import org.whispersystems.signalservice.api.storage.SignalGroupV2Record;
import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.Map; import java.util.Map;
@ -33,12 +34,13 @@ class GroupV2ConflictMerger implements StorageSyncHelper.ConflictMerger<SignalGr
@Override @Override
public @NonNull SignalGroupV2Record merge(@NonNull SignalGroupV2Record remote, @NonNull SignalGroupV2Record local, @NonNull StorageSyncHelper.KeyGenerator keyGenerator) { public @NonNull SignalGroupV2Record merge(@NonNull SignalGroupV2Record remote, @NonNull SignalGroupV2Record local, @NonNull StorageSyncHelper.KeyGenerator keyGenerator) {
byte[] unknownFields = remote.serializeUnknownFields();
boolean blocked = remote.isBlocked(); boolean blocked = remote.isBlocked();
boolean profileSharing = remote.isProfileSharingEnabled() || local.isProfileSharingEnabled(); boolean profileSharing = remote.isProfileSharingEnabled() || local.isProfileSharingEnabled();
boolean archived = remote.isArchived(); boolean archived = remote.isArchived();
boolean matchesRemote = blocked == remote.isBlocked() && profileSharing == remote.isProfileSharingEnabled() && archived == remote.isArchived(); boolean matchesRemote = Arrays.equals(unknownFields, remote.serializeUnknownFields()) && blocked == remote.isBlocked() && profileSharing == remote.isProfileSharingEnabled() && archived == remote.isArchived();
boolean matchesLocal = blocked == local.isBlocked() && profileSharing == local.isProfileSharingEnabled() && archived == local.isArchived(); boolean matchesLocal = Arrays.equals(unknownFields, local.serializeUnknownFields()) && blocked == local.isBlocked() && profileSharing == local.isProfileSharingEnabled() && archived == local.isArchived();
if (matchesRemote) { if (matchesRemote) {
return remote; return remote;
@ -46,6 +48,7 @@ class GroupV2ConflictMerger implements StorageSyncHelper.ConflictMerger<SignalGr
return local; return local;
} else { } else {
return new SignalGroupV2Record.Builder(keyGenerator.generate(), remote.getMasterKey()) return new SignalGroupV2Record.Builder(keyGenerator.generate(), remote.getMasterKey())
.setUnknownFields(unknownFields)
.setBlocked(blocked) .setBlocked(blocked)
.setProfileSharingEnabled(blocked) .setProfileSharingEnabled(blocked)
.build(); .build();

View File

@ -385,7 +385,10 @@ public final class StorageSyncHelper {
} }
public static SignalStorageRecord buildAccountRecord(@NonNull Context context, @NonNull Recipient self) { public static SignalStorageRecord buildAccountRecord(@NonNull Context context, @NonNull Recipient self) {
RecipientSettings settings = DatabaseFactory.getRecipientDatabase(context).getRecipientSettingsForSync(self.getId());
SignalAccountRecord account = new SignalAccountRecord.Builder(self.getStorageServiceId()) SignalAccountRecord account = new SignalAccountRecord.Builder(self.getStorageServiceId())
.setUnknownFields(settings != null ? settings.getStorageProto() : null)
.setProfileKey(self.getProfileKey()) .setProfileKey(self.getProfileKey())
.setGivenName(self.getProfileName().getGivenName()) .setGivenName(self.getProfileName().getGivenName())
.setFamilyName(self.getProfileName().getFamilyName()) .setFamilyName(self.getProfileName().getFamilyName())

View File

@ -43,6 +43,7 @@ public final class StorageSyncModels {
} }
return new SignalContactRecord.Builder(rawStorageId, new SignalServiceAddress(recipient.getUuid(), recipient.getE164())) return new SignalContactRecord.Builder(rawStorageId, new SignalServiceAddress(recipient.getUuid(), recipient.getE164()))
.setUnknownFields(recipient.getStorageProto())
.setProfileKey(recipient.getProfileKey()) .setProfileKey(recipient.getProfileKey())
.setGivenName(recipient.getProfileName().getGivenName()) .setGivenName(recipient.getProfileName().getGivenName())
.setFamilyName(recipient.getProfileName().getFamilyName()) .setFamilyName(recipient.getProfileName().getFamilyName())
@ -66,6 +67,7 @@ public final class StorageSyncModels {
} }
return new SignalGroupV1Record.Builder(rawStorageId, groupId.getDecodedId()) return new SignalGroupV1Record.Builder(rawStorageId, groupId.getDecodedId())
.setUnknownFields(recipient.getStorageProto())
.setBlocked(recipient.isBlocked()) .setBlocked(recipient.isBlocked())
.setProfileSharingEnabled(recipient.isProfileSharing()) .setProfileSharingEnabled(recipient.isProfileSharing())
.setArchived(archived.contains(recipient.getId())) .setArchived(archived.contains(recipient.getId()))
@ -90,6 +92,7 @@ public final class StorageSyncModels {
} }
return new SignalGroupV2Record.Builder(rawStorageId, groupMasterKey) return new SignalGroupV2Record.Builder(rawStorageId, groupMasterKey)
.setUnknownFields(recipient.getStorageProto())
.setBlocked(recipient.isBlocked()) .setBlocked(recipient.isBlocked())
.setProfileSharingEnabled(recipient.isProfileSharing()) .setProfileSharingEnabled(recipient.isProfileSharing())
.setArchived(archived.contains(recipient.getId())) .setArchived(archived.contains(recipient.getId()))

View File

@ -4,6 +4,7 @@ import com.google.protobuf.ByteString;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.util.OptionalUtil; import org.whispersystems.signalservice.api.util.OptionalUtil;
import org.whispersystems.signalservice.api.util.ProtoUtil;
import org.whispersystems.signalservice.internal.storage.protos.AccountRecord; import org.whispersystems.signalservice.internal.storage.protos.AccountRecord;
import java.util.Objects; import java.util.Objects;
@ -12,6 +13,7 @@ public final class SignalAccountRecord implements SignalRecord {
private final StorageId id; private final StorageId id;
private final AccountRecord proto; private final AccountRecord proto;
private final boolean hasUnknownFields;
private final Optional<String> givenName; private final Optional<String> givenName;
private final Optional<String> familyName; private final Optional<String> familyName;
@ -19,8 +21,9 @@ public final class SignalAccountRecord implements SignalRecord {
private final Optional<byte[]> profileKey; private final Optional<byte[]> profileKey;
public SignalAccountRecord(StorageId id, AccountRecord proto) { public SignalAccountRecord(StorageId id, AccountRecord proto) {
this.id = id; this.id = id;
this.proto = proto; this.proto = proto;
this.hasUnknownFields = ProtoUtil.hasUnknownFields(proto);
this.givenName = OptionalUtil.absentIfEmpty(proto.getGivenName()); this.givenName = OptionalUtil.absentIfEmpty(proto.getGivenName());
this.familyName = OptionalUtil.absentIfEmpty(proto.getFamilyName()); this.familyName = OptionalUtil.absentIfEmpty(proto.getFamilyName());
@ -33,6 +36,14 @@ public final class SignalAccountRecord implements SignalRecord {
return id; return id;
} }
public boolean hasUnknownFields() {
return hasUnknownFields;
}
public byte[] serializeUnknownFields() {
return hasUnknownFields ? proto.toByteArray() : null;
}
public Optional<String> getGivenName() { public Optional<String> getGivenName() {
return givenName; return givenName;
} }
@ -91,11 +102,18 @@ public final class SignalAccountRecord implements SignalRecord {
private final StorageId id; private final StorageId id;
private final AccountRecord.Builder builder; private final AccountRecord.Builder builder;
private byte[] unknownFields;
public Builder(byte[] rawId) { public Builder(byte[] rawId) {
this.id = StorageId.forAccount(rawId); this.id = StorageId.forAccount(rawId);
this.builder = AccountRecord.newBuilder(); this.builder = AccountRecord.newBuilder();
} }
public Builder setUnknownFields(byte[] serializedUnknowns) {
this.unknownFields = serializedUnknowns;
return this;
}
public Builder setGivenName(String givenName) { public Builder setGivenName(String givenName) {
builder.setGivenName(givenName == null ? "" : givenName); builder.setGivenName(givenName == null ? "" : givenName);
return this; return this;
@ -142,7 +160,13 @@ public final class SignalAccountRecord implements SignalRecord {
} }
public SignalAccountRecord build() { public SignalAccountRecord build() {
return new SignalAccountRecord(id, builder.build()); AccountRecord proto = builder.build();
if (unknownFields != null) {
proto = ProtoUtil.combineWithUnknownFields(proto, unknownFields);
}
return new SignalAccountRecord(id, proto);
} }
} }
} }

View File

@ -5,6 +5,7 @@ import com.google.protobuf.ByteString;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.OptionalUtil; import org.whispersystems.signalservice.api.util.OptionalUtil;
import org.whispersystems.signalservice.api.util.ProtoUtil;
import org.whispersystems.signalservice.api.util.UuidUtil; import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord; import org.whispersystems.signalservice.internal.storage.protos.ContactRecord;
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord.IdentityState; import org.whispersystems.signalservice.internal.storage.protos.ContactRecord.IdentityState;
@ -15,6 +16,7 @@ public final class SignalContactRecord implements SignalRecord {
private final StorageId id; private final StorageId id;
private final ContactRecord proto; private final ContactRecord proto;
private final boolean hasUnknownFields;
private final SignalServiceAddress address; private final SignalServiceAddress address;
private final Optional<String> givenName; private final Optional<String> givenName;
@ -24,8 +26,9 @@ public final class SignalContactRecord implements SignalRecord {
private final Optional<byte[]> identityKey; private final Optional<byte[]> identityKey;
public SignalContactRecord(StorageId id, ContactRecord proto) { public SignalContactRecord(StorageId id, ContactRecord proto) {
this.id = id; this.id = id;
this.proto = proto; this.proto = proto;
this.hasUnknownFields = ProtoUtil.hasUnknownFields(proto);
this.address = new SignalServiceAddress(UuidUtil.parseOrNull(proto.getServiceUuid()), proto.getServiceE164()); this.address = new SignalServiceAddress(UuidUtil.parseOrNull(proto.getServiceUuid()), proto.getServiceE164());
this.givenName = OptionalUtil.absentIfEmpty(proto.getGivenName()); this.givenName = OptionalUtil.absentIfEmpty(proto.getGivenName());
@ -40,6 +43,14 @@ public final class SignalContactRecord implements SignalRecord {
return id; return id;
} }
public boolean hasUnknownFields() {
return hasUnknownFields;
}
public byte[] serializeUnknownFields() {
return hasUnknownFields ? proto.toByteArray() : null;
}
public SignalServiceAddress getAddress() { public SignalServiceAddress getAddress() {
return address; return address;
} }
@ -102,6 +113,8 @@ public final class SignalContactRecord implements SignalRecord {
private final StorageId id; private final StorageId id;
private final ContactRecord.Builder builder; private final ContactRecord.Builder builder;
private byte[] unknownFields;
public Builder(byte[] rawId, SignalServiceAddress address) { public Builder(byte[] rawId, SignalServiceAddress address) {
this.id = StorageId.forContact(rawId); this.id = StorageId.forContact(rawId);
this.builder = ContactRecord.newBuilder(); this.builder = ContactRecord.newBuilder();
@ -110,6 +123,11 @@ public final class SignalContactRecord implements SignalRecord {
builder.setServiceE164(address.getNumber().or("")); builder.setServiceE164(address.getNumber().or(""));
} }
public Builder setUnknownFields(byte[] serializedUnknowns) {
this.unknownFields = serializedUnknowns;
return this;
}
public Builder setGivenName(String givenName) { public Builder setGivenName(String givenName) {
builder.setGivenName(givenName == null ? "" : givenName); builder.setGivenName(givenName == null ? "" : givenName);
return this; return this;
@ -156,7 +174,13 @@ public final class SignalContactRecord implements SignalRecord {
} }
public SignalContactRecord build() { public SignalContactRecord build() {
return new SignalContactRecord(id, builder.build()); ContactRecord proto = builder.build();
if (unknownFields != null) {
proto = ProtoUtil.combineWithUnknownFields(proto, unknownFields);
}
return new SignalContactRecord(id, proto);
} }
} }
} }

View File

@ -2,6 +2,7 @@ package org.whispersystems.signalservice.api.storage;
import com.google.protobuf.ByteString; import com.google.protobuf.ByteString;
import org.whispersystems.signalservice.api.util.ProtoUtil;
import org.whispersystems.signalservice.internal.storage.protos.GroupV1Record; import org.whispersystems.signalservice.internal.storage.protos.GroupV1Record;
import java.util.Objects; import java.util.Objects;
@ -11,11 +12,13 @@ public final class SignalGroupV1Record implements SignalRecord {
private final StorageId id; private final StorageId id;
private final GroupV1Record proto; private final GroupV1Record proto;
private final byte[] groupId; private final byte[] groupId;
private final boolean hasUnknownFields;
public SignalGroupV1Record(StorageId id, GroupV1Record proto) { public SignalGroupV1Record(StorageId id, GroupV1Record proto) {
this.id = id; this.id = id;
this.proto = proto; this.proto = proto;
this.groupId = proto.getId().toByteArray(); this.groupId = proto.getId().toByteArray();
this.hasUnknownFields = ProtoUtil.hasUnknownFields(proto);
} }
@Override @Override
@ -23,6 +26,14 @@ public final class SignalGroupV1Record implements SignalRecord {
return id; return id;
} }
public boolean hasUnknownFields() {
return hasUnknownFields;
}
public byte[] serializeUnknownFields() {
return hasUnknownFields ? proto.toByteArray() : null;
}
public byte[] getGroupId() { public byte[] getGroupId() {
return groupId; return groupId;
} }
@ -61,6 +72,8 @@ public final class SignalGroupV1Record implements SignalRecord {
private final StorageId id; private final StorageId id;
private final GroupV1Record.Builder builder; private final GroupV1Record.Builder builder;
private byte[] unknownFields;
public Builder(byte[] rawId, byte[] groupId) { public Builder(byte[] rawId, byte[] groupId) {
this.id = StorageId.forGroupV1(rawId); this.id = StorageId.forGroupV1(rawId);
this.builder = GroupV1Record.newBuilder(); this.builder = GroupV1Record.newBuilder();
@ -68,6 +81,11 @@ public final class SignalGroupV1Record implements SignalRecord {
builder.setId(ByteString.copyFrom(groupId)); builder.setId(ByteString.copyFrom(groupId));
} }
public Builder setUnknownFields(byte[] serializedUnknowns) {
this.unknownFields = serializedUnknowns;
return this;
}
public Builder setBlocked(boolean blocked) { public Builder setBlocked(boolean blocked) {
builder.setBlocked(blocked); builder.setBlocked(blocked);
return this; return this;
@ -84,7 +102,13 @@ public final class SignalGroupV1Record implements SignalRecord {
} }
public SignalGroupV1Record build() { public SignalGroupV1Record build() {
return new SignalGroupV1Record(id, builder.build()); GroupV1Record proto = builder.build();
if (unknownFields != null) {
proto = ProtoUtil.combineWithUnknownFields(proto, unknownFields);
}
return new SignalGroupV1Record(id, proto);
} }
} }
} }

View File

@ -4,7 +4,7 @@ import com.google.protobuf.ByteString;
import org.signal.zkgroup.InvalidInputException; import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.groups.GroupMasterKey; import org.signal.zkgroup.groups.GroupMasterKey;
import org.whispersystems.signalservice.internal.storage.protos.GroupV1Record; import org.whispersystems.signalservice.api.util.ProtoUtil;
import org.whispersystems.signalservice.internal.storage.protos.GroupV2Record; import org.whispersystems.signalservice.internal.storage.protos.GroupV2Record;
import java.util.Objects; import java.util.Objects;
@ -14,10 +14,12 @@ public final class SignalGroupV2Record implements SignalRecord {
private final StorageId id; private final StorageId id;
private final GroupV2Record proto; private final GroupV2Record proto;
private final GroupMasterKey masterKey; private final GroupMasterKey masterKey;
private final boolean hasUnknownFields;
public SignalGroupV2Record(StorageId id, GroupV2Record proto) { public SignalGroupV2Record(StorageId id, GroupV2Record proto) {
this.id = id; this.id = id;
this.proto = proto; this.proto = proto;
this.hasUnknownFields = ProtoUtil.hasUnknownFields(proto);
try { try {
this.masterKey = new GroupMasterKey(proto.getMasterKey().toByteArray()); this.masterKey = new GroupMasterKey(proto.getMasterKey().toByteArray());
} catch (InvalidInputException e) { } catch (InvalidInputException e) {
@ -30,6 +32,14 @@ public final class SignalGroupV2Record implements SignalRecord {
return id; return id;
} }
public boolean hasUnknownFields() {
return hasUnknownFields;
}
public byte[] serializeUnknownFields() {
return hasUnknownFields ? proto.toByteArray() : null;
}
public GroupMasterKey getMasterKey() { public GroupMasterKey getMasterKey() {
return masterKey; return masterKey;
} }
@ -68,6 +78,8 @@ public final class SignalGroupV2Record implements SignalRecord {
private final StorageId id; private final StorageId id;
private final GroupV2Record.Builder builder; private final GroupV2Record.Builder builder;
private byte[] unknownFields;
public Builder(byte[] rawId, GroupMasterKey masterKey) { public Builder(byte[] rawId, GroupMasterKey masterKey) {
this.id = StorageId.forGroupV2(rawId); this.id = StorageId.forGroupV2(rawId);
this.builder = GroupV2Record.newBuilder(); this.builder = GroupV2Record.newBuilder();
@ -75,6 +87,11 @@ public final class SignalGroupV2Record implements SignalRecord {
builder.setMasterKey(ByteString.copyFrom(masterKey.serialize())); builder.setMasterKey(ByteString.copyFrom(masterKey.serialize()));
} }
public Builder setUnknownFields(byte[] serializedUnknowns) {
this.unknownFields = serializedUnknowns;
return this;
}
public Builder setBlocked(boolean blocked) { public Builder setBlocked(boolean blocked) {
builder.setBlocked(blocked); builder.setBlocked(blocked);
return this; return this;
@ -91,7 +108,13 @@ public final class SignalGroupV2Record implements SignalRecord {
} }
public SignalGroupV2Record build() { public SignalGroupV2Record build() {
return new SignalGroupV2Record(id, builder.build()); GroupV2Record proto = builder.build();
if (unknownFields != null) {
proto = ProtoUtil.combineWithUnknownFields(proto, unknownFields);
}
return new SignalGroupV2Record(id, proto);
} }
} }
} }

View File

@ -0,0 +1,134 @@
package org.whispersystems.signalservice.api.util;
import com.google.protobuf.CodedOutputStream;
import com.google.protobuf.GeneratedMessageLite;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.UnknownFieldSetLite;
import org.whispersystems.libsignal.logging.Log;
import org.whispersystems.libsignal.util.ByteUtil;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.LinkedList;
import java.util.List;
public final class ProtoUtil {
private static final String TAG = ProtoUtil.class.getSimpleName();
private static final String DEFAULT_INSTANCE = "DEFAULT_INSTANCE";
private ProtoUtil() { }
/**
* True if there are unknown fields anywhere inside the proto or its nested protos.
*/
@SuppressWarnings("rawtypes")
public static boolean hasUnknownFields(GeneratedMessageLite rootProto) {
try {
List<GeneratedMessageLite> allProtos = getInnerProtos(rootProto);
allProtos.add(rootProto);
for (GeneratedMessageLite proto : allProtos) {
Field field = GeneratedMessageLite.class.getDeclaredField("unknownFields");
field.setAccessible(true);
UnknownFieldSetLite unknownFields = (UnknownFieldSetLite) field.get(proto);
if (unknownFields != null && unknownFields.getSerializedSize() > 0) {
return true;
}
}
} catch (NoSuchFieldException | IllegalAccessException e) {
Log.w(TAG, "Failed to read proto private fields! Assuming no unknown fields.");
}
return false;
}
/**
* This takes two arguments: A proto model, and the bytes of another proto model of the same type.
* This will take the proto model and append onto it any unknown fields from the serialized proto
* model. Why is this useful? Well, if you do {@code myProto.parseFrom(data).toBuilder().build()},
* you will lose any unknown fields that were in {@code data}. This lets you create a new model
* and plop the unknown fields back on from some other instance.
*
* A notable limitation of the current implementation is, however, that it does not support adding
* back unknown fields to *inner* messages. Unknown fields on inner messages will simply not be
* acknowledged.
*/
@SuppressWarnings({"rawtypes", "unchecked"})
public static <Proto extends GeneratedMessageLite> Proto combineWithUnknownFields(Proto proto, byte[] serializedWithUnknownFields) {
if (serializedWithUnknownFields == null) {
return proto;
}
try {
Proto protoWithUnknownFields = (Proto) proto.getParserForType().parseFrom(serializedWithUnknownFields);
byte[] unknownFields = getUnknownFields(protoWithUnknownFields);
if (unknownFields == null) {
return proto;
}
byte[] combined = ByteUtil.combine(proto.toByteArray(), unknownFields);
return (Proto) proto.getParserForType().parseFrom(combined);
} catch (InvalidProtocolBufferException e) {
throw new IllegalArgumentException();
}
}
@SuppressWarnings("rawtypes")
private static byte[] getUnknownFields(GeneratedMessageLite proto) {
try {
Field field = GeneratedMessageLite.class.getDeclaredField("unknownFields");
field.setAccessible(true);
UnknownFieldSetLite unknownFields = (UnknownFieldSetLite) field.get(proto);
if (unknownFields == null || unknownFields.getSerializedSize() == 0) {
return null;
}
ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
CodedOutputStream outputStream = CodedOutputStream.newInstance(byteStream);
unknownFields.writeTo(outputStream);
outputStream.flush();
return byteStream.toByteArray();
} catch (NoSuchFieldException | IllegalAccessException | IOException e) {
Log.w(TAG, "Failed to retrieve unknown fields.", e);
return null;
}
}
/**
* Recursively retrieves all inner complex proto types inside a given proto.
*/
@SuppressWarnings("rawtypes")
private static List<GeneratedMessageLite> getInnerProtos(GeneratedMessageLite proto) {
List<GeneratedMessageLite> innerProtos = new LinkedList<>();
try {
Field[] fields = proto.getClass().getDeclaredFields();
for (Field field : fields) {
if (!field.getName().equals(DEFAULT_INSTANCE) && GeneratedMessageLite.class.isAssignableFrom(field.getType())) {
field.setAccessible(true);
GeneratedMessageLite inner = (GeneratedMessageLite) field.get(proto);
innerProtos.add(inner);
innerProtos.addAll(getInnerProtos(inner));
}
}
} catch (IllegalAccessException e) {
Log.w(TAG, "Failed to get inner protos!", e);
}
return innerProtos;
}
}

View File

@ -0,0 +1,229 @@
package org.whispersystems.signalservice.api.util;
import com.google.protobuf.InvalidProtocolBufferException;
import org.junit.Assert;
import org.junit.Test;
import org.thoughtcrime.securesms.util.testprotos.TestInnerMessage;
import org.thoughtcrime.securesms.util.testprotos.TestInnerMessageWithNewString;
import org.thoughtcrime.securesms.util.testprotos.TestPerson;
import org.thoughtcrime.securesms.util.testprotos.TestPersonWithNewFieldOnMessage;
import org.thoughtcrime.securesms.util.testprotos.TestPersonWithNewMessage;
import org.thoughtcrime.securesms.util.testprotos.TestPersonWithNewRepeatedString;
import org.thoughtcrime.securesms.util.testprotos.TestPersonWithNewString;
import org.thoughtcrime.securesms.util.testprotos.TestPersonWithNewStringAndInt;
import org.whispersystems.signalservice.api.util.ProtoUtil;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
public class ProtoUtilTest {
@Test
public void hasUnknownFields_noUnknowns() {
TestPerson person = TestPerson.newBuilder()
.setName("Peter Parker")
.setAge(23)
.build();
assertFalse(ProtoUtil.hasUnknownFields(person));
}
@Test
public void hasUnknownFields_unknownString() throws InvalidProtocolBufferException {
TestPersonWithNewString person = TestPersonWithNewString.newBuilder()
.setName("Peter Parker")
.setAge(23)
.setJob("Reporter")
.build();
TestPerson personWithUnknowns = TestPerson.parseFrom(person.toByteArray());
assertTrue(ProtoUtil.hasUnknownFields(personWithUnknowns));
}
@Test
public void hasUnknownFields_multipleUnknowns() throws InvalidProtocolBufferException {
TestPersonWithNewStringAndInt person = TestPersonWithNewStringAndInt.newBuilder()
.setName("Peter Parker")
.setAge(23)
.setJob("Reporter")
.setSalary(75_000)
.build();
TestPerson personWithUnknowns = TestPerson.parseFrom(person.toByteArray());
assertTrue(ProtoUtil.hasUnknownFields(personWithUnknowns));
}
@Test
public void hasUnknownFields_unknownMessage() throws InvalidProtocolBufferException {
TestPersonWithNewMessage person = TestPersonWithNewMessage.newBuilder()
.setName("Peter Parker")
.setAge(23)
.setJob(TestPersonWithNewMessage.Job.newBuilder()
.setTitle("Reporter")
.setSalary(75_000))
.build();
TestPerson personWithUnknowns = TestPerson.parseFrom(person.toByteArray());
assertTrue(ProtoUtil.hasUnknownFields(personWithUnknowns));
}
@Test
public void hasUnknownFields_unknownInsideMessage() throws InvalidProtocolBufferException {
TestPersonWithNewFieldOnMessage person = TestPersonWithNewFieldOnMessage.newBuilder()
.setName("Peter Parker")
.setAge(23)
.setJob(TestPersonWithNewFieldOnMessage.Job.newBuilder()
.setTitle("Reporter")
.setSalary(75_000)
.setStartDate(100))
.build();
TestPersonWithNewMessage personWithUnknowns = TestPersonWithNewMessage.parseFrom(person.toByteArray());
assertTrue(ProtoUtil.hasUnknownFields(personWithUnknowns));
}
@Test
public void combineWithUnknownFields_noUnknowns() throws InvalidProtocolBufferException {
TestPerson personWithUnknowns = TestPerson.newBuilder()
.setName("Peter Parker")
.setAge(23)
.build();
TestPerson localRepresentation = TestPerson.newBuilder()
.setName("Spider-Man")
.setAge(23)
.build();
TestPerson combinedWithUnknowns = ProtoUtil.combineWithUnknownFields(localRepresentation, personWithUnknowns.toByteArray());
TestPersonWithNewString reparsedPerson = TestPersonWithNewString.parseFrom(combinedWithUnknowns.toByteArray());
Assert.assertEquals("Spider-Man", reparsedPerson.getName());
Assert.assertEquals(23, reparsedPerson.getAge());
}
@Test
public void combineWithUnknownFields_appendedString() throws InvalidProtocolBufferException {
TestPersonWithNewString personWithUnknowns = TestPersonWithNewString.newBuilder()
.setName("Peter Parker")
.setAge(23)
.setJob("Reporter")
.build();
TestPerson localRepresentation = TestPerson.newBuilder()
.setName("Spider-Man")
.setAge(23)
.build();
TestPerson combinedWithUnknowns = ProtoUtil.combineWithUnknownFields(localRepresentation, personWithUnknowns.toByteArray());
TestPersonWithNewString reparsedPerson = TestPersonWithNewString.parseFrom(combinedWithUnknowns.toByteArray());
Assert.assertEquals("Spider-Man", reparsedPerson.getName());
Assert.assertEquals(23, reparsedPerson.getAge());
Assert.assertEquals("Reporter", reparsedPerson.getJob());
}
@Test
public void combineWithUnknownFields_appendedRepeatedString() throws InvalidProtocolBufferException {
TestPersonWithNewRepeatedString personWithUnknowns = TestPersonWithNewRepeatedString.newBuilder()
.setName("Peter Parker")
.setAge(23)
.addJobs("Reporter")
.addJobs("Super Hero")
.build();
TestPerson localRepresentation = TestPerson.newBuilder()
.setName("Spider-Man")
.setAge(23)
.build();
TestPerson combinedWithUnknowns = ProtoUtil.combineWithUnknownFields(localRepresentation, personWithUnknowns.toByteArray());
TestPersonWithNewRepeatedString reparsedPerson = TestPersonWithNewRepeatedString.parseFrom(combinedWithUnknowns.toByteArray());
Assert.assertEquals("Spider-Man", reparsedPerson.getName());
Assert.assertEquals(23, reparsedPerson.getAge());
Assert.assertEquals(2, reparsedPerson.getJobsCount());
Assert.assertEquals("Reporter", reparsedPerson.getJobs(0));
Assert.assertEquals("Super Hero", reparsedPerson.getJobs(1));
}
@Test
public void combineWithUnknownFields_appendedStringAndInt() throws InvalidProtocolBufferException {
TestPersonWithNewStringAndInt personWithUnknowns = TestPersonWithNewStringAndInt.newBuilder()
.setName("Peter Parker")
.setAge(23)
.setJob("Reporter")
.setSalary(75_000)
.build();
TestPerson localRepresentation = TestPerson.newBuilder()
.setName("Spider-Man")
.setAge(23)
.build();
TestPerson combinedWithUnknowns = ProtoUtil.combineWithUnknownFields(localRepresentation, personWithUnknowns.toByteArray());
TestPersonWithNewStringAndInt reparsedPerson = TestPersonWithNewStringAndInt.parseFrom(combinedWithUnknowns.toByteArray());
Assert.assertEquals("Spider-Man", reparsedPerson.getName());
Assert.assertEquals(23, reparsedPerson.getAge());
Assert.assertEquals("Reporter", reparsedPerson.getJob());
Assert.assertEquals(75_000, reparsedPerson.getSalary());
}
@Test
public void combineWithUnknownFields_appendedMessage() throws InvalidProtocolBufferException {
TestPersonWithNewMessage personWithUnknowns = TestPersonWithNewMessage.newBuilder()
.setName("Peter Parker")
.setAge(23)
.setJob(TestPersonWithNewMessage.Job.newBuilder()
.setTitle("Reporter")
.setSalary(75_000))
.build();
TestPerson localRepresentation = TestPerson.newBuilder()
.setName("Spider-Man")
.setAge(23)
.build();
TestPerson combinedWithUnknowns = ProtoUtil.combineWithUnknownFields(localRepresentation, personWithUnknowns.toByteArray());
TestPersonWithNewMessage reparsedPerson = TestPersonWithNewMessage.parseFrom(combinedWithUnknowns.toByteArray());
Assert.assertEquals("Spider-Man", reparsedPerson.getName());
Assert.assertEquals(23, reparsedPerson.getAge());
Assert.assertEquals("Reporter", reparsedPerson.getJob().getTitle());
Assert.assertEquals(75_000, reparsedPerson.getJob().getSalary());
}
/**
* This isn't ideal behavior. This is more to show how something works. In the future, it'd be
* nice to support inner unknown fields.
*/
@Test
public void combineWithUnknownFields_innerMessagesUnknownsIgnored() throws InvalidProtocolBufferException {
TestInnerMessageWithNewString test = TestInnerMessageWithNewString.newBuilder()
.setInner(TestInnerMessageWithNewString.Inner.newBuilder()
.setA("a1")
.setB("b1")
.build())
.build();
TestInnerMessage localRepresentation = TestInnerMessage.newBuilder()
.setInner(TestInnerMessage.Inner.newBuilder()
.setA("a2")
.build())
.build();
TestInnerMessage combined = ProtoUtil.combineWithUnknownFields(localRepresentation, test.toByteArray());
TestInnerMessageWithNewString reparsedTest = TestInnerMessageWithNewString.parseFrom(combined.toByteArray());
Assert.assertEquals("a2", reparsedTest.getInner().getA());
Assert.assertEquals("", reparsedTest.getInner().getB());
}
}

View File

@ -0,0 +1,70 @@
syntax = "proto3";
package signal;
option java_package = "org.thoughtcrime.securesms.util.testprotos";
option java_multiple_files = true;
message TestPerson {
string name = 1;
int32 age = 2;
}
message TestPersonWithNewString {
string name = 1;
int32 age = 2;
string job = 3;
}
message TestPersonWithNewRepeatedString {
string name = 1;
int32 age = 2;
repeated string jobs = 3;
}
message TestPersonWithNewStringAndInt {
string name = 1;
int32 age = 2;
string job = 3;
int32 salary = 4;
}
message TestPersonWithNewMessage {
message Job {
string title = 1;
uint32 salary = 2;
}
string name = 1;
int32 age = 2;
Job job = 3;
}
message TestPersonWithNewFieldOnMessage {
message Job {
string title = 1;
uint32 salary = 2;
uint64 startDate = 3;
}
string name = 1;
int32 age = 2;
Job job = 3;
}
message TestInnerMessage {
message Inner {
string a = 1;
}
Inner inner = 1;
}
message TestInnerMessageWithNewString {
message Inner {
string a = 1;
string b = 2;
}
Inner inner = 1;
}