Initial Commit
This commit is contained in:
commit
d0417339c1
158 files changed
+31368
No files matched your search
@@ -0,0 +1,199 @@
|
||||
export class ProtobufUtils {
|
||||
static encodeVarint(value: number): Uint8Array {
|
||||
// Note: Javascript bitwise operators treat operands as 32-bit integers.
|
||||
// However, for typical token lengths and small field IDs, this is fine.
|
||||
// Expiry timestamp (seconds) fits in 32-bit (until 2038) but might exceed if it's milliseconds.
|
||||
// If value is larger than 2^32, we need BigInt.
|
||||
|
||||
const buf: number[] = [];
|
||||
let val = BigInt(value);
|
||||
|
||||
while (val >= 128n) {
|
||||
buf.push(Number((val & 127n) | 128n));
|
||||
val >>= 7n;
|
||||
}
|
||||
buf.push(Number(val));
|
||||
return new Uint8Array(buf);
|
||||
}
|
||||
|
||||
static readVarint(data: Uint8Array, offset: number): { value: bigint; nextOffset: number } {
|
||||
let result = 0n;
|
||||
let shift = 0n;
|
||||
let pos = offset;
|
||||
|
||||
while (pos < data.length) {
|
||||
const byte = BigInt(data[pos]);
|
||||
result |= (byte & 127n) << shift;
|
||||
pos++;
|
||||
if ((byte & 128n) === 0n) {
|
||||
return { value: result, nextOffset: pos };
|
||||
}
|
||||
shift += 7n;
|
||||
}
|
||||
throw new Error('Incomplete varint data');
|
||||
}
|
||||
|
||||
// Skip a field based on wire type
|
||||
static skipField(data: Uint8Array, offset: number, wireType: number): number {
|
||||
switch (wireType) {
|
||||
case 0: // Varint
|
||||
return this.readVarint(data, offset).nextOffset;
|
||||
case 1: // 64-bit
|
||||
return offset + 8;
|
||||
case 2: // Length-delimited
|
||||
const { value: length, nextOffset } = this.readVarint(data, offset);
|
||||
return nextOffset + Number(length);
|
||||
case 5: // 32-bit
|
||||
return offset + 4;
|
||||
default:
|
||||
throw new Error(`Unknown wire type: ${wireType}`);
|
||||
}
|
||||
}
|
||||
|
||||
static removeField(data: Uint8Array, fieldNum: number): Uint8Array {
|
||||
const result: number[] = [];
|
||||
let offset = 0;
|
||||
|
||||
while (offset < data.length) {
|
||||
const startOffset = offset;
|
||||
const { value: tag, nextOffset } = this.readVarint(data, offset);
|
||||
const wireType = Number(tag & 7n);
|
||||
const currentField = Number(tag >> 3n);
|
||||
|
||||
if (currentField === fieldNum) {
|
||||
// Skip
|
||||
offset = this.skipField(data, nextOffset, wireType);
|
||||
} else {
|
||||
// Copy
|
||||
const endOffset = this.skipField(data, nextOffset, wireType);
|
||||
for (let i = startOffset; i < endOffset; i++) {
|
||||
result.push(data[i]);
|
||||
}
|
||||
offset = endOffset;
|
||||
}
|
||||
}
|
||||
return new Uint8Array(result);
|
||||
}
|
||||
|
||||
// Create string field (WireType 2)
|
||||
static createStringField(fieldNum: number, value: string): Uint8Array {
|
||||
const tag = (fieldNum << 3) | 2;
|
||||
const utf8Encode = new TextEncoder();
|
||||
const bytes = utf8Encode.encode(value);
|
||||
|
||||
const tagBytes = this.encodeVarint(tag);
|
||||
const lenBytes = this.encodeVarint(bytes.length);
|
||||
|
||||
const result = new Uint8Array(tagBytes.length + lenBytes.length + bytes.length);
|
||||
result.set(tagBytes, 0);
|
||||
result.set(lenBytes, tagBytes.length);
|
||||
result.set(bytes, tagBytes.length + lenBytes.length);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Create timestamp field (Field 4 -> Field 1 varint)
|
||||
static createTimestampField(fieldNum: number, seconds: number): Uint8Array {
|
||||
// Timestamp message format: Field 1 (seconds) as varint
|
||||
const innerTag = (1 << 3) | 0;
|
||||
const innerTagBytes = this.encodeVarint(innerTag);
|
||||
const secondsBytes = this.encodeVarint(seconds);
|
||||
|
||||
const innerMsg = new Uint8Array(innerTagBytes.length + secondsBytes.length);
|
||||
innerMsg.set(innerTagBytes, 0);
|
||||
innerMsg.set(secondsBytes, innerTagBytes.length);
|
||||
|
||||
// Wrap in length delimited
|
||||
const tag = (fieldNum << 3) | 2;
|
||||
const tagBytes = this.encodeVarint(tag);
|
||||
const lenBytes = this.encodeVarint(innerMsg.length);
|
||||
|
||||
const result = new Uint8Array(tagBytes.length + lenBytes.length + innerMsg.length);
|
||||
result.set(tagBytes, 0);
|
||||
result.set(lenBytes, tagBytes.length);
|
||||
result.set(innerMsg, tagBytes.length + lenBytes.length);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Extract field by ID
|
||||
static getField(data: Uint8Array, fieldNum: number): Uint8Array | null {
|
||||
let offset = 0;
|
||||
while (offset < data.length) {
|
||||
const { value: tag, nextOffset } = this.readVarint(data, offset);
|
||||
const wireType = Number(tag & 7n);
|
||||
const currentField = Number(tag >> 3n);
|
||||
|
||||
if (currentField === fieldNum) {
|
||||
if (wireType === 2) {
|
||||
// Length delimited
|
||||
const { value: length, nextOffset: dataStart } = this.readVarint(data, nextOffset);
|
||||
return data.slice(dataStart, dataStart + Number(length));
|
||||
}
|
||||
// For sync feature, we mainly care about wireType 2 (string/bytes)
|
||||
return null;
|
||||
}
|
||||
|
||||
offset = this.skipField(data, nextOffset, wireType);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static readString(data: Uint8Array): string {
|
||||
const dec = new TextDecoder();
|
||||
return dec.decode(data);
|
||||
}
|
||||
|
||||
static extractOAuthTokenInfo(
|
||||
data: Uint8Array,
|
||||
): { accessToken: string; refreshToken: string } | null {
|
||||
// 1. Find Field 6 (OAuthTokenInfo)
|
||||
const field6Data = this.getField(data, 6);
|
||||
if (!field6Data) return null;
|
||||
|
||||
// 2. Parse Field 6 content
|
||||
// Field 1: Access Token (String)
|
||||
// Field 2: Type (String, "Bearer")
|
||||
// Field 3: Refresh Token (String)
|
||||
// Field 4: Expiry (Varint? Timestamp?) - In create we used createTimestampField which effectively makes it Field 4 -> Field 1.
|
||||
// But here we just need Access/Refresh.
|
||||
|
||||
const accessTokenBytes = this.getField(field6Data, 1);
|
||||
const refreshTokenBytes = this.getField(field6Data, 3);
|
||||
|
||||
if (accessTokenBytes && refreshTokenBytes) {
|
||||
return {
|
||||
accessToken: this.readString(accessTokenBytes),
|
||||
refreshToken: this.readString(refreshTokenBytes),
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static createOAuthTokenInfo(
|
||||
accessToken: string,
|
||||
refreshToken: string,
|
||||
expiry: number,
|
||||
): Uint8Array {
|
||||
const f1 = this.createStringField(1, accessToken);
|
||||
const f2 = this.createStringField(2, 'Bearer');
|
||||
const f3 = this.createStringField(3, refreshToken);
|
||||
const f4 = this.createTimestampField(4, expiry);
|
||||
|
||||
const combined = new Uint8Array(f1.length + f2.length + f3.length + f4.length);
|
||||
combined.set(f1, 0);
|
||||
combined.set(f2, f1.length);
|
||||
combined.set(f3, f1.length + f2.length);
|
||||
combined.set(f4, f1.length + f2.length + f3.length);
|
||||
|
||||
// Wrap as Field 6
|
||||
const tag6 = (6 << 3) | 2;
|
||||
const tag6Bytes = this.encodeVarint(tag6);
|
||||
const lenBytes = this.encodeVarint(combined.length);
|
||||
|
||||
const result = new Uint8Array(tag6Bytes.length + lenBytes.length + combined.length);
|
||||
result.set(tag6Bytes, 0);
|
||||
result.set(lenBytes, tag6Bytes.length);
|
||||
result.set(combined, tag6Bytes.length + lenBytes.length);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user