diff --git a/packages/alphatab/src/importer/Gp3To5Importer.ts b/packages/alphatab/src/importer/Gp3To5Importer.ts index cc74873be..9d564d3f2 100644 --- a/packages/alphatab/src/importer/Gp3To5Importer.ts +++ b/packages/alphatab/src/importer/Gp3To5Importer.ts @@ -57,7 +57,7 @@ export class Gp3To5Importer extends ScoreImporter { // NOTE: General Midi only defines percussion instruments from 35-81 // Guitar Pro 5 allowed GS extensions (27-34 and 82-87) - // GP7-8 do not have all these definitions anymore, this lookup ensures some fallback + // GP7-8 do not have all these definitions anymore, this lookup ensures some fallback // (even if they are not correct) // we can support this properly in future when we allow custom alphaTex articulation definitions // then we don't need to rely on GP specifics anymore but handle things on export/import @@ -1571,12 +1571,13 @@ export class GpBinaryHelpers { * @returns */ public static gpReadStringByteLength(data: IReadable, length: number, encoding: string): string { - const stringLength: number = data.readByte(); - const s: string = GpBinaryHelpers.gpReadString(data, stringLength, encoding); - if (stringLength < length) { - data.skip(length - stringLength); - } - return s; + // Fixed-width string field: 1 length byte + `length` data bytes, decoded + // up to min(stringLength, length). Always consumes 1 + length bytes. + const stringLength = data.readByte(); + const fieldBytes = new Uint8Array(length); + data.read(fieldBytes, 0, length); + const effectiveLength = Math.min(stringLength, length); + return IOHelper.toString(fieldBytes.subarray(0, effectiveLength), encoding); } } diff --git a/packages/alphatab/src/io/IOHelper.ts b/packages/alphatab/src/io/IOHelper.ts index db8151068..2d3814ed8 100644 --- a/packages/alphatab/src/io/IOHelper.ts +++ b/packages/alphatab/src/io/IOHelper.ts @@ -155,7 +155,7 @@ export class IOHelper { encoding = 'utf-8'; } const decoder: TextDecoder = new TextDecoder(encoding); - return decoder.decode(data.buffer as ArrayBuffer); + return decoder.decode(data); } private static _detectEncoding(data: Uint8Array): string | null { diff --git a/packages/alphatab/test-data/guitarpro5/chord-name-overflow.gp5 b/packages/alphatab/test-data/guitarpro5/chord-name-overflow.gp5 new file mode 100755 index 000000000..8de558027 Binary files /dev/null and b/packages/alphatab/test-data/guitarpro5/chord-name-overflow.gp5 differ diff --git a/packages/alphatab/test/importer/Gp5Importer.test.ts b/packages/alphatab/test/importer/Gp5Importer.test.ts index d114b0d88..d954477d7 100644 --- a/packages/alphatab/test/importer/Gp5Importer.test.ts +++ b/packages/alphatab/test/importer/Gp5Importer.test.ts @@ -1,5 +1,7 @@ import { describe, expect, it } from 'vitest'; import { Settings } from '@coderline/alphatab/Settings'; +import { GpBinaryHelpers } from '@coderline/alphatab/importer/Gp3To5Importer'; +import { ByteBuffer } from '@coderline/alphatab/io/ByteBuffer'; import { type Beat, BeatBeamingMode } from '@coderline/alphatab/model/Beat'; import { Direction } from '@coderline/alphatab/model/Direction'; import { Ottavia } from '@coderline/alphatab/model/Ottavia'; @@ -407,9 +409,7 @@ describe('Gp5ImporterTest', () => { expect(score.tracks[0].staves[0].bars[1].voices[0].beats[3].preferredBeamDirection).not.toBeTruthy(); // break - expect(score.tracks[0].staves[0].bars[2].voices[0].beats[0].beamingMode).toBe( - BeatBeamingMode.ForceSplitToNext - ); + expect(score.tracks[0].staves[0].bars[2].voices[0].beats[0].beamingMode).toBe(BeatBeamingMode.ForceSplitToNext); expect(score.tracks[0].staves[0].bars[2].voices[0].beats[0].invertBeamDirection).toBe(false); expect(score.tracks[0].staves[0].bars[2].voices[0].beats[0].preferredBeamDirection).not.toBeTruthy(); @@ -419,9 +419,7 @@ describe('Gp5ImporterTest', () => { expect(score.tracks[0].staves[0].bars[2].voices[0].beats[1].invertBeamDirection).toBe(false); expect(score.tracks[0].staves[0].bars[2].voices[0].beats[1].preferredBeamDirection).not.toBeTruthy(); - expect(score.tracks[0].staves[0].bars[2].voices[0].beats[2].beamingMode).toBe( - BeatBeamingMode.ForceSplitToNext - ); + expect(score.tracks[0].staves[0].bars[2].voices[0].beats[2].beamingMode).toBe(BeatBeamingMode.ForceSplitToNext); expect(score.tracks[0].staves[0].bars[2].voices[0].beats[2].invertBeamDirection).toBe(false); expect(score.tracks[0].staves[0].bars[2].voices[0].beats[2].preferredBeamDirection).not.toBeTruthy(); @@ -447,9 +445,7 @@ describe('Gp5ImporterTest', () => { // invert to down expect(score.tracks[0].staves[0].bars[4].voices[0].beats[0].beamingMode).toBe(BeatBeamingMode.Auto); expect(score.tracks[0].staves[0].bars[4].voices[0].beats[0].invertBeamDirection).toBe(false); - expect(score.tracks[0].staves[0].bars[4].voices[0].beats[0].preferredBeamDirection).toBe( - BeamDirection.Down - ); + expect(score.tracks[0].staves[0].bars[4].voices[0].beats[0].preferredBeamDirection).toBe(BeamDirection.Down); // invert to up expect(score.tracks[0].staves[0].bars[5].voices[0].beats[0].beamingMode).toBe(BeatBeamingMode.Auto); @@ -523,18 +519,14 @@ describe('Gp5ImporterTest', () => { expect(score.style!.headerAndFooter.has(ScoreSubElement.Transcriber)).toBe(false); expect(score.style!.headerAndFooter.has(ScoreSubElement.Copyright)).toBe(true); - expect(score.style!.headerAndFooter.get(ScoreSubElement.Copyright)!.template).toBe( - 'Copyright: %COPYRIGHT%' - ); + expect(score.style!.headerAndFooter.get(ScoreSubElement.Copyright)!.template).toBe('Copyright: %COPYRIGHT%'); expect(score.style!.headerAndFooter.get(ScoreSubElement.Copyright)!.isVisible).toBe(true); expect(score.style!.headerAndFooter.get(ScoreSubElement.Copyright)!.textAlign).toBe(TextAlign.Center); expect(score.style!.headerAndFooter.has(ScoreSubElement.CopyrightSecondLine)).toBe(true); expect(score.style!.headerAndFooter.get(ScoreSubElement.CopyrightSecondLine)!.template).toBe('Copyright2'); expect(score.style!.headerAndFooter.get(ScoreSubElement.CopyrightSecondLine)!.isVisible).toBe(true); - expect(score.style!.headerAndFooter.get(ScoreSubElement.CopyrightSecondLine)!.textAlign).toBe( - TextAlign.Center - ); + expect(score.style!.headerAndFooter.get(ScoreSubElement.CopyrightSecondLine)!.textAlign).toBe(TextAlign.Center); }); it('bank', async () => { @@ -569,4 +561,35 @@ describe('Gp5ImporterTest', () => { } } }); + + it('chord-name-overflow', async () => { + // GP5 file with a chord name length byte that exceeds the 21-byte field + // (length=32). Pre-fix, gpReadStringByteLength consumed the full 32 bytes, + // mis-aligning the stream and triggering an unbounded readBend loop. + const score = ( + await GpImporterTestHelper.prepareImporterWithFile('guitarpro5/chord-name-overflow.gp5') + ).readScore(); + expect(score.tracks.length).toBe(1); + expect(score.masterBars.length).toBe(193); + }); + + it('gpReadStringByteLength caps consumption at field width', () => { + const sentinelByte = 0xca; + const fieldSize = 21; + const overlongHint = 32; + + const raw = new Uint8Array(fieldSize + 2); + raw[0] = overlongHint; + for(let i = 0; i < fieldSize; i++) { + raw[i + 1] = 0x41; + } + raw[fieldSize + 1] = sentinelByte; + + const buffer = ByteBuffer.fromBuffer(raw); + + const result = GpBinaryHelpers.gpReadStringByteLength(buffer, fieldSize, 'utf-8'); + expect(result).toBe('A'.repeat(fieldSize)); + expect(buffer.position).toBe(1 + fieldSize); + expect(buffer.readByte()).toBe(sentinelByte); + }); }); diff --git a/packages/csharp/src/AlphaTab/Core/EcmaScript/TextDecoder.cs b/packages/csharp/src/AlphaTab/Core/EcmaScript/TextDecoder.cs index 6f1ca5a56..c84b8fd37 100644 --- a/packages/csharp/src/AlphaTab/Core/EcmaScript/TextDecoder.cs +++ b/packages/csharp/src/AlphaTab/Core/EcmaScript/TextDecoder.cs @@ -15,4 +15,9 @@ public string Decode(ArrayBuffer data) { return _encoding.GetString(data.Raw, 0, (int)data.ByteLength); } + + public string Decode(Uint8Array data) + { + return _encoding.GetString(data.Buffer.Raw, (int)data.ByteOffset, (int)data.Length); + } } diff --git a/packages/kotlin/src/android/src/main/java/alphaTab/core/Globals.kt b/packages/kotlin/src/android/src/main/java/alphaTab/core/Globals.kt index 798dee2ec..679418d08 100644 --- a/packages/kotlin/src/android/src/main/java/alphaTab/core/Globals.kt +++ b/packages/kotlin/src/android/src/main/java/alphaTab/core/Globals.kt @@ -40,12 +40,6 @@ internal inline fun UByteArray.decodeToDoubleArray(): DoubleArray { return da } -@ExperimentalUnsignedTypes -internal inline fun UByteArray.decodeToString(encoding: String): String { - return String(this.toByteArray(), 0, this.size, Charset.forName(encoding)) -} - - internal inline fun > List.sort() { this.sort { a, b -> a.compareTo(b).toDouble() diff --git a/packages/kotlin/src/android/src/main/java/alphaTab/core/ecmaScript/TextDecoder.kt b/packages/kotlin/src/android/src/main/java/alphaTab/core/ecmaScript/TextDecoder.kt index 61b58483d..59a95ecb7 100644 --- a/packages/kotlin/src/android/src/main/java/alphaTab/core/ecmaScript/TextDecoder.kt +++ b/packages/kotlin/src/android/src/main/java/alphaTab/core/ecmaScript/TextDecoder.kt @@ -1,13 +1,18 @@ package alphaTab.core.ecmaScript -import alphaTab.core.decodeToString import alphaTab.core.ecmaScript.ArrayBuffer +import java.nio.charset.Charset internal class TextDecoder(encoding:String) { private val _encoding:String = encoding @ExperimentalUnsignedTypes public fun decode(buffer: ArrayBuffer): String { - return buffer.decodeToString(_encoding) + return String(buffer.toByteArray(), 0, buffer.size, Charset.forName(_encoding)) + } + + @ExperimentalUnsignedTypes + public fun decode(buffer: Uint8Array): String { + return String(buffer.buffer.toByteArray(), buffer.byteOffset.toInt(), buffer.length.toInt(), Charset.forName(_encoding)) } }