diff --git a/packages/alphatab/src/ImporterSettings.ts b/packages/alphatab/src/ImporterSettings.ts index 5eb3dbafd..a6553f4b7 100644 --- a/packages/alphatab/src/ImporterSettings.ts +++ b/packages/alphatab/src/ImporterSettings.ts @@ -63,4 +63,16 @@ export class ImporterSettings { * ![Disabled](https://alphatab.net/img/reference/property/beattextaslyrics-disabled.png) */ public beatTextAsLyrics: boolean = false; + + /** + * This setting controls the escape hatch for handling potentially malicous or corrupt + * input files. At selected spots in the codebase, we use this buffer size as maximum + * allowed sizes. e.g. during unzipping or decoding strings. + * This prevents resource exhaustion, especially when alphaTab is used on server side. + * Increase this buffer size if you need to handle very big files. + * @defaultValue `128000000` + * @category Core + * @since 1.9.0 + */ + public maxDecodingBufferSize: number = 128000000; } diff --git a/packages/alphatab/src/generated/ImporterSettingsJson.ts b/packages/alphatab/src/generated/ImporterSettingsJson.ts index 30e8377d8..582768163 100644 --- a/packages/alphatab/src/generated/ImporterSettingsJson.ts +++ b/packages/alphatab/src/generated/ImporterSettingsJson.ts @@ -67,4 +67,15 @@ export interface ImporterSettingsJson { * ![Disabled](https://alphatab.net/img/reference/property/beattextaslyrics-disabled.png) */ beatTextAsLyrics?: boolean; + /** + * This setting controls the escape hatch for handling potentially malicous or corrupt + * input files. At selected spots in the codebase, we use this buffer size as maximum + * allowed sizes. e.g. during unzipping or decoding strings. + * This prevents resource exhaustion, especially when alphaTab is used on server side. + * Increase this buffer size if you need to handle very big files. + * @defaultValue `128000000` + * @category Core + * @since 1.9.0 + */ + maxDecodingBufferSize?: number; } diff --git a/packages/alphatab/src/generated/ImporterSettingsSerializer.ts b/packages/alphatab/src/generated/ImporterSettingsSerializer.ts index 5ddd11854..90545e615 100644 --- a/packages/alphatab/src/generated/ImporterSettingsSerializer.ts +++ b/packages/alphatab/src/generated/ImporterSettingsSerializer.ts @@ -23,6 +23,7 @@ export class ImporterSettingsSerializer { o.set("encoding", obj.encoding); o.set("mergepartgroupsinmusicxml", obj.mergePartGroupsInMusicXml); o.set("beattextaslyrics", obj.beatTextAsLyrics); + o.set("maxdecodingbuffersize", obj.maxDecodingBufferSize); return o; } public static setProperty(obj: ImporterSettings, property: string, v: unknown): boolean { @@ -36,6 +37,9 @@ export class ImporterSettingsSerializer { case "beattextaslyrics": obj.beatTextAsLyrics = v! as boolean; return true; + case "maxdecodingbuffersize": + obj.maxDecodingBufferSize = v! as number; + return true; } return false; } diff --git a/packages/alphatab/src/importer/BinaryStylesheet.ts b/packages/alphatab/src/importer/BinaryStylesheet.ts index 3da315306..7c19d014b 100644 --- a/packages/alphatab/src/importer/BinaryStylesheet.ts +++ b/packages/alphatab/src/importer/BinaryStylesheet.ts @@ -76,18 +76,18 @@ export class BinaryStylesheet { private readonly _types: Map = new Map(); public readonly raw: Map = new Map(); - public constructor(data?: Uint8Array) { + public constructor(data?: Uint8Array, maxDecodingBufferSize: number = 0) { if (data) { - this._read(data); + this._read(data, maxDecodingBufferSize); } } - private _read(data: Uint8Array) { + private _read(data: Uint8Array, maxDecodingBufferSize: number) { // BinaryStylesheet apears to be big-endien const readable: ByteBuffer = ByteBuffer.fromBuffer(data); const entryCount: number = IOHelper.readInt32BE(readable); for (let i: number = 0; i < entryCount; i++) { - const key: string = GpBinaryHelpers.gpReadString(readable, readable.readByte(), 'utf-8'); + const key: string = GpBinaryHelpers.gpReadString(readable, readable.readByte(), 'utf-8', maxDecodingBufferSize); const type: DataType = readable.readByte() as DataType; this._types.set(key, type); switch (type) { @@ -104,7 +104,7 @@ export class BinaryStylesheet { this.addValue(key, fvalue); break; case DataType.String: - const s: string = GpBinaryHelpers.gpReadString(readable, IOHelper.readInt16BE(readable), 'utf-8'); + const s: string = GpBinaryHelpers.gpReadString(readable, IOHelper.readInt16BE(readable), 'utf-8', maxDecodingBufferSize); this.addValue(key, s); break; case DataType.Point: diff --git a/packages/alphatab/src/importer/CapellaImporter.ts b/packages/alphatab/src/importer/CapellaImporter.ts index c380b4110..b1d4727b7 100644 --- a/packages/alphatab/src/importer/CapellaImporter.ts +++ b/packages/alphatab/src/importer/CapellaImporter.ts @@ -21,7 +21,7 @@ export class CapellaImporter extends ScoreImporter { public readScore(): Score { Logger.debug(this.name, 'Loading ZIP entries'); - const fileSystem: ZipReader = new ZipReader(this.data); + const fileSystem: ZipReader = new ZipReader(this.data, this.settings.importer.maxDecodingBufferSize); let entries: ZipEntry[]; let xml: string | null = null; entries = fileSystem.read(); diff --git a/packages/alphatab/src/importer/Gp3To5Importer.ts b/packages/alphatab/src/importer/Gp3To5Importer.ts index 9d564d3f2..30d10c2c3 100644 --- a/packages/alphatab/src/importer/Gp3To5Importer.ts +++ b/packages/alphatab/src/importer/Gp3To5Importer.ts @@ -4,7 +4,7 @@ import { ScoreImporter } from '@coderline/alphatab/importer/ScoreImporter'; import { UnsupportedFormatError } from '@coderline/alphatab/importer/UnsupportedFormatError'; import { IOHelper } from '@coderline/alphatab/io/IOHelper'; -import type { IReadable } from '@coderline/alphatab/io/IReadable'; +import { OverflowError, type IReadable } from '@coderline/alphatab/io/IReadable'; import { AccentuationType } from '@coderline/alphatab/model/AccentuationType'; import { Automation, AutomationType } from '@coderline/alphatab/model/Automation'; import { Bar, BarLineStyle } from '@coderline/alphatab/model/Bar'; @@ -126,7 +126,11 @@ export class Gp3To5Importer extends ScoreImporter { this._initialTempo = Automation.buildTempoAutomation(false, 0, 0, 0); if (this._versionNumber >= 500) { this.readPageSetup(); - this._initialTempo.text = GpBinaryHelpers.gpReadStringIntByte(this.data, this.settings.importer.encoding); + this._initialTempo.text = GpBinaryHelpers.gpReadStringIntByte( + this.data, + this.settings.importer.encoding, + this.settings.importer.maxDecodingBufferSize + ); } // tempo stuff this._initialTempo.value = IOHelper.readInt32LE(this.data); @@ -170,7 +174,11 @@ export class Gp3To5Importer extends ScoreImporter { } // contents this._barCount = IOHelper.readInt32LE(this.data); + this._ensureLoopBoundary(this._barCount, Gp3To5Importer._maxBarCount, 'bar count'); + this._trackCount = IOHelper.readInt32LE(this.data); + this._ensureLoopBoundary(this._trackCount, Gp3To5Importer._maxTrackCount, 'track count'); + this.readMasterBars(); this.readTracks(); this.readBars(); @@ -224,36 +232,108 @@ export class Gp3To5Importer extends ScoreImporter { } public readScoreInformation(): void { - this._score.title = GpBinaryHelpers.gpReadStringIntUnused(this.data, this.settings.importer.encoding); - this._score.subTitle = GpBinaryHelpers.gpReadStringIntUnused(this.data, this.settings.importer.encoding); - this._score.artist = GpBinaryHelpers.gpReadStringIntUnused(this.data, this.settings.importer.encoding); - this._score.album = GpBinaryHelpers.gpReadStringIntUnused(this.data, this.settings.importer.encoding); - this._score.words = GpBinaryHelpers.gpReadStringIntUnused(this.data, this.settings.importer.encoding); + this._score.title = GpBinaryHelpers.gpReadStringIntUnused( + this.data, + this.settings.importer.encoding, + this.settings.importer.maxDecodingBufferSize + ); + this._score.subTitle = GpBinaryHelpers.gpReadStringIntUnused( + this.data, + this.settings.importer.encoding, + this.settings.importer.maxDecodingBufferSize + ); + this._score.artist = GpBinaryHelpers.gpReadStringIntUnused( + this.data, + this.settings.importer.encoding, + this.settings.importer.maxDecodingBufferSize + ); + this._score.album = GpBinaryHelpers.gpReadStringIntUnused( + this.data, + this.settings.importer.encoding, + this.settings.importer.maxDecodingBufferSize + ); + this._score.words = GpBinaryHelpers.gpReadStringIntUnused( + this.data, + this.settings.importer.encoding, + this.settings.importer.maxDecodingBufferSize + ); this._score.music = this._versionNumber >= 500 - ? GpBinaryHelpers.gpReadStringIntUnused(this.data, this.settings.importer.encoding) + ? GpBinaryHelpers.gpReadStringIntUnused( + this.data, + this.settings.importer.encoding, + this.settings.importer.maxDecodingBufferSize + ) : this._score.words; - this._score.copyright = GpBinaryHelpers.gpReadStringIntUnused(this.data, this.settings.importer.encoding); - this._score.tab = GpBinaryHelpers.gpReadStringIntUnused(this.data, this.settings.importer.encoding); - this._score.instructions = GpBinaryHelpers.gpReadStringIntUnused(this.data, this.settings.importer.encoding); + this._score.copyright = GpBinaryHelpers.gpReadStringIntUnused( + this.data, + this.settings.importer.encoding, + this.settings.importer.maxDecodingBufferSize + ); + this._score.tab = GpBinaryHelpers.gpReadStringIntUnused( + this.data, + this.settings.importer.encoding, + this.settings.importer.maxDecodingBufferSize + ); + this._score.instructions = GpBinaryHelpers.gpReadStringIntUnused( + this.data, + this.settings.importer.encoding, + this.settings.importer.maxDecodingBufferSize + ); const noticeLines: number = IOHelper.readInt32LE(this.data); + this._ensureLoopBoundary(noticeLines, Gp3To5Importer._maxNoticeLines, 'notice line count'); let notice: string = ''; for (let i: number = 0; i < noticeLines; i++) { if (i > 0) { notice += '\r\n'; } - notice += GpBinaryHelpers.gpReadStringIntUnused(this.data, this.settings.importer.encoding)?.toString(); + notice += GpBinaryHelpers.gpReadStringIntUnused( + this.data, + this.settings.importer.encoding, + this.settings.importer.maxDecodingBufferSize + )?.toString(); } this._score.notices = notice; } + // very generous thresholds for values which control loop boundaries + // prevents DoS or resource exhaustion for corrupt files or files with malicious intent + // not configurable, as realistically GP3-5 files will not exceed these values, + + // I don't hink anyone is that verbose in the small GP5 box where you can add notices + private static readonly _maxNoticeLines = 1000; + + // I haven't encountered such a long song in the wild. beyond 1000 bars something is clearly off + private static readonly _maxBarCount = 1000; + + // I think GP5 itself limits already to ~10. 100 tracks is just unrealistic, proof me wrong + private static readonly _maxTrackCount = 100; + + // nobody reallistically writes that many beats in one bar either. + private static readonly _maxBeatCount = 100; + + // I think GP5 already limits this to way less, very generous to allow 4 times more than likely the UI supports + private static readonly _maxBendPointCount = BendPoint.MaxPosition * 4; + + private _ensureLoopBoundary(value: number, maximumValue: number, label: string) { + if (value > maximumValue) { + throw new OverflowError( + `'${label}' with value ${value} has exceeded the internal safety threshold of ${maximumValue}` + ); + } + } + public readLyrics(): void { this._lyrics = []; this._lyricsTrack = IOHelper.readInt32LE(this.data) - 1; for (let i: number = 0; i < 5; i++) { const lyrics: Lyrics = new Lyrics(); lyrics.startBar = IOHelper.readInt32LE(this.data) - 1; - lyrics.text = GpBinaryHelpers.gpReadStringInt(this.data, this.settings.importer.encoding); + lyrics.text = GpBinaryHelpers.gpReadStringInt( + this.data, + this.settings.importer.encoding, + this.settings.importer.maxDecodingBufferSize + ); this._lyrics.push(lyrics); } } @@ -272,49 +352,89 @@ export class Gp3To5Importer extends ScoreImporter { ModelUtils.getOrCreateHeaderFooterStyle(this._score, ScoreSubElement.Title).isVisible = (flags & (0x01 << 0)) !== 0; ModelUtils.getOrCreateHeaderFooterStyle(this._score, ScoreSubElement.Title).template = - GpBinaryHelpers.gpReadStringIntByte(this.data, this.settings.importer.encoding); + GpBinaryHelpers.gpReadStringIntByte( + this.data, + this.settings.importer.encoding, + this.settings.importer.maxDecodingBufferSize + ); ModelUtils.getOrCreateHeaderFooterStyle(this._score, ScoreSubElement.SubTitle).isVisible = (flags & (0x01 << 1)) !== 0; ModelUtils.getOrCreateHeaderFooterStyle(this._score, ScoreSubElement.SubTitle).template = - GpBinaryHelpers.gpReadStringIntByte(this.data, this.settings.importer.encoding); + GpBinaryHelpers.gpReadStringIntByte( + this.data, + this.settings.importer.encoding, + this.settings.importer.maxDecodingBufferSize + ); ModelUtils.getOrCreateHeaderFooterStyle(this._score, ScoreSubElement.Artist).isVisible = (flags & (0x01 << 2)) !== 0; ModelUtils.getOrCreateHeaderFooterStyle(this._score, ScoreSubElement.Artist).template = - GpBinaryHelpers.gpReadStringIntByte(this.data, this.settings.importer.encoding); + GpBinaryHelpers.gpReadStringIntByte( + this.data, + this.settings.importer.encoding, + this.settings.importer.maxDecodingBufferSize + ); ModelUtils.getOrCreateHeaderFooterStyle(this._score, ScoreSubElement.Album).isVisible = (flags & (0x01 << 3)) !== 0; ModelUtils.getOrCreateHeaderFooterStyle(this._score, ScoreSubElement.Album).template = - GpBinaryHelpers.gpReadStringIntByte(this.data, this.settings.importer.encoding); + GpBinaryHelpers.gpReadStringIntByte( + this.data, + this.settings.importer.encoding, + this.settings.importer.maxDecodingBufferSize + ); ModelUtils.getOrCreateHeaderFooterStyle(this._score, ScoreSubElement.Words).isVisible = (flags & (0x01 << 4)) !== 0; ModelUtils.getOrCreateHeaderFooterStyle(this._score, ScoreSubElement.Words).template = - GpBinaryHelpers.gpReadStringIntByte(this.data, this.settings.importer.encoding); + GpBinaryHelpers.gpReadStringIntByte( + this.data, + this.settings.importer.encoding, + this.settings.importer.maxDecodingBufferSize + ); ModelUtils.getOrCreateHeaderFooterStyle(this._score, ScoreSubElement.Music).isVisible = (flags & (0x01 << 5)) !== 0; ModelUtils.getOrCreateHeaderFooterStyle(this._score, ScoreSubElement.Music).template = - GpBinaryHelpers.gpReadStringIntByte(this.data, this.settings.importer.encoding); + GpBinaryHelpers.gpReadStringIntByte( + this.data, + this.settings.importer.encoding, + this.settings.importer.maxDecodingBufferSize + ); ModelUtils.getOrCreateHeaderFooterStyle(this._score, ScoreSubElement.WordsAndMusic).isVisible = (flags & (0x01 << 6)) !== 0; ModelUtils.getOrCreateHeaderFooterStyle(this._score, ScoreSubElement.WordsAndMusic).template = - GpBinaryHelpers.gpReadStringIntByte(this.data, this.settings.importer.encoding); + GpBinaryHelpers.gpReadStringIntByte( + this.data, + this.settings.importer.encoding, + this.settings.importer.maxDecodingBufferSize + ); ModelUtils.getOrCreateHeaderFooterStyle(this._score, ScoreSubElement.Copyright).isVisible = (flags & (0x01 << 7)) !== 0; ModelUtils.getOrCreateHeaderFooterStyle(this._score, ScoreSubElement.Copyright).template = - GpBinaryHelpers.gpReadStringIntByte(this.data, this.settings.importer.encoding); + GpBinaryHelpers.gpReadStringIntByte( + this.data, + this.settings.importer.encoding, + this.settings.importer.maxDecodingBufferSize + ); ModelUtils.getOrCreateHeaderFooterStyle(this._score, ScoreSubElement.CopyrightSecondLine).isVisible = (flags & (0x01 << 7)) !== 0; ModelUtils.getOrCreateHeaderFooterStyle(this._score, ScoreSubElement.CopyrightSecondLine).template = - GpBinaryHelpers.gpReadStringIntByte(this.data, this.settings.importer.encoding); + GpBinaryHelpers.gpReadStringIntByte( + this.data, + this.settings.importer.encoding, + this.settings.importer.maxDecodingBufferSize + ); // page number format - GpBinaryHelpers.gpReadStringIntByte(this.data, this.settings.importer.encoding); + GpBinaryHelpers.gpReadStringIntByte( + this.data, + this.settings.importer.encoding, + this.settings.importer.maxDecodingBufferSize + ); } public readPlaybackInfos(): void { @@ -397,7 +517,11 @@ export class Gp3To5Importer extends ScoreImporter { // marker if ((flags & 0x20) !== 0) { const section: Section = new Section(); - section.text = GpBinaryHelpers.gpReadStringIntByte(this.data, this.settings.importer.encoding); + section.text = GpBinaryHelpers.gpReadStringIntByte( + this.data, + this.settings.importer.encoding, + this.settings.importer.maxDecodingBufferSize + ); section.marker = ''; GpBinaryHelpers.gpReadColor(this.data, false); newMasterBar.section = section; @@ -589,10 +713,18 @@ export class Gp3To5Importer extends ScoreImporter { this.data.skip(4); // RSE: effect name - GpBinaryHelpers.gpReadStringIntByte(this.data, this.settings.importer.encoding); + GpBinaryHelpers.gpReadStringIntByte( + this.data, + this.settings.importer.encoding, + this.settings.importer.maxDecodingBufferSize + ); // RSE: effect category - GpBinaryHelpers.gpReadStringIntByte(this.data, this.settings.importer.encoding); + GpBinaryHelpers.gpReadStringIntByte( + this.data, + this.settings.importer.encoding, + this.settings.importer.maxDecodingBufferSize + ); } } else { if (tuning[tuning.length - 1] < Gp3To5Importer._bassClefTuningThreshold) { @@ -651,6 +783,8 @@ export class Gp3To5Importer extends ScoreImporter { } const newVoice: Voice = new Voice(); bar.addVoice(newVoice); + + this._ensureLoopBoundary(beatCount, Gp3To5Importer._maxBeatCount, 'beat count'); for (let i: number = 0; i < beatCount; i++) { this.readBeat(track, bar, newVoice); } @@ -732,7 +866,11 @@ export class Gp3To5Importer extends ScoreImporter { const beatTextAsLyrics = this.settings.importer.beatTextAsLyrics && track.index !== this._lyricsTrack; // detect if not lyrics track if ((flags & 0x04) !== 0) { - const text = GpBinaryHelpers.gpReadStringIntUnused(this.data, this.settings.importer.encoding); + const text = GpBinaryHelpers.gpReadStringIntUnused( + this.data, + this.settings.importer.encoding, + this.settings.importer.maxDecodingBufferSize + ); if (beatTextAsLyrics) { const lyrics = new Lyrics(); lyrics.text = text.trim(); @@ -925,7 +1063,11 @@ export class Gp3To5Importer extends ScoreImporter { } } else { const strings: number = this._versionNumber >= 406 ? 7 : 6; - chord.name = GpBinaryHelpers.gpReadStringIntByte(this.data, this.settings.importer.encoding); + chord.name = GpBinaryHelpers.gpReadStringIntByte( + this.data, + this.settings.importer.encoding, + this.settings.importer.maxDecodingBufferSize + ); chord.firstFret = IOHelper.readInt32LE(this.data); if (chord.firstFret > 0) { for (let i: number = 0; i < strings; i++) { @@ -1039,6 +1181,7 @@ export class Gp3To5Importer extends ScoreImporter { IOHelper.readInt32LE(this.data); // value const pointCount: number = IOHelper.readInt32LE(this.data); + this._ensureLoopBoundary(pointCount, Gp3To5Importer._maxBendPointCount, 'tremolo bar point count'); if (pointCount > 0) { for (let i: number = 0; i < pointCount; i++) { const point: BendPoint = new BendPoint(0, 0); @@ -1109,7 +1252,11 @@ export class Gp3To5Importer extends ScoreImporter { const phaser: number = IOHelper.readSInt8(this.data); const tremolo: number = IOHelper.readSInt8(this.data); if (this._versionNumber >= 500) { - tableChange.tempoName = GpBinaryHelpers.gpReadStringIntByte(this.data, this.settings.importer.encoding); + tableChange.tempoName = GpBinaryHelpers.gpReadStringIntByte( + this.data, + this.settings.importer.encoding, + this.settings.importer.maxDecodingBufferSize + ); } tableChange.tempo = IOHelper.readInt32LE(this.data); @@ -1155,8 +1302,16 @@ export class Gp3To5Importer extends ScoreImporter { } // unknown if (this._versionNumber >= 510) { - GpBinaryHelpers.gpReadStringIntByte(this.data, this.settings.importer.encoding); - GpBinaryHelpers.gpReadStringIntByte(this.data, this.settings.importer.encoding); + GpBinaryHelpers.gpReadStringIntByte( + this.data, + this.settings.importer.encoding, + this.settings.importer.maxDecodingBufferSize + ); + GpBinaryHelpers.gpReadStringIntByte( + this.data, + this.settings.importer.encoding, + this.settings.importer.maxDecodingBufferSize + ); } if (tableChange.volume >= 0) { const volumeAutomation: Automation = new Automation(); @@ -1337,6 +1492,8 @@ export class Gp3To5Importer extends ScoreImporter { IOHelper.readInt32LE(this.data); // value const pointCount: number = IOHelper.readInt32LE(this.data); + + this._ensureLoopBoundary(pointCount, Gp3To5Importer._maxBendPointCount, 'note bend point count'); if (pointCount > 0) { for (let i: number = 0; i < pointCount; i++) { const point: BendPoint = new BendPoint(0, 0); @@ -1529,28 +1686,36 @@ export class GpBinaryHelpers { * Skips an integer (4byte) and reads a string using * a bytesize */ - public static gpReadStringIntUnused(data: IReadable, encoding: string): string { + public static gpReadStringIntUnused(data: IReadable, encoding: string, maxDecodingBufferSize: number): string { data.skip(4); - return GpBinaryHelpers.gpReadString(data, data.readByte(), encoding); + return GpBinaryHelpers.gpReadString(data, data.readByte(), encoding, maxDecodingBufferSize); } /** * Reads an integer as size, and then the string itself */ - public static gpReadStringInt(data: IReadable, encoding: string): string { - return GpBinaryHelpers.gpReadString(data, IOHelper.readInt32LE(data), encoding); + public static gpReadStringInt(data: IReadable, encoding: string, maxDecodingBufferSize: number): string { + return GpBinaryHelpers.gpReadString(data, IOHelper.readInt32LE(data), encoding, maxDecodingBufferSize); } /** * Reads an integer as size, skips a byte and reads the string itself */ - public static gpReadStringIntByte(data: IReadable, encoding: string): string { + public static gpReadStringIntByte(data: IReadable, encoding: string, maxDecodingBufferSize: number): string { const length: number = IOHelper.readInt32LE(data) - 1; data.readByte(); - return GpBinaryHelpers.gpReadString(data, length, encoding); + return GpBinaryHelpers.gpReadString(data, length, encoding, maxDecodingBufferSize); } - public static gpReadString(data: IReadable, length: number, encoding: string): string { + public static gpReadString( + data: IReadable, + length: number, + encoding: string, + maxDecodingBufferSize: number + ): string { + if (length > maxDecodingBufferSize) { + throw new OverflowError(`Detected string exceeding maxDecodingBufferSize at offset ${data.position}`); + } const b: Uint8Array = new Uint8Array(length); data.read(b, 0, b.length); return IOHelper.toString(b, encoding); diff --git a/packages/alphatab/src/importer/Gp7To8Importer.ts b/packages/alphatab/src/importer/Gp7To8Importer.ts index 3503c85bc..8b9dbd96c 100644 --- a/packages/alphatab/src/importer/Gp7To8Importer.ts +++ b/packages/alphatab/src/importer/Gp7To8Importer.ts @@ -26,7 +26,7 @@ export class Gp7To8Importer extends ScoreImporter { // at first we need to load the binary file system // from the GPX container Logger.debug(this.name, 'Loading ZIP entries'); - const fileSystem: ZipReader = new ZipReader(this.data); + const fileSystem: ZipReader = new ZipReader(this.data, this.settings.importer.maxDecodingBufferSize); let entries: ZipEntry[]; try { entries = fileSystem.read(); @@ -78,7 +78,7 @@ export class Gp7To8Importer extends ScoreImporter { if (binaryStylesheetData) { Logger.debug(this.name, 'Start Parsing BinaryStylesheet'); - const stylesheet: BinaryStylesheet = new BinaryStylesheet(binaryStylesheetData); + const stylesheet: BinaryStylesheet = new BinaryStylesheet(binaryStylesheetData, this.settings.importer.maxDecodingBufferSize); stylesheet.apply(score); Logger.debug(this.name, 'BinaryStylesheet parsed'); } diff --git a/packages/alphatab/src/importer/GpxImporter.ts b/packages/alphatab/src/importer/GpxImporter.ts index 722d7bcf9..7d180d7a5 100644 --- a/packages/alphatab/src/importer/GpxImporter.ts +++ b/packages/alphatab/src/importer/GpxImporter.ts @@ -69,7 +69,7 @@ export class GpxImporter extends ScoreImporter { if (binaryStylesheetData) { Logger.debug(this.name, 'Start Parsing BinaryStylesheet'); - const binaryStylesheet: BinaryStylesheet = new BinaryStylesheet(binaryStylesheetData); + const binaryStylesheet: BinaryStylesheet = new BinaryStylesheet(binaryStylesheetData, this.settings.importer.maxDecodingBufferSize); binaryStylesheet.apply(score); Logger.debug(this.name, 'BinaryStylesheet parsed'); } diff --git a/packages/alphatab/src/importer/MusicXmlImporter.ts b/packages/alphatab/src/importer/MusicXmlImporter.ts index 41ae4f533..70844be76 100644 --- a/packages/alphatab/src/importer/MusicXmlImporter.ts +++ b/packages/alphatab/src/importer/MusicXmlImporter.ts @@ -230,7 +230,7 @@ export class MusicXmlImporter extends ScoreImporter { } private _extractMusicXml(): string { - const zip = new ZipReader(this.data); + const zip = new ZipReader(this.data, this.settings.importer.maxDecodingBufferSize); let entries: ZipEntry[]; try { entries = zip.read(); diff --git a/packages/alphatab/src/importer/ScoreImporter.ts b/packages/alphatab/src/importer/ScoreImporter.ts index 5b0edc554..e914c29c9 100644 --- a/packages/alphatab/src/importer/ScoreImporter.ts +++ b/packages/alphatab/src/importer/ScoreImporter.ts @@ -1,4 +1,4 @@ -import type { IReadable } from '@coderline/alphatab/io/IReadable'; +import { ThrowingReadable, type IReadable } from '@coderline/alphatab/io/IReadable'; import { Score } from '@coderline/alphatab/model/Score'; import type { Settings } from '@coderline/alphatab/Settings'; @@ -15,7 +15,11 @@ export abstract class ScoreImporter { * Initializes the importer with the given data and settings. */ public init(data: IReadable, settings: Settings): void { - this.data = data; + if (data instanceof ThrowingReadable) { + this.data = data; + } else { + this.data = new ThrowingReadable(data); + } this.settings = settings; // when beginning reading a new score we reset the IDs. Score.resetIds(); diff --git a/packages/alphatab/src/importer/ScoreLoader.ts b/packages/alphatab/src/importer/ScoreLoader.ts index 2408aa243..c85c43005 100644 --- a/packages/alphatab/src/importer/ScoreLoader.ts +++ b/packages/alphatab/src/importer/ScoreLoader.ts @@ -4,6 +4,7 @@ import { AlphaTexImporter } from '@coderline/alphatab/importer/AlphaTexImporter' import type { ScoreImporter } from '@coderline/alphatab/importer/ScoreImporter'; import { UnsupportedFormatError } from '@coderline/alphatab/importer/UnsupportedFormatError'; import { ByteBuffer } from '@coderline/alphatab/io/ByteBuffer'; +import { ThrowingReadable } from '@coderline/alphatab/io/IReadable'; import { Logger } from '@coderline/alphatab/Logger'; import type { Score } from '@coderline/alphatab/model/Score'; import { Settings } from '@coderline/alphatab/Settings'; @@ -88,12 +89,12 @@ export class ScoreLoader { const importers: ScoreImporter[] = Environment.buildImporters(); Logger.debug('ScoreLoader', `Loading score from ${data.length} bytes using ${importers.length} importers`); let score: Score | null = null; - const bb: ByteBuffer = ByteBuffer.fromBuffer(data); + const readable = new ThrowingReadable(ByteBuffer.fromBuffer(data)); for (const importer of importers) { - bb.reset(); + readable.reset(); try { Logger.debug('ScoreLoader', `Importing using importer ${importer.name}`); - importer.init(bb, settings); + importer.init(readable, settings); score = importer.readScore(); Logger.debug('ScoreLoader', `Score imported using ${importer.name}`); break; diff --git a/packages/alphatab/src/io/IReadable.ts b/packages/alphatab/src/io/IReadable.ts index f76a10429..bbcde26a6 100644 --- a/packages/alphatab/src/io/IReadable.ts +++ b/packages/alphatab/src/io/IReadable.ts @@ -49,10 +49,73 @@ export interface IReadable { } /** - * @internal + * Thrown whenever we hit the end of input data unexpectedly. + * @public */ export class EndOfReaderError extends AlphaTabError { public constructor() { super(AlphaTabErrorType.Format, 'Unexpected end of data within reader'); } } + +/** + * Thrown whenever an overflow in data or buffer sizes is detected. + * @public + */ +export class OverflowError extends AlphaTabError { + public constructor(message: string) { + super(AlphaTabErrorType.Format, message); + } +} + +/** + * An {@see IReadable} implementation throwing when the end of stream is reached guarding against + * corrupted or maliciously crafted files leading to endless reading + * @internal + */ +export class ThrowingReadable implements IReadable { + private _readable: IReadable; + public constructor(readable: IReadable) { + this._readable = readable; + } + public get position(): number { + return this._readable.position; + } + + public set position(value: number) { + this._readable.position = value; + } + + public get length(): number { + return this._readable.length; + } + + public reset(): void { + this._readable.reset(); + } + + public skip(offset: number): void { + this._readable.skip(offset); + } + + private _requireBytes(bytes: number) { + const remaining = this.length - this.position; + if (remaining < bytes) { + throw new EndOfReaderError(); + } + } + + public readByte(): number { + this._requireBytes(1); + return this._readable.readByte(); + } + + public read(buffer: Uint8Array, offset: number, count: number): number { + this._requireBytes(count); + return this._readable.read(buffer, offset, count); + } + + public readAll(): Uint8Array { + return this._readable.readAll(); + } +} diff --git a/packages/alphatab/src/io/_barrel.ts b/packages/alphatab/src/io/_barrel.ts index 8491fe7c3..0f1d10278 100644 --- a/packages/alphatab/src/io/_barrel.ts +++ b/packages/alphatab/src/io/_barrel.ts @@ -1,4 +1,4 @@ export type { IWriteable } from '@coderline/alphatab/io/IWriteable'; -export type { IReadable } from '@coderline/alphatab/io/IReadable'; +export type { IReadable, OverflowError, EndOfReaderError } from '@coderline/alphatab/io/IReadable'; export { ByteBuffer } from '@coderline/alphatab/io/ByteBuffer'; export { IOHelper } from '@coderline/alphatab/io/IOHelper'; diff --git a/packages/alphatab/src/zip/ZipReader.ts b/packages/alphatab/src/zip/ZipReader.ts index 1c929bf61..bf28690ba 100644 --- a/packages/alphatab/src/zip/ZipReader.ts +++ b/packages/alphatab/src/zip/ZipReader.ts @@ -1,6 +1,6 @@ import { ByteBuffer } from '@coderline/alphatab/io/ByteBuffer'; import { IOHelper } from '@coderline/alphatab/io/IOHelper'; -import type { IReadable } from '@coderline/alphatab/io/IReadable'; +import { OverflowError, type IReadable } from '@coderline/alphatab/io/IReadable'; import { Inflate } from '@coderline/alphatab/zip/Inflate'; import { ZipEntry } from '@coderline/alphatab/zip/ZipEntry'; @@ -9,9 +9,11 @@ import { ZipEntry } from '@coderline/alphatab/zip/ZipEntry'; */ export class ZipReader { private _readable: IReadable; + private _maxDecodingBufferSize: number; - public constructor(readable: IReadable) { + public constructor(readable: IReadable, maxDecodingBufferSize: number) { this._readable = readable; + this._maxDecodingBufferSize = maxDecodingBufferSize; } public read(): ZipEntry[] { @@ -48,8 +50,16 @@ export class ZipReader { IOHelper.readInt32LE(readable); // compressed size const uncompressedSize: number = IOHelper.readInt32LE(readable); + if (uncompressedSize > this._maxDecodingBufferSize) { + throw new OverflowError(`Zip contains files exceeding the configured maxDecodingBufferSize`); + } const fileNameLength: number = IOHelper.readInt16LE(readable); + if (fileNameLength > this._maxDecodingBufferSize) { + throw new OverflowError(`Zip contains file names exceeding the configured maxDecodingBufferSize`); + } + const extraFieldLength: number = IOHelper.readInt16LE(readable); + const fname: string = IOHelper.toString(IOHelper.readByteArray(readable, fileNameLength), 'utf-8'); readable.skip(extraFieldLength); @@ -62,6 +72,11 @@ export class ZipReader { while (true) { const bytes: number = z.readBytes(buffer, 0, buffer.length); target.write(buffer, 0, bytes); + if (target.length > this._maxDecodingBufferSize) { + throw new OverflowError( + `Zip entry "${fname}" contains data exceeding the configured maxDecodingBufferSize` + ); + } if (bytes < buffer.length) { break; } diff --git a/packages/alphatab/test-data/corrupt/healthy.gp b/packages/alphatab/test-data/corrupt/healthy.gp new file mode 100644 index 000000000..68f509ecf Binary files /dev/null and b/packages/alphatab/test-data/corrupt/healthy.gp differ diff --git a/packages/alphatab/test-data/corrupt/healthy.gp5 b/packages/alphatab/test-data/corrupt/healthy.gp5 new file mode 100644 index 000000000..63f1bad4a Binary files /dev/null and b/packages/alphatab/test-data/corrupt/healthy.gp5 differ diff --git a/packages/alphatab/test/exporter/Gp7Exporter.test.ts b/packages/alphatab/test/exporter/Gp7Exporter.test.ts index 60d749252..271e705d3 100644 --- a/packages/alphatab/test/exporter/Gp7Exporter.test.ts +++ b/packages/alphatab/test/exporter/Gp7Exporter.test.ts @@ -193,7 +193,8 @@ describe('Gp7ExporterTest', () => { it('percussion-articulations', async () => { const settings = new Settings(); const zip = new ZipReader( - ByteBuffer.fromBuffer(await TestPlatform.loadFile('test-data/exporter/articulations.gp')) + ByteBuffer.fromBuffer(await TestPlatform.loadFile('test-data/exporter/articulations.gp')), + settings.importer.maxDecodingBufferSize ).read(); const gpifData = zip.find(e => e.fileName === 'score.gpif')!.data; @@ -371,7 +372,8 @@ describe('Gp7ExporterTest', () => { it('sound-mapper', async () => { const settings = new Settings(); const zip = new ZipReader( - ByteBuffer.fromBuffer(await TestPlatform.loadFile('test-data/exporter/articulations.gp')) + ByteBuffer.fromBuffer(await TestPlatform.loadFile('test-data/exporter/articulations.gp')), + settings.importer.maxDecodingBufferSize ).read(); const gpifData = zip.find(e => e.fileName === 'score.gpif')!.data; @@ -416,7 +418,7 @@ describe('Gp7ExporterTest', () => { }); function getInstrumentSet(gp: Uint8Array) { - const zip = new ZipReader(ByteBuffer.fromBuffer(gp)); + const zip = new ZipReader(ByteBuffer.fromBuffer(gp), new Settings().importer.maxDecodingBufferSize); const gpifData = zip.read().find(e => e.fileName === 'score.gpif')!.data; const xml = new XmlDocument(); xml.parse(IOHelper.toString(gpifData, '')); diff --git a/packages/alphatab/test/importer/BinaryStylesheet.test.ts b/packages/alphatab/test/importer/BinaryStylesheet.test.ts index a732ff8e5..6931185b1 100644 --- a/packages/alphatab/test/importer/BinaryStylesheet.test.ts +++ b/packages/alphatab/test/importer/BinaryStylesheet.test.ts @@ -5,7 +5,7 @@ import { TestPlatform } from 'test/TestPlatform'; describe('BinaryStylesheetParserTest', () => { it('testRead', async () => { const data = await TestPlatform.loadFile('test-data/guitarpro7/BinaryStylesheet'); - const stylesheet: BinaryStylesheet = new BinaryStylesheet(data); + const stylesheet: BinaryStylesheet = new BinaryStylesheet(data, 1280000); expect(stylesheet.raw.has('Global/chordNameStyle')).toBe(true); expect(stylesheet.raw.get('Global/chordNameStyle')).toBe(2); diff --git a/packages/alphatab/test/importer/Gp5Importer.test.ts b/packages/alphatab/test/importer/Gp5Importer.test.ts index d954477d7..f8e3a9cb5 100644 --- a/packages/alphatab/test/importer/Gp5Importer.test.ts +++ b/packages/alphatab/test/importer/Gp5Importer.test.ts @@ -12,6 +12,8 @@ import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection import { GpImporterTestHelper } from 'test/importer/GpImporterTestHelper'; import { Clef } from '@coderline/alphatab/model/Clef'; import { PercussionMapper } from '@coderline/alphatab/model/PercussionMapper'; +import { EndOfReaderError, OverflowError } from '@coderline/alphatab/io/IReadable'; +import { TestPlatform } from 'test/TestPlatform'; describe('Gp5ImporterTest', () => { it('score-info', async () => { @@ -580,7 +582,7 @@ describe('Gp5ImporterTest', () => { const raw = new Uint8Array(fieldSize + 2); raw[0] = overlongHint; - for(let i = 0; i < fieldSize; i++) { + for (let i = 0; i < fieldSize; i++) { raw[i + 1] = 0x41; } raw[fieldSize + 1] = sentinelByte; @@ -592,4 +594,49 @@ describe('Gp5ImporterTest', () => { expect(buffer.position).toBe(1 + fieldSize); expect(buffer.readByte()).toBe(sentinelByte); }); + + describe('corrupt', () => { + async function corruptTest(intToWrite: number, offset: number, expectedOverflowLabel: string) { + const buffer = await TestPlatform.loadFile(`test-data/corrupt/healthy.gp5`); + + buffer[offset + 0] = (intToWrite >> 0) & 0xff; + buffer[offset + 1] = (intToWrite >> 8) & 0xff; + buffer[offset + 2] = (intToWrite >> 16) & 0xff; + buffer[offset + 3] = (intToWrite >> 24) & 0xff; + + const importer = GpImporterTestHelper.prepareImporterWithBytes(buffer, new Settings()); + + try { + importer.readScore(); + throw new Error('Expected readScore to fail with an OverflowError'); + } catch (e) { + if (e instanceof OverflowError) { + expect((e as OverflowError).message).toContain(expectedOverflowLabel); + return; + } + throw e; + } + } + + it('max-bar-count', async () => await corruptTest(5000, 1235, 'bar count')); + + it('max-track-count', async () => await corruptTest(300, 1239, 'track count')); + + it('notice-lines-count', async () => await corruptTest(5000, 82, 'notice line count')); + + it('beat-count', async () => await corruptTest(200, 1460, 'beat count')); + + it('tremolo-count', async () => await corruptTest(500, 1584, 'tremolo bar point count')); + + it('bend-count', async () => await corruptTest(500, 1479, 'note bend point count')); + it('eof', async () => { + let buffer = await TestPlatform.loadFile(`test-data/corrupt/healthy.gp5`); + + buffer = buffer.slice(0, buffer.length / 2); + + const importer = GpImporterTestHelper.prepareImporterWithBytes(buffer, new Settings()); + + expect(()=> importer.readScore()).toThrow(EndOfReaderError); + }); + }); }); diff --git a/packages/alphatab/test/zip/ZipReaderWriter.test.ts b/packages/alphatab/test/zip/ZipReaderWriter.test.ts index 3ee641ea8..5f236b65e 100644 --- a/packages/alphatab/test/zip/ZipReaderWriter.test.ts +++ b/packages/alphatab/test/zip/ZipReaderWriter.test.ts @@ -5,10 +5,11 @@ import { ZipEntry } from '@coderline/alphatab/zip/ZipEntry'; import { ZipReader } from '@coderline/alphatab/zip/ZipReader'; import { ZipWriter } from '@coderline/alphatab/zip/ZipWriter'; import { TestPlatform } from 'test/TestPlatform'; +import { OverflowError } from '@coderline/alphatab/io/IReadable'; describe('ZipReaderWriter', () => { it('simple-read', async () => { const data = await TestPlatform.loadFile('test-data/guitarpro7/score-info.gp'); - const reader = new ZipReader(ByteBuffer.fromBuffer(data)); + const reader = new ZipReader(ByteBuffer.fromBuffer(data), 128000000); const entries = reader.read(); expect(entries.map(e => e.fileName).join(',')).toBe( @@ -45,7 +46,7 @@ describe('ZipReaderWriter', () => { writer.end(); data.position = 0; - const reader = new ZipReader(data); + const reader = new ZipReader(data, 128000000); const entries = reader.read(); expect(entries[0].fileName).toBe('File01.txt'); @@ -57,4 +58,69 @@ describe('ZipReaderWriter', () => { expect(entries[3].fileName).toBe('LargeFile'); expect(IOHelper.toString(entries[3].data, 'utf-8')).toBe(text); }); + + describe('corrupt', () => { + async function corruptTest( + maxBuffer: number, + mainpulate: (buffer: Uint8Array) => void, + expectedOverflowLabel: string + ) { + const buffer = await TestPlatform.loadFile(`test-data/corrupt/healthy.gp`); + + mainpulate(buffer); + + const importer = new ZipReader(ByteBuffer.fromBuffer(buffer), maxBuffer); + + try { + importer.read(); + throw new Error('Expected zip read to fail with an OverflowError'); + } catch (e) { + if (e instanceof OverflowError) { + expect((e as OverflowError).message).toContain(expectedOverflowLabel); + return; + } + throw e; + } + } + + // properly announce compressed size which exceeds the range (100kb max, 300kb announced) + it('uncompressed-size', async () => + await corruptTest( + 100000, + buffer => { + const intToWrite = 300000; + buffer[22] = (intToWrite >> 0) & 0xff; + buffer[23] = (intToWrite >> 8) & 0xff; + buffer[24] = (intToWrite >> 16) & 0xff; + buffer[25] = (intToWrite >> 24) & 0xff; + }, + 'contains files exceeding' + )); + + // properly announce filename size which exceeds the range (30kb max, 31kb announced) + it('filename', async () => + await corruptTest( + 30000, + buffer => { + const shortToWrite = 31000; + buffer[26] = (shortToWrite >> 0) & 0xff; + buffer[27] = (shortToWrite >> 8) & 0xff; + }, + 'contains file names exceeding' + )); + + // unexpected large inflation (binary stylesheet is ~21kb, limit to 15kb and fake binary stylesheet to be in-limit ) + it('inflate', async () => + await corruptTest( + 15000, + buffer => { + const intToWrite = 10000; + buffer[60] = (intToWrite >> 0) & 0xff; + buffer[61] = (intToWrite >> 8) & 0xff; + buffer[62] = (intToWrite >> 16) & 0xff; + buffer[63] = (intToWrite >> 24) & 0xff; + }, + 'Zip entry "Content/BinaryStylesheet" contains data' + )); + }); }); diff --git a/packages/csharp/src/AlphaTab.Test/Test/Globals.cs b/packages/csharp/src/AlphaTab.Test/Test/Globals.cs index b8cf08b94..1634e4879 100644 --- a/packages/csharp/src/AlphaTab.Test/Test/Globals.cs +++ b/packages/csharp/src/AlphaTab.Test/Test/Globals.cs @@ -368,6 +368,10 @@ public void Contain(object element) { CollectionAssert.Contains(collection, element, _message); } + else if(_actual is string s) + { + Assert.Contains((string)element, s, _message); + } else { Assert.Fail("Contain can only be used with collection operands"); diff --git a/packages/kotlin/src/android/src/test/java/alphaTab/core/TestGlobals.kt b/packages/kotlin/src/android/src/test/java/alphaTab/core/TestGlobals.kt index e60c9e6db..17972d74e 100644 --- a/packages/kotlin/src/android/src/test/java/alphaTab/core/TestGlobals.kt +++ b/packages/kotlin/src/android/src/test/java/alphaTab/core/TestGlobals.kt @@ -231,6 +231,11 @@ class Expector(private val actual: T, private val message: String? = null) { message ?: "Expected collection ${actual.joinToString(",")} to contain $value", actual.contains(value) ) + } else if(actual is String) { + Assert.assertTrue( + message ?: "Expected string $actual to contain $value", + actual.contains(value as String) + ) } else { Assert.fail("contain can only be used with Iterable operands"); } diff --git a/packages/playground/src/apps/TestResultsApp.ts b/packages/playground/src/apps/TestResultsApp.ts index ab83a9b2f..35da520b2 100644 --- a/packages/playground/src/apps/TestResultsApp.ts +++ b/packages/playground/src/apps/TestResultsApp.ts @@ -352,7 +352,7 @@ export class TestResultsApp implements Mountable { if (!(buffer instanceof ArrayBuffer)) { return; } - const zip = new ZipReader(ByteBuffer.fromBuffer(new Uint8Array(buffer))); + const zip = new ZipReader(ByteBuffer.fromBuffer(new Uint8Array(buffer)), 128000000); const entries = zip.read(); const grouped = new Map(); for (const entry of entries) { diff --git a/packages/transpiler/src/csharp/CSharpEmitterContext.ts b/packages/transpiler/src/csharp/CSharpEmitterContext.ts index 04a5c9d7d..aa17dcd52 100644 --- a/packages/transpiler/src/csharp/CSharpEmitterContext.ts +++ b/packages/transpiler/src/csharp/CSharpEmitterContext.ts @@ -73,13 +73,14 @@ export default class CSharpEmitterContext { public isPropertySymbol(tsSymbol: ts.Symbol) { if ( - (tsSymbol.flags & (ts.SymbolFlags.Property | ts.SymbolFlags.GetAccessor | ts.SymbolFlags.SetAccessor)) !== 0 + (tsSymbol.flags & (ts.SymbolFlags.Property | ts.SymbolFlags.GetAccessor | ts.SymbolFlags.SetAccessor)) !== + 0 ) { return true; } // globals - if((tsSymbol.flags & ts.SymbolFlags.FunctionScopedVariable) !== 0 && this.isGlobalVariable(tsSymbol)) { + if ((tsSymbol.flags & ts.SymbolFlags.FunctionScopedVariable) !== 0 && this.isGlobalVariable(tsSymbol)) { return true; } @@ -645,19 +646,9 @@ export default class CSharpEmitterContext { } as cs.TypeReference; // remove ArrayBuffer type arguments - switch (symbolName) { - case 'Int8Array': - case 'Uint8Array': - case 'Int16Array': - case 'Uint16Array': - case 'Int32Array': - case 'Uint32Array': - case 'Float32Array': - case 'Float64Array': - case 'DataView': - typeRef.typeArguments = []; - typeRef.tsSymbol = undefined; - break; + if (this._isTypedArrayName(symbolName)) { + typeRef.typeArguments = []; + typeRef.tsSymbol = undefined; } if (!typeRef.typeArguments && (tsType as ts.Type).aliasTypeArguments) { @@ -669,6 +660,21 @@ export default class CSharpEmitterContext { return typeRef; } } + private _isTypedArrayName(symbolName: string) { + switch (symbolName) { + case 'Int8Array': + case 'Uint8Array': + case 'Int16Array': + case 'Uint16Array': + case 'Int32Array': + case 'Uint32Array': + case 'Float32Array': + case 'Float64Array': + case 'DataView': + return true; + } + return false; + } private resolveExternalModuleOfType(tsSymbol: ts.Symbol): string | undefined { // TODO: the future goal here is to find the import statement which brought the type into the current module @@ -844,7 +850,8 @@ export default class CSharpEmitterContext { return null; } - if ('typeArguments' in pTsType && cs.isTypeReference(pType)) { + if(!this._isTypedArrayName(pTsType.symbol?.name ?? '') + && 'typeArguments' in pTsType && cs.isTypeReference(pType)) { const args = this.typeChecker.getTypeArguments(pTsType as ts.TypeReference); if (args.length > 0) { pType.typeArguments = args.map(a => this.getTypeFromTsType(pType, a)!);