From dc438b3adb6c9683b1439bd74d8ea35e886b43c7 Mon Sep 17 00:00:00 2001 From: Danielku15 Date: Sun, 17 May 2026 13:26:34 +0200 Subject: [PATCH 1/7] fix: fail fast when hitting EOF on importers --- .../alphatab/src/importer/ScoreImporter.ts | 8 ++- packages/alphatab/src/importer/ScoreLoader.ts | 7 +-- packages/alphatab/src/io/IReadable.ts | 52 +++++++++++++++++++ 3 files changed, 62 insertions(+), 5 deletions(-) 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..dc75846c8 100644 --- a/packages/alphatab/src/io/IReadable.ts +++ b/packages/alphatab/src/io/IReadable.ts @@ -56,3 +56,55 @@ export class EndOfReaderError extends AlphaTabError { super(AlphaTabErrorType.Format, 'Unexpected end of data within reader'); } } + +/** + * 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(); + } +} From c6a5eb57d067e94a92f8c1f2a7a2eef11428c836 Mon Sep 17 00:00:00 2001 From: Danielku15 Date: Sun, 17 May 2026 14:13:22 +0200 Subject: [PATCH 2/7] fix: add max decoding buffer threshold to importers --- packages/alphatab/src/ImporterSettings.ts | 12 ++ .../src/generated/CoreSettingsJson.ts | 11 + .../src/generated/CoreSettingsSerializer.ts | 4 + .../alphatab/src/importer/CapellaImporter.ts | 2 +- .../alphatab/src/importer/Gp3To5Importer.ts | 204 ++++++++++++++---- .../alphatab/src/importer/Gp7To8Importer.ts | 2 +- .../alphatab/src/importer/MusicXmlImporter.ts | 2 +- packages/alphatab/src/io/IReadable.ts | 13 +- packages/alphatab/src/io/_barrel.ts | 2 +- packages/alphatab/src/zip/ZipReader.ts | 19 +- .../test/exporter/Gp7Exporter.test.ts | 8 +- .../alphatab/test/zip/ZipReaderWriter.test.ts | 4 +- .../playground/src/apps/TestResultsApp.ts | 2 +- 13 files changed, 234 insertions(+), 51 deletions(-) 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/CoreSettingsJson.ts b/packages/alphatab/src/generated/CoreSettingsJson.ts index 49b551225..89f21e728 100644 --- a/packages/alphatab/src/generated/CoreSettingsJson.ts +++ b/packages/alphatab/src/generated/CoreSettingsJson.ts @@ -174,4 +174,15 @@ export interface CoreSettingsJson { * ``` */ includeNoteBounds?: 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/CoreSettingsSerializer.ts b/packages/alphatab/src/generated/CoreSettingsSerializer.ts index 5b1dd0127..1d23ebf4b 100644 --- a/packages/alphatab/src/generated/CoreSettingsSerializer.ts +++ b/packages/alphatab/src/generated/CoreSettingsSerializer.ts @@ -45,6 +45,7 @@ export class CoreSettingsSerializer { o.set("loglevel", obj.logLevel as number); o.set("useworkers", obj.useWorkers); o.set("includenotebounds", obj.includeNoteBounds); + o.set("maxdecodingbuffersize", obj.maxDecodingBufferSize); return o; } public static setProperty(obj: CoreSettings, property: string, v: unknown): boolean { @@ -91,6 +92,9 @@ export class CoreSettingsSerializer { case "includenotebounds": obj.includeNoteBounds = v! as boolean; return true; + case "maxdecodingbuffersize": + obj.maxDecodingBufferSize = v! as number; + return true; } return false; } 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..8ac91981e 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); @@ -224,25 +228,65 @@ 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); 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; } @@ -253,7 +297,11 @@ export class Gp3To5Importer extends ScoreImporter { 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 +320,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 +485,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 +681,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) { @@ -732,7 +832,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 +1029,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++) { @@ -1109,7 +1217,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 +1267,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(); @@ -1529,28 +1649,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..cfac60202 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(); 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/io/IReadable.ts b/packages/alphatab/src/io/IReadable.ts index dc75846c8..bbcde26a6 100644 --- a/packages/alphatab/src/io/IReadable.ts +++ b/packages/alphatab/src/io/IReadable.ts @@ -49,7 +49,8 @@ export interface IReadable { } /** - * @internal + * Thrown whenever we hit the end of input data unexpectedly. + * @public */ export class EndOfReaderError extends AlphaTabError { public constructor() { @@ -57,6 +58,16 @@ export class EndOfReaderError extends AlphaTabError { } } +/** + * 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 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..478de9d4d 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 file contents exceeding the configured maxDecodingBufferSize` + ); + } if (bytes < buffer.length) { break; } 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/zip/ZipReaderWriter.test.ts b/packages/alphatab/test/zip/ZipReaderWriter.test.ts index 3ee641ea8..d7b282bf1 100644 --- a/packages/alphatab/test/zip/ZipReaderWriter.test.ts +++ b/packages/alphatab/test/zip/ZipReaderWriter.test.ts @@ -8,7 +8,7 @@ import { TestPlatform } from 'test/TestPlatform'; 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 +45,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'); 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) { From 0d76d066cc632fbe1c2a3c4dfdcf39c7e6cea3eb Mon Sep 17 00:00:00 2001 From: Danielku15 Date: Sun, 17 May 2026 14:13:47 +0200 Subject: [PATCH 3/7] fix: add loop boundary safety --- .../alphatab/src/importer/Gp3To5Importer.ts | 37 ++++++++++++++++++ .../corrupt/corrupted-bend-point-count.gp5 | Bin 0 -> 3803 bytes .../test/importer/Gp5Importer.test.ts | 8 ++++ 3 files changed, 45 insertions(+) create mode 100644 packages/alphatab/test-data/corrupt/corrupted-bend-point-count.gp5 diff --git a/packages/alphatab/src/importer/Gp3To5Importer.ts b/packages/alphatab/src/importer/Gp3To5Importer.ts index 8ac91981e..30d10c2c3 100644 --- a/packages/alphatab/src/importer/Gp3To5Importer.ts +++ b/packages/alphatab/src/importer/Gp3To5Importer.ts @@ -174,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(); @@ -277,6 +281,7 @@ export class Gp3To5Importer extends ScoreImporter { 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) { @@ -291,6 +296,33 @@ export class Gp3To5Importer extends ScoreImporter { 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; @@ -751,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); } @@ -1147,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); @@ -1457,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); diff --git a/packages/alphatab/test-data/corrupt/corrupted-bend-point-count.gp5 b/packages/alphatab/test-data/corrupt/corrupted-bend-point-count.gp5 new file mode 100644 index 0000000000000000000000000000000000000000..4effb7d976be8305c43884008f9fb72dd6f29342 GIT binary patch literal 3803 zcmeHJ-A)rh6y7eBLgh~l#tXzr$P$I1U?gC?X=xEQ0o!c1AzoIxLSt#k?v{in@F{!; zyf%8FH(r_Q1NbP^^UZYkEXA4V;~FeRp}4MIN9c9fNL#j9unpyx-d->67}v=#xm$P3o_Kj8jNP1-Ehyzq!>jB` zSNs22jXBVBmL`gqifA zLphadB2#sUFN%CcWGkLuchNyG%}Dz2NZ9q76`AMpokua&PArG=SKS1{Du= z{9oV*Y(_<}j%Q{dcow$O#h@LF-?}E(MFx>{6*$;=yEtL)5eSB)n-p1y_i;WKl@+cN zN { it('score-info', async () => { @@ -592,4 +593,11 @@ describe('Gp5ImporterTest', () => { expect(buffer.position).toBe(1 + fieldSize); expect(buffer.readByte()).toBe(sentinelByte); }); + + it('corrupted-bend-point-count', async () => { + const reader = await GpImporterTestHelper.prepareImporterWithFile( + 'corrupt/corrupted-bend-point-count.gp5' + ); + expect(() => reader.readScore()).toThrow(OverflowError); + }); }); From bbc7f5bd488fd5deb3a7809901970d5aa4e9d748 Mon Sep 17 00:00:00 2001 From: Danielku15 Date: Sun, 17 May 2026 15:02:05 +0200 Subject: [PATCH 4/7] test: add corrupt file test coverage --- packages/alphatab/src/zip/ZipReader.ts | 2 +- .../corrupt/corrupted-bend-point-count.gp5 | Bin 3803 -> 0 bytes .../alphatab/test-data/corrupt/healthy.gp | Bin 0 -> 12842 bytes .../alphatab/test-data/corrupt/healthy.gp5 | Bin 0 -> 1718 bytes .../test/importer/Gp5Importer.test.ts | 53 ++++++++++++-- .../alphatab/test/zip/ZipReaderWriter.test.ts | 66 ++++++++++++++++++ 6 files changed, 113 insertions(+), 8 deletions(-) delete mode 100644 packages/alphatab/test-data/corrupt/corrupted-bend-point-count.gp5 create mode 100644 packages/alphatab/test-data/corrupt/healthy.gp create mode 100644 packages/alphatab/test-data/corrupt/healthy.gp5 diff --git a/packages/alphatab/src/zip/ZipReader.ts b/packages/alphatab/src/zip/ZipReader.ts index 478de9d4d..bf28690ba 100644 --- a/packages/alphatab/src/zip/ZipReader.ts +++ b/packages/alphatab/src/zip/ZipReader.ts @@ -74,7 +74,7 @@ export class ZipReader { target.write(buffer, 0, bytes); if (target.length > this._maxDecodingBufferSize) { throw new OverflowError( - `Zip entry ${fname} contains file contents exceeding the configured maxDecodingBufferSize` + `Zip entry "${fname}" contains data exceeding the configured maxDecodingBufferSize` ); } if (bytes < buffer.length) { diff --git a/packages/alphatab/test-data/corrupt/corrupted-bend-point-count.gp5 b/packages/alphatab/test-data/corrupt/corrupted-bend-point-count.gp5 deleted file mode 100644 index 4effb7d976be8305c43884008f9fb72dd6f29342..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3803 zcmeHJ-A)rh6y7eBLgh~l#tXzr$P$I1U?gC?X=xEQ0o!c1AzoIxLSt#k?v{in@F{!; zyf%8FH(r_Q1NbP^^UZYkEXA4V;~FeRp}4MIN9c9fNL#j9unpyx-d->67}v=#xm$P3o_Kj8jNP1-Ehyzq!>jB` zSNs22jXBVBmL`gqifA zLphadB2#sUFN%CcWGkLuchNyG%}Dz2NZ9q76`AMpokua&PArG=SKS1{Du= z{9oV*Y(_<}j%Q{dcow$O#h@LF-?}E(MFx>{6*$;=yEtL)5eSB)n-p1y_i;WKl@+cN zNPyI8*S3qw$-GuZM@CBzux;i&&B)x|62$9 z>^U>vv)Aldb7p<^R*(S$M+f{ngZ@=0Vr%1MV&lZ5@V8pffVZ+g8}_f=-?fA-Yz!RS zl%3qHOdQQkOq?=wWNh)(ohxm$$fC%X^C^__izN<)zSJ_XU{VP&4-6bp_DZ}j#c`IY zZG|puKICZ0JtzgLHVlkO3lhWl5knhRfy!dKfv|~>zZ_bgv)a@`Jp0mZxR2v2Y-v4PE-%wcm^=tZ31ZU!TMCHvC z_?LfOtvvNLS;-b#ueeo0RVQk5zrLnm>FGdv7RM)`AYY5R6HdWUU$UXnFH}+i?*{iUW|T!DXo>DiPDcQu=ePKYxtf+AAGvr(c_q()*#2}eJnzsWhl;c^ zGb{e|5{2c8T|s{0!)?J9dMGquXLR;sGY^YgC&#;7qTBw6=th|=l?^n736POCvnrE;2DAIJmYiKUzISq58H+pO&v7wR6~KeIN*j z({(weHoEhrBkU#kPN(xj%tIRr6w@{eiKuA2JMDLp75ByAgUyuhNb)~OXUzD?*BLCJ zCZZ&f(%JBv+RCu!CZirQySo8}G>{37Bm8EfKzd@~OKzS|F4Z`lgGC=#W-jTz-S0c7 zzOQ&*utzQ{wAsxT1U~vd1BinkHY~PW29c7(K8pLXDlEZp8dZO&>&MsA^`GBrMjVL# zx^Ei=De_tHmF9LDM1-I(dW~>=z$WW^W@G1C3B4im%+WP!te4L7qM;~1X!So zi9zM~f}i|R^daA0jeZX^_@vvFapZ!h&(fbVc{Jw8Ru-jQ!%L}$HUcVBCb07Px*?#+ z?6V6qHW=kW$%8j1xr4Ax0fc6OAtz-{e^N#ySZ=u=I&)OgKBPUA6dRupIy3A-cu?7j z15k`q+6A2oWrjDUh&~aEnb~CDxso57&mJ^cYDdoNAq+W!U! zN2~(B7{S-{4&FGRT*$s%H8tErp&u#_#4$N$)&ie@zI!u5x3^hbVKN&QbsV7{3;24b zx8?-%lQv*XE>`!Id+|VmkHdlm4Gi@2;Zzhd=KC}o&WOzei6)O$F-3r<0U@>7N9O)- zzVb~chS{WPNlXM0S%p_oQe?U(By^WvZEbvHb*g2ghKZTBAC&Hv9odD^6Nd&aD4c{Z(9ywv>zX#9q@oLU*GyB#81Zbg6wa9~?c{rK zLjD92Ksvcr>rj_+BQ=r*vcvGxoo=!**7W&<1Hcxi!LZh)RV^9~a2E%d{>@6~l#QJTvrYbqbXV*^fOVgw2 z1lj{WDh<@G?9#ZTVWbpZlZ7U!CZdOS!6{Pr4bMywfn*0S0xr{IpO@$v{ciBat=W@V zXW@>z=+QmMch|nS0ZjCLY437hapEaaZf9QwY)=xmIs8*BzC<9yWWvfXrqn-Z9lnaPgt1<%!$>lQgqC}gKZtLkcD6^SL#5f{{UHGp#NPtQRhPl89+y;AV^Nz`I8 z*fx;cSP~Y-~T`I-YIxrSel>LxI_- zf5e)_hqEOJIBCQqyLBVce>*m8v4+xOt;#eE91%pLuOzK@4U=PV&0(sh%<|`u)^0EM zsmFrT*Xat+qaa0pIu?a~4FL+A{q64y6Uddj@FLc?0g1lk+&>5gxCt1E*N&j0vGE;4 z!Xt?fEy#Gu3b1hXKaX8jCEKkL@1u}x{KN#uJ)%%{z}Bxx&km1)h|OQ`lHJV9O;?=< z9H0Csk&VM1Fm{X&6%DE!>!9x&4d*Zr=ZMq~*BuCF)Icdo0=xYRGIKL|9<3BZ9q$Rt z7lP#|7~c?9st-%<3cH>BYWssdkaa3^{dzTfQBIdS*oH4e`2B_PYE%n=cuX5oW2Y}6 zok#;*s^#;ykJUdO>5a+Z#9Jb6hx;D#lnT(~I;$%e*)d4z0rEKAQrK=TSJ zo=!l6BEsWM%hQ{fl*d|*hzP^_oLeVdovPVwQ)Xu*w(6rPY6OlKZxEa~JK6;niJ3fa z@EXYi_*gfgGoVD_%HpV;K~Go?!OdN~))WYhl`kWEiHp+NWGbi-D(AN+%fS?>El3lh z%S{&zQ)_x7u`wolTJ9@WP4n7Xx;h;Fot}O1r~}8`w7ce~8A4-}$7YE_A(7_r;>dA9 zT&dtw;~odYL>99wN_G7^REiAB=N-k90uL?yp>h4L0X!7dX+QYJ)KTjfHx=Cu#@EL^ zLh*~LRTeQ1TMG`zOnCN>Pd@B5YpU(155ez>v7iZIBbk;|5NE;?5pa68t+^NkHVa=M zv9g(?6~qOb7%$d*lTwCsyzoqgTo#B|>408)`g)!W7hYCYRtTUAu$MwC+m#EzOgTEk zm8MWDaJGqN#=Rps9gVLQGZJ4OTb^d)xPMH*IfIHr#w+%~I}hRDibK1HqF?(cc}tzD zLfxI4>rLOCZ#0Qk*>(U6H>xdnkylZ)CJd>LAh?y{_?#5!sOS!so45~U)4_j{r(V^T zc$B%rtUllNe6u_s=59TlwUGXNPf`Dz8%RHxI9!?u6D4&>gOtQS-B#vX#3*J(={hd3H_GaI>}6XHG)gKDSfb*Av*e%~V~>m*m8T zlH9}xa+HP_ic&))SLUMtWO%R}I^1|ab>q;TITNQ;0MepF`@r%9mr8Igt! z?~qyU&H)*WdhRMxpuW$UG(3ikAg0PnzLfVo2EBk~Anm}vsJ}WDo-x(Viqu@Le)q&O zua0AV)~$whCfV?9_F?njz~S4WbC-fUqQeo0AGz+F>D*$;S|i>SabU9Rp$c+>s}ooB z)S-Ah;nz$->Eux?D?M}J^c6pt@lm8}aRk>B?Vy1$sILNWwpUs3Xq35G zCNRRn3;a(`dStA%RKxnoH$tVcS94qJW8EQ2-oL`w5O;lpbmOvzID{F1C5Bg zj^uou3(z3+=EAD`pE`VQ5S@Yo0!10hdOe|iDt!dCA9u$EvSzcaqY%H`EsU6D;jxv~ zE5LD@a{Q2`mviVuIOOOOGtv>nFC(eKLcAxgOL~dCc!h1XW#0ZSvItc2kd6EZHQ9l% zJ5mC9M*3m-`#xa^47cx;*}a(tQ-0>^M3yFMjn>9HBft_%1^iEQ?A{r+=nGu#iNYU; z&89%W_590Gt~FVV`B#webFQ5Dp|ZQ47E3TK;#HqE;h(1Wl{Me{>jwsX?r+O6wlq-? z!h*jUwp$$coaOTbcjNass4vcs$b7OF2Cwu_OA<_XPQN_@0v5>1wWpExVtkM#s1>t%njyJ)2@)#J> zhT%{c@XB-90cv6skLy`&D;hxBF`^iateuU%!;COv?>hLSmTvx_hw;W>2{J(#i=d)c zCUss#grQ8I$B*-P0NNj?z#GRahm%Se$)^uia~7r#*^X_`DMdd7$;*P#XWy5}n)4+- zfzd(Bs?tCN7e$-RBx)+i1@SFME0fy<-TW|J(}1Y5+nXw9e2U+o&B3EaTcbgQ;f<>6 zwu-yV2(ez-bTd;F73kFD%bY8JLTx}3T9-BH`!f1M4V_vI7jKoqUNw;!IHCyQUE!+Y zq#DkvlqVHrrdb~VztXesfCE|AMr>5mXLt`2^L$PR@!nrcQQmp*6y8C^60kmA;(pZ9 zsUv_96QY=sUz${7hgME0;$XqS*M0fK>_XRn@a6aXRlqHXj!e*PG^r1WJ$?T zF62a7vI=Hd`bowFIW2Pg7r`dVuO?bLB#qZgs%%;}j>l{DbI@UUlk?>eTO^%lUvkI3 z_?7y7p9VQxlQPi3%s%h~p=3{n(73PE2c$FMB*fAnQGXZ_9f>83_nL$nSZdp-;lmQ{ z#p!-9N!N1}Hqatg3OhkzGfrVAo;RMT3F4Zmp7&Qz9aQV4!#sgYzm)Bxzv2lE2-Q40 z+lPx*Y)X$o452qJ;mBbeAm++{-&D-0Sfe(lCQr**l7gI>?bq_G%Up$L$k3P!E%+!()Y-)crD=l z*OpMrHbeY-mh15Ex?JyZ5PnFb^dFXxf6X#l;lqAunr8!5bW)mPD-7-Q0BQSRq>8;N z8mzC1cZ{l`uBjD)bqSqLII&!_Qj>GT1`o@UR4r1P6nUhtut(afFH5ig6`%*hYyzO0{ zem4EZ|V><-B)hHU29WT(O!5ANI|_fi_fc) z70$*A8^KB&0hOZ7VWeR>TrnSR#o>fHxRzAts5CI|z;ot7hke zI9jNG?e;m&(}SyGTJN*TaH}g1rb0p&@pzUO%+$2-16JfWQwg~=;8P^n4%=$JYXp;C zi8%1;v`MGGEzIPZ3CRsVeT3M`C9vwiNy;nZ2he^iDdQ5yV*L5Du%rLF!A4BUlW#HR z!QwTlLdp;K^dV)@bVW;#c26V|V;_lhP%pBFy3~7J;KS@O4VlEzw6YOO{7kMy+zpN9 zB2KAf@o~Cz0D!%}qAR3DJN82Xx*Q9J%Az@5RCs8m3$2My**cd&WfCj(t2c4{**BWX z%eHK+M>KYKO5VykWHm;;q=p`>oMSSx!$ZpY1Ifb6OVo~wcz zN`L*h$|R$N*xepfQGCXu4Y?^vMLl8drxm?QB-AW&+?ZbCtw9eh_6;qL6=}<}^cCui z9q-GEDgYpW2>|?O{*3nr{w!~Up}0>ApOd;GpIWTU>8CObHj++xyUd32yDqyOF<$~Gt1`BE zjghMDjTUa8H4Ah_AzGa?vW*ow7V59&XC^cOTU%t7J6@~?J z$O`R@sKZJdR8*}W42o+vujiKBjOx+H8MuP_-siS4fdByY|M2?Phkr4&UnRdU3n&}e zI+&2a&v7MIF`q+K#w{q%-hgx;=e)do(=Z2llvlxX`%E*?YIATFtxyQ0HBJT>E1<%I5CVptC$z}vD_ybkwxL}*1c~#p8t}y2uT@c4^iqP{_Z)i^I zqo>Y6gb0%7v!&te#1b8r{{uXK%&BqsldklD{a`VVRUpGL?P2_#P044DL%uoijqDunVnlQ5dXUHeQiPH^1$QOg_ z)SPF5Pr^Z?o{pcN_cxw1y=CF=x5uxeK%RHIQmc@fvL4+>Ev+jXa@BfzRzz7ymoI%N zyop)#3i#Q@iC=iAggnGZOkx`V>0tHpTp74V6X50`h^Y(UR=%v}v~mhs$mf7CdbA@x6_jc5D2Pc-_l= zXN@)O#}bAha5{JVdqyzqr`k(_METEM*R6zJdqSKY6aEz-ZS)iVhZc@+q4PFk9aq&qNNTHN3wqmJ zx#uk&&yJ)m*toc~7r}|JxZ9RnCP=T!<;yOJDEXV_({k2WK9UO{EOtjLap2?) z5Qd`j7$X61kd2YX5IBsHD8e`L<(-j4kb~eG9mn|x=`AbwGy4d~23>${>$lHiE?&Fo zq9M@2U;xu9qLG!8gw&7PQ46=GM&p1qI|NSBj94fPssE)9f*_r? zUZrPnLg*sZfb!udOsFyvSxJ%tdw*~(}cCblC;F*yq;;e+B_xA&4@B?Uzr=h~cm zRv#sQxRMKhxiRjR+(P4mHPr85Xx2>G;0m9MdGy|f82<(xa!zvLB`Spn9q=9<9(kDD zI-njMTSD0!5mY%l3f_W*85I*bPj|v3e`*jd8uE*rjsCW!=mwzsl-exZPRwo2#r&cM1-oV;HVII3I&n3sWU@{ivd*%MMPOu z*a*?)Vhgfik@qP+4?RHJWv4yPLm_oxUUIbD$jb-}7>q03y`lht7^EGLK7n$Q%Mhdy z(thj=5yb1qfp@2TrAFZ1;&8=@!Y3WQ7e^vCUq-#QHO0+}uiY|momUH{s?cnx`LG#J zk;5FdHH_v?yCCblE^JuK6h0Ww**tSNu-|F=he2WqycFhle$Du}izB4+wS|87vF)>I z#e->$(epuCw^_+`x8p|%)q^XI!4(a+jZ69otW8j7_S;DmtWkzdrQ=twT4OU)4sNC3 z^&gqHJW5h!@owMnKgwwJMiRE!IK|Y0&L=NJy@c9mZ{67}*sx#NzJyw$M2xcfChU-6 zraW8w=-s?u?;~Mdu~5r$b{oUap(1bV4sF7><0aBCkRpVj6z2tY_l3Vw@k51S*g#GQ?-BrR8-M*g3}0jOqboW%tj_xn zN^OAuo?8Fi?FMB3ZK8DkwZ{0{g27R2m_)W4fGob@@=6e5D7n40)e~z?LZWPp`bCs9 zM$*{i&xV1VIMOQrJs=)I+Zq2uZ2uPJn2zM>YpdeW%~H#{fZk&lo$dEa6z|=8Q-S-< zw2^^`F6xM(;)p);h_SthPV9)m!cK(0q4b(r3jo*Kq7N*B|mdZc5E+j9=Wv>YlrEoMpPMNJjj00QJz99{*qA$&%33I{Wh$Ke?l+9E zv5BbVHSq}=j~U9WCkDbkW3Cc3vauWwSiysrL26-O zFQGB*pl8m@g3o+vd%V9hw&TCMPx_$Kufp6hSXdS$V>gUr*Qqp#J?_hlD_d91O>Mxi zt2^>s&r+z*uuCeFq8{<=@6g7Ti2aegh)~oEtZq{&vLXE^m!_RFr!8ZhpFjG5leC*M zJnVvlxFW6VP7ZpSOeI1?I%kbOp*1PSM55!6D{bFa8n_#KsKN!vnHo2h2+!E1*+O?Z$LV&U^M;Q{v{>VP&`62}B3>EpabCbq4sV*>Jy{4FK(|BE4 zfDrxJf?gDV?mbAx7u@%yazZ>vxoilAr)}LGj-)<>*wm4=VUi6Ia+2}5XhVzTZB%eK z;D-vh}uADz_5@VE*g$LXn`MjU)=0jVs-vFaA*Jak_utxb#=38 zx#F#U{JOe5JGfcF9Z2ZxOYn*Qf?PoYzw1Y14 zG7}eJzQC*Ja5QY?X(82Ys5FZn#o28F4bgttZhh)u^>#VWImJi0+x3|jv9i`e`xHHS z%#v&acVRPfxE^lJo)kNe==Xx$8dM{`pZs`s+BdoI`#~--PY_^OQ-6NcN0@AAFi0_j z@>}hRPHV>8nKP+M_(_83t9k#9YK?zq@J<$m*H$Pt%hyyg2B6O014-soWs%(D6Lp;E zI7TY{m6uEi)^gN?VHf%Zbee1tr=KPcAE4BzJ%*s;Q>dRCZ3|{G3C4A@{Mi~u*Jp@> zzP%B#uC1?ZuzCesFV~dq0LH(w;;?J!8^5McFpDj63~}2queHvmq3jeoA3Q;VKN{#e z$u6Ju3jX^hs4RlJt8@Pw1;dS5nX##Kn@49-)~sH;);v#lh>wcToosBIr>1O2tA0h4 zjynk6wH0J%i@P^d>sNJyPh`VwtzZ1<^X*9R;)++3?JZr*Aj)=S4~$t8@0t>?U)jsk z8!a@y4iwQ_t}Y!38e2ajM6@YIX==5V@OA3!|8x^6ofK|u1%Xh7n%3+C-{|8ah8vn? z!0IPi8Zlx#_$naNJ4b+QRq`P8#W(4ac*mw`T^+NcxkD5#Qp{=vatPxGsvAs&D0Uq2 z_kn1y3 z%(Bos(oew`?vzni=`d2R+e-IbH5J^l=9nU~XBvp>a2OFOv_BaW1Q?RWG%K$0z)mEG zu0@e2CuF!>a%I;_$4+-lm2gHah=>c}WiF8yvziF}sKB0vrVO*gg{lln6j9)mqGU`S zUo+l7CqqcI#m!E1Oo8$7HIp0!0KpXWRaCy-Q)tM{cCt^V$W5juyvWLxnRW-|lZjiP z7EKxK=(slo*QpU`Bv1`N9XJyoZH*@K705_$AAO_9!=X}DbX-{1gdN4h2h|$ab7Z%| zA3ql4Hm%gwx<%4VZMEQgkx#Ar;R9fn`>KhGOi|^%t zO%=-<-HT@i7OgjoFNVO@u=cE_y`e0sB>lMp&3vIi_2hyqeLlg+&hhU$E=JWF+fzD} znCYF{jxmq9_{Aa}2h{nHZIE5O8fcURE~=S|;Gso9lYL2Z&@dzD?2sSPBEA=b8kPHd z^fCLTv8fVAgNmm>qJOsmu}Q;??x7IfK@GP4?$>ZBNsY7~HJU_+3eL!qR-htHZ`+{K z@dRrjMPheKi5Xo3u#nN> zns)crdY=eS@ua8lCNr4*s8--iM`$=x>N4?sN(_50uB|%C7b`S1>NJ;b9o@3R>)O37 zB$s>o1_wG1jQ0T3s5Jhgwe_1o-Gt#8T|8(1j8h&0Fj*f5{{l!jwB%^fu4H9k1W=+s z=thvH)Z~X7yk+taqYGhZKXX4H`kN9DA+Lx|2xILQ5qKpRRw_QJhUL!pjB0W|2?|YS zH)oarrBR0pX|Rx+w!-V%0^bc^o8{Ha*5{>KlTLkb%S92Qc$*i!x{3sq)eJqM*=O|9 zm#|`IJ<*1i%%O6){+Zd^#p0=#!lCw1+Lm>ddpuQ(xlQ});-Sobkc@9;9hw|x50vf0 zMmPl8&)qUQFV~q*@w9u#@2+kjuU6l^Jp&cv?G}q0okYKM5G(|Jw<)!u+f^26qPseQ z+*^fR^+IjcvSA4r|M=;i7wdL&h9!(+Wn&kP1qlIY7D9PgRgibb3YGpI*KrW1Ih`+J zoiZxA+We&+7r}BO25PP*RcdP*C!bVct}shn8AIMpwzTgdU!a|$^sto{WPqJFcmE)>C%|Ljh2f)IMR08)xdeG zu{1SP&Fg7tJRmfm^B84dM{YGG$DA{h+F`I|wj_~uN-1N^M!Gy|&+X(|GRINo%bcQV z>h77Up6`2Iq&kVOhAO@W(tSKCV_lB8*ZCNEg$M4Wd4CJ{ZO>h;d~$|#1x*6rJekye&#u4p zRB6@LM|>b^F}SLk8;lsz@%uzEQm&YCAUN}c6lP>FEw7~wiBmnByvR6$^Nm)uB z$Tt70b}(AYCD^aBUzqB@qz*8@zO^=SGWZ*ZF54;H1_fNil^py^z5tMaYX|}g0Q=83 zhX1Pa%a8%DLT}sOuuDPa@2!4kZf^+x4d1_E{5KB%+g^Pse|`Mj>aVwr|5fFW-2M&s z|F&1(!e83IRsIe9|Etg&uz!Q~Z*INu=HK?}d;15We}Vpgm3brJZ}j_(g8$ho39Nq} z&tF;pZwO1^&*`|0?r_ zG~aOM8`k`@O#8o=`8z@XE%X15`%jyH4x2Z3cz&I;f4K7}8v5tK@&*9jlK(dd@Ww-b z+pDkDe^B_Hv|> z|Eu6zY`?|zTYSGIuD|Wow}$keg1DS}=*AHMZzU}=Fm4dH) literal 0 HcmV?d00001 diff --git a/packages/alphatab/test-data/corrupt/healthy.gp5 b/packages/alphatab/test-data/corrupt/healthy.gp5 new file mode 100644 index 0000000000000000000000000000000000000000..63f1bad4a903761bd7fd815c846650ae5cd79b7b GIT binary patch literal 1718 zcmeHH%}T>S5N^`{YEci07j-TxdZ<=G6!c(>S|iqmBt`TV+eHI0kxeS}U3?oao;>(M zb!O8V(IR>kKbYCsZ+><&VQ2o>&`*pb3mR?1(JU~n7CdaNZPYUo3h8mGhRe21XEb5xB7z2Cffjw#_>|!MoK;6>}#|_l5&I zXY0&lvaMIhCZKz%^ht=1cEbGY{|@}^4h((->OmHT9*>^O;gdlSImC0d(eq+gfa$w^ zE=Kg2wSe3{$}Y+_$|gz;Wd(&mCy-EedXIzw%9FH)iiVe>W^?&Mu~e=MvJ^N<9K|i6 zv8df~;dR^qbf2THf$71a7sa8Vi9uz`+TA@kVZfsdeilOAQSu5_Q)w#ZB{ftuE{T-) zk_KH|VsB*7s@AE(c#HUb>66Fd;K`{k_zS4H@1U}eNM@+4G`=gD9{8zv>iFJD)7}71 C@Rkh# literal 0 HcmV?d00001 diff --git a/packages/alphatab/test/importer/Gp5Importer.test.ts b/packages/alphatab/test/importer/Gp5Importer.test.ts index 12fe5e4d1..f8e3a9cb5 100644 --- a/packages/alphatab/test/importer/Gp5Importer.test.ts +++ b/packages/alphatab/test/importer/Gp5Importer.test.ts @@ -12,7 +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 { OverflowError } from '@coderline/alphatab/io/IReadable'; +import { EndOfReaderError, OverflowError } from '@coderline/alphatab/io/IReadable'; +import { TestPlatform } from 'test/TestPlatform'; describe('Gp5ImporterTest', () => { it('score-info', async () => { @@ -581,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; @@ -594,10 +595,48 @@ describe('Gp5ImporterTest', () => { expect(buffer.readByte()).toBe(sentinelByte); }); - it('corrupted-bend-point-count', async () => { - const reader = await GpImporterTestHelper.prepareImporterWithFile( - 'corrupt/corrupted-bend-point-count.gp5' - ); - expect(() => reader.readScore()).toThrow(OverflowError); + 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 d7b282bf1..5f236b65e 100644 --- a/packages/alphatab/test/zip/ZipReaderWriter.test.ts +++ b/packages/alphatab/test/zip/ZipReaderWriter.test.ts @@ -5,6 +5,7 @@ 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'); @@ -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' + )); + }); }); From 2f450e4755795c871aa451f71f48a5cfe19f2766 Mon Sep 17 00:00:00 2001 From: Danielku15 Date: Sun, 17 May 2026 15:08:52 +0200 Subject: [PATCH 5/7] refactor: pass max buffer to binarystylesheet --- packages/alphatab/src/generated/CoreSettingsJson.ts | 11 ----------- .../alphatab/src/generated/CoreSettingsSerializer.ts | 4 ---- .../alphatab/src/generated/ImporterSettingsJson.ts | 11 +++++++++++ .../src/generated/ImporterSettingsSerializer.ts | 4 ++++ packages/alphatab/src/importer/BinaryStylesheet.ts | 10 +++++----- packages/alphatab/src/importer/Gp7To8Importer.ts | 2 +- packages/alphatab/src/importer/GpxImporter.ts | 2 +- .../alphatab/test/importer/BinaryStylesheet.test.ts | 2 +- 8 files changed, 23 insertions(+), 23 deletions(-) diff --git a/packages/alphatab/src/generated/CoreSettingsJson.ts b/packages/alphatab/src/generated/CoreSettingsJson.ts index 89f21e728..49b551225 100644 --- a/packages/alphatab/src/generated/CoreSettingsJson.ts +++ b/packages/alphatab/src/generated/CoreSettingsJson.ts @@ -174,15 +174,4 @@ export interface CoreSettingsJson { * ``` */ includeNoteBounds?: 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/CoreSettingsSerializer.ts b/packages/alphatab/src/generated/CoreSettingsSerializer.ts index 1d23ebf4b..5b1dd0127 100644 --- a/packages/alphatab/src/generated/CoreSettingsSerializer.ts +++ b/packages/alphatab/src/generated/CoreSettingsSerializer.ts @@ -45,7 +45,6 @@ export class CoreSettingsSerializer { o.set("loglevel", obj.logLevel as number); o.set("useworkers", obj.useWorkers); o.set("includenotebounds", obj.includeNoteBounds); - o.set("maxdecodingbuffersize", obj.maxDecodingBufferSize); return o; } public static setProperty(obj: CoreSettings, property: string, v: unknown): boolean { @@ -92,9 +91,6 @@ export class CoreSettingsSerializer { case "includenotebounds": obj.includeNoteBounds = v! as boolean; return true; - case "maxdecodingbuffersize": - obj.maxDecodingBufferSize = v! as number; - return true; } return false; } 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/Gp7To8Importer.ts b/packages/alphatab/src/importer/Gp7To8Importer.ts index cfac60202..8b9dbd96c 100644 --- a/packages/alphatab/src/importer/Gp7To8Importer.ts +++ b/packages/alphatab/src/importer/Gp7To8Importer.ts @@ -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/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); From dc9de53ad9628a6c0cb5482f98f92f40f114e8b9 Mon Sep 17 00:00:00 2001 From: Danielku15 Date: Sun, 17 May 2026 15:29:34 +0200 Subject: [PATCH 6/7] build: fix typed array generics --- .../src/csharp/CSharpEmitterContext.ts | 39 +++++++++++-------- 1 file changed, 23 insertions(+), 16 deletions(-) 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)!); From 3d7d3bafd2cd3e879d2cf35e65f4439653ee5eab Mon Sep 17 00:00:00 2001 From: Danielku15 Date: Sun, 17 May 2026 17:19:55 +0200 Subject: [PATCH 7/7] test: string contains assertions --- packages/csharp/src/AlphaTab.Test/Test/Globals.cs | 4 ++++ .../src/android/src/test/java/alphaTab/core/TestGlobals.kt | 5 +++++ 2 files changed, 9 insertions(+) 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"); }