@swf2es/format 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/LICENSE.third-party +35 -0
- package/README.md +22 -2
- package/dist/abc-names.d.ts +3 -0
- package/dist/abc-names.d.ts.map +1 -0
- package/dist/abc-names.js +127 -0
- package/dist/abc-names.js.map +1 -0
- package/dist/actions.d.ts +106 -0
- package/dist/actions.d.ts.map +1 -0
- package/dist/actions.js +223 -0
- package/dist/actions.js.map +1 -0
- package/dist/air-descriptor.d.ts +11 -0
- package/dist/air-descriptor.d.ts.map +1 -0
- package/dist/air-descriptor.js +64 -0
- package/dist/air-descriptor.js.map +1 -0
- package/dist/api-versions.d.ts +12 -0
- package/dist/api-versions.d.ts.map +1 -0
- package/dist/api-versions.js +23 -0
- package/dist/api-versions.js.map +1 -0
- package/dist/bitmap.d.ts +34 -0
- package/dist/bitmap.d.ts.map +1 -0
- package/dist/bitmap.js +200 -0
- package/dist/bitmap.js.map +1 -0
- package/dist/button.d.ts +39 -0
- package/dist/button.d.ts.map +1 -0
- package/dist/button.js +61 -0
- package/dist/button.js.map +1 -0
- package/dist/compression.d.ts +47 -0
- package/dist/compression.d.ts.map +1 -0
- package/dist/compression.js +201 -0
- package/dist/compression.js.map +1 -0
- package/dist/display.d.ts +106 -0
- package/dist/display.d.ts.map +1 -0
- package/dist/display.js +306 -0
- package/dist/display.js.map +1 -0
- package/dist/filters.d.ts +66 -0
- package/dist/filters.d.ts.map +1 -0
- package/dist/filters.js +137 -0
- package/dist/filters.js.map +1 -0
- package/dist/font.d.ts +60 -0
- package/dist/font.d.ts.map +1 -0
- package/dist/font.js +169 -0
- package/dist/font.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/dist/lzma.d.ts +33 -0
- package/dist/lzma.d.ts.map +1 -0
- package/dist/lzma.js +279 -0
- package/dist/lzma.js.map +1 -0
- package/dist/pbj.d.ts +178 -0
- package/dist/pbj.d.ts.map +1 -0
- package/dist/pbj.js +485 -0
- package/dist/pbj.js.map +1 -0
- package/dist/shape.d.ts +152 -0
- package/dist/shape.d.ts.map +1 -0
- package/dist/shape.js +412 -0
- package/dist/shape.js.map +1 -0
- package/dist/sound.d.ts +71 -0
- package/dist/sound.d.ts.map +1 -0
- package/dist/sound.js +92 -0
- package/dist/sound.js.map +1 -0
- package/dist/swf.d.ts +77 -0
- package/dist/swf.d.ts.map +1 -0
- package/dist/swf.js +164 -0
- package/dist/swf.js.map +1 -0
- package/dist/tags.d.ts +57 -0
- package/dist/tags.d.ts.map +1 -0
- package/dist/tags.js +59 -0
- package/dist/tags.js.map +1 -0
- package/dist/text.d.ts +75 -0
- package/dist/text.d.ts.map +1 -0
- package/dist/text.js +136 -0
- package/dist/text.js.map +1 -0
- package/dist/zip.d.ts +17 -0
- package/dist/zip.d.ts.map +1 -0
- package/dist/zip.js +92 -0
- package/dist/zip.js.map +1 -0
- package/package.json +33 -3
- package/src/abc-names.ts +138 -0
- package/src/actions.ts +319 -0
- package/src/air-descriptor.ts +84 -0
- package/src/api-versions.ts +27 -0
- package/src/bitmap.ts +259 -0
- package/src/button.ts +99 -0
- package/src/compression.ts +243 -0
- package/src/display.ts +419 -0
- package/src/filters.ts +206 -0
- package/src/font.ts +238 -0
- package/src/index.ts +151 -0
- package/src/pbj.ts +651 -0
- package/src/shape.ts +618 -0
- package/src/sound.ts +173 -0
- package/src/swf.ts +234 -0
- package/src/tags.ts +58 -0
- package/src/text.ts +213 -0
- package/src/zip.ts +135 -0
package/src/button.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// DefineButton and DefineButton2 (SWF specification, chapter 4): a
|
|
2
|
+
// button's records, each a character shown in some of its four states, and
|
|
3
|
+
// whether it tracks as a menu. Their actions are the SWF's action blocks
|
|
4
|
+
// (actions.ts), which a button gives by their indices.
|
|
5
|
+
import type { ActionList } from "./actions.js";
|
|
6
|
+
import { readFilterBytes } from "./display.js";
|
|
7
|
+
import { type ColorTransform, type Matrix, readColorTransform, readMatrix } from "./shape.js";
|
|
8
|
+
import { SwfReader, type Tag } from "./swf.js";
|
|
9
|
+
import { DefineButton2 } from "./tags.js";
|
|
10
|
+
|
|
11
|
+
/** A button's state, as BUTTONRECORD's flags have them. */
|
|
12
|
+
export const BUTTON_UP = 0x01;
|
|
13
|
+
export const BUTTON_OVER = 0x02;
|
|
14
|
+
export const BUTTON_DOWN = 0x04;
|
|
15
|
+
export const BUTTON_HIT_TEST = 0x08;
|
|
16
|
+
|
|
17
|
+
/** A character a button shows in the states `states` names (BUTTON_UP and the rest). */
|
|
18
|
+
export interface ButtonRecord {
|
|
19
|
+
states: number;
|
|
20
|
+
character: number;
|
|
21
|
+
depth: number;
|
|
22
|
+
matrix: Matrix;
|
|
23
|
+
/** DefineButton2's; null in a DefineButton. */
|
|
24
|
+
colorTransform: ColorTransform | null;
|
|
25
|
+
/** The filter list's bytes, undecoded, as a place keeps them; null without one. */
|
|
26
|
+
filters: Uint8Array | null;
|
|
27
|
+
blendMode: number | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface Button {
|
|
31
|
+
id: number;
|
|
32
|
+
records: ButtonRecord[];
|
|
33
|
+
/** DefineButton2's TrackAsMenu: a press elsewhere can be released on it. */
|
|
34
|
+
trackAsMenu: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* The indices of its AVM1 actions in the SWF's ActionList; null without
|
|
37
|
+
* them, or when read without the list.
|
|
38
|
+
*/
|
|
39
|
+
actions: number[] | null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** A DefineButton or DefineButton2; given the SWF's action list, with the indices of its actions. */
|
|
43
|
+
export function readButton(bytes: Uint8Array, tag: Tag, actions?: ActionList): Button {
|
|
44
|
+
const { button } = parseButton(bytes, tag);
|
|
45
|
+
button.actions = actions?.byTag.get(tag.offset) ?? null;
|
|
46
|
+
return button;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** A button's id, and where its actions start: -1 for a DefineButton2 whose action offset is 0. */
|
|
50
|
+
export function buttonActionsOffset(bytes: Uint8Array, tag: Tag): { id: number; offset: number } {
|
|
51
|
+
const { button, actions } = parseButton(bytes, tag);
|
|
52
|
+
return { id: button.id, offset: actions };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function parseButton(bytes: Uint8Array, tag: Tag): { button: Button; actions: number } {
|
|
56
|
+
const r = new SwfReader(bytes, tag.offset, tag.offset + tag.length);
|
|
57
|
+
const two = tag.code === DefineButton2;
|
|
58
|
+
const button: Button = { id: r.u16(), records: [], trackAsMenu: false, actions: null };
|
|
59
|
+
// Only whether it is 0 counts: the actions start after the records' end, as Ruffle reads them.
|
|
60
|
+
let hasActions = true;
|
|
61
|
+
if (two) {
|
|
62
|
+
button.trackAsMenu = (r.u8() & 0x01) !== 0;
|
|
63
|
+
hasActions = r.u16() !== 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// The records end at a flags byte of 0.
|
|
67
|
+
while (r.pos < r.end && !r.overrun) {
|
|
68
|
+
const flags = r.u8();
|
|
69
|
+
if (flags === 0) {
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const record: ButtonRecord = {
|
|
74
|
+
states: flags & 0x0f,
|
|
75
|
+
character: r.u16(),
|
|
76
|
+
depth: r.u16(),
|
|
77
|
+
matrix: readMatrix(r),
|
|
78
|
+
colorTransform: null,
|
|
79
|
+
filters: null,
|
|
80
|
+
blendMode: null,
|
|
81
|
+
};
|
|
82
|
+
if (two) {
|
|
83
|
+
record.colorTransform = readColorTransform(r, true);
|
|
84
|
+
if (flags & 0x10) {
|
|
85
|
+
record.filters = readFilterBytes(r);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (flags & 0x20) {
|
|
89
|
+
record.blendMode = r.u8();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!r.overrun) {
|
|
94
|
+
button.records.push(record);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return { button, actions: hasActions ? Math.min(r.pos, r.end) : -1 };
|
|
99
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compression as SWF files and ByteArray use it, synchronously and with no
|
|
3
|
+
* platform API, through @swf2es/native/compress: zlib itself for zlib and raw
|
|
4
|
+
* deflate, the LZMA SDK's encoder for LZMA, and an LZMA decoder after the
|
|
5
|
+
* SDK's specification.
|
|
6
|
+
*/
|
|
7
|
+
import {
|
|
8
|
+
CORRUPT,
|
|
9
|
+
type Decoded,
|
|
10
|
+
DICTIONARY,
|
|
11
|
+
DONE,
|
|
12
|
+
deflate,
|
|
13
|
+
FULL,
|
|
14
|
+
inflate,
|
|
15
|
+
lzmaDecode,
|
|
16
|
+
lzmaEncode,
|
|
17
|
+
TRUNCATED,
|
|
18
|
+
} from "@swf2es/native/compress";
|
|
19
|
+
|
|
20
|
+
/** Data a decompressor cannot read: corrupt, truncated, or not what it says. */
|
|
21
|
+
export class CompressedDataError extends Error {}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Data whose output does not fit: more than the decompressor's memory, or
|
|
25
|
+
* the page's, can hold. avmplus' ByteArray fails such an uncompress with
|
|
26
|
+
* MemoryError 1000, where it fails corrupt data with IOError 2058.
|
|
27
|
+
*/
|
|
28
|
+
export class CompressedDataTooLargeError extends CompressedDataError {}
|
|
29
|
+
|
|
30
|
+
/** A deflate level: 0 stores, 1 is fastest, 9 smallest. */
|
|
31
|
+
export type ZlibLevel = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
|
|
32
|
+
|
|
33
|
+
/** `data` as a zlib stream, as zlib's deflate writes it at `level`, 9 by default, as avmplus' ByteArray. */
|
|
34
|
+
export function zlibCompress(data: Uint8Array, level: ZlibLevel = 9): Uint8Array {
|
|
35
|
+
return deflate(data, level, false);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** `data` as a raw deflate stream, with no zlib header or checksum. */
|
|
39
|
+
export function deflateCompress(data: Uint8Array): Uint8Array {
|
|
40
|
+
return deflate(data, 9, true);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The most an inflate may write: as much as a wasm32 memory holds. */
|
|
44
|
+
const NO_LIMIT = 2 ** 32 - 1;
|
|
45
|
+
|
|
46
|
+
/** A failed decode's status as a CompressedDataError. */
|
|
47
|
+
function failure(result: Decoded): CompressedDataError {
|
|
48
|
+
switch (result.status) {
|
|
49
|
+
case TRUNCATED:
|
|
50
|
+
return new CompressedDataError("truncated");
|
|
51
|
+
case CORRUPT:
|
|
52
|
+
return new CompressedDataError(result.message ?? "corrupt");
|
|
53
|
+
case DICTIONARY:
|
|
54
|
+
return new CompressedDataError("needs a preset dictionary");
|
|
55
|
+
default:
|
|
56
|
+
return new CompressedDataTooLargeError("out of memory");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The bytes of a zlib stream (or, `raw`, of a raw deflate stream), up to its
|
|
62
|
+
* end: anything after is left, as zlib's inflate leaves it. The header is
|
|
63
|
+
* checked here, so that every way it can be wrong reads as avmplus' does.
|
|
64
|
+
*/
|
|
65
|
+
export function zlibUncompress(data: Uint8Array, raw = false): Uint8Array {
|
|
66
|
+
if (!raw) {
|
|
67
|
+
if (data.length < 2) {
|
|
68
|
+
throw new CompressedDataError("truncated");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const cmf = data[0];
|
|
72
|
+
const flg = data[1];
|
|
73
|
+
if ((cmf & 0x0f) !== 8 || cmf >> 4 > 7 || ((cmf << 8) | flg) % 31 !== 0) {
|
|
74
|
+
throw new CompressedDataError("incorrect header check");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (flg & 0x20) {
|
|
78
|
+
throw new CompressedDataError("needs a preset dictionary");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const result = inflate(data, raw, NO_LIMIT);
|
|
83
|
+
if (result.status !== DONE) {
|
|
84
|
+
throw failure(result);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return result.bytes;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The size of an LZMA header: its 5 property bytes and the 8-byte length of what it holds. */
|
|
91
|
+
export const LZMA_HEADER = 13;
|
|
92
|
+
|
|
93
|
+
/** `data` as ByteArray's LZMA: the 5 property bytes, its length in 8 bytes, little-endian, and the stream. */
|
|
94
|
+
export function lzmaByteArrayCompress(data: Uint8Array): Uint8Array {
|
|
95
|
+
return lzmaEncode(data);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* ByteArray's LZMA, as avmplus reads it: the length its header gives must
|
|
100
|
+
* be what the stream holds. A length past 32 bits is not read here; the
|
|
101
|
+
* caller checks it first, as avmplus fails it before trying.
|
|
102
|
+
*/
|
|
103
|
+
export function lzmaByteArrayUncompress(data: Uint8Array): Uint8Array {
|
|
104
|
+
const length = (data[5] | (data[6] << 8) | (data[7] << 16) | (data[8] << 24)) >>> 0;
|
|
105
|
+
if (length > MAX_LZMA_RATIO * (data.length - LZMA_HEADER) + 4096) {
|
|
106
|
+
throw new CompressedDataError(`${length} bytes in a stream of ${data.length}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const result = lzmaDecode(data, 0, LZMA_HEADER, length);
|
|
110
|
+
if (result.status !== DONE) {
|
|
111
|
+
throw failure(result);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (result.bytes.length !== length) {
|
|
115
|
+
throw new CompressedDataError("length mismatch");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return result.bytes;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* More than LZMA can expand a byte to. Its best case, a long run of one
|
|
123
|
+
* byte, is matches of 273 bytes that each cost 14 decisions, each at least
|
|
124
|
+
* the 0.022 bits of LZMA's likeliest probability (2017/2048): 7090:1,
|
|
125
|
+
* which an encoder nears on 32 MB of zeros (6959:1). A header naming more
|
|
126
|
+
* than this of its stream is refused before decoding; one naming less is
|
|
127
|
+
* decoded, and a decoder that would read past the stream's end fails.
|
|
128
|
+
*/
|
|
129
|
+
const MAX_LZMA_RATIO = 8192;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The first `length` bytes of a SWF as an uncompressed one, or fewer if it
|
|
133
|
+
* has fewer, decompressing only as far as they need: the header and the
|
|
134
|
+
* first tags, without trusting the header's file length, which a SWF of
|
|
135
|
+
* 30 bytes may give as 4 GB. Whatever cannot be read ends it early.
|
|
136
|
+
*/
|
|
137
|
+
export function decompressSwfPrefix(swf: Uint8Array, length: number): Uint8Array {
|
|
138
|
+
const signature = String.fromCharCode(swf[0], swf[1], swf[2]);
|
|
139
|
+
if (swf.length < 8 || signature === "FWS") {
|
|
140
|
+
return swf.subarray(0, length);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const fileLength = new DataView(swf.buffer, swf.byteOffset, swf.byteLength).getUint32(4, true);
|
|
144
|
+
// No further than the header's length, where decompressSwf stops too.
|
|
145
|
+
const want = Math.max(0, Math.min(length, fileLength) - 8);
|
|
146
|
+
const options = { offset: 8, partial: true };
|
|
147
|
+
let out: Uint8Array = new Uint8Array(8);
|
|
148
|
+
// A kilobyte of the body first, as a header and the first tags need, and
|
|
149
|
+
// more only while the input runs out before the output: a large SWF is
|
|
150
|
+
// not copied whole for its first bytes. What decoded before any failure
|
|
151
|
+
// is kept, so a SWF cut short still has its start.
|
|
152
|
+
for (let input = 1024; ; input *= 16) {
|
|
153
|
+
const end = Math.min(swf.length, (signature === "ZWS" ? 17 : 8) + input);
|
|
154
|
+
let result: Decoded | null = null;
|
|
155
|
+
if (signature === "CWS") {
|
|
156
|
+
result = inflate(swf.subarray(8, end), false, want, options);
|
|
157
|
+
} else if (signature === "ZWS" && swf.length >= 17) {
|
|
158
|
+
const size = Math.min(want, Math.max(0, fileLength - 8));
|
|
159
|
+
result = lzmaDecode(swf.subarray(0, end), 12, 17, size, options);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (result === null) {
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
out = result.bytes;
|
|
167
|
+
if (result.status !== TRUNCATED || end === swf.length) {
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
out.set(swf.subarray(0, 8), 0);
|
|
173
|
+
out[0] = 0x46; // F
|
|
174
|
+
return out;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* A SWF's bytes as an uncompressed one: the header as FWS, then the body,
|
|
179
|
+
* inflated for CWS, and for ZWS decoded from LZMA, whose header SWF writes
|
|
180
|
+
* differently (after the 8 bytes of the SWF header, a 4-byte compressed
|
|
181
|
+
* length and the 5 property bytes, and no 8-byte size).
|
|
182
|
+
*/
|
|
183
|
+
export function decompressSwf(swf: Uint8Array): Uint8Array {
|
|
184
|
+
const signature = String.fromCharCode(swf[0], swf[1], swf[2]);
|
|
185
|
+
if (signature === "FWS") {
|
|
186
|
+
return swf;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const fileLength = new DataView(swf.buffer, swf.byteOffset, swf.byteLength).getUint32(4, true);
|
|
190
|
+
if (fileLength < 8 && (signature === "CWS" || signature === "ZWS")) {
|
|
191
|
+
throw new CompressedDataError(`a file length of ${fileLength}, shorter than its header`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
let result: Decoded;
|
|
195
|
+
if (signature === "CWS") {
|
|
196
|
+
// A byte past the header's length tells a longer body from one that
|
|
197
|
+
// fits, which then ends with its checksum checked.
|
|
198
|
+
result = inflate(swf.subarray(8), false, fileLength - 8 + 1, { offset: 8 });
|
|
199
|
+
if (result.status === TRUNCATED) {
|
|
200
|
+
// Some SWFs omit the Adler-32 trailer despite holding a complete deflate stream.
|
|
201
|
+
const raw = inflate(swf.subarray(10), true, fileLength - 8 + 1, { offset: 8 });
|
|
202
|
+
if ((raw.status === DONE || raw.status === FULL) && 10 + raw.used + 4 > swf.length) {
|
|
203
|
+
result = raw;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// A longer body, as a packer may leave bytes after the End tag inside
|
|
208
|
+
// the stream, is read as far as the header's length and no further, as
|
|
209
|
+
// Flash Player plays it, and as a ZWS's decoding stops.
|
|
210
|
+
if (result.status === FULL) {
|
|
211
|
+
result = { ...result, status: DONE, bytes: result.bytes.subarray(0, fileLength) };
|
|
212
|
+
}
|
|
213
|
+
} else if (signature === "ZWS") {
|
|
214
|
+
if (swf.length < 17) {
|
|
215
|
+
throw new CompressedDataError("truncated");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// A length no stream that short can hold is refused before decoding;
|
|
219
|
+
// within it, the decoder stops where the stream does.
|
|
220
|
+
if (fileLength - 8 > MAX_LZMA_RATIO * (swf.length - 17) + 4096) {
|
|
221
|
+
throw new CompressedDataError(`a body of ${fileLength - 8} bytes in ${swf.length} of SWF`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
result = lzmaDecode(swf, 12, 17, fileLength - 8, { offset: 8 });
|
|
225
|
+
} else {
|
|
226
|
+
throw new TypeError(`Not a SWF file (signature ${JSON.stringify(signature)})`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (result.status !== DONE) {
|
|
230
|
+
throw failure(result);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const file = result.bytes;
|
|
234
|
+
if (file.length !== fileLength) {
|
|
235
|
+
throw new CompressedDataError(
|
|
236
|
+
`body of ${file.length - 8} bytes where the header says ${fileLength - 8}`,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
file.set(swf.subarray(0, 8), 0);
|
|
241
|
+
file[0] = 0x46; // F
|
|
242
|
+
return file;
|
|
243
|
+
}
|