signal-desktop/ts/textsecure/ContactsParser.ts
Fedor Indutny 366b875fd2 Introduce Service Id Types
Co-authored-by: Scott Nonnenberg <scott@signal.org>
2023-08-21 09:30:32 -07:00

147 lines
3.5 KiB
TypeScript

// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable max-classes-per-file */
import protobuf from '../protobuf/wrap';
import { SignalService as Proto } from '../protobuf';
import { normalizeAci } from '../types/ServiceId';
import { DurationInSeconds } from '../util/durations';
import * as Errors from '../types/errors';
import * as log from '../logging/log';
import Avatar = Proto.ContactDetails.IAvatar;
const { Reader } = protobuf;
type OptionalFields = { avatar?: Avatar | null; expireTimer?: number | null };
type DecoderBase<Message extends OptionalFields> = {
decodeDelimited(reader: protobuf.Reader): Message | undefined;
};
type HydratedAvatar = Avatar & { data: Uint8Array };
type MessageWithAvatar<Message extends OptionalFields> = Omit<
Message,
'avatar'
> & {
avatar?: HydratedAvatar;
expireTimer?: DurationInSeconds;
};
export type ModifiedContactDetails = MessageWithAvatar<Proto.ContactDetails>;
/* eslint-disable @typescript-eslint/brace-style -- Prettier conflicts with ESLint */
abstract class ParserBase<
Message extends OptionalFields,
Decoder extends DecoderBase<Message>,
Result
> implements Iterable<Result>
{
/* eslint-enable @typescript-eslint/brace-style */
protected readonly reader: protobuf.Reader;
constructor(bytes: Uint8Array, private readonly decoder: Decoder) {
this.reader = new Reader(bytes);
}
protected decodeDelimited(): MessageWithAvatar<Message> | undefined {
if (this.reader.pos === this.reader.len) {
return undefined; // eof
}
try {
const proto = this.decoder.decodeDelimited(this.reader);
if (!proto) {
return undefined;
}
let avatar: HydratedAvatar | undefined;
if (proto.avatar) {
const attachmentLen = proto.avatar.length ?? 0;
const avatarData = this.reader.buf.slice(
this.reader.pos,
this.reader.pos + attachmentLen
);
this.reader.skip(attachmentLen);
avatar = {
...proto.avatar,
data: avatarData,
};
}
let expireTimer: DurationInSeconds | undefined;
if (proto.expireTimer != null) {
expireTimer = DurationInSeconds.fromSeconds(proto.expireTimer);
}
return {
...proto,
avatar,
expireTimer,
};
} catch (error) {
log.error('ProtoParser.next error:', Errors.toLogFormat(error));
return undefined;
}
}
public abstract next(): Result | undefined;
*[Symbol.iterator](): Iterator<Result> {
let result = this.next();
while (result !== undefined) {
yield result;
result = this.next();
}
}
}
export class ContactBuffer extends ParserBase<
Proto.ContactDetails,
typeof Proto.ContactDetails,
ModifiedContactDetails
> {
constructor(arrayBuffer: Uint8Array) {
super(arrayBuffer, Proto.ContactDetails);
}
public override next(): ModifiedContactDetails | undefined {
const proto = this.decodeDelimited();
if (!proto) {
return undefined;
}
if (!proto.aci) {
return proto;
}
const { verified } = proto;
return {
...proto,
verified:
verified && verified.destinationAci
? {
...verified,
destinationAci: normalizeAci(
verified.destinationAci,
'ContactBuffer.verified.destinationAci'
),
}
: verified,
aci: normalizeAci(proto.aci, 'ContactBuffer.aci'),
};
}
}