fix: properly handle file uploads + downloading as node stream
This commit is contained in:
parent
a2739b678c
commit
fb72d3194d
39 changed files with 388 additions and 263 deletions
|
@ -11,6 +11,9 @@ export default defineConfig({
|
||||||
'packages/**/*.test-d.ts',
|
'packages/**/*.test-d.ts',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
setupFiles: [
|
||||||
|
'./.config/vitest.setup.mts'
|
||||||
|
]
|
||||||
},
|
},
|
||||||
define: {
|
define: {
|
||||||
'import.meta.env.TEST_ENV': '"node"'
|
'import.meta.env.TEST_ENV': '"node"'
|
||||||
|
|
9
.config/vitest.setup.mts
Normal file
9
.config/vitest.setup.mts
Normal 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())
|
||||||
|
}
|
|
@ -2248,7 +2248,7 @@ export interface TelegramClient extends ITelegramClient {
|
||||||
downloadAsBuffer(location: FileDownloadLocation, params?: FileDownloadParameters): Promise<Uint8Array>
|
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.
|
* Promise will resolve once the download is complete.
|
||||||
*
|
*
|
||||||
* **Available**: ✅ both users and bots
|
* **Available**: ✅ both users and bots
|
||||||
|
@ -2267,6 +2267,15 @@ export interface TelegramClient extends ITelegramClient {
|
||||||
* @param params Download parameters
|
* @param params Download parameters
|
||||||
*/
|
*/
|
||||||
downloadAsIterable(input: FileDownloadLocation, params?: FileDownloadParameters): AsyncIterableIterator<Uint8Array>
|
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,
|
* Download a file and return it as a readable stream,
|
||||||
* streaming file contents.
|
* streaming file contents.
|
||||||
|
@ -2320,9 +2329,6 @@ export interface TelegramClient extends ITelegramClient {
|
||||||
uploadFile(params: {
|
uploadFile(params: {
|
||||||
/**
|
/**
|
||||||
* Upload file source.
|
* 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
|
file: UploadFileLike
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
|
@ -5,7 +5,7 @@ import { FileDownloadLocation, FileDownloadParameters } from '../../types/index.
|
||||||
|
|
||||||
// @available=both
|
// @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.
|
* Promise will resolve once the download is complete.
|
||||||
*
|
*
|
||||||
* @param filename Local file name to which the remote file will be downloaded
|
* @param filename Local file name to which the remote file will be downloaded
|
||||||
|
|
|
@ -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
|
|
@ -1,5 +1,6 @@
|
||||||
import { tl } from '@mtcute/tl'
|
import { tl } from '@mtcute/tl'
|
||||||
|
|
||||||
|
import { getPlatform } from '../../../platform.js'
|
||||||
import { MtArgumentError } from '../../../types/errors.js'
|
import { MtArgumentError } from '../../../types/errors.js'
|
||||||
import { randomLong } from '../../../utils/long-utils.js'
|
import { randomLong } from '../../../utils/long-utils.js'
|
||||||
import { ITelegramClient } from '../../client.types.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 = 4000 // 512 kb * 4000 = 2000 MiB
|
||||||
const MAX_PART_COUNT_PREMIUM = 8000 // 512 kb * 8000 = 4000 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
|
// @available=both
|
||||||
/**
|
/**
|
||||||
* Upload a file to Telegram servers, without actually
|
* Upload a file to Telegram servers, without actually
|
||||||
|
@ -101,19 +106,30 @@ export async function uploadFile(
|
||||||
let fileName = DEFAULT_FILE_NAME
|
let fileName = DEFAULT_FILE_NAME
|
||||||
let fileMime = params.fileMime
|
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)) {
|
if (ArrayBuffer.isView(file)) {
|
||||||
fileSize = file.length
|
fileSize = file.length
|
||||||
file = bufferToStream(file)
|
file = bufferToStream(file)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof File !== 'undefined' && file instanceof File) {
|
if (HAS_FILE && file instanceof File) {
|
||||||
fileName = file.name
|
fileName = file.name
|
||||||
fileSize = file.size
|
fileSize = file.size
|
||||||
file = file.stream()
|
file = file.stream()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof file === 'object' && 'headers' in file && 'body' in file && 'url' in file) {
|
if (HAS_RESPONSE && file instanceof Response) {
|
||||||
// fetch() response
|
|
||||||
const length = parseInt(file.headers.get('content-length') || '0')
|
const length = parseInt(file.headers.get('content-length') || '0')
|
||||||
if (!isNaN(length) && length) fileSize = length
|
if (!isNaN(length) && length) fileSize = length
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { Readable } from 'node:stream'
|
|
||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
import { createChunkedReader, nodeReadableToWeb } from './stream-utils.js'
|
import { createChunkedReader } from './stream-utils.js'
|
||||||
|
|
||||||
describe('createChunkedReader', () => {
|
describe('createChunkedReader', () => {
|
||||||
it('should correctly handle chunks smaller than chunkSize', async () => {
|
it('should correctly handle chunks smaller than chunkSize', async () => {
|
||||||
|
@ -82,26 +81,3 @@ describe('createChunkedReader', () => {
|
||||||
expect(await reader.read()).to.be.null
|
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 })
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
|
@ -135,36 +135,3 @@ export function createChunkedReader(stream: ReadableStream<Uint8Array>, chunkSiz
|
||||||
read: readLocked,
|
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()
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,15 +1,39 @@
|
||||||
import { ITlPlatform, TlBinaryReader, TlBinaryWriter } from '@mtcute/tl-runtime'
|
import { ITlPlatform, TlBinaryReader, TlBinaryWriter } from '@mtcute/tl-runtime'
|
||||||
|
|
||||||
|
import { UploadFileLike } from './highlevel/types/files/utils.js'
|
||||||
import { MtUnsupportedError } from './types/errors.js'
|
import { MtUnsupportedError } from './types/errors.js'
|
||||||
|
import { MaybePromise } from './types/index.js'
|
||||||
|
|
||||||
export interface ICorePlatform extends ITlPlatform {
|
export interface ICorePlatform extends ITlPlatform {
|
||||||
beforeExit(fn: () => void): () => void
|
beforeExit(fn: () => void): () => void
|
||||||
log(color: number, level: number, tag: string, fmt: string, args: unknown[]): void
|
log(color: number, level: number, tag: string, fmt: string, args: unknown[]): void
|
||||||
getDefaultLogLevel(): number | null
|
getDefaultLogLevel(): number | null
|
||||||
getDeviceModel(): string
|
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 {
|
export function setPlatform(platform: ICorePlatform): void {
|
||||||
if (_platform) {
|
if (_platform) {
|
||||||
|
@ -23,6 +47,10 @@ export function setPlatform(platform: ICorePlatform): void {
|
||||||
_platform = platform
|
_platform = platform
|
||||||
TlBinaryReader.platform = platform
|
TlBinaryReader.platform = platform
|
||||||
TlBinaryWriter.platform = platform
|
TlBinaryWriter.platform = platform
|
||||||
|
|
||||||
|
if (globalObject) {
|
||||||
|
globalObject.__MTCUTE_PLATFORM__ = platform
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPlatform(): ICorePlatform {
|
export function getPlatform(): ICorePlatform {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { describe, expect, it } from 'vitest'
|
import { beforeAll, describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
import { defaultCryptoProvider } from '@mtcute/test'
|
import { defaultCryptoProvider } from '@mtcute/test'
|
||||||
|
|
||||||
|
@ -6,6 +6,10 @@ import { findKeyByFingerprints, parsePublicKey } from '../index.js'
|
||||||
|
|
||||||
const crypto = defaultCryptoProvider
|
const crypto = defaultCryptoProvider
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await crypto.initialize()
|
||||||
|
})
|
||||||
|
|
||||||
describe('parsePublicKey', () => {
|
describe('parsePublicKey', () => {
|
||||||
it('should parse telegram public keys', () => {
|
it('should parse telegram public keys', () => {
|
||||||
expect(
|
expect(
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
import { describe, expect, it } from 'vitest'
|
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'
|
import { Dispatcher, PropagationAction } from '../src/index.js'
|
||||||
|
|
||||||
describe('Dispatcher', () => {
|
describe('Dispatcher', () => {
|
||||||
// todo: replace with proper mocked TelegramClient
|
// 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()
|
const emptyPeers = new PeersIndex()
|
||||||
|
|
||||||
describe('Raw updates', () => {
|
describe('Raw updates', () => {
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { setPlatform } from '@mtcute/core/platform.js'
|
||||||
import { SqliteStorage } from '@mtcute/sqlite'
|
import { SqliteStorage } from '@mtcute/sqlite'
|
||||||
|
|
||||||
import { downloadToFile } from './methods/download-file.js'
|
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 { NodePlatform } from './platform.js'
|
||||||
import { NodeCryptoProvider } from './utils/crypto.js'
|
import { NodeCryptoProvider } from './utils/crypto.js'
|
||||||
import { TcpTransport } from './utils/tcp.js'
|
import { TcpTransport } from './utils/tcp.js'
|
||||||
|
@ -120,7 +120,10 @@ export class TelegramClient extends TelegramClientBase {
|
||||||
return downloadToFile(this, filename, location, params)
|
return downloadToFile(this, filename, location, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
uploadFile(params: Parameters<typeof uploadFile>[1]) {
|
downloadAsNodeStream(
|
||||||
return uploadFile(this, params)
|
location: FileDownloadLocation,
|
||||||
|
params?: FileDownloadParameters | undefined,
|
||||||
|
) {
|
||||||
|
return downloadAsNodeStream(this, location, params)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
export * from './client.js'
|
export * from './client.js'
|
||||||
export * from './platform.js'
|
export * from './platform.js'
|
||||||
export * from './utils/tcp.js'
|
|
||||||
export * from './utils/crypto.js'
|
export * from './utils/crypto.js'
|
||||||
|
export * from './utils/tcp.js'
|
||||||
export * from '@mtcute/core'
|
export * from '@mtcute/core'
|
||||||
export * from '@mtcute/html-parser'
|
export * from '@mtcute/html-parser'
|
||||||
export * from '@mtcute/markdown-parser'
|
export * from '@mtcute/markdown-parser'
|
||||||
|
|
|
@ -2,4 +2,4 @@
|
||||||
export * from '@mtcute/core/methods.js'
|
export * from '@mtcute/core/methods.js'
|
||||||
|
|
||||||
export { downloadToFile } from './methods/download-file.js'
|
export { downloadToFile } from './methods/download-file.js'
|
||||||
export { uploadFile } from './methods/upload-file.js'
|
export { downloadAsNodeStream } from './methods/download-node-stream.js'
|
||||||
|
|
19
packages/node/src/methods/download-node-stream.ts
Normal file
19
packages/node/src/methods/download-node-stream.ts
Normal 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))
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ import { ICorePlatform } from '@mtcute/core/platform.js'
|
||||||
|
|
||||||
import { beforeExit } from './utils/exit-hook.js'
|
import { beforeExit } from './utils/exit-hook.js'
|
||||||
import { defaultLoggingHandler } from './utils/logging.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')
|
const BUFFER_BASE64_URL_AVAILABLE = typeof Buffer.isEncoding === 'function' && Buffer.isEncoding('base64url')
|
||||||
|
|
||||||
|
@ -17,6 +18,7 @@ export class NodePlatform implements ICorePlatform {
|
||||||
// ICorePlatform
|
// ICorePlatform
|
||||||
log!: typeof defaultLoggingHandler
|
log!: typeof defaultLoggingHandler
|
||||||
beforeExit!: typeof beforeExit
|
beforeExit!: typeof beforeExit
|
||||||
|
normalizeFile!: typeof normalizeFile
|
||||||
|
|
||||||
getDeviceModel(): string {
|
getDeviceModel(): string {
|
||||||
return `${os.type()} ${os.arch()} ${os.release()}`
|
return `${os.type()} ${os.arch()} ${os.release()}`
|
||||||
|
@ -76,3 +78,4 @@ export class NodePlatform implements ICorePlatform {
|
||||||
|
|
||||||
NodePlatform.prototype.log = defaultLoggingHandler
|
NodePlatform.prototype.log = defaultLoggingHandler
|
||||||
NodePlatform.prototype.beforeExit = beforeExit
|
NodePlatform.prototype.beforeExit = beforeExit
|
||||||
|
NodePlatform.prototype.normalizeFile = normalizeFile
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
|
export * from './utils/stream-utils.js'
|
||||||
export * from '@mtcute/core/utils.js'
|
export * from '@mtcute/core/utils.js'
|
||||||
|
|
|
@ -3,17 +3,11 @@ import { stat } from 'fs/promises'
|
||||||
import { basename } from 'path'
|
import { basename } from 'path'
|
||||||
import { Readable } from 'stream'
|
import { Readable } from 'stream'
|
||||||
|
|
||||||
import { ITelegramClient } from '@mtcute/core'
|
import { UploadFileLike } from '@mtcute/core'
|
||||||
import { uploadFile as uploadFileCore } from '@mtcute/core/methods.js'
|
|
||||||
|
|
||||||
import { nodeStreamToWeb } from '../utils/stream-utils.js'
|
import { nodeStreamToWeb } from '../utils/stream-utils.js'
|
||||||
|
|
||||||
export async function uploadFile(
|
export async function normalizeFile(file: UploadFileLike) {
|
||||||
client: ITelegramClient,
|
|
||||||
params: Parameters<typeof uploadFileCore>[1],
|
|
||||||
) {
|
|
||||||
let file = params.file
|
|
||||||
|
|
||||||
if (typeof file === 'string') {
|
if (typeof file === 'string') {
|
||||||
file = createReadStream(file)
|
file = createReadStream(file)
|
||||||
}
|
}
|
||||||
|
@ -22,20 +16,19 @@ export async function uploadFile(
|
||||||
const fileName = basename(file.path.toString())
|
const fileName = basename(file.path.toString())
|
||||||
const fileSize = await stat(file.path.toString()).then((stat) => stat.size)
|
const fileSize = await stat(file.path.toString()).then((stat) => stat.size)
|
||||||
|
|
||||||
return uploadFileCore(client, {
|
return {
|
||||||
...params,
|
|
||||||
file: nodeStreamToWeb(file),
|
file: nodeStreamToWeb(file),
|
||||||
fileName,
|
fileName,
|
||||||
fileSize,
|
fileSize,
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file instanceof Readable) {
|
if (file instanceof Readable) {
|
||||||
return uploadFileCore(client, {
|
return {
|
||||||
...params,
|
|
||||||
file: nodeStreamToWeb(file),
|
file: nodeStreamToWeb(file),
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return uploadFileCore(client, params)
|
// string -> ReadStream, thus already handled
|
||||||
|
return null
|
||||||
}
|
}
|
59
packages/node/src/utils/stream-utils.test.ts
Normal file
59
packages/node/src/utils/stream-utils.test.ts
Normal 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', () => {})
|
||||||
|
}
|
|
@ -1,26 +1,78 @@
|
||||||
import { Readable } from 'stream'
|
import { Readable } from 'stream'
|
||||||
|
|
||||||
|
import { isNodeVersionAfter } from './version.js'
|
||||||
|
|
||||||
export function nodeStreamToWeb(stream: Readable): ReadableStream<Uint8Array> {
|
export function nodeStreamToWeb(stream: Readable): ReadableStream<Uint8Array> {
|
||||||
if (typeof Readable.toWeb === 'function') {
|
if (typeof Readable.toWeb === 'function') {
|
||||||
return Readable.toWeb(stream)
|
return Readable.toWeb(stream)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// otherwise, use a silly little adapter
|
||||||
|
|
||||||
|
stream.pause()
|
||||||
|
|
||||||
return new ReadableStream({
|
return new ReadableStream({
|
||||||
start(controller) {
|
start(c) {
|
||||||
stream.on('data', (chunk) => {
|
stream.on('data', (chunk) => {
|
||||||
controller.enqueue(chunk)
|
c.enqueue(chunk as Uint8Array)
|
||||||
})
|
})
|
||||||
stream.on('end', () => {
|
stream.on('end', () => {
|
||||||
controller.close()
|
c.close()
|
||||||
})
|
})
|
||||||
stream.on('error', (err) => {
|
stream.on('error', (err) => {
|
||||||
controller.error(err)
|
c.error(err)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
cancel() {
|
pull() {
|
||||||
if (typeof stream.destroy === 'function') {
|
stream.resume()
|
||||||
stream.destroy()
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@ import { describe, expect, it, MockedObject, vi } from 'vitest'
|
||||||
import { TransportState } from '@mtcute/core'
|
import { TransportState } from '@mtcute/core'
|
||||||
import { getPlatform } from '@mtcute/core/platform.js'
|
import { getPlatform } from '@mtcute/core/platform.js'
|
||||||
import { defaultProductionDc, LogManager } from '@mtcute/core/utils.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') {
|
if (import.meta.env.TEST_ENV === 'node' || import.meta.env.TEST_ENV === 'bun') {
|
||||||
vi.doMock('net', () => ({
|
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 connect = vi.mocked(net.connect)
|
||||||
|
|
||||||
const { TcpTransport } = await import('./tcp.js')
|
const { TcpTransport } = await import('./tcp.js')
|
||||||
|
const { defaultTestCryptoProvider, u8HexDecode } = await import('@mtcute/test')
|
||||||
|
|
||||||
describe('TcpTransport', () => {
|
describe('TcpTransport', () => {
|
||||||
const getLastSocket = () => {
|
const getLastSocket = () => {
|
||||||
|
|
14
packages/node/src/utils/version.ts
Normal file
14
packages/node/src/utils/version.ts
Normal 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
|
||||||
|
}
|
|
@ -1,7 +1,6 @@
|
||||||
export * from './client.js'
|
export * from './client.js'
|
||||||
export * from './crypto.js'
|
export * from './crypto.js'
|
||||||
export * from './platform.js'
|
export * from './platform.js'
|
||||||
export * from './platform.js'
|
|
||||||
export * from './storage.js'
|
export * from './storage.js'
|
||||||
export * from './storage/index.js'
|
export * from './storage/index.js'
|
||||||
export * from './stub.js'
|
export * from './stub.js'
|
||||||
|
|
|
@ -94,6 +94,10 @@ describe('TlBinaryWriter', () => {
|
||||||
expect(testSingleMethod(1004, (w) => w.bytes(random1000bytes))).toEqual(hexEncode(buffer))
|
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 = {
|
const stubObjectsMap: TlWriterMap = {
|
||||||
deadbeef: function (w, obj) {
|
deadbeef: function (w, obj) {
|
||||||
w.uint(0xdeadbeef)
|
w.uint(0xdeadbeef)
|
||||||
|
|
|
@ -26,6 +26,8 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@mtcute/tl-runtime": "workspace:^"
|
"@mtcute/core": "workspace:^",
|
||||||
|
"@mtcute/web": "workspace:^",
|
||||||
|
"@mtcute/node": "workspace:^"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,26 +1,29 @@
|
||||||
import { beforeAll, describe, expect, it } from 'vitest'
|
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 () => {
|
beforeAll(async () => {
|
||||||
await initAsync()
|
await initWasm()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('aes-ctr', () => {
|
describe('aes-ctr', () => {
|
||||||
const key = hexDecodeToBuffer('603DEB1015CA71BE2B73AEF0857D77811F352C073B6108D72D9810A30914DFF4')
|
const key = p.hexDecode('603DEB1015CA71BE2B73AEF0857D77811F352C073B6108D72D9810A30914DFF4')
|
||||||
const iv = hexDecodeToBuffer('F0F1F2F3F4F5F6F7F8F9FAFBFCFDFEFF')
|
const iv = p.hexDecode('F0F1F2F3F4F5F6F7F8F9FAFBFCFDFEFF')
|
||||||
|
|
||||||
describe('NIST', () => {
|
describe('NIST', () => {
|
||||||
// https://csrc.nist.gov/CSRC/media/Projects/Cryptographic-Standards-and-Guidelines/documents/examples/AES_CTR.pdf
|
// 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
|
`6BC1BEE2 2E409F96 E93D7E11 7393172A
|
||||||
AE2D8A57 1E03AC9C 9EB76FAC 45AF8E51
|
AE2D8A57 1E03AC9C 9EB76FAC 45AF8E51
|
||||||
30C81C46 A35CE411 E5FBC119 1A0A52EF
|
30C81C46 A35CE411 E5FBC119 1A0A52EF
|
||||||
F69F2445 DF4F9B17 AD2B417B E66C3710`.replace(/\s/g, ''),
|
F69F2445 DF4F9B17 AD2B417B E66C3710`.replace(/\s/g, ''),
|
||||||
)
|
)
|
||||||
const dataEnc = hexDecodeToBuffer(
|
const dataEnc = p.hexDecode(
|
||||||
`601EC313 775789A5 B7A7F504 BBF3D228
|
`601EC313 775789A5 B7A7F504 BBF3D228
|
||||||
F443E3CA 4D62B59A CA84E990 CACAF5C5
|
F443E3CA 4D62B59A CA84E990 CACAF5C5
|
||||||
2B0930DA A23DE94C E87017BA 2D84988D
|
2B0930DA A23DE94C E87017BA 2D84988D
|
||||||
|
@ -32,7 +35,7 @@ describe('aes-ctr', () => {
|
||||||
const res = ctr256(ctr, data)
|
const res = ctr256(ctr, data)
|
||||||
freeCtr256(ctr)
|
freeCtr256(ctr)
|
||||||
|
|
||||||
expect(hexEncode(res)).toEqual(hexEncode(dataEnc))
|
expect(p.hexEncode(res)).toEqual(p.hexEncode(dataEnc))
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should correctly decrypt', () => {
|
it('should correctly decrypt', () => {
|
||||||
|
@ -40,15 +43,15 @@ describe('aes-ctr', () => {
|
||||||
const res = ctr256(ctr, dataEnc)
|
const res = ctr256(ctr, dataEnc)
|
||||||
freeCtr256(ctr)
|
freeCtr256(ctr)
|
||||||
|
|
||||||
expect(hexEncode(res)).toEqual(hexEncode(data))
|
expect(p.hexEncode(res)).toEqual(p.hexEncode(data))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('stream', () => {
|
describe('stream', () => {
|
||||||
const data = hexDecodeToBuffer('6BC1BEE22E409F96E93D7E117393172A')
|
const data = p.hexDecode('6BC1BEE22E409F96E93D7E117393172A')
|
||||||
const dataEnc1 = hexDecodeToBuffer('601ec313775789a5b7a7f504bbf3d228')
|
const dataEnc1 = p.hexDecode('601ec313775789a5b7a7f504bbf3d228')
|
||||||
const dataEnc2 = hexDecodeToBuffer('31afd77f7d218690bd0ef82dfcf66cbe')
|
const dataEnc2 = p.hexDecode('31afd77f7d218690bd0ef82dfcf66cbe')
|
||||||
const dataEnc3 = hexDecodeToBuffer('7000927e2f2192cbe4b6a8b2441ddd48')
|
const dataEnc3 = p.hexDecode('7000927e2f2192cbe4b6a8b2441ddd48')
|
||||||
|
|
||||||
it('should correctly encrypt', () => {
|
it('should correctly encrypt', () => {
|
||||||
const ctr = createCtr256(key, iv)
|
const ctr = createCtr256(key, iv)
|
||||||
|
@ -58,9 +61,9 @@ describe('aes-ctr', () => {
|
||||||
|
|
||||||
freeCtr256(ctr)
|
freeCtr256(ctr)
|
||||||
|
|
||||||
expect(hexEncode(res1)).toEqual(hexEncode(dataEnc1))
|
expect(p.hexEncode(res1)).toEqual(p.hexEncode(dataEnc1))
|
||||||
expect(hexEncode(res2)).toEqual(hexEncode(dataEnc2))
|
expect(p.hexEncode(res2)).toEqual(p.hexEncode(dataEnc2))
|
||||||
expect(hexEncode(res3)).toEqual(hexEncode(dataEnc3))
|
expect(p.hexEncode(res3)).toEqual(p.hexEncode(dataEnc3))
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should correctly decrypt', () => {
|
it('should correctly decrypt', () => {
|
||||||
|
@ -71,20 +74,20 @@ describe('aes-ctr', () => {
|
||||||
|
|
||||||
freeCtr256(ctr)
|
freeCtr256(ctr)
|
||||||
|
|
||||||
expect(hexEncode(res1)).toEqual(hexEncode(data))
|
expect(p.hexEncode(res1)).toEqual(p.hexEncode(data))
|
||||||
expect(hexEncode(res2)).toEqual(hexEncode(data))
|
expect(p.hexEncode(res2)).toEqual(p.hexEncode(data))
|
||||||
expect(hexEncode(res3)).toEqual(hexEncode(data))
|
expect(p.hexEncode(res3)).toEqual(p.hexEncode(data))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('stream (unaligned)', () => {
|
describe('stream (unaligned)', () => {
|
||||||
const data = hexDecodeToBuffer('6BC1BEE22E40')
|
const data = p.hexDecode('6BC1BEE22E40')
|
||||||
const dataEnc1 = hexDecodeToBuffer('601ec3137757')
|
const dataEnc1 = p.hexDecode('601ec3137757')
|
||||||
const dataEnc2 = hexDecodeToBuffer('7df2e078a555')
|
const dataEnc2 = p.hexDecode('7df2e078a555')
|
||||||
const dataEnc3 = hexDecodeToBuffer('a3a17be0742e')
|
const dataEnc3 = p.hexDecode('a3a17be0742e')
|
||||||
const dataEnc4 = hexDecodeToBuffer('025ced833746')
|
const dataEnc4 = p.hexDecode('025ced833746')
|
||||||
const dataEnc5 = hexDecodeToBuffer('3ff238dea125')
|
const dataEnc5 = p.hexDecode('3ff238dea125')
|
||||||
const dataEnc6 = hexDecodeToBuffer('1055a52302dc')
|
const dataEnc6 = p.hexDecode('1055a52302dc')
|
||||||
|
|
||||||
it('should correctly encrypt', () => {
|
it('should correctly encrypt', () => {
|
||||||
const ctr = createCtr256(key, iv)
|
const ctr = createCtr256(key, iv)
|
||||||
|
@ -97,12 +100,12 @@ describe('aes-ctr', () => {
|
||||||
|
|
||||||
freeCtr256(ctr)
|
freeCtr256(ctr)
|
||||||
|
|
||||||
expect(hexEncode(res1)).toEqual(hexEncode(dataEnc1))
|
expect(p.hexEncode(res1)).toEqual(p.hexEncode(dataEnc1))
|
||||||
expect(hexEncode(res2)).toEqual(hexEncode(dataEnc2))
|
expect(p.hexEncode(res2)).toEqual(p.hexEncode(dataEnc2))
|
||||||
expect(hexEncode(res3)).toEqual(hexEncode(dataEnc3))
|
expect(p.hexEncode(res3)).toEqual(p.hexEncode(dataEnc3))
|
||||||
expect(hexEncode(res4)).toEqual(hexEncode(dataEnc4))
|
expect(p.hexEncode(res4)).toEqual(p.hexEncode(dataEnc4))
|
||||||
expect(hexEncode(res5)).toEqual(hexEncode(dataEnc5))
|
expect(p.hexEncode(res5)).toEqual(p.hexEncode(dataEnc5))
|
||||||
expect(hexEncode(res6)).toEqual(hexEncode(dataEnc6))
|
expect(p.hexEncode(res6)).toEqual(p.hexEncode(dataEnc6))
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should correctly decrypt', () => {
|
it('should correctly decrypt', () => {
|
||||||
|
@ -116,17 +119,17 @@ describe('aes-ctr', () => {
|
||||||
|
|
||||||
freeCtr256(ctr)
|
freeCtr256(ctr)
|
||||||
|
|
||||||
expect(hexEncode(res1)).toEqual(hexEncode(data))
|
expect(p.hexEncode(res1)).toEqual(p.hexEncode(data))
|
||||||
expect(hexEncode(res2)).toEqual(hexEncode(data))
|
expect(p.hexEncode(res2)).toEqual(p.hexEncode(data))
|
||||||
expect(hexEncode(res3)).toEqual(hexEncode(data))
|
expect(p.hexEncode(res3)).toEqual(p.hexEncode(data))
|
||||||
expect(hexEncode(res4)).toEqual(hexEncode(data))
|
expect(p.hexEncode(res4)).toEqual(p.hexEncode(data))
|
||||||
expect(hexEncode(res5)).toEqual(hexEncode(data))
|
expect(p.hexEncode(res5)).toEqual(p.hexEncode(data))
|
||||||
expect(hexEncode(res6)).toEqual(hexEncode(data))
|
expect(p.hexEncode(res6)).toEqual(p.hexEncode(data))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should not leak memory', () => {
|
it('should not leak memory', () => {
|
||||||
const data = hexDecodeToBuffer('6BC1BEE22E409F96E93D7E117393172A')
|
const data = p.hexDecode('6BC1BEE22E409F96E93D7E117393172A')
|
||||||
const mem = __getWasm().memory.buffer
|
const mem = __getWasm().memory.buffer
|
||||||
const memSize = mem.byteLength
|
const memSize = mem.byteLength
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,17 @@
|
||||||
import { beforeAll, describe, expect, it } from 'vitest'
|
import { beforeAll, describe, expect, it } from 'vitest'
|
||||||
import { gzipSync } from 'zlib'
|
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 () => {
|
beforeAll(async () => {
|
||||||
await initAsync()
|
await initWasm()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const p = getPlatform()
|
||||||
|
|
||||||
function gzipSyncWrap(data: Uint8Array) {
|
function gzipSyncWrap(data: Uint8Array) {
|
||||||
if (import.meta.env.TEST_ENV === 'browser') {
|
if (import.meta.env.TEST_ENV === 'browser') {
|
||||||
// @ts-expect-error fucking crutch because @jspm/core uses Buffer.isBuffer for some reason
|
// @ts-expect-error fucking crutch because @jspm/core uses Buffer.isBuffer for some reason
|
||||||
|
@ -23,7 +26,7 @@ function gzipSyncWrap(data: Uint8Array) {
|
||||||
describe('gunzip', () => {
|
describe('gunzip', () => {
|
||||||
it('should correctly read zlib headers', () => {
|
it('should correctly read zlib headers', () => {
|
||||||
const wasm = __getWasm()
|
const wasm = __getWasm()
|
||||||
const data = gzipSyncWrap(utf8EncodeToBuffer('hello world'))
|
const data = gzipSyncWrap(p.utf8Encode('hello world'))
|
||||||
|
|
||||||
const inputPtr = wasm.__malloc(data.length)
|
const inputPtr = wasm.__malloc(data.length)
|
||||||
new Uint8Array(wasm.memory.buffer).set(data, inputPtr)
|
new Uint8Array(wasm.memory.buffer).set(data, inputPtr)
|
||||||
|
@ -33,11 +36,11 @@ describe('gunzip', () => {
|
||||||
|
|
||||||
it('should correctly inflate', () => {
|
it('should correctly inflate', () => {
|
||||||
const data = Array.from({ length: 1000 }, () => 'a').join('')
|
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).not.toBeNull()
|
||||||
expect(res.length).toBeLessThan(100)
|
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', () => {
|
it('should not leak memory', () => {
|
||||||
|
@ -45,11 +48,11 @@ describe('gunzip', () => {
|
||||||
|
|
||||||
for (let i = 0; i < 100; i++) {
|
for (let i = 0; i < 100; i++) {
|
||||||
const data = Array.from({ length: 1000 }, () => 'a').join('')
|
const data = Array.from({ length: 1000 }, () => 'a').join('')
|
||||||
const deflated = gzipSyncWrap(utf8EncodeToBuffer(data))
|
const deflated = gzipSyncWrap(p.utf8Encode(data))
|
||||||
|
|
||||||
const res = gunzip(deflated)
|
const res = gunzip(deflated)
|
||||||
|
|
||||||
expect(utf8Decode(res)).toEqual(data)
|
expect(p.utf8Decode(res)).toEqual(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(__getWasm().memory.buffer.byteLength).toEqual(memSize)
|
expect(__getWasm().memory.buffer.byteLength).toEqual(memSize)
|
||||||
|
|
|
@ -1,18 +1,21 @@
|
||||||
import { beforeAll, describe, expect, it } from 'vitest'
|
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 () => {
|
beforeAll(async () => {
|
||||||
await initAsync()
|
await initWasm()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const p = getPlatform()
|
||||||
|
|
||||||
describe('sha256', () => {
|
describe('sha256', () => {
|
||||||
it('should correctly calculate sha-256 hash', () => {
|
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', () => {
|
it('should not leak memory', () => {
|
||||||
|
@ -20,7 +23,7 @@ describe('sha256', () => {
|
||||||
const memSize = mem.byteLength
|
const memSize = mem.byteLength
|
||||||
|
|
||||||
for (let i = 0; i < 100; i++) {
|
for (let i = 0; i < 100; i++) {
|
||||||
sha256(utf8EncodeToBuffer('abc'))
|
sha256(p.utf8Encode('abc'))
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(mem.byteLength).toEqual(memSize)
|
expect(mem.byteLength).toEqual(memSize)
|
||||||
|
@ -29,9 +32,9 @@ describe('sha256', () => {
|
||||||
|
|
||||||
describe('sha1', () => {
|
describe('sha1', () => {
|
||||||
it('should correctly calculate sha-1 hash', () => {
|
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', () => {
|
it('should not leak memory', () => {
|
||||||
|
@ -39,7 +42,7 @@ describe('sha1', () => {
|
||||||
const memSize = mem.byteLength
|
const memSize = mem.byteLength
|
||||||
|
|
||||||
for (let i = 0; i < 100; i++) {
|
for (let i = 0; i < 100; i++) {
|
||||||
sha1(utf8EncodeToBuffer('abc'))
|
sha1(p.utf8Encode('abc'))
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(mem.byteLength).toEqual(memSize)
|
expect(mem.byteLength).toEqual(memSize)
|
||||||
|
|
|
@ -1,31 +1,34 @@
|
||||||
/* eslint-disable no-restricted-globals */
|
/* eslint-disable no-restricted-globals */
|
||||||
import { beforeAll, describe, expect, it } from 'vitest'
|
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 () => {
|
beforeAll(async () => {
|
||||||
await initAsync()
|
await initWasm()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('aes-ige', () => {
|
describe('aes-ige', () => {
|
||||||
const key = hexDecodeToBuffer('5468697320697320616E20696D706C655468697320697320616E20696D706C65')
|
const key = p.hexDecode('5468697320697320616E20696D706C655468697320697320616E20696D706C65')
|
||||||
const iv = hexDecodeToBuffer('6D656E746174696F6E206F6620494745206D6F646520666F72204F70656E5353')
|
const iv = p.hexDecode('6D656E746174696F6E206F6620494745206D6F646520666F72204F70656E5353')
|
||||||
|
|
||||||
const data = hexDecodeToBuffer('99706487a1cde613bc6de0b6f24b1c7aa448c8b9c3403e3467a8cad89340f53b')
|
const data = p.hexDecode('99706487a1cde613bc6de0b6f24b1c7aa448c8b9c3403e3467a8cad89340f53b')
|
||||||
const dataEnc = hexDecodeToBuffer('792ea8ae577b1a66cb3bd92679b8030ca54ee631976bd3a04547fdcb4639fa69')
|
const dataEnc = p.hexDecode('792ea8ae577b1a66cb3bd92679b8030ca54ee631976bd3a04547fdcb4639fa69')
|
||||||
|
|
||||||
it('should correctly encrypt', () => {
|
it('should correctly encrypt', () => {
|
||||||
const aes = ige256Encrypt(data, key, iv)
|
const aes = ige256Encrypt(data, key, iv)
|
||||||
|
|
||||||
expect(hexEncode(aes)).toEqual(hexEncode(dataEnc))
|
expect(p.hexEncode(aes)).toEqual(p.hexEncode(dataEnc))
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should correctly decrypt', () => {
|
it('should correctly decrypt', () => {
|
||||||
const aes = ige256Decrypt(dataEnc, key, iv)
|
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', () => {
|
it('should not leak memory', () => {
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { initSync } from '../src/index.js'
|
import { initSync } from '../src/index.js'
|
||||||
|
|
||||||
// todo: use platform-specific packages
|
|
||||||
export async function initWasm() {
|
export async function initWasm() {
|
||||||
const url = new URL('../lib/mtcute.wasm', import.meta.url)
|
const url = new URL('../lib/mtcute.wasm', import.meta.url)
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,17 @@
|
||||||
import { beforeAll, describe, expect, it } from 'vitest'
|
import { beforeAll, describe, expect, it } from 'vitest'
|
||||||
import { inflateSync } from 'zlib'
|
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 () => {
|
beforeAll(async () => {
|
||||||
await initAsync()
|
await initWasm()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const p = getPlatform()
|
||||||
|
|
||||||
function inflateSyncWrap(data: Uint8Array) {
|
function inflateSyncWrap(data: Uint8Array) {
|
||||||
if (import.meta.env.TEST_ENV === 'browser') {
|
if (import.meta.env.TEST_ENV === 'browser') {
|
||||||
// @ts-expect-error fucking crutch because @jspm/core uses Buffer.isBuffer for some reason
|
// @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', () => {
|
describe('zlib deflate', () => {
|
||||||
it('should add zlib headers', () => {
|
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).not.toBeNull()
|
||||||
expect(res!.slice(0, 2)).toEqual(new Uint8Array([0x78, 0x9c]))
|
expect(res!.slice(0, 2)).toEqual(new Uint8Array([0x78, 0x9c]))
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should return null if compressed data is larger than size', () => {
|
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()
|
expect(res).toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should correctly deflate', () => {
|
it('should correctly deflate', () => {
|
||||||
const data = Array.from({ length: 1000 }, () => 'a').join('')
|
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).not.toBeNull()
|
||||||
expect(res!.length).toBeLessThan(100)
|
expect(res!.length).toBeLessThan(100)
|
||||||
expect(inflateSyncWrap(res!)).toEqual(utf8EncodeToBuffer(data))
|
expect(inflateSyncWrap(res!)).toEqual(p.utf8Encode(data))
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should not leak memory', () => {
|
it('should not leak memory', () => {
|
||||||
|
@ -48,11 +51,11 @@ describe('zlib deflate', () => {
|
||||||
|
|
||||||
for (let i = 0; i < 100; i++) {
|
for (let i = 0; i < 100; i++) {
|
||||||
const data = Array.from({ length: 1000 }, () => 'a').join('')
|
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!)
|
const res = inflateSyncWrap(deflated!)
|
||||||
|
|
||||||
expect(utf8Decode(res)).toEqual(data)
|
expect(p.utf8Decode(res)).toEqual(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(__getWasm().memory.buffer.byteLength).toEqual(memSize)
|
expect(__getWasm().memory.buffer.byteLength).toEqual(memSize)
|
||||||
|
|
|
@ -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 }))
|
|
||||||
})
|
|
|
@ -1,2 +1,3 @@
|
||||||
export * from './crypto.js'
|
export * from './crypto.js'
|
||||||
|
export * from './idb/index.js'
|
||||||
export * from './platform.js'
|
export * from './platform.js'
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { describe, expect, it, Mock, MockedObject, vi } from 'vitest'
|
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 { 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'
|
import { WebSocketTransport } from './websocket.js'
|
||||||
|
|
||||||
const p = getPlatform()
|
const p = getPlatform()
|
|
@ -1,10 +1,15 @@
|
||||||
import EventEmitter from 'events'
|
import EventEmitter from 'events'
|
||||||
|
|
||||||
import { MtcuteError, MtUnsupportedError } from '../../types/errors.js'
|
import {
|
||||||
import { BasicDcOption, ICryptoProvider, Logger } from '../../utils/index.js'
|
IntermediatePacketCodec,
|
||||||
import { IPacketCodec, ITelegramTransport, TransportState } from './abstract.js'
|
IPacketCodec,
|
||||||
import { IntermediatePacketCodec } from './intermediate.js'
|
ITelegramTransport,
|
||||||
import { ObfuscatedPacketCodec } from './obfuscated.js'
|
MtcuteError,
|
||||||
|
MtUnsupportedError,
|
||||||
|
ObfuscatedPacketCodec,
|
||||||
|
TransportState,
|
||||||
|
} from '@mtcute/core'
|
||||||
|
import { BasicDcOption, ICryptoProvider, Logger } from '@mtcute/core/utils.js'
|
||||||
|
|
||||||
export type WebSocketConstructor = {
|
export type WebSocketConstructor = {
|
||||||
new (address: string, protocol?: string): WebSocket
|
new (address: string, protocol?: string): WebSocket
|
|
@ -1 +0,0 @@
|
||||||
export * from '@mtcute/core/utils.js'
|
|
|
@ -390,9 +390,15 @@ importers:
|
||||||
|
|
||||||
packages/wasm:
|
packages/wasm:
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@mtcute/tl-runtime':
|
'@mtcute/core':
|
||||||
specifier: workspace:^
|
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:
|
packages/web:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
Loading…
Reference in a new issue