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