@flighthq/swf 0.3.0 → 0.3.1-next.320.fe56fc2
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.
- package/dist/contract.d.ts +1 -0
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +1 -0
- package/dist/contract.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/swfDocument.d.ts +5 -5
- package/dist/swfDocument.d.ts.map +1 -1
- package/dist/swfDocument.js +359 -102
- package/dist/swfDocument.js.map +1 -1
- package/dist/swfFilter.d.ts +2 -2
- package/dist/swfFilter.d.ts.map +1 -1
- package/dist/swfFilter.js +24 -2
- package/dist/swfFilter.js.map +1 -1
- package/dist/swfFrameAction.d.ts +2 -2
- package/dist/swfFrameAction.d.ts.map +1 -1
- package/dist/swfFrameAction.js +13 -6
- package/dist/swfFrameAction.js.map +1 -1
- package/dist/swfFrameActionTestHelper.d.ts +2 -0
- package/dist/swfFrameActionTestHelper.d.ts.map +1 -0
- package/dist/swfFrameActionTestHelper.js +99 -0
- package/dist/swfFrameActionTestHelper.js.map +1 -0
- package/dist/swfImageDecoder.d.ts +4 -0
- package/dist/swfImageDecoder.d.ts.map +1 -0
- package/dist/swfImageDecoder.js +21 -0
- package/dist/swfImageDecoder.js.map +1 -0
- package/dist/swfMorphShape.d.ts +2 -1
- package/dist/swfMorphShape.d.ts.map +1 -1
- package/dist/swfMorphShape.js +18 -7
- package/dist/swfMorphShape.js.map +1 -1
- package/dist/swfText.d.ts +2 -2
- package/dist/swfText.d.ts.map +1 -1
- package/dist/swfText.js +15 -2
- package/dist/swfText.js.map +1 -1
- package/package.json +24 -22
- package/src/swfDocument.test.ts +1527 -163
- package/src/swfFilter.test.ts +53 -0
- package/src/swfFrameAction.test.ts +26 -97
- package/src/swfImageDecoder.test.ts +53 -0
- package/src/swfImageResources.test.ts +14 -17
- package/src/swfMorphShape.test.ts +18 -0
package/src/swfDocument.test.ts
CHANGED
|
@@ -5,6 +5,8 @@ import {
|
|
|
5
5
|
} from '@flighthq/compression/contract';
|
|
6
6
|
import { createGlyphRasterizerBackendFromGlyphOutlineSource } from '@flighthq/font/contract';
|
|
7
7
|
import { createGlyphAtlas, getGlyphAtlasEntry } from '@flighthq/glyphatlas/contract';
|
|
8
|
+
import { clearImageDecoders } from '@flighthq/image-codec/contract';
|
|
9
|
+
import { collectImportDiagnostics } from '@flighthq/importdiagnostics/contract';
|
|
8
10
|
import {
|
|
9
11
|
getMovieClipCurrentFrame,
|
|
10
12
|
getMovieClipCurrentLabel,
|
|
@@ -26,6 +28,7 @@ import {
|
|
|
26
28
|
import {
|
|
27
29
|
createScene2DDocumentFromBytes,
|
|
28
30
|
createScene2DDocumentImporterRegistry,
|
|
31
|
+
loadScene2DImageResources,
|
|
29
32
|
resolveScene2DResources,
|
|
30
33
|
} from '@flighthq/scene2d-resources/contract';
|
|
31
34
|
import { createDisplayObject } from '@flighthq/scene2d/contract';
|
|
@@ -37,6 +40,7 @@ import type {
|
|
|
37
40
|
TimelineAudioCue,
|
|
38
41
|
MorphShape,
|
|
39
42
|
MovieClip,
|
|
43
|
+
Node2D,
|
|
40
44
|
RichText,
|
|
41
45
|
Scale9Shape,
|
|
42
46
|
Shape,
|
|
@@ -50,6 +54,7 @@ import {
|
|
|
50
54
|
Compression,
|
|
51
55
|
DisplayObjectKind,
|
|
52
56
|
ImageResourceReferenceKind,
|
|
57
|
+
ImportDiagnosticSeverity,
|
|
53
58
|
MorphShapeKind,
|
|
54
59
|
MovieClipKind,
|
|
55
60
|
ResourceResolutionState,
|
|
@@ -67,10 +72,18 @@ import {
|
|
|
67
72
|
readSwfExportedSymbolNames,
|
|
68
73
|
registerSwfScene2DDocumentImporter,
|
|
69
74
|
} from './swfDocument';
|
|
75
|
+
import { buildFrameScriptAbc } from './swfFrameActionTestHelper';
|
|
76
|
+
import { registerSwfImageDecoders } from './swfImageDecoder';
|
|
70
77
|
import { ShapeWriter } from './swfShapeTestHelper';
|
|
71
78
|
|
|
72
|
-
beforeEach(() =>
|
|
73
|
-
|
|
79
|
+
beforeEach(() => {
|
|
80
|
+
clearImageDecoders();
|
|
81
|
+
unregisterDecompressor(Compression.Deflate);
|
|
82
|
+
});
|
|
83
|
+
afterEach(() => {
|
|
84
|
+
clearImageDecoders();
|
|
85
|
+
unregisterDecompressor(Compression.Deflate);
|
|
86
|
+
});
|
|
74
87
|
|
|
75
88
|
describe('createGlyphOutlineSourcesFromSwf', () => {
|
|
76
89
|
it('funnels a DefineFont2 outline and code table through the font adapter into glyphatlas', () => {
|
|
@@ -1193,8 +1206,7 @@ describe('createScene2DFromSwf', () => {
|
|
|
1193
1206
|
expect(drawn.data.commands.filter((token) => token === 'lineTo')).toHaveLength(2);
|
|
1194
1207
|
});
|
|
1195
1208
|
|
|
1196
|
-
it('
|
|
1197
|
-
registerDeflateDecompressor();
|
|
1209
|
+
it('emits a lossless bitmap reference synchronously and resolves it only in the async resource pass', async () => {
|
|
1198
1210
|
const art = new ShapeWriter();
|
|
1199
1211
|
art.writeFillStyleCount(1);
|
|
1200
1212
|
art.writeBitmapFillStyle(0x41, 9, 20);
|
|
@@ -1216,18 +1228,27 @@ describe('createScene2DFromSwf', () => {
|
|
|
1216
1228
|
createTag(TAG_END),
|
|
1217
1229
|
]),
|
|
1218
1230
|
);
|
|
1219
|
-
unregisterDecompressor(Compression.Deflate);
|
|
1220
|
-
|
|
1221
1231
|
const drawn = getNodeChildren(document!.root)[0] as Shape;
|
|
1222
1232
|
expect(drawn.data.commands[0]).toBe('beginTextureFill');
|
|
1223
|
-
// A lossless payload is not an image file, so it resolves here rather than through @flighthq/image —
|
|
1224
|
-
// which is why it leaves nothing on the document's image-resource contract.
|
|
1225
1233
|
const texture = drawn.data.commands[2] as Texture2D;
|
|
1234
|
+
const reference = document!.imageResources[0];
|
|
1235
|
+
expect(document!.imageResources).toHaveLength(1);
|
|
1236
|
+
expect(reference.mimeType).toBe('image/x-swf-lossless');
|
|
1237
|
+
expect(reference.kind === ImageResourceReferenceKind.Embedded && reference.alphaType).toBe('opaque');
|
|
1238
|
+
expect(reference.textures).toEqual([texture]);
|
|
1239
|
+
expect(getTextureSource(texture)).toBeNull();
|
|
1240
|
+
|
|
1241
|
+
// Registration is caller-owned and happens after parsing; neither parsing nor reference creation
|
|
1242
|
+
// secretly installs a decoder or starts async work.
|
|
1243
|
+
registerDeflateDecompressor();
|
|
1244
|
+
registerSwfImageDecoders();
|
|
1245
|
+
const resources = await loadScene2DImageResources(document!);
|
|
1246
|
+
|
|
1247
|
+
expect(resources.resolved).toEqual([reference]);
|
|
1226
1248
|
expectLosslessTexturePixel(texture);
|
|
1227
|
-
expect(document!.imageResources).toEqual([]);
|
|
1228
1249
|
});
|
|
1229
1250
|
|
|
1230
|
-
it('
|
|
1251
|
+
it('parses lossless bitmap reference data without requiring a registered decompressor', () => {
|
|
1231
1252
|
const art = new ShapeWriter();
|
|
1232
1253
|
art.writeFillStyleCount(1);
|
|
1233
1254
|
art.writeBitmapFillStyle(0x41, 9, 20);
|
|
@@ -1253,6 +1274,44 @@ describe('createScene2DFromSwf', () => {
|
|
|
1253
1274
|
const drawn = getNodeChildren(document!.root)[0] as Shape;
|
|
1254
1275
|
expect(drawn.data.commands[0]).toBe('beginTextureFill');
|
|
1255
1276
|
expect(getTextureSource(drawn.data.commands[2] as Texture2D)).toBeNull();
|
|
1277
|
+
expect(document!.imageResources).toHaveLength(1);
|
|
1278
|
+
expect(document!.imageResources[0].state).toBe(ResourceResolutionState.Unresolved);
|
|
1279
|
+
});
|
|
1280
|
+
|
|
1281
|
+
it('carries premultiplied lossless-alpha bytes through the reference into the resolved Bitmap', async () => {
|
|
1282
|
+
const art = new ShapeWriter();
|
|
1283
|
+
art.writeFillStyleCount(1);
|
|
1284
|
+
art.writeBitmapFillStyle(0x41, 9, 20);
|
|
1285
|
+
art.writeLineStyleCount(0);
|
|
1286
|
+
art.writeStyleBits(1, 0);
|
|
1287
|
+
art.writeStyleChange({ fill1: 1, moveToX: 0, moveToY: 0 });
|
|
1288
|
+
art.writeStraightEdge(800, 0);
|
|
1289
|
+
art.writeStraightEdge(0, 800);
|
|
1290
|
+
art.writeEndShape();
|
|
1291
|
+
const pixels = losslessPayload(5, 1, 1, storedDeflate([0x80, 0x40, 0x20, 0x10]));
|
|
1292
|
+
const document = createScene2DFromSwf(
|
|
1293
|
+
createSwf([
|
|
1294
|
+
createTag(TAG_DEFINE_BITS_LOSSLESS_2, joinBytes(uint16(9), pixels)),
|
|
1295
|
+
createTag(TAG_DEFINE_SHAPE_3, joinBytes(uint16(7), createRectangle(0, 800, 0, 800), art.toBytes())),
|
|
1296
|
+
createTag(TAG_PLACE_OBJECT_2, joinBytes(new Uint8Array([PLACE_HAS_CHARACTER]), uint16(1), uint16(7))),
|
|
1297
|
+
createTag(TAG_SHOW_FRAME),
|
|
1298
|
+
createTag(TAG_END),
|
|
1299
|
+
]),
|
|
1300
|
+
)!;
|
|
1301
|
+
const reference = document.imageResources[0];
|
|
1302
|
+
expect(reference.kind === ImageResourceReferenceKind.Embedded && reference.alphaType).toBe('premultiplied');
|
|
1303
|
+
|
|
1304
|
+
registerDeflateDecompressor();
|
|
1305
|
+
registerSwfImageDecoders();
|
|
1306
|
+
await loadScene2DImageResources(document);
|
|
1307
|
+
|
|
1308
|
+
const drawn = getNodeChildren(document.root)[0] as Shape;
|
|
1309
|
+
const source = getTextureSource(drawn.data.commands[2] as Texture2D);
|
|
1310
|
+
expect(source?.kind).toBe(BitmapTextureSourceKind);
|
|
1311
|
+
if (source?.kind !== BitmapTextureSourceKind) throw new Error('expected a lossless bitmap source');
|
|
1312
|
+
const bitmap = source as Bitmap;
|
|
1313
|
+
expect(bitmap.alphaType).toBe('premultiplied');
|
|
1314
|
+
expect([...bitmap.data]).toEqual([0x40, 0x20, 0x10, 0x80]);
|
|
1256
1315
|
});
|
|
1257
1316
|
|
|
1258
1317
|
it('samples one bitmap character through a texture per sampling variant', () => {
|
|
@@ -2439,51 +2498,128 @@ describe('createScene2DFromSwf', () => {
|
|
|
2439
2498
|
});
|
|
2440
2499
|
});
|
|
2441
2500
|
|
|
2442
|
-
describe('createScene2DFromSwf
|
|
2443
|
-
it('reports
|
|
2444
|
-
const
|
|
2445
|
-
const
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
);
|
|
2501
|
+
describe('createScene2DFromSwf import diagnostics', () => {
|
|
2502
|
+
it('reports an unreadable container as a Reject naming which check refused it', () => {
|
|
2503
|
+
const notSwf = new Uint8Array([0x50, 0x4b, 0x03, 0x04, 0, 0, 0, 0]);
|
|
2504
|
+
const diagnostics = collectImportDiagnostics((sink) => {
|
|
2505
|
+
expect(createScene2DFromSwf(notSwf, sink)).toBeNull();
|
|
2506
|
+
});
|
|
2507
|
+
|
|
2508
|
+
expect(diagnostics.map((entry) => entry.kind)).toEqual(['swf.invalid-signature']);
|
|
2509
|
+
expect(diagnostics[0].severity).toBe(ImportDiagnosticSeverity.Reject);
|
|
2510
|
+
expect(diagnostics[0].origin).toBe('uncompressSwfSource');
|
|
2511
|
+
});
|
|
2512
|
+
|
|
2513
|
+
it('separates an unregistered decompressor from a corrupt body, which share one null sentinel', () => {
|
|
2514
|
+
const uncompressed = createSwf([createTag(TAG_SHOW_FRAME), createTag(TAG_END)]);
|
|
2515
|
+
const compressed = joinBytes(uncompressed.subarray(0, 8), uncompressed.subarray(8));
|
|
2516
|
+
compressed[0] = 0x43;
|
|
2517
|
+
|
|
2518
|
+
// Nothing registered: the file is readable after one registration, which is not the same failure as
|
|
2519
|
+
// a corrupt stream even though both come back null.
|
|
2520
|
+
const unregistered = collectImportDiagnostics((sink) => {
|
|
2521
|
+
expect(createScene2DFromSwf(compressed, sink)).toBeNull();
|
|
2522
|
+
});
|
|
2523
|
+
expect(unregistered.map((entry) => entry.kind)).toEqual(['swf.no-decompressor-registered']);
|
|
2524
|
+
|
|
2525
|
+
registerDecompressor(Compression.Deflate, () => null);
|
|
2526
|
+
try {
|
|
2527
|
+
const failed = collectImportDiagnostics((sink) => {
|
|
2528
|
+
expect(createScene2DFromSwf(compressed, sink)).toBeNull();
|
|
2529
|
+
});
|
|
2530
|
+
expect(failed.map((entry) => entry.kind)).toEqual(['swf.decompression-failed']);
|
|
2531
|
+
} finally {
|
|
2532
|
+
unregisterDecompressor(Compression.Deflate);
|
|
2533
|
+
}
|
|
2534
|
+
});
|
|
2535
|
+
|
|
2536
|
+
it('records nothing at all when the caller engages no collector', () => {
|
|
2537
|
+
const notSwf = new Uint8Array([0x50, 0x4b, 0x03, 0x04, 0, 0, 0, 0]);
|
|
2538
|
+
// The whole seam is opt-in: the ordinary call must not build a crumb, so this asserts the shape of
|
|
2539
|
+
// the default path rather than any particular diagnostic.
|
|
2540
|
+
expect(createScene2DFromSwf(notSwf)).toBeNull();
|
|
2541
|
+
expect(collectImportDiagnostics(() => {})).toEqual([]);
|
|
2542
|
+
});
|
|
2543
|
+
|
|
2544
|
+
it('reports a deliberately declined tag as a Skip, which is correct behaviour rather than failure', () => {
|
|
2545
|
+
// VideoFrame payloads are codec packets this importer does not carry. The document still imports;
|
|
2546
|
+
// what the crumb adds is that a caller can now tell "declined on purpose" from "silently lost".
|
|
2547
|
+
const file = createSwf([
|
|
2548
|
+
createTag(
|
|
2549
|
+
TAG_DEFINE_VIDEO_STREAM,
|
|
2550
|
+
joinBytes(uint16(4), uint16(1), uint16(16), uint16(16), new Uint8Array([0x01, 2])),
|
|
2551
|
+
),
|
|
2552
|
+
createTag(61, joinBytes(uint16(4), uint16(0), new Uint8Array([0, 0, 0, 0]))),
|
|
2553
|
+
createTag(TAG_SHOW_FRAME),
|
|
2554
|
+
createTag(TAG_END),
|
|
2555
|
+
]);
|
|
2556
|
+
const diagnostics = collectImportDiagnostics((sink) => {
|
|
2557
|
+
expect(createScene2DFromSwf(file, sink)).not.toBeNull();
|
|
2558
|
+
});
|
|
2559
|
+
|
|
2560
|
+
const declined = diagnostics.filter((entry) => entry.kind === 'swf.video-frame-payload');
|
|
2561
|
+
expect(declined).toHaveLength(1);
|
|
2562
|
+
expect(declined[0].severity).toBe(ImportDiagnosticSeverity.Skip);
|
|
2563
|
+
expect(declined[0].detail).toEqual({ capability: 'swf.video.video-frame', tag: 61 });
|
|
2564
|
+
});
|
|
2565
|
+
|
|
2566
|
+
it('stays silent on metadata tags, whose absence costs a document nothing', () => {
|
|
2567
|
+
// The line this asserts is the one that keeps the report usable: a caller filtering crumbs should not
|
|
2568
|
+
// have to skip past authoring metadata to find the entries that mean something.
|
|
2569
|
+
const file = createSwf([
|
|
2570
|
+
createTag(69, new Uint8Array([0, 0, 0, 0])),
|
|
2571
|
+
createTag(77, swfString('<rdf/>')),
|
|
2572
|
+
createTag(TAG_SHOW_FRAME),
|
|
2573
|
+
createTag(TAG_END),
|
|
2574
|
+
]);
|
|
2575
|
+
const diagnostics = collectImportDiagnostics((sink) => {
|
|
2576
|
+
expect(createScene2DFromSwf(file, sink)).not.toBeNull();
|
|
2577
|
+
});
|
|
2578
|
+
|
|
2579
|
+
expect(diagnostics).toEqual([]);
|
|
2580
|
+
});
|
|
2581
|
+
|
|
2582
|
+
it('reports an unreadable shape body as a Recover, since the placeholder still places and sizes', () => {
|
|
2583
|
+
// A body the decoder cannot read costs that character's drawing and nothing else, so the honest
|
|
2584
|
+
// outcome is Recover: the document still imports and the character still has its authored bounds.
|
|
2585
|
+
const file = createSwf([
|
|
2586
|
+
createTag(TAG_DEFINE_SHAPE, joinBytes(uint16(1), createRectangle(0, 20, 0, 20), new Uint8Array([0xff]))),
|
|
2587
|
+
createTag(TAG_SHOW_FRAME),
|
|
2588
|
+
createTag(TAG_END),
|
|
2589
|
+
]);
|
|
2590
|
+
const diagnostics = collectImportDiagnostics((sink) => {
|
|
2591
|
+
expect(createScene2DFromSwf(file, sink)).not.toBeNull();
|
|
2592
|
+
});
|
|
2593
|
+
|
|
2594
|
+
const recovered = diagnostics.filter((entry) => entry.kind === 'swf.shape-body-unreadable');
|
|
2595
|
+
expect(recovered).toHaveLength(1);
|
|
2596
|
+
expect(recovered[0].severity).toBe(ImportDiagnosticSeverity.Recover);
|
|
2597
|
+
expect(recovered[0].detail).toEqual({ capability: 'swf.shape.define-shape', characterId: 1, version: 1 });
|
|
2598
|
+
});
|
|
2599
|
+
|
|
2600
|
+
it('reports a morph definition that does not decode, which otherwise leaves no trace at all', () => {
|
|
2601
|
+
// Header reads cleanly — id and both bounds — so the definition is accepted and only the morph body
|
|
2602
|
+
// fails. That is the case with no signal: the character is absent and the import still succeeds.
|
|
2450
2603
|
const body = joinBytes(
|
|
2451
2604
|
uint16(7),
|
|
2452
2605
|
createRectangle(0, 200, 0, 200),
|
|
2453
2606
|
createRectangle(0, 400, 0, 400),
|
|
2454
|
-
uint32(
|
|
2455
|
-
|
|
2456
|
-
startEdges,
|
|
2457
|
-
endEdges,
|
|
2458
|
-
);
|
|
2459
|
-
const place = (depth: number, ratio: number): Uint8Array =>
|
|
2460
|
-
createTag(
|
|
2461
|
-
TAG_PLACE_OBJECT_2,
|
|
2462
|
-
joinBytes(new Uint8Array([PLACE_HAS_CHARACTER | PLACE_HAS_RATIO]), uint16(depth), uint16(7), uint16(ratio)),
|
|
2463
|
-
);
|
|
2464
|
-
|
|
2465
|
-
const document = createScene2DFromSwf(
|
|
2466
|
-
createSwf([
|
|
2467
|
-
createTag(TAG_DEFINE_MORPH_SHAPE, body),
|
|
2468
|
-
place(1, 0),
|
|
2469
|
-
place(2, 0x7fff),
|
|
2470
|
-
place(3, 0xffff),
|
|
2471
|
-
createTag(TAG_SHOW_FRAME),
|
|
2472
|
-
createTag(TAG_END),
|
|
2473
|
-
]),
|
|
2607
|
+
uint32(0xffff),
|
|
2608
|
+
new Uint8Array([0]),
|
|
2474
2609
|
);
|
|
2610
|
+
const diagnostics = collectImportDiagnostics((sink) => {
|
|
2611
|
+
expect(
|
|
2612
|
+
createScene2DFromSwf(createSwf([createTag(TAG_DEFINE_MORPH_SHAPE, body), createTag(TAG_END)]), sink),
|
|
2613
|
+
).not.toBeNull();
|
|
2614
|
+
});
|
|
2475
2615
|
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
expect(
|
|
2480
|
-
expect(boxes[1]).toBeCloseTo(15, 1);
|
|
2481
|
-
expect(boxes[2]).toBeCloseTo(20);
|
|
2616
|
+
const dropped = diagnostics.filter((entry) => entry.kind === 'swf.morph-shape-undecodable');
|
|
2617
|
+
expect(dropped).toHaveLength(1);
|
|
2618
|
+
expect(dropped[0].severity).toBe(ImportDiagnosticSeverity.Drop);
|
|
2619
|
+
expect(dropped[0].detail).toEqual({ capability: 'swf.morph.define-morph-shape', characterId: 7 });
|
|
2482
2620
|
});
|
|
2483
|
-
});
|
|
2484
2621
|
|
|
2485
|
-
|
|
2486
|
-
it('places a morph character and drives its progress from the placement ratio', () => {
|
|
2622
|
+
it('stays silent about a morph definition that decodes, so the drop entry carries information', () => {
|
|
2487
2623
|
const startEdges = morphBox(200);
|
|
2488
2624
|
const endEdges = morphBox(400);
|
|
2489
2625
|
const styles = joinBytes(
|
|
@@ -2499,131 +2635,1342 @@ describe('createScene2DFromSwf morph shapes', () => {
|
|
|
2499
2635
|
startEdges,
|
|
2500
2636
|
endEdges,
|
|
2501
2637
|
);
|
|
2638
|
+
const diagnostics = collectImportDiagnostics((sink) => {
|
|
2639
|
+
expect(
|
|
2640
|
+
createScene2DFromSwf(createSwf([createTag(TAG_DEFINE_MORPH_SHAPE, body), createTag(TAG_END)]), sink),
|
|
2641
|
+
).not.toBeNull();
|
|
2642
|
+
});
|
|
2502
2643
|
|
|
2503
|
-
|
|
2504
|
-
createSwf([
|
|
2505
|
-
createTag(TAG_DEFINE_MORPH_SHAPE, body),
|
|
2506
|
-
// Two placements of one character at different ratios: the case a single shared node cannot serve.
|
|
2507
|
-
createTag(
|
|
2508
|
-
TAG_PLACE_OBJECT_2,
|
|
2509
|
-
joinBytes(new Uint8Array([PLACE_HAS_CHARACTER | PLACE_HAS_RATIO]), uint16(1), uint16(7), uint16(0)),
|
|
2510
|
-
),
|
|
2511
|
-
createTag(
|
|
2512
|
-
TAG_PLACE_OBJECT_2,
|
|
2513
|
-
joinBytes(new Uint8Array([PLACE_HAS_CHARACTER | PLACE_HAS_RATIO]), uint16(2), uint16(7), uint16(0xffff)),
|
|
2514
|
-
),
|
|
2515
|
-
createTag(TAG_SHOW_FRAME),
|
|
2516
|
-
createTag(TAG_END),
|
|
2517
|
-
]),
|
|
2518
|
-
);
|
|
2519
|
-
|
|
2520
|
-
const [first, second] = getNodeChildren(document!.root) as MorphShape[];
|
|
2521
|
-
expect(first.kind).toBe(MorphShapeKind);
|
|
2522
|
-
expect(second.kind).toBe(MorphShapeKind);
|
|
2523
|
-
// Distinct nodes, each sitting at its own point along the same definition.
|
|
2524
|
-
expect(first).not.toBe(second);
|
|
2525
|
-
expect(first.data.progress).toBe(0);
|
|
2526
|
-
expect(second.data.progress).toBe(1);
|
|
2527
|
-
expect(morphWidth(first)).toBeCloseTo(10);
|
|
2528
|
-
expect(morphWidth(second)).toBeCloseTo(20);
|
|
2644
|
+
expect(diagnostics.filter((entry) => entry.kind === 'swf.morph-shape-undecodable')).toEqual([]);
|
|
2529
2645
|
});
|
|
2530
|
-
});
|
|
2531
2646
|
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
]),
|
|
2647
|
+
it('reports a static text body that does not compose, which the deferred pass would otherwise swallow', () => {
|
|
2648
|
+
// A record header declaring a font and a colour, then nothing to read them from. Composition is
|
|
2649
|
+
// deferred to after every font is parsed, so this failure surfaces far from the tag that carried it.
|
|
2650
|
+
const record = new BitWriter();
|
|
2651
|
+
record.writeUnsigned(1, 1);
|
|
2652
|
+
record.writeUnsigned(0, 3);
|
|
2653
|
+
record.writeUnsigned(1, 1);
|
|
2654
|
+
record.writeUnsigned(1, 1);
|
|
2655
|
+
record.writeUnsigned(0, 1);
|
|
2656
|
+
record.writeUnsigned(0, 1);
|
|
2657
|
+
const text = joinBytes(
|
|
2658
|
+
uint16(6),
|
|
2659
|
+
createRectangle(0, 1024, 0, 1024),
|
|
2660
|
+
createMatrix(1, 0, 0, 1, 0, 0),
|
|
2661
|
+
new Uint8Array([4, 8]),
|
|
2662
|
+
record.toBytes(),
|
|
2549
2663
|
);
|
|
2664
|
+
const diagnostics = collectImportDiagnostics((sink) => {
|
|
2665
|
+
expect(
|
|
2666
|
+
createScene2DFromSwf(createSwf([createTag(TAG_DEFINE_TEXT, text), createTag(TAG_END)]), sink),
|
|
2667
|
+
).not.toBeNull();
|
|
2668
|
+
});
|
|
2550
2669
|
|
|
2551
|
-
const
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
expect(
|
|
2555
|
-
expect(result!.appearances).toEqual([
|
|
2556
|
-
{ advancedBlendMode: AdvancedBlendMode.Overlay, effects: [], frame: 1, node: overlaid },
|
|
2557
|
-
]);
|
|
2670
|
+
const dropped = diagnostics.filter((entry) => entry.kind === 'swf.text-shape-uncomposable');
|
|
2671
|
+
expect(dropped).toHaveLength(1);
|
|
2672
|
+
expect(dropped[0].severity).toBe(ImportDiagnosticSeverity.Drop);
|
|
2673
|
+
expect(dropped[0].detail).toEqual({ capability: 'swf.text.define-text', characterId: 6 });
|
|
2558
2674
|
});
|
|
2559
2675
|
|
|
2560
|
-
it('
|
|
2561
|
-
const
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
]),
|
|
2676
|
+
it('stays silent about a static text body that composes, so the drop entry carries information', () => {
|
|
2677
|
+
const glyphBytes = new Uint8Array([0x30, 0x28, 0x00, 0x00, 0x40, 0x00]);
|
|
2678
|
+
const font = joinBytes(uint16(4), uint16(2), glyphBytes);
|
|
2679
|
+
const record = new BitWriter();
|
|
2680
|
+
record.writeUnsigned(1, 1);
|
|
2681
|
+
record.writeUnsigned(0, 3);
|
|
2682
|
+
record.writeUnsigned(1, 1);
|
|
2683
|
+
record.writeUnsigned(1, 1);
|
|
2684
|
+
record.writeUnsigned(0, 1);
|
|
2685
|
+
record.writeUnsigned(0, 1);
|
|
2686
|
+
const text = joinBytes(
|
|
2687
|
+
uint16(6),
|
|
2688
|
+
createRectangle(0, 1024, 0, 1024),
|
|
2689
|
+
createMatrix(1, 0, 0, 1, 0, 0),
|
|
2690
|
+
new Uint8Array([4, 8]),
|
|
2691
|
+
record.toBytes(),
|
|
2692
|
+
uint16(4),
|
|
2693
|
+
new Uint8Array([0xff, 0x00, 0x00]),
|
|
2694
|
+
uint16(1024),
|
|
2695
|
+
new Uint8Array([1]),
|
|
2696
|
+
packGlyphEntry(0, 600),
|
|
2697
|
+
new Uint8Array([0]),
|
|
2578
2698
|
);
|
|
2699
|
+
let document: ReturnType<typeof createScene2DFromSwf> = null;
|
|
2700
|
+
const diagnostics = collectImportDiagnostics((sink) => {
|
|
2701
|
+
document = createScene2DFromSwf(
|
|
2702
|
+
createSwf([
|
|
2703
|
+
createTag(TAG_DEFINE_FONT, font),
|
|
2704
|
+
createTag(TAG_DEFINE_TEXT, text),
|
|
2705
|
+
createTag(TAG_PLACE_OBJECT_2, joinBytes(new Uint8Array([PLACE_HAS_CHARACTER]), uint16(1), uint16(6))),
|
|
2706
|
+
createTag(TAG_SHOW_FRAME),
|
|
2707
|
+
createTag(TAG_END),
|
|
2708
|
+
]),
|
|
2709
|
+
sink,
|
|
2710
|
+
);
|
|
2711
|
+
});
|
|
2579
2712
|
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
expect(
|
|
2713
|
+
// The silence is only informative if the composition it is silent about actually happened: a run
|
|
2714
|
+
// that composed nothing would be silent for the wrong reason and prove nothing.
|
|
2715
|
+
expect((getNodeChildren(document!.root)[0] as Shape).kind).toBe(ShapeKind);
|
|
2716
|
+
expect(diagnostics.filter((entry) => entry.kind === 'swf.text-shape-uncomposable')).toEqual([]);
|
|
2583
2717
|
});
|
|
2584
2718
|
|
|
2585
|
-
it('reports
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
uint16(7),
|
|
2594
|
-
swfString('blurred'),
|
|
2595
|
-
new Uint8Array([1, 1]),
|
|
2596
|
-
uint32(4 * FIXED_16_ONE),
|
|
2597
|
-
uint32(2 * FIXED_16_ONE),
|
|
2598
|
-
new Uint8Array([0]),
|
|
2599
|
-
),
|
|
2600
|
-
),
|
|
2601
|
-
createTag(TAG_SHOW_FRAME),
|
|
2602
|
-
createTag(TAG_END),
|
|
2603
|
-
]),
|
|
2604
|
-
);
|
|
2719
|
+
it('reports an edit text body that does not parse, which otherwise loses the field with no signal', () => {
|
|
2720
|
+
// HasFont is set and the font id and height that must follow it are not there.
|
|
2721
|
+
const field = joinBytes(uint16(12), createRectangle(0, 4000, 0, 800), new Uint8Array([0x80 | 0x01, 0x00]));
|
|
2722
|
+
const diagnostics = collectImportDiagnostics((sink) => {
|
|
2723
|
+
expect(
|
|
2724
|
+
createScene2DFromSwf(createSwf([createTag(TAG_DEFINE_EDIT_TEXT, field), createTag(TAG_END)]), sink),
|
|
2725
|
+
).not.toBeNull();
|
|
2726
|
+
});
|
|
2605
2727
|
|
|
2606
|
-
const
|
|
2607
|
-
expect(
|
|
2608
|
-
expect(
|
|
2609
|
-
expect(
|
|
2728
|
+
const dropped = diagnostics.filter((entry) => entry.kind === 'swf.edit-text-unparseable');
|
|
2729
|
+
expect(dropped).toHaveLength(1);
|
|
2730
|
+
expect(dropped[0].severity).toBe(ImportDiagnosticSeverity.Drop);
|
|
2731
|
+
expect(dropped[0].detail).toEqual({ capability: 'swf.text.define-edit-text', characterId: 12 });
|
|
2610
2732
|
});
|
|
2611
2733
|
|
|
2612
|
-
it('
|
|
2613
|
-
const
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2734
|
+
it('stays silent about an edit text body that parses, so the drop entry carries information', () => {
|
|
2735
|
+
const field = joinBytes(
|
|
2736
|
+
uint16(12),
|
|
2737
|
+
createRectangle(0, 4000, 0, 800),
|
|
2738
|
+
new Uint8Array([0x80 | 0x40 | 0x20 | 0x04 | 0x01, 0x20 | 0x08]),
|
|
2739
|
+
uint16(7),
|
|
2740
|
+
uint16(240),
|
|
2741
|
+
new Uint8Array([0x11, 0x22, 0x33, 0xff]),
|
|
2742
|
+
new Uint8Array([2]),
|
|
2743
|
+
uint16(20),
|
|
2744
|
+
uint16(40),
|
|
2745
|
+
uint16(60),
|
|
2746
|
+
uint16(80),
|
|
2747
|
+
swfString('varName'),
|
|
2748
|
+
swfString('Hello'),
|
|
2749
|
+
);
|
|
2750
|
+
let document: ReturnType<typeof createScene2DFromSwf> = null;
|
|
2751
|
+
const diagnostics = collectImportDiagnostics((sink) => {
|
|
2752
|
+
document = createScene2DFromSwf(
|
|
2753
|
+
createSwf([
|
|
2754
|
+
createTag(TAG_DEFINE_EDIT_TEXT, field),
|
|
2755
|
+
createTag(TAG_PLACE_OBJECT_2, joinBytes(new Uint8Array([PLACE_HAS_CHARACTER]), uint16(1), uint16(12))),
|
|
2756
|
+
createTag(TAG_SHOW_FRAME),
|
|
2757
|
+
createTag(TAG_END),
|
|
2758
|
+
]),
|
|
2759
|
+
sink,
|
|
2760
|
+
);
|
|
2761
|
+
});
|
|
2762
|
+
|
|
2763
|
+
// Non-vacuous: a run that parsed no edit text would be silent for the wrong reason.
|
|
2764
|
+
expect(getNodeChildren(document!.root)).toHaveLength(1);
|
|
2765
|
+
expect(diagnostics.filter((entry) => entry.kind === 'swf.edit-text-unparseable')).toEqual([]);
|
|
2766
|
+
});
|
|
2767
|
+
|
|
2768
|
+
it('reports an ABC blob that yields no frame scripts, naming which of the two DoABC forms it was', () => {
|
|
2769
|
+
// A DoABC payload whose ABC body is not readable. The two tag forms are separate capabilities, so
|
|
2770
|
+
// the entry has to name which one it was or it cannot be joined to either.
|
|
2771
|
+
const named = joinBytes(uint32(0), swfString('frame'), new Uint8Array([0xff, 0xff, 0xff, 0xff]));
|
|
2772
|
+
const diagnostics = collectImportDiagnostics((sink) => {
|
|
2773
|
+
expect(createScene2DFromSwf(createSwf([createTag(TAG_DO_ABC, named), createTag(TAG_END)]), sink)).not.toBeNull();
|
|
2774
|
+
});
|
|
2775
|
+
|
|
2776
|
+
const dropped = diagnostics.filter((entry) => entry.kind === 'swf.abc-frame-scripts-unreadable');
|
|
2777
|
+
expect(dropped).toHaveLength(1);
|
|
2778
|
+
expect(dropped[0].severity).toBe(ImportDiagnosticSeverity.Drop);
|
|
2779
|
+
expect(dropped[0].detail).toEqual({ capability: 'swf.script.do-abc' });
|
|
2780
|
+
});
|
|
2781
|
+
|
|
2782
|
+
it('names the anonymous DoABC form separately, since the two are different capabilities', () => {
|
|
2783
|
+
const anonymous = new Uint8Array([0xff, 0xff, 0xff, 0xff]);
|
|
2784
|
+
const diagnostics = collectImportDiagnostics((sink) => {
|
|
2785
|
+
expect(
|
|
2786
|
+
createScene2DFromSwf(createSwf([createTag(TAG_DO_ABC_ANONYMOUS, anonymous), createTag(TAG_END)]), sink),
|
|
2787
|
+
).not.toBeNull();
|
|
2788
|
+
});
|
|
2789
|
+
|
|
2790
|
+
const dropped = diagnostics.filter((entry) => entry.kind === 'swf.abc-frame-scripts-unreadable');
|
|
2791
|
+
expect(dropped).toHaveLength(1);
|
|
2792
|
+
expect(dropped[0].detail).toEqual({ capability: 'swf.script.do-abc-anonymous' });
|
|
2793
|
+
});
|
|
2794
|
+
|
|
2795
|
+
it('reports a declared blend mode left unread behind a filter list that did not finish', () => {
|
|
2796
|
+
// Filter id 0xff has no known payload width, so the list cannot be walked to its end and the blend
|
|
2797
|
+
// byte behind it is unreachable. The placement keeps everything else, which is why this is silent.
|
|
2798
|
+
const diagnostics = collectImportDiagnostics((sink) => {
|
|
2799
|
+
expect(
|
|
2800
|
+
createScene2DFromSwf(
|
|
2801
|
+
createSwf([
|
|
2802
|
+
createTag(
|
|
2803
|
+
TAG_PLACE_OBJECT_3,
|
|
2804
|
+
joinBytes(
|
|
2805
|
+
new Uint8Array([PLACE_HAS_NAME | PLACE_HAS_CHARACTER, PLACE3_HAS_FILTER_LIST | PLACE3_HAS_BLEND_MODE]),
|
|
2806
|
+
uint16(3),
|
|
2807
|
+
uint16(7),
|
|
2808
|
+
swfString('filtered'),
|
|
2809
|
+
new Uint8Array([1, 0xff]),
|
|
2810
|
+
new Uint8Array([SWF_BLEND_MULTIPLY]),
|
|
2811
|
+
),
|
|
2812
|
+
),
|
|
2813
|
+
createTag(TAG_SHOW_FRAME),
|
|
2814
|
+
createTag(TAG_END),
|
|
2815
|
+
]),
|
|
2816
|
+
sink,
|
|
2817
|
+
),
|
|
2818
|
+
).not.toBeNull();
|
|
2819
|
+
});
|
|
2820
|
+
|
|
2821
|
+
const dropped = diagnostics.filter((entry) => entry.kind === 'swf.blend-mode-behind-unread-filters');
|
|
2822
|
+
expect(dropped).toHaveLength(1);
|
|
2823
|
+
expect(dropped[0].severity).toBe(ImportDiagnosticSeverity.Drop);
|
|
2824
|
+
expect(dropped[0].detail).toEqual({ capability: 'swf.placement.blend-mode' });
|
|
2825
|
+
});
|
|
2826
|
+
|
|
2827
|
+
it('stays silent when a declared blend mode is reachable, so the drop entry carries information', () => {
|
|
2828
|
+
let document: ReturnType<typeof createScene2DFromSwf> = null;
|
|
2829
|
+
const diagnostics = collectImportDiagnostics((sink) => {
|
|
2830
|
+
document = createScene2DFromSwf(
|
|
2831
|
+
createSwf([
|
|
2832
|
+
createTag(
|
|
2833
|
+
TAG_PLACE_OBJECT_3,
|
|
2834
|
+
joinBytes(
|
|
2835
|
+
new Uint8Array([PLACE_HAS_NAME | PLACE_HAS_CHARACTER, PLACE3_HAS_BLEND_MODE]),
|
|
2836
|
+
uint16(3),
|
|
2837
|
+
uint16(7),
|
|
2838
|
+
swfString('multiplied'),
|
|
2839
|
+
new Uint8Array([SWF_BLEND_MULTIPLY]),
|
|
2840
|
+
),
|
|
2841
|
+
),
|
|
2842
|
+
createTag(TAG_SHOW_FRAME),
|
|
2843
|
+
createTag(TAG_END),
|
|
2844
|
+
]),
|
|
2845
|
+
sink,
|
|
2846
|
+
);
|
|
2847
|
+
});
|
|
2848
|
+
|
|
2849
|
+
// Non-vacuous: the blend mode was actually read and applied.
|
|
2850
|
+
expect(document!.slots[0].target.blendMode).toBe(BlendMode.Multiply);
|
|
2851
|
+
expect(diagnostics.filter((entry) => entry.kind === 'swf.blend-mode-behind-unread-filters')).toEqual([]);
|
|
2852
|
+
});
|
|
2853
|
+
|
|
2854
|
+
it('counts the children a sprite bounds union could not include, since the box survives smaller', () => {
|
|
2855
|
+
// Character 7 is defined and character 9 is not. The union covers 7 alone, so the sprite's authored
|
|
2856
|
+
// box is real and short — the diminished case a count catches and an existence check cannot.
|
|
2857
|
+
const diagnostics = collectImportDiagnostics((sink) => {
|
|
2858
|
+
expect(
|
|
2859
|
+
createScene2DFromSwf(
|
|
2860
|
+
createSwf([
|
|
2861
|
+
createTag(TAG_DEFINE_SHAPE, joinBytes(uint16(7), createRectangle(0, 200, 0, 100))),
|
|
2862
|
+
createTag(
|
|
2863
|
+
TAG_DEFINE_SPRITE,
|
|
2864
|
+
joinBytes(
|
|
2865
|
+
uint16(20),
|
|
2866
|
+
uint16(1),
|
|
2867
|
+
createTag(TAG_PLACE_OBJECT_2, joinBytes(new Uint8Array([PLACE_HAS_CHARACTER]), uint16(1), uint16(7))),
|
|
2868
|
+
createTag(TAG_PLACE_OBJECT_2, joinBytes(new Uint8Array([PLACE_HAS_CHARACTER]), uint16(2), uint16(9))),
|
|
2869
|
+
createTag(TAG_SHOW_FRAME),
|
|
2870
|
+
createTag(TAG_END),
|
|
2871
|
+
),
|
|
2872
|
+
),
|
|
2873
|
+
createTag(TAG_PLACE_OBJECT_2, joinBytes(new Uint8Array([PLACE_HAS_CHARACTER]), uint16(1), uint16(20))),
|
|
2874
|
+
createTag(TAG_SHOW_FRAME),
|
|
2875
|
+
createTag(TAG_END),
|
|
2876
|
+
]),
|
|
2877
|
+
sink,
|
|
2878
|
+
),
|
|
2879
|
+
).not.toBeNull();
|
|
2880
|
+
});
|
|
2881
|
+
|
|
2882
|
+
const dropped = diagnostics.filter((entry) => entry.kind === 'swf.sprite-bounds-short');
|
|
2883
|
+
expect(dropped).toHaveLength(1);
|
|
2884
|
+
expect(dropped[0].severity).toBe(ImportDiagnosticSeverity.Drop);
|
|
2885
|
+
expect(dropped[0].detail).toEqual({
|
|
2886
|
+
capability: 'swf.timeline.define-sprite',
|
|
2887
|
+
characterId: 20,
|
|
2888
|
+
missingChildren: 1,
|
|
2889
|
+
});
|
|
2890
|
+
});
|
|
2891
|
+
|
|
2892
|
+
it('stays silent when every child of a sprite contributes its bounds', () => {
|
|
2893
|
+
let document: ReturnType<typeof createScene2DFromSwf> = null;
|
|
2894
|
+
const diagnostics = collectImportDiagnostics((sink) => {
|
|
2895
|
+
document = createScene2DFromSwf(
|
|
2896
|
+
createSwf([
|
|
2897
|
+
createTag(TAG_DEFINE_SHAPE, joinBytes(uint16(7), createRectangle(0, 200, 0, 100))),
|
|
2898
|
+
createTag(
|
|
2899
|
+
TAG_DEFINE_SPRITE,
|
|
2900
|
+
joinBytes(
|
|
2901
|
+
uint16(20),
|
|
2902
|
+
uint16(1),
|
|
2903
|
+
createTag(TAG_PLACE_OBJECT_2, joinBytes(new Uint8Array([PLACE_HAS_CHARACTER]), uint16(1), uint16(7))),
|
|
2904
|
+
createTag(TAG_SHOW_FRAME),
|
|
2905
|
+
createTag(TAG_END),
|
|
2906
|
+
),
|
|
2907
|
+
),
|
|
2908
|
+
createTag(TAG_PLACE_OBJECT_2, joinBytes(new Uint8Array([PLACE_HAS_CHARACTER]), uint16(1), uint16(20))),
|
|
2909
|
+
createTag(TAG_SHOW_FRAME),
|
|
2910
|
+
createTag(TAG_END),
|
|
2911
|
+
]),
|
|
2912
|
+
sink,
|
|
2913
|
+
);
|
|
2914
|
+
});
|
|
2915
|
+
|
|
2916
|
+
// Non-vacuous: the sprite exists and its union was actually computed.
|
|
2917
|
+
expect(getNodeChildren(document!.root)).toHaveLength(1);
|
|
2918
|
+
expect(diagnostics.filter((entry) => entry.kind === 'swf.sprite-bounds-short')).toEqual([]);
|
|
2919
|
+
});
|
|
2920
|
+
|
|
2921
|
+
it('reports an advanced blend that had no node to carry it, which the appearance report alone holds', () => {
|
|
2922
|
+
// An unnamed placement of a character that was never defined earns no node, so the appearance report
|
|
2923
|
+
// — the only carrier for advanced blends and filter lists — has nothing to attach to.
|
|
2924
|
+
const diagnostics = collectImportDiagnostics((sink) => {
|
|
2925
|
+
expect(
|
|
2926
|
+
createScene2DFromSwf(
|
|
2927
|
+
createSwf([
|
|
2928
|
+
createTag(
|
|
2929
|
+
TAG_PLACE_OBJECT_3,
|
|
2930
|
+
joinBytes(
|
|
2931
|
+
new Uint8Array([PLACE_HAS_CHARACTER, PLACE3_HAS_BLEND_MODE]),
|
|
2932
|
+
uint16(1),
|
|
2933
|
+
uint16(7),
|
|
2934
|
+
new Uint8Array([SWF_BLEND_OVERLAY]),
|
|
2935
|
+
),
|
|
2936
|
+
),
|
|
2937
|
+
createTag(TAG_SHOW_FRAME),
|
|
2938
|
+
createTag(TAG_END),
|
|
2939
|
+
]),
|
|
2940
|
+
sink,
|
|
2941
|
+
),
|
|
2942
|
+
).not.toBeNull();
|
|
2943
|
+
});
|
|
2944
|
+
|
|
2945
|
+
const dropped = diagnostics.filter((entry) => entry.kind === 'swf.appearance-without-node');
|
|
2946
|
+
expect(dropped).toHaveLength(1);
|
|
2947
|
+
expect(dropped[0].severity).toBe(ImportDiagnosticSeverity.Drop);
|
|
2948
|
+
expect(dropped[0].detail).toEqual({ capability: 'swf.placement.blend-mode', frame: 1 });
|
|
2949
|
+
});
|
|
2950
|
+
|
|
2951
|
+
it('stays silent when an advanced blend has a node to carry it, so the drop entry carries information', () => {
|
|
2952
|
+
const diagnostics = collectImportDiagnostics((sink) => {
|
|
2953
|
+
const result = createScene2DImportFromSwf(
|
|
2954
|
+
createSwf([
|
|
2955
|
+
createTag(
|
|
2956
|
+
TAG_PLACE_OBJECT_3,
|
|
2957
|
+
joinBytes(
|
|
2958
|
+
new Uint8Array([PLACE_HAS_NAME | PLACE_HAS_CHARACTER, PLACE3_HAS_BLEND_MODE]),
|
|
2959
|
+
uint16(3),
|
|
2960
|
+
uint16(7),
|
|
2961
|
+
swfString('overlaid'),
|
|
2962
|
+
new Uint8Array([SWF_BLEND_OVERLAY]),
|
|
2963
|
+
),
|
|
2964
|
+
),
|
|
2965
|
+
createTag(TAG_SHOW_FRAME),
|
|
2966
|
+
createTag(TAG_END),
|
|
2967
|
+
]),
|
|
2968
|
+
sink,
|
|
2969
|
+
);
|
|
2970
|
+
// Non-vacuous: the appearance really was recorded against a node.
|
|
2971
|
+
expect(result!.appearances).toHaveLength(1);
|
|
2972
|
+
});
|
|
2973
|
+
|
|
2974
|
+
expect(diagnostics.filter((entry) => entry.kind === 'swf.appearance-without-node')).toEqual([]);
|
|
2975
|
+
});
|
|
2976
|
+
|
|
2977
|
+
it('reports a reused font character id, the one case where the document imports and is simply wrong', () => {
|
|
2978
|
+
// Two DefineFont tags claim id 4. The second replaces the first; the document imports, the font
|
|
2979
|
+
// exists, and it is the wrong font — the substituted case no existence check and no count can see.
|
|
2980
|
+
const glyphBytes = new Uint8Array([0x30, 0x28, 0x00, 0x00, 0x40, 0x00]);
|
|
2981
|
+
const font = joinBytes(uint16(4), uint16(2), glyphBytes);
|
|
2982
|
+
const diagnostics = collectImportDiagnostics((sink) => {
|
|
2983
|
+
expect(
|
|
2984
|
+
createScene2DFromSwf(
|
|
2985
|
+
createSwf([createTag(TAG_DEFINE_FONT, font), createTag(TAG_DEFINE_FONT, font), createTag(TAG_END)]),
|
|
2986
|
+
sink,
|
|
2987
|
+
),
|
|
2988
|
+
).not.toBeNull();
|
|
2989
|
+
});
|
|
2990
|
+
|
|
2991
|
+
const dropped = diagnostics.filter((entry) => entry.kind === 'swf.font-character-id-reused');
|
|
2992
|
+
expect(dropped).toHaveLength(1);
|
|
2993
|
+
expect(dropped[0].severity).toBe(ImportDiagnosticSeverity.Drop);
|
|
2994
|
+
expect(dropped[0].detail).toEqual({ capability: 'swf.font.define-font', characterId: 4 });
|
|
2995
|
+
});
|
|
2996
|
+
|
|
2997
|
+
it('stays silent about a font whose character id is used once, so the entry carries information', () => {
|
|
2998
|
+
const glyphBytes = new Uint8Array([0x30, 0x28, 0x00, 0x00, 0x40, 0x00]);
|
|
2999
|
+
const diagnostics = collectImportDiagnostics((sink) => {
|
|
3000
|
+
expect(
|
|
3001
|
+
createScene2DFromSwf(
|
|
3002
|
+
createSwf([
|
|
3003
|
+
createTag(TAG_DEFINE_FONT, joinBytes(uint16(4), uint16(2), glyphBytes)),
|
|
3004
|
+
createTag(TAG_DEFINE_FONT, joinBytes(uint16(5), uint16(2), glyphBytes)),
|
|
3005
|
+
createTag(TAG_END),
|
|
3006
|
+
]),
|
|
3007
|
+
sink,
|
|
3008
|
+
),
|
|
3009
|
+
).not.toBeNull();
|
|
3010
|
+
});
|
|
3011
|
+
|
|
3012
|
+
expect(diagnostics.filter((entry) => entry.kind === 'swf.font-character-id-reused')).toEqual([]);
|
|
3013
|
+
});
|
|
3014
|
+
|
|
3015
|
+
it('reports an edit text whose font id resolves to no name, leaving the field sized but unfamilied', () => {
|
|
3016
|
+
// The field keeps its box, colour and size and simply has no font family. Asserting the box survives
|
|
3017
|
+
// is what makes this the diminished case rather than a missing field.
|
|
3018
|
+
const field = joinBytes(
|
|
3019
|
+
uint16(12),
|
|
3020
|
+
createRectangle(0, 4000, 0, 800),
|
|
3021
|
+
new Uint8Array([0x80 | 0x40 | 0x20 | 0x04 | 0x01, 0x20 | 0x08]),
|
|
3022
|
+
uint16(7),
|
|
3023
|
+
uint16(240),
|
|
3024
|
+
new Uint8Array([0x11, 0x22, 0x33, 0xff]),
|
|
3025
|
+
new Uint8Array([2]),
|
|
3026
|
+
uint16(20),
|
|
3027
|
+
uint16(40),
|
|
3028
|
+
uint16(60),
|
|
3029
|
+
uint16(80),
|
|
3030
|
+
swfString('varName'),
|
|
3031
|
+
swfString('Hello'),
|
|
3032
|
+
);
|
|
3033
|
+
let document: ReturnType<typeof createScene2DFromSwf> = null;
|
|
3034
|
+
const diagnostics = collectImportDiagnostics((sink) => {
|
|
3035
|
+
document = createScene2DFromSwf(
|
|
3036
|
+
createSwf([
|
|
3037
|
+
createTag(TAG_DEFINE_EDIT_TEXT, field),
|
|
3038
|
+
createTag(TAG_PLACE_OBJECT_2, joinBytes(new Uint8Array([PLACE_HAS_CHARACTER]), uint16(1), uint16(12))),
|
|
3039
|
+
createTag(TAG_SHOW_FRAME),
|
|
3040
|
+
createTag(TAG_END),
|
|
3041
|
+
]),
|
|
3042
|
+
sink,
|
|
3043
|
+
);
|
|
3044
|
+
});
|
|
3045
|
+
|
|
3046
|
+
expect(getNodeChildren(document!.root)).toHaveLength(1);
|
|
3047
|
+
const dropped = diagnostics.filter((entry) => entry.kind === 'swf.edit-text-font-name-unresolved');
|
|
3048
|
+
expect(dropped).toHaveLength(1);
|
|
3049
|
+
expect(dropped[0].severity).toBe(ImportDiagnosticSeverity.Drop);
|
|
3050
|
+
expect(dropped[0].detail).toEqual({ capability: 'swf.text.define-edit-text', fontId: 7 });
|
|
3051
|
+
});
|
|
3052
|
+
|
|
3053
|
+
it('reports a declined frame script through the full import path, not only the reader in isolation', () => {
|
|
3054
|
+
// A fire proof at the reader shows the wire fires; it does not show production reaches the branch.
|
|
3055
|
+
// Family 8 was reachable only by a route other than the one predicted, so this asks the same question
|
|
3056
|
+
// of the frame-script wire by carrying a real DoABC payload through createScene2DFromSwf.
|
|
3057
|
+
const abc = buildFrameScriptAbc(1);
|
|
3058
|
+
const payload = joinBytes(uint32(0), swfString('frame'), abc);
|
|
3059
|
+
const diagnostics = collectImportDiagnostics((sink) => {
|
|
3060
|
+
expect(
|
|
3061
|
+
createScene2DFromSwf(createSwf([createTag(TAG_DO_ABC, payload), createTag(TAG_END)]), sink),
|
|
3062
|
+
).not.toBeNull();
|
|
3063
|
+
});
|
|
3064
|
+
|
|
3065
|
+
const dropped = diagnostics.filter((entry) => entry.kind === 'swf.abc-frame-script-declined');
|
|
3066
|
+
expect(dropped).toHaveLength(1);
|
|
3067
|
+
expect(dropped[0].detail).toEqual({ frame: 1, reason: 'commands-declined' });
|
|
3068
|
+
});
|
|
3069
|
+
|
|
3070
|
+
it('reports an undecodable morph for DefineMorphShape2, not only the generation the wire was written on', () => {
|
|
3071
|
+
// A wire carrying a version ternary is only proven for the branch a test exercises. This is the same
|
|
3072
|
+
// check already applied to the shape generations, applied to the morph wire.
|
|
3073
|
+
const body = joinBytes(
|
|
3074
|
+
uint16(7),
|
|
3075
|
+
createRectangle(0, 200, 0, 200),
|
|
3076
|
+
createRectangle(0, 400, 0, 400),
|
|
3077
|
+
createRectangle(0, 200, 0, 200),
|
|
3078
|
+
createRectangle(0, 400, 0, 400),
|
|
3079
|
+
new Uint8Array([0]),
|
|
3080
|
+
uint32(0xffff),
|
|
3081
|
+
new Uint8Array([0]),
|
|
3082
|
+
);
|
|
3083
|
+
const diagnostics = collectImportDiagnostics((sink) => {
|
|
3084
|
+
expect(
|
|
3085
|
+
createScene2DFromSwf(createSwf([createTag(TAG_DEFINE_MORPH_SHAPE_2, body), createTag(TAG_END)]), sink),
|
|
3086
|
+
).not.toBeNull();
|
|
3087
|
+
});
|
|
3088
|
+
|
|
3089
|
+
const dropped = diagnostics.filter((entry) => entry.kind === 'swf.morph-shape-undecodable');
|
|
3090
|
+
expect(dropped).toHaveLength(1);
|
|
3091
|
+
expect(dropped[0].detail).toEqual({ capability: 'swf.morph.define-morph-shape-2', characterId: 7 });
|
|
3092
|
+
});
|
|
3093
|
+
|
|
3094
|
+
it('reports an uncomposable body for DefineText2, not only the generation the wire was written on', () => {
|
|
3095
|
+
const record = new BitWriter();
|
|
3096
|
+
record.writeUnsigned(1, 1);
|
|
3097
|
+
record.writeUnsigned(0, 3);
|
|
3098
|
+
record.writeUnsigned(1, 1);
|
|
3099
|
+
record.writeUnsigned(1, 1);
|
|
3100
|
+
record.writeUnsigned(0, 1);
|
|
3101
|
+
record.writeUnsigned(0, 1);
|
|
3102
|
+
const text = joinBytes(
|
|
3103
|
+
uint16(6),
|
|
3104
|
+
createRectangle(0, 1024, 0, 1024),
|
|
3105
|
+
createMatrix(1, 0, 0, 1, 0, 0),
|
|
3106
|
+
new Uint8Array([4, 8]),
|
|
3107
|
+
record.toBytes(),
|
|
3108
|
+
);
|
|
3109
|
+
const diagnostics = collectImportDiagnostics((sink) => {
|
|
3110
|
+
expect(
|
|
3111
|
+
createScene2DFromSwf(createSwf([createTag(TAG_DEFINE_TEXT_2, text), createTag(TAG_END)]), sink),
|
|
3112
|
+
).not.toBeNull();
|
|
3113
|
+
});
|
|
3114
|
+
|
|
3115
|
+
const dropped = diagnostics.filter((entry) => entry.kind === 'swf.text-shape-uncomposable');
|
|
3116
|
+
expect(dropped).toHaveLength(1);
|
|
3117
|
+
expect(dropped[0].detail).toEqual({ capability: 'swf.text.define-text-2', characterId: 6 });
|
|
3118
|
+
});
|
|
3119
|
+
|
|
3120
|
+
it('reports a reused character id for DefineFont2, not only the generation the wire was written on', () => {
|
|
3121
|
+
// A glyph-less DefineFont2 still parses and still claims the id, which is all the duplicate needs.
|
|
3122
|
+
const font2 = joinBytes(uint16(4), new Uint8Array([0, 0, 0]), uint16(0), uint16(0));
|
|
3123
|
+
const diagnostics = collectImportDiagnostics((sink) => {
|
|
3124
|
+
expect(
|
|
3125
|
+
createScene2DFromSwf(
|
|
3126
|
+
createSwf([createTag(TAG_DEFINE_FONT_2, font2), createTag(TAG_DEFINE_FONT_2, font2), createTag(TAG_END)]),
|
|
3127
|
+
sink,
|
|
3128
|
+
),
|
|
3129
|
+
).not.toBeNull();
|
|
3130
|
+
});
|
|
3131
|
+
|
|
3132
|
+
const dropped = diagnostics.filter((entry) => entry.kind === 'swf.font-character-id-reused');
|
|
3133
|
+
expect(dropped).toHaveLength(1);
|
|
3134
|
+
expect(dropped[0].detail).toEqual({ capability: 'swf.font.define-font-2', characterId: 4 });
|
|
3135
|
+
});
|
|
3136
|
+
|
|
3137
|
+
it('stays silent when an edit text font id does resolve, so the unresolved entry carries information', () => {
|
|
3138
|
+
// Recorded earlier as an absent silence proof: it needs a DefineFont2 that parses, and none existed.
|
|
3139
|
+
// The version-routing work produced one — a glyph-less DefineFont2 parses and carries a name — so the
|
|
3140
|
+
// hole closes. Worth noting the fixture came from unrelated work rather than from trying harder.
|
|
3141
|
+
const font2 = joinBytes(uint16(7), new Uint8Array([0, 0, 5]), swfBytes('Arial'), uint16(0), uint16(0));
|
|
3142
|
+
const field = joinBytes(
|
|
3143
|
+
uint16(12),
|
|
3144
|
+
createRectangle(0, 4000, 0, 800),
|
|
3145
|
+
new Uint8Array([0x80 | 0x40 | 0x20 | 0x04 | 0x01, 0x20 | 0x08]),
|
|
3146
|
+
uint16(7),
|
|
3147
|
+
uint16(240),
|
|
3148
|
+
new Uint8Array([0x11, 0x22, 0x33, 0xff]),
|
|
3149
|
+
new Uint8Array([2]),
|
|
3150
|
+
uint16(20),
|
|
3151
|
+
uint16(40),
|
|
3152
|
+
uint16(60),
|
|
3153
|
+
uint16(80),
|
|
3154
|
+
swfString('varName'),
|
|
3155
|
+
swfString('Hello'),
|
|
3156
|
+
);
|
|
3157
|
+
let document: ReturnType<typeof createScene2DFromSwf> = null;
|
|
3158
|
+
const diagnostics = collectImportDiagnostics((sink) => {
|
|
3159
|
+
document = createScene2DFromSwf(
|
|
3160
|
+
createSwf([
|
|
3161
|
+
createTag(TAG_DEFINE_FONT_2, font2),
|
|
3162
|
+
createTag(TAG_DEFINE_EDIT_TEXT, field),
|
|
3163
|
+
createTag(TAG_PLACE_OBJECT_2, joinBytes(new Uint8Array([PLACE_HAS_CHARACTER]), uint16(1), uint16(12))),
|
|
3164
|
+
createTag(TAG_SHOW_FRAME),
|
|
3165
|
+
createTag(TAG_END),
|
|
3166
|
+
]),
|
|
3167
|
+
sink,
|
|
3168
|
+
);
|
|
3169
|
+
});
|
|
3170
|
+
|
|
3171
|
+
// Non-vacuous: the field exists, so the resolver really ran against a font id that resolved.
|
|
3172
|
+
expect(getNodeChildren(document!.root)).toHaveLength(1);
|
|
3173
|
+
expect(diagnostics.filter((entry) => entry.kind === 'swf.edit-text-font-name-unresolved')).toEqual([]);
|
|
3174
|
+
});
|
|
3175
|
+
|
|
3176
|
+
it('stays silent about an anonymous DoABC it does obey, so the drop entry carries information', () => {
|
|
3177
|
+
// An anonymous DoABC carries the raw ABC with no flags-and-name header, so the shared builder is
|
|
3178
|
+
// enough on its own. Recorded earlier as an absent silence proof; the helper closed it.
|
|
3179
|
+
const diagnostics = collectImportDiagnostics((sink) => {
|
|
3180
|
+
expect(
|
|
3181
|
+
createScene2DFromSwf(
|
|
3182
|
+
createSwf([createTag(TAG_DO_ABC_ANONYMOUS, buildFrameScriptAbc()), createTag(TAG_END)]),
|
|
3183
|
+
sink,
|
|
3184
|
+
),
|
|
3185
|
+
).not.toBeNull();
|
|
3186
|
+
});
|
|
3187
|
+
|
|
3188
|
+
expect(diagnostics.filter((entry) => entry.kind === 'swf.abc-frame-scripts-unreadable')).toEqual([]);
|
|
3189
|
+
expect(diagnostics.filter((entry) => entry.kind === 'swf.abc-frame-script-declined')).toEqual([]);
|
|
3190
|
+
});
|
|
3191
|
+
|
|
3192
|
+
it('stays silent about a DefineText2 body that composes, so the drop entry carries information', () => {
|
|
3193
|
+
const glyphBytes = new Uint8Array([0x30, 0x28, 0x00, 0x00, 0x40, 0x00]);
|
|
3194
|
+
const font = joinBytes(uint16(4), uint16(2), glyphBytes);
|
|
3195
|
+
const record = new BitWriter();
|
|
3196
|
+
record.writeUnsigned(1, 1);
|
|
3197
|
+
record.writeUnsigned(0, 3);
|
|
3198
|
+
record.writeUnsigned(1, 1);
|
|
3199
|
+
record.writeUnsigned(1, 1);
|
|
3200
|
+
record.writeUnsigned(0, 1);
|
|
3201
|
+
record.writeUnsigned(0, 1);
|
|
3202
|
+
const text = joinBytes(
|
|
3203
|
+
uint16(6),
|
|
3204
|
+
createRectangle(0, 1024, 0, 1024),
|
|
3205
|
+
createMatrix(1, 0, 0, 1, 0, 0),
|
|
3206
|
+
new Uint8Array([4, 8]),
|
|
3207
|
+
record.toBytes(),
|
|
3208
|
+
uint16(4),
|
|
3209
|
+
// DefineText2 records carry RGBA where DefineText carries RGB — the version difference that makes
|
|
3210
|
+
// this a separate silence proof rather than the same one relabelled.
|
|
3211
|
+
new Uint8Array([0xff, 0x00, 0x00, 0xff]),
|
|
3212
|
+
uint16(1024),
|
|
3213
|
+
new Uint8Array([1]),
|
|
3214
|
+
packGlyphEntry(0, 600),
|
|
3215
|
+
new Uint8Array([0]),
|
|
3216
|
+
);
|
|
3217
|
+
let document: ReturnType<typeof createScene2DFromSwf> = null;
|
|
3218
|
+
const diagnostics = collectImportDiagnostics((sink) => {
|
|
3219
|
+
document = createScene2DFromSwf(
|
|
3220
|
+
createSwf([
|
|
3221
|
+
createTag(TAG_DEFINE_FONT, font),
|
|
3222
|
+
createTag(TAG_DEFINE_TEXT_2, text),
|
|
3223
|
+
createTag(TAG_PLACE_OBJECT_2, joinBytes(new Uint8Array([PLACE_HAS_CHARACTER]), uint16(1), uint16(6))),
|
|
3224
|
+
createTag(TAG_SHOW_FRAME),
|
|
3225
|
+
createTag(TAG_END),
|
|
3226
|
+
]),
|
|
3227
|
+
sink,
|
|
3228
|
+
);
|
|
3229
|
+
});
|
|
3230
|
+
|
|
3231
|
+
// Non-vacuous: a run that composed nothing would be silent for the wrong reason.
|
|
3232
|
+
expect((getNodeChildren(document!.root)[0] as Shape).kind).toBe(ShapeKind);
|
|
3233
|
+
expect(diagnostics.filter((entry) => entry.kind === 'swf.text-shape-uncomposable')).toEqual([]);
|
|
3234
|
+
});
|
|
3235
|
+
|
|
3236
|
+
it('reports each whole-document header rejection with its own cause, not one shared null', () => {
|
|
3237
|
+
// Every case below loses the entire document. Before these wires the caller got one null and could
|
|
3238
|
+
// not tell a truncated header from an unregistered decompressor, which IS reported upstream.
|
|
3239
|
+
const kindFor = (file: Uint8Array): readonly string[] =>
|
|
3240
|
+
collectImportDiagnostics((sink) => {
|
|
3241
|
+
expect(createScene2DFromSwf(file, sink)).toBeNull();
|
|
3242
|
+
}).map((entry) => entry.kind);
|
|
3243
|
+
|
|
3244
|
+
// Version 0 is invalid however well-formed the rest is.
|
|
3245
|
+
const zeroVersion = joinBytes(new Uint8Array([0x46, 0x57, 0x53, 0]), uint32(64), createRectangle(0, 10, 0, 10));
|
|
3246
|
+
expect(kindFor(zeroVersion)).toContain('swf.header-fields-invalid');
|
|
3247
|
+
|
|
3248
|
+
// A declared length that passes the minimum, and a stage RECT claiming 31 bits per field with far
|
|
3249
|
+
// too few bytes behind it inside that length.
|
|
3250
|
+
const noStage = joinBytes(new Uint8Array([0x46, 0x57, 0x53, 9]), uint32(12), new Uint8Array([0xf8, 0, 0, 0]));
|
|
3251
|
+
expect(kindFor(noStage)).toContain('swf.stage-bounds-unreadable');
|
|
3252
|
+
|
|
3253
|
+
// A stage RECT that reads, then no frame rate or frame count behind it.
|
|
3254
|
+
const stage = createRectangle(0, 2000, 0, 1000);
|
|
3255
|
+
const noFrameRate = joinBytes(new Uint8Array([0x46, 0x57, 0x53, 9]), uint32(8 + stage.length), stage);
|
|
3256
|
+
expect(kindFor(noFrameRate)).toContain('swf.header-truncated');
|
|
3257
|
+
});
|
|
3258
|
+
|
|
3259
|
+
it('reports a discarded JPEG alpha stream as a Drop, since the bytes are present and go unread', () => {
|
|
3260
|
+
const jpeg3 = createJpegHeader(23, 17);
|
|
3261
|
+
const file = createSwf([
|
|
3262
|
+
createTag(TAG_DEFINE_BITS_JPEG_3, joinBytes(uint16(16), uint32(jpeg3.length), jpeg3, new Uint8Array([1, 2, 3]))),
|
|
3263
|
+
createTag(TAG_SHOW_FRAME),
|
|
3264
|
+
createTag(TAG_END),
|
|
3265
|
+
]);
|
|
3266
|
+
const diagnostics = collectImportDiagnostics((sink) => {
|
|
3267
|
+
expect(createScene2DFromSwf(file, sink)).not.toBeNull();
|
|
3268
|
+
});
|
|
3269
|
+
|
|
3270
|
+
const dropped = diagnostics.filter((entry) => entry.kind === 'swf.jpeg-alpha-stream');
|
|
3271
|
+
expect(dropped).toHaveLength(1);
|
|
3272
|
+
expect(dropped[0].severity).toBe(ImportDiagnosticSeverity.Drop);
|
|
3273
|
+
expect(dropped[0].detail).toEqual({
|
|
3274
|
+
capability: 'swf.bitmap.define-bits-jpeg-3',
|
|
3275
|
+
characterId: 16,
|
|
3276
|
+
discardedBytes: 3,
|
|
3277
|
+
});
|
|
3278
|
+
});
|
|
3279
|
+
|
|
3280
|
+
it('reports a legacy split JPEG with no tables in the file as a Drop', () => {
|
|
3281
|
+
const file = createSwf([
|
|
3282
|
+
createTag(TAG_DEFINE_BITS, joinBytes(uint16(9), new Uint8Array([0xff, 0xd8, 0xff, 0xd9]))),
|
|
3283
|
+
createTag(TAG_SHOW_FRAME),
|
|
3284
|
+
createTag(TAG_END),
|
|
3285
|
+
]);
|
|
3286
|
+
const diagnostics = collectImportDiagnostics((sink) => {
|
|
3287
|
+
expect(createScene2DFromSwf(file, sink)).not.toBeNull();
|
|
3288
|
+
});
|
|
3289
|
+
|
|
3290
|
+
const dropped = diagnostics.filter((entry) => entry.kind === 'swf.jpeg-tables-missing');
|
|
3291
|
+
expect(dropped).toHaveLength(1);
|
|
3292
|
+
expect(dropped[0].severity).toBe(ImportDiagnosticSeverity.Drop);
|
|
3293
|
+
expect(dropped[0].detail).toEqual({ capability: 'swf.bitmap.define-bits-jpeg-tables', characterId: 9 });
|
|
3294
|
+
});
|
|
3295
|
+
|
|
3296
|
+
it('reports scene names as a Skip, since labels import and the named range has no subject', () => {
|
|
3297
|
+
const file = createSwf([
|
|
3298
|
+
createTag(
|
|
3299
|
+
TAG_DEFINE_SCENE_AND_FRAME_LABEL_DATA,
|
|
3300
|
+
joinBytes(encodedUint32(1), encodedUint32(0), swfString('Scene 1'), encodedUint32(0)),
|
|
3301
|
+
),
|
|
3302
|
+
createTag(TAG_SHOW_FRAME),
|
|
3303
|
+
createTag(TAG_END),
|
|
3304
|
+
]);
|
|
3305
|
+
const diagnostics = collectImportDiagnostics((sink) => {
|
|
3306
|
+
expect(createScene2DFromSwf(file, sink)).not.toBeNull();
|
|
3307
|
+
});
|
|
3308
|
+
|
|
3309
|
+
const skipped = diagnostics.filter((entry) => entry.kind === 'swf.scene-names');
|
|
3310
|
+
expect(skipped).toHaveLength(1);
|
|
3311
|
+
expect(skipped[0].severity).toBe(ImportDiagnosticSeverity.Skip);
|
|
3312
|
+
expect(skipped[0].detail).toEqual({
|
|
3313
|
+
capability: 'swf.timeline.define-scene-and-frame-label-data',
|
|
3314
|
+
sceneCount: 1,
|
|
3315
|
+
});
|
|
3316
|
+
});
|
|
3317
|
+
|
|
3318
|
+
it('reports a frame script declined for carrying more than playback commands', () => {
|
|
3319
|
+
// 0x96 push, then a non-playback action: the block is declined WHOLE, because honouring the legible
|
|
3320
|
+
// half would misrepresent the frame. The crumb is what tells a caller the frame had a script at all.
|
|
3321
|
+
const file = createSwf([
|
|
3322
|
+
createTag(TAG_DO_ACTION, new Uint8Array([0x96, 0x02, 0x00, 0x08, 0x00, 0x3d, 0x00])),
|
|
3323
|
+
createTag(TAG_SHOW_FRAME),
|
|
3324
|
+
createTag(TAG_END),
|
|
3325
|
+
]);
|
|
3326
|
+
const diagnostics = collectImportDiagnostics((sink) => {
|
|
3327
|
+
expect(createScene2DFromSwf(file, sink)).not.toBeNull();
|
|
3328
|
+
});
|
|
3329
|
+
|
|
3330
|
+
const skipped = diagnostics.filter((entry) => entry.kind === 'swf.frame-script-declined');
|
|
3331
|
+
expect(skipped).toHaveLength(1);
|
|
3332
|
+
expect(skipped[0].severity).toBe(ImportDiagnosticSeverity.Skip);
|
|
3333
|
+
expect(skipped[0].detail).toEqual({ capability: 'swf.script.do-action', frame: 1 });
|
|
3334
|
+
});
|
|
3335
|
+
|
|
3336
|
+
it('reports an init action declined the same way DoAction is, which it silently did not', () => {
|
|
3337
|
+
// The identical decline four lines above the DoAction case reported nothing. Found by the silent-drop
|
|
3338
|
+
// shape sweep, not by reading — three passes over this file read both branches and saw no asymmetry.
|
|
3339
|
+
const file = createSwf([
|
|
3340
|
+
createTag(TAG_DO_INIT_ACTION, joinBytes(uint16(20), new Uint8Array([0x96, 0x02, 0x00, 0x08, 0x00, 0x3d, 0x00]))),
|
|
3341
|
+
createTag(TAG_SHOW_FRAME),
|
|
3342
|
+
createTag(TAG_END),
|
|
3343
|
+
]);
|
|
3344
|
+
const diagnostics = collectImportDiagnostics((sink) => {
|
|
3345
|
+
expect(createScene2DFromSwf(file, sink)).not.toBeNull();
|
|
3346
|
+
});
|
|
3347
|
+
|
|
3348
|
+
const skipped = diagnostics.filter((entry) => entry.kind === 'swf.frame-script-declined');
|
|
3349
|
+
expect(skipped).toHaveLength(1);
|
|
3350
|
+
expect(skipped[0].severity).toBe(ImportDiagnosticSeverity.Skip);
|
|
3351
|
+
expect(skipped[0].detail).toEqual({ capability: 'swf.script.do-init-action', characterId: 20 });
|
|
3352
|
+
});
|
|
3353
|
+
|
|
3354
|
+
it('stays silent about an init action it does obey, so the skip entry carries information', () => {
|
|
3355
|
+
const file = createSwf([
|
|
3356
|
+
createTag(TAG_DO_INIT_ACTION, joinBytes(uint16(20), new Uint8Array([0x07, 0x00]))),
|
|
3357
|
+
createTag(TAG_SHOW_FRAME),
|
|
3358
|
+
createTag(TAG_END),
|
|
3359
|
+
]);
|
|
3360
|
+
const diagnostics = collectImportDiagnostics((sink) => {
|
|
3361
|
+
expect(createScene2DFromSwf(file, sink)).not.toBeNull();
|
|
3362
|
+
});
|
|
3363
|
+
|
|
3364
|
+
expect(diagnostics.filter((entry) => entry.kind === 'swf.frame-script-declined')).toEqual([]);
|
|
3365
|
+
});
|
|
3366
|
+
|
|
3367
|
+
it('reports nested masks collapsing, since the outer one is not applied at all', () => {
|
|
3368
|
+
// Two clip depths covering one instance: Flight carries one clip per node, so the outer mask is
|
|
3369
|
+
// simply not applied and the instance shows more than the file said. The crumb is the only signal.
|
|
3370
|
+
const file = createSwf([
|
|
3371
|
+
createTag(
|
|
3372
|
+
TAG_PLACE_OBJECT_2,
|
|
3373
|
+
joinBytes(new Uint8Array([PLACE_HAS_CLIP_DEPTH | PLACE_HAS_CHARACTER]), uint16(1), uint16(1), uint16(9)),
|
|
3374
|
+
),
|
|
3375
|
+
createTag(
|
|
3376
|
+
TAG_PLACE_OBJECT_2,
|
|
3377
|
+
joinBytes(new Uint8Array([PLACE_HAS_CLIP_DEPTH | PLACE_HAS_CHARACTER]), uint16(2), uint16(2), uint16(9)),
|
|
3378
|
+
),
|
|
3379
|
+
createTag(TAG_PLACE_OBJECT_2, joinBytes(new Uint8Array([PLACE_HAS_CHARACTER]), uint16(3), uint16(3))),
|
|
3380
|
+
createTag(TAG_SHOW_FRAME),
|
|
3381
|
+
createTag(TAG_END),
|
|
3382
|
+
]);
|
|
3383
|
+
const diagnostics = collectImportDiagnostics((sink) => {
|
|
3384
|
+
createScene2DFromSwf(file, sink);
|
|
3385
|
+
});
|
|
3386
|
+
|
|
3387
|
+
const collapsed = diagnostics.filter((entry) => entry.kind === 'swf.nested-mask-collapsed');
|
|
3388
|
+
expect(collapsed.length).toBeGreaterThan(0);
|
|
3389
|
+
expect(collapsed[0].severity).toBe(ImportDiagnosticSeverity.Skip);
|
|
3390
|
+
expect(collapsed[0].detail?.capability).toBe('swf.placement.clip-depth');
|
|
3391
|
+
expect(collapsed[0].detail?.covering).toBe(2);
|
|
3392
|
+
|
|
3393
|
+
// The masks name characters with no decoded geometry, so the same fixture exercises clip-depth's
|
|
3394
|
+
// other loss path. Both paths must be proven for the capability to count as instrumented.
|
|
3395
|
+
const unmasked = diagnostics.filter((entry) => entry.kind === 'swf.mask-without-geometry');
|
|
3396
|
+
expect(unmasked.length).toBeGreaterThan(0);
|
|
3397
|
+
expect(unmasked[0].severity).toBe(ImportDiagnosticSeverity.Recover);
|
|
3398
|
+
expect(unmasked[0].detail?.capability).toBe('swf.placement.clip-depth');
|
|
3399
|
+
});
|
|
3400
|
+
|
|
3401
|
+
it('reports a button interaction state other than up, which a still scene does not hold', () => {
|
|
3402
|
+
const file = createSwf([
|
|
3403
|
+
createTag(
|
|
3404
|
+
TAG_DEFINE_BUTTON_2,
|
|
3405
|
+
joinBytes(
|
|
3406
|
+
uint16(12),
|
|
3407
|
+
new Uint8Array([0, 0, 0]),
|
|
3408
|
+
new Uint8Array([0x02]),
|
|
3409
|
+
uint16(7),
|
|
3410
|
+
uint16(1),
|
|
3411
|
+
createRectangle(0, 0, 0, 0),
|
|
3412
|
+
new Uint8Array([0, 0]),
|
|
3413
|
+
),
|
|
3414
|
+
),
|
|
3415
|
+
createTag(TAG_SHOW_FRAME),
|
|
3416
|
+
createTag(TAG_END),
|
|
3417
|
+
]);
|
|
3418
|
+
const diagnostics = collectImportDiagnostics((sink) => {
|
|
3419
|
+
expect(createScene2DFromSwf(file, sink)).not.toBeNull();
|
|
3420
|
+
});
|
|
3421
|
+
|
|
3422
|
+
const skipped = diagnostics.filter((entry) => entry.kind === 'swf.button-interaction-state');
|
|
3423
|
+
expect(skipped.length).toBeGreaterThan(0);
|
|
3424
|
+
expect(skipped[0].severity).toBe(ImportDiagnosticSeverity.Skip);
|
|
3425
|
+
// The PAYLOAD, not just the trigger: a crumb that fires correctly while recording something false
|
|
3426
|
+
// is a clean positive that is not one. The fixture's non-up record names character 7 with state
|
|
3427
|
+
// flags 0x02, and the crumb must say so rather than merely existing.
|
|
3428
|
+
expect(skipped[0].detail).toEqual({ characterId: 7, flags: 0x02 });
|
|
3429
|
+
});
|
|
3430
|
+
|
|
3431
|
+
it('reports a legacy split JPEG whose halves will not splice into a readable image', () => {
|
|
3432
|
+
const file = createSwf([
|
|
3433
|
+
createTag(TAG_JPEG_TABLES, new Uint8Array([0xff, 0xd8, 0xff, 0xd9])),
|
|
3434
|
+
createTag(TAG_DEFINE_BITS, joinBytes(uint16(9), new Uint8Array([0x01, 0x02, 0x03, 0x04]))),
|
|
3435
|
+
createTag(TAG_SHOW_FRAME),
|
|
3436
|
+
createTag(TAG_END),
|
|
3437
|
+
]);
|
|
3438
|
+
const diagnostics = collectImportDiagnostics((sink) => {
|
|
3439
|
+
expect(createScene2DFromSwf(file, sink)).not.toBeNull();
|
|
3440
|
+
});
|
|
3441
|
+
|
|
3442
|
+
const dropped = diagnostics.filter((entry) => entry.kind === 'swf.jpeg-tables-unsplittable');
|
|
3443
|
+
expect(dropped).toHaveLength(1);
|
|
3444
|
+
expect(dropped[0].severity).toBe(ImportDiagnosticSeverity.Drop);
|
|
3445
|
+
expect(dropped[0].detail).toEqual({ capability: 'swf.bitmap.define-bits-jpeg-tables', characterId: 9 });
|
|
3446
|
+
});
|
|
3447
|
+
|
|
3448
|
+
it('reports a font whose glyph table does not decode, which costs the whole font not one glyph', () => {
|
|
3449
|
+
// Found by auditing my own trustworthy-silence claim rather than by a failing test: a font that
|
|
3450
|
+
// vanishes entirely and a font that imports cleanly both produced no crumb before this.
|
|
3451
|
+
const file = createSwf([
|
|
3452
|
+
createTag(TAG_DEFINE_FONT_2, joinBytes(uint16(4), new Uint8Array([0xff, 0xff, 0xff, 0xff]))),
|
|
3453
|
+
createTag(TAG_SHOW_FRAME),
|
|
3454
|
+
createTag(TAG_END),
|
|
3455
|
+
]);
|
|
3456
|
+
const diagnostics = collectImportDiagnostics((sink) => {
|
|
3457
|
+
expect(createScene2DFromSwf(file, sink)).not.toBeNull();
|
|
3458
|
+
});
|
|
3459
|
+
|
|
3460
|
+
const dropped = diagnostics.filter((entry) => entry.kind === 'swf.font-glyph-table');
|
|
3461
|
+
expect(dropped).toHaveLength(1);
|
|
3462
|
+
expect(dropped[0].severity).toBe(ImportDiagnosticSeverity.Drop);
|
|
3463
|
+
expect(dropped[0].detail).toEqual({ capability: 'swf.font.define-font-2', characterId: 4 });
|
|
3464
|
+
});
|
|
3465
|
+
|
|
3466
|
+
it('reports a non-MP3 sound stream, whose blocks do not concatenate', () => {
|
|
3467
|
+
// Stream format 1 (ADPCM) in the high nibble of the stream flags, with a non-zero samples-per-frame
|
|
3468
|
+
// so the header actually starts a stream. Only MP3 blocks concatenate, so the rest are a real loss.
|
|
3469
|
+
const file = createSwf([
|
|
3470
|
+
createTag(18, joinBytes(new Uint8Array([0, 1 << 4]), uint16(1152))),
|
|
3471
|
+
createTag(19, new Uint8Array([1, 2, 3, 4])),
|
|
3472
|
+
createTag(TAG_SHOW_FRAME),
|
|
3473
|
+
createTag(TAG_END),
|
|
3474
|
+
]);
|
|
3475
|
+
const diagnostics = collectImportDiagnostics((sink) => {
|
|
3476
|
+
expect(createScene2DFromSwf(file, sink)).not.toBeNull();
|
|
3477
|
+
});
|
|
3478
|
+
|
|
3479
|
+
const skipped = diagnostics.filter((entry) => entry.kind === 'swf.stream-sound-format');
|
|
3480
|
+
expect(skipped).toHaveLength(1);
|
|
3481
|
+
expect(skipped[0].severity).toBe(ImportDiagnosticSeverity.Skip);
|
|
3482
|
+
expect(skipped[0].detail).toEqual({ capability: 'swf.axis.sound-format-non-mp3', format: 1 });
|
|
3483
|
+
});
|
|
3484
|
+
|
|
3485
|
+
it('reports one glyph whose outline does not decode, which costs that glyph and not the font', () => {
|
|
3486
|
+
// Two glyphs: the first is a real square, the second is a body the shape reader cannot read. The
|
|
3487
|
+
// font still imports — that is the point of the Drop rather than a whole-font failure.
|
|
3488
|
+
const glyph = new ShapeWriter();
|
|
3489
|
+
glyph.writeStyleBits(1, 0);
|
|
3490
|
+
glyph.writeStyleChange({ fill1: 1, moveToX: 0, moveToY: 0 });
|
|
3491
|
+
glyph.writeStraightEdge(256, 0);
|
|
3492
|
+
glyph.writeStraightEdge(0, 256);
|
|
3493
|
+
glyph.writeStraightEdge(-256, 0);
|
|
3494
|
+
glyph.writeStraightEdge(0, -256);
|
|
3495
|
+
glyph.writeEndShape();
|
|
3496
|
+
const good = glyph.toBytes();
|
|
3497
|
+
const bad = new Uint8Array([0xff]);
|
|
3498
|
+
const offsets = joinBytes(uint16(6), uint16(6 + good.length), uint16(6 + good.length + bad.length));
|
|
3499
|
+
const file = createSwf([
|
|
3500
|
+
createTag(
|
|
3501
|
+
TAG_DEFINE_FONT_2,
|
|
3502
|
+
joinBytes(uint16(4), new Uint8Array([0, 0, 0]), uint16(2), offsets, good, bad, new Uint8Array([0x41, 0x42])),
|
|
3503
|
+
),
|
|
3504
|
+
createTag(TAG_SHOW_FRAME),
|
|
3505
|
+
createTag(TAG_END),
|
|
3506
|
+
]);
|
|
3507
|
+
const diagnostics = collectImportDiagnostics((sink) => {
|
|
3508
|
+
expect(createScene2DFromSwf(file, sink)).not.toBeNull();
|
|
3509
|
+
});
|
|
3510
|
+
|
|
3511
|
+
const dropped = diagnostics.filter((entry) => entry.kind === 'swf.font-glyph-outline');
|
|
3512
|
+
expect(dropped).toHaveLength(1);
|
|
3513
|
+
expect(dropped[0].severity).toBe(ImportDiagnosticSeverity.Drop);
|
|
3514
|
+
expect(dropped[0].detail?.capability).toBe('swf.font.define-font-2');
|
|
3515
|
+
expect(dropped[0].detail?.lostGlyphs).toBe(1);
|
|
3516
|
+
});
|
|
3517
|
+
|
|
3518
|
+
it('stays silent about a shape, a video stream and a scene table that lose nothing', () => {
|
|
3519
|
+
// A SILENCE PROOF IS VACUOUS UNLESS THE CAPABILITY WAS ACTUALLY EXERCISED — silence because the
|
|
3520
|
+
// feature was absent proves nothing about the wire. So each assertion below is paired with a positive
|
|
3521
|
+
// check that the construct really was imported.
|
|
3522
|
+
const writer = new ShapeWriter();
|
|
3523
|
+
writer.writeSolidFillStyles([0x3366cc]);
|
|
3524
|
+
writer.writeLineStyleCount(0);
|
|
3525
|
+
writer.writeStyleBits(1, 0);
|
|
3526
|
+
writer.writeStyleChange({ fill1: 1, moveToX: 0, moveToY: 0 });
|
|
3527
|
+
writer.writeStraightEdge(400, 0);
|
|
3528
|
+
writer.writeStraightEdge(0, 400);
|
|
3529
|
+
writer.writeStraightEdge(-400, 0);
|
|
3530
|
+
writer.writeStraightEdge(0, -400);
|
|
3531
|
+
writer.writeEndShape();
|
|
3532
|
+
|
|
3533
|
+
const file = createSwf([
|
|
3534
|
+
createTag(TAG_DEFINE_SHAPE, joinBytes(uint16(7), createRectangle(0, 400, 0, 400), writer.toBytes())),
|
|
3535
|
+
createTag(
|
|
3536
|
+
TAG_DEFINE_VIDEO_STREAM,
|
|
3537
|
+
joinBytes(uint16(4), uint16(1), uint16(16), uint16(16), new Uint8Array([0x01, 2])),
|
|
3538
|
+
),
|
|
3539
|
+
// A scene table with zero scenes still carries its label, so the tag is exercised and loses nothing.
|
|
3540
|
+
createTag(
|
|
3541
|
+
TAG_DEFINE_SCENE_AND_FRAME_LABEL_DATA,
|
|
3542
|
+
joinBytes(encodedUint32(0), encodedUint32(1), encodedUint32(0), swfString('start')),
|
|
3543
|
+
),
|
|
3544
|
+
createTag(TAG_PLACE_OBJECT_2, joinBytes(new Uint8Array([PLACE_HAS_CHARACTER]), uint16(1), uint16(7))),
|
|
3545
|
+
createTag(TAG_PLACE_OBJECT_2, joinBytes(new Uint8Array([PLACE_HAS_CHARACTER]), uint16(2), uint16(4))),
|
|
3546
|
+
createTag(TAG_SHOW_FRAME),
|
|
3547
|
+
createTag(TAG_END),
|
|
3548
|
+
]);
|
|
3549
|
+
|
|
3550
|
+
const diagnostics = collectImportDiagnostics((sink) => {
|
|
3551
|
+
const document = createScene2DFromSwf(file, sink);
|
|
3552
|
+
// The capabilities really were exercised: the shape drew, the video character materialized, and
|
|
3553
|
+
// the label survived.
|
|
3554
|
+
const children = getNodeChildren(document?.root as Node2D);
|
|
3555
|
+
expect(children.length).toBe(2);
|
|
3556
|
+
expect(getMovieClipCurrentLabel(document?.root as MovieClip)?.name).toBe('start');
|
|
3557
|
+
});
|
|
3558
|
+
|
|
3559
|
+
const kinds = diagnostics.map((entry) => entry.kind);
|
|
3560
|
+
expect(kinds).not.toContain('swf.shape-body-unreadable');
|
|
3561
|
+
expect(kinds).not.toContain('swf.video-frame-payload');
|
|
3562
|
+
expect(kinds).not.toContain('swf.scene-names');
|
|
3563
|
+
});
|
|
3564
|
+
|
|
3565
|
+
it('stays silent about a frame script and a mask that lose nothing', () => {
|
|
3566
|
+
const mask = new ShapeWriter();
|
|
3567
|
+
mask.writeSolidFillStyles([0xffffff]);
|
|
3568
|
+
mask.writeLineStyleCount(0);
|
|
3569
|
+
mask.writeStyleBits(1, 0);
|
|
3570
|
+
mask.writeStyleChange({ fill1: 1, moveToX: 0, moveToY: 0 });
|
|
3571
|
+
mask.writeStraightEdge(200, 0);
|
|
3572
|
+
mask.writeStraightEdge(0, 200);
|
|
3573
|
+
mask.writeStraightEdge(-200, 0);
|
|
3574
|
+
mask.writeStraightEdge(0, -200);
|
|
3575
|
+
mask.writeEndShape();
|
|
3576
|
+
|
|
3577
|
+
const file = createSwf([
|
|
3578
|
+
createTag(TAG_DEFINE_SHAPE, joinBytes(uint16(5), createRectangle(0, 200, 0, 200), mask.toBytes())),
|
|
3579
|
+
// One mask with real geometry over one instance: clip-depth is exercised and neither of its two
|
|
3580
|
+
// loss paths applies.
|
|
3581
|
+
createTag(
|
|
3582
|
+
TAG_PLACE_OBJECT_2,
|
|
3583
|
+
joinBytes(new Uint8Array([PLACE_HAS_CLIP_DEPTH | PLACE_HAS_CHARACTER]), uint16(1), uint16(5), uint16(3)),
|
|
3584
|
+
),
|
|
3585
|
+
createTag(TAG_PLACE_OBJECT_2, joinBytes(new Uint8Array([PLACE_HAS_CHARACTER]), uint16(2), uint16(5))),
|
|
3586
|
+
// A block that is ONLY playback commands is recognized whole, so nothing is declined.
|
|
3587
|
+
createTag(TAG_DO_ACTION, new Uint8Array([0x07, 0x00])),
|
|
3588
|
+
createTag(TAG_SHOW_FRAME),
|
|
3589
|
+
createTag(TAG_END),
|
|
3590
|
+
]);
|
|
3591
|
+
|
|
3592
|
+
const diagnostics = collectImportDiagnostics((sink) => {
|
|
3593
|
+
const document = createScene2DFromSwf(file, sink);
|
|
3594
|
+
expect(getMovieClipFrameScript(document?.root as MovieClip, 1)).not.toBeNull();
|
|
3595
|
+
});
|
|
3596
|
+
|
|
3597
|
+
const kinds = diagnostics.map((entry) => entry.kind);
|
|
3598
|
+
expect(kinds).not.toContain('swf.frame-script-declined');
|
|
3599
|
+
expect(kinds).not.toContain('swf.mask-without-geometry');
|
|
3600
|
+
expect(kinds).not.toContain('swf.nested-mask-collapsed');
|
|
3601
|
+
});
|
|
3602
|
+
|
|
3603
|
+
it('stays silent about a font, a spliced JPEG and a JPEG3 that lose nothing', () => {
|
|
3604
|
+
// The three capabilities that fire on the corpus without a silence proof yet. Each assertion is
|
|
3605
|
+
// paired with a positive check, so silence cannot come from the construct simply being absent.
|
|
3606
|
+
const glyph = new ShapeWriter();
|
|
3607
|
+
glyph.writeStyleBits(1, 0);
|
|
3608
|
+
glyph.writeStyleChange({ fill1: 1, moveToX: 0, moveToY: 0 });
|
|
3609
|
+
glyph.writeStraightEdge(256, 0);
|
|
3610
|
+
glyph.writeStraightEdge(0, 256);
|
|
3611
|
+
glyph.writeStraightEdge(-256, 0);
|
|
3612
|
+
glyph.writeStraightEdge(0, -256);
|
|
3613
|
+
glyph.writeEndShape();
|
|
3614
|
+
const glyphBytes = glyph.toBytes();
|
|
3615
|
+
const tables = new Uint8Array([0xff, 0xd8, 0xff, 0xdb, 0x00, 0x04, 0x11, 0x22, 0xff, 0xd9]);
|
|
3616
|
+
const jpeg3 = createJpegHeader(23, 17);
|
|
3617
|
+
|
|
3618
|
+
const file = createSwf([
|
|
3619
|
+
createTag(
|
|
3620
|
+
TAG_DEFINE_FONT_2,
|
|
3621
|
+
joinBytes(
|
|
3622
|
+
uint16(4),
|
|
3623
|
+
new Uint8Array([0, 0, 0]),
|
|
3624
|
+
uint16(1),
|
|
3625
|
+
joinBytes(uint16(4), uint16(4 + glyphBytes.length)),
|
|
3626
|
+
glyphBytes,
|
|
3627
|
+
new Uint8Array([0x41]),
|
|
3628
|
+
),
|
|
3629
|
+
),
|
|
3630
|
+
createTag(TAG_JPEG_TABLES, tables),
|
|
3631
|
+
createTag(TAG_DEFINE_BITS, joinBytes(uint16(9), createJpegHeader(11, 13))),
|
|
3632
|
+
// alphaDataOffset equal to the payload length: the colour stream is the whole body, so there is
|
|
3633
|
+
// no alpha block to discard and nothing is lost.
|
|
3634
|
+
createTag(TAG_DEFINE_BITS_JPEG_3, joinBytes(uint16(16), uint32(jpeg3.length), jpeg3)),
|
|
3635
|
+
// Placed, because an image earns a resource only when something samples it — without these the
|
|
3636
|
+
// silence below would be silence about constructs that never entered the document.
|
|
3637
|
+
createTag(TAG_PLACE_OBJECT_2, joinBytes(new Uint8Array([PLACE_HAS_CHARACTER]), uint16(1), uint16(9))),
|
|
3638
|
+
createTag(TAG_PLACE_OBJECT_2, joinBytes(new Uint8Array([PLACE_HAS_CHARACTER]), uint16(2), uint16(16))),
|
|
3639
|
+
createTag(TAG_SHOW_FRAME),
|
|
3640
|
+
createTag(TAG_END),
|
|
3641
|
+
]);
|
|
3642
|
+
|
|
3643
|
+
const diagnostics = collectImportDiagnostics((sink) => {
|
|
3644
|
+
const document = createScene2DFromSwf(file, sink);
|
|
3645
|
+
// All three constructs really imported: two image resources plus a recoverable font.
|
|
3646
|
+
expect(document?.imageResources.length).toBe(2);
|
|
3647
|
+
expect(createGlyphOutlineSourcesFromSwf(file)?.size).toBe(1);
|
|
3648
|
+
});
|
|
3649
|
+
|
|
3650
|
+
const kinds = diagnostics.map((entry) => entry.kind);
|
|
3651
|
+
expect(kinds).not.toContain('swf.font-glyph-table');
|
|
3652
|
+
expect(kinds).not.toContain('swf.font-glyph-outline');
|
|
3653
|
+
expect(kinds).not.toContain('swf.jpeg-tables-missing');
|
|
3654
|
+
expect(kinds).not.toContain('swf.jpeg-tables-unsplittable');
|
|
3655
|
+
expect(kinds).not.toContain('swf.jpeg-alpha-stream');
|
|
3656
|
+
});
|
|
3657
|
+
|
|
3658
|
+
it('stays silent about an MP3 stream, whose blocks do concatenate', () => {
|
|
3659
|
+
// The last capability without a silence proof, and the only one that never fires on the sampled
|
|
3660
|
+
// corpus — so it is last by the ordering rather than unimportant. Format 2 is MP3 in the high nibble.
|
|
3661
|
+
const file = createSwf([
|
|
3662
|
+
createTag(18, joinBytes(new Uint8Array([0, 2 << 4]), uint16(1152))),
|
|
3663
|
+
createTag(19, joinBytes(uint16(1), uint16(0), new Uint8Array([0xff, 0xfb, 0x90, 0x00]))),
|
|
3664
|
+
createTag(TAG_SHOW_FRAME),
|
|
3665
|
+
createTag(TAG_END),
|
|
3666
|
+
]);
|
|
3667
|
+
|
|
3668
|
+
const diagnostics = collectImportDiagnostics((sink) => {
|
|
3669
|
+
const document = createScene2DFromSwf(file, sink);
|
|
3670
|
+
// The stream really was carried: an MP3 head plus a block produces an audio resource.
|
|
3671
|
+
expect(document?.audioResources.length).toBe(1);
|
|
3672
|
+
});
|
|
3673
|
+
|
|
3674
|
+
expect(diagnostics.map((entry) => entry.kind)).not.toContain('swf.stream-sound-format');
|
|
3675
|
+
});
|
|
3676
|
+
|
|
3677
|
+
it('reports a label naming a frame the timeline never reaches, and stays silent when it does', () => {
|
|
3678
|
+
// The second way DefineSceneAndFrameLabelData loses data, found by auditing the CLAIM rather than
|
|
3679
|
+
// the wire: swf.scene-names covered the scene table only, while an out-of-range label was filtered
|
|
3680
|
+
// out silently and the capability still claimed trustworthy silence.
|
|
3681
|
+
const past = createSwf([
|
|
3682
|
+
createTag(
|
|
3683
|
+
TAG_DEFINE_SCENE_AND_FRAME_LABEL_DATA,
|
|
3684
|
+
joinBytes(encodedUint32(0), encodedUint32(1), encodedUint32(40), swfString('never')),
|
|
3685
|
+
),
|
|
3686
|
+
createTag(TAG_SHOW_FRAME),
|
|
3687
|
+
createTag(TAG_END),
|
|
3688
|
+
]);
|
|
3689
|
+
const dropped = collectImportDiagnostics((sink) => {
|
|
3690
|
+
expect(createScene2DFromSwf(past, sink)).not.toBeNull();
|
|
3691
|
+
}).filter((entry) => entry.kind === 'swf.label-past-last-frame');
|
|
3692
|
+
expect(dropped).toHaveLength(1);
|
|
3693
|
+
expect(dropped[0].severity).toBe(ImportDiagnosticSeverity.Drop);
|
|
3694
|
+
expect(dropped[0].detail).toEqual({ capability: 'swf.timeline.frame-label', dropped: 1, frames: 1 });
|
|
3695
|
+
|
|
3696
|
+
// A label the timeline does reach loses nothing, so no crumb — and it really did import.
|
|
3697
|
+
const reached = createSwf([
|
|
3698
|
+
createTag(
|
|
3699
|
+
TAG_DEFINE_SCENE_AND_FRAME_LABEL_DATA,
|
|
3700
|
+
joinBytes(encodedUint32(0), encodedUint32(1), encodedUint32(0), swfString('start')),
|
|
3701
|
+
),
|
|
3702
|
+
createTag(TAG_SHOW_FRAME),
|
|
3703
|
+
createTag(TAG_END),
|
|
3704
|
+
]);
|
|
3705
|
+
const quiet = collectImportDiagnostics((sink) => {
|
|
3706
|
+
const document = createScene2DFromSwf(reached, sink);
|
|
3707
|
+
expect(getMovieClipCurrentLabel(document?.root as MovieClip)?.name).toBe('start');
|
|
3708
|
+
});
|
|
3709
|
+
expect(quiet.map((entry) => entry.kind)).not.toContain('swf.label-past-last-frame');
|
|
3710
|
+
});
|
|
3711
|
+
|
|
3712
|
+
it('reports an unreadable body for every shape generation, not just the one the wire was written on', () => {
|
|
3713
|
+
// PROPERTY 4 APPLIED TO THE PROOF MAPPING: the shape wire was proven on DefineShape alone while the
|
|
3714
|
+
// mapping claimed all four generations. The code path is shared and parameterised by version, so what
|
|
3715
|
+
// was untested was the ROUTING — that each tag reaches it and reports its own capability id.
|
|
3716
|
+
const file = createSwf([
|
|
3717
|
+
createTag(TAG_DEFINE_SHAPE_2, joinBytes(uint16(2), createRectangle(0, 20, 0, 20), new Uint8Array([0xff]))),
|
|
3718
|
+
createTag(TAG_DEFINE_SHAPE_3, joinBytes(uint16(3), createRectangle(0, 20, 0, 20), new Uint8Array([0xff]))),
|
|
3719
|
+
createTag(
|
|
3720
|
+
TAG_DEFINE_SHAPE_4,
|
|
3721
|
+
joinBytes(uint16(4), createRectangle(0, 20, 0, 20), createRectangle(0, 20, 0, 20), new Uint8Array([0, 0xff])),
|
|
3722
|
+
),
|
|
3723
|
+
createTag(TAG_SHOW_FRAME),
|
|
3724
|
+
createTag(TAG_END),
|
|
3725
|
+
]);
|
|
3726
|
+
const diagnostics = collectImportDiagnostics((sink) => {
|
|
3727
|
+
expect(createScene2DFromSwf(file, sink)).not.toBeNull();
|
|
3728
|
+
});
|
|
3729
|
+
|
|
3730
|
+
const capabilities = diagnostics
|
|
3731
|
+
.filter((entry) => entry.kind === 'swf.shape-body-unreadable')
|
|
3732
|
+
.map((entry) => entry.detail?.capability);
|
|
3733
|
+
expect(capabilities).toEqual(['swf.shape.define-shape-2', 'swf.shape.define-shape-3', 'swf.shape.define-shape-4']);
|
|
3734
|
+
});
|
|
3735
|
+
|
|
3736
|
+
it('reports an unreadable glyph table for DefineFont and DefineFont3, not only DefineFont2', () => {
|
|
3737
|
+
const file = createSwf([
|
|
3738
|
+
createTag(TAG_DEFINE_FONT, joinBytes(uint16(6), new Uint8Array([0xff, 0xff]))),
|
|
3739
|
+
createTag(TAG_DEFINE_FONT_3, joinBytes(uint16(7), new Uint8Array([0xff, 0xff, 0xff, 0xff]))),
|
|
3740
|
+
createTag(TAG_SHOW_FRAME),
|
|
3741
|
+
createTag(TAG_END),
|
|
3742
|
+
]);
|
|
3743
|
+
const diagnostics = collectImportDiagnostics((sink) => {
|
|
3744
|
+
expect(createScene2DFromSwf(file, sink)).not.toBeNull();
|
|
3745
|
+
});
|
|
3746
|
+
|
|
3747
|
+
const capabilities = diagnostics
|
|
3748
|
+
.filter((entry) => entry.kind === 'swf.font-glyph-table')
|
|
3749
|
+
.map((entry) => entry.detail?.capability);
|
|
3750
|
+
expect(capabilities).toEqual(['swf.font.define-font', 'swf.font.define-font-3']);
|
|
3751
|
+
});
|
|
3752
|
+
|
|
3753
|
+
it('reports a discarded alpha stream for DefineBitsJPEG4, whose header differs from JPEG3', () => {
|
|
3754
|
+
const jpeg4 = createJpegHeader(31, 19);
|
|
3755
|
+
const file = createSwf([
|
|
3756
|
+
// JPEG4 carries a deblocking uint16 between the offset and the payload, so its routing is a
|
|
3757
|
+
// genuinely different path from JPEG3's and needs its own proof.
|
|
3758
|
+
createTag(
|
|
3759
|
+
TAG_DEFINE_BITS_JPEG_4,
|
|
3760
|
+
joinBytes(uint16(17), uint32(jpeg4.length + 2), uint16(0), jpeg4, new Uint8Array([4, 5, 6])),
|
|
3761
|
+
),
|
|
3762
|
+
createTag(TAG_SHOW_FRAME),
|
|
3763
|
+
createTag(TAG_END),
|
|
3764
|
+
]);
|
|
3765
|
+
const diagnostics = collectImportDiagnostics((sink) => {
|
|
3766
|
+
expect(createScene2DFromSwf(file, sink)).not.toBeNull();
|
|
3767
|
+
});
|
|
3768
|
+
|
|
3769
|
+
const dropped = diagnostics.filter((entry) => entry.kind === 'swf.jpeg-alpha-stream');
|
|
3770
|
+
expect(dropped).toHaveLength(1);
|
|
3771
|
+
expect(dropped[0].detail).toEqual({
|
|
3772
|
+
capability: 'swf.bitmap.define-bits-jpeg-4',
|
|
3773
|
+
characterId: 17,
|
|
3774
|
+
discardedBytes: 3,
|
|
3775
|
+
});
|
|
3776
|
+
});
|
|
3777
|
+
|
|
3778
|
+
it('names the symbol a caller asked for that the file does not export', () => {
|
|
3779
|
+
const file = createSwf([createTag(TAG_SHOW_FRAME), createTag(TAG_END)]);
|
|
3780
|
+
const diagnostics = collectImportDiagnostics((sink) => {
|
|
3781
|
+
expect(createScene2DSymbolFromSwf(file, 'absent', sink)).toBeNull();
|
|
3782
|
+
});
|
|
3783
|
+
|
|
3784
|
+
expect(diagnostics.map((entry) => entry.kind)).toEqual(['swf.unknown-linkage-name']);
|
|
3785
|
+
expect(diagnostics[0].detail).toEqual({ linkageName: 'absent' });
|
|
3786
|
+
});
|
|
3787
|
+
});
|
|
3788
|
+
|
|
3789
|
+
describe('createScene2DFromSwf morph bounds', () => {
|
|
3790
|
+
it('reports the box the morph occupies at its ratio, not the union of both endpoints', () => {
|
|
3791
|
+
const startEdges = morphBox(200);
|
|
3792
|
+
const endEdges = morphBox(400);
|
|
3793
|
+
const styles = joinBytes(
|
|
3794
|
+
new Uint8Array([1, 0x00, 0xff, 0x00, 0x00, 0xff, 0x00, 0x00, 0xff, 0xff]),
|
|
3795
|
+
new Uint8Array([0]),
|
|
3796
|
+
);
|
|
3797
|
+
const body = joinBytes(
|
|
3798
|
+
uint16(7),
|
|
3799
|
+
createRectangle(0, 200, 0, 200),
|
|
3800
|
+
createRectangle(0, 400, 0, 400),
|
|
3801
|
+
uint32(styles.length + startEdges.length),
|
|
3802
|
+
styles,
|
|
3803
|
+
startEdges,
|
|
3804
|
+
endEdges,
|
|
3805
|
+
);
|
|
3806
|
+
const place = (depth: number, ratio: number): Uint8Array =>
|
|
3807
|
+
createTag(
|
|
3808
|
+
TAG_PLACE_OBJECT_2,
|
|
3809
|
+
joinBytes(new Uint8Array([PLACE_HAS_CHARACTER | PLACE_HAS_RATIO]), uint16(depth), uint16(7), uint16(ratio)),
|
|
3810
|
+
);
|
|
3811
|
+
|
|
3812
|
+
const document = createScene2DFromSwf(
|
|
3813
|
+
createSwf([
|
|
3814
|
+
createTag(TAG_DEFINE_MORPH_SHAPE, body),
|
|
3815
|
+
place(1, 0),
|
|
3816
|
+
place(2, 0x7fff),
|
|
3817
|
+
place(3, 0xffff),
|
|
3818
|
+
createTag(TAG_SHOW_FRAME),
|
|
3819
|
+
createTag(TAG_END),
|
|
3820
|
+
]),
|
|
3821
|
+
);
|
|
3822
|
+
|
|
3823
|
+
// The union would report 20 for all three. Each instance reports the box it is actually at, which is
|
|
3824
|
+
// what layout and hit testing read.
|
|
3825
|
+
const boxes = getNodeChildren(document!.root).map((node) => getNodeLocalBoundsRectangle(node).width);
|
|
3826
|
+
expect(boxes[0]).toBeCloseTo(10);
|
|
3827
|
+
expect(boxes[1]).toBeCloseTo(15, 1);
|
|
3828
|
+
expect(boxes[2]).toBeCloseTo(20);
|
|
3829
|
+
});
|
|
3830
|
+
});
|
|
3831
|
+
|
|
3832
|
+
describe('createScene2DFromSwf morph shapes', () => {
|
|
3833
|
+
it('places a morph character and drives its progress from the placement ratio', () => {
|
|
3834
|
+
const startEdges = morphBox(200);
|
|
3835
|
+
const endEdges = morphBox(400);
|
|
3836
|
+
const styles = joinBytes(
|
|
3837
|
+
new Uint8Array([1, 0x00, 0xff, 0x00, 0x00, 0xff, 0x00, 0x00, 0xff, 0xff]),
|
|
3838
|
+
new Uint8Array([0]),
|
|
3839
|
+
);
|
|
3840
|
+
const body = joinBytes(
|
|
3841
|
+
uint16(7),
|
|
3842
|
+
createRectangle(0, 200, 0, 200),
|
|
3843
|
+
createRectangle(0, 400, 0, 400),
|
|
3844
|
+
uint32(styles.length + startEdges.length),
|
|
3845
|
+
styles,
|
|
3846
|
+
startEdges,
|
|
3847
|
+
endEdges,
|
|
3848
|
+
);
|
|
3849
|
+
|
|
3850
|
+
const document = createScene2DFromSwf(
|
|
3851
|
+
createSwf([
|
|
3852
|
+
createTag(TAG_DEFINE_MORPH_SHAPE, body),
|
|
3853
|
+
// Two placements of one character at different ratios: the case a single shared node cannot serve.
|
|
3854
|
+
createTag(
|
|
3855
|
+
TAG_PLACE_OBJECT_2,
|
|
3856
|
+
joinBytes(new Uint8Array([PLACE_HAS_CHARACTER | PLACE_HAS_RATIO]), uint16(1), uint16(7), uint16(0)),
|
|
3857
|
+
),
|
|
3858
|
+
createTag(
|
|
3859
|
+
TAG_PLACE_OBJECT_2,
|
|
3860
|
+
joinBytes(new Uint8Array([PLACE_HAS_CHARACTER | PLACE_HAS_RATIO]), uint16(2), uint16(7), uint16(0xffff)),
|
|
3861
|
+
),
|
|
3862
|
+
createTag(TAG_SHOW_FRAME),
|
|
3863
|
+
createTag(TAG_END),
|
|
3864
|
+
]),
|
|
3865
|
+
);
|
|
3866
|
+
|
|
3867
|
+
const [first, second] = getNodeChildren(document!.root) as MorphShape[];
|
|
3868
|
+
expect(first.kind).toBe(MorphShapeKind);
|
|
3869
|
+
expect(second.kind).toBe(MorphShapeKind);
|
|
3870
|
+
// Distinct nodes, each sitting at its own point along the same definition.
|
|
3871
|
+
expect(first).not.toBe(second);
|
|
3872
|
+
expect(first.data.progress).toBe(0);
|
|
3873
|
+
expect(second.data.progress).toBe(1);
|
|
3874
|
+
expect(morphWidth(first)).toBeCloseTo(10);
|
|
3875
|
+
expect(morphWidth(second)).toBeCloseTo(20);
|
|
3876
|
+
});
|
|
3877
|
+
});
|
|
3878
|
+
|
|
3879
|
+
describe('createScene2DImportFromSwf', () => {
|
|
3880
|
+
it('keeps an advanced blend mode off the node and reports it for a BlendEffect instead', () => {
|
|
3881
|
+
const result = createScene2DImportFromSwf(
|
|
3882
|
+
createSwf([
|
|
3883
|
+
createTag(
|
|
3884
|
+
TAG_PLACE_OBJECT_3,
|
|
3885
|
+
joinBytes(
|
|
3886
|
+
new Uint8Array([PLACE_HAS_NAME | PLACE_HAS_CHARACTER, PLACE3_HAS_BLEND_MODE]),
|
|
3887
|
+
uint16(3),
|
|
3888
|
+
uint16(7),
|
|
3889
|
+
swfString('overlaid'),
|
|
3890
|
+
new Uint8Array([SWF_BLEND_OVERLAY]),
|
|
3891
|
+
),
|
|
3892
|
+
),
|
|
3893
|
+
createTag(TAG_SHOW_FRAME),
|
|
3894
|
+
createTag(TAG_END),
|
|
3895
|
+
]),
|
|
3896
|
+
);
|
|
3897
|
+
|
|
3898
|
+
const overlaid = result!.document.slots[0].target;
|
|
3899
|
+
// Assigning an advanced mode to the node would render as a silent Normal, which is the whole reason
|
|
3900
|
+
// the two tiers are separate.
|
|
3901
|
+
expect(overlaid.blendMode).toBe(BlendMode.Normal);
|
|
3902
|
+
expect(result!.appearances).toEqual([
|
|
3903
|
+
{ advancedBlendMode: AdvancedBlendMode.Overlay, effects: [], frame: 1, node: overlaid },
|
|
3904
|
+
]);
|
|
3905
|
+
});
|
|
3906
|
+
|
|
3907
|
+
it('does not invent a trailing blend mode from an unknown filter payload', () => {
|
|
3908
|
+
const result = createScene2DImportFromSwf(
|
|
3909
|
+
createSwf([
|
|
3910
|
+
createTag(
|
|
3911
|
+
TAG_PLACE_OBJECT_3,
|
|
3912
|
+
joinBytes(
|
|
3913
|
+
new Uint8Array([PLACE_HAS_NAME | PLACE_HAS_CHARACTER, PLACE3_HAS_FILTER_LIST | PLACE3_HAS_BLEND_MODE]),
|
|
3914
|
+
uint16(3),
|
|
3915
|
+
uint16(7),
|
|
3916
|
+
swfString('unknown-filter'),
|
|
3917
|
+
// Filter 0xfe is variable-width and unknown. Its first payload byte deliberately spells
|
|
3918
|
+
// Overlay; the real blend byte after it spells Multiply. Neither offset is safely reachable.
|
|
3919
|
+
new Uint8Array([1, 0xfe, SWF_BLEND_OVERLAY, SWF_BLEND_MULTIPLY]),
|
|
3920
|
+
),
|
|
3921
|
+
),
|
|
3922
|
+
createTag(TAG_SHOW_FRAME),
|
|
3923
|
+
createTag(TAG_END),
|
|
3924
|
+
]),
|
|
3925
|
+
);
|
|
3926
|
+
|
|
3927
|
+
const node = result!.document.slots[0].target;
|
|
3928
|
+
expect(node.blendMode).toBe(BlendMode.Normal);
|
|
3929
|
+
expect(result!.appearances).toHaveLength(0);
|
|
3930
|
+
});
|
|
3931
|
+
|
|
3932
|
+
it('reports a placement filter list as effect descriptors and attaches nothing', () => {
|
|
3933
|
+
const result = createScene2DImportFromSwf(
|
|
3934
|
+
createSwf([
|
|
3935
|
+
createTag(
|
|
3936
|
+
TAG_PLACE_OBJECT_3,
|
|
3937
|
+
joinBytes(
|
|
3938
|
+
new Uint8Array([PLACE_HAS_NAME | PLACE_HAS_CHARACTER, PLACE3_HAS_FILTER_LIST]),
|
|
3939
|
+
uint16(3),
|
|
3940
|
+
uint16(7),
|
|
3941
|
+
swfString('blurred'),
|
|
3942
|
+
new Uint8Array([1, 1]),
|
|
3943
|
+
uint32(4 * FIXED_16_ONE),
|
|
3944
|
+
uint32(2 * FIXED_16_ONE),
|
|
3945
|
+
new Uint8Array([0]),
|
|
3946
|
+
),
|
|
3947
|
+
),
|
|
3948
|
+
createTag(TAG_SHOW_FRAME),
|
|
3949
|
+
createTag(TAG_END),
|
|
3950
|
+
]),
|
|
3951
|
+
);
|
|
3952
|
+
|
|
3953
|
+
const blurred = result!.document.slots[0].target;
|
|
3954
|
+
expect(result!.appearances).toHaveLength(1);
|
|
3955
|
+
expect(result!.appearances[0]).toMatchObject({ advancedBlendMode: null, frame: 1, node: blurred });
|
|
3956
|
+
expect(result!.appearances[0].effects[0]).toMatchObject({ blurX: 4, blurY: 2, kind: 'BlurEffect' });
|
|
3957
|
+
});
|
|
3958
|
+
|
|
3959
|
+
it('joins a colour-matrix filter onto the node adjustments, since a pointwise remap folds into the draw', () => {
|
|
3960
|
+
const cells: Uint8Array[] = [];
|
|
3961
|
+
for (let index = 0; index < 20; index++) cells.push(float32(index % 6 === 0 ? 1 : 0));
|
|
3962
|
+
const result = createScene2DImportFromSwf(
|
|
3963
|
+
createSwf([
|
|
3964
|
+
createTag(
|
|
3965
|
+
TAG_PLACE_OBJECT_3,
|
|
3966
|
+
joinBytes(
|
|
3967
|
+
new Uint8Array([PLACE_HAS_NAME | PLACE_HAS_CHARACTER, PLACE3_HAS_FILTER_LIST]),
|
|
3968
|
+
uint16(3),
|
|
3969
|
+
uint16(7),
|
|
3970
|
+
swfString('remapped'),
|
|
3971
|
+
new Uint8Array([1, 6]),
|
|
3972
|
+
...cells,
|
|
3973
|
+
),
|
|
2627
3974
|
),
|
|
2628
3975
|
createTag(TAG_SHOW_FRAME),
|
|
2629
3976
|
createTag(TAG_END),
|
|
@@ -2719,8 +4066,7 @@ describe('createScene2DSymbolFromSwf', () => {
|
|
|
2719
4066
|
expect(symbol.slots[0].target).toBe(getNodeChildren(symbol.root)[0]);
|
|
2720
4067
|
});
|
|
2721
4068
|
|
|
2722
|
-
it('
|
|
2723
|
-
registerDeflateDecompressor();
|
|
4069
|
+
it('carries a lossless bitmap reference the symbol resource pass can resolve', async () => {
|
|
2724
4070
|
const art = new ShapeWriter();
|
|
2725
4071
|
art.writeFillStyleCount(1);
|
|
2726
4072
|
art.writeBitmapFillStyle(0x41, 9, 20);
|
|
@@ -2744,11 +4090,17 @@ describe('createScene2DSymbolFromSwf', () => {
|
|
|
2744
4090
|
]),
|
|
2745
4091
|
'Art',
|
|
2746
4092
|
);
|
|
2747
|
-
unregisterDecompressor(Compression.Deflate);
|
|
2748
|
-
|
|
2749
4093
|
const drawn = symbol!.root as Shape;
|
|
2750
4094
|
expect(drawn.data.commands[0]).toBe('beginTextureFill');
|
|
2751
|
-
|
|
4095
|
+
const texture = drawn.data.commands[2] as Texture2D;
|
|
4096
|
+
expect(getTextureSource(texture)).toBeNull();
|
|
4097
|
+
expect(symbol!.imageResources).toHaveLength(1);
|
|
4098
|
+
|
|
4099
|
+
registerDeflateDecompressor();
|
|
4100
|
+
registerSwfImageDecoders();
|
|
4101
|
+
await loadScene2DImageResources(symbol!);
|
|
4102
|
+
|
|
4103
|
+
expectLosslessTexturePixel(texture);
|
|
2752
4104
|
});
|
|
2753
4105
|
|
|
2754
4106
|
it('hands an encoded bitmap out on the image-resource contract, naming the waiting texture', () => {
|
|
@@ -3041,6 +4393,10 @@ function signedBitCount(values: ReadonlyArray<number>): number {
|
|
|
3041
4393
|
return 32;
|
|
3042
4394
|
}
|
|
3043
4395
|
|
|
4396
|
+
function swfBytes(value: string): Uint8Array {
|
|
4397
|
+
return new TextEncoder().encode(value);
|
|
4398
|
+
}
|
|
4399
|
+
|
|
3044
4400
|
function swfString(value: string): Uint8Array {
|
|
3045
4401
|
return joinBytes(_encoder.encode(value), new Uint8Array([0]));
|
|
3046
4402
|
}
|
|
@@ -3095,15 +4451,23 @@ const TAG_DEFINE_BITS = 6;
|
|
|
3095
4451
|
const TAG_DEFINE_BUTTON_2 = 34;
|
|
3096
4452
|
const TAG_DEFINE_EDIT_TEXT = 37;
|
|
3097
4453
|
const TAG_DEFINE_FONT = 10;
|
|
4454
|
+
const TAG_DEFINE_MORPH_SHAPE_2 = 84;
|
|
4455
|
+
const TAG_DEFINE_TEXT_2 = 33;
|
|
3098
4456
|
const TAG_DEFINE_FONT_2 = 48;
|
|
4457
|
+
const TAG_DEFINE_FONT_3 = 75;
|
|
3099
4458
|
const TAG_DEFINE_FONT_INFO = 13;
|
|
3100
4459
|
const TAG_DEFINE_SHAPE = 2;
|
|
4460
|
+
const TAG_DEFINE_SHAPE_2 = 22;
|
|
3101
4461
|
const TAG_DEFINE_SHAPE_3 = 32;
|
|
4462
|
+
const TAG_DEFINE_SHAPE_4 = 83;
|
|
3102
4463
|
const TAG_DEFINE_SPRITE = 39;
|
|
3103
4464
|
const TAG_DEFINE_TEXT = 11;
|
|
3104
4465
|
const TAG_DEFINE_VIDEO_STREAM = 60;
|
|
3105
4466
|
const TAG_VIDEO_FRAME = 61;
|
|
4467
|
+
const TAG_DO_ABC = 82;
|
|
4468
|
+
const TAG_DO_ABC_ANONYMOUS = 72;
|
|
3106
4469
|
const TAG_DO_ACTION = 12;
|
|
4470
|
+
const TAG_DO_INIT_ACTION = 59;
|
|
3107
4471
|
const TAG_EXPORT_ASSETS = 56;
|
|
3108
4472
|
const TAG_FILE_ATTRIBUTES = 69;
|
|
3109
4473
|
const TAG_JPEG_TABLES = 8;
|