Skip to content

Commit 1dccc2c

Browse files
authored
fix: アートワーク取得の信頼性改善とクラスタサイズ0対応 (#57) (#66)
## Summary - Fileパケット(clusterSize=0)でアートワークデータが抽出できないバグを修正 (#57, #58, #59) - 5スキル並列レビュー(CodeRabbit, requesting-code-review, simplify, Codex adversarial-review, TDD)を3ラウンド実施し、発見された全問題(T1-T23)を修正 ### 主な変更 **バグ修正** - `clusterSize=0` のFileパケットからバッファ末尾までをデータとして読み取るよう修正 - `requestData` がFileパケット応答で resolve しないバグを修正 - CUEデータパーサーの `cueStart` offset を仕様書通り byte 47 に修正 - `ipToNumber` の空セグメント検証漏れ(`"192.168..1"` 等)を修正 - `CueData` から `loopOutTime` を削除(byte 46-49 が CUE 1 開始 byte 47 と重複するため) - converge 後のサブネット外 Master OptIn で `this.server` が上書きされる信頼境界退行を修正 - `connectToAdapter` (switchAdapter経由) の pre-connected パスにもサブネットチェックを追加 - `disconnectSockets` で共有ソケットの重複 close を排除 - 通常 assembler パスと fileChunks パスの両方で `readAssembled` 後の data null チェックを追加 **信頼性向上** - `handleFileChunkPacket` メソッド抽出で `receiveUnicast` のネスト深化を解消 - `fileCollectionTimeout` (200ms) に加え `requestTimeout` を全体上限タイマーとして設定し無限蓄積を防止 - `maxFileChunkBytes` (10MB) によるメモリ上限を追加 - `TCNetDataPacketArtwork.readAssembled` に JPEG SOI マーカー (0xFF 0xD8) 検証を追加 - `resolveAdapterByRemoteAddress` に try-catch (IPv6防御) と重複サブネット曖昧性検出を追加 - `getClusterEnd` ユーティリティ関数でバッファ末尾計算ロジックを統一 **テスト (+10件, 269テスト全パス)** - `ipToNumber` エラーケース6件、`listNetworkAdapters` エッジケース3件 - サブネットフィルタ(connected / pre-connected)、重複サブネット曖昧性検出 - `totalPackets=0` の単一/マルチFileパケットアセンブリ、requestTimeout上限 - JPEG SOI 検証(不正データ reject) - テストの private アクセスを `configureMockBroadcast` ヘルパーに集約 ### 破壊的変更 - `CueData.loopOutTime` を削除(byte 46-49 が CUE 1 と重複し信頼できない値のため) ## Test plan - [x] `npx vitest run --project unit` — 269テスト全パス - [x] lefthook pre-commit フック全パス (typecheck, build, test, lint, format) - [x] 実機テスト: Bridge + CDJ 環境でアートワーク取得を確認
1 parent 01ab150 commit 1dccc2c

13 files changed

Lines changed: 1076 additions & 122 deletions

.changeset/artwork-cluster-size.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@9c5s/node-tcnet": patch
3+
---
4+
5+
fix: アートワーク取得の信頼性改善とクラスタサイズ0対応
6+
7+
- FileパケットのclusterSize=0対応とアートワークデータ抽出の修正
8+
- CUEデータパーサーのbyte offset修正とloopOutTime削除(byte重複)
9+
- ソケット共有設計のサブネットフィルタリングとlongest prefix match
10+
- JPEG SOIマーカー検証、fileChunksメモリ上限、requestTimeout上限の追加
11+
- ipToNumber入力検証強化とgetClusterEndユーティリティ統一
12+
13+
BREAKING CHANGE: CueData.loopOutTimeを削除(byte 46-49がCUE 1と重複し信頼できない値のため)

src/multi-packet.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { getClusterEnd } from "./utils";
2+
13
/**
24
* BigWaveFormとBeatGridのマルチパケットを組み立てるアセンブラ
35
* @category Utilities
@@ -22,9 +24,10 @@ export class MultiPacketAssembler {
2224
const packetNo = buffer.readUInt32LE(34);
2325
const clusterSize = buffer.readUInt32LE(38);
2426
const dataStart = 42;
25-
if (dataStart + clusterSize > buffer.length) return false;
27+
const end = getClusterEnd(buffer.length, dataStart, clusterSize);
28+
if (clusterSize > 0 && end !== dataStart + clusterSize) return false;
2629
// T3: Buffer.from() でコピーを保持し、元バッファへの参照共有を防ぐ
27-
this.packets.set(packetNo, Buffer.from(buffer.slice(dataStart, dataStart + clusterSize)));
30+
this.packets.set(packetNo, Buffer.from(buffer.slice(dataStart, end)));
2831
return this.packets.size >= this.totalPackets;
2932
}
3033

src/network.ts

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { assert } from "./utils";
1+
import { assert, getClusterEnd } from "./utils";
22
import type {
33
ArtworkData,
44
CueData,
@@ -630,30 +630,33 @@ export class TCNetDataPacketCUE extends TCNetDataPacket {
630630
/** バッファからパケットデータを読み取る */
631631
read(): void {
632632
const loopInTime = this.buffer.readUInt32LE(42);
633-
const loopOutTime = this.buffer.readUInt32LE(46);
633+
// byte 46-49 (Loop OUT Time) はCUE 1開始(byte 47)と重複するため読み取らない
634+
// CUE 1にデータがあるとloopOutTimeは汚染された値になる
634635
const cues: CuePoint[] = [];
635-
// 仕様書にはCUE 1開始を byte 47 と記載しているが、
636-
// Loop OUT Time (byte 46-49) と重複するため仕様書の誤記。
637-
// 実機検証に基づき byte 50 を採用。
638-
const cueStart = 50;
636+
const cueStart = 47;
639637
for (let i = 0; i < 18; i++) {
640638
const offset = cueStart + i * 22;
641639
if (offset + 22 > this.buffer.length) break;
642640
const type = this.buffer.readUInt8(offset);
643-
if (type === 0) continue;
641+
const inTime = this.buffer.readUInt32LE(offset + 2);
642+
const outTime = this.buffer.readUInt32LE(offset + 6);
643+
// BridgeはTYPEフィールドを0で送信する場合がある
644+
// type=0かつinTime/outTime両方0のエントリのみスキップする
645+
// type有効値(>=1)でinTime/outTime=0のケース(トラック先頭CUE)は保持する
646+
if (type === 0 && inTime === 0 && outTime === 0) continue;
644647
cues.push({
645648
index: i + 1,
646649
type,
647-
inTime: this.buffer.readUInt32LE(offset + 2),
648-
outTime: this.buffer.readUInt32LE(offset + 6),
650+
inTime,
651+
outTime,
649652
color: {
650653
r: this.buffer.readUInt8(offset + 11),
651654
g: this.buffer.readUInt8(offset + 12),
652655
b: this.buffer.readUInt8(offset + 13),
653656
},
654657
});
655658
}
656-
this.data = { loopInTime, loopOutTime, cues };
659+
this.data = { loopInTime, cues };
657660
}
658661

659662
/** パケットデータをバッファに書き込む */
@@ -909,7 +912,7 @@ export class TCNetDataPacketArtwork extends TCNetDataPacket {
909912
return;
910913
}
911914
const clusterSize = this.buffer.readUInt32LE(38);
912-
const end = Math.min(dataStart + clusterSize, this.buffer.length);
915+
const end = getClusterEnd(this.buffer.length, dataStart, clusterSize);
913916
this.data = { jpeg: Buffer.from(this.buffer.slice(dataStart, end)) };
914917
}
915918

@@ -918,6 +921,11 @@ export class TCNetDataPacketArtwork extends TCNetDataPacket {
918921
* @param assembled - 結合済みバッファ
919922
*/
920923
readAssembled(assembled: Buffer): void {
924+
// JPEG SOIマーカー(0xFF 0xD8)が存在しないデータは不正とみなす
925+
if (assembled.length < 2 || assembled[0] !== 0xff || assembled[1] !== 0xd8) {
926+
this.data = null;
927+
return;
928+
}
921929
this.data = { jpeg: Buffer.from(assembled) };
922930
}
923931

0 commit comments

Comments
 (0)