rhythia-api 242.0.0 → 243.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,5 @@
1
+ import { Buffer } from "buffer";
2
+
1
3
  type DataTypeID =
2
4
  | 0x00
3
5
  | 0x01
@@ -13,6 +15,22 @@ type DataTypeID =
13
15
  | 0x0b
14
16
  | 0x0c;
15
17
 
18
+ const enum DataType {
19
+ End = 0x00,
20
+ Byte = 0x01,
21
+ UInt16 = 0x02,
22
+ UInt32 = 0x03,
23
+ UInt64 = 0x04,
24
+ Float = 0x05,
25
+ Double = 0x06,
26
+ Position = 0x07,
27
+ Buffer = 0x08,
28
+ String = 0x09,
29
+ LongBuffer = 0x0a,
30
+ LongString = 0x0b,
31
+ Array = 0x0c,
32
+ }
33
+
16
34
  interface Header {
17
35
  signature: Buffer;
18
36
  version: number;
@@ -70,33 +88,66 @@ interface MarkerDefinition {
70
88
  interface Marker {
71
89
  position: number;
72
90
  type: number;
73
- data: Buffer;
91
+ data: any;
74
92
  }
75
93
 
76
94
  export class SSPMParser {
95
+ private static readonly SIGNATURE = 0x6d2b5353;
77
96
  private buffer: Buffer;
78
- private offset: number = 0;
97
+ private offset = 0;
79
98
 
80
99
  constructor(buffer: Buffer) {
81
100
  this.buffer = buffer;
82
101
  }
83
102
 
84
103
  private checkBounds(length: number): void {
85
- if (this.offset + length > this.buffer.length) {
104
+ if (length < 0 || this.offset + length > this.buffer.length) {
86
105
  throw new RangeError(
87
- `Attempt to read beyond buffer length: Offset=${this.offset}, Length=${length}, Buffer Length=${this.buffer.length}`
106
+ `Attempt to read beyond buffer length: offset=${this.offset}, length=${length}, bufferLength=${this.buffer.length}`
88
107
  );
89
108
  }
90
109
  }
91
110
 
92
- private log(message: string): void {
93
- // console.log(`[Offset: ${this.offset}] ${message}`);
111
+ private seek(position: number): void {
112
+ if (
113
+ !Number.isInteger(position) ||
114
+ position < 0 ||
115
+ position > this.buffer.length
116
+ ) {
117
+ throw new RangeError(`Invalid seek position: ${position}`);
118
+ }
119
+ this.offset = position;
120
+ }
121
+
122
+ private skip(length: number): void {
123
+ this.checkBounds(length);
124
+ this.offset += length;
125
+ }
126
+
127
+ private log(_message: string): void {}
128
+
129
+ private readUInt8(): number {
130
+ this.checkBounds(1);
131
+ return this.buffer.readUInt8(this.offset++);
132
+ }
133
+
134
+ private readInt32(): number {
135
+ this.checkBounds(4);
136
+ const value = this.buffer.readInt32LE(this.offset);
137
+ this.offset += 4;
138
+ return value;
94
139
  }
95
140
 
96
141
  private readUInt16(): number {
97
142
  this.checkBounds(2);
98
143
  const value = this.buffer.readUInt16LE(this.offset);
99
- this.log(`Read UInt16: ${value}`);
144
+ this.offset += 2;
145
+ return value;
146
+ }
147
+
148
+ private readInt16(): number {
149
+ this.checkBounds(2);
150
+ const value = this.buffer.readInt16LE(this.offset);
100
151
  this.offset += 2;
101
152
  return value;
102
153
  }
@@ -104,39 +155,55 @@ export class SSPMParser {
104
155
  private readUInt32(): number {
105
156
  this.checkBounds(4);
106
157
  const value = this.buffer.readUInt32LE(this.offset);
107
- this.log(`Read UInt32: ${value}`);
108
158
  this.offset += 4;
109
159
  return value;
110
160
  }
111
161
 
162
+ private readUInt64BigInt(): bigint {
163
+ this.checkBounds(8);
164
+ const value = this.buffer.readBigUInt64LE(this.offset);
165
+ this.offset += 8;
166
+ return value;
167
+ }
168
+
112
169
  private readUInt64(): number {
170
+ const value = this.readUInt64BigInt();
171
+ const asNumber = Number(value);
172
+
173
+ if (!Number.isSafeInteger(asNumber)) {
174
+ throw new RangeError(`OutOfRange: ${value.toString()}`);
175
+ }
176
+
177
+ return asNumber;
178
+ }
179
+
180
+ private readFloat32(): number {
181
+ this.checkBounds(4);
182
+ const value = this.buffer.readFloatLE(this.offset);
183
+ this.offset += 4;
184
+ return value;
185
+ }
186
+
187
+ private readFloat64(): number {
113
188
  this.checkBounds(8);
114
- const low = this.buffer.readUInt32LE(this.offset);
115
- const high = this.buffer.readUInt32LE(this.offset + 4);
189
+ const value = this.buffer.readDoubleLE(this.offset);
116
190
  this.offset += 8;
117
- const value = low + high * 2 ** 32;
118
- this.log(`Read UInt64: ${value} (Low: ${low}, High: ${high})`);
119
191
  return value;
120
192
  }
121
193
 
122
194
  private readBytes(length: number): Buffer {
123
195
  this.checkBounds(length);
124
- const value = this.buffer.slice(this.offset, this.offset + length);
125
- this.log(`Read ${length} bytes`);
196
+ const value = this.buffer.subarray(this.offset, this.offset + length);
126
197
  this.offset += length;
127
198
  return value;
128
199
  }
129
200
 
130
- private readString(): string {
131
- const length = this.readUInt16();
132
- this.checkBounds(length);
133
- const value = this.readBytes(length).toString("utf-8");
134
- this.log(`Read String of length ${length}: ${value}`);
135
- return value;
201
+ private readString(longString = false): string {
202
+ const length = longString ? this.readUInt32() : this.readUInt16();
203
+ return this.readBytes(length).toString("utf8");
136
204
  }
137
205
 
138
206
  private readStringList(count: number): string[] {
139
- console.log("[DANGER] Reading String List");
140
207
  const list: string[] = [];
141
208
  for (let i = 0; i < count; i++) {
142
209
  list.push(this.readString());
@@ -144,77 +211,129 @@ export class SSPMParser {
144
211
  return list;
145
212
  }
146
213
 
147
- private readMarkerField(typeID: DataTypeID): any {
214
+ private readPosition(): { x: number; y: number; type: "int" | "quantum" } {
215
+ const type = this.readUInt8();
216
+
217
+ if (type === 0x00) {
218
+ return {
219
+ x: this.readUInt8(),
220
+ y: this.readUInt8(),
221
+ type: "int",
222
+ };
223
+ }
224
+
225
+ if (type === 0x01) {
226
+ return {
227
+ x: this.readFloat32(),
228
+ y: this.readFloat32(),
229
+ type: "quantum",
230
+ };
231
+ }
232
+
233
+ throw new Error(`Invalid position type: ${type}`);
234
+ }
235
+
236
+ private readTypedValue(typeID: DataTypeID): any {
148
237
  switch (typeID) {
149
- case 0x01: // 1 byte integer
150
- this.checkBounds(1);
151
- const int8 = this.buffer.readInt8(this.offset++);
152
- this.log(`Read Int8: ${int8}`);
153
- return int8;
154
- case 0x02: // 2 byte uint
238
+ case DataType.End:
239
+ return null;
240
+ case DataType.Byte:
241
+ return this.readUInt8();
242
+ case DataType.UInt16:
155
243
  return this.readUInt16();
156
- case 0x03: // 4 byte uint
244
+ case DataType.UInt32:
157
245
  return this.readUInt32();
158
- case 0x04: // 8 byte uint
246
+ case DataType.UInt64:
159
247
  return this.readUInt64();
160
- case 0x05: // 4 byte float
161
- this.checkBounds(4);
162
- const floatVal32 = this.buffer.readFloatLE(this.offset);
163
- this.log(`Read Float32: ${floatVal32}`);
164
- this.offset += 4;
165
- return floatVal32;
166
- case 0x06: // 8 byte float
167
- this.checkBounds(8);
168
- const floatVal64 = this.buffer.readDoubleLE(this.offset);
169
- this.log(`Read Float64: ${floatVal64}`);
170
- this.offset += 8;
171
- return floatVal64;
172
- case 0x07: // position type
173
- const isQuantum = this.buffer.readUInt8(this.offset++);
174
- let posData;
175
- if (isQuantum === 0x00) {
176
- this.checkBounds(2);
177
- const posX = this.buffer.readUInt8(this.offset++);
178
- const posY = this.buffer.readUInt8(this.offset++);
179
- this.log(`Read Position Int: x=${posX}, y=${posY}`);
180
- posData = { x: posX, y: posY, type: "int" };
181
- } else {
182
- this.checkBounds(8);
183
- const posX = this.buffer.readFloatLE(this.offset);
184
- this.offset += 4;
185
- const posY = this.buffer.readFloatLE(this.offset);
186
- this.offset += 4;
187
- this.log(`Read Position Quantum: x=${posX}, y=${posY}`);
188
- posData = { x: posX, y: posY, type: "quantum" };
248
+ case DataType.Float:
249
+ return this.readFloat32();
250
+ case DataType.Double:
251
+ return this.readFloat64();
252
+ case DataType.Position:
253
+ return this.readPosition();
254
+ case DataType.Buffer: {
255
+ const length = this.readUInt16();
256
+ return this.readBytes(length);
257
+ }
258
+ case DataType.String:
259
+ return this.readString(false);
260
+ case DataType.LongBuffer: {
261
+ const length = this.readUInt32();
262
+ return this.readBytes(length);
263
+ }
264
+ case DataType.LongString:
265
+ return this.readString(true);
266
+ case DataType.Array:
267
+ throw new Error("Array type must be handled separately.");
268
+ default:
269
+ throw new Error(`Unknown data type: ${typeID}`);
270
+ }
271
+ }
272
+
273
+ private skipTypedValue(typeID: DataTypeID): void {
274
+ switch (typeID) {
275
+ case DataType.End:
276
+ return;
277
+ case DataType.Byte:
278
+ this.skip(1);
279
+ return;
280
+ case DataType.UInt16:
281
+ this.skip(2);
282
+ return;
283
+ case DataType.UInt32:
284
+ this.skip(4);
285
+ return;
286
+ case DataType.UInt64:
287
+ this.skip(8);
288
+ return;
289
+ case DataType.Float:
290
+ this.skip(4);
291
+ return;
292
+ case DataType.Double:
293
+ this.skip(8);
294
+ return;
295
+ case DataType.Position: {
296
+ const posType = this.readUInt8();
297
+ if (posType === 0x00) {
298
+ this.skip(2);
299
+ return;
189
300
  }
190
- return posData;
191
- case 0x08: // buffer
192
- case 0x09: // string
193
- const length16 = this.readUInt16();
194
- this.checkBounds(length16);
195
- const value = this.readBytes(length16).toString("utf-8");
196
- this.log(`Read Buffer/String of length ${length16}`);
197
- return value;
198
- case 0x0a: // long buffer
199
- case 0x0b: // long string
200
- const length32 = this.readUInt32();
201
- this.log(`Reading Buffer/String of length ${length32}`);
202
- this.checkBounds(length32);
203
- const longValue = this.readBytes(length32).toString("utf-8");
204
- this.log(`Read Long Buffer/String of length ${length32}`);
205
- return longValue;
206
- case 0x00: // end type, should not appear here
301
+ if (posType === 0x01) {
302
+ this.skip(8);
303
+ return;
304
+ }
305
+ throw new Error(`Invalid position type while skipping: ${posType}`);
306
+ }
307
+ case DataType.Buffer:
308
+ case DataType.String:
309
+ this.skip(this.readUInt16());
310
+ return;
311
+ case DataType.LongBuffer:
312
+ case DataType.LongString:
313
+ this.skip(this.readUInt32());
314
+ return;
315
+ case DataType.Array:
316
+ throw new Error("Array is not allowed in markers.");
207
317
  default:
208
- throw new Error("Unexpected DataTypeID in marker definition.");
318
+ throw new Error(`Unknown data type while skipping: ${typeID}`);
209
319
  }
210
320
  }
211
321
 
212
- private readMarkerData(typeIDs: DataTypeID[]): any {
213
- const dataObject: any = {};
214
- for (const [index, typeID] of typeIDs.entries()) {
215
- dataObject[`field${index}`] = this.readMarkerField(typeID);
322
+ private readCustomValue(type: DataTypeID, arrayType?: DataTypeID): any {
323
+ if (type === DataType.Array) {
324
+ if (arrayType == null) {
325
+ throw new Error("Array custom field missing array element type.");
326
+ }
327
+
328
+ const count = this.readUInt32();
329
+ const values: any[] = [];
330
+ for (let i = 0; i < count; i++) {
331
+ values.push(this.readTypedValue(arrayType));
332
+ }
333
+ return values;
216
334
  }
217
- return dataObject;
335
+
336
+ return this.readTypedValue(type);
218
337
  }
219
338
 
220
339
  parse(): {
@@ -228,30 +347,33 @@ export class SSPMParser {
228
347
  markerDefinitions: MarkerDefinition[];
229
348
  markers: Marker[];
230
349
  } {
231
- // Header
232
350
  const header: Header = {
233
351
  signature: this.readBytes(4),
234
- version: this.readUInt16(),
352
+ version: this.readInt16(),
235
353
  reserved: this.readBytes(4),
236
354
  };
237
- if (header.version == 1) {
238
- throw "Can't parse";
355
+
356
+ const signatureValue = header.signature.readInt32LE(0);
357
+ if (signatureValue !== SSPMParser.SIGNATURE) {
358
+ throw new Error("Invalid SSPM signature.");
359
+ }
360
+
361
+ if (header.version !== 2) {
362
+ throw new Error(`Invalid SSPM version: ${header.version}`);
239
363
  }
240
364
 
241
- // Static Metadata
242
365
  const metadata: StaticMetadata = {
243
366
  sha1: this.readBytes(20),
244
367
  lastMarkerPos: this.readUInt32(),
245
368
  noteCount: this.readUInt32(),
246
369
  markerCount: this.readUInt32(),
247
- difficulty: this.buffer.readUInt8(this.offset++),
370
+ difficulty: this.readUInt8(),
248
371
  rating: this.readUInt16(),
249
- hasAudio: this.buffer.readUInt8(this.offset++) === 1,
250
- hasCover: this.buffer.readUInt8(this.offset++) === 1,
251
- requiresMod: this.buffer.readUInt8(this.offset++) === 1,
372
+ hasAudio: this.readUInt8() === 0x01,
373
+ hasCover: this.readUInt8() === 0x01,
374
+ requiresMod: this.readUInt8() === 0x01,
252
375
  };
253
376
 
254
- // Pointers
255
377
  const pointers: Pointers = {
256
378
  customDataOffset: this.readUInt64(),
257
379
  customDataLength: this.readUInt64(),
@@ -265,19 +387,6 @@ export class SSPMParser {
265
387
  markerLength: this.readUInt64(),
266
388
  };
267
389
 
268
- // Log derived pointer values
269
- this.log(`customDataOffset: ${pointers.customDataOffset}`);
270
- this.log(`customDataLength: ${pointers.customDataLength}`);
271
- this.log(`audioOffset: ${pointers.audioOffset}`);
272
- this.log(`audioLength: ${pointers.audioLength}`);
273
- this.log(`coverOffset: ${pointers.coverOffset}`);
274
- this.log(`coverLength: ${pointers.coverLength}`);
275
- this.log(`markerDefinitionsOffset: ${pointers.markerDefinitionsOffset}`);
276
- this.log(`markerDefinitionsLength: ${pointers.markerDefinitionsLength}`);
277
- this.log(`markerOffset: ${pointers.markerOffset}`);
278
- this.log(`markerLength: ${pointers.markerLength}`);
279
-
280
- // Strings
281
390
  const strings: Strings = {
282
391
  mapID: this.readString(),
283
392
  mapName: this.readString(),
@@ -285,89 +394,114 @@ export class SSPMParser {
285
394
  mappers: this.readStringList(this.readUInt16()),
286
395
  };
287
396
 
288
- let customData: CustomData = { fields: [] };
289
- try {
290
- if (pointers.customDataOffset && pointers.customDataLength) {
291
- this.log(
292
- `Reading Custom Data, Offset: ${pointers.customDataOffset}, Length: ${pointers.customDataLength}`
293
- );
294
- this.offset = Number(pointers.customDataOffset);
295
- const fieldCount = this.readUInt16();
296
- this.log(`Fields: ${fieldCount.toString()}`);
297
- for (let i = 0; i < fieldCount; i++) {
298
- const id = this.readString();
299
- const type = this.buffer.readUInt8(this.offset++) as DataTypeID;
300
- let arrayType: DataTypeID | undefined;
301
- if (type === 0x0c) {
302
- arrayType = this.buffer.readUInt8(this.offset++) as DataTypeID;
303
- }
304
- const length = this.readUInt32();
305
- // this.checkBounds(length);
306
- const value = this.readBytes(length);
307
- customData.fields.push({ id, type, arrayType, value });
397
+ const customData: CustomData = { fields: [] };
398
+
399
+ if (pointers.customDataOffset !== 0) {
400
+ this.seek(pointers.customDataOffset);
401
+
402
+ const fieldCount = this.readUInt16();
403
+ for (let i = 0; i < fieldCount; i++) {
404
+ const id = this.readString();
405
+ const type = this.readUInt8() as DataTypeID;
406
+
407
+ let arrayType: DataTypeID | undefined;
408
+ if (type === DataType.Array) {
409
+ arrayType = this.readUInt8() as DataTypeID;
308
410
  }
411
+
412
+ const value = this.readCustomValue(type, arrayType);
413
+ customData.fields.push({ id, type, arrayType, value });
309
414
  }
310
- } catch (error) {}
415
+ }
311
416
 
312
417
  let audio: Buffer | undefined;
313
418
  if (
314
419
  metadata.hasAudio &&
315
- pointers.audioOffset != 0 &&
316
- pointers.audioLength != 0
420
+ pointers.audioOffset !== 0 &&
421
+ pointers.audioLength !== 0
317
422
  ) {
318
- this.log(
319
- `Reading Audio Data, Offset: ${pointers.audioOffset}, Length: ${pointers.audioLength}`
320
- );
321
- this.offset = Number(pointers.audioOffset);
322
- this.checkBounds(Number(pointers.audioLength));
323
- audio = this.readBytes(Number(pointers.audioLength));
423
+ this.seek(pointers.audioOffset);
424
+ audio = this.readBytes(pointers.audioLength);
324
425
  }
325
426
 
326
427
  let cover: Buffer | undefined;
327
428
  if (
328
429
  metadata.hasCover &&
329
- pointers.coverOffset != 0 &&
330
- pointers.coverLength != 0
430
+ pointers.coverOffset !== 0 &&
431
+ pointers.coverLength !== 0
331
432
  ) {
332
- this.log(
333
- `Reading Cover Data, Offset: ${pointers.coverOffset}, Length: ${pointers.coverLength}`
334
- );
335
- this.offset = Number(pointers.coverOffset);
336
- this.checkBounds(Number(pointers.coverLength));
337
- cover = this.readBytes(Number(pointers.coverLength));
433
+ this.seek(pointers.coverOffset);
434
+ cover = this.readBytes(pointers.coverLength);
338
435
  }
339
436
 
340
- // Marker Definitions
341
- this.log(
342
- `Reading Marker Definitions, Offset: ${pointers.markerDefinitionsOffset}, Length: ${pointers.markerDefinitionsLength}`
343
- );
344
- this.offset = Number(pointers.markerDefinitionsOffset);
345
- const markerDefCount = this.buffer.readUInt8(this.offset++);
437
+ if (pointers.markerDefinitionsOffset === 0) {
438
+ throw new Error("Missing marker definitions offset.");
439
+ }
440
+
441
+ this.seek(pointers.markerDefinitionsOffset);
442
+
443
+ const markerDefCount = this.readUInt8();
346
444
  const markerDefinitions: MarkerDefinition[] = [];
445
+
347
446
  for (let i = 0; i < markerDefCount; i++) {
348
447
  const id = this.readString();
349
- const valueCount = this.buffer.readUInt8(this.offset++);
448
+ const valueCount = this.readUInt8();
449
+
350
450
  const values: DataTypeID[] = [];
351
451
  for (let j = 0; j < valueCount; j++) {
352
- values.push(this.buffer.readUInt8(this.offset++) as DataTypeID);
452
+ values.push(this.readUInt8() as DataTypeID);
453
+ }
454
+
455
+ const terminator = this.readUInt8();
456
+ if (terminator !== 0x00) {
457
+ throw new Error(
458
+ `Invalid marker definition terminator for "${id}": expected 0x00, got 0x${terminator.toString(16)}`
459
+ );
353
460
  }
461
+
354
462
  markerDefinitions.push({ id, values });
355
463
  }
356
464
 
357
- // Markers
358
- this.log(
359
- `Reading Markers, Offset: ${pointers.markerOffset}, Length: ${pointers.markerLength}`
360
- );
361
- this.offset = Number(pointers.markerOffset);
362
- const endOffset = this.offset + Number(pointers.markerLength);
465
+ this.seek(pointers.markerOffset);
363
466
 
364
467
  const markers: Marker[] = [];
468
+ const noteDefIndex = markerDefinitions.findIndex(
469
+ (d) => d.id === "ssp_note"
470
+ );
471
+ const effectiveNoteDefIndex = noteDefIndex >= 0 ? noteDefIndex : 0;
365
472
 
366
- while (this.offset < endOffset) {
473
+ for (let i = 0; i < metadata.markerCount; i++) {
367
474
  const position = this.readUInt32();
368
- const type = this.buffer.readUInt8(this.offset++);
369
- const def = markerDefinitions.find((d) => d.id === "ssp_note"); // Adjust as needed for other marker types
370
- const data = def ? this.readMarkerData(def.values) : {};
475
+ const type = this.readUInt8();
476
+
477
+ if (type >= markerDefinitions.length) {
478
+ throw new Error(`Invalid marker type index: ${type}`);
479
+ }
480
+
481
+ const def = markerDefinitions[type];
482
+ const isNote = type === effectiveNoteDefIndex;
483
+
484
+ const data: Record<string, any> = {};
485
+ let firstNotePositionCaptured = false;
486
+
487
+ for (let fieldIndex = 0; fieldIndex < def.values.length; fieldIndex++) {
488
+ const valueType = def.values[fieldIndex];
489
+
490
+ if (
491
+ isNote &&
492
+ valueType === DataType.Position &&
493
+ !firstNotePositionCaptured
494
+ ) {
495
+ const pos = this.readPosition();
496
+ data[`field${fieldIndex}`] = pos;
497
+ data.position = pos;
498
+ firstNotePositionCaptured = true;
499
+ continue;
500
+ }
501
+
502
+ data[`field${fieldIndex}`] = this.readTypedValue(valueType);
503
+ }
504
+
371
505
  markers.push({ position, type, data });
372
506
  }
373
507