Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 31 additions & 80 deletions patches/midi-file+1.2.4.patch
Original file line number Diff line number Diff line change
@@ -1,84 +1,26 @@
diff --git a/node_modules/midi-file/lib/midi-parser.js b/node_modules/midi-file/lib/midi-parser.js
index 50ed069..93868ee 100644
index 50ed069..f6828a5 100644
--- a/node_modules/midi-file/lib/midi-parser.js
+++ b/node_modules/midi-file/lib/midi-parser.js
@@ -308,9 +308,47 @@ Parser.prototype.readBytes = function(len) {
return bytes
@@ -309,8 +309,15 @@ Parser.prototype.readBytes = function(len) {
}

+var sharedUtf8Decoder = new TextDecoder('utf-8')
+

Parser.prototype.readString = function(len) {
- var bytes = this.readBytes(len)
- return String.fromCharCode.apply(null, bytes)
+ // Strings can be multibyte-encoded or not.
+ // Fast path: all ASCII (bytes < 0x80) -> fromCharCode directly, no TextDecoder.
+ // Slow path: try UTF-8, fall back to Latin-1 if UTF-8 produces replacement chars.
+ var start = this.pos
+ var end = start + len
+ var buffer = this.buffer
+ var allAscii = true
+ for (var i = start; i < end; i++) {
+ if (buffer[i] >= 0x80) { allAscii = false; break }
+ }
+ this.pos = end
+ if (allAscii) {
+ // fromCharCode.apply is the fastest path for small ASCII strings.
+ // For very long strings, the stack can blow up — chunk it.
+ var CHUNK = 0x8000
+ if (len <= CHUNK) {
+ // Use subarray (no copy) when available (Uint8Array); fall back to slice for plain arrays.
+ var view = buffer.subarray ? buffer.subarray(start, end) : buffer.slice(start, end)
+ return String.fromCharCode.apply(null, view)
+ }
+ var out = ''
+ for (var j = start; j < end; j += CHUNK) {
+ var chunkEnd = j + CHUNK < end ? j + CHUNK : end
+ var chunk = buffer.subarray ? buffer.subarray(j, chunkEnd) : buffer.slice(j, chunkEnd)
+ out += String.fromCharCode.apply(null, chunk)
+ }
+ return out
+ }
+ // Non-ASCII: try UTF-8 decode; fall back to Latin-1 (fromCharCode per byte) if
+ // UTF-8 produced replacement chars or didn't shorten the string.
+ var bytes = buffer.subarray ? buffer.subarray(start, end) : buffer.slice(start, end)
+ var multibyteString = sharedUtf8Decoder.decode(bytes)
+ // Latin-1 interpretation: each byte → one codepoint. Length equals `len`.
+ if (multibyteString.length < len && multibyteString.indexOf('\uFFFD') === -1) {
+ // Try UTF-8 first; fall back to Latin-1 if UTF-8 produces replacement chars.
var bytes = this.readBytes(len)
- return String.fromCharCode.apply(null, bytes)
+ var multibyteString = new TextDecoder().decode(bytes)
+ var singlebyteString = String.fromCharCode.apply(null, bytes)
+ if (singlebyteString.length > multibyteString.length && !multibyteString.includes('\uFFFD')) {
+ return multibyteString
+ }
+ // Build Latin-1 string via fromCharCode on the byte values.
+ return String.fromCharCode.apply(null, bytes)
+ return singlebyteString
}

Parser.prototype.readVarInt = function() {
@@ -321,14 +359,19 @@ Parser.prototype.readBytes = function(len) {


Parser.prototype.readVarInt = function() {
var result = 0
- while (!this.eof()) {
- var b = this.readUInt8()
+ var buffer = this.buffer
+ var pos = this.pos
+ var bufferLen = this.bufferLen
+ while (pos < bufferLen) {
+ var b = buffer[pos++]
if (b & 0x80) {
result += (b & 0x7f)
result <<= 7
} else {
// b is last byte
+ this.pos = pos
return result + b
}
}
// premature eof
+ this.pos = pos
return result
}

diff --git a/node_modules/midi-file/lib/midi-writer.js b/node_modules/midi-file/lib/midi-writer.js
index c1a438d..cbd1a73 100644
index c1a438d..3e30cee 100644
--- a/node_modules/midi-file/lib/midi-writer.js
+++ b/node_modules/midi-file/lib/midi-writer.js
@@ -80,50 +80,43 @@ function writeEvent(w, event, lastEventTypeByte, useByte9ForNoteOff) {
Expand All @@ -89,59 +31,68 @@ index c1a438d..cbd1a73 100644
- w.writeString(text)
+ w.writeStringWithLength(text)
break;

case 'copyrightNotice':
w.writeUInt8(0xFF)
w.writeUInt8(0x02)
- w.writeVarInt(text.length)
- w.writeString(text)
+ w.writeStringWithLength(text)
break;

case 'trackName':
w.writeUInt8(0xFF)
w.writeUInt8(0x03)
- w.writeVarInt(text.length)
- w.writeString(text)
+ w.writeStringWithLength(text)
break;

case 'instrumentName':
w.writeUInt8(0xFF)
w.writeUInt8(0x04)
- w.writeVarInt(text.length)
- w.writeString(text)
+ w.writeStringWithLength(text)
break;

case 'lyrics':
w.writeUInt8(0xFF)
w.writeUInt8(0x05)
- w.writeVarInt(text.length)
- w.writeString(text)
+ w.writeStringWithLength(text)
break;

case 'marker':
w.writeUInt8(0xFF)
w.writeUInt8(0x06)
- w.writeVarInt(text.length)
- w.writeString(text)
+ w.writeStringWithLength(text)
break;

case 'cuePoint':
w.writeUInt8(0xFF)
w.writeUInt8(0x07)
- w.writeVarInt(text.length)
- w.writeString(text)
+ w.writeStringWithLength(text)
break;

case 'channelPrefix':
@@ -325,11 +318,14 @@ Writer.prototype.writeBytes = function(arr) {
@@ -321,15 +314,22 @@ Writer.prototype.writeInt32 = Writer.prototype.writeUInt32


Writer.prototype.writeBytes = function(arr) {
- this.buffer = this.buffer.concat(Array.prototype.slice.call(arr, 0))
+ var buf = this.buffer
+ var len = arr.length
+ for (var i = 0; i < len; i++) {
+ buf.push(arr[i])
+ }
}

Writer.prototype.writeString = function(str) {
- var i, len = str.length, arr = []
- for (i=0; i < len; i++) {
Expand All @@ -157,5 +108,5 @@ index c1a438d..cbd1a73 100644
+ this.writeVarInt(bytes.length)
+ this.writeBytes(bytes)
}

Writer.prototype.writeVarInt = function(v) {
132 changes: 132 additions & 0 deletions src/__tests__/chart-document.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/**
* Tests for `ChartDocument` + `writeChartFolder` — the orchestrator that
* glues writeChartFile/writeMidiFile + writeIniFile + passthrough assets
* into a flat file list suitable for zip/sng packaging.
*/

import { describe, expect, it } from 'vitest'

import { createEmptyChart } from '../chart/create-chart'
import { writeChartFolder } from '../chart/chart-document'
import { parseChartAndIni } from '../chart/parse-chart-and-ini'

function textOf(data: Uint8Array): string {
return new TextDecoder().decode(data)
}

function findFile(files: { fileName: string; data: Uint8Array }[], name: string) {
return files.find(f => f.fileName === name)
}

describe('writeChartFolder: format selection', () => {
it('emits notes.chart + song.ini for a .chart document', () => {
const chart = createEmptyChart({ format: 'chart' })
chart.metadata.name = 'Test Song'
const out = writeChartFolder({ parsedChart: chart, assets: [] })
expect(findFile(out, 'notes.chart')).toBeDefined()
expect(findFile(out, 'notes.mid')).toBeUndefined()
expect(findFile(out, 'song.ini')).toBeDefined()
})

it('emits notes.mid + song.ini for a .mid document', () => {
const chart = createEmptyChart({ format: 'mid' })
chart.metadata.name = 'Test Song'
const out = writeChartFolder({ parsedChart: chart, assets: [] })
expect(findFile(out, 'notes.mid')).toBeDefined()
expect(findFile(out, 'notes.chart')).toBeUndefined()
expect(findFile(out, 'song.ini')).toBeDefined()
})
})

describe('writeChartFolder: ini content', () => {
it('writes parsedChart.metadata to song.ini', () => {
const chart = createEmptyChart({ format: 'chart' })
chart.metadata.name = 'My Song'
chart.metadata.artist = 'My Artist'
const out = writeChartFolder({ parsedChart: chart, assets: [] })
const iniText = textOf(findFile(out, 'song.ini')!.data)
expect(iniText).toContain('name = My Song')
expect(iniText).toContain('artist = My Artist')
})

it('does NOT leak chart_offset into song.ini ([Song]-only field)', () => {
const chart = createEmptyChart({ format: 'chart' })
chart.metadata.chart_offset = 250
const out = writeChartFolder({ parsedChart: chart, assets: [] })
const iniText = textOf(findFile(out, 'song.ini')!.data)
expect(iniText).not.toContain('chart_offset')
})

it('preserves extraIniFields for unknown ini keys', () => {
const chart = createEmptyChart({ format: 'chart' })
chart.metadata.extraIniFields = { custom_field: 'custom_value' }
const out = writeChartFolder({ parsedChart: chart, assets: [] })
const iniText = textOf(findFile(out, 'song.ini')!.data)
expect(iniText).toContain('custom_field = custom_value')
})
})

describe('writeChartFolder: assets passthrough', () => {
it('passes audio/image assets through verbatim', () => {
const chart = createEmptyChart({ format: 'chart' })
const ogg = new Uint8Array([0x4f, 0x67, 0x67, 0x53]) // "OggS"
const png = new Uint8Array([0x89, 0x50, 0x4e, 0x47])
const out = writeChartFolder({
parsedChart: chart,
assets: [
{ fileName: 'song.ogg', data: ogg },
{ fileName: 'album.png', data: png },
],
})
expect(findFile(out, 'song.ogg')!.data).toBe(ogg)
expect(findFile(out, 'album.png')!.data).toBe(png)
})

it('emits chart file BEFORE ini BEFORE assets', () => {
const chart = createEmptyChart({ format: 'chart' })
const out = writeChartFolder({
parsedChart: chart,
assets: [{ fileName: 'song.ogg', data: new Uint8Array(4) }],
})
const names = out.map(f => f.fileName)
expect(names[0]).toBe('notes.chart')
expect(names[1]).toBe('song.ini')
expect(names[2]).toBe('song.ogg')
})
})

describe('writeChartFolder: round-trip via parseChartAndIni', () => {
it('.chart folder round-trips through parseChartAndIni with metadata intact', () => {
const chart = createEmptyChart({ format: 'chart', resolution: 480 })
chart.metadata.name = 'Round Trip'
chart.metadata.artist = 'Tester'
chart.metadata.pro_drums = true

const out = writeChartFolder({ parsedChart: chart, assets: [] })
const re = parseChartAndIni(out)

expect(re.parsedChart).not.toBeNull()
expect(re.parsedChart!.metadata.name).toBe('Round Trip')
expect(re.parsedChart!.metadata.artist).toBe('Tester')
expect(re.parsedChart!.metadata.pro_drums).toBe(true)
expect(re.hasIni).toBe(true)
})

it('.mid folder round-trips with metadata AND assets preserved', () => {
const chart = createEmptyChart({ format: 'mid', resolution: 480 })
chart.metadata.name = 'Midi Song'
const ogg = new Uint8Array([0x4f, 0x67, 0x67, 0x53, 0x00])

const out = writeChartFolder({
parsedChart: chart,
assets: [{ fileName: 'song.ogg', data: ogg }],
})
const re = parseChartAndIni(out)

expect(re.parsedChart!.format).toBe('mid')
expect(re.parsedChart!.metadata.name).toBe('Midi Song')
// Asset survives as a file in the output set (parseChartAndIni doesn't
// surface assets on its result — but we can still find it in `out`).
expect(findFile(out, 'song.ogg')!.data).toBe(ogg)
})
})
Loading