mtcute/packages/file-id/src/parse.ts
Alina Tumanova f5976a2d74
ESM + end-to-end tests (#11)
* feat: moved tl-runtime to esm and native ArrayBuffers

* feat: migration to esm

* fix(core): web-related fixes

* test: finally, some good fucking e2e

* chore: fixed linters etc

* ci: added e2e to ci

* build(tl): fixed gen-code on node 20

* fix: codegen Uint8Array, not Buffer

never `git reset --hard` kids

* build: only do type-aware linting for `packages/*`

* build: ignore no-unresolved in ci for e2e

* fix: node 16 doesn't have subtle crypto apparently?

* fix(tests): use Uint8Array

for gods sake please can i just merge this already

* ci: don't parallel tasks in ci

because machines are utter garbage and it may just randomly break

* ci: pass secrets to e2e tests

* ci: separate cli command for ci

apparently im retarded

* fix: run codegen in e2e

im actually retarded

* ci: more fixes for e2e

* ci: debugging stuff

* ci: still debugging

* ci: hopefully fix ci???
2023-10-16 19:23:53 +03:00

332 lines
11 KiB
TypeScript

import { base64DecodeToBuffer, base64Encode, TlBinaryReader } from '@mtcute/core/utils.js'
import { tdFileId as td } from './types.js'
import { telegramRleDecode } from './utils.js'
function parseWebFileLocation(reader: TlBinaryReader): td.RawWebRemoteFileLocation {
return {
_: 'web',
url: reader.string(),
accessHash: reader.long(),
}
}
function parsePhotoSizeSource(reader: TlBinaryReader): td.TypePhotoSizeSource {
const variant = reader.int()
switch (variant) {
case 0 /* LEGACY */:
return {
_: 'legacy',
secret: reader.long(),
}
case 1 /* THUMBNAIL */: {
const fileType = reader.int()
if (fileType < 0 || fileType >= td.FileType.Size) {
throw new td.UnsupportedError(`Unsupported file type: ${fileType} (${base64Encode(reader.uint8View)})`)
}
const thumbnailType = reader.int()
if (thumbnailType < 0 || thumbnailType > 255) {
throw new td.InvalidFileIdError(
`Wrong thumbnail type: ${thumbnailType} (${base64Encode(reader.uint8View)})`,
)
}
return {
_: 'thumbnail',
fileType,
thumbnailType: String.fromCharCode(thumbnailType),
}
}
case 2 /* DIALOG_PHOTO_SMALL */:
case 3 /* DIALOG_PHOTO_BIG */:
return {
_: 'dialogPhoto',
big: variant === 3,
id: reader.int53(),
accessHash: reader.long(),
}
case 4 /* STICKERSET_THUMBNAIL */:
return {
_: 'stickerSetThumbnail',
id: reader.long(),
accessHash: reader.long(),
}
case 5 /* FULL_LEGACY */: {
const res: td.RawPhotoSizeSourceFullLegacy = {
_: 'fullLegacy',
volumeId: reader.long(),
secret: reader.long(),
localId: reader.int(),
}
if (res.localId < 0) {
throw new td.InvalidFileIdError('Wrong local_id (< 0)')
}
return res
}
case 6 /* DIALOG_PHOTO_SMALL_LEGACY */:
case 7 /* DIALOG_PHOTO_BIG_LEGACY */: {
const res: td.RawPhotoSizeSourceDialogPhotoLegacy = {
_: 'dialogPhotoLegacy',
big: variant === 7,
id: reader.int53(),
accessHash: reader.long(),
volumeId: reader.long(),
localId: reader.int(),
}
if (res.localId < 0) {
throw new td.InvalidFileIdError('Wrong local_id (< 0)')
}
return res
}
case 8 /* STICKERSET_THUMBNAIL_LEGACY */: {
const res: td.RawPhotoSizeSourceStickerSetThumbnailLegacy = {
_: 'stickerSetThumbnailLegacy',
id: reader.long(),
accessHash: reader.long(),
volumeId: reader.long(),
localId: reader.int(),
}
if (res.localId < 0) {
throw new td.InvalidFileIdError('Wrong local_id (< 0)')
}
return res
}
case 9 /* STICKERSET_THUMBNAIL_VERSION */:
return {
_: 'stickerSetThumbnailVersion',
id: reader.long(),
accessHash: reader.long(),
version: reader.int(),
}
default:
throw new td.UnsupportedError(
`Unsupported photo size source ${variant} (${base64Encode(reader.uint8View)})`,
)
}
}
function parsePhotoFileLocation(reader: TlBinaryReader, version: number): td.RawPhotoRemoteFileLocation {
const id = reader.long()
const accessHash = reader.long()
let source: td.TypePhotoSizeSource
if (version >= 32) {
source = parsePhotoSizeSource(reader)
} else {
const volumeId = reader.long()
let localId = 0
if (version >= 22) {
source = parsePhotoSizeSource(reader)
localId = reader.int()
} else {
source = {
_: 'fullLegacy',
secret: reader.long(),
localId: reader.int(),
volumeId,
}
}
switch (source._) {
case 'legacy':
source = {
_: 'fullLegacy',
secret: reader.long(),
localId: reader.int(),
volumeId,
}
break
case 'fullLegacy':
case 'thumbnail':
break
case 'dialogPhoto':
source = {
_: 'dialogPhotoLegacy',
id: source.id,
accessHash: source.accessHash,
big: source.big,
localId,
volumeId,
}
break
case 'stickerSetThumbnail':
source = {
_: 'stickerSetThumbnailLegacy',
id: source.id,
accessHash: source.accessHash,
localId,
volumeId,
}
break
default:
throw new td.InvalidFileIdError('Invalid PhotoSizeSource in legacy PhotoRemoteFileLocation')
}
}
return {
_: 'photo',
id,
accessHash,
source,
}
}
function parseCommonFileLocation(reader: TlBinaryReader): td.RawCommonRemoteFileLocation {
return {
_: 'common',
id: reader.long(),
accessHash: reader.long(),
}
}
function fromPersistentIdV23(binary: Uint8Array, version: number): td.RawFullRemoteFileLocation {
if (version < 0 || version > td.CURRENT_VERSION) {
throw new td.UnsupportedError(`Unsupported file ID v3 subversion: ${version} (${base64Encode(binary)})`)
}
binary = telegramRleDecode(binary)
const reader = TlBinaryReader.manual(binary)
let fileType = reader.int()
const isWeb = Boolean(fileType & td.WEB_LOCATION_FLAG)
const hasFileReference = Boolean(fileType & td.FILE_REFERENCE_FLAG)
fileType &= ~td.WEB_LOCATION_FLAG
fileType &= ~td.FILE_REFERENCE_FLAG
if (fileType < 0 || fileType >= td.FileType.Size) {
throw new td.UnsupportedError(`Unsupported file type: ${fileType} (${base64Encode(binary)})`)
}
const dcId = reader.int()
let fileReference: Uint8Array | null = null
if (hasFileReference) {
fileReference = reader.bytes()
if (fileReference.length === 1 && fileReference[0] === 0x23 /* # */) {
// "invalid file reference"
// see https://github.com/tdlib/td/blob/ed291840d3a841bb5b49457c88c57e8467e4a5b0/td/telegram/files/FileLocation.h#L32
fileReference = null
}
}
let location: td.TypeRemoteFileLocation
if (isWeb) {
location = parseWebFileLocation(reader)
} else {
switch (fileType) {
case td.FileType.Photo:
case td.FileType.ProfilePhoto:
case td.FileType.Thumbnail:
case td.FileType.EncryptedThumbnail:
case td.FileType.Wallpaper: {
// location_type = photo
location = parsePhotoFileLocation(reader, version)
// validate
switch (location.source._) {
case 'thumbnail':
if (
location.source.fileType !== fileType ||
(fileType !== td.FileType.Photo &&
fileType !== td.FileType.Thumbnail &&
fileType !== td.FileType.EncryptedThumbnail)
) {
throw new td.InvalidFileIdError('Invalid FileType in PhotoRemoteFileLocation Thumbnail')
}
break
case 'dialogPhoto':
case 'dialogPhotoLegacy':
if (fileType !== td.FileType.ProfilePhoto) {
throw new td.InvalidFileIdError('Invalid FileType in PhotoRemoteFileLocation DialogPhoto')
}
break
case 'stickerSetThumbnail':
case 'stickerSetThumbnailLegacy':
case 'stickerSetThumbnailVersion':
if (fileType !== td.FileType.Thumbnail) {
throw new td.InvalidFileIdError(
'Invalid FileType in PhotoRemoteFileLocation StickerSetThumbnail',
)
}
break
}
break
}
case td.FileType.Video:
case td.FileType.VoiceNote:
case td.FileType.Document:
case td.FileType.Sticker:
case td.FileType.Audio:
case td.FileType.Animation:
case td.FileType.Encrypted:
case td.FileType.VideoNote:
case td.FileType.SecureRaw:
case td.FileType.Secure:
case td.FileType.Background:
case td.FileType.DocumentAsFile: {
// location_type = common
location = parseCommonFileLocation(reader)
break
}
default:
throw new td.UnsupportedError(`Invalid file type: ${fileType} (${base64Encode(binary)})`)
}
}
return {
_: 'remoteFileLocation',
dcId,
type: fileType,
fileReference,
location,
}
}
function fromPersistentIdV2(binary: Uint8Array) {
return fromPersistentIdV23(binary.subarray(0, -1), 0)
}
function fromPersistentIdV3(binary: Uint8Array) {
const subversion = binary[binary.length - 2]
return fromPersistentIdV23(binary.subarray(0, -2), subversion)
}
/**
* Parse TDLib and Bot API compatible File ID
*
* @param fileId File ID as a base-64 encoded string or Buffer
*/
export function parseFileId(fileId: string | Uint8Array): td.RawFullRemoteFileLocation {
if (typeof fileId === 'string') fileId = base64DecodeToBuffer(fileId, true)
const version = fileId[fileId.length - 1]
if (version === td.PERSISTENT_ID_VERSION_OLD) {
return fromPersistentIdV2(fileId)
}
if (version === td.PERSISTENT_ID_VERSION) {
return fromPersistentIdV3(fileId)
}
throw new td.UnsupportedError(`Unsupported file ID version: ${version} (${base64Encode(fileId)})`)
}