fix: properly handle file uploads + downloading as node stream

This commit is contained in:
alina 🌸 2024-03-01 01:52:17 +03:00
parent a2739b678c
commit fb72d3194d
Signed by: teidesu
SSH key fingerprint: SHA256:uNeCpw6aTSU4aIObXLvHfLkDa82HWH9EiOj9AXOIRpI
39 changed files with 388 additions and 263 deletions

View file

@ -11,6 +11,9 @@ export default defineConfig({
'packages/**/*.test-d.ts',
],
},
setupFiles: [
'./.config/vitest.setup.mts'
]
},
define: {
'import.meta.env.TEST_ENV': '"node"'

9
.config/vitest.setup.mts Normal file
View file

@ -0,0 +1,9 @@
import { setPlatform } from '../packages/core/src/platform.js'
// @ts-expect-error no .env here
const TEST_ENV = import.meta.env.TEST_ENV
if (TEST_ENV === 'browser') {
setPlatform(new (await import('../packages/web/src/platform.js')).WebPlatform())
} else {
setPlatform(new (await import('../packages/node/src/platform.js')).NodePlatform())
}

View file

@ -2248,7 +2248,7 @@ export interface TelegramClient extends ITelegramClient {
downloadAsBuffer(location: FileDownloadLocation, params?: FileDownloadParameters): Promise<Uint8Array>
/**
* Download a remote file to a local file (only for NodeJS).
* Download a remote file to a local file (only for Node.js).
* Promise will resolve once the download is complete.
*
* **Available**: both users and bots
@ -2267,6 +2267,15 @@ export interface TelegramClient extends ITelegramClient {
* @param params Download parameters
*/
downloadAsIterable(input: FileDownloadLocation, params?: FileDownloadParameters): AsyncIterableIterator<Uint8Array>
/**
* Download a remote file as a Node.js Readable stream.
*
* **Available**: both users and bots
*
* @param params File download parameters
*/
downloadAsNodeStream(location: FileDownloadLocation, params?: FileDownloadParameters): import('stream').Readable
/**
* Download a file and return it as a readable stream,
* streaming file contents.
@ -2320,9 +2329,6 @@ export interface TelegramClient extends ITelegramClient {
uploadFile(params: {
/**
* Upload file source.
*
* > **Note**: `fs.ReadStream` is a subclass of `stream.Readable` and contains
* > info about file name, thus you don't need to pass them explicitly.
*/
file: UploadFileLike

View file

@ -1,32 +0,0 @@
import { createReadStream, promises, ReadStream } from 'node:fs'
import { basename } from 'node:path'
import { Readable } from 'node:stream'
import { nodeReadableToWeb } from '../../utils/stream-utils.js'
/** @internal */
export function _createFileStream(path: string): ReadStream {
return createReadStream(path)
}
/** @internal */
export function _isFileStream(stream: unknown): stream is ReadStream {
return stream instanceof ReadStream
}
/** @internal */
export async function _extractFileStreamMeta(stream: ReadStream): Promise<[string, number]> {
const fileName = basename(stream.path.toString())
const fileSize = await promises.stat(stream.path.toString()).then((stat) => stat.size)
return [fileName, fileSize]
}
/** @internal */
export function _handleNodeStream<T>(val: T | Readable): T | ReadableStream<Uint8Array> {
if (val instanceof Readable) {
return nodeReadableToWeb(val)
}
return val
}

View file

@ -1,23 +0,0 @@
import { MtArgumentError } from '../../../types/errors.js'
/** @internal */
export function _createFileStream(): never {
throw new MtArgumentError('Cannot create file stream on web platform')
}
/** @internal */
export function _isFileStream() {
return false
}
/** @internal */
export function _extractFileStreamMeta(): never {
throw new Error('UNREACHABLE')
}
/** @internal */
export function _handleNodeStream(val: unknown) {
return val
}
// all the above functions shall be inlined by terser

View file

@ -5,7 +5,7 @@ import { FileDownloadLocation, FileDownloadParameters } from '../../types/index.
// @available=both
/**
* Download a remote file to a local file (only for NodeJS).
* Download a remote file to a local file (only for Node.js).
* Promise will resolve once the download is complete.
*
* @param filename Local file name to which the remote file will be downloaded

View file

@ -0,0 +1,16 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { ITelegramClient } from '../../client.types.js'
import { FileDownloadLocation, FileDownloadParameters } from '../../types/index.js'
// @available=both
/**
* Download a remote file as a Node.js Readable stream.
*
* @param params File download parameters
*/
declare function downloadAsNodeStream(
client: ITelegramClient,
location: FileDownloadLocation,
params?: FileDownloadParameters,
): import('stream').Readable

View file

@ -1,5 +1,6 @@
import { tl } from '@mtcute/tl'
import { getPlatform } from '../../../platform.js'
import { MtArgumentError } from '../../../types/errors.js'
import { randomLong } from '../../../utils/long-utils.js'
import { ITelegramClient } from '../../client.types.js'
@ -21,6 +22,10 @@ const REQUESTS_PER_CONNECTION = 3
const MAX_PART_COUNT = 4000 // 512 kb * 4000 = 2000 MiB
const MAX_PART_COUNT_PREMIUM = 8000 // 512 kb * 8000 = 4000 MiB
// platform-specific
const HAS_FILE = typeof File !== 'undefined'
const HAS_RESPONSE = typeof Response !== 'undefined'
// @available=both
/**
* Upload a file to Telegram servers, without actually
@ -101,19 +106,30 @@ export async function uploadFile(
let fileName = DEFAULT_FILE_NAME
let fileMime = params.fileMime
const platform = getPlatform()
if (platform.normalizeFile) {
const res = await platform.normalizeFile(file)
if (res?.file) {
file = res.file
if (res.fileSize) fileSize = res.fileSize
if (res.fileName) fileName = res.fileName
}
}
if (ArrayBuffer.isView(file)) {
fileSize = file.length
file = bufferToStream(file)
}
if (typeof File !== 'undefined' && file instanceof File) {
if (HAS_FILE && file instanceof File) {
fileName = file.name
fileSize = file.size
file = file.stream()
}
if (typeof file === 'object' && 'headers' in file && 'body' in file && 'url' in file) {
// fetch() response
if (HAS_RESPONSE && file instanceof Response) {
const length = parseInt(file.headers.get('content-length') || '0')
if (!isNaN(length) && length) fileSize = length

View file

@ -1,7 +1,6 @@
import { Readable } from 'node:stream'
import { describe, expect, it } from 'vitest'
import { createChunkedReader, nodeReadableToWeb } from './stream-utils.js'
import { createChunkedReader } from './stream-utils.js'
describe('createChunkedReader', () => {
it('should correctly handle chunks smaller than chunkSize', async () => {
@ -82,26 +81,3 @@ describe('createChunkedReader', () => {
expect(await reader.read()).to.be.null
})
})
if (import.meta.env.TEST_ENV === 'node' || import.meta.env.TEST_ENV === 'bun') {
describe('nodeReadableToWeb', () => {
it('should correctly convert a readable stream', async () => {
const stream = new Readable({
read() {
// eslint-disable-next-line no-restricted-globals
this.push(Buffer.from([1, 2, 3]))
// eslint-disable-next-line no-restricted-globals
this.push(Buffer.from([4, 5, 6]))
this.push(null)
},
})
const webStream = nodeReadableToWeb(stream)
const reader = webStream.getReader()
expect(await reader.read()).to.deep.equal({ value: new Uint8Array([1, 2, 3]), done: false })
expect(await reader.read()).to.deep.equal({ value: new Uint8Array([4, 5, 6]), done: false })
expect(await reader.read()).to.deep.equal({ value: undefined, done: true })
})
})
}

View file

@ -135,36 +135,3 @@ export function createChunkedReader(stream: ReadableStream<Uint8Array>, chunkSiz
read: readLocked,
}
}
export function nodeReadableToWeb(stream: NodeJS.ReadableStream): ReadableStream {
// using .constructor here to avoid import hacks
const ctor = stream.constructor as {
toWeb?: (stream: NodeJS.ReadableStream) => ReadableStream
}
if (ctor.toWeb) {
// use `Readable.toWeb` if available
return ctor.toWeb(stream)
}
// otherwise, use a silly little adapter
stream.pause()
return new ReadableStream({
start(c) {
stream.on('data', (chunk) => {
c.enqueue(chunk)
})
stream.on('end', () => {
c.close()
})
stream.on('error', (err) => {
c.error(err)
})
},
pull() {
stream.resume()
},
})
}

View file

@ -1,15 +1,39 @@
import { ITlPlatform, TlBinaryReader, TlBinaryWriter } from '@mtcute/tl-runtime'
import { UploadFileLike } from './highlevel/types/files/utils.js'
import { MtUnsupportedError } from './types/errors.js'
import { MaybePromise } from './types/index.js'
export interface ICorePlatform extends ITlPlatform {
beforeExit(fn: () => void): () => void
log(color: number, level: number, tag: string, fmt: string, args: unknown[]): void
getDefaultLogLevel(): number | null
getDeviceModel(): string
normalizeFile?(file: UploadFileLike): MaybePromise<{
file?: UploadFileLike
fileSize?: number
fileName?: string
} | null>
}
let _platform: ICorePlatform | null = null
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let globalObject: any
if (typeof globalThis !== 'undefined') {
globalObject = globalThis
} else if (typeof global !== 'undefined') {
globalObject = global
} else if (typeof self !== 'undefined') {
globalObject = self
} else if (typeof window !== 'undefined') {
globalObject = window
}
// NB: when using with some bundlers (e.g. vite) re-importing this module will not return the same object
// so we need to store the platform in a global object to be able to survive hot-reloads etc.
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
let _platform: ICorePlatform | null = globalObject?.__MTCUTE_PLATFORM__ ?? null
export function setPlatform(platform: ICorePlatform): void {
if (_platform) {
@ -23,6 +47,10 @@ export function setPlatform(platform: ICorePlatform): void {
_platform = platform
TlBinaryReader.platform = platform
TlBinaryWriter.platform = platform
if (globalObject) {
globalObject.__MTCUTE_PLATFORM__ = platform
}
}
export function getPlatform(): ICorePlatform {

View file

@ -1,4 +1,4 @@
import { describe, expect, it } from 'vitest'
import { beforeAll, describe, expect, it } from 'vitest'
import { defaultCryptoProvider } from '@mtcute/test'
@ -6,6 +6,10 @@ import { findKeyByFingerprints, parsePublicKey } from '../index.js'
const crypto = defaultCryptoProvider
beforeAll(async () => {
await crypto.initialize()
})
describe('parsePublicKey', () => {
it('should parse telegram public keys', () => {
expect(

View file

@ -1,12 +1,14 @@
import { describe, expect, it } from 'vitest'
import { PeersIndex, TelegramClient } from '@mtcute/core'
import { PeersIndex } from '@mtcute/core'
import { TelegramClient } from '@mtcute/core/client.js'
import { StubTelegramClient } from '@mtcute/test'
import { Dispatcher, PropagationAction } from '../src/index.js'
describe('Dispatcher', () => {
// todo: replace with proper mocked TelegramClient
const client = new TelegramClient({ apiId: 0, apiHash: '' })
const client = new TelegramClient({ client: new StubTelegramClient({ disableUpdates: false }) })
const emptyPeers = new PeersIndex()
describe('Raw updates', () => {

View file

@ -7,7 +7,7 @@ import { setPlatform } from '@mtcute/core/platform.js'
import { SqliteStorage } from '@mtcute/sqlite'
import { downloadToFile } from './methods/download-file.js'
import { uploadFile } from './methods/upload-file.js'
import { downloadAsNodeStream } from './methods/download-node-stream.js'
import { NodePlatform } from './platform.js'
import { NodeCryptoProvider } from './utils/crypto.js'
import { TcpTransport } from './utils/tcp.js'
@ -120,7 +120,10 @@ export class TelegramClient extends TelegramClientBase {
return downloadToFile(this, filename, location, params)
}
uploadFile(params: Parameters<typeof uploadFile>[1]) {
return uploadFile(this, params)
downloadAsNodeStream(
location: FileDownloadLocation,
params?: FileDownloadParameters | undefined,
) {
return downloadAsNodeStream(this, location, params)
}
}

View file

@ -1,7 +1,7 @@
export * from './client.js'
export * from './platform.js'
export * from './utils/tcp.js'
export * from './utils/crypto.js'
export * from './utils/tcp.js'
export * from '@mtcute/core'
export * from '@mtcute/html-parser'
export * from '@mtcute/markdown-parser'

View file

@ -2,4 +2,4 @@
export * from '@mtcute/core/methods.js'
export { downloadToFile } from './methods/download-file.js'
export { uploadFile } from './methods/upload-file.js'
export { downloadAsNodeStream } from './methods/download-node-stream.js'

View file

@ -0,0 +1,19 @@
import { Readable } from 'stream'
import { FileDownloadLocation, FileDownloadParameters, ITelegramClient } from '@mtcute/core'
import { downloadAsStream } from '@mtcute/core/methods.js'
import { webStreamToNode } from '../utils/stream-utils.js'
/**
* Download a remote file as a Node.js Readable stream.
*
* @param params File download parameters
*/
export function downloadAsNodeStream(
client: ITelegramClient,
location: FileDownloadLocation,
params?: FileDownloadParameters,
): Readable {
return webStreamToNode(downloadAsStream(client, location, params))
}

View file

@ -4,6 +4,7 @@ import { ICorePlatform } from '@mtcute/core/platform.js'
import { beforeExit } from './utils/exit-hook.js'
import { defaultLoggingHandler } from './utils/logging.js'
import { normalizeFile } from './utils/normalize-file.js'
const BUFFER_BASE64_URL_AVAILABLE = typeof Buffer.isEncoding === 'function' && Buffer.isEncoding('base64url')
@ -17,6 +18,7 @@ export class NodePlatform implements ICorePlatform {
// ICorePlatform
log!: typeof defaultLoggingHandler
beforeExit!: typeof beforeExit
normalizeFile!: typeof normalizeFile
getDeviceModel(): string {
return `${os.type()} ${os.arch()} ${os.release()}`
@ -76,3 +78,4 @@ export class NodePlatform implements ICorePlatform {
NodePlatform.prototype.log = defaultLoggingHandler
NodePlatform.prototype.beforeExit = beforeExit
NodePlatform.prototype.normalizeFile = normalizeFile

View file

@ -1 +1,2 @@
export * from './utils/stream-utils.js'
export * from '@mtcute/core/utils.js'

View file

@ -3,17 +3,11 @@ import { stat } from 'fs/promises'
import { basename } from 'path'
import { Readable } from 'stream'
import { ITelegramClient } from '@mtcute/core'
import { uploadFile as uploadFileCore } from '@mtcute/core/methods.js'
import { UploadFileLike } from '@mtcute/core'
import { nodeStreamToWeb } from '../utils/stream-utils.js'
export async function uploadFile(
client: ITelegramClient,
params: Parameters<typeof uploadFileCore>[1],
) {
let file = params.file
export async function normalizeFile(file: UploadFileLike) {
if (typeof file === 'string') {
file = createReadStream(file)
}
@ -22,20 +16,19 @@ export async function uploadFile(
const fileName = basename(file.path.toString())
const fileSize = await stat(file.path.toString()).then((stat) => stat.size)
return uploadFileCore(client, {
...params,
return {
file: nodeStreamToWeb(file),
fileName,
fileSize,
})
}
}
if (file instanceof Readable) {
return uploadFileCore(client, {
...params,
return {
file: nodeStreamToWeb(file),
})
}
}
return uploadFileCore(client, params)
// string -> ReadStream, thus already handled
return null
}

View file

@ -0,0 +1,59 @@
import { Readable } from 'stream'
import { describe, expect, it } from 'vitest'
import { nodeStreamToWeb, webStreamToNode } from './stream-utils.js'
if (import.meta.env.TEST_ENV === 'node' || import.meta.env.TEST_ENV === 'bun') {
describe('nodeStreamToWeb', () => {
it('should correctly convert a readable stream', async () => {
const stream = new Readable({
read() {
// eslint-disable-next-line no-restricted-globals
this.push(Buffer.from([1, 2, 3]))
// eslint-disable-next-line no-restricted-globals
this.push(Buffer.from([4, 5, 6]))
this.push(null)
},
})
const webStream = nodeStreamToWeb(stream)
const reader = webStream.getReader()
expect(await reader.read()).to.deep.equal({ value: new Uint8Array([1, 2, 3]), done: false })
expect(await reader.read()).to.deep.equal({ value: new Uint8Array([4, 5, 6]), done: false })
expect(await reader.read()).to.deep.equal({ value: undefined, done: true })
})
})
describe('webStreamToNode', () => {
it('should correctly convert a readable stream', async () => {
const stream = new ReadableStream<Uint8Array>({
start(controller) {
controller.enqueue(new Uint8Array([1, 2, 3]))
controller.enqueue(new Uint8Array([4, 5, 6]))
controller.close()
},
})
const nodeStream = webStreamToNode(stream)
const chunks: Buffer[] = []
nodeStream.on('data', (chunk) => {
chunks.push(chunk as Buffer)
})
await new Promise<void>((resolve, reject) => {
nodeStream.on('end', () => {
try {
expect(chunks).to.deep.equal([Buffer.from([1, 2, 3]), Buffer.from([4, 5, 6])])
resolve()
} catch (err) {
reject(err)
}
})
})
})
})
} else {
describe.skip('node stream utils', () => {})
}

View file

@ -1,26 +1,78 @@
import { Readable } from 'stream'
import { isNodeVersionAfter } from './version.js'
export function nodeStreamToWeb(stream: Readable): ReadableStream<Uint8Array> {
if (typeof Readable.toWeb === 'function') {
return Readable.toWeb(stream)
}
// otherwise, use a silly little adapter
stream.pause()
return new ReadableStream({
start(controller) {
start(c) {
stream.on('data', (chunk) => {
controller.enqueue(chunk)
c.enqueue(chunk as Uint8Array)
})
stream.on('end', () => {
controller.close()
c.close()
})
stream.on('error', (err) => {
controller.error(err)
c.error(err)
})
},
cancel() {
if (typeof stream.destroy === 'function') {
stream.destroy()
}
pull() {
stream.resume()
},
})
}
export function webStreamToNode(stream: ReadableStream<Uint8Array>): Readable {
if (
typeof Readable.fromWeb === 'function' &&
isNodeVersionAfter(18, 13, 0) // https://github.com/nodejs/node/issues/42694
) {
// @ts-expect-error node typings are wrong lmao
return Readable.fromWeb(stream)
}
const reader = stream.getReader()
let ended = false
const readable = new Readable({
async read() {
try {
const { done, value } = await reader.read()
if (done) {
this.push(null)
} else {
this.push(Buffer.from(value.buffer, value.byteOffset, value.byteLength))
}
} catch (err) {
this.destroy(err as Error)
}
},
destroy(error, cb) {
if (!ended) {
void reader.cancel(error).catch(() => {}).then(() => {
cb(error)
})
return
}
cb(error)
},
})
reader.closed.then(() => {
ended = true
}).catch((err) => {
readable.destroy(err as Error)
})
return readable
}

View file

@ -4,7 +4,6 @@ import { describe, expect, it, MockedObject, vi } from 'vitest'
import { TransportState } from '@mtcute/core'
import { getPlatform } from '@mtcute/core/platform.js'
import { defaultProductionDc, LogManager } from '@mtcute/core/utils.js'
import { defaultTestCryptoProvider, u8HexDecode } from '@mtcute/test'
if (import.meta.env.TEST_ENV === 'node' || import.meta.env.TEST_ENV === 'bun') {
vi.doMock('net', () => ({
@ -27,6 +26,7 @@ if (import.meta.env.TEST_ENV === 'node' || import.meta.env.TEST_ENV === 'bun') {
const connect = vi.mocked(net.connect)
const { TcpTransport } = await import('./tcp.js')
const { defaultTestCryptoProvider, u8HexDecode } = await import('@mtcute/test')
describe('TcpTransport', () => {
const getLastSocket = () => {

View file

@ -0,0 +1,14 @@
export const NODE_VERSION = typeof process !== 'undefined' && 'node' in process.versions ? process.versions.node : null
export const NODE_VERSION_TUPLE = NODE_VERSION ? NODE_VERSION.split('.').map(Number) : null
export function isNodeVersionAfter(major: number, minor: number, patch: number): boolean {
if (!NODE_VERSION_TUPLE) return true // assume non-node environment is always "after"
const [a, b, c] = NODE_VERSION_TUPLE
if (a > major) return true
if (a < major) return false
if (b > minor) return true
if (b < minor) return false
return c >= patch
}

View file

@ -1,7 +1,6 @@
export * from './client.js'
export * from './crypto.js'
export * from './platform.js'
export * from './platform.js'
export * from './storage.js'
export * from './storage/index.js'
export * from './stub.js'

View file

@ -94,6 +94,10 @@ describe('TlBinaryWriter', () => {
expect(testSingleMethod(1004, (w) => w.bytes(random1000bytes))).toEqual(hexEncode(buffer))
})
it('should write tg-encoded string', () => {
expect(testSingleMethod(8, (w) => w.string('test'))).toEqual('0474657374000000')
})
const stubObjectsMap: TlWriterMap = {
deadbeef: function (w, obj) {
w.uint(0xdeadbeef)

View file

@ -26,6 +26,8 @@
}
},
"devDependencies": {
"@mtcute/tl-runtime": "workspace:^"
"@mtcute/core": "workspace:^",
"@mtcute/web": "workspace:^",
"@mtcute/node": "workspace:^"
}
}

View file

@ -1,26 +1,29 @@
import { beforeAll, describe, expect, it } from 'vitest'
import { hexDecodeToBuffer, hexEncode } from '@mtcute/tl-runtime'
import { getPlatform } from '@mtcute/core/platform.js'
import { __getWasm, createCtr256, ctr256, freeCtr256, initAsync } from '../src/index.js'
import { __getWasm, createCtr256, ctr256, freeCtr256 } from '../src/index.js'
import { initWasm } from './init.js'
const p = getPlatform()
beforeAll(async () => {
await initAsync()
await initWasm()
})
describe('aes-ctr', () => {
const key = hexDecodeToBuffer('603DEB1015CA71BE2B73AEF0857D77811F352C073B6108D72D9810A30914DFF4')
const iv = hexDecodeToBuffer('F0F1F2F3F4F5F6F7F8F9FAFBFCFDFEFF')
const key = p.hexDecode('603DEB1015CA71BE2B73AEF0857D77811F352C073B6108D72D9810A30914DFF4')
const iv = p.hexDecode('F0F1F2F3F4F5F6F7F8F9FAFBFCFDFEFF')
describe('NIST', () => {
// https://csrc.nist.gov/CSRC/media/Projects/Cryptographic-Standards-and-Guidelines/documents/examples/AES_CTR.pdf
const data = hexDecodeToBuffer(
const data = p.hexDecode(
`6BC1BEE2 2E409F96 E93D7E11 7393172A
AE2D8A57 1E03AC9C 9EB76FAC 45AF8E51
30C81C46 A35CE411 E5FBC119 1A0A52EF
F69F2445 DF4F9B17 AD2B417B E66C3710`.replace(/\s/g, ''),
)
const dataEnc = hexDecodeToBuffer(
const dataEnc = p.hexDecode(
`601EC313 775789A5 B7A7F504 BBF3D228
F443E3CA 4D62B59A CA84E990 CACAF5C5
2B0930DA A23DE94C E87017BA 2D84988D
@ -32,7 +35,7 @@ describe('aes-ctr', () => {
const res = ctr256(ctr, data)
freeCtr256(ctr)
expect(hexEncode(res)).toEqual(hexEncode(dataEnc))
expect(p.hexEncode(res)).toEqual(p.hexEncode(dataEnc))
})
it('should correctly decrypt', () => {
@ -40,15 +43,15 @@ describe('aes-ctr', () => {
const res = ctr256(ctr, dataEnc)
freeCtr256(ctr)
expect(hexEncode(res)).toEqual(hexEncode(data))
expect(p.hexEncode(res)).toEqual(p.hexEncode(data))
})
})
describe('stream', () => {
const data = hexDecodeToBuffer('6BC1BEE22E409F96E93D7E117393172A')
const dataEnc1 = hexDecodeToBuffer('601ec313775789a5b7a7f504bbf3d228')
const dataEnc2 = hexDecodeToBuffer('31afd77f7d218690bd0ef82dfcf66cbe')
const dataEnc3 = hexDecodeToBuffer('7000927e2f2192cbe4b6a8b2441ddd48')
const data = p.hexDecode('6BC1BEE22E409F96E93D7E117393172A')
const dataEnc1 = p.hexDecode('601ec313775789a5b7a7f504bbf3d228')
const dataEnc2 = p.hexDecode('31afd77f7d218690bd0ef82dfcf66cbe')
const dataEnc3 = p.hexDecode('7000927e2f2192cbe4b6a8b2441ddd48')
it('should correctly encrypt', () => {
const ctr = createCtr256(key, iv)
@ -58,9 +61,9 @@ describe('aes-ctr', () => {
freeCtr256(ctr)
expect(hexEncode(res1)).toEqual(hexEncode(dataEnc1))
expect(hexEncode(res2)).toEqual(hexEncode(dataEnc2))
expect(hexEncode(res3)).toEqual(hexEncode(dataEnc3))
expect(p.hexEncode(res1)).toEqual(p.hexEncode(dataEnc1))
expect(p.hexEncode(res2)).toEqual(p.hexEncode(dataEnc2))
expect(p.hexEncode(res3)).toEqual(p.hexEncode(dataEnc3))
})
it('should correctly decrypt', () => {
@ -71,20 +74,20 @@ describe('aes-ctr', () => {
freeCtr256(ctr)
expect(hexEncode(res1)).toEqual(hexEncode(data))
expect(hexEncode(res2)).toEqual(hexEncode(data))
expect(hexEncode(res3)).toEqual(hexEncode(data))
expect(p.hexEncode(res1)).toEqual(p.hexEncode(data))
expect(p.hexEncode(res2)).toEqual(p.hexEncode(data))
expect(p.hexEncode(res3)).toEqual(p.hexEncode(data))
})
})
describe('stream (unaligned)', () => {
const data = hexDecodeToBuffer('6BC1BEE22E40')
const dataEnc1 = hexDecodeToBuffer('601ec3137757')
const dataEnc2 = hexDecodeToBuffer('7df2e078a555')
const dataEnc3 = hexDecodeToBuffer('a3a17be0742e')
const dataEnc4 = hexDecodeToBuffer('025ced833746')
const dataEnc5 = hexDecodeToBuffer('3ff238dea125')
const dataEnc6 = hexDecodeToBuffer('1055a52302dc')
const data = p.hexDecode('6BC1BEE22E40')
const dataEnc1 = p.hexDecode('601ec3137757')
const dataEnc2 = p.hexDecode('7df2e078a555')
const dataEnc3 = p.hexDecode('a3a17be0742e')
const dataEnc4 = p.hexDecode('025ced833746')
const dataEnc5 = p.hexDecode('3ff238dea125')
const dataEnc6 = p.hexDecode('1055a52302dc')
it('should correctly encrypt', () => {
const ctr = createCtr256(key, iv)
@ -97,12 +100,12 @@ describe('aes-ctr', () => {
freeCtr256(ctr)
expect(hexEncode(res1)).toEqual(hexEncode(dataEnc1))
expect(hexEncode(res2)).toEqual(hexEncode(dataEnc2))
expect(hexEncode(res3)).toEqual(hexEncode(dataEnc3))
expect(hexEncode(res4)).toEqual(hexEncode(dataEnc4))
expect(hexEncode(res5)).toEqual(hexEncode(dataEnc5))
expect(hexEncode(res6)).toEqual(hexEncode(dataEnc6))
expect(p.hexEncode(res1)).toEqual(p.hexEncode(dataEnc1))
expect(p.hexEncode(res2)).toEqual(p.hexEncode(dataEnc2))
expect(p.hexEncode(res3)).toEqual(p.hexEncode(dataEnc3))
expect(p.hexEncode(res4)).toEqual(p.hexEncode(dataEnc4))
expect(p.hexEncode(res5)).toEqual(p.hexEncode(dataEnc5))
expect(p.hexEncode(res6)).toEqual(p.hexEncode(dataEnc6))
})
it('should correctly decrypt', () => {
@ -116,17 +119,17 @@ describe('aes-ctr', () => {
freeCtr256(ctr)
expect(hexEncode(res1)).toEqual(hexEncode(data))
expect(hexEncode(res2)).toEqual(hexEncode(data))
expect(hexEncode(res3)).toEqual(hexEncode(data))
expect(hexEncode(res4)).toEqual(hexEncode(data))
expect(hexEncode(res5)).toEqual(hexEncode(data))
expect(hexEncode(res6)).toEqual(hexEncode(data))
expect(p.hexEncode(res1)).toEqual(p.hexEncode(data))
expect(p.hexEncode(res2)).toEqual(p.hexEncode(data))
expect(p.hexEncode(res3)).toEqual(p.hexEncode(data))
expect(p.hexEncode(res4)).toEqual(p.hexEncode(data))
expect(p.hexEncode(res5)).toEqual(p.hexEncode(data))
expect(p.hexEncode(res6)).toEqual(p.hexEncode(data))
})
})
it('should not leak memory', () => {
const data = hexDecodeToBuffer('6BC1BEE22E409F96E93D7E117393172A')
const data = p.hexDecode('6BC1BEE22E409F96E93D7E117393172A')
const mem = __getWasm().memory.buffer
const memSize = mem.byteLength

View file

@ -1,14 +1,17 @@
import { beforeAll, describe, expect, it } from 'vitest'
import { gzipSync } from 'zlib'
import { utf8Decode, utf8EncodeToBuffer } from '@mtcute/tl-runtime'
import { getPlatform } from '@mtcute/core/platform.js'
import { __getWasm, gunzip, initAsync } from '../src/index.js'
import { __getWasm, gunzip } from '../src/index.js'
import { initWasm } from './init.js'
beforeAll(async () => {
await initAsync()
await initWasm()
})
const p = getPlatform()
function gzipSyncWrap(data: Uint8Array) {
if (import.meta.env.TEST_ENV === 'browser') {
// @ts-expect-error fucking crutch because @jspm/core uses Buffer.isBuffer for some reason
@ -23,7 +26,7 @@ function gzipSyncWrap(data: Uint8Array) {
describe('gunzip', () => {
it('should correctly read zlib headers', () => {
const wasm = __getWasm()
const data = gzipSyncWrap(utf8EncodeToBuffer('hello world'))
const data = gzipSyncWrap(p.utf8Encode('hello world'))
const inputPtr = wasm.__malloc(data.length)
new Uint8Array(wasm.memory.buffer).set(data, inputPtr)
@ -33,11 +36,11 @@ describe('gunzip', () => {
it('should correctly inflate', () => {
const data = Array.from({ length: 1000 }, () => 'a').join('')
const res = gzipSyncWrap(utf8EncodeToBuffer(data))
const res = gzipSyncWrap(p.utf8Encode(data))
expect(res).not.toBeNull()
expect(res.length).toBeLessThan(100)
expect(gunzip(res)).toEqual(new Uint8Array(utf8EncodeToBuffer(data)))
expect(gunzip(res)).toEqual(new Uint8Array(p.utf8Encode(data)))
})
it('should not leak memory', () => {
@ -45,11 +48,11 @@ describe('gunzip', () => {
for (let i = 0; i < 100; i++) {
const data = Array.from({ length: 1000 }, () => 'a').join('')
const deflated = gzipSyncWrap(utf8EncodeToBuffer(data))
const deflated = gzipSyncWrap(p.utf8Encode(data))
const res = gunzip(deflated)
expect(utf8Decode(res)).toEqual(data)
expect(p.utf8Decode(res)).toEqual(data)
}
expect(__getWasm().memory.buffer.byteLength).toEqual(memSize)

View file

@ -1,18 +1,21 @@
import { beforeAll, describe, expect, it } from 'vitest'
import { hexEncode, utf8EncodeToBuffer } from '@mtcute/tl-runtime'
import { getPlatform } from '@mtcute/core/platform.js'
import { __getWasm, initAsync, sha1, sha256 } from '../src/index.js'
import { __getWasm, sha1, sha256 } from '../src/index.js'
import { initWasm } from './init.js'
beforeAll(async () => {
await initAsync()
await initWasm()
})
const p = getPlatform()
describe('sha256', () => {
it('should correctly calculate sha-256 hash', () => {
const hash = sha256(utf8EncodeToBuffer('abc'))
const hash = sha256(p.utf8Encode('abc'))
expect(hexEncode(hash)).toEqual('ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad')
expect(p.hexEncode(hash)).toEqual('ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad')
})
it('should not leak memory', () => {
@ -20,7 +23,7 @@ describe('sha256', () => {
const memSize = mem.byteLength
for (let i = 0; i < 100; i++) {
sha256(utf8EncodeToBuffer('abc'))
sha256(p.utf8Encode('abc'))
}
expect(mem.byteLength).toEqual(memSize)
@ -29,9 +32,9 @@ describe('sha256', () => {
describe('sha1', () => {
it('should correctly calculate sha-1 hash', () => {
const hash = sha1(utf8EncodeToBuffer('abc'))
const hash = sha1(p.utf8Encode('abc'))
expect(hexEncode(hash)).toEqual('a9993e364706816aba3e25717850c26c9cd0d89d')
expect(p.hexEncode(hash)).toEqual('a9993e364706816aba3e25717850c26c9cd0d89d')
})
it('should not leak memory', () => {
@ -39,7 +42,7 @@ describe('sha1', () => {
const memSize = mem.byteLength
for (let i = 0; i < 100; i++) {
sha1(utf8EncodeToBuffer('abc'))
sha1(p.utf8Encode('abc'))
}
expect(mem.byteLength).toEqual(memSize)

View file

@ -1,31 +1,34 @@
/* eslint-disable no-restricted-globals */
import { beforeAll, describe, expect, it } from 'vitest'
import { hexDecodeToBuffer, hexEncode } from '@mtcute/tl-runtime'
import { getPlatform } from '@mtcute/core/platform.js'
import { __getWasm, ige256Decrypt, ige256Encrypt, initAsync } from '../src/index.js'
import { __getWasm, ige256Decrypt, ige256Encrypt } from '../src/index.js'
import { initWasm } from './init.js'
const p = getPlatform()
beforeAll(async () => {
await initAsync()
await initWasm()
})
describe('aes-ige', () => {
const key = hexDecodeToBuffer('5468697320697320616E20696D706C655468697320697320616E20696D706C65')
const iv = hexDecodeToBuffer('6D656E746174696F6E206F6620494745206D6F646520666F72204F70656E5353')
const key = p.hexDecode('5468697320697320616E20696D706C655468697320697320616E20696D706C65')
const iv = p.hexDecode('6D656E746174696F6E206F6620494745206D6F646520666F72204F70656E5353')
const data = hexDecodeToBuffer('99706487a1cde613bc6de0b6f24b1c7aa448c8b9c3403e3467a8cad89340f53b')
const dataEnc = hexDecodeToBuffer('792ea8ae577b1a66cb3bd92679b8030ca54ee631976bd3a04547fdcb4639fa69')
const data = p.hexDecode('99706487a1cde613bc6de0b6f24b1c7aa448c8b9c3403e3467a8cad89340f53b')
const dataEnc = p.hexDecode('792ea8ae577b1a66cb3bd92679b8030ca54ee631976bd3a04547fdcb4639fa69')
it('should correctly encrypt', () => {
const aes = ige256Encrypt(data, key, iv)
expect(hexEncode(aes)).toEqual(hexEncode(dataEnc))
expect(p.hexEncode(aes)).toEqual(p.hexEncode(dataEnc))
})
it('should correctly decrypt', () => {
const aes = ige256Decrypt(dataEnc, key, iv)
expect(hexEncode(aes)).toEqual(hexEncode(data))
expect(p.hexEncode(aes)).toEqual(p.hexEncode(data))
})
it('should not leak memory', () => {

View file

@ -1,6 +1,5 @@
import { initSync } from '../src/index.js'
// todo: use platform-specific packages
export async function initWasm() {
const url = new URL('../lib/mtcute.wasm', import.meta.url)

View file

@ -1,14 +1,17 @@
import { beforeAll, describe, expect, it } from 'vitest'
import { inflateSync } from 'zlib'
import { utf8Decode, utf8EncodeToBuffer } from '@mtcute/tl-runtime'
import { getPlatform } from '@mtcute/core/platform.js'
import { __getWasm, deflateMaxSize, initAsync } from '../src/index.js'
import { __getWasm, deflateMaxSize } from '../src/index.js'
import { initWasm } from './init.js'
beforeAll(async () => {
await initAsync()
await initWasm()
})
const p = getPlatform()
function inflateSyncWrap(data: Uint8Array) {
if (import.meta.env.TEST_ENV === 'browser') {
// @ts-expect-error fucking crutch because @jspm/core uses Buffer.isBuffer for some reason
@ -22,25 +25,25 @@ function inflateSyncWrap(data: Uint8Array) {
describe('zlib deflate', () => {
it('should add zlib headers', () => {
const res = deflateMaxSize(utf8EncodeToBuffer('hello world'), 100)
const res = deflateMaxSize(p.utf8Encode('hello world'), 100)
expect(res).not.toBeNull()
expect(res!.slice(0, 2)).toEqual(new Uint8Array([0x78, 0x9c]))
})
it('should return null if compressed data is larger than size', () => {
const res = deflateMaxSize(utf8EncodeToBuffer('hello world'), 1)
const res = deflateMaxSize(p.utf8Encode('hello world'), 1)
expect(res).toBeNull()
})
it('should correctly deflate', () => {
const data = Array.from({ length: 1000 }, () => 'a').join('')
const res = deflateMaxSize(utf8EncodeToBuffer(data), 100)
const res = deflateMaxSize(p.utf8Encode(data), 100)
expect(res).not.toBeNull()
expect(res!.length).toBeLessThan(100)
expect(inflateSyncWrap(res!)).toEqual(utf8EncodeToBuffer(data))
expect(inflateSyncWrap(res!)).toEqual(p.utf8Encode(data))
})
it('should not leak memory', () => {
@ -48,11 +51,11 @@ describe('zlib deflate', () => {
for (let i = 0; i < 100; i++) {
const data = Array.from({ length: 1000 }, () => 'a').join('')
const deflated = deflateMaxSize(utf8EncodeToBuffer(data), 100)
const deflated = deflateMaxSize(p.utf8Encode(data), 100)
const res = inflateSyncWrap(deflated!)
expect(utf8Decode(res)).toEqual(data)
expect(p.utf8Decode(res)).toEqual(data)
}
expect(__getWasm().memory.buffer.byteLength).toEqual(memSize)

View file

@ -1,21 +0,0 @@
import { describe } from 'vitest'
import { testCryptoProvider } from '@mtcute/test'
import { WebCryptoProvider } from './crypto.js'
describe('WebCryptoProvider', async () => {
let crypto = globalThis.crypto
if (!crypto && typeof process !== 'undefined') {
crypto = await import('crypto').then((m) => m.webcrypto as Crypto)
}
if (!crypto) {
console.warn('Skipping WebCryptoProvider tests (no webcrypto)')
return
}
testCryptoProvider(new WebCryptoProvider({ crypto }))
})

View file

@ -1,2 +1,3 @@
export * from './crypto.js'
export * from './idb/index.js'
export * from './platform.js'

View file

@ -1,10 +1,10 @@
import { describe, expect, it, Mock, MockedObject, vi } from 'vitest'
import { TransportState } from '@mtcute/core'
import { getPlatform } from '@mtcute/core/platform.js'
import { defaultProductionDc, LogManager } from '@mtcute/core/utils.js'
import { defaultTestCryptoProvider, u8HexDecode } from '@mtcute/test'
import { getPlatform } from '../../platform.js'
import { defaultProductionDc, LogManager } from '../../utils/index.js'
import { TransportState } from './abstract.js'
import { WebSocketTransport } from './websocket.js'
const p = getPlatform()

View file

@ -1,10 +1,15 @@
import EventEmitter from 'events'
import { MtcuteError, MtUnsupportedError } from '../../types/errors.js'
import { BasicDcOption, ICryptoProvider, Logger } from '../../utils/index.js'
import { IPacketCodec, ITelegramTransport, TransportState } from './abstract.js'
import { IntermediatePacketCodec } from './intermediate.js'
import { ObfuscatedPacketCodec } from './obfuscated.js'
import {
IntermediatePacketCodec,
IPacketCodec,
ITelegramTransport,
MtcuteError,
MtUnsupportedError,
ObfuscatedPacketCodec,
TransportState,
} from '@mtcute/core'
import { BasicDcOption, ICryptoProvider, Logger } from '@mtcute/core/utils.js'
export type WebSocketConstructor = {
new (address: string, protocol?: string): WebSocket

View file

@ -1 +0,0 @@
export * from '@mtcute/core/utils.js'

View file

@ -390,9 +390,15 @@ importers:
packages/wasm:
devDependencies:
'@mtcute/tl-runtime':
'@mtcute/core':
specifier: workspace:^
version: link:../tl-runtime
version: link:../core
'@mtcute/node':
specifier: workspace:^
version: link:../node
'@mtcute/web':
specifier: workspace:^
version: link:../web
packages/web:
dependencies: