├── tests └── local-tags │ └── examples │ └── show-frame │ ├── input.bytes │ ├── value.json │ └── src │ └── input.txt ├── rs ├── fuzz │ ├── .gitignore │ ├── fuzz_targets │ │ ├── swf.rs │ │ └── tag.rs │ ├── Cargo.toml │ └── Cargo.lock ├── rustfmt.toml ├── clippy.toml ├── src │ ├── streaming │ │ ├── mod.rs │ │ ├── parser │ │ │ ├── lzma.rs │ │ │ ├── deflate.rs │ │ │ └── simple.rs │ │ ├── decompress.rs │ │ ├── tag.rs │ │ └── movie.rs │ ├── complete │ │ ├── mod.rs │ │ ├── base.rs │ │ ├── video.rs │ │ ├── sound.rs │ │ ├── gradient.rs │ │ └── movie.rs │ ├── stream_buffer.rs │ └── lib.rs ├── bin │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── .gitignore ├── Cargo.toml └── README.md ├── docs ├── avm1 │ └── actions │ │ ├── fs-command2.md │ │ ├── strict-mode.md │ │ ├── end-drag.md │ │ ├── play.md │ │ ├── stop.md │ │ ├── stop-sounds.md │ │ ├── next-frame.md │ │ ├── toggle-quality.md │ │ ├── previous-frame.md │ │ ├── pop.md │ │ ├── push-duplicate.md │ │ ├── delete.md │ │ ├── goto-label.md │ │ ├── remove-sprite.md │ │ ├── goto-frame.md │ │ ├── return.md │ │ ├── string-add.md │ │ ├── define-local2.md │ │ ├── string-length.md │ │ ├── delete2.md │ │ ├── get-time.md │ │ ├── define-local.md │ │ ├── stack-swap.md │ │ ├── random-number.md │ │ ├── ascii-to-char.md │ │ ├── char-to-ascii.md │ │ ├── to-integer.md │ │ ├── set-target2.md │ │ ├── constant-pool.md │ │ ├── clone-sprite.md │ │ ├── decrement.md │ │ ├── increment.md │ │ ├── equals2.md │ │ ├── modulo.md │ │ ├── set-property.md │ │ ├── jump.md │ │ ├── target-path.md │ │ ├── add.md │ │ ├── strict-equals.md │ │ ├── string-less.md │ │ ├── trace.md │ │ ├── subtract.md │ │ ├── multiply.md │ │ ├── string-greater.md │ │ ├── less.md │ │ ├── store-register.md │ │ ├── or.md │ │ ├── bit-or.md │ │ ├── to-string.md │ │ ├── wait-for-frame.md │ │ ├── bit-xor.md │ │ ├── mb-string-length.md │ │ ├── set-member.md │ │ ├── get-variable.md │ │ ├── string-equals.md │ │ ├── enumerate2.md │ │ ├── add2.md │ │ ├── to-number.md │ │ ├── type-of.md │ │ ├── get-url.md │ │ ├── string-extract.md │ │ ├── bit-and.md │ │ ├── wait-for-frame2.md │ │ ├── equals.md │ │ ├── bit-l-shift.md │ │ ├── mb-ascii-to-char.md │ │ ├── mb-char-to-ascii.md │ │ ├── and.md │ │ ├── divide.md │ │ ├── less2.md │ │ ├── greater.md │ │ ├── bit-u-r-shift.md │ │ ├── bit-r-shift.md │ │ ├── call.md │ │ ├── set-variable.md │ │ ├── not.md │ │ ├── if.md │ │ ├── mb-string-extract.md │ │ ├── with.md │ │ ├── start-drag.md │ │ ├── enumerate.md │ │ ├── cast-op.md │ │ ├── throw.md │ │ ├── set-target.md │ │ ├── instance-of.md │ │ ├── implements-op.md │ │ ├── init-array.md │ │ ├── call-function.md │ │ ├── get-member.md │ │ ├── new-object.md │ │ ├── init-object.md │ │ ├── goto-frame2.md │ │ ├── extends.md │ │ ├── new-method.md │ │ ├── call-method.md │ │ ├── get-property.md │ │ ├── try.md │ │ ├── define-function.md │ │ ├── push.md │ │ └── get-url2.md ├── grammar.md └── tags │ └── define-morph-shape.md ├── ts ├── .yarnrc.yml ├── src │ ├── test │ │ ├── meta.mts │ │ ├── tsconfig.json │ │ ├── utils.mts │ │ ├── movies.spec.mts │ │ ├── tags.spec.mts │ │ └── various.spec.mts │ ├── lib │ │ ├── tsconfig.json │ │ ├── concat-bytes.mts │ │ ├── errors │ │ │ ├── incomplete-tag-header.mts │ │ │ ├── incomplete-stream.mts │ │ │ └── incomplete-tag.mts │ │ ├── index.mts │ │ └── parsers │ │ │ ├── video.mts │ │ │ ├── movie.mts │ │ │ ├── header.mts │ │ │ ├── sound.mts │ │ │ └── gradient.mts │ └── main │ │ ├── tsconfig.json │ │ └── main.mts ├── tsconfig.json ├── .eslintrc.json ├── package.json ├── .gitignore └── README.md ├── .gitattributes ├── update-submodules.sh ├── .editorconfig ├── .gitmodules ├── .github └── workflows │ ├── check-ts.yml │ └── check-rs.yml ├── .gitignore └── README.md /tests/local-tags/examples/show-frame/input.bytes: -------------------------------------------------------------------------------- 1 | @ -------------------------------------------------------------------------------- /rs/fuzz/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | target 3 | corpus 4 | artifacts 5 | -------------------------------------------------------------------------------- /rs/rustfmt.toml: -------------------------------------------------------------------------------- 1 | # cargo fmt --all 2 | max_width = 120 3 | tab_spaces = 2 4 | -------------------------------------------------------------------------------- /docs/avm1/actions/fs-command2.md: -------------------------------------------------------------------------------- 1 | # FsCommand2 2 | 3 | - Action Code: `0x2d` 4 | -------------------------------------------------------------------------------- /docs/avm1/actions/strict-mode.md: -------------------------------------------------------------------------------- 1 | # StrictMode 2 | 3 | - Action Code: `0x89` 4 | -------------------------------------------------------------------------------- /tests/local-tags/examples/show-frame/value.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "ShowFrame" 3 | } 4 | -------------------------------------------------------------------------------- /ts/.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: pnp 2 | 3 | yarnPath: .yarn/releases/yarn-4.0.0-rc.4.cjs 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Enforce `lf` for text files (even on Windows) 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /update-submodules.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | git submodule update --init --recursive --remote 3 | -------------------------------------------------------------------------------- /rs/clippy.toml: -------------------------------------------------------------------------------- 1 | # cargo clippy --all-targets --all-features -- -D warnings 2 | 3 | # (Using default config) 4 | -------------------------------------------------------------------------------- /tests/local-tags/examples/show-frame/src/input.txt: -------------------------------------------------------------------------------- 1 | 4000 # 1 * 64 + 0 = 0x0040 2 | -------------------------------------------------------------------------------- /rs/src/streaming/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod basic_data_types; 2 | pub(crate) mod decompress; 3 | pub mod movie; 4 | pub mod parser; 5 | pub mod tag; 6 | -------------------------------------------------------------------------------- /rs/fuzz/fuzz_targets/swf.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | use libfuzzer_sys::fuzz_target; 3 | 4 | fuzz_target!(|data: &[u8]| { 5 | let _ = swf_parser::complete::parse_swf(data); 6 | }); 7 | -------------------------------------------------------------------------------- /ts/src/test/meta.mts: -------------------------------------------------------------------------------- 1 | import furi from "furi"; 2 | 3 | export const dirname: string = furi.toSysPath(furi.join(import.meta.url, "..")); 4 | 5 | // tslint:disable-next-line:no-default-export 6 | export default {dirname}; 7 | -------------------------------------------------------------------------------- /rs/fuzz/fuzz_targets/tag.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | use libfuzzer_sys::fuzz_target; 3 | 4 | fuzz_target!(|data: &[u8]| { 5 | if let Some((swf_version, data)) = data.split_first() { 6 | let _ = swf_parser::complete::parse_tag(data, *swf_version); 7 | } 8 | }); 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | indent_brace_style = K&R 11 | insert_final_newline = true 12 | trim_trailing_whitespace = true 13 | max_line_length = 120 14 | -------------------------------------------------------------------------------- /ts/src/lib/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "composite": true, 5 | "noEmit": false, 6 | "rootDir": ".", 7 | "outDir": "../../lib" 8 | }, 9 | "include": [ 10 | "./**/*.mts" 11 | ], 12 | "exclude": [], 13 | "references": [] 14 | } 15 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "tests/movies"] 2 | path = tests/movies 3 | url = https://github.com/open-flash-db/movies.git 4 | [submodule "tests/tags"] 5 | path = tests/tags 6 | url = https://github.com/open-flash-db/tags.git 7 | [submodule "tests/various"] 8 | path = tests/various 9 | url = https://github.com/open-flash-db/various.git 10 | -------------------------------------------------------------------------------- /ts/src/main/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": false, 5 | "rootDir": ".", 6 | "outDir": "../../main" 7 | }, 8 | "include": [ 9 | "**/*.mts" 10 | ], 11 | "exclude": [], 12 | "references": [ 13 | { 14 | "path": "../lib" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /ts/src/test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": false, 5 | "rootDir": ".", 6 | "outDir": "../../test" 7 | }, 8 | "include": [ 9 | "**/*.mts" 10 | ], 11 | "exclude": [], 12 | "references": [ 13 | { 14 | "path": "../lib" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /rs/src/complete/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod base; 2 | pub(crate) mod button; 3 | pub(crate) mod display; 4 | pub(crate) mod gradient; 5 | pub(crate) mod image; 6 | pub(crate) mod morph_shape; 7 | pub(crate) mod movie; 8 | pub(crate) mod shape; 9 | pub(crate) mod sound; 10 | pub(crate) mod tag; 11 | pub(crate) mod text; 12 | pub(crate) mod video; 13 | 14 | pub use movie::parse_swf; 15 | pub use movie::SwfParseError; 16 | pub use tag::parse_tag; 17 | -------------------------------------------------------------------------------- /docs/avm1/actions/end-drag.md: -------------------------------------------------------------------------------- 1 | # EndDrag 2 | 3 | ``` 4 | [] → [] 5 | ``` 6 | 7 | - Action Code: `0x28` 8 | - Stack: `0 → 0` 9 | - SWF version: `4` 10 | 11 | ## Original documentation 12 | 13 | ### ActionEndDrag 14 | 15 | ActionEndDrag ends the drag operation in progress, if any. 16 | 17 | | Field | Type | Comment | 18 | |-------------------|--------------------|-------------------| 19 | | ActionEndDrag | ACTIONRECORDHEADER | ActionCode = 0x28 | 20 | -------------------------------------------------------------------------------- /docs/avm1/actions/play.md: -------------------------------------------------------------------------------- 1 | # Play 2 | 3 | ``` 4 | [] → [] 5 | ``` 6 | 7 | - Action Code: `0x06` 8 | - Stack: `0 → 0` 9 | - SWF version: `3` 10 | 11 | ## Original documentation 12 | 13 | ### ActionPlay 14 | 15 | ActionPlay instructs Flash Player to start playing at the current frame. 16 | 17 | | Field | Type | Comment | 18 | |-----------------|--------------------|-------------------| 19 | | ActionPlay | ACTIONRECORDHEADER | ActionCode = 0x06 | 20 | -------------------------------------------------------------------------------- /ts/src/lib/concat-bytes.mts: -------------------------------------------------------------------------------- 1 | import { UintSize } from "semantic-types"; 2 | 3 | export function concatBytes(chunks: Uint8Array[]): Uint8Array { 4 | let totalSize: UintSize = 0; 5 | for (const chunk of chunks) { 6 | totalSize += chunk.length; 7 | } 8 | const result: Uint8Array = new Uint8Array(totalSize); 9 | let offset: UintSize = 0; 10 | for (const chunk of chunks) { 11 | result.set(chunk, offset); 12 | offset += chunk.length; 13 | } 14 | return result; 15 | } 16 | -------------------------------------------------------------------------------- /docs/avm1/actions/stop.md: -------------------------------------------------------------------------------- 1 | # Stop 2 | 3 | ``` 4 | [] → [] 5 | ``` 6 | 7 | - Action Code: `0x07` 8 | - Stack: `0 → 0` 9 | - SWF version: `3` 10 | 11 | ## Original documentation 12 | 13 | ### ActionStop 14 | 15 | ActionStop instructs Flash Player to stop playing the file at the current frame. 16 | 17 | | Field | Type | Comment | 18 | |-----------------|--------------------|-------------------| 19 | | ActionStop | ACTIONRECORDHEADER | ActionCode = 0x07 | 20 | -------------------------------------------------------------------------------- /docs/avm1/actions/stop-sounds.md: -------------------------------------------------------------------------------- 1 | # StopSounds 2 | 3 | ``` 4 | [] → [] 5 | ``` 6 | 7 | - Action Code: `0x09` 8 | - Stack: `0 → 0` 9 | - SWF version: `3` 10 | 11 | ## Original documentation 12 | 13 | ### ActionStopSounds 14 | 15 | ActionStopSounds instructs Flash Player to stop playing all sounds. 16 | 17 | | Field | Type | Comment | 18 | |------------------|--------------------|-------------------| 19 | | ActionStopSounds | ACTIONRECORDHEADER | ActionCode = 0x09 | 20 | -------------------------------------------------------------------------------- /docs/avm1/actions/next-frame.md: -------------------------------------------------------------------------------- 1 | # NextFrame 2 | 3 | ``` 4 | [] → [] 5 | ``` 6 | 7 | - Action Code: `0x04` 8 | - Stack: `0 → 0` 9 | - SWF version: `3` 10 | 11 | ## Original documentation 12 | 13 | ### ActionNextFrame 14 | 15 | ActionNextFrame instructs Flash Player to go to the next frame in the current file. 16 | 17 | | Field | Type | Comment | 18 | |-----------------|--------------------|-------------------| 19 | | ActionNextFrame | ACTIONRECORDHEADER | ActionCode = 0x04 | 20 | -------------------------------------------------------------------------------- /docs/avm1/actions/toggle-quality.md: -------------------------------------------------------------------------------- 1 | # ToggleQuality 2 | 3 | ``` 4 | [] → [] 5 | ``` 6 | 7 | - Action Code: `0x08` 8 | - Stack: `0 → 0` 9 | - SWF version: `3` 10 | 11 | ## Original documentation 12 | 13 | ### ActionToggleQuality 14 | 15 | ActionToggleQuality toggles the display between high and low quality. 16 | 17 | | Field | Type | Comment | 18 | |--------------------|--------------------|-------------------| 19 | | ActionToggleQualty | ACTIONRECORDHEADER | ActionCode = 0x08 | 20 | -------------------------------------------------------------------------------- /docs/avm1/actions/previous-frame.md: -------------------------------------------------------------------------------- 1 | # PreviousFrame 2 | 3 | ``` 4 | [] → [] 5 | ``` 6 | 7 | - Action Code: `0x05` 8 | - Stack: `0 → 0` 9 | - SWF version: `3` 10 | 11 | ## Original documentation 12 | 13 | ### ActionPreviousFrame 14 | 15 | ActionPreviousFrame instructs Flash Player to go to the previous frame of the 16 | current file. 17 | 18 | | Field | Type | Comment | 19 | |-----------------|--------------------|-------------------| 20 | | ActionPrevFrame | ACTIONRECORDHEADER | ActionCode = 0x05 | 21 | -------------------------------------------------------------------------------- /docs/avm1/actions/pop.md: -------------------------------------------------------------------------------- 1 | # Pop 2 | 3 | ``` 4 | [a] → [] 5 | ``` 6 | 7 | - Action Code: `0x17` 8 | - Stack: `1 → 0` 9 | - SWF version: `4` 10 | 11 | ## Original documentation 12 | 13 | ### ActionPop 14 | 15 | ActionPop pops a value from the stack and discards it. 16 | 17 | | Field | Type | Comment | 18 | |--------------------|--------------------|-------------------| 19 | | ActionPop | ACTIONRECORDHEADER | ActionCode = 0x17 | 20 | 21 | ActionPop pops a value off the stack and discards the value. 22 | -------------------------------------------------------------------------------- /ts/src/lib/errors/incomplete-tag-header.mts: -------------------------------------------------------------------------------- 1 | import incident, { Incident } from "incident"; 2 | 3 | export type Name = "IncompleteTagHeader"; 4 | export const name: Name = "IncompleteTagHeader"; 5 | 6 | export interface Data { 7 | } 8 | 9 | export type Cause = undefined; 10 | export type IncompleteTagHeaderError = Incident; 11 | 12 | export function format(_: Data) { 13 | return "Failed to parse tag header: Not enough data"; 14 | } 15 | 16 | export function createIncompleteTagHeaderError(): IncompleteTagHeaderError { 17 | return new incident.Incident(name, {}, format); 18 | } 19 | -------------------------------------------------------------------------------- /docs/avm1/actions/push-duplicate.md: -------------------------------------------------------------------------------- 1 | # PushDuplicate 2 | 3 | ``` 4 | [value] → [value, value] 5 | ``` 6 | 7 | - Action Code: `0x4c` 8 | - Stack: `1 → 2` 9 | - SWF version: `5` 10 | 11 | ## Original documentation 12 | 13 | ### ActionPushDuplicate 14 | 15 | ActionPushDuplicate pushes a duplicate of top of stack (the current return value) to the stack. 16 | 17 | | Field | Type | Comment | 18 | |---------------------|--------------------|--------------------------------| 19 | | ActionPushDuplicate | ACTIONRECORDHEADER | ActionCode = 0x4C | 20 | -------------------------------------------------------------------------------- /rs/fuzz/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "swf-parser-fuzz" 3 | version = "0.0.0" 4 | authors = ["Automatically generated"] 5 | publish = false 6 | edition = "2021" 7 | 8 | [package.metadata] 9 | cargo-fuzz = true 10 | 11 | [dependencies] 12 | libfuzzer-sys = "^0.4.3" 13 | 14 | [dependencies.swf-parser] 15 | path = "../." 16 | 17 | # Prevent this from interfering with workspaces 18 | [workspace] 19 | members = ["."] 20 | 21 | [[bin]] 22 | name = "swf" 23 | path = "fuzz_targets/swf.rs" 24 | test = false 25 | doc = false 26 | 27 | [[bin]] 28 | name = "tag" 29 | path = "fuzz_targets/tag.rs" 30 | test = false 31 | doc = false 32 | -------------------------------------------------------------------------------- /docs/avm1/actions/delete.md: -------------------------------------------------------------------------------- 1 | # Delete 2 | 3 | ``` 4 | [object, name] → [] 5 | ``` 6 | 7 | - Action Code: `0x3a` 8 | - Stack: `2 → 0` 9 | - SWF version: `5` 10 | 11 | ## Original documentation 12 | 13 | ### ActionDelete 14 | 15 | ActionDelete deletes a named property from a ScriptObject. 16 | 17 | | Field | Type | Comment | 18 | |-----------------|--------------------|-------------------| 19 | | ActionDelete | ACTIONRECORDHEADER | ActionCode = 0x3A | 20 | 21 | ActionDelete does the following: 22 | 1. Pops the name of the property to delete off the stack. 23 | 2. Pops the object to delete the property from. 24 | -------------------------------------------------------------------------------- /docs/avm1/actions/goto-label.md: -------------------------------------------------------------------------------- 1 | # GotoLabel 2 | 3 | ``` 4 | [] → [] 5 | ``` 6 | 7 | - Action Code: `0x8c` 8 | - Stack: `0 → 0` 9 | - SWF version: `3` 10 | 11 | ## Original documentation 12 | 13 | ### ActionGoToLabel 14 | 15 | ActionGoToLabel instructs Flash Player to go to the frame associated with the specified label. You can attach a 16 | label to a frame with the FrameLabel tag. 17 | 18 | | Field | Type | Comment | 19 | |-----------------|--------------------|-------------------| 20 | | ActionGoToLabel | ACTIONRECORDHEADER | ActionCode = 0x8C | 21 | | Label | STRING | Frame label | 22 | -------------------------------------------------------------------------------- /docs/avm1/actions/remove-sprite.md: -------------------------------------------------------------------------------- 1 | # CloneSprite 2 | 3 | ``` 4 | [target] → [] 5 | ``` 6 | 7 | - Action Code: `0x25` 8 | - Stack: `1 → 0` 9 | - SWF version: `4` 10 | 11 | ## Original documentation 12 | 13 | ### ActionRemoveSprite 14 | 15 | ActionRemoveSprite removes a clone sprite. 16 | 17 | | Field | Type | Comment | 18 | |--------------------|--------------------|-------------------| 19 | | ActionRemoveSprite | ACTIONRECORDHEADER | ActionCode = 0x25 | 20 | 21 | ActionRemoveSprite does the following: 22 | 1. Pops a target off the stack. 23 | 2. Removes the clone movie clip that the target path target identifies. 24 | -------------------------------------------------------------------------------- /docs/avm1/actions/goto-frame.md: -------------------------------------------------------------------------------- 1 | # GotoFrame 2 | 3 | ``` 4 | [] → [] 5 | ``` 6 | 7 | - Action Code: `0x81` 8 | - Stack: `0 → 0` 9 | - SWF version: `3` 10 | 11 | ## Original documentation 12 | 13 | ### ActionGotoFrame 14 | 15 | ActionGotoFrame instructs Flash Player to go to the specified frame in the current file. 16 | 17 | | Field | Type | Comment | 18 | |-----------------|--------------------|---------------------------------------| 19 | | ActionGotoFrame | ACTIONRECORDHEADER | ActionCode = 0x81; Length is always 2 | 20 | | Frame | UI16 | Frame index | 21 | -------------------------------------------------------------------------------- /ts/src/lib/errors/incomplete-stream.mts: -------------------------------------------------------------------------------- 1 | import incident, { Incident } from "incident"; 2 | 3 | export type Name = "IncompleteStream"; 4 | export const name: Name = "IncompleteStream"; 5 | 6 | export interface Data { 7 | needed?: number; 8 | } 9 | 10 | export type Cause = undefined; 11 | export type IncompleteStreamError = Incident; 12 | 13 | export function format({needed}: Data) { 14 | return `Need ${needed === undefined ? "" : needed} more bytes to process the stream`; 15 | } 16 | 17 | export function createIncompleteStreamError(needed?: number): IncompleteStreamError { 18 | return new incident.Incident(name, {needed}, format); 19 | } 20 | -------------------------------------------------------------------------------- /docs/avm1/actions/return.md: -------------------------------------------------------------------------------- 1 | # Return 2 | 3 | ``` 4 | [value] → [] 5 | ``` 6 | 7 | - Action Code: `0x3e` 8 | - Stack: `1 → 0` 9 | - SWF version: `5` 10 | 11 | ## Original documentation 12 | 13 | ### ActionReturn 14 | 15 | ActionReturn forces the return item to be pushed off the stack and returned. If a return is not appropriate, the 16 | return item is discarded. 17 | 18 | | Field | Type | Comment | 19 | |---------------------|--------------------|--------------------------------| 20 | | ActionReturn | ACTIONRECORDHEADER | ActionCode = 0x3E | 21 | 22 | ActionReturn pops a value off the stack. 23 | -------------------------------------------------------------------------------- /docs/avm1/actions/string-add.md: -------------------------------------------------------------------------------- 1 | # StringAdd 2 | 3 | ``` 4 | [b, a] → [String(b) + String(a)] 5 | ``` 6 | 7 | - Action Code: `0x21` 8 | - Stack: `2 → 1` 9 | - SWF version: `4` 10 | 11 | ## Original documentation 12 | 13 | ### ActionStringAdd 14 | 15 | ActionStringAdd concatenates two strings. 16 | 17 | | Field | Type | Comment | 18 | |--------------------|--------------------|-------------------| 19 | | ActionStringAdd | ACTIONRECORDHEADER | ActionCode = 0x21 | 20 | 21 | ActionStringAdd does the following: 22 | 1. Pops value A off the stack. 23 | 2. Pops value B off the stack. 24 | 3. Pushes the concatenation BA to the stack. 25 | -------------------------------------------------------------------------------- /docs/avm1/actions/define-local2.md: -------------------------------------------------------------------------------- 1 | # DefineLocal2 2 | 3 | ``` 4 | [name] → [] 5 | ``` 6 | 7 | - Action Code: `0x41` 8 | - Stack: `1 → 0` 9 | - SWF version: `5` 10 | 11 | ## Original documentation 12 | 13 | ### ActionDefineLocal2 14 | 15 | ActionDefineLocal2 defines a local variable without setting its value. If the variable already exists, nothing 16 | happens. The initial value of the local variable is undefined. 17 | 18 | | Field | Type | Comment | 19 | |-----------------|--------------------|-------------------| 20 | | ActionAnd | ACTIONRECORDHEADER | ActionCode = 0x41 | 21 | 22 | ActionDefineLocal2 pops name off the stack. 23 | -------------------------------------------------------------------------------- /docs/avm1/actions/string-length.md: -------------------------------------------------------------------------------- 1 | # StringLength 2 | 3 | ``` 4 | [a] → [String(a).length] 5 | ``` 6 | 7 | - Action Code: `0x13` 8 | - Stack: `1 → 1` 9 | - SWF version: `4` 10 | 11 | ## Original documentation 12 | 13 | ### ActionStringLength 14 | 15 | ActionStringLength computes the length of a string. 16 | 17 | | Field | Type | Comment | 18 | |--------------------|--------------------|-------------------| 19 | | ActionStringLength | ACTIONRECORDHEADER | ActionCode = 0x14 | 20 | 21 | ActionStringLength does the following: 22 | 1. Pops a string off the stack. 23 | 2. Calculates the length of the string and pushes it to the stack. 24 | -------------------------------------------------------------------------------- /rs/bin/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "swf-parser-bin" 3 | version = "0.14.0" 4 | authors = ["Charles Samborski "] 5 | description = "SWF parser" 6 | documentation = "https://github.com/open-flash/swf-parser" 7 | homepage = "https://github.com/open-flash/swf-parser" 8 | repository = "https://github.com/open-flash/swf-parser" 9 | readme = "../README.md" 10 | keywords = ["parser", "swf", "flash"] 11 | license = "AGPL-3.0-or-later" 12 | edition = "2021" 13 | rust-version = "1.60.0" 14 | 15 | [[bin]] 16 | name = "swf-parser" 17 | path = "src/main.rs" 18 | 19 | [dependencies] 20 | serde_json_v8 = "^0.1.1" 21 | swf-parser = { path = "../." } 22 | swf-types = "^0.14.0" 23 | -------------------------------------------------------------------------------- /docs/avm1/actions/delete2.md: -------------------------------------------------------------------------------- 1 | # Delete 2 | 3 | ``` 4 | [name] → [] 5 | ``` 6 | 7 | - Action Code: `0x3b` 8 | - Stack: `1 → 0` 9 | - SWF version: `5` 10 | 11 | ## Original documentation 12 | 13 | ### ActionDelete2 14 | 15 | ActionDelete2 deletes a named property. Flash Player first looks for the property in the current scope, and if the 16 | property cannot be found, continues to search in the encompassing scopes. 17 | 18 | | Field | Type | Comment | 19 | |-----------------|--------------------|-------------------| 20 | | ActionDelete2 | ACTIONRECORDHEADER | ActionCode = 0x3B | 21 | 22 | ActionDelete2 pops the name of the property to delete off the stack. 23 | -------------------------------------------------------------------------------- /docs/avm1/actions/get-time.md: -------------------------------------------------------------------------------- 1 | # GetTime 2 | 3 | ``` 4 | [] → [msSinceStart()] 5 | ``` 6 | 7 | - Action Code: `0x34` 8 | - Stack: `0 → 1` 9 | - SWF version: `4` 10 | 11 | ## Original documentation 12 | 13 | ### ActionGetTime 14 | 15 | ActionGetTime reports the milliseconds since Adobe Flash Player started. 16 | 17 | | Field | Type | Comment | 18 | |--------------------|--------------------|-------------------| 19 | | ActionGetTime | ACTIONRECORDHEADER | ActionCode = 0x34 | 20 | 21 | ActionGetTime does the following: 22 | 1. Calculates as an integer the number of milliseconds since Flash Player was started. 23 | 2. Pushes the number to the stack. 24 | -------------------------------------------------------------------------------- /ts/src/lib/errors/incomplete-tag.mts: -------------------------------------------------------------------------------- 1 | import incident, { Incident } from "incident"; 2 | 3 | export type Name = "IncompleteTag"; 4 | export const name: Name = "IncompleteTag"; 5 | 6 | export interface Data { 7 | available: number; 8 | needed: number; 9 | } 10 | 11 | export type Cause = undefined; 12 | export type IncompleteTagError = Incident; 13 | 14 | export function format({needed, available}: Data) { 15 | return `Failed to parse tag: Not enough data: ${available} / ${needed} bytes`; 16 | } 17 | 18 | export function createIncompleteTagError(available: number, needed: number): IncompleteTagError { 19 | return new incident.Incident(name, {available, needed}, format); 20 | } 21 | -------------------------------------------------------------------------------- /docs/avm1/actions/define-local.md: -------------------------------------------------------------------------------- 1 | # DefineLocal 2 | 3 | ``` 4 | [name, value] → [] 5 | ``` 6 | 7 | - Action Code: `0x3c` 8 | - Stack: `2 → 0` 9 | - SWF version: `5` 10 | 11 | ## Original documentation 12 | 13 | ### ActionDefineLocal 14 | 15 | ActionDefineLocal defines a local variable and sets its value. If the variable already exists, the value is set to the 16 | newly specified value. 17 | 18 | | Field | Type | Comment | 19 | |-----------------|--------------------|-------------------| 20 | | ActionAnd | ACTIONRECORDHEADER | ActionCode = 0x3C | 21 | 22 | ActionDefineLocal does the following: 23 | 1. Pops a value off the stack. 24 | 2. Pops a name off the stack. 25 | -------------------------------------------------------------------------------- /docs/avm1/actions/stack-swap.md: -------------------------------------------------------------------------------- 1 | # StackSwap 2 | 3 | ``` 4 | [item2, item1] → [item1, item2] 5 | ``` 6 | 7 | - Action Code: `0x4d` 8 | - Stack: `2 → 2` 9 | - SWF version: `5` 10 | 11 | ## Original documentation 12 | 13 | ### ActionStackSwap 14 | 15 | ActionStackSwap swaps the top two ScriptAtoms on the stack. 16 | 17 | | Field | Type | Comment | 18 | |---------------------|--------------------|--------------------------------| 19 | | ActionStackSwap | ACTIONRECORDHEADER | ActionCode = 0x4D | 20 | 21 | ActionStackSwap does the following: 22 | 1. Pops Item1 and then Item2 off of the stack. 23 | 2. Pushes Item1 and then Item2 back to the stack. 24 | -------------------------------------------------------------------------------- /docs/avm1/actions/random-number.md: -------------------------------------------------------------------------------- 1 | # RandomNumber 2 | 3 | ``` 4 | [max] → [randomInt(0, max)] 5 | ``` 6 | 7 | - Action Code: `0x30` 8 | - Stack: `1 → 1` 9 | - SWF version: `4` 10 | 11 | ## Original documentation 12 | 13 | ### ActionRandomNumber 14 | 15 | ActionRandomNumber calculates a random number. 16 | 17 | | Field | Type | Comment | 18 | |--------------------|--------------------|-------------------| 19 | | ActionRandomNumber | ACTIONRECORDHEADER | ActionCode = 0x30 | 20 | 21 | ActionRandomNumber does the following: 22 | 1. Pops the maximum off the stack. 23 | 2. Calculates a random number as an integer in the range 0...(maximum-1). 24 | 3. Pushes the random number to the stack. 25 | -------------------------------------------------------------------------------- /docs/avm1/actions/ascii-to-char.md: -------------------------------------------------------------------------------- 1 | # AsciiToChar 2 | 3 | ``` 4 | [a] → [asciiToChar(a)] 5 | ``` 6 | 7 | - Action Code: `0x33` 8 | - Stack: `1 → 1` 9 | - SWF version: `4` 10 | 11 | ## Original documentation 12 | 13 | ### ActionAsciiToChar 14 | 15 | ActionAsciiToChar converts a value to an ASCII character code. 16 | 17 | | Field | Type | Comment | 18 | |-------------------|--------------------|-------------------| 19 | | ActionAsciiToChar | ACTIONRECORDHEADER | ActionCode = 0x33 | 20 | 21 | ActionAsciiToChar does the following: 22 | 1. Pops a value off the stack. 23 | 2. Converts the value from a number to the corresponding ASCII character. 24 | 3. Pushes the resulting character to the stack. 25 | -------------------------------------------------------------------------------- /docs/avm1/actions/char-to-ascii.md: -------------------------------------------------------------------------------- 1 | # CharToAscii 2 | 3 | ``` 4 | [a] → [charToAscii(a)] 5 | ``` 6 | 7 | - Action Code: `0x32` 8 | - Stack: `1 → 1` 9 | - SWF version: `4` 10 | 11 | ## Original documentation 12 | 13 | ### ActionCharToAscii 14 | 15 | ActionCharToAscii converts character code to ASCII. 16 | 17 | | Field | Type | Comment | 18 | |-------------------|--------------------|-------------------| 19 | | ActionCharToAscii | ACTIONRECORDHEADER | ActionCode = 0x32 | 20 | 21 | ActionCharToAscii does the following: 22 | 1. Pops a value off the stack. 23 | 2. Converts the first character of the value to a numeric ASCII character code. 24 | 3. Pushes the resulting character code to the stack. 25 | -------------------------------------------------------------------------------- /docs/avm1/actions/to-integer.md: -------------------------------------------------------------------------------- 1 | # ToInteger 2 | 3 | ``` 4 | [a] → [~~Number(a)] 5 | ``` 6 | 7 | - Action Code: `0x18` 8 | - Stack: `1 → 1` 9 | - SWF version: `4` 10 | 11 | ## Original documentation 12 | 13 | ### ActionToInteger 14 | 15 | ActionToInteger converts a value to an integer. 16 | 17 | | Field | Type | Comment | 18 | |-----------------|--------------------|-------------------| 19 | | ActionToInteger | ACTIONRECORDHEADER | ActionCode = 0x18 | 20 | 21 | ActionToInteger does the following: 22 | 1. Pops a value off the stack. 23 | 2. Converts the value to a number. 24 | 3. Discards any digits after the decimal point, resulting in an integer. 25 | 4. Pushes the resulting integer to the stack. 26 | -------------------------------------------------------------------------------- /docs/avm1/actions/set-target2.md: -------------------------------------------------------------------------------- 1 | # SetTarget2 2 | 3 | ``` 4 | [target] → [] 5 | ``` 6 | 7 | - Action Code: `0x20` 8 | - Stack: `1 → 0` 9 | - SWF version: `4` 10 | 11 | ## Original documentation 12 | 13 | ### ActionSetTarget2 14 | 15 | ActionSetTarget2 sets the current context and is stack based. 16 | 17 | | Field | Type | Comment | 18 | |------------------|--------------------|-------------------| 19 | | ActionSetTarget2 | ACTIONRECORDHEADER | ActionCode = 0x20 | 20 | 21 | ActionSetTarget2 pops the target off the stack and makes it the current active context. 22 | This action behaves exactly like ActionSetTarget but is stack based to enable the target path to be the result of 23 | expression evaluation. 24 | -------------------------------------------------------------------------------- /docs/avm1/actions/constant-pool.md: -------------------------------------------------------------------------------- 1 | # ConstantPool 2 | 3 | ``` 4 | [] → [] 5 | ``` 6 | 7 | - Action Code: `0x88` 8 | - Stack: `0 → 0` 9 | - SWF version: `5` 10 | 11 | ## Original documentation 12 | 13 | ### ActionConstantPool 14 | 15 | ActionConstantPool creates a new constant pool, and replaces the old constant pool if one already exists. 16 | 17 | | Field | Type | Comment | 18 | |--------------------|--------------------|-------------------------------| 19 | | ActionConstantPool | ACTIONRECORDHEADER | ActionCode = 0x88 | 20 | | Count | UI16 | Number of constants to follow | 21 | | ConstantPool | STRING[Count] | String constants | 22 | -------------------------------------------------------------------------------- /docs/avm1/actions/clone-sprite.md: -------------------------------------------------------------------------------- 1 | # CloneSprite 2 | 3 | ``` 4 | [source, target, depth] → [] 5 | ``` 6 | 7 | - Action Code: `0x24` 8 | - Stack: `3 → 0` 9 | - SWF version: `4` 10 | 11 | ## Original documentation 12 | 13 | ### ActionCloneSprite 14 | 15 | ActionCloneSprite clones a sprite. 16 | 17 | | Field | Type | Comment | 18 | |-------------------|--------------------|-------------------| 19 | | ActionCloneSprite | ACTIONRECORDHEADER | ActionCode = 0x24 | 20 | 21 | ActionCloneSprite does the following: 22 | 1. Pops a depth off the stack. 23 | 2. Pops a target off the stack. 24 | 3. Pops a source off the stack. 25 | 4. Duplicates the movie clip source, giving the new instance the name target, at z-order depth 26 | -------------------------------------------------------------------------------- /docs/avm1/actions/decrement.md: -------------------------------------------------------------------------------- 1 | # Decrement 2 | 3 | ``` 4 | [value] → [Number(value) - 1] 5 | ``` 6 | 7 | - Action Code: `0x51` 8 | - Stack: `1 → 1` 9 | - SWF version: `5` 10 | 11 | ## Original documentation 12 | 13 | ### ActionDecrement 14 | 15 | ActionDecrement pops a value from the stack, converts it to number type, decrements it by 1, and pushes it 16 | back to the stack. 17 | 18 | | Field | Type | Comment | 19 | |-------------------|--------------------|--------------------------------| 20 | | ActionDecrement | ACTIONRECORDHEADER | ActionCode = 0x51 | 21 | 22 | ActionDecrement does the following: 23 | 1. Pops the number off of the stack. 24 | 2. Pushes the result on to the stack. 25 | -------------------------------------------------------------------------------- /docs/avm1/actions/increment.md: -------------------------------------------------------------------------------- 1 | # Increment 2 | 3 | ``` 4 | [value] → [Number(value) + 1] 5 | ``` 6 | 7 | - Action Code: `0x50` 8 | - Stack: `1 → 1` 9 | - SWF version: `5` 10 | 11 | ## Original documentation 12 | 13 | ### ActionIncrement 14 | 15 | ActionIncrement pops a value from the stack, converts it to number type, increments it by 1, and pushes it back 16 | to the stack. 17 | 18 | | Field | Type | Comment | 19 | |-------------------|--------------------|--------------------------------| 20 | | ActionIncrement | ACTIONRECORDHEADER | ActionCode = 0x50 | 21 | 22 | ActionIncrement does the following: 23 | 1. Pops the number off of the stack. 24 | 2. Pushes the result on to the stack. 25 | -------------------------------------------------------------------------------- /docs/avm1/actions/equals2.md: -------------------------------------------------------------------------------- 1 | # Equals2 2 | 3 | ``` 4 | [arg2, arg1] → [arg2 == arg1] 5 | ``` 6 | 7 | - Action Code: `0x49` 8 | - Stack: `2 → 1` 9 | - SWF version: `5` 10 | 11 | ## Original documentation 12 | 13 | ### ActionEquals2 14 | 15 | ActionEquals2 is similar to ActionEquals, but ActionEquals2 knows about types. The equality comparison 16 | algorithm from ECMA-262 Section 11.9.3 is applied. 17 | 18 | | Field | Type | Comment | 19 | |-----------------|--------------------|-------------------| 20 | | ActionEquals2 | ACTIONRECORDHEADER | ActionCode = 0x49 | 21 | 22 | ActionEquals2 does the following: 23 | 1. Pops arg1 off the stack. 24 | 2. Pops arg2 off the stack. 25 | 3. Pushes the return value to the stack. 26 | -------------------------------------------------------------------------------- /docs/avm1/actions/modulo.md: -------------------------------------------------------------------------------- 1 | # Modulo 2 | 3 | ``` 4 | [arg2, arg1] → [arg2 % arg1] 5 | ``` 6 | 7 | - Action Code: `0x3f` 8 | - Stack: `1 → 1` 9 | - SWF version: `5` 10 | 11 | **Note**: This is not a modulo but remainder. 12 | 13 | ## Original documentation 14 | 15 | ### ActionModulo 16 | 17 | ActionModulo calculates x modulo y. If y is 0, then NaN (0x7FC00000) is pushed to the stack. 18 | 19 | | Field | Type | Comment | 20 | |-------------------|--------------------|--------------------------------| 21 | | ActionModulo | ACTIONRECORDHEADER | ActionCode = 0x3F | 22 | 23 | ActionModulo does the following: 24 | 1. Pops x then y off of the stack. 25 | 2. Pushes the value x % y on to the stack. 26 | -------------------------------------------------------------------------------- /docs/avm1/actions/set-property.md: -------------------------------------------------------------------------------- 1 | # SetProperty 2 | 3 | ``` 4 | [target, index, target] → [] 5 | ``` 6 | 7 | - Action Code: `0x23` 8 | - Stack: `3 → 0` 9 | - SWF version: `4` 10 | 11 | ## Original documentation 12 | 13 | ### ActionSetProperty 14 | 15 | ActionSetProperty sets a file property. 16 | 17 | | Field | Type | Comment | 18 | |-------------------|--------------------|-------------------| 19 | | ActionSetProperty | ACTIONRECORDHEADER | ActionCode = 0x23 | 20 | 21 | ActionSetProperty does the following: 22 | 1. Pops a value off the stack. 23 | 2. Pops an index off the stack. 24 | 3. Pops a target off the stack. 25 | 4. Sets the property enumerated as index in the movie clip with the target path target to the value value. 26 | -------------------------------------------------------------------------------- /docs/avm1/actions/jump.md: -------------------------------------------------------------------------------- 1 | # Jump 2 | 3 | ``` 4 | [] → [] 5 | ``` 6 | 7 | - Action Code: `0x99` 8 | - Stack: `0 → 0` 9 | - SWF version: `4` 10 | 11 | ## Original documentation 12 | 13 | ### ActionJump 14 | 15 | ActionJump creates an unconditional branch. 16 | 17 | | Field | Type | Comment | 18 | |--------------------|--------------------|-------------------| 19 | | ActionJump | ACTIONRECORDHEADER | ActionCode = 0x99 | 20 | | BranchOffset | SI16 | Offset | 21 | 22 | ActionJump adds BranchOffset bytes to the instruction pointer in the execution stream. The offset is a signed 23 | quantity, enabling branches from –32,768 bytes to 32,767 bytes. An offset of 0 points to the action directly after 24 | the ActionJump action. 25 | -------------------------------------------------------------------------------- /docs/avm1/actions/target-path.md: -------------------------------------------------------------------------------- 1 | # SetMember 2 | 3 | ``` 4 | [object] → [targetPath] 5 | ``` 6 | 7 | - Action Code: `0x45` 8 | - Stack: `1 → 1` 9 | - SWF version: `5` 10 | 11 | ## Original documentation 12 | 13 | ### ActionTargetPath 14 | 15 | If the object in the stack is of type MovieClip, the object’s target path is pushed on the stack in dot notation. If 16 | the object is not a MovieClip, the result is undefined rather than the movie clip target path. 17 | 18 | | Field | Type | Comment | 19 | |-------------------|--------------------|-------------------| 20 | | ActionTargetPath | ACTIONRECORDHEADER | ActionCode = 0x45 | 21 | 22 | ActionTargetPath does the following: 23 | 1. Pops the object off the stack. 24 | 2. Pushes the target path onto the stack. 25 | -------------------------------------------------------------------------------- /docs/avm1/actions/add.md: -------------------------------------------------------------------------------- 1 | # Add 2 | 3 | ``` 4 | [arg2, arg1] → [add(arg2, arg1)] 5 | ``` 6 | 7 | - Action Code: `0x0a` 8 | - Stack: `2 → 1` 9 | - SWF version: `4` 10 | 11 | ## Original documentation 12 | 13 | ### ActionAdd 14 | 15 | ActionAdd adds two numbers and pushes the result back to the stack. 16 | 17 | | Field | Type | Comment | 18 | |-------------------|--------------------|--------------------------------| 19 | | ActionAdd | ACTIONRECORDHEADER | ActionCode = 0x0A | 20 | 21 | ActionAdd does the following: 22 | 1. Pops value A off the stack. 23 | 2. Pops value B off the stack. 24 | 3. Converts A and B to floating-point; non-numeric values evaluate to 0. 25 | 4. Adds the numbers A and B. 26 | 5. Pushes the result, A+B, to the stack. 27 | -------------------------------------------------------------------------------- /docs/avm1/actions/strict-equals.md: -------------------------------------------------------------------------------- 1 | # StrictEquals 2 | 3 | ``` 4 | [arg2, arg1] → [arg2 === arg1] 5 | ``` 6 | 7 | - Action Code: `0x66` 8 | - Stack: `2 → 1` 9 | - SWF version: `6` 10 | 11 | ## Original documentation 12 | 13 | ### ActionStrictEquals 14 | 15 | ActionStrictEquals is similar to ActionEquals2, but the two arguments must be of the same type in order to be 16 | considered equal. Implements the ‘===’ operator from the ActionScript language. 17 | 18 | | Field | Type | Comment | 19 | |--------------------|--------------------|-------------------| 20 | | ActionStrictEquals | ACTIONRECORDHEADER | ActionCode = 0x66 | 21 | 22 | ActionStrictEquals does the following: 23 | 1. Pops arg1 then arg2 off the stack. 24 | 2. Pushes the return value, a Boolean value, to the stack. 25 | -------------------------------------------------------------------------------- /docs/avm1/actions/string-less.md: -------------------------------------------------------------------------------- 1 | # StringLess 2 | 3 | ``` 4 | [b, a] → [stringLess(b, a)] 5 | ``` 6 | 7 | - Action Code: `0x29` 8 | - Stack: `2 → 1` 9 | - SWF version: `4` 10 | 11 | ## Original documentation 12 | 13 | ### ActionStringLess 14 | 15 | ActionStringAdd concatenates two strings. 16 | 17 | | Field | Type | Comment | 18 | |--------------------|--------------------|-------------------| 19 | | ActionStringLess | ACTIONRECORDHEADER | ActionCode = 0x29 | 20 | 21 | ActionStringLess does the following: 22 | 1. Pops value A off the stack. 23 | 2. Pops value B off the stack. 24 | 3. If B < A using a byte-by-byte comparison, true is pushed to the stack for SWF 5 and later (SWF 4 pushes 25 | 1); otherwise, false is pushed to the stack for SWF 5 and later (SWF 4 pushes 0). 26 | -------------------------------------------------------------------------------- /rs/bin/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::fs::File; 3 | use std::io::prelude::*; 4 | 5 | use swf_parser::complete::parse_swf; 6 | 7 | use swf_types as swf; 8 | 9 | fn main() { 10 | let args: Vec = env::args().collect(); 11 | if args.len() < 2 { 12 | println!("Missing input path"); 13 | return; 14 | } 15 | 16 | let file_path = &args[1]; 17 | // println!("Reading file: {}", filename); 18 | 19 | let mut file = File::open(file_path).expect("File not found"); 20 | let mut data: Vec = Vec::new(); 21 | file.read_to_end(&mut data).expect("Unable to read file"); 22 | 23 | // println!("Input:\n{:?}", &data); 24 | 25 | let movie: swf::Movie = parse_swf(&data[..]).expect("Failed to parse movie"); 26 | println!("{}", serde_json_v8::to_string_pretty(&movie).unwrap()); 27 | } 28 | -------------------------------------------------------------------------------- /docs/avm1/actions/trace.md: -------------------------------------------------------------------------------- 1 | # Trace 2 | 3 | ``` 4 | [value] → [] 5 | ``` 6 | 7 | - Action Code: `0x26` 8 | - Stack: `1 → 0` 9 | - SWF version: `4` 10 | 11 | ## Original documentation 12 | 13 | ### ActionWaitForFrame2 14 | 15 | ActionTrace sends a debugging output string. 16 | 17 | | Field | Type | Comment | 18 | |---------------------|--------------------|---------------------------------------| 19 | | ActionTrace | ACTIONRECORDHEADER | ActionCode = 0x26 | 20 | 21 | ActionTrace does the following: 22 | 1. Pops a value off the stack. 23 | 2. In the Test Movie mode of the Adobe Flash editor, ActionTrace appends a value to the output window if 24 | the debugging level is not set to None. 25 | In Adobe Flash Player, nothing happens. 26 | -------------------------------------------------------------------------------- /docs/avm1/actions/subtract.md: -------------------------------------------------------------------------------- 1 | # Subtract 2 | 3 | ``` 4 | [arg2, arg1] → [arg2 - arg1] 5 | ``` 6 | 7 | - Action Code: `0x0b` 8 | - Stack: `2 → 1` 9 | - SWF version: `4` 10 | 11 | ## Original documentation 12 | 13 | ### ActionAdd 14 | 15 | ActionSubtract subtracts two numbers and pushes the result back to the stack. 16 | 17 | | Field | Type | Comment | 18 | |-------------------|--------------------|--------------------------------| 19 | | ActionSubtract | ACTIONRECORDHEADER | ActionCode = 0x0B | 20 | 21 | ActionSubtract does the following: 22 | 1. Pops value A off the stack. 23 | 2. Pops value B off the stack. 24 | 3. Converts A and B to floating-point; non-numeric values evaluate to 0. 25 | 4. Subtracts A from B. 26 | 5. Pushes the result, B-A, to the stack. 27 | -------------------------------------------------------------------------------- /docs/avm1/actions/multiply.md: -------------------------------------------------------------------------------- 1 | # Multiply 2 | 3 | ``` 4 | [arg2, arg1] → [arg2 * arg1] 5 | ``` 6 | 7 | - Action Code: `0x0c` 8 | - Stack: `2 → 1` 9 | - SWF version: `4` 10 | 11 | ## Original documentation 12 | 13 | ### ActionMultiply 14 | 15 | ActionMultiply multiplies two numbers and pushes the result back to the stack. 16 | 17 | | Field | Type | Comment | 18 | |-------------------|--------------------|--------------------------------| 19 | | ActionMultiply | ACTIONRECORDHEADER | ActionCode = 0x0C | 20 | 21 | ActionMultiply does the following: 22 | 1. Pops value A off the stack. 23 | 2. Pops value B off the stack. 24 | 3. Converts A and B to floating-point; non-numeric values evaluate to 0. 25 | 4. Multiplies A times B. 26 | 5. Pushes the result, A*B, to the stack. 27 | -------------------------------------------------------------------------------- /docs/avm1/actions/string-greater.md: -------------------------------------------------------------------------------- 1 | # Greater 2 | 3 | ``` 4 | [arg2, arg1] → [stringLess(arg2, arg1)] 5 | ``` 6 | 7 | - Action Code: `0x68` 8 | - Stack: `2 → 1` 9 | - SWF version: `6` 10 | 11 | ## Original documentation 12 | 13 | ### ActionStringGreater 14 | 15 | ActionStringGreater is the exact opposite of ActionStringLess. This action code was added for the same reasons 16 | as ActionGreater. 17 | 18 | | Field | Type | Comment | 19 | |---------------------|--------------------|-------------------| 20 | | ActionStringGreater | ACTIONRECORDHEADER | ActionCode = 0x68 | 21 | 22 | ActionStringGreater does the following: 23 | 1. Pops arg1 and then arg2 off of the stack. 24 | 2. Compares if arg2 > arg1, using byte-by-byte comparison. 25 | 3. Pushes the return value, a Boolean value, onto the stack. 26 | -------------------------------------------------------------------------------- /ts/src/main/main.mts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import { JSON_VALUE_WRITER } from "kryo-json/json-value-writer"; 3 | import * as sysPath from "path"; 4 | import { $Movie, Movie } from "swf-types/movie"; 5 | 6 | import { parseSwf } from "../lib/index.mjs"; 7 | 8 | async function main(): Promise { 9 | if (process.argv.length < 3) { 10 | console.error("Missing input path"); 11 | return; 12 | } 13 | const filePath: string = process.argv[2]; 14 | const absFilePath: string = sysPath.resolve(filePath); 15 | const bytes: Uint8Array = fs.readFileSync(absFilePath); 16 | const movie: Movie = parseSwf(bytes); 17 | console.log(JSON.stringify($Movie.write(JSON_VALUE_WRITER, movie), null, 2)); 18 | } 19 | 20 | main() 21 | .catch((err: Error): never => { 22 | console.error(err.stack); 23 | process.exit(1); 24 | }); 25 | -------------------------------------------------------------------------------- /docs/avm1/actions/less.md: -------------------------------------------------------------------------------- 1 | # Less 2 | 3 | ``` 4 | [b, a] → [b < a] 5 | ``` 6 | 7 | - Action Code: `0x0f` 8 | - Stack: `2 → 1` 9 | - SWF version: `4` 10 | 11 | ## Original documentation 12 | 13 | ### ActionLess 14 | 15 | ActionLess tests if a number is less than another number 16 | 17 | | Field | Type | Comment | 18 | |-----------------|--------------------|-------------------| 19 | | ActionLess | ACTIONRECORDHEADER | ActionCode = 0x0F | 20 | 21 | ActionLess does the following: 22 | 1. Pops value A off the stack. 23 | 2. Pops value B off the stack. 24 | 3. Converts A and B to floating-point; non-numeric values evaluate to 0. 25 | 4. If B < A, true is pushed to the stack for SWF 5 and later (1 is pushed for SWF 4); 26 | otherwise, false is pushed to the stack for SWF 5 and later (0 is pushed for SWF 4). 27 | -------------------------------------------------------------------------------- /docs/avm1/actions/store-register.md: -------------------------------------------------------------------------------- 1 | # StoreRegister 2 | 3 | ``` 4 | [value] → [value] 5 | ``` 6 | 7 | - Action Code: `0x87` 8 | - Stack: `1 → 1` 9 | - SWF version: `5` 10 | 11 | ## Original documentation 12 | 13 | ### ActionStoreRegister 14 | 15 | ActionStoreRegister reads the next object from the stack (without popping it) and stores it in one of four 16 | registers. If ActionDefineFunction2 is used, up to 256 registers are available. 17 | 18 | | Field | Type | Comment | 19 | |---------------------|--------------------|--------------------------------| 20 | | ActionStoreRegister | ACTIONRECORDHEADER | ActionCode = 0x87 | 21 | | RegisterNumber | UI8 | | 22 | 23 | ActionStoreRegister parses register number from the StoreRegister tag. 24 | -------------------------------------------------------------------------------- /docs/avm1/actions/or.md: -------------------------------------------------------------------------------- 1 | # Or 2 | 3 | ``` 4 | [b, a] → [b || a] 5 | ``` 6 | 7 | - Action Code: `0x11` 8 | - Stack: `2 → 1` 9 | - SWF version: `4` 10 | 11 | ## Original documentation 12 | 13 | ### ActionOr 14 | 15 | ActionOr performs a logical OR of two numbers. 16 | 17 | | Field | Type | Comment | 18 | |-----------------|--------------------|-------------------| 19 | | ActionOr | ACTIONRECORDHEADER | ActionCode = 0x11 | 20 | 21 | ActionOr does the following: 22 | 1. Pops value A off the stack. 23 | 2. Pops value B off the stack. 24 | 3. Converts A and B to floating-point; non-numeric values evaluate to 0. 25 | 4. If either of the numbers is nonzero, true is pushed to the stack for SWF 5 and later (1 is pushed for SWF 26 | 4); otherwise, false is pushed to the stack for SWF 5 and later (0 is pushed for SWF 4). 27 | -------------------------------------------------------------------------------- /.github/workflows/check-ts.yml: -------------------------------------------------------------------------------- 1 | name: "check-ts" 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | check-ts: 11 | runs-on: "ubuntu-latest" 12 | 13 | strategy: 14 | matrix: 15 | # Support policy: Current and all LTS. 16 | node-version: ["14", "16", "18"] 17 | 18 | steps: 19 | - uses: actions/checkout@v3 20 | with: 21 | submodules: "recursive" 22 | 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v2 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | cache: "yarn" 28 | cache-dependency-path: "ts/yarn.lock" 29 | 30 | - name: Build and test 31 | run: | 32 | cd ts 33 | yarn install 34 | yarn test 35 | -------------------------------------------------------------------------------- /docs/avm1/actions/bit-or.md: -------------------------------------------------------------------------------- 1 | # BitOr 2 | 3 | ``` 4 | [arg2, arg1] → [arg2 | arg1] 5 | ``` 6 | 7 | - Action Code: `0x61` 8 | - Stack: `1 → 1` 9 | - SWF version: `5` 10 | 11 | ## Original documentation 12 | 13 | ### ActionBitOr 14 | 15 | ActionBitOr pops two numbers off of the stack, performs a bitwise OR, and pushes an S32 number to the stack. 16 | The arguments are converted to 32-bit unsigned integers before performing the bitwise operation. The result is 17 | a SIGNED 32-bit integer. 18 | 19 | | Field | Type | Comment | 20 | |-------------------|--------------------|--------------------------------| 21 | | ActionBitOr | ACTIONRECORDHEADER | ActionCode = 0x61 | 22 | 23 | ActionBitOr does the following: 24 | 1. Pops arg1 then arg2 off of the stack. 25 | 2. Pushes the result to the stack. 26 | -------------------------------------------------------------------------------- /docs/avm1/actions/to-string.md: -------------------------------------------------------------------------------- 1 | # ToString 2 | 3 | ``` 4 | [object] → ["" + object] 5 | ``` 6 | 7 | - Action Code: `0x4b` 8 | - Stack: `1 → 1` 9 | - SWF version: `5` 10 | 11 | ## Original documentation 12 | 13 | ### ActionToString 14 | 15 | ActionToString converts the object on the top of the stack into a String, and pushes the string back to the stack. 16 | For the Object type, the ActionScript toString() method is invoked to convert the object to the String type for 17 | ActionToString. 18 | 19 | | Field | Type | Comment | 20 | |-------------------|--------------------|--------------------------------| 21 | | ActionToString | ACTIONRECORDHEADER | ActionCode = 0x4B | 22 | 23 | ActionToString does the following: 24 | 1. Pops the object off of the stack. 25 | 2. Pushes the string on to the stack. 26 | -------------------------------------------------------------------------------- /docs/avm1/actions/wait-for-frame.md: -------------------------------------------------------------------------------- 1 | # WaitForFrame 2 | 3 | ``` 4 | [] → [] 5 | ``` 6 | 7 | - Action Code: `0x8a` 8 | - Stack: `0 → 0` 9 | - SWF version: `3` 10 | 11 | ## Original documentation 12 | 13 | ### ActionWaitForFrame 14 | 15 | ActionWaitForFrame instructs Flash Player to wait until the specified frame; otherwise skips the specified 16 | number of actions. 17 | 18 | | Field | Type | Comment | 19 | |--------------------|--------------------|--------------------------------------------------| 20 | | ActionWaitForFrame | ACTIONRECORDHEADER | ActionCode = 0x8A; Length is always 3 | 21 | | Frame | UI16 | Frame to wait for | 22 | | SkipCount | UI8 | Number of actions to skip if frame is not loaded | 23 | -------------------------------------------------------------------------------- /docs/avm1/actions/bit-xor.md: -------------------------------------------------------------------------------- 1 | # BitXor 2 | 3 | ``` 4 | [arg2, arg1] → [arg2 ^ arg1] 5 | ``` 6 | 7 | - Action Code: `0x62` 8 | - Stack: `1 → 1` 9 | - SWF version: `5` 10 | 11 | ## Original documentation 12 | 13 | ### ActionBitXor 14 | 15 | ActionBitXor pops two numbers off of the stack, performs a bitwise XOR, and pushes an S32 number to the 16 | stack. 17 | 18 | The arguments are converted to 32-bit unsigned integers before performing the bitwise operation. The result is 19 | a SIGNED 32-bit integer. 20 | 21 | | Field | Type | Comment | 22 | |-------------------|--------------------|--------------------------------| 23 | | ActionBitXor | ACTIONRECORDHEADER | ActionCode = 0x62 | 24 | 25 | ActionBitXor does the following: 26 | 1. Pops arg1 and arg2 off of the stack. 27 | 2. Pushes the result back to the stack. 28 | -------------------------------------------------------------------------------- /docs/avm1/actions/mb-string-length.md: -------------------------------------------------------------------------------- 1 | # MbStringLength 2 | 3 | ``` 4 | [a] → [String(a).length] 5 | ``` 6 | 7 | - Action Code: `0x31` 8 | - Stack: `1 → 1` 9 | - SWF version: `4` 10 | 11 | ## Original documentation 12 | 13 | ### ActionMBStringLength 14 | 15 | ActionMBStringLength computes the length of a string and is multi-byte aware. 16 | 17 | | Field | Type | Comment | 18 | |----------------------|--------------------|-------------------| 19 | | ActionMBStringLength | ACTIONRECORDHEADER | ActionCode = 0x31 | 20 | 21 | ActionMBStringLength does the following: 22 | 1. Pops a string off the stack. 23 | 2. Calculates the length of the string in characters and pushes it to the stack. 24 | 25 | This is a multi-byte aware version of ActionStringLength. On systems with double-byte support, a double-byte 26 | character is counted as a single character. 27 | -------------------------------------------------------------------------------- /docs/avm1/actions/set-member.md: -------------------------------------------------------------------------------- 1 | # SetMember 2 | 3 | ``` 4 | [object, propertyName, newValue] → [] 5 | ``` 6 | 7 | - Action Code: `0x1d` 8 | - Stack: `3 → 0` 9 | - SWF version: `5` 10 | 11 | **Note**: The is a typo in the step 2 of the official documentation ("property name"). 12 | 13 | ## Original documentation 14 | 15 | ### ActionSetMember 16 | 17 | ActionSetMember sets a property of an object. If the property does not already exist, it is created. Any existing 18 | value in the property is overwritten. 19 | 20 | | Field | Type | Comment | 21 | |-------------------|--------------------|-------------------| 22 | | ActionSetMember | ACTIONRECORDHEADER | ActionCode = 0x45 | 23 | 24 | ActionSetMember does the following: 25 | 1. Pops the new value off the stack. 26 | 2. Pops the object name off the stack. 27 | 3. Pops the object off of the stack. 28 | -------------------------------------------------------------------------------- /docs/avm1/actions/get-variable.md: -------------------------------------------------------------------------------- 1 | # GetVariable 2 | 3 | ``` 4 | [name] → [eval(name)] 5 | ``` 6 | 7 | - Action Code: `0x1c` 8 | - Stack: `1 → 1` 9 | - SWF version: `4` 10 | 11 | ## Original documentation 12 | 13 | ### ActionGetVariable 14 | 15 | ActionGetVariable gets a variable’s value. 16 | 17 | | Field | Type | Comment | 18 | |-------------------|--------------------|-------------------| 19 | | ActionGetVariable | ACTIONRECORDHEADER | ActionCode = 0x1C | 20 | 21 | ActionGetVariable does the following: 22 | 1. Pops a name off the stack, a string that names is the variable to get. 23 | 2. Pushes the value of the variable to the stack. 24 | A variable in another execution context can be referenced by prefixing the variable name with the target path 25 | and a colon. For example: /A/B:FOO references variable FOO in a movie clip with a target path of /A/B. 26 | -------------------------------------------------------------------------------- /docs/avm1/actions/string-equals.md: -------------------------------------------------------------------------------- 1 | # StringEquals 2 | 3 | ``` 4 | [b, a] → [String(b) === String(a)] 5 | ``` 6 | 7 | - Action Code: `0x13` 8 | - Stack: `2 → 1` 9 | - SWF version: `4` 10 | 11 | ## Original documentation 12 | 13 | ### ActionStringEquals 14 | 15 | ActionStringEquals tests two strings for equality. 16 | 17 | | Field | Type | Comment | 18 | |--------------------|--------------------|-------------------| 19 | | ActionStringEquals | ACTIONRECORDHEADER | ActionCode = 0x13 | 20 | 21 | ActionStringEquals does the following: 22 | 1. Pops value A off the stack. 23 | 2. Pops value B off the stack. 24 | 3. Compares A and B as strings.( The comparison is case-sensitive) 25 | 4. If the strings are equal, true is pushed to the stack for SWF 5 and later (SWF 4 pushes 1). 26 | 5. Otherwise, false is pushed to the stack for SWF 5 and later (SWF 4 pushes 0). 27 | -------------------------------------------------------------------------------- /docs/avm1/actions/enumerate2.md: -------------------------------------------------------------------------------- 1 | # Enumerate2 2 | 3 | ``` 4 | [object] → [null, ...propertieNames] 5 | ``` 6 | 7 | - Action Code: `0x46` 8 | - Stack: `1 → 1+` 9 | - SWF version: `6` 10 | 11 | ## Original documentation 12 | 13 | ### ActionEnumerate2 14 | 15 | ActionEnumerate2 is similar to ActionEnumerate, but uses a stack argument of object type rather than using a 16 | string to specify its name. 17 | 18 | | Field | Type | Comment | 19 | |--------------------|--------------------|-------------------| 20 | | ActionEnumerate2 | ACTIONRECORDHEADER | ActionCode = 0x55 | 21 | 22 | ActionEnumerate2 does the following: 23 | 1. Pops obj off of the stack. 24 | 2. Pushes a null value onto the stack to indicate the end of the slot names. 25 | 3. Pushes each slot name (a string) from obj onto the stack. 26 | 27 | **Note**: The order in which slot names are pushed is undefined. 28 | -------------------------------------------------------------------------------- /docs/avm1/actions/add2.md: -------------------------------------------------------------------------------- 1 | # Add2 2 | 3 | ``` 4 | [arg2, arg1] → [arg2 + arg1] 5 | ``` 6 | 7 | - Action Code: `0x47` 8 | - Stack: `2 → 1` 9 | - SWF version: `5` 10 | 11 | ## Original documentation 12 | 13 | ### ActionAdd2 14 | 15 | ActionAdd2 is similar to ActionAdd, but performs the addition differently, according to the data types of the 16 | arguments. The addition operator algorithm in ECMA-262 Section 11.6.1 is used. If string concatenation is 17 | applied, the concatenated string is arg2 followed by arg1. 18 | 19 | | Field | Type | Comment | 20 | |-------------------|--------------------|--------------------------------| 21 | | ActionAdd2 | ACTIONRECORDHEADER | ActionCode = 0x47 | 22 | 23 | ActionAdd2 does the following: 24 | 1. Pops arg1 off of the stack. 25 | 2. Pops arg2 off of the stack. 26 | 3. Pushes the result back to the stack. 27 | -------------------------------------------------------------------------------- /docs/avm1/actions/to-number.md: -------------------------------------------------------------------------------- 1 | # ToNumber 2 | 3 | ``` 4 | [object] → [+object] 5 | ``` 6 | 7 | - Action Code: `0x4a` 8 | - Stack: `1 → 1` 9 | - SWF version: `5` 10 | 11 | ## Original documentation 12 | 13 | ### ActionToNumber 14 | 15 | Converts the object on the top of the stack into a number, and pushes the number back to the stack. 16 | For the Object type, the ActionScript valueOf() method is invoked to convert the object to a Number type for 17 | ActionToNumber. Conversions between primitive types, such as from String to Number, are built-in. 18 | 19 | | Field | Type | Comment | 20 | |-------------------|--------------------|--------------------------------| 21 | | ActionToNumber | ACTIONRECORDHEADER | ActionCode = 0x4A | 22 | 23 | ActionToNumber does the following: 24 | 1. Pops the object off of the stack. 25 | 2. Pushes the number on to the stack. 26 | -------------------------------------------------------------------------------- /docs/avm1/actions/type-of.md: -------------------------------------------------------------------------------- 1 | # TypeOf 2 | 3 | ``` 4 | [object] → [typeof object] 5 | ``` 6 | 7 | - Action Code: `0x44` 8 | - Stack: `1 → 1` 9 | - SWF version: `5` 10 | 11 | ## Original documentation 12 | 13 | ### ActionToString 14 | 15 | ActionTypeOf pushes the object type to the stack, which is equivalent to the ActionScript TypeOf() method. The 16 | possible types are: 17 | - `number` 18 | - `boolean` 19 | - `string` 20 | - `object` 21 | - `movieclip` 22 | - `null` 23 | - `undefined` 24 | - `function` 25 | 26 | | Field | Type | Comment | 27 | |-------------------|--------------------|--------------------------------| 28 | | ActionTypeOf | ACTIONRECORDHEADER | ActionCode = 0x44 | 29 | 30 | ActionTypeOf does the following: 31 | 1. Pops the value to determine the type of off the stack. 32 | 2. Pushes a string with the type of the object on to the stack. 33 | -------------------------------------------------------------------------------- /docs/avm1/actions/get-url.md: -------------------------------------------------------------------------------- 1 | # GetUrl 2 | 3 | ``` 4 | [] → [] 5 | ``` 6 | 7 | - Action Code: `0x83` 8 | - Stack: `0 → 0` 9 | - SWF version: `3` 10 | 11 | ## Original documentation 12 | 13 | ### ActionGetURL 14 | 15 | ActionGetURL instructs Flash Player to get the URL that UrlString specifies. The URL can be of any type, including 16 | an HTML file, an image or another SWF file. If the file is playing in a browser, the URL is displayed in the frame 17 | that TargetString specifies. The "_level0" and "_level1" special target names are used to load another SWF file 18 | into levels 0 and 1 respectively. 19 | 20 | | Field | Type | Comment | 21 | |-----------------|--------------------|-------------------| 22 | | ActionGetURL | ACTIONRECORDHEADER | ActionCode = 0x83 | 23 | | UrlString | STRING | Target URL string | 24 | | TargetString | STRING | Target string | 25 | -------------------------------------------------------------------------------- /docs/avm1/actions/string-extract.md: -------------------------------------------------------------------------------- 1 | # StringExtract 2 | 3 | ``` 4 | [string, index, count] → [String(string).substr(index, count)] 5 | ``` 6 | 7 | - Action Code: `0x15` 8 | - Stack: `3 → 1` 9 | - SWF version: `4` 10 | 11 | ## Original documentation 12 | 13 | ### ActionStringExtract 14 | 15 | ActionStringEquals tests two strings for equality. 16 | 17 | | Field | Type | Comment | 18 | |---------------------|--------------------|-------------------| 19 | | ActionStringExtract | ACTIONRECORDHEADER | ActionCode = 0x15 | 20 | 21 | ActionStringExtract does the following: 22 | 1. Pops number count off the stack. 23 | 2. Pops number index off the stack. 24 | 3. Pops string string off the stack. 25 | 4. Pushes the substring of the string starting at the indexed character and count characters 26 | in length to the stack. 27 | 5. If either index or count do not evaluate to integers, the result is the empty string. 28 | -------------------------------------------------------------------------------- /docs/avm1/actions/bit-and.md: -------------------------------------------------------------------------------- 1 | # BitAnd 2 | 3 | ``` 4 | [arg2, arg1] → [arg2 & arg1] 5 | ``` 6 | 7 | - Action Code: `0x60` 8 | - Stack: `1 → 1` 9 | - SWF version: `5` 10 | 11 | **Note**: There is an issue with the formatting of the table in the official documentation. 12 | 13 | ## Original documentation 14 | 15 | ### ActionBitAnd 16 | 17 | ActionBitAnd pops two numbers off of the stack, performs a bitwise AND, and pushes an S32 number to the 18 | stack. The arguments are converted to 32-bit unsigned integers before performing the bitwise operation. The 19 | result is a SIGNED 32-bit integer. 20 | 21 | | Field | Type | Comment | 22 | |-------------------|--------------------------------------|---------| 23 | | ActionBitAnd | ACTIONRECORDHEADER ActionCode = 0x60 | | 24 | 25 | ActionBitAnd does the following: 26 | 1. Pops arg1 then arg2 off of the stack. 27 | 2. Pushes the result to the stack. 28 | -------------------------------------------------------------------------------- /docs/avm1/actions/wait-for-frame2.md: -------------------------------------------------------------------------------- 1 | # WaitForFrame2 2 | 3 | ``` 4 | [frame] → [] 5 | ``` 6 | 7 | - Action Code: `0x82` 8 | - Stack: `1 → 0` 9 | - SWF version: `4` 10 | 11 | ## Original documentation 12 | 13 | ### ActionWaitForFrame2 14 | 15 | ActionWaitForFrame2 waits for a frame to be loaded and is stack based. 16 | 17 | | Field | Type | Comment | 18 | |---------------------|--------------------|---------------------------------------| 19 | | ActionWaitForFrame2 | ACTIONRECORDHEADER | ActionCode = 0x8D; Length is always 1 | 20 | | SkipCount | UI8 | The number of actions to skip | 21 | 22 | ActionWaitForFrame2 does the following: 23 | 1. Pops a frame off the stack. 24 | 2. If the frame is loaded, skip the next n actions that follow the current action, where n is indicated by 25 | SkipCount. 26 | The frame is evaluated in the same way as ActionGotoFrame2. 27 | -------------------------------------------------------------------------------- /docs/avm1/actions/equals.md: -------------------------------------------------------------------------------- 1 | # Equals 2 | 3 | ``` 4 | [arg2, arg1] → [equals(arg2, arg1)] 5 | ``` 6 | 7 | - Action Code: `0x0e` 8 | - Stack: `2 → 1` 9 | - SWF version: `4` 10 | 11 | ## Original documentation 12 | 13 | ### ActionEquals 14 | 15 | ActionEquals tests two numbers for equality. 16 | 17 | | Field | Type | Comment | 18 | |-----------------|--------------------|-------------------| 19 | | ActionEquals | ACTIONRECORDHEADER | ActionCode = 0x0E | 20 | 21 | ActionEquals does the following: 22 | 1. Pops value A off the stack. 23 | 2. Pops value B off the stack. 24 | 3. Converts A and B to floating-point; non-numeric values evaluate to 0. 25 | 4. Compares the numbers for equality. 26 | 5. If the numbers are equal, true is pushed to the stack for SWF 5 and later. 27 | 6. For SWF 4, 1 is pushed to the stack. 28 | 7. Otherwise, false is pushed to the stack for SWF 5 and later. (For SWF 4, 0 is pushed to the stack.) 29 | -------------------------------------------------------------------------------- /docs/avm1/actions/bit-l-shift.md: -------------------------------------------------------------------------------- 1 | # BitLShift 2 | 3 | ``` 4 | [arg2, arg1] → [arg2 << arg1] 5 | ``` 6 | 7 | - Action Code: `0x63` 8 | - Stack: `1 → 1` 9 | - SWF version: `5` 10 | 11 | ## Original documentation 12 | 13 | ### ActionBitLShift 14 | 15 | ActionBitLShift pops the shift count arg and then value off of the stack. The value argument is converted to 32- 16 | bit signed integer and only the least significant 5 bits are used as the shift count. The bits in the value arg are 17 | shifted to the left by the shift count. ActionBitLShift pushes an S32 number to the stack. 18 | 19 | | Field | Type | Comment | 20 | |-------------------|--------------------|--------------------------------| 21 | | ActionBitLShift | ACTIONRECORDHEADER | ActionCode = 0x63 | 22 | 23 | ActionBitLShift does the following: 24 | 1. Pops shift count arg, then value off of the stack. 25 | 2. Pushes the result to the stack. 26 | -------------------------------------------------------------------------------- /docs/avm1/actions/mb-ascii-to-char.md: -------------------------------------------------------------------------------- 1 | # MbAsciiToChar 2 | 3 | ``` 4 | [a] → [String.fromCharCode(Number(a))] 5 | ``` 6 | 7 | - Action Code: `0x37` 8 | - Stack: `1 → 1` 9 | - SWF version: `4` 10 | 11 | ## Original documentation 12 | 13 | ### ActionMbAsciiToChar 14 | 15 | ActionMBAsciiToChar converts ASCII to character code and is multi-byte aware. 16 | 17 | | Field | Type | Comment | 18 | |---------------------|--------------------|-------------------| 19 | | ActionMbAsciiToChar | ACTIONRECORDHEADER | ActionCode = 0x37 | 20 | 21 | ActionMBAsciiToChar does the following: 22 | 1. Pops a value off the stack. 23 | 2. Converts the value from a number to the corresponding character. If the character is a 16-bit value (>= 24 | 256), a double-byte character is constructed with the first byte containing the high-order byte, and the 25 | second byte containing the low-order byte. 26 | 3. Pushes the resulting character to the stack. 27 | -------------------------------------------------------------------------------- /docs/avm1/actions/mb-char-to-ascii.md: -------------------------------------------------------------------------------- 1 | # MbCharToAscii 2 | 3 | ``` 4 | [a] → [String(a).charCodeAt(0)] 5 | ``` 6 | 7 | - Action Code: `0x36` 8 | - Stack: `1 → 1` 9 | - SWF version: `4` 10 | 11 | ## Original documentation 12 | 13 | ### ActionMbCharToAscii 14 | 15 | ActionMBCharToAscii converts character code to ASCII and is multi-byte aware. 16 | 17 | | Field | Type | Comment | 18 | |---------------------|--------------------|-------------------| 19 | | ActionMbCharToAscii | ACTIONRECORDHEADER | ActionCode = 0x36 | 20 | 21 | ActionMBCharToAscii does the following: 22 | 1. Pops a value off the stack. 23 | 2. Converts the first character of the value to a numeric character code. 24 | If the first character of the value is a double-byte character, a 16-bit value is constructed with the first 25 | byte as the high-order byte and the second byte as the low-order byte. 26 | 3. Pushes the resulting character code to the stack. 27 | -------------------------------------------------------------------------------- /docs/avm1/actions/and.md: -------------------------------------------------------------------------------- 1 | # And 2 | 3 | ``` 4 | [b, a] → [b && a] 5 | ``` 6 | 7 | - Action Code: `0x10` 8 | - Stack: `2 → 1` 9 | - SWF version: `4` 10 | 11 | ## Notes 12 | 13 | - There is a typo in the official documentation. "ActionAdd" should be "ActionAnd". 14 | 15 | ## Original documentation 16 | 17 | ### ActionAnd 18 | 19 | ActionAnd performs a logical AND of two numbers. 20 | 21 | | Field | Type | Comment | 22 | |-----------------|--------------------|-------------------| 23 | | ActionAnd | ACTIONRECORDHEADER | ActionCode = 0x10 | 24 | 25 | ActionAdd does the following: 26 | 1. Pops value A off the stack. 27 | 2. Pops value B off the stack. 28 | 3. Converts A and B to floating-point; non-numeric values evaluate to 0. 29 | 4. If both numbers are nonzero, true is pushed to the stack for SWF 5 and later (1 is pushed for SWF 4); 30 | otherwise, false is pushed to the stack for SWF 5 and later (0 is pushed for SWF 4). 31 | -------------------------------------------------------------------------------- /docs/avm1/actions/divide.md: -------------------------------------------------------------------------------- 1 | # Divide 2 | 3 | ``` 4 | [arg2, arg1] → [arg2 / arg1] 5 | ``` 6 | 7 | - Action Code: `0x0c` 8 | - Stack: `2 → 1` 9 | - SWF version: `4` 10 | 11 | ## Original documentation 12 | 13 | ### ActionDivide 14 | 15 | ActionDivide divides two numbers and pushes the result back to the stack. 16 | 17 | | Field | Type | Comment | 18 | |-------------------|--------------------|--------------------------------| 19 | | ActionDivide | ACTIONRECORDHEADER | ActionCode = 0x0D | 20 | 21 | ActionDivide does the following: 22 | 1. Pops value A off the stack. 23 | 2. Pops value B off the stack. 24 | 3. Converts A and B to floating-point; non-numeric values evaluate to 0. 25 | 4. Divides B by A. 26 | 5. Pushes the result, B/A, to the stack. 27 | 6. If A is zero, the result NaN, Infinity, or -Infinity is pushed to the stack in SWF 5 and later. In SWF 4, the 28 | result is the string #ERROR#. 29 | -------------------------------------------------------------------------------- /docs/avm1/actions/less2.md: -------------------------------------------------------------------------------- 1 | # Less2 2 | 3 | ``` 4 | [arg2, arg1] → [arg2 < arg1] 5 | ``` 6 | 7 | - Action Code: `0x48` 8 | - Stack: `1 → 1` 9 | - SWF version: `5` 10 | 11 | ## Original documentation 12 | 13 | ### ActionAdd2 14 | 15 | ActionLess2 calculates whether arg1 is less than arg2 and pushes a Boolean return value to the stack. This action 16 | is similar to ActionLess, but performs the comparison differently according to the data types of the arguments. 17 | The abstract relational comparison algorithm in ECMA-262 Section 11.8.5 is used. 18 | 19 | | Field | Type | Comment | 20 | |-------------------|--------------------|--------------------------------| 21 | | ActionLess2 | ACTIONRECORDHEADER | ActionCode = 0x48 | 22 | 23 | ActionLess2 does the following: 24 | 1. Pops arg1 off of the stack. 25 | 2. Pops arg2 off of the stack. 26 | 3. Compares arg2 < arg1. 27 | 4. Pushes the return value (a Boolean value) onto the stack. 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Editor # 3 | ############################################################################### 4 | # JetBrains (Webstorm, IntelliJ IDEA, ...) 5 | .idea/ 6 | *.iml 7 | 8 | # Komodo 9 | *.komodoproject 10 | .komodotools 11 | 12 | ############################################################################### 13 | # Temporary files # 14 | ############################################################################### 15 | *.log 16 | *.tmp 17 | tmp/ 18 | logs/ 19 | lcov.info 20 | 21 | ############################################################################### 22 | # Other # 23 | ############################################################################### 24 | # Runtime data 25 | pids 26 | *.pid 27 | *.seed 28 | -------------------------------------------------------------------------------- /docs/avm1/actions/greater.md: -------------------------------------------------------------------------------- 1 | # Greater 2 | 3 | ``` 4 | [arg2, arg1] → [arg2 > arg1] 5 | ``` 6 | 7 | - Action Code: `0x67` 8 | - Stack: `2 → 1` 9 | - SWF version: `6` 10 | 11 | ## Original documentation 12 | 13 | ### ActionGreater 14 | 15 | ActionGreater is the exact opposite of ActionLess2. Originally there was no ActionGreater, because it can be 16 | emulated by reversing the order of argument pushing, then performing an ActionLess followed by an ActionNot. 17 | However, this argument reversal resulted in a reversal of the usual order of evaluation of arguments, which in a 18 | few cases led to surprises. 19 | 20 | | Field | Type | Comment | 21 | |--------------------|--------------------|-------------------| 22 | | ActionGreater | ACTIONRECORDHEADER | ActionCode = 0x67 | 23 | 24 | ActionGreater does the following: 25 | 1. Pops arg1 and then arg2 off of the stack. 26 | 2. Compares if arg2 > arg1. 27 | 3. Pushes the return value, a Boolean value, onto the stack. 28 | -------------------------------------------------------------------------------- /docs/avm1/actions/bit-u-r-shift.md: -------------------------------------------------------------------------------- 1 | # BitURShift 2 | 3 | ``` 4 | [arg2, arg1] → [arg2 >>> arg1] 5 | ``` 6 | 7 | - Action Code: `0x65` 8 | - Stack: `1 → 1` 9 | - SWF version: `5` 10 | 11 | ## Original documentation 12 | 13 | ### ActionBitURShift 14 | 15 | ActionBitURShift pops the value and shift count arguments from the stack. The value argument is converted to 16 | 32-bit signed integer and only the least significant 5 bits are used as the shift count. 17 | 18 | The bits in the arg value are shifted to the right by the shift count. ActionBitURShift pushes a UI32 number to the 19 | stack. 20 | 21 | | Field | Type | Comment | 22 | |-------------------|--------------------|--------------------------------| 23 | | ActionBitURShift | ACTIONRECORDHEADER | ActionCode = 0x65 | 24 | 25 | ActionBitURShift does the following: 26 | 1. Pops the shift count from the stack. 27 | 2. Pops the value to shift from the stack. 28 | 3. Pushes the result to the stack. 29 | -------------------------------------------------------------------------------- /docs/avm1/actions/bit-r-shift.md: -------------------------------------------------------------------------------- 1 | # BitRShift 2 | 3 | ``` 4 | [arg2, arg1] → [arg2 >> arg1] 5 | ``` 6 | 7 | - Action Code: `0x64` 8 | - Stack: `1 → 1` 9 | - SWF version: `5` 10 | 11 | ## Original documentation 12 | 13 | ### ActionBitRShift 14 | 15 | ActionBitRShift pops the shift count from the stack. Pops the value from the stack. The value argument is 16 | converted to a 32-bit signed integer and only the least significant 5 bits are used as the shift count. 17 | 18 | The bits in the arg value are shifted to the right by the shift count. ActionBitRShift pushes an S32 number to the 19 | stack. 20 | 21 | | Field | Type | Comment | 22 | |-------------------|--------------------|--------------------------------| 23 | | ActionBitRShift | ACTIONRECORDHEADER | ActionCode = 0x64 | 24 | 25 | ActionBitRShift does the following: 26 | 1. Pops the shift count from the stack. 27 | 2. Pops the value to shift from the stack. 28 | 3. Pushes the result to the stack. 29 | -------------------------------------------------------------------------------- /docs/avm1/actions/call.md: -------------------------------------------------------------------------------- 1 | # Call 2 | 3 | ``` 4 | [frame] → [] 5 | ``` 6 | 7 | - Action Code: `0x9e` 8 | - Stack: `1 → 0` 9 | - SWF version: `4` 10 | 11 | ## Original documentation 12 | 13 | ### ActionCall 14 | 15 | ActionCall calls a subroutine. 16 | 17 | | Field | Type | Comment | 18 | |--------------------|--------------------|-------------------| 19 | | ActionCall | ACTIONRECORDHEADER | ActionCode = 0x9E | 20 | 21 | ActionCall does the following: 22 | 1. Pops a value off the stack. This value should be either a string that matches a frame label, or a number 23 | that indicates a frame number. The value can be prefixed by a target string that identifies the movie clip 24 | that contains the frame being called. 25 | 2. If the frame is successfully located, the actions in the target frame are executed. After the actions in the 26 | target frame are executed, execution resumes at the instruction after the ActionCall instruction. 27 | 3. If the frame cannot be found, nothing happens. 28 | -------------------------------------------------------------------------------- /docs/avm1/actions/set-variable.md: -------------------------------------------------------------------------------- 1 | # SetVariable 2 | 3 | ``` 4 | [name, value] → [] 5 | ``` 6 | 7 | - Action Code: `0x1d` 8 | - Stack: `2 → 0` 9 | - SWF version: `4` 10 | 11 | This is equivalent to the expression `void (name = value)` or the statement `name = value;`. 12 | 13 | ## Original documentation 14 | 15 | ### ActionSetVariable 16 | 17 | ActionSetVariable sets a variable. 18 | 19 | | Field | Type | Comment | 20 | |-------------------|--------------------|-------------------| 21 | | ActionSetVariable | ACTIONRECORDHEADER | ActionCode = 0x1D | 22 | 23 | ActionSetVariable does the following: 24 | 1. Pops the value off the stack. 25 | 2. Pops the name off the stack, a string which names the variable to set. 26 | 3. Sets the variable name in the current execution context to value. 27 | A variable in another execution context can be referenced by prefixing the variable name with the target path 28 | and a colon. For example: /A/B:FOO references the FOO variable in the movie clip with a target path of /A/B. 29 | -------------------------------------------------------------------------------- /docs/avm1/actions/not.md: -------------------------------------------------------------------------------- 1 | # Not 2 | 3 | ``` 4 | [a] → [!a] 5 | ``` 6 | 7 | - Action Code: `0x12` 8 | - Stack: `1 → 1` 9 | - SWF version: `4` 10 | 11 | ## Original documentation 12 | 13 | ### ActionAnd 14 | 15 | ActionNot performs a logical NOT of a number. 16 | 17 | **Note**: In SWF 5 files, the ActionNot operator converts its argument to a Boolean value, and pushes a result of 18 | type Boolean. In SWF 4 files, the argument and result are numbers. 19 | 20 | | Field | Type | Comment | 21 | |-----------------|--------------------|-------------------| 22 | | ActionNot | ACTIONRECORDHEADER | ActionCode = 0x12 | 23 | | Result | Boolean | | 24 | 25 | ActionNot does the following: 26 | 1. Pops a value off the stack. 27 | 2. Converts the value to floating point; non-numeric values evaluate to 0. 28 | 3. If the value is zero, true is pushed on the stack for SWF 5 and later (1 is pushed for SWF 4). 29 | 4. If the value is nonzero, false is pushed on the stack for SWF 5 and later (0 is pushed for SWF 4). 30 | -------------------------------------------------------------------------------- /docs/avm1/actions/if.md: -------------------------------------------------------------------------------- 1 | # If 2 | 3 | ``` 4 | [condition] → [] 5 | ``` 6 | 7 | - Action Code: `0x9d` 8 | - Stack: `1 → 0` 9 | - SWF version: `4` 10 | 11 | ## Original documentation 12 | 13 | ### ActionIf 14 | 15 | ActionIf creates a conditional test and branch. 16 | 17 | | Field | Type | Comment | 18 | |--------------------|--------------------|-------------------| 19 | | ActionIf | ACTIONRECORDHEADER | ActionCode = 0x9D | 20 | | BranchOffset | SI16 | Offset | 21 | 22 | ActionIf does the following: 23 | 1. Pops Condition, a number, off the stack. 24 | 2. Converts Condition to a Boolean value. 25 | 3. Tests if Condition is true. If Condition is true, BranchOffset bytes are added to the instruction pointer in 26 | the execution stream. 27 | 28 | **Note**: When playing a SWF 4 file, Condition is not converted to a Boolean value and is instead compared to 0, 29 | not true. 30 | 31 | The offset is a signed quantity, enabling branches from –32768 bytes to 32767 bytes. An offset of 0 points to the 32 | action directly after the ActionIf action. 33 | -------------------------------------------------------------------------------- /docs/avm1/actions/mb-string-extract.md: -------------------------------------------------------------------------------- 1 | # StringExtract 2 | 3 | ``` 4 | [string, index, count] → [String(string).substr(index, count)] 5 | ``` 6 | 7 | - Action Code: `0x35` 8 | - Stack: `3 → 1` 9 | - SWF version: `4` 10 | 11 | ## Original documentation 12 | 13 | ### ActionMBStringExtract 14 | 15 | ActionMBStringExtract extracts a substring from a string and is multi-byte aware. 16 | 17 | | Field | Type | Comment | 18 | |-----------------------|--------------------|-------------------| 19 | | ActionMBStringExtract | ACTIONRECORDHEADER | ActionCode = 0x35 | 20 | 21 | It does the following: 22 | 1. Pops the number count off the stack. 23 | 2. Pops the number index off the stack. 24 | 3. Pops the string string off the stack. 25 | 4. Pushes the substring of string starting at the index’th character and count characters in length to the 26 | stack. 27 | Note: If either index or count do not evaluate to integers, the result is the empty string. 28 | This is a multi-byte aware version of ActionStringExtract. Index and count are treated as character indexes, 29 | counting double-byte characters as single characters. 30 | -------------------------------------------------------------------------------- /docs/avm1/actions/with.md: -------------------------------------------------------------------------------- 1 | # With 2 | 3 | ``` 4 | [context] → [] 5 | ``` 6 | 7 | - Action Code: `0x94` 8 | - Stack: `1 → 0` 9 | - SWF version: `5` 10 | 11 | ## Original documentation 12 | 13 | ### ActionWith 14 | 15 | Defines a With block of script. 16 | 17 | | Field | Type | Comment | 18 | |-------------------|--------------------|--------------------------------| 19 | | ActionTargetPath | ACTIONRECORDHEADER | ActionCode = 0x94 | 20 | | Size | UI16 | # of bytes of code that follow | 21 | 22 | ActionWith does the following: 23 | 1. Pops the object involved with the With. 24 | 2. Parses the size (body length) of the With block from the ActionWith tag. 25 | 3. Checks to see if the depth of calls exceeds the maximum depth, which is 16 for SWF 6 and later, and 8 26 | for SWF 5. If the With depth exceeds the maximum depth, the next Size bytes of data are skipped rather 27 | than executed. 28 | 4. After the ActionWith tag, the next Size bytes of action codes are considered to be the body of the With 29 | block. 30 | 5. Adds the With block to the scope chain. 31 | -------------------------------------------------------------------------------- /docs/avm1/actions/start-drag.md: -------------------------------------------------------------------------------- 1 | # StartDrag 2 | 3 | ``` 4 | [constrain, lockcenter, target] → [] 5 | [x1, y1, x2, y2, constrain, lockcenter, target] → [] 6 | ``` 7 | 8 | - Action Code: `0x27` 9 | - Stack: `3 → 0` or `7 → 0` 10 | - SWF version: `4` 11 | 12 | ## Original documentation 13 | 14 | ### ActionStartDrag 15 | 16 | ActionStartDrag starts dragging a movie clip. 17 | 18 | | Field | Type | Comment | 19 | |--------------------|--------------------|-------------------| 20 | | ActionStartDrag | ACTIONRECORDHEADER | ActionCode = 0x27 | 21 | 22 | ActionStartDrag does the following: 23 | 1. Pops a target off the stack; target identifies the movie clip to be dragged. 24 | 2. Pops lockcenter off the stack. If lockcenter evaluates to a nonzero value, the center of the dragged 25 | movie clip is locked to the mouse position. Otherwise, the movie clip moves relative to the mouse 26 | position when the drag started. 27 | 3. Pops constrain off the stack. 28 | 4. If constrain evaluates to a nonzero value: 29 | - Pops y2 off the stack. 30 | - Pops x2 off the stack. 31 | - Pops y1 off the stack. 32 | - Pops x1 off the stack. 33 | -------------------------------------------------------------------------------- /rs/src/stream_buffer.rs: -------------------------------------------------------------------------------- 1 | /// Trait representing the buffer backing a streaming parser. 2 | /// 3 | /// This trait provides a way to keep only the unparsed input in memory. 4 | pub trait StreamBuffer { 5 | fn new() -> Self; 6 | 7 | /// Add unparsed data at the end of the buffer. 8 | fn write(&mut self, unparsed_bytes: &[u8]); 9 | 10 | /// Get the unparsed data. 11 | fn get(&self) -> &[u8]; 12 | 13 | /// Mark the provided count of bytes as _parsed_. 14 | fn clear(&mut self, parsed_size: usize); 15 | } 16 | 17 | /// Stream buffer backed a `Vec`. 18 | pub struct FlatBuffer { 19 | parsed: usize, 20 | inner: Vec, 21 | } 22 | 23 | impl FlatBuffer {} 24 | 25 | impl StreamBuffer for FlatBuffer { 26 | fn new() -> Self { 27 | Self { 28 | parsed: 0, 29 | inner: Vec::new(), 30 | } 31 | } 32 | 33 | fn write(&mut self, unparsed_bytes: &[u8]) { 34 | self.inner.extend_from_slice(unparsed_bytes) 35 | } 36 | 37 | fn get(&self) -> &[u8] { 38 | &self.inner[self.parsed..] 39 | } 40 | 41 | fn clear(&mut self, parsed_size: usize) { 42 | self.parsed += parsed_size; 43 | } 44 | } 45 | 46 | // TODO: Ring buffer backed by a `SliceDeque`? 47 | -------------------------------------------------------------------------------- /docs/avm1/actions/enumerate.md: -------------------------------------------------------------------------------- 1 | # Enumerate 2 | 3 | ``` 4 | [objectName] → [null, ...propertieNames] 5 | ``` 6 | 7 | - Action Code: `0x46` 8 | - Stack: `1 → 1+` 9 | - SWF version: `5` 10 | 11 | ## Original documentation 12 | 13 | ### ActionDelete2 14 | 15 | ActionEnumerate obtains the names of all “slots” in use in an ActionScript object—that is, for an object obj, all 16 | names X that could be retrieved with the syntax obj.X. ActionEnumerate is used to implement the for..in 17 | statement in ActionScript. 18 | 19 | **Note**: Certain special slot names are omitted; for a list of these, search for the term DontEnum in the ECMA-262 20 | standard. 21 | 22 | | Field | Type | Comment | 23 | |-----------------|--------------------|-------------------| 24 | | ActionEnumerate | ACTIONRECORDHEADER | ActionCode = 0x46 | 25 | 26 | ActionEnumerate does the following: 27 | 28 | 1. Pops the name of the object variable (which can include slash-path or dot-path syntax) off of the stack. 29 | 2. Pushes a null value onto the stack to indicate the end of the slot names. 30 | 3. Pushes each slot name (a string) onto the stack. 31 | 32 | The order in which slot names are pushed is undefined. 33 | -------------------------------------------------------------------------------- /ts/src/lib/index.mts: -------------------------------------------------------------------------------- 1 | import { ReadableStream } from "@open-flash/stream"; 2 | import { Uint8 } from "semantic-types"; 3 | import * as swf from "swf-types"; 4 | 5 | import { parseSwf as parseSwfStream } from "./parsers/movie.mjs"; 6 | import { parseTag as parseTagStream } from "./parsers/tags.mjs"; 7 | 8 | export { swf }; 9 | 10 | /** 11 | * Parses a completely loaded SWF file. 12 | * 13 | * @param bytes SWF file to parse 14 | * @returns The parsed Movie 15 | */ 16 | export function parseSwf(bytes: Uint8Array): swf.Movie { 17 | const byteStream: ReadableStream = new ReadableStream(bytes); 18 | return parseSwfStream(byteStream); 19 | } 20 | 21 | /** 22 | * Parses the tag at the start of `input`. 23 | * 24 | * This parser assumes that `input` is complete: it has all the data until the end of the movie. 25 | * 26 | * @param bytes Tag to parse 27 | * @param swfVersion SWF version to use for tags depending on the SWF version 28 | * @returns The parsed tag, or `undefined` if an error occurred. 29 | */ 30 | export function parseTag(bytes: Uint8Array, swfVersion: Uint8): swf.Tag | undefined { 31 | const byteStream: ReadableStream = new ReadableStream(bytes); 32 | return parseTagStream(byteStream, swfVersion); 33 | } 34 | -------------------------------------------------------------------------------- /docs/avm1/actions/cast-op.md: -------------------------------------------------------------------------------- 1 | # CastOp 2 | 3 | ``` 4 | [constructor, scriptObject] → [scriptObject instanceof constructor ? scriptObject : null] 5 | ``` 6 | 7 | - Action Code: `0x2b` 8 | - Stack: `2 → 1` 9 | - SWF version: `7` 10 | 11 | ## Original documentation 12 | 13 | ### ActionCastOp 14 | 15 | ActionCastOp implements the ActionScript cast operator, which allows the casting from one data type to 16 | another. ActionCastOp pops an object off the stack and attempts to convert the object to an instance of the 17 | class or to the interface represented by the constructor function. 18 | 19 | 20 | | Field | Type | Comment | 21 | |--------------------|--------------------|-------------------| 22 | | ActionCastOp | ACTIONRECORDHEADER | ActionCode = 0x2B | 23 | 24 | ActionCastOp does the following: 25 | 1. Pops the ScriptObject to cast off the stack. 26 | 2. Pops the constructor function off the stack. 27 | 3. Determines if object is an instance of constructor (doing the same comparison as ActionInstanceOf). 28 | 4. If the object is an instance of constructor, the popped ScriptObject is pushed onto the stack. 29 | If the object is not an instance of constructor, a null value is pushed onto the stack. 30 | -------------------------------------------------------------------------------- /docs/avm1/actions/throw.md: -------------------------------------------------------------------------------- 1 | # Throw 2 | 3 | ``` 4 | [error] → [] 5 | ``` 6 | 7 | - Action Code: `0x2a` 8 | - Stack: `1 → 0` 9 | - SWF version: `7` 10 | 11 | ## Original documentation 12 | 13 | ### ActionThrow 14 | 15 | ActionThrow implements the ActionScript throw keyword. ActionThrow is used to signal, or throw, an 16 | exceptional condition, which is handled by the exception handlers declared with ActionTry. 17 | If any code within the try block throws an object, control passes to the catch block, if one exists, then to the 18 | finally block, if one exists. The finally block always executes, regardless of whether an error was thrown. 19 | If an exceptional condition occurs within a function and the function does not include a catch handler, the 20 | function and any caller functions are exited until a catch block is found (executing all finally handlers at all 21 | levels). 22 | Any ActionScript data type can be thrown, though typically usage is to throw objects. 23 | 24 | | Field | Type | Comment | 25 | |--------------------|--------------------|-------------------| 26 | | ActionThrow | ACTIONRECORDHEADER | ActionCode = 0x2A | 27 | 28 | ActionThrow pops the value to be thrown off the stack. 29 | -------------------------------------------------------------------------------- /docs/avm1/actions/set-target.md: -------------------------------------------------------------------------------- 1 | # SetTarget 2 | 3 | ``` 4 | [] → [] 5 | ``` 6 | 7 | - Action Code: `0x8b` 8 | - Stack: `0 → 0` 9 | - SWF version: `3` 10 | 11 | ## Original documentation 12 | 13 | ### ActionSetTarget 14 | 15 | ActionSetTarget instructs Flash Player to change the context of subsequent actions, so they apply to a named 16 | object (TargetName) rather than the current file. 17 | 18 | For example, the SetTarget action can be used to control the Timeline of a sprite object. The following sequence 19 | of actions sends a sprite called "spinner" to the first frame in its Timeline: 20 | 21 | 1. SetTarget "spinner" 22 | 2. GotoFrame zero 23 | 3. SetTarget "" (empty string) 24 | 4. End of actions. (Action code = 0) 25 | 26 | All actions following SetTarget “spinner” apply to the spinner object until SetTarget “”, which sets the action 27 | context back to the current file. For a complete discussion of target names see DefineSprite. 28 | 29 | | Field | Type | Comment | 30 | |--------------------|--------------------|-------------------------| 31 | | ActionSetTarget | ACTIONRECORDHEADER | ActionCode = 0x8B | 32 | | TargetName | STRING | Target of action target | 33 | 34 | -------------------------------------------------------------------------------- /docs/avm1/actions/instance-of.md: -------------------------------------------------------------------------------- 1 | # InstanceOf 2 | 3 | ``` 4 | [object, class] → [object instanceof class] 5 | ``` 6 | 7 | - Action Code: `0x54` 8 | - Stack: `2 → 1` 9 | - SWF version: `6` 10 | 11 | ## Original documentation 12 | 13 | ### ActionInstanceOf 14 | 15 | ActionInstanceOf implements the ActionScript instanceof() operator. This is a Boolean operator that indicates 16 | whether the left operand (typically an object) is an instance of the class represented by a constructor function 17 | passed as the right operand. 18 | 19 | Additionally, with SWF 7 or later, ActionInstanceOf also supports with interfaces. If the right operand 20 | constructor is a reference to an interface object, and the left operand implements this interface, 21 | ActionInstanceOf accurately reports that the left operand is an instance of the right operand. 22 | 23 | | Field | Type | Comment | 24 | |--------------------|--------------------|-------------------| 25 | | ActionInstanceOf | ACTIONRECORDHEADER | ActionCode = 0x54 | 26 | 27 | ActionInstanceOf does the following: 28 | 1. Pops constr then obj off of the stack. 29 | 2. Determines if obj is an instance of constr. 30 | 3. Pushes the return value (a Boolean value) onto the stack. 31 | -------------------------------------------------------------------------------- /rs/.gitignore: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Editor # 3 | ############################################################################### 4 | # JetBrains (Webstorm, IntelliJ IDEA, ...) 5 | .idea/ 6 | *.iml 7 | 8 | # Komodo 9 | *.komodoproject 10 | .komodotools 11 | 12 | ############################################################################### 13 | # Build # 14 | ############################################################################### 15 | /target/ 16 | 17 | ############################################################################### 18 | # Temporary files # 19 | ############################################################################### 20 | *.log 21 | *.tmp 22 | tmp/ 23 | logs/ 24 | 25 | ############################################################################### 26 | # Other # 27 | ############################################################################### 28 | # Runtime data 29 | pids 30 | *.pid 31 | *.seed 32 | -------------------------------------------------------------------------------- /rs/src/complete/base.rs: -------------------------------------------------------------------------------- 1 | use nom::IResult as NomResult; 2 | 3 | /// Creates a parser skipping `count` bytes. 4 | pub(crate) fn skip>(count: C) -> impl FnMut(I) -> NomResult 5 | where 6 | I: nom::InputIter + nom::InputTake, 7 | C: nom::ToUsize, 8 | { 9 | use nom::bytes::complete::take; 10 | use nom::combinator::map; 11 | map(take(count), |_| ()) 12 | } 13 | 14 | /// Take with an offset 15 | pub(crate) fn offset_take>(offset: C, count: C) -> impl Fn(I) -> NomResult 16 | where 17 | I: nom::InputIter + nom::InputTake, 18 | C: nom::ToUsize, 19 | { 20 | let offset = offset.to_usize(); 21 | let count = count.to_usize(); 22 | move |i: I| match i.slice_index(offset) { 23 | Err(_) => Err(nom::Err::Error(nom::error::ParseError::from_error_kind( 24 | i, 25 | nom::error::ErrorKind::Eof, 26 | ))), 27 | Ok(index) => { 28 | let (suffix, _) = i.take_split(index); 29 | match suffix.slice_index(count) { 30 | Err(_) => Err(nom::Err::Error(nom::error::ParseError::from_error_kind( 31 | i, 32 | nom::error::ErrorKind::Eof, 33 | ))), 34 | Ok(index) => Ok(suffix.take_split(index)), 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /rs/src/streaming/parser/lzma.rs: -------------------------------------------------------------------------------- 1 | use crate::stream_buffer::StreamBuffer; 2 | use swf_types::{Header as SwfHeader, SwfSignature, Tag}; 3 | 4 | use super::{ParseTagsError, SimpleStream}; 5 | 6 | // TODO: Send PR to lzma-rs to support LZMA stream parsing 7 | struct LzmaParser {} 8 | 9 | impl LzmaParser { 10 | pub fn new() -> Self { 11 | unimplemented!("LZMA decompression is unsupported in streaming mode"); 12 | } 13 | } 14 | 15 | /// State of the `Lzma` payload parser 16 | pub(crate) struct LzmaStream { 17 | #[allow(dead_code)] 18 | lzma_parser: LzmaParser, 19 | simple: SimpleStream, 20 | } 21 | 22 | impl LzmaStream { 23 | pub(crate) fn new(buffer: B, signature: SwfSignature) -> Self { 24 | let lzma_parser = LzmaParser::new(); 25 | let simple = SimpleStream::new(B::new(), signature); 26 | let mut stream = Self { lzma_parser, simple }; 27 | stream.write(buffer.get()); 28 | stream 29 | } 30 | 31 | pub(crate) fn write(&mut self, mut _bytes: &[u8]) { 32 | unreachable!() 33 | } 34 | 35 | pub(crate) fn header(self) -> Result<(SwfHeader, Self), Self> { 36 | unreachable!() 37 | } 38 | 39 | pub(crate) fn tags(&mut self) -> Result>, ParseTagsError> { 40 | self.simple.tags() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /.github/workflows/check-rs.yml: -------------------------------------------------------------------------------- 1 | name: "check-rs" 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | check-rs: 11 | runs-on: "ubuntu-latest" 12 | 13 | strategy: 14 | matrix: 15 | rust-version: ["1.60.0", "stable"] 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | with: 20 | submodules: "recursive" 21 | 22 | - name: Use Rust ${{ matrix.rust-version }} 23 | uses: actions-rs/toolchain@v1 24 | with: 25 | toolchain: ${{ matrix.rust-version }} 26 | components: rustfmt, clippy 27 | 28 | - name: Annotate commit with clippy warnings 29 | # Use `actions-rs/clippy` once `working-directory` is supported (PR #158) 30 | uses: reinismu/clippy-check@ce65cdb6b7d4419dcd2e3b2125134b89c1dadecf 31 | with: 32 | token: ${{ secrets.GITHUB_TOKEN }} 33 | args: "--all-targets --all-features -- -D warnings" 34 | working-directory: "rs" 35 | 36 | - name: Build and test 37 | uses: actions-rs/tarpaulin@v0.1 38 | with: 39 | out-type: "lcov" 40 | args: "--manifest-path ./rs/Cargo.toml --count" 41 | 42 | - name: Upload coverage report to codecov.io 43 | uses: codecov/codecov-action@v2 44 | with: 45 | files: "./lcov.info" 46 | -------------------------------------------------------------------------------- /rs/src/complete/video.rs: -------------------------------------------------------------------------------- 1 | use nom::number::complete::le_u8 as parse_u8; 2 | use nom::IResult as NomResult; 3 | use swf_types as ast; 4 | 5 | pub fn video_deblocking_from_code(video_deblocking_id: u8) -> Result { 6 | match video_deblocking_id { 7 | 0 => Ok(ast::VideoDeblocking::PacketValue), 8 | 1 => Ok(ast::VideoDeblocking::Off), 9 | 2 => Ok(ast::VideoDeblocking::Level1), 10 | 3 => Ok(ast::VideoDeblocking::Level2), 11 | 4 => Ok(ast::VideoDeblocking::Level3), 12 | 5 => Ok(ast::VideoDeblocking::Level4), 13 | _ => Err(()), 14 | } 15 | } 16 | 17 | pub fn parse_videoc_codec(input: &[u8]) -> NomResult<&[u8], ast::VideoCodec> { 18 | let (input, codec_id) = parse_u8(input)?; 19 | let codec = video_codec_from_code(codec_id).map_err(|_| nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Switch)))?; 20 | Ok((input, codec)) 21 | } 22 | 23 | pub fn video_codec_from_code(video_codec_id: u8) -> Result { 24 | match video_codec_id { 25 | 0 => Ok(ast::VideoCodec::None), 26 | 1 => Ok(ast::VideoCodec::Jpeg), 27 | 2 => Ok(ast::VideoCodec::Sorenson), 28 | 3 => Ok(ast::VideoCodec::Screen), 29 | 4 => Ok(ast::VideoCodec::Vp6), 30 | 5 => Ok(ast::VideoCodec::Vp6Alpha), 31 | 6 => Ok(ast::VideoCodec::Screen2), 32 | 7 => Ok(ast::VideoCodec::Avc), 33 | _ => Err(()), 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /rs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "swf-parser" 3 | version = "0.14.0" 4 | authors = ["Charles Samborski "] 5 | description = "SWF parser" 6 | documentation = "https://docs.rs/swf-parser" 7 | homepage = "https://github.com/open-flash/swf-parser" 8 | repository = "https://github.com/open-flash/swf-parser" 9 | readme = "./README.md" 10 | keywords = ["parser", "swf", "flash"] 11 | license = "AGPL-3.0-or-later" 12 | edition = "2021" 13 | rust-version = "1.60.0" 14 | 15 | [lib] 16 | name = "swf_parser" 17 | path = "src/lib.rs" 18 | 19 | [dependencies] 20 | half = "1.8.2" 21 | inflate = { version = "0.4.5", optional = true } 22 | lzma-rs = { version = "0.2.0", optional = true } 23 | memchr = "2.5.0" 24 | nom = "7.1.1" 25 | swf-fixed = "0.1.5" 26 | swf-types = { version = "0.14.0", default-features = false } 27 | 28 | [dev-dependencies] 29 | serde = "1.0.137" 30 | serde_json_v8 = "0.1.1" 31 | swf-types = { version = "0.14.0", features = ["serde"] } 32 | test-generator = "0.3.0" 33 | 34 | [features] 35 | default = ["deflate", "lzma"] 36 | # Enable support for SWF movies compressed with declate 37 | deflate = ["dep:inflate"] 38 | # Enable support for SWF movies compressed with LZMA 39 | lzma = ["dep:lzma-rs"] 40 | 41 | # When testing larger files, increasing `opt-level` provides a significant speed-up. 42 | # [profile.test] 43 | # opt-level = 2 44 | 45 | [workspace] 46 | members = ["bin"] 47 | -------------------------------------------------------------------------------- /rs/src/streaming/decompress.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::error::Error; 3 | 4 | type Output<'a> = (&'a [u8], Cow<'a, [u8]>); 5 | 6 | // TODO: return NomError::Incomplete on incomplete inputs? 7 | 8 | pub(crate) fn decompress_none(bytes: &[u8]) -> Result, Box> { 9 | Ok((&[][..], bytes.into())) 10 | } 11 | 12 | #[cfg(feature = "deflate")] 13 | pub(crate) fn decompress_zlib(bytes: &[u8]) -> Result, Box> { 14 | let out = inflate::inflate_bytes_zlib(bytes).map_err(|msg| { 15 | Box::::from(msg) 16 | })?; 17 | Ok((&[][..], out.into())) 18 | } 19 | 20 | #[cfg(not(feature = "deflate"))] 21 | pub(crate) fn decompress_zlib(_bytes: &[u8]) -> Result, Box> { 22 | Err(Box::::from("unsupported SWF compression method `Deflate`: compile `swf-parser` with the `deflate` feature")) 23 | } 24 | 25 | #[cfg(feature = "lzma")] 26 | pub(crate) fn decompress_lzma(mut bytes: &[u8]) -> Result, Box> { 27 | let mut out = Vec::new(); 28 | lzma_rs::lzma_decompress(&mut bytes, &mut out).map_err(Box::new)?; 29 | Ok((bytes, out.into())) 30 | } 31 | 32 | #[cfg(not(feature = "lzma"))] 33 | pub(crate) fn decompress_lzma(_bytes: &[u8]) -> Result, Box> { 34 | Err(Box::::from("unsupported SWF compression method `Lzma`: compile `swf-parser` with the `lzma` feature")) 35 | } 36 | -------------------------------------------------------------------------------- /docs/avm1/actions/implements-op.md: -------------------------------------------------------------------------------- 1 | # ImplementsOp 2 | 3 | ``` 4 | [...interfaces, count, class] → [] 5 | ``` 6 | 7 | - Action Code: `0x2c` 8 | - Stack: `2+ → 0` 9 | - SWF version: `7` 10 | 11 | Does something like `class.__implementedInterfaces = interfaces`. 12 | 13 | ## Original documentation 14 | 15 | ### ActionImplementsOp 16 | 17 | ActionImplementsOp implements the ActionScript implements keyword. The ActionImplementsOp action 18 | specifies the interfaces that a class implements, for use by ActionCastOp. ActionImplementsOp can also specify 19 | the interfaces that an interface implements, as interfaces can extend other interfaces. 20 | 21 | | Field | Type | Comment | 22 | |--------------------|--------------------|-------------------| 23 | | ActionImplementsOp | ACTIONRECORDHEADER | ActionCode = 0x2C | 24 | 25 | ActionImplementsOp does the following: 26 | 1. Pops the constructor function off the stack. The constructor function represents the class that will 27 | implement the interfaces. The constructor function must have a prototype property. 28 | 2. Pops the count of implemented interfaces off the stack. 29 | 3. For each interface count, pops a constructor function off of the stack. The constructor function 30 | represents an interface. 31 | 4. Sets the constructor function’s list of interfaces to the array collected in the previous step, and sets the 32 | count of interfaces to the count popped in step 2. 33 | -------------------------------------------------------------------------------- /docs/avm1/actions/init-array.md: -------------------------------------------------------------------------------- 1 | # InitArray 2 | 3 | ``` 4 | [...items, itemCount] → [array] 5 | ``` 6 | 7 | - Action Code: `0x42` 8 | - Stack: `1+ → 1` 9 | - SWF version: `5` 10 | 11 | ## Original documentation 12 | 13 | ### ActionInitArray 14 | 15 | ActionInitArray initializes an array in a ScriptObject and is similar to ActionInitObject. The newly created object is 16 | pushed to the stack. The stack is the only existing reference to the newly created object. A subsequent 17 | SetVariable or SetMember action can store the newly created object in a variable. 18 | 19 | | Field | Type | Comment | 20 | |-----------------|--------------------|-------------------| 21 | | ActionInitArray | ACTIONRECORDHEADER | ActionCode = 0x42 | 22 | 23 | ActionInitArray pops elems and then [arg1, arg2, ..., argn] off the stack. ActionInitArray does the following: 24 | 25 | 1. Gets the number of arguments (or elements) from the stack. 26 | 2. If arguments are present, ActionInitArray initializes an array object with the right number of elements. 27 | 3. Initializes the array as a ScriptObject. 28 | 4. Sets the object type to Array. 29 | 5. Populates the array with initial elements by popping the values off of the stack. 30 | 31 | For all of the call actions (ActionCallMethod, ActionNewMethod, ActionNewObject, and ActionCallFunction) and 32 | initialization actions (ActionInitObject and ActionInitArray), the arguments of the function are pushed onto the 33 | stack in reverse order, with the rightmost argument first and the leftmost argument last. The arguments are 34 | subsequently popped off in order (first to last). 35 | -------------------------------------------------------------------------------- /ts/src/test/utils.mts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import sysPath from "path"; 3 | 4 | import meta from "./meta.mjs"; 5 | 6 | export const testResourcesRoot: string = meta.dirname; 7 | 8 | export function readTestResource(path: string): Buffer { 9 | return fs.readFileSync(sysPath.resolve(testResourcesRoot, path)); 10 | } 11 | 12 | export function readTestJson(path: string): any { 13 | return JSON.parse(readTestResource(path).toString("utf-8")); 14 | } 15 | 16 | export async function readTextFile(filePath: fs.PathLike): Promise { 17 | return new Promise((resolve, reject): void => { 18 | fs.readFile(filePath, {encoding: "utf-8"}, (err: NodeJS.ErrnoException | null, data: string): void => { 19 | if (err !== null) { 20 | reject(err); 21 | } else { 22 | resolve(data); 23 | } 24 | }); 25 | }); 26 | } 27 | 28 | export async function readFile(filePath: fs.PathLike): Promise { 29 | return new Promise((resolve, reject): void => { 30 | fs.readFile(filePath, {encoding: null}, (err: NodeJS.ErrnoException | null, data: Buffer): void => { 31 | if (err !== null) { 32 | reject(err); 33 | } else { 34 | resolve(data); 35 | } 36 | }); 37 | }); 38 | } 39 | 40 | export async function writeTextFile(filePath: fs.PathLike, text: string): Promise { 41 | return new Promise((resolve, reject): void => { 42 | fs.writeFile(filePath, text, (err: NodeJS.ErrnoException | null): void => { 43 | if (err !== null) { 44 | reject(err); 45 | } else { 46 | resolve(); 47 | } 48 | }); 49 | }); 50 | } 51 | -------------------------------------------------------------------------------- /docs/avm1/actions/call-function.md: -------------------------------------------------------------------------------- 1 | # CallFunction 2 | 3 | ``` 4 | [...arguments, argsCount, functionName] → [returnValue] 5 | ``` 6 | 7 | - Action Code: `0x3d` 8 | - Stack: `2+ → 1` 9 | - SWF version: `5` 10 | 11 | ## Original documentation 12 | 13 | ### ActionCallFunction 14 | 15 | ActionCallFunction executes a function. The function can be an ActionScript built-in function (such as parseInt), a 16 | user-defined ActionScript function, or a native function. For more information, see ActionNewObject. 17 | 18 | | Field | Type | Comment | 19 | |--------------------|--------------------|-------------------| 20 | | ActionCallFunction | ACTIONRECORDHEADER | ActionCode = 0x3D | 21 | 22 | ActionCallFunction does the following: 23 | 1. Pops the function name (String) from the stack. 24 | 2. Pops numArgs (int) from the stack. 25 | 3. Pops the arguments off the stack. 26 | 4. Invokes the function, passing the arguments to it. 27 | 5. Pushes the return value of the function invocation to the stack. 28 | 29 | If no appropriate return value is present (that is, the function does not have a return statement), a push 30 | undefined message is generated by the compiler and is pushed to the stack. The undefined return value 31 | should be popped off the stack. 32 | 33 | For all of the call actions (ActionCallMethod, ActionNewMethod, ActionNewObject, and ActionCallFunction) and 34 | initialization actions (ActionInitObject and ActionInitArray), the arguments of the function are pushed onto the 35 | stack in reverse order, with the rightmost argument first and the leftmost argument last. The arguments are 36 | subsequently popped off in order (first to last). 37 | -------------------------------------------------------------------------------- /ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": false, 4 | "allowSyntheticDefaultImports": true, 5 | "allowUnreachableCode": false, 6 | "allowUnusedLabels": false, 7 | "alwaysStrict": true, 8 | "charset": "utf8", 9 | "checkJs": false, 10 | "declaration": true, 11 | "disableSizeLimit": false, 12 | "downlevelIteration": false, 13 | "emitBOM": false, 14 | "emitDecoratorMetadata": true, 15 | "esModuleInterop": false, 16 | "experimentalDecorators": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "importHelpers": false, 19 | "inlineSourceMap": false, 20 | "inlineSources": false, 21 | "isolatedModules": false, 22 | "lib": [ 23 | "es2021" 24 | ], 25 | "locale": "en-US", 26 | "module": "node12", 27 | "moduleResolution": "node12", 28 | "newLine": "lf", 29 | "noEmit": true, 30 | "noEmitHelpers": false, 31 | "noEmitOnError": true, 32 | "noErrorTruncation": true, 33 | "noFallthroughCasesInSwitch": true, 34 | "noImplicitAny": true, 35 | "noImplicitReturns": true, 36 | "noImplicitThis": true, 37 | "noStrictGenericChecks": false, 38 | "noUnusedLocals": true, 39 | "noUnusedParameters": true, 40 | "noImplicitUseStrict": false, 41 | "noLib": false, 42 | "noResolve": false, 43 | "preserveConstEnums": true, 44 | "removeComments": false, 45 | "skipLibCheck": true, 46 | "sourceMap": true, 47 | "strict": true, 48 | "strictNullChecks": true, 49 | "suppressExcessPropertyErrors": false, 50 | "suppressImplicitAnyIndexErrors": false, 51 | "target": "es2021", 52 | "traceResolution": false, 53 | "rootDir": "." 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /ts/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es2021": true 4 | }, 5 | "plugins": [ 6 | "@typescript-eslint", 7 | "simple-import-sort" 8 | ], 9 | "parser": "@typescript-eslint/parser", 10 | "extends": [ 11 | "eslint:recommended", 12 | "plugin:@typescript-eslint/eslint-recommended" 13 | ], 14 | "globals": {}, 15 | "ignorePatterns": [ 16 | "node_modules" 17 | ], 18 | "rules": { 19 | "block-scoped-var": "error", 20 | "curly": [ 21 | "error", 22 | "all" 23 | ], 24 | "dot-location": [ 25 | "error", 26 | "property" 27 | ], 28 | "eqeqeq": "error", 29 | "indent": [ 30 | "error", 31 | 2, 32 | { 33 | "SwitchCase": 1 34 | } 35 | ], 36 | "linebreak-style": [ 37 | "error", 38 | "unix" 39 | ], 40 | "no-duplicate-imports": "error", 41 | "no-eval": "error", 42 | "no-label-var": "error", 43 | "no-tabs": "error", 44 | "no-trailing-spaces": "error", 45 | "no-unused-vars": "off", 46 | "no-use-before-define": "off", 47 | "no-var": "error", 48 | "one-var-declaration-per-line": "error", 49 | "quotes": [ 50 | "error", 51 | "double" 52 | ], 53 | "semi": [ 54 | "error", 55 | "always" 56 | ], 57 | "semi-spacing": [ 58 | "error", 59 | { 60 | "before": false, 61 | "after": true 62 | } 63 | ], 64 | "simple-import-sort/imports": "error", 65 | "simple-import-sort/exports": "error", 66 | "space-before-blocks": [ 67 | "error", 68 | "always" 69 | ], 70 | "space-infix-ops": "error", 71 | "unicode-bom": [ 72 | "error", 73 | "never" 74 | ] 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /ts/src/lib/parsers/video.mts: -------------------------------------------------------------------------------- 1 | import { ReadableByteStream } from "@open-flash/stream"; 2 | import incident from "incident"; 3 | import { Uint3, Uint8 } from "semantic-types"; 4 | import { VideoCodec } from "swf-types/video/video-codec"; 5 | import { VideoDeblocking } from "swf-types/video/video-deblocking"; 6 | 7 | export function getVideoDeblockingFromCode(videoDeblockingCode: Uint3): VideoDeblocking { 8 | switch (videoDeblockingCode) { 9 | case 0: 10 | return VideoDeblocking.PacketValue; 11 | case 1: 12 | return VideoDeblocking.Off; 13 | case 2: 14 | return VideoDeblocking.Level1; 15 | case 3: 16 | return VideoDeblocking.Level2; 17 | case 4: 18 | return VideoDeblocking.Level3; 19 | case 5: 20 | return VideoDeblocking.Level4; 21 | default: 22 | throw new incident.Incident("UnexpectedVideoDeblockingCode", {code: videoDeblockingCode}); 23 | } 24 | } 25 | 26 | export function parseVideoCodec(byteStream: ReadableByteStream): VideoCodec { 27 | return getVideoCodecFromCode(byteStream.readUint8()); 28 | } 29 | 30 | export function getVideoCodecFromCode(videoCodecCode: Uint8): VideoCodec { 31 | switch (videoCodecCode) { 32 | case 0: 33 | return VideoCodec.None; 34 | case 1: 35 | return VideoCodec.Jpeg; 36 | case 2: 37 | return VideoCodec.Sorenson; 38 | case 3: 39 | return VideoCodec.Screen; 40 | case 4: 41 | return VideoCodec.Vp6; 42 | case 5: 43 | return VideoCodec.Vp6Alpha; 44 | case 6: 45 | return VideoCodec.Screen2; 46 | case 7: 47 | return VideoCodec.Avc; 48 | default: 49 | throw new incident.Incident("UnexpectedVideoCodecCode", {code: videoCodecCode}); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /docs/avm1/actions/get-member.md: -------------------------------------------------------------------------------- 1 | # GetMember 2 | 3 | ``` 4 | [object, name] → [object[name]] 5 | ``` 6 | 7 | - Action Code: `0x4e` 8 | - Stack: `2 → 1` 9 | - SWF version: `5` 10 | 11 | ## Original documentation 12 | 13 | ### ActionGetMember 14 | 15 | ActionGetMember retrieves a named property from an object, and pushes the value of the property onto the 16 | stack. 17 | 18 | | Field | Type | Comment | 19 | |-----------------|--------------------|-------------------| 20 | | ActionGetMember | ACTIONRECORDHEADER | ActionCode = 0x4E | 21 | 22 | ActionGetMember does the following: 23 | 24 | 1. Pops the name of the member function. 25 | 2. Pops the ScriptObject object off of the stack. 26 | 3. Pushes the value of the property on to the stack. 27 | 28 | For example, assume obj is an object, and it is assigned a property, foo, as follows: 29 | ```as2 30 | obj.foo = 3; 31 | ``` 32 | 33 | Then, ActionGetMember with object set to obj and name set to foo pushes 3 onto the stack. If the specified 34 | property does not exist, undefined is pushed to the stack. 35 | 36 | The object parameter cannot actually be of type Object. If the object parameter is a primitive type such as 37 | number, Boolean, or string, it is converted automatically to a temporary wrapper object of class Number, 38 | Boolean, or String. Thus, methods of wrapper objects can be invoked on values of primitive types. For example, 39 | the following correctly prints 5: 40 | 41 | ```as2 42 | var x = "Hello"; 43 | trace (x.length); 44 | ``` 45 | 46 | In this case, the variable, x, contains the primitive string, "Hello". When x.length is retrieved, a temporary 47 | wrapper object for x is created by using the String type, which has a length property. 48 | -------------------------------------------------------------------------------- /docs/avm1/actions/new-object.md: -------------------------------------------------------------------------------- 1 | # NewObject 2 | 3 | ``` 4 | [...args, argsCount, objectName] → [newObject] 5 | ``` 6 | 7 | - Action Code: `0x40` 8 | - Stack: `2+ → 1` 9 | - SWF version: `5` 10 | 11 | ## Original documentation 12 | 13 | ### ActionNewObject 14 | 15 | ActionNewObject invokes a constructor function. A new object is created and passed to the constructor function 16 | as the this keyword. In addition, arguments can optionally be specified to the constructor function on the stack. 17 | The return value of the constructor function is discarded. The newly constructed object is pushed to the stack. 18 | ActionNewObject is similar to ActionCallFunction and ActionNewMethod. 19 | 20 | | Field | Type | Comment | 21 | |------------------|--------------------|-------------------| 22 | | ActionNewObject | ACTIONRECORDHEADER | ActionCode = 0x40 | 23 | 24 | ActionNewObject does the following: 25 | 1. Pops the object name (STRING) this from the stack. 26 | 2. Pops numArgs (int) from the stack. 27 | 3. Pops the arguments off the stack. 28 | 4. Invokes the named object as a constructor function, passing it the specified arguments and a newly 29 | constructed object as the this keyword. 30 | 5. The return value of the constructor function is discarded. 31 | 6. The newly constructed object is pushed to the stack. 32 | 33 | For all of the call actions (ActionCallMethod, ActionNewMethod, ActionNewObject, and ActionCallFunction) and 34 | initialization actions (ActionInitObject and ActionInitArray), the arguments of the function are pushed onto the 35 | stack in reverse order, with the rightmost argument first and the leftmost argument last. The arguments are 36 | subsequently popped off in order (first to last). 37 | -------------------------------------------------------------------------------- /docs/avm1/actions/init-object.md: -------------------------------------------------------------------------------- 1 | # InitObject 2 | 3 | ``` 4 | [...(value, name), itemCount] → [object] 5 | ``` 6 | 7 | - Action Code: `0x43` 8 | - Stack: `2k + 1 → 1` 9 | - SWF version: `5` 10 | 11 | ## Original documentation 12 | 13 | ### ActionInitObject 14 | 15 | ActionInitObject initializes an object and is similar to ActionInitArray. The newly created object is pushed to the 16 | stack. The stack is the only existing reference to the newly created object. A subsequent SetVariable or 17 | SetMember action can store the newly created object in a variable. 18 | 19 | | Field | Type | Comment | 20 | |------------------|--------------------|-------------------| 21 | | ActionInitObject | ACTIONRECORDHEADER | ActionCode = 0x43 | 22 | 23 | ActionInitObject pops elems off of the stack. Pops [value1, name1, ..., valueN, nameN] off the stack. 24 | 25 | ActionInitObject does the following: 26 | 1. Pops the number of initial properties from the stack. 27 | 2. Initializes the object as a ScriptObject. 28 | 3. Sets the object type to Object. 29 | 4. Pops each initial property off the stack. 30 | For each initial property, the value of the property is popped off the stack, then the name of the 31 | property is popped off the stack. The name of the property is converted to a string. The value can be of 32 | any type. 33 | 34 | For all of the call actions (ActionCallMethod, ActionNewMethod, ActionNewObject, and ActionCallFunction) and 35 | initialization actions (ActionInitObject and ActionInitArray), the arguments of the function are pushed onto the 36 | stack in reverse order, with the rightmost argument first and the leftmost argument last. The arguments are 37 | subsequently popped off in order (first to last). 38 | -------------------------------------------------------------------------------- /docs/avm1/actions/goto-frame2.md: -------------------------------------------------------------------------------- 1 | # GotoFrame2 2 | 3 | ``` 4 | [] → [] 5 | ``` 6 | 7 | - Action Code: `0x9f` 8 | - Stack: `0 → 0` 9 | - SWF version: `4` 10 | 11 | ## Original documentation 12 | 13 | ### ActionGotoFrame2 14 | 15 | ActionGotoFrame2 goes to a frame and is stack based. 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 45 | 46 | 47 | 48 | 49 | 50 | 51 |
FieldTypeComment
ActionGotoFrame2ACTIONRECORDHEADERActionCode = 0x9F
ReservedUB[6]Always 0
SceneBiasFlagUB[1]Scene bias flag
Play flagUB[1] 42 | 0 = Go to frame and stop
43 | 1 = Go to frame and play 44 |
SceneBiasIf SceneBiasFlag = 1, UI16 Number to be added to frame determined by stack argument
52 | 53 | ActionGotoFrame2 does the following: 54 | 1. Pops a frame off the stack. 55 | - If the frame is a number, n, the next frame of the movie to be displayed is the nth frame in the 56 | current movie clip. 57 | - If the frame is a string, frame is treated as a frame label. If the specified label exists in the current 58 | movie clip, the labeled frame will become the current frame. Otherwise, the action is ignored. 59 | 2. Either a frame or a number can be prefixed by a target path, for example, /MovieClip:3 or 60 | /MovieClip:FrameLabel. 61 | 3. If the Play flag is set, the action goes to the specified frame and begins playing the enclosing movie clip. 62 | Otherwise, the action goes to the specified frame and stops. 63 | -------------------------------------------------------------------------------- /docs/avm1/actions/extends.md: -------------------------------------------------------------------------------- 1 | # Extends 2 | 3 | ``` 4 | [subclass, superclass] → [] 5 | ``` 6 | 7 | - Action Code: `0x69` 8 | - Stack: `2 → 0` 9 | - SWF version: `7` 10 | 11 | ## Original documentation 12 | 13 | ### ActionExtends 14 | 15 | ActionExtends implements the ActionScript extends keyword. ActionExtends creates an inheritance relationship 16 | between two classes, called the subclass and the superclass. 17 | 18 | SWF 7 adds ActionExtends to the file format to avoid spurious calls to the superclass constructor function (which 19 | would occur when inheritance was established under ActionScript 1.0). Consider the following code: 20 | 21 | ```as2 22 | Subclass.prototype = new Superclass(); 23 | ``` 24 | 25 | Before the existence of ActionExtends, this code would result in a spurious call to the Superclass 26 | superconstructor function. Now, ActionExtends is generated by the ActionScript compiler when the code class A 27 | extends B is encountered, to set up the inheritance relationship between A and B. 28 | 29 | | Field | Type | Comment | 30 | |--------------------|--------------------|-------------------| 31 | | ActionCastOp | ACTIONRECORDHEADER | ActionCode = 0x69 | 32 | 33 | ActionExtends does the following: 34 | 1. Pops the ScriptObject superclass constructor off the stack. 35 | 2. Pops the ScriptObject subclass constructor off the stack. 36 | 3. Creates a new ScriptObject. 37 | 4. Sets the new ScriptObject’s proto property to the superclass’ prototype property. 38 | 5. Sets the new ScriptObject’s constructor property to the superclass. 39 | 6. Sets the subclass’ prototype property to the new ScriptObject. These steps are 40 | the equivalent to the following ActionScript: 41 | 42 | ```as2 43 | Subclass.prototype = new Object(); 44 | Subclass.prototype. proto = Superclass.prototype; 45 | Subclass.prototype. constructor = Superclass; 46 | ``` 47 | -------------------------------------------------------------------------------- /docs/avm1/actions/new-method.md: -------------------------------------------------------------------------------- 1 | # NewMethod 2 | 3 | ``` 4 | [...args, argsCount, object, methodName] → [newObject] 5 | ``` 6 | 7 | - Action Code: `0x53` 8 | - Stack: `3+ → 1` 9 | - SWF version: `5` 10 | 11 | ## Original documentation 12 | 13 | ### ActionNewMethod 14 | 15 | ActionNewMethod invokes a constructor function to create a new object. A new object is constructed and 16 | passed to the constructor function as the value of the this keyword. Arguments can be specified to the 17 | constructor function. The return value from the constructor function is discarded. The newly constructed object 18 | is pushed to the stack, similar to ActionCallMethod and ActionNewObject. 19 | 20 | | Field | Type | Comment | 21 | |------------------|--------------------|-------------------| 22 | | ActionNewMethod | ACTIONRECORDHEADER | ActionCode = 0x53 | 23 | 24 | ActionNewMethod does the following: 25 | 1. Pops the name of the method from the stack. 26 | 2. Pops the ScriptObject from the stack. If the name of the method is blank, the ScriptObject is treated as a 27 | function object that is invoked as the constructor function. If the method name is not blank, the named 28 | method of the ScriptObject is invoked. 29 | 3. Pops the number of arguments from the stack. 30 | 4. Executes the method call. 31 | 5. Pushes the newly constructed object to the stack. If no appropriate return value occurs (for instance, the 32 | function does not have a return statement), the compiler generates a push undefined and pushes it to 33 | the stack. The undefined return value should be popped off the stack. 34 | 35 | For all of the call actions (ActionCallMethod, ActionNewMethod, ActionNewObject, and ActionCallFunction) and 36 | initialization actions (ActionInitObject and ActionInitArray), the arguments of the function are pushed onto the 37 | stack in reverse order, with the rightmost argument first and the leftmost argument last. The arguments are 38 | subsequently popped off in order (first to last). 39 | -------------------------------------------------------------------------------- /docs/avm1/actions/call-method.md: -------------------------------------------------------------------------------- 1 | # CallMethod 2 | 3 | ``` 4 | [...arguments, argsCount, methodName, object] → [returnValue] 5 | ``` 6 | 7 | - Action Code: `0x52` 8 | - Stack: `3+ → 1` 9 | - SWF version: `5` 10 | 11 | ## Original documentation 12 | 13 | ### ActionCallMethod 14 | 15 | ActionCallMethod pushes a method (function) call onto the stack, similar to ActionNewMethod. 16 | 17 | | Field | Type | Comment | 18 | |--------------------|--------------------|-------------------| 19 | | ActionCallMethod | ACTIONRECORDHEADER | ActionCode = 0x52 | 20 | 21 | If the named method exists, ActionCallMethod does the following: 22 | 1. Pops the name of the method from the stack. If the method name is blank or undefined, the object is 23 | taken to be a function object that should be invoked, rather than the container object of a method. For 24 | example, if CallMethod is invoked with object obj and method name blank, it's equivalent to using the 25 | syntax: 26 | ```as2 27 | obj(); 28 | ``` 29 | If a method’s name is foo, it's equivalent to: 30 | ```as2 31 | obj.foo(); 32 | ``` 33 | 2. Pops the ScriptObject, object, from the stack. 34 | 3. Pops the number of arguments, args, from the stack. 35 | 4. Pops the arguments off the stack. 36 | 5. Executes the method call with the specified arguments. 37 | 6. Pushes the return value of the method or function to the stack. 38 | If no appropriate return value is present (the function does not have a return statement), a push 39 | undefined is generated by the compiler and is pushed to the stack. The undefined return value should 40 | be popped off the stack. 41 | 42 | For all of the call actions (ActionCallMethod, ActionNewMethod, ActionNewObject, and ActionCallFunction) and 43 | initialization actions (ActionInitObject and ActionInitArray), the arguments of the function are pushed onto the 44 | stack in reverse order, with the rightmost argument first and the leftmost argument last. The arguments are 45 | subsequently popped off in order (first to last). 46 | -------------------------------------------------------------------------------- /docs/avm1/actions/get-property.md: -------------------------------------------------------------------------------- 1 | # GetProperty 2 | 3 | ``` 4 | [target, index] → [target[getPropertyname(index)]] 5 | ``` 6 | 7 | - Action Code: `0x22` 8 | - Stack: `2 → 1` 9 | - SWF version: `4` 10 | 11 | ## Original documentation 12 | 13 | ### ActionGetProperty 14 | 15 | ActionGetProperty gets a file property. 16 | 17 | | Field | Type | Comment | 18 | |-------------------|--------------------|-------------------| 19 | | ActionGetProperty | ACTIONRECORDHEADER | ActionCode = 0x22 | 20 | 21 | ActionGetProperty does the following: 22 | 1. Pops index off the stack. 23 | 2. Pops target off the stack. 24 | 3. Retrieves the value of the property enumerated as index from the movie clip with target path target and 25 | pushes the value to the stack. 26 | The following table lists property index values. The _quality, _xmouse and _ymouse properties are available in 27 | SWF 5 and later. 28 | 29 | 30 | | Property | Value | 31 | |-------------------|--------------------| 32 | | _X | 0 | 33 | | _Y | 1 | 34 | | _xscale | 2 | 35 | | _yscale | 3 | 36 | | _currentframe | 4 | 37 | | _totalframes | 5 | 38 | | _alpha | 6 | 39 | | _visible | 7 | 40 | | _width | 8 | 41 | | _height | 9 | 42 | | _rotation | 10 | 43 | | _target | 11 | 44 | | _framesloaded | 12 | 45 | | _name | 13 | 46 | | _droptarget | 14 | 47 | | _url | 15 | 48 | | _highquality | 16 | 49 | | _focusrect | 17 | 50 | | _soundbuftime | 18 | 51 | | _quality | 19 | 52 | | _xmouse | 20 | 53 | | _ymouse | 21 | 54 | -------------------------------------------------------------------------------- /ts/src/lib/parsers/movie.mts: -------------------------------------------------------------------------------- 1 | import { ReadableStream } from "@open-flash/stream"; 2 | import incident from "incident"; 3 | import pako from "pako"; 4 | import { Uint8 } from "semantic-types"; 5 | import { CompressionMethod } from "swf-types/compression-method"; 6 | import { Header } from "swf-types/header"; 7 | import { Movie } from "swf-types/movie"; 8 | import { SwfSignature } from "swf-types/swf-signature"; 9 | import { Tag } from "swf-types/tag"; 10 | 11 | import { parseHeader, parseSwfSignature } from "./header.mjs"; 12 | import { parseTagBlockString } from "./tags.mjs"; 13 | 14 | /** 15 | * Parses a completely loaded SWF file. 16 | * 17 | * @param byteStream SWF stream to parse 18 | */ 19 | export function parseSwf(byteStream: ReadableStream): Movie { 20 | const signature: SwfSignature = parseSwfSignature(byteStream); 21 | switch (signature.compressionMethod) { 22 | case CompressionMethod.None: { 23 | return parseMovie(byteStream, signature.swfVersion); 24 | } 25 | case CompressionMethod.Deflate: { 26 | const tail: Uint8Array = byteStream.tailBytes(); 27 | const payload: Uint8Array = pako.inflate(tail); 28 | const payloadStream: ReadableStream = new ReadableStream(payload); 29 | return parseMovie(payloadStream, signature.swfVersion); 30 | } 31 | case CompressionMethod.Lzma: { 32 | throw new incident.Incident("NotImplemented", "Support for LZMA compression is not implemented yet"); 33 | } 34 | default: { 35 | throw new incident.Incident("UnknownCompressionMethod", "Unknown compression method"); 36 | } 37 | } 38 | } 39 | 40 | /** 41 | * Parses a completely loaded movie. 42 | * 43 | * The movie is the uncompressed payload of the SWF. 44 | * 45 | * @param byteStream Movie bytestream 46 | * @param swfVersion Parsed movie. 47 | */ 48 | export function parseMovie(byteStream: ReadableStream, swfVersion: Uint8): Movie { 49 | const header: Header = parseHeader(byteStream, swfVersion); 50 | const tags: Tag[] = parseTagBlockString(byteStream, swfVersion); 51 | return {header, tags}; 52 | } 53 | -------------------------------------------------------------------------------- /ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "swf-parser", 3 | "version": "0.14.1", 4 | "description": "SWF parser", 5 | "licenses": [ 6 | { 7 | "type": "AGPL-3.0-or-later", 8 | "url": "https://spdx.org/licenses/AGPL-3.0-or-later.html" 9 | } 10 | ], 11 | "keywords": [ 12 | "swf", 13 | "parser" 14 | ], 15 | "homepage": "https://github.com/open-flash/swf-parser", 16 | "repository": { 17 | "type": "git", 18 | "url": "git@github.com:open-flash/swf-parser.git" 19 | }, 20 | "scripts": { 21 | "clean": "rimraf \"./@(lib|main|test)\"", 22 | "lint": "eslint \"./src/**/*.mts\"", 23 | "format": "eslint --fix \"./src/**/*.mts\"", 24 | "build": "tsc --build src/lib/tsconfig.json", 25 | "prepack": "yarn run build", 26 | "test:build": "tsc --build src/test/tsconfig.json", 27 | "test": "yarn run test:build && mocha \"test/**/*.spec.mjs\"", 28 | "main:build": "tsc --build src/main/tsconfig.json", 29 | "start": "yarn run main:build && node ./main/main.mjs" 30 | }, 31 | "engines": { 32 | "node": ">=14.13.1" 33 | }, 34 | "dependencies": { 35 | "@open-flash/stream": "^0.5.0", 36 | "@types/pako": "^1.0.3", 37 | "incident": "^3.2.1", 38 | "pako": "^2.0.4", 39 | "semantic-types": "^0.1.1", 40 | "swf-types": "^0.14.0" 41 | }, 42 | "devDependencies": { 43 | "@types/chai": "^4.3.1", 44 | "@types/mocha": "^9.1.1", 45 | "@types/node": "^17.0.31", 46 | "@typescript-eslint/eslint-plugin": "^5.22.0", 47 | "@typescript-eslint/parser": "^5.22.0", 48 | "chai": "^4.3.6", 49 | "eslint": "^8.15.0", 50 | "eslint-plugin-simple-import-sort": "^7.0.0", 51 | "furi": "^2.0.0", 52 | "kryo": "^0.14.0", 53 | "kryo-json": "^0.14.0", 54 | "mocha": "^10.0.0", 55 | "rimraf": "^3.0.2", 56 | "typescript": "=4.7.0-beta" 57 | }, 58 | "type": "module", 59 | "exports": { 60 | ".": "./lib/index.mjs", 61 | "./*": "./lib/*.mjs" 62 | }, 63 | "files": [ 64 | "./lib/**/*.(mjs|map|mts)", 65 | "./src/lib/**/*.mts" 66 | ], 67 | "packageManager": "yarn@4.0.0-rc.4" 68 | } 69 | -------------------------------------------------------------------------------- /rs/src/streaming/parser/deflate.rs: -------------------------------------------------------------------------------- 1 | use crate::stream_buffer::StreamBuffer; 2 | use inflate::InflateStream; 3 | use swf_types::{Header as SwfHeader, SwfSignature, Tag}; 4 | use super::{ParseTagsError, SimpleStream}; 5 | 6 | /// State of the `Deflate` payload parser 7 | pub(crate) struct DeflateStream { 8 | inflater: InflateStream, 9 | simple: SimpleStream, 10 | } 11 | 12 | impl DeflateStream { 13 | pub(crate) fn new(buffer: B, signature: SwfSignature) -> Self { 14 | let inflater = inflate::InflateStream::from_zlib(); 15 | let simple = SimpleStream::new(B::new(), signature); 16 | let mut deflate_stream = Self { inflater, simple }; 17 | deflate_stream.write(buffer.get()); 18 | deflate_stream 19 | } 20 | 21 | /// Appends data to the internal buffer. 22 | pub(crate) fn write(&mut self, mut bytes: &[u8]) { 23 | while !bytes.is_empty() { 24 | match self.inflater.update(bytes) { 25 | Ok((read_count, chunk)) => { 26 | bytes = &bytes[read_count..]; 27 | self.simple.write(chunk); 28 | } 29 | Err(e) => panic!("Failed to write Deflate payload {}", e), 30 | } 31 | } 32 | } 33 | 34 | /// Finishes parsing the SWF header from the internal buffer. 35 | pub(crate) fn header(self) -> Result<(SwfHeader, Self), Self> { 36 | match self.simple.header() { 37 | Ok((header, simple)) => Ok(( 38 | header, 39 | Self { 40 | inflater: self.inflater, 41 | simple, 42 | }, 43 | )), 44 | Err(simple) => Err(Self { 45 | inflater: self.inflater, 46 | simple, 47 | }), 48 | } 49 | } 50 | 51 | /// Parses the available tags from the internal buffer. 52 | /// 53 | /// Returns `Ok(None)` if parsing is complete (there are no more tags). 54 | /// Returns `Ok(Some(Vec))` when some tags are available. `Vec` is non-empty. 55 | /// Returns `Err(())` when there's not enough data or an error occurs. 56 | pub(crate) fn tags(&mut self) -> Result>, ParseTagsError> { 57 | self.simple.tags() 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /ts/.gitignore: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Editor # 3 | ############################################################################### 4 | # JetBrains products (Webstorm, IntelliJ IDEA, ...) 5 | .idea/ 6 | *.iml 7 | 8 | # Komodo 9 | *.komodoproject 10 | .komodotools 11 | 12 | ############################################################################### 13 | # Build # 14 | ############################################################################### 15 | /coverage/ 16 | /typedoc/ 17 | /lib/ 18 | /main/ 19 | /test/ 20 | 21 | ############################################################################### 22 | # Dependencies # 23 | ############################################################################### 24 | # Node dependencies directory 25 | node_modules/ 26 | # Yarn without Zero-Installs 27 | # https://yarnpkg.com/advanced/qa#which-files-should-be-gitignored 28 | .pnp.* 29 | .yarn/* 30 | !.yarn/patches 31 | !.yarn/plugins 32 | !.yarn/releases 33 | !.yarn/sdks 34 | !.yarn/versions 35 | 36 | ############################################################################### 37 | # Local configuration # 38 | ############################################################################### 39 | test-config.js 40 | .env 41 | 42 | ############################################################################### 43 | # Temporary files # 44 | ############################################################################### 45 | *.log 46 | *.tmp 47 | tmp/ 48 | logs/ 49 | 50 | ############################################################################### 51 | # Other # 52 | ############################################################################### 53 | # Runtime data 54 | pids 55 | *.pid 56 | *.seed 57 | -------------------------------------------------------------------------------- /ts/README.md: -------------------------------------------------------------------------------- 1 | 2 | Open Flash logo 4 | 5 | 6 | # SWF Parser (TypeScript) 7 | 8 | [![GitHub repository](https://img.shields.io/badge/GitHub-open--flash%2Fswf--parser-informational.svg)](https://github.com/open-flash/swf-parser) 9 | npm package 10 | TypeScript checks status 11 | 12 | SWF parser implemented in Typescript, for Node and browsers. 13 | Converts bytes to [`swf-types` movies][swf-types]. 14 | 15 | ## Usage 16 | 17 | ```typescript 18 | import fs from "fs"; 19 | import { Movie } from "swf-types"; 20 | import { parseSwf } from "swf-parser"; 21 | 22 | const bytes: Uint8Array = fs.readFileSync("movie.swf"); 23 | const movie: Movie = parseSwf(bytes); 24 | ``` 25 | 26 | ## Contributing 27 | 28 | This repo uses Git submodules for its test samples: 29 | 30 | ```sh 31 | # Clone with submodules 32 | git clone --recurse-submodules git://github.com/open-flash/swf-parser.git 33 | # Update submodules for an already-cloned repo 34 | git submodule update --init --recursive --remote 35 | ``` 36 | 37 | This library uses Gulp and npm for its builds, yarn is recommended for the 38 | dependencies. **The commands must be run from the `ts` directory.** 39 | 40 | ``` 41 | cd ts 42 | yarn install 43 | # work your changes... 44 | yarn test 45 | ``` 46 | 47 | Prefer non-`master` branches when sending a PR so your changes can be rebased if 48 | needed. All the commits must be made on top of `master` (fast-forward merge). 49 | CI must pass for changes to be accepted. 50 | 51 | **[Documentation for the available Gulp tasks](https://github.com/demurgos/turbo-gulp/blob/master/docs/usage.md#main-tasks)** 52 | 53 | [swf-types]: https://github.com/open-flash/swf-types 54 | -------------------------------------------------------------------------------- /ts/src/lib/parsers/header.mts: -------------------------------------------------------------------------------- 1 | import { ReadableByteStream } from "@open-flash/stream"; 2 | import incident from "incident"; 3 | import { Uint8,Uint16, Uint32 } from "semantic-types"; 4 | import { CompressionMethod } from "swf-types/compression-method"; 5 | import { Ufixed8P8 } from "swf-types/fixed-point/ufixed8p8"; 6 | import { Header } from "swf-types/header"; 7 | import { Rect } from "swf-types/rect"; 8 | import { SwfSignature } from "swf-types/swf-signature"; 9 | 10 | import { createIncompleteStreamError } from "../errors/incomplete-stream.mjs"; 11 | import { parseRect } from "./basic-data-types.mjs"; 12 | 13 | const UPPER_C: number = "C".charCodeAt(0); 14 | const UPPER_F: number = "F".charCodeAt(0); 15 | const UPPER_S: number = "S".charCodeAt(0); 16 | const UPPER_W: number = "W".charCodeAt(0); 17 | const UPPER_Z: number = "Z".charCodeAt(0); 18 | 19 | export function parseSwfSignature(byteStream: ReadableByteStream): SwfSignature { 20 | if (byteStream.available() < 8) { 21 | throw createIncompleteStreamError(8); 22 | } 23 | 24 | const compressionMethod: CompressionMethod = parseCompressionMethod(byteStream); 25 | const swfVersion: Uint8 = byteStream.readUint8(); 26 | const uncompressedFileLength: Uint32 = byteStream.readUint32LE(); 27 | 28 | return {compressionMethod, swfVersion, uncompressedFileLength}; 29 | } 30 | 31 | // TODO: Move to `movie.ts` 32 | export function parseCompressionMethod(byteStream: ReadableByteStream): CompressionMethod { 33 | const bytes: Uint8Array = byteStream.takeBytes(3); 34 | // Read FWS, CWS or ZWS 35 | if (bytes[1] !== UPPER_W || bytes[2] !== UPPER_S) { 36 | throw incident.Incident("InvalidCompressionMethod", {bytes}, "Invalid compression method"); 37 | } 38 | 39 | switch (bytes[0]) { 40 | case UPPER_F: 41 | return CompressionMethod.None; 42 | case UPPER_C: 43 | return CompressionMethod.Deflate; 44 | case UPPER_Z: 45 | return CompressionMethod.Lzma; 46 | default: 47 | throw incident.Incident("InvalidCompressionMethod", {bytes}, "Invalid compression method"); 48 | } 49 | } 50 | 51 | export function parseHeader(byteStream: ReadableByteStream, swfVersion: Uint8): Header { 52 | const frameSize: Rect = parseRect(byteStream); 53 | const frameRate: Ufixed8P8 = Ufixed8P8.fromEpsilons(byteStream.readUint16LE()); 54 | const frameCount: Uint16 = byteStream.readUint16LE(); 55 | return {swfVersion, frameSize, frameRate, frameCount}; 56 | } 57 | -------------------------------------------------------------------------------- /docs/avm1/actions/try.md: -------------------------------------------------------------------------------- 1 | # Try 2 | 3 | ``` 4 | [] → [] 5 | ``` 6 | 7 | - Action Code: `0x8f` 8 | - Stack: `0 → 0` 9 | - SWF version: `7` 10 | 11 | ## Original documentation 12 | 13 | ### ActionTry 14 | 15 | ActionTry defines handlers for exceptional conditions, implementing the ActionScript try, catch, and finally 16 | keywords. 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 41 | 42 | 43 | 44 | 45 | 49 | 50 | 51 | 52 | 53 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 |
FieldTypeComment
ActionTryACTIONRECORDHEADERActionCode = 0x8F
ReservedUB[5]Always zero
CatchInRegisterFlagUB[1] 38 | 0 - Do not put caught object into register (insteadstore in named variable)
39 | 1 - Put caught object into register (do not store in named variable) 40 |
FinallyBlockFlagUB[1] 46 | 0 - No finally block
47 | 1 - Has finally block 48 |
CatchBlockFlagUB[1] 54 | 0 - No catch block
55 | 1 - Has catch block 56 |
TrySizeUI16Length of the try block
CatchSizeUI16Length of the catch block
FinallySizeUI16Length of the finally block
CatchNameIf CatchInRegisterFlag = 0, STRINGName of the catch variable
CatchRegisterIf CatchInRegisterFlag = 1, UI8Register to catch into
TryBodyUI8[TrySize]Body of the try block
CatchBodyUI8[CatchSize]Body of the catch block, if any
FinallyBodyUI8[FinallySize]Body of the finally block, if any
99 | 100 | The CatchSize and FinallySize fields always exist, whether or not the CatchBlockFlag or FinallyBlockFlag settings 101 | are 1. 102 | The try, catch, and finally blocks do not use end tags to mark the end of their respective blocks. Instead, the 103 | length of a block is set by the TrySize, CatchSize, and FinallySize values. 104 | -------------------------------------------------------------------------------- /docs/avm1/actions/define-function.md: -------------------------------------------------------------------------------- 1 | # DefineFunction 2 | 3 | ``` 4 | [] → [function] 5 | ``` 6 | 7 | - Action Code: `0x9b` 8 | - Stack: `0 → 1` 9 | - SWF version: `5` 10 | 11 | ## Original documentation 12 | 13 | ### ActionDefineFunction 14 | 15 | **Note**: ActionDefineFunction is rarely used as of SWF 7 and later; it was superseded by ActionDefineFunction2. 16 | 17 | ActionDefineFunction defines a function with a given name and body size. 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 |
FieldTypeComment
ActionDefineFunctionACTIONRECORDHEADERActionCode = 0x9B
FunctionNameSTRINGFunction name, empty if anonymous
NumParamsUI16# of parameters
param 1STRINGParameter name 1
param 2STRINGParameter name 2
... 53 |
param NSTRINGParameter name N
codeSizeUI16# of bytes of code that follow
66 | 67 | ActionDefineFunction parses (in order) FunctionName, NumParams, [param1, param2, ..., param N] and then 68 | code size. 69 | 70 | ActionDefineFunction does the following: 71 | 72 | 1. Parses the name of the function (name) from the action tag. 73 | 2. Skips the parameters in the tag. 74 | 3. Parses the code size from the tag. After the DefineFunction tag, the next codeSize bytes of action data are 75 | considered to be the body of the function. 76 | 4. Gets the code for the function. 77 | 78 | ActionDefineFunction can be used in the following ways: 79 | 80 | Usage 1 Pushes an anonymous function on the stack that does not persist. This function is a function literal that 81 | is declared in an expression instead of a statement. An anonymous function can be used to define a function, 82 | return its value, and assign it to a variable in one expression, as in the following ActionScript: 83 | ```as2 84 | area = (function () {return Math.PI * radius *radius;})(5); 85 | ``` 86 | 87 | Usage 2 Sets a variable with a given FunctionName and a given function definition. This is the more 88 | conventional function definition. For example, in ActionScript: 89 | ```as2 90 | function Circle(radius) { 91 | this.radius = radius; 92 | this.area = Math.PI * radius * radius; 93 | } 94 | ``` 95 | -------------------------------------------------------------------------------- /rs/README.md: -------------------------------------------------------------------------------- 1 | 2 | Open Flash logo 4 | 5 | 6 | # SWF Parser (Rust) 7 | 8 | [![GitHub repository](https://img.shields.io/badge/GitHub-open--flash%2Fswf--parser-informational.svg)](https://github.com/open-flash/swf-parser) 9 | crates.io crate 10 | Rust checks status 11 | docs.rs/swf-parser 12 | 13 | SWF parser implemented in Rust. 14 | Converts bytes to [`swf-types` movies][swf-types]. 15 | 16 | ## Usage 17 | 18 | ```rust 19 | use swf_parser::parse_swf; 20 | use swf_types::Movie; 21 | 22 | fn main() { 23 | let swf_bytes: Vec = ::std::fs::read("movie.swf").expect("Failed to read movie"); 24 | let movie: Movie = parse_swf(&swf_bytes).expect("Failed to parse SWF"); 25 | } 26 | ``` 27 | 28 | ## Features 29 | 30 | SWF decompression is provided by the following features, enabled by default: 31 | 32 | - `deflate`: enable support for `CompressionMethod::Deflate`, using the [`inflate`](https://github.com/image-rs/inflate) crate. 33 | - `lzma`: enable support for `CompressionMethod::Lzma`, using the [`lzma-rs`](https://github.com/gendx/lzma-rs) crate. 34 | 35 | Disabling these features will cause the SWF parsing functions to fail when passed the corresponding `CompressionMethod`. 36 | 37 | ## Contributing 38 | 39 | This repo uses Git submodules for its test samples: 40 | 41 | ```sh 42 | # Clone with submodules 43 | git clone --recurse-submodules git://github.com/open-flash/swf-parser.git 44 | # Update submodules for an already-cloned repo 45 | git submodule update --init --recursive --remote 46 | ``` 47 | 48 | This library is a standard Cargo project. You can test your changes with 49 | `cargo test`. **The commands must be run from the `rs` directory.** 50 | 51 | ## Fuzzing 52 | 53 | The Rust implementation supports fuzzing: 54 | 55 | ``` 56 | # Make sure that you have `cargo-fuzz` 57 | cargo install cargo-fuzz 58 | # Fuzz the `swf` parser 59 | cargo fuzz run swf 60 | ``` 61 | 62 | Prefer non-`master` branches when sending a PR so your changes can be rebased if 63 | needed. All the commits must be made on top of `master` (fast-forward merge). 64 | CI must pass for changes to be accepted. 65 | 66 | [swf-types]: https://github.com/open-flash/swf-types 67 | -------------------------------------------------------------------------------- /rs/src/streaming/parser/simple.rs: -------------------------------------------------------------------------------- 1 | use crate::stream_buffer::StreamBuffer; 2 | use crate::streaming::movie::parse_header; 3 | use crate::streaming::tag::parse_tag; 4 | use swf_types::{Header as SwfHeader, Tag, SwfSignature}; 5 | 6 | use super::ParseTagsError; 7 | 8 | /// State of the uncompressed payload parser 9 | pub(crate) struct SimpleStream { 10 | buffer: B, 11 | swf_version: u8, 12 | is_end: bool, 13 | } 14 | 15 | impl SimpleStream { 16 | pub(crate) fn new(buffer: B, signature: SwfSignature) -> Self { 17 | // TODO: track uncompressed length of signature? 18 | Self { 19 | buffer, 20 | swf_version: signature.swf_version, 21 | is_end: false, 22 | } 23 | } 24 | 25 | /// Appends data to the internal buffer. 26 | pub(crate) fn write(&mut self, bytes: &[u8]) { 27 | self.buffer.write(bytes); 28 | } 29 | 30 | /// Finishes parsing the SWF header from the internal buffer. 31 | pub(crate) fn header(mut self) -> Result<(SwfHeader, Self), Self> { 32 | let buffer: &[u8] = self.buffer.get(); 33 | let (remaining, header) = match parse_header(buffer, self.swf_version) { 34 | Ok(ok) => ok, 35 | Err(nom::Err::Incomplete(_)) => return Err(self), 36 | Err(nom::Err::Error(_)) | Err(nom::Err::Failure(_)) => return Err(self), 37 | }; 38 | let parsed_len: usize = buffer.len() - remaining.len(); 39 | 40 | self.buffer.clear(parsed_len); 41 | 42 | Ok((header, self)) 43 | } 44 | 45 | /// Parses the available tags from the internal buffer. 46 | /// 47 | /// Returns `Ok(None)` if parsing is complete (there are no more tags). 48 | /// Returns `Ok(Some(Vec))` when some tags are available. `Vec` is non-empty. 49 | /// Returns `Err(())` when there's not enough data or an error occurs. 50 | pub(crate) fn tags(&mut self) -> Result>, ParseTagsError> { 51 | if self.is_end { 52 | return Ok(None); 53 | } 54 | 55 | let buffer: &[u8] = self.buffer.get(); 56 | 57 | let mut input: &[u8] = buffer; 58 | let mut tags: Vec = Vec::new(); 59 | let is_end: bool = loop { 60 | match parse_tag(input, self.swf_version) { 61 | Ok((_, None)) => { 62 | input = &[][..]; 63 | break true; 64 | } 65 | Ok((next_input, Some(tag))) => { 66 | tags.push(tag); 67 | input = next_input; 68 | } 69 | Err(_) => { 70 | break false; 71 | } 72 | }; 73 | }; 74 | 75 | if is_end { 76 | self.is_end = true; 77 | } 78 | 79 | let parsed_len: usize = buffer.len() - input.len(); 80 | self.buffer.clear(parsed_len); 81 | 82 | if tags.is_empty() { 83 | if is_end { 84 | Ok(None) 85 | } else { 86 | Err(ParseTagsError) 87 | } 88 | } else { 89 | Ok(Some(tags)) 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /docs/avm1/actions/push.md: -------------------------------------------------------------------------------- 1 | # Push 2 | 3 | ``` 4 | [] → [value*] 5 | ``` 6 | 7 | - Action Code: `0x96` 8 | - Stack: `0 → n` 9 | - SWF version: `4` 10 | 11 | ## Original documentation 12 | 13 | ### ActionPush 14 | 15 | ActionPush pushes one or more values to the stack. 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 |
FieldTypeComment
ActionPushACTIONRECORDHEADERActionCode = 0x96
TypeUI8

0 = string literal
32 | 1 = floating-point literal

33 | The following types are available in SWF, 5 and later:
34 | 2 = null
35 | 3 = undefined
36 | 4 = register
37 | 5 = boolean
38 | 6 = double
39 | 7 = integer
40 | 8 = constant 8
41 | 9 = constant 16

StringIf Type = 0, STRINGNull-terminated character string
FloatIf Type = 1, FLOAT32-bit IEEE single-precision little-endian floating-point value
RegisterNumberIf Type = 4, UI8Register number
BooleanIf Type = 5, UI8Boolean value
DoubleIf Type = 6, DOUBLE64-bit IEEE double-precision little-endian double value
IntegerIf Type = 7, UI3232-bit little-endian integer
Constant8If Type = 8, UI8Constant pool index (for indexes < 256) (see ActionConstantPool)
Constant16If Type = 9, UI16Constant pool index (for indexes >= 256) (see ActionConstantPool)
84 | 85 | ActionPush pushes one or more values onto the stack. The Type field specifies the type of the value to be 86 | pushed. 87 | 88 | If Type = 1, the value to be pushed is specified as a 32-bit IEEE single-precision little-endian floating-point value. 89 | PropertyIds are pushed as FLOATs. ActionGetProperty and ActionSetProperty use PropertyIds to access the 90 | properties of named objects. 91 | 92 | If Type = 4, the value to be pushed is a register number. Flash Player supports up to 4 registers. With the use of 93 | ActionDefineFunction2, up to 256 registers can be used. 94 | 95 | In the first field of ActionPush, the length in ACTIONRECORD defines the total number of Type and value bytes 96 | that follow the ACTIONRECORD itself. More than one set of Type and value fields may follow the first field, 97 | depending on the number of bytes that the length in ACTIONRECORD specifies. 98 | ActionPop 99 | -------------------------------------------------------------------------------- /docs/avm1/actions/get-url2.md: -------------------------------------------------------------------------------- 1 | # GetUrl2 2 | 3 | ``` 4 | [url, target] → [] 5 | ``` 6 | 7 | - Action Code: `0x9a` 8 | - Stack: `2 → 0` 9 | - SWF version: `4` 10 | 11 | ## Original documentation 12 | 13 | ### ActionGetURL2 14 | 15 | ActionGetURL2 gets a URL and is stack based. 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 30 | 31 | 32 | 33 | 34 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 51 | 52 | 53 | 54 | 55 | 59 | 60 |
FieldTypeComment
ActionGetURL2ACTIONRECORDHEADER 27 | ActionCode = 0x9A
28 | Length is always 1 29 |
SendVarsMethodUB[2] 35 | 0 = None; 1 = GET
36 | 2 = POST 37 |
ReservedUB[4]Always 0
LoadTargetFlagUB[1] 48 | 0 = Target is a browser window
49 | 1 = Target is a path to a sprite 50 |
LoadVariablesFlagUB[1] 56 | 0 = No variables to load
57 | 1 = Load variables 58 |
61 | 62 | ActionGetURL2 does the following: 63 | 1. Pops target off the stack. 64 | - A LoadTargetFlag value of 0 indicates that the target is a window. The target can be an empty string 65 | to indicate the current window. 66 | - A LoadTargetFlag value of 1 indicates that the target is a path to a sprite. The target path can be in 67 | slash or dot syntax. 68 | 2. Pops a URL off the stack; the URL specifies the URL to be retrieved. 69 | 3. SendVarsMethod specifies the method to use for the HTTP request. 70 | - A SendVarsMethod value of 0 indicates that this is not a form request, so the movie clip’s variables 71 | should not be encoded and submitted. 72 | - A SendVarsMethod value of 1 specifies a HTTP GET request. 73 | - A SendVarsMethod value of 2 specifies a HTTP POST request. 74 | 4. If the SendVarsMethod value is 1 (GET) or 2 (POST), the variables in the current movie clip are submitted 75 | to the URL by using the standard x-www-form-urlencoded encoding and the HTTP request method 76 | specified by method. 77 | 78 | If the LoadVariablesFlag is set, the server is expected to respond with a MIME type of application/x-www-form- 79 | urlencoded and a body in the format var1=value1&var2=value2&...&varx=valuex. This response is used to 80 | populate ActionScript variables rather than display a document. The variables populated can be in a timeline (if 81 | LoadTargetFlag is 0) or in the specified sprite (if LoadTargetFlag is 1). 82 | 83 | If the LoadTargetFlag is specified without the LoadVariablesFlag, the server is expected to respond with a MIME 84 | type of application/x-shockwave-flash and a body that consists of a SWF file. This response is used to load a 85 | subfile into the specified sprite rather than to display an HTML document. 86 | -------------------------------------------------------------------------------- /rs/src/streaming/tag.rs: -------------------------------------------------------------------------------- 1 | use crate::complete::tag::parse_tag_body; 2 | use nom::number::streaming::{le_u16 as parse_le_u16, le_u32 as parse_le_u32}; 3 | use nom::IResult as NomResult; 4 | use std::convert::TryFrom; 5 | use swf_types as ast; 6 | 7 | /// Represents an error caused by incomplete input. 8 | #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] 9 | pub(crate) enum StreamingTagError { 10 | /// Indicates that the input is not long enough to read the tag header. 11 | /// 12 | /// A header is `1`, `2` or `6` bytes long. 13 | /// Parsing with an input of 6 bytes or more guarantees that this error 14 | /// will not occur. 15 | IncompleteHeader, 16 | 17 | /// Indicates that the input is not long enough to read the full tag 18 | /// (header and body). 19 | /// 20 | /// The value indicates the full length of the tag. Parsing with an input at 21 | /// least (or exactly) this long guarantees that the tag will be parsed. 22 | IncompleteTag(usize), 23 | } 24 | 25 | /// Parses the tag at the start of the (possibly incomplete) input. 26 | /// 27 | /// In case of success, returns the remaining input and `Tag`. 28 | /// In case of error, returns the original input and error description. 29 | pub(crate) fn parse_tag(input: &[u8], swf_version: u8) -> Result<(&[u8], Option), StreamingTagError> { 30 | let base_input = input; // Keep original input to compute lengths. 31 | let (input, header) = match parse_tag_header(input) { 32 | Ok(ok) => ok, 33 | Err(_) => return Err(StreamingTagError::IncompleteHeader), 34 | }; 35 | 36 | // `EndOfTags` 37 | if header.code == 0 { 38 | return Ok((&[], None)); 39 | } 40 | 41 | let body_len = usize::try_from(header.length).unwrap(); 42 | if input.len() < body_len { 43 | let header_len = base_input.len() - input.len(); 44 | let tag_len = header_len + body_len; 45 | return Err(StreamingTagError::IncompleteTag(tag_len)); 46 | } 47 | let (tag_body, input) = input.split_at(body_len); 48 | let tag = parse_tag_body(tag_body, header.code, swf_version); 49 | Ok((input, Some(tag))) 50 | } 51 | 52 | pub(crate) fn parse_tag_header(input: &[u8]) -> NomResult<&[u8], ast::TagHeader> { 53 | // TODO: More test samples to pin down how Adobe's player handles this case (header starting with a NUL byte). 54 | // if input.len() > 0 && input[0] == 0 { 55 | // // If the first byte is `NUL`, treat it as an end-of-tags marker. 56 | // // This is not documented but seems to be how SWF works in practice 57 | // return Ok((&input[1..], ast::TagHeader { code: 0, length: 0})); 58 | // } 59 | 60 | let (input, code_and_length) = parse_le_u16(input)?; 61 | let code = code_and_length >> 6; 62 | let max_length = (1 << 6) - 1; 63 | let length = code_and_length & max_length; 64 | let (input, length) = if length < max_length { 65 | (input, u32::from(length)) 66 | } else { 67 | debug_assert_eq!(length, max_length); 68 | parse_le_u32(input)? 69 | }; 70 | 71 | Ok((input, ast::TagHeader { code, length })) 72 | } 73 | -------------------------------------------------------------------------------- /ts/src/test/movies.spec.mts: -------------------------------------------------------------------------------- 1 | import chai from "chai"; 2 | import fs from "fs"; 3 | import { JSON_READER } from "kryo-json/json-reader"; 4 | import { JSON_VALUE_WRITER } from "kryo-json/json-value-writer"; 5 | import sysPath from "path"; 6 | import { $Movie, Movie } from "swf-types/movie"; 7 | 8 | import { parseSwf } from "../lib/index.mjs"; 9 | import meta from "./meta.mjs"; 10 | import { readFile, readTextFile, writeTextFile } from "./utils.mjs"; 11 | 12 | const PROJECT_ROOT: string = sysPath.join(meta.dirname, ".."); 13 | const MOVIE_SAMPLES_ROOT: string = sysPath.join(PROJECT_ROOT, "..", "tests", "movies"); 14 | 15 | // `BLACKLIST` can be used to forcefully skip some tests. 16 | const BLACKLIST: ReadonlySet = new Set([ 17 | ]); 18 | // `WHITELIST` can be used to only enable a few tests. 19 | const WHITELIST: ReadonlySet = new Set([ 20 | // "hello-world", 21 | ]); 22 | 23 | describe("movies", function () { 24 | this.timeout(300000); // The timeout is this high due to CI being extremely slow 25 | 26 | for (const sample of getSamples()) { 27 | it(sample.name, async function () { 28 | const inputBytes: Buffer = await readFile(sample.moviePath); 29 | const actualMovie: Movie = parseSwf(inputBytes); 30 | const testErr: Error | undefined = $Movie.testError!(actualMovie); 31 | try { 32 | chai.assert.isUndefined(testErr, "InvalidMovie"); 33 | } catch (err) { 34 | console.error(testErr!.toString()); 35 | throw err; 36 | } 37 | const actualJson: string = JSON.stringify($Movie.write(JSON_VALUE_WRITER, actualMovie), null, 2); 38 | await writeTextFile(sysPath.join(MOVIE_SAMPLES_ROOT, sample.name, "local-ast.ts.json"), `${actualJson}\n`); 39 | const expectedJson: string = await readTextFile(sample.astPath); 40 | const expectedMovie: Movie = $Movie.read(JSON_READER, expectedJson); 41 | try { 42 | chai.assert.isTrue($Movie.equals(actualMovie, expectedMovie)); 43 | } catch (err) { 44 | chai.assert.strictEqual( 45 | actualJson, 46 | JSON.stringify($Movie.write(JSON_VALUE_WRITER, expectedMovie), null, 2), 47 | ); 48 | throw err; 49 | } 50 | }); 51 | } 52 | }); 53 | 54 | interface Sample { 55 | name: string; 56 | moviePath: string; 57 | astPath: string; 58 | } 59 | 60 | function* getSamples(): IterableIterator { 61 | for (const dirEnt of fs.readdirSync(MOVIE_SAMPLES_ROOT, {withFileTypes: true})) { 62 | if (!dirEnt.isDirectory()) { 63 | continue; 64 | } 65 | const testName: string = dirEnt.name; 66 | const testPath: string = sysPath.join(MOVIE_SAMPLES_ROOT, testName); 67 | 68 | if (BLACKLIST.has(testName)) { 69 | continue; 70 | } else if (WHITELIST.size > 0 && !WHITELIST.has(testName)) { 71 | continue; 72 | } 73 | 74 | const moviePath: string = sysPath.join(testPath, "main.swf"); 75 | const astPath: string = sysPath.join(testPath, "ast.json"); 76 | 77 | yield {name: testName, moviePath, astPath}; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /rs/src/complete/sound.rs: -------------------------------------------------------------------------------- 1 | use nom::number::complete::{le_u16 as parse_le_u16, le_u32 as parse_le_u32, le_u8 as parse_u8}; 2 | use nom::IResult as NomResult; 3 | use swf_types as swf; 4 | 5 | pub fn sound_rate_from_code(sound_rate_code: u8) -> Result { 6 | match sound_rate_code { 7 | 0 => Ok(swf::SoundRate::SoundRate5500), 8 | 1 => Ok(swf::SoundRate::SoundRate11000), 9 | 2 => Ok(swf::SoundRate::SoundRate22000), 10 | 3 => Ok(swf::SoundRate::SoundRate44000), 11 | _ => Err(()), 12 | } 13 | } 14 | 15 | pub fn audio_coding_format_from_code(audio_codec_code: u8) -> Result { 16 | match audio_codec_code { 17 | 0 => Ok(swf::AudioCodingFormat::UncompressedNativeEndian), 18 | 1 => Ok(swf::AudioCodingFormat::Adpcm), 19 | 2 => Ok(swf::AudioCodingFormat::Mp3), 20 | 3 => Ok(swf::AudioCodingFormat::UncompressedLittleEndian), 21 | 4 => Ok(swf::AudioCodingFormat::Nellymoser16), 22 | 5 => Ok(swf::AudioCodingFormat::Nellymoser8), 23 | 6 => Ok(swf::AudioCodingFormat::Nellymoser), 24 | 11 => Ok(swf::AudioCodingFormat::Speex), 25 | _ => Err(()), 26 | } 27 | } 28 | 29 | // TODO: Move this to a method on `swf::AudioCodingFormat` 30 | pub fn is_uncompressed_audio_coding_format(format: swf::AudioCodingFormat) -> bool { 31 | matches!(format, swf::AudioCodingFormat::UncompressedNativeEndian | swf::AudioCodingFormat::UncompressedLittleEndian) 32 | } 33 | 34 | pub fn parse_sound_info(input: &[u8]) -> NomResult<&[u8], swf::SoundInfo> { 35 | use nom::combinator::cond; 36 | use nom::multi::count; 37 | 38 | let (input, flags) = parse_u8(input)?; 39 | #[allow(clippy::identity_op)] 40 | let has_in_point = (flags & (1 << 0)) != 0; 41 | let has_out_point = (flags & (1 << 1)) != 0; 42 | let has_loops = (flags & (1 << 2)) != 0; 43 | let has_envelope = (flags & (1 << 3)) != 0; 44 | let sync_no_multiple = (flags & (1 << 4)) != 0; 45 | let sync_stop = (flags & (1 << 5)) != 0; 46 | // Bits [6, 7] are reserved 47 | let (input, in_point) = cond(has_in_point, parse_le_u32)(input)?; 48 | let (input, out_point) = cond(has_out_point, parse_le_u32)(input)?; 49 | let (input, loop_count) = cond(has_loops, parse_le_u16)(input)?; 50 | let (input, envelope_records) = if has_envelope { 51 | let (input, record_count) = parse_u8(input)?; 52 | let (input, envelope_records) = count(parse_sound_envelope, usize::from(record_count))(input)?; 53 | (input, Some(envelope_records)) 54 | } else { 55 | (input, None) 56 | }; 57 | 58 | Ok(( 59 | input, 60 | swf::SoundInfo { 61 | sync_stop, 62 | sync_no_multiple, 63 | in_point, 64 | out_point, 65 | loop_count, 66 | envelope_records, 67 | }, 68 | )) 69 | } 70 | 71 | pub fn parse_sound_envelope(input: &[u8]) -> NomResult<&[u8], swf::SoundEnvelope> { 72 | let (input, pos44) = parse_le_u32(input)?; 73 | let (input, left_level) = parse_le_u16(input)?; 74 | let (input, right_level) = parse_le_u16(input)?; 75 | Ok(( 76 | input, 77 | swf::SoundEnvelope { 78 | pos44, 79 | left_level, 80 | right_level, 81 | }, 82 | )) 83 | } 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Open Flash logo 4 | 5 | 6 | # SWF Parser 7 | 8 | [![GitHub repository](https://img.shields.io/badge/GitHub-open--flash%2Fswf--parser-informational.svg)](https://github.com/open-flash/swf-parser) 9 | 10 | SWF parser implemented in Rust and Typescript (Node and browser). 11 | Converts bytes to [`swf-types` movies][swf-types]. 12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 27 | 30 | 33 | 36 | 37 | 38 | 41 | 44 | 47 | 50 | 51 | 52 |
ImplementationPackageChecksDocumentation
25 | Rust 26 | 28 | crates.io crate 29 | 31 | Rust checks status 32 | 34 | docs.rs/swf-parser 35 |
39 | TypeScript 40 | 42 | npm package 43 | 45 | TypeScript checks status 46 | 48 | Source Code ¯\_(ツ)_/¯ 49 |
53 | 54 | This library is part of the [Open Flash][ofl] project. 55 | 56 | ## Usage 57 | 58 | - [Rust](./rs/README.md#usage) 59 | - [Typescript](./ts/README.md#usage) 60 | 61 | ## Goal 62 | 63 | The goal is to provide a complete SWF parser. The initial implementation 64 | requires the movie to be fully buffered before parsing but incremental 65 | parsing (for streams) is planned. 66 | This parser should be easily embeddable: it is intended for SWF players, 67 | analysis tools or any other project having to manipulate SWF files. 68 | 69 | ## Status 70 | 71 | Ready for use. 72 | 73 | The Rust and Typescript implementations are kept in sync. They both have 74 | complete support for SWF file format specification. 75 | Help is welcome to improve ergonomics and performance of the parser. 76 | 77 | ## Contributing 78 | 79 | Each implementation lives in its own directory (`rs` or `ts`). The commands 80 | must be executed from these "project roots", not from the "repo root". 81 | 82 | Check the implementation-specific guides: 83 | 84 | - [Rust](./rs/README.md#contributing) 85 | - [Typescript](./ts/README.md#contributing) 86 | 87 | You can also use the library and report any issues you encounter on the Github 88 | issues page. 89 | 90 | [ofl]: https://github.com/open-flash/open-flash 91 | [swf-types]: https://github.com/open-flash/swf-types 92 | -------------------------------------------------------------------------------- /ts/src/lib/parsers/sound.mts: -------------------------------------------------------------------------------- 1 | import { ReadableByteStream } from "@open-flash/stream"; 2 | import incident from "incident"; 3 | import { Uint2, Uint4, Uint8, Uint16, Uint32, UintSize } from "semantic-types"; 4 | import { AudioCodingFormat } from "swf-types/sound/audio-coding-format"; 5 | import { SoundEnvelope } from "swf-types/sound/sound-envelope"; 6 | import { SoundInfo } from "swf-types/sound/sound-info"; 7 | import { SoundRate } from "swf-types/sound/sound-rate"; 8 | 9 | export function getSoundRateFromCode(soundRateCode: Uint2): SoundRate { 10 | switch (soundRateCode) { 11 | case 0: 12 | return 5500; 13 | case 1: 14 | return 11000; 15 | case 2: 16 | return 22000; 17 | case 3: 18 | return 44000; 19 | default: 20 | throw new incident.Incident("UnexpectedSoundRateCode", {code: soundRateCode}); 21 | } 22 | } 23 | 24 | export function getAudioCodingFormatFromCode(formatCode: Uint4): AudioCodingFormat { 25 | switch (formatCode) { 26 | case 0: 27 | return AudioCodingFormat.UncompressedNativeEndian; 28 | case 1: 29 | return AudioCodingFormat.Adpcm; 30 | case 2: 31 | return AudioCodingFormat.Mp3; 32 | case 3: 33 | return AudioCodingFormat.UncompressedLittleEndian; 34 | case 4: 35 | return AudioCodingFormat.Nellymoser16; 36 | case 5: 37 | return AudioCodingFormat.Nellymoser8; 38 | case 6: 39 | return AudioCodingFormat.Nellymoser; 40 | case 11: 41 | return AudioCodingFormat.Speex; 42 | default: 43 | throw new incident.Incident("UnexpectedAudioCodingFormatCode", {code: formatCode}); 44 | } 45 | } 46 | 47 | export function isUncompressedAudioCodingFormat(format: AudioCodingFormat): boolean { 48 | return format === AudioCodingFormat.UncompressedNativeEndian || format === AudioCodingFormat.UncompressedLittleEndian; 49 | } 50 | 51 | export function parseSoundInfo(byteStream: ReadableByteStream): SoundInfo { 52 | const flags: Uint8 = byteStream.readUint8(); 53 | 54 | const hasInPoint: boolean = (flags & (1 << 0)) !== 0; 55 | const hasOutPoint: boolean = (flags & (1 << 1)) !== 0; 56 | const hasLoops: boolean = (flags & (1 << 2)) !== 0; 57 | const hasEnvelope: boolean = (flags & (1 << 3)) !== 0; 58 | const syncNoMultiple: boolean = (flags & (1 << 4)) !== 0; 59 | const syncStop: boolean = (flags & (1 << 5)) !== 0; 60 | // Bits [6, 7] are reserved 61 | 62 | const inPoint: Uint32 | undefined = hasInPoint ? byteStream.readUint32LE() : undefined; 63 | const outPoint: Uint32 | undefined = hasOutPoint ? byteStream.readUint32LE() : undefined; 64 | const loopCount: Uint16 | undefined = hasLoops ? byteStream.readUint16LE() : undefined; 65 | let envelopeRecords: SoundEnvelope[] | undefined; 66 | if (hasEnvelope) { 67 | envelopeRecords = []; 68 | const recordCount: UintSize = byteStream.readUint8(); 69 | for (let i: UintSize = 0; i < recordCount; i++) { 70 | envelopeRecords.push(parseSoundEnvelope(byteStream)); 71 | } 72 | } 73 | 74 | return {syncStop, syncNoMultiple, inPoint, outPoint, loopCount, envelopeRecords}; 75 | } 76 | 77 | export function parseSoundEnvelope(byteStream: ReadableByteStream): SoundEnvelope { 78 | const pos44: Uint32 = byteStream.readUint32LE(); 79 | const leftLevel: Uint16 = byteStream.readUint16LE(); 80 | const rightLevel: Uint16 = byteStream.readUint16LE(); 81 | return {pos44, leftLevel, rightLevel}; 82 | } 83 | -------------------------------------------------------------------------------- /rs/src/complete/gradient.rs: -------------------------------------------------------------------------------- 1 | use crate::streaming::basic_data_types::{parse_s_rgb8, parse_straight_s_rgba8}; 2 | use nom::number::complete::le_u8 as parse_u8; 3 | use nom::IResult as NomResult; 4 | use swf_types as swf; 5 | 6 | #[allow(unused_variables)] 7 | pub fn parse_color_stop(input: &[u8], with_alpha: bool) -> NomResult<&[u8], swf::ColorStop> { 8 | use nom::combinator::map; 9 | 10 | let (input, ratio) = parse_u8(input)?; 11 | let (input, color) = if with_alpha { 12 | parse_straight_s_rgba8(input)? 13 | } else { 14 | map(parse_s_rgb8, |c| swf::StraightSRgba8 { 15 | r: c.r, 16 | g: c.g, 17 | b: c.b, 18 | a: 255, 19 | })(input)? 20 | }; 21 | 22 | Ok((input, swf::ColorStop { ratio, color })) 23 | } 24 | 25 | pub fn parse_gradient(input: &[u8], with_alpha: bool) -> NomResult<&[u8], swf::Gradient> { 26 | let (input, flags) = parse_u8(input)?; 27 | let spread_code = flags >> 6; 28 | let color_space_code = (flags & ((1 << 6) - 1)) >> 4; 29 | let color_count = flags & ((1 << 4) - 1); 30 | 31 | let spread = match spread_code { 32 | 0 => swf::GradientSpread::Pad, 33 | 1 => swf::GradientSpread::Reflect, 34 | 2 => swf::GradientSpread::Repeat, 35 | _ => return Err(nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Switch))), 36 | }; 37 | 38 | let color_space = match color_space_code { 39 | 0 => swf::ColorSpace::SRgb, 40 | 1 => swf::ColorSpace::LinearRgb, 41 | _ => return Err(nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Switch))), 42 | }; 43 | 44 | let (input, colors) = nom::multi::count(|i| parse_color_stop(i, with_alpha), color_count as usize)(input)?; 45 | 46 | Ok(( 47 | input, 48 | swf::Gradient { 49 | spread, 50 | color_space, 51 | colors, 52 | }, 53 | )) 54 | } 55 | 56 | #[allow(unused_variables)] 57 | pub fn parse_morph_color_stop(input: &[u8], with_alpha: bool) -> NomResult<&[u8], swf::MorphColorStop> { 58 | let (input, start) = parse_color_stop(input, with_alpha)?; 59 | let (input, end) = parse_color_stop(input, with_alpha)?; 60 | 61 | Ok(( 62 | input, 63 | swf::MorphColorStop { 64 | ratio: start.ratio, 65 | color: start.color, 66 | morph_ratio: end.ratio, 67 | morph_color: end.color, 68 | }, 69 | )) 70 | } 71 | 72 | #[allow(unused_variables)] 73 | pub fn parse_morph_gradient(input: &[u8], with_alpha: bool) -> NomResult<&[u8], swf::MorphGradient> { 74 | let (input, flags) = parse_u8(input)?; 75 | let spread_code = flags >> 6; 76 | let color_space_code = (flags & ((1 << 6) - 1)) >> 4; 77 | let color_count = flags & ((1 << 4) - 1); 78 | 79 | let spread = match spread_code { 80 | 0 => swf::GradientSpread::Pad, 81 | 1 => swf::GradientSpread::Reflect, 82 | 2 => swf::GradientSpread::Repeat, 83 | _ => return Err(nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Switch))), 84 | }; 85 | 86 | let color_space = match color_space_code { 87 | 0 => swf::ColorSpace::SRgb, 88 | 1 => swf::ColorSpace::LinearRgb, 89 | _ => return Err(nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Switch))), 90 | }; 91 | 92 | let (input, colors) = nom::multi::count(|i| parse_morph_color_stop(i, with_alpha), color_count as usize)(input)?; 93 | 94 | Ok(( 95 | input, 96 | swf::MorphGradient { 97 | spread, 98 | color_space, 99 | colors, 100 | }, 101 | )) 102 | } 103 | -------------------------------------------------------------------------------- /docs/grammar.md: -------------------------------------------------------------------------------- 1 | Convention (not yet normalized everywhere): 2 | - `*string`: Repeat until end of sequence marker 3 | - `*block`: Repeat until end of stream 4 | - `*list`: Read count and repeat 5 | - `*count`: Number of items 6 | - `*bits`: Can be non-aligned 7 | 8 | The grammar is based on EBNF with a Prolog-like predicate logic. 9 | 10 | ```antlr 11 | matrix : matrixBits PADDING_BITS 12 | matrixBits : TODO 13 | rect : rectBits PADDING_BITS 14 | rectBits : UINT_BITS(5, n) SINT_BITS(n) SINT_BITS(n) SINT_BITS(n) SINT_BITS(n) 15 | sRgb8 : UINT8 UINT8 UINT8 16 | 17 | movie : header tagString 18 | header : signature rect UFIXED8U8 UINT16_LE 19 | signature : compressionMethod UINT8 UINT32 20 | compressionMethod : [ '\x46' | '\5a' | '\x43' ] '\x57' '\x53' 21 | tagString : tag * endOfTags 22 | endOfTags : '\x00' 23 | tag : 24 | tagHeader(1) tags_showFrame 25 | | tagHeader(2) tags_defineShape 26 | | tagHeader(9) tags_setBackgroundColor 27 | | tagHeader(11) tags_defineText 28 | | tagHeader(84) tags_defineMorphShape2 29 | | TODO 30 | tagHeader(code) : TODO; 31 | tags_showFrame : ε 32 | tags_defineShape : UINT16_LE rect shape 33 | shape : shapeBits PADDING_BITS 34 | shapeBits : shapeStylesBits(fillBits, lineBits) shapeRecordStringBits(fillBits, lineBits) 35 | shapeStylesBits(fillBits, lineBits) : PADDING_BITS fillStyleList lineStyleList UINT_BITS(4, fillBits) UINT_BITS(4, lineBits) 36 | fillStyleList : listLength fillStyle * 37 | listLength : [ '\x00'..'\xfe' ] | '\xff' UINT16_LE 38 | fillStyle : 39 | '\x00' solidFillStyle 40 | | TODO 41 | solidFillStyle : sRgb8 42 | lineStyleList : listLength lineStyle * 43 | lineStyle : UINT16_LE sRgb8 // TODO; 44 | shapeRecordStringBits(fillBits, lineBits) : 45 | UINT_BITS(6, 0) 46 | | BOOL_BITS(⊥) styleChangeBits(fillBits, lineBits, nextFillBits, nextLineBits) shapeRecordStringBits(nextFillBits, nextLineBits) 47 | | BOOL_BITS(⊤) [ BOOL_BITS(⊥) curvedEdgeBits | BOOL_BITS(⊤) straightEdgeBits ] shapeRecordStringBits(fillBits, lineBits) 48 | styleChangeBits(fillBits, lineBits, nextFillBits, nextLineBits) : BOOL_BITS(hasNewStyles) BOOL_BITS(changeLineStyle) BOOL_BITS(changeRightFill) BOOL_BITS(changeLeftFill) BOOL_BITS(hasMoveTo) styleChangeMoveToBits(hasMoveTo) styleIndexBits(changeLeftFill, fillBits) styleIndexBits(changeRightFill, fillBits) styleIndexBits(changeLineStyle, lineBits) newStylesBits(hasNewStyles, fillBits, lineBits, nextFillBits, nextLineBits) 49 | styleChangeMoveToBits(⊥) : ε 50 | styleChangeMoveToBits(⊤) : UINT_BITS(5, n) SINT_BITS(n) SINT_BITS(n) 51 | styleIndexBits(⊥, _) : ε 52 | styleIndexBits(⊤, n) : UINT_BITS(n) 53 | newStylesBits(⊥, fillBits, lineBits, fillBits, lineBits) : ε 54 | newStylesBits(⊤, _, _, nextFillBits, nextLineBits) : shapeStylesBits(nextFillBits, nextLineBits) 55 | curvedEdgeBits : UINT_BITS(4, n) SINT_BITS(n + 2) SINT_BITS(n + 2) SINT_BITS(n + 2) SINT_BITS(n + 2) 56 | straightEdgeBits : UINT_BITS(4, n) [ BOOL_BITS(⊤) SINT_BITS(n + 2) | BOOL_BITS(⊥) BOOL_BITS ] SINT_BITS(n + 2) 57 | tags_setBackgroundColor : sRgb8 58 | tags_defineText : UINT16_LE rect matrix UINT8(indexBits) UINT8(advanceBits) textRecordStringBits(indexBits, advanceBits) 59 | textRecordStringBits(indexBits, advanceBits) : 60 | '\x00' 61 | | textRecordBits(indexBits, advanceBits) textRecordStringBits(indexBits, advanceBits) 62 | textRecordBits(indexBits, advanceBits) : BOOL_BITS(hasFont) BOOL_BITS(hasColor) BOOL_BITS(hasOffsetX) BOOL_BITS(hasOffsetY) textRecordFontId(hasFont) TODO 63 | textRecordFontId(⊥) : ε 64 | textRecordFontId(⊤) : UINT16_LE 65 | 66 | tags_defineShape : UINT16_LE rect rect rect rect UINT8 UINT32_LE 67 | 68 | 69 | ``` 70 | -------------------------------------------------------------------------------- /docs/tags/define-morph-shape.md: -------------------------------------------------------------------------------- 1 | # DefineMorphShape 2 | 3 | - Tag Code: `46 = 0x2e` 4 | - SWF version: `3` 5 | 6 | ## Original documentation 7 | 8 | ### DefineMorphShape 9 | 10 | The DefineMorphShape tag defines the start and end states of a morph sequence. A morph object should be 11 | displayed with the PlaceObject2 tag, where the ratio field specifies how far the morph has progressed. 12 | 13 | The minimum file format version is SWF 3. 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 55 | 56 | 57 | 58 | 59 | 65 | 66 | 67 | 68 | 69 | 74 | 75 | 76 | 77 | 78 | 82 | 83 |
FieldTypeComment
HeaderRECORDHEADERTag type = 46
CharacterIdUI16ID for this character
StartBoundsRECTBounds of the start shape
EndBoundsRECTBounds of the end shape
OffsetUI32Indicates offset to EndEdges
MorphFillStylesMORPHFILLSTYLEARRAY 50 | Fill style information is stored in the same manner as for a 51 | standard shape; however, each fill consists of interleaved 52 | information based on a single style type to accommodate 53 | morphing. 54 |
MorphLineStylesMORPHLINESTYLEARRAY 60 | Line style information is stored in the same manner as for a 61 | standard shape; however, each line consists of interleaved 62 | information based on a single style type to accommodate 63 | morphing. 64 |
StartEdgesSHAPE 70 | Contains the set of edges and the style bits that indicate style 71 | changes (for example, MoveTo, FillStyle, and LineStyle). Number 72 | of edges must equal the number of edges in EndEdges. 73 |
EndEdgesSHAPE 79 | Contains only the set of edges, with no style information. 80 | Number of edges must equal the number of edges in StartEdges. 81 |
84 | 85 | - StartBounds This defines the bounding-box of the shape at the start of the morph. 86 | - EndBounds - This defines the bounding-box at the end of the morph. 87 | - MorphFillStyles This contains an array of interleaved fill styles for the start and end shapes. The fill style 88 | for the start shape is followed by the corresponding fill style for the end shape. 89 | - MorphLineStyles - This contains an array of interleaved line styles. 90 | - StartEdges - This array specifies the edges for the start shape, and the style change records for both 91 | shapes. Because the StyleChangeRecords must be the same for the start and end shapes, they are 92 | defined only in the StartEdges array. 93 | - EndEdges - This array specifies the edges for the end shape, and contains no style change records. The 94 | number of edges specified in StartEdges must equal the number of edges in EndEdges. 95 | 96 | Strictly speaking, MoveTo records fall into the category of StyleChangeRecords; however, they should be 97 | included in both the StartEdges and EndEdges arrays. 98 | 99 | It is possible for an edge to change type over the course of a morph sequence. A straight edge can become a 100 | curved edge and vice versa. In this case, think of both edges as curved. A straight edge can be easily represented 101 | as a curve, by placing the off-curve (control) point at the midpoint of the straight edge, and the on-curve 102 | (anchor) point at the end of the straight edge. The calculation is as follows: 103 | 104 | ``` 105 | CurveControlDelta.x = StraightDelta.x / 2; 106 | CurveControlDelta.y = StraightDelta.y / 2; 107 | CurveAnchorDelta.x = StraightDelta.x / 2; 108 | CurveAnchorDelta.y = StraightDelta.y / 2; 109 | ``` 110 | -------------------------------------------------------------------------------- /ts/src/lib/parsers/gradient.mts: -------------------------------------------------------------------------------- 1 | import { ReadableByteStream } from "@open-flash/stream"; 2 | import { Uint2, Uint4, Uint8 } from "semantic-types"; 3 | import { ColorSpace } from "swf-types/color-space"; 4 | import { ColorStop } from "swf-types/color-stop"; 5 | import { GradientSpread } from "swf-types/gradient-spread"; 6 | import { Gradient } from "swf-types/gradient"; 7 | import { MorphColorStop } from "swf-types/morph-color-stop"; 8 | import { MorphGradient } from "swf-types/morph-gradient"; 9 | import { StraightSRgba8 } from "swf-types/straight-s-rgba8"; 10 | 11 | import { parseSRgb8, parseStraightSRgba8 } from "./basic-data-types.mjs"; 12 | 13 | export function parseColorStop(byteStream: ReadableByteStream, withAlpha: boolean): ColorStop { 14 | const ratio: Uint8 = byteStream.readUint8(); 15 | let color: StraightSRgba8; 16 | if (withAlpha) { 17 | color = parseStraightSRgba8(byteStream); 18 | } else { 19 | color = {...parseSRgb8(byteStream), a: 255}; 20 | } 21 | return {ratio, color}; 22 | } 23 | 24 | export function parseGradient(byteStream: ReadableByteStream, withAlpha: boolean): Gradient { 25 | const flags: Uint8 = byteStream.readUint8(); 26 | // The spec says that spreadId and colorSpaceId should be ignored for shapeVersion < Shape4 27 | // and color count should be capped at 8. We're ignoring it for the moment. 28 | const spreadId: Uint2 = ((flags & ((1 << 8) - 1)) >>> 6) as Uint2; 29 | const colorSpaceId: Uint2 = ((flags & ((1 << 6) - 1)) >>> 4) as Uint2; 30 | const colorCount: Uint4 = (flags & ((1 << 4) - 1)) as Uint4; 31 | let spread: GradientSpread; 32 | switch (spreadId) { 33 | case 0: 34 | spread = GradientSpread.Pad; 35 | break; 36 | case 1: 37 | spread = GradientSpread.Reflect; 38 | break; 39 | case 2: 40 | spread = GradientSpread.Repeat; 41 | break; 42 | default: 43 | throw new Error("Unexpected gradient spread"); 44 | } 45 | let colorSpace: ColorSpace; 46 | switch (colorSpaceId) { 47 | case 0: 48 | colorSpace = ColorSpace.SRgb; 49 | break; 50 | case 1: 51 | colorSpace = ColorSpace.LinearRgb; 52 | break; 53 | default: 54 | throw new Error("Unexpected gradient spread"); 55 | } 56 | const colors: ColorStop[] = []; 57 | for (let i: number = 0; i < colorCount; i++) { 58 | colors.push(parseColorStop(byteStream, withAlpha)); 59 | } 60 | return { 61 | spread, 62 | colorSpace, 63 | colors, 64 | }; 65 | } 66 | 67 | export function parseMorphColorStop(byteStream: ReadableByteStream, withAlpha: boolean): MorphColorStop { 68 | const {ratio, color} = parseColorStop(byteStream, withAlpha); 69 | const {ratio: morphRatio, color: morphColor} = parseColorStop(byteStream, withAlpha); 70 | return {ratio, color, morphRatio, morphColor}; 71 | } 72 | 73 | export function parseMorphGradient(byteStream: ReadableByteStream, withAlpha: boolean): MorphGradient { 74 | const flags: Uint8 = byteStream.readUint8(); 75 | const spreadId: Uint2 = ((flags & ((1 << 8) - 1)) >>> 6) as Uint2; 76 | const colorSpaceId: Uint2 = ((flags & ((1 << 6) - 1)) >>> 4) as Uint2; 77 | const colorCount: Uint4 = (flags & ((1 << 4) - 1)) as Uint4; 78 | let spread: GradientSpread; 79 | switch (spreadId) { 80 | case 0: 81 | spread = GradientSpread.Pad; 82 | break; 83 | case 1: 84 | spread = GradientSpread.Reflect; 85 | break; 86 | case 2: 87 | spread = GradientSpread.Repeat; 88 | break; 89 | default: 90 | throw new Error("Unexpected gradient spread"); 91 | } 92 | let colorSpace: ColorSpace; 93 | switch (colorSpaceId) { 94 | case 0: 95 | colorSpace = ColorSpace.SRgb; 96 | break; 97 | case 1: 98 | colorSpace = ColorSpace.LinearRgb; 99 | break; 100 | default: 101 | throw new Error("Unexpected gradient spread"); 102 | } 103 | const colors: MorphColorStop[] = []; 104 | for (let i: number = 0; i < colorCount; i++) { 105 | colors.push(parseMorphColorStop(byteStream, withAlpha)); 106 | } 107 | return { 108 | spread, 109 | colorSpace, 110 | colors, 111 | }; 112 | } 113 | -------------------------------------------------------------------------------- /ts/src/test/tags.spec.mts: -------------------------------------------------------------------------------- 1 | import { ReadableStream, ReadableByteStream } from "@open-flash/stream"; 2 | import chai from "chai"; 3 | import fs from "fs"; 4 | import { IoType } from "kryo"; 5 | import { JSON_READER } from "kryo-json/json-reader"; 6 | import { JSON_VALUE_WRITER } from "kryo-json/json-value-writer"; 7 | import sysPath from "path"; 8 | import { $Tag,Tag } from "swf-types/tag"; 9 | 10 | import { parseTag } from "../lib/parsers/tags.mjs"; 11 | import meta from "./meta.mjs"; 12 | import { readFile, readTextFile, writeTextFile } from "./utils.mjs"; 13 | 14 | const PROJECT_ROOT: string = sysPath.join(meta.dirname, ".."); 15 | const TAG_SAMPLES_ROOT: string = sysPath.join(PROJECT_ROOT, "..", "tests", "tags"); 16 | 17 | // `BLACKLIST` can be used to forcefully skip some tests. 18 | const BLACKLIST: ReadonlySet = new Set([ 19 | // "define-shape/shape1-squares", 20 | // "raw-body/invalid-define-font-offset", 21 | // "raw-body/non-utf8-string", 22 | ]); 23 | // `WHITELIST` can be used to only enable a few tests. 24 | const WHITELIST: ReadonlySet = new Set([ 25 | // "place-object/po2-place-id-1", 26 | // "place-object/po3-update-depth-1", 27 | ]); 28 | 29 | describe("tags", function () { 30 | for (const group of getSampleGroups()) { 31 | describe(group.name, function () { 32 | for (const sample of getSamplesFromGroup(group)) { 33 | it(sample.name, async function () { 34 | const inputBytes: Uint8Array = await readFile(sample.inputPath); 35 | const s: ReadableByteStream = new ReadableStream(inputBytes); 36 | const actualValue: Tag = sample.parser(s); 37 | const actualJson: string = `${JSON.stringify(group.type.write(JSON_VALUE_WRITER, actualValue), null, 2)}\n`; 38 | 39 | await writeTextFile(sample.actualPath, actualJson); 40 | 41 | chai.assert.isUndefined(group.type.testError!(actualValue)); 42 | 43 | const expectedJson: string = await readTextFile(sample.valuePath); 44 | const expectedValue: Tag = group.type.read(JSON_READER, expectedJson); 45 | 46 | try { 47 | chai.assert.isTrue(group.type.equals(actualValue, expectedValue)); 48 | } catch (err) { 49 | chai.assert.strictEqual(actualJson, expectedJson); 50 | throw err; 51 | } 52 | }); 53 | } 54 | }); 55 | } 56 | }); 57 | 58 | interface SampleGroup { 59 | name: string; 60 | type: IoType; 61 | } 62 | 63 | function* getSampleGroups(): IterableIterator { 64 | for (const dirEnt of fs.readdirSync(TAG_SAMPLES_ROOT, {withFileTypes: true})) { 65 | if (!dirEnt.isDirectory()) { 66 | continue; 67 | } 68 | const name: string = dirEnt.name; 69 | yield { 70 | name, 71 | type: $Tag, 72 | }; 73 | } 74 | } 75 | 76 | interface Sample { 77 | name: string; 78 | inputPath: string; 79 | actualPath: string; 80 | valuePath: string; 81 | 82 | parser(byteStream: ReadableByteStream): Tag; 83 | } 84 | 85 | function* getSamplesFromGroup(group: SampleGroup): IterableIterator { 86 | const groupPath: string = sysPath.join(TAG_SAMPLES_ROOT, group.name); 87 | for (const dirEnt of fs.readdirSync(groupPath, {withFileTypes: true})) { 88 | if (!dirEnt.isDirectory()) { 89 | continue; 90 | } 91 | const testName: string = dirEnt.name; 92 | const testPath: string = sysPath.join(groupPath, testName); 93 | const fullName: string = `${group.name}/${testName}`; 94 | 95 | if (BLACKLIST.has(fullName)) { 96 | continue; 97 | } else if (WHITELIST.size > 0 && !WHITELIST.has(fullName)) { 98 | continue; 99 | } 100 | 101 | const inputPath: string = sysPath.join(testPath, "input.bytes"); 102 | const actualPath: string = sysPath.join(testPath, "local-value.ts.json"); 103 | const valuePath: string = sysPath.join(testPath, "value.json"); 104 | 105 | let swfVersion: number; 106 | switch (fullName) { 107 | case "place-object/po2-swf5": 108 | swfVersion = 5; 109 | break; 110 | default: 111 | swfVersion = 10; 112 | break; 113 | } 114 | 115 | yield { 116 | name: testName, 117 | inputPath, 118 | actualPath, 119 | valuePath, 120 | parser: (stream: ReadableByteStream) => parseTag(stream, swfVersion)!, 121 | }; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /rs/fuzz/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "adler32" 7 | version = "1.2.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" 10 | 11 | [[package]] 12 | name = "arbitrary" 13 | version = "1.1.0" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "c38b6b6b79f671c25e1a3e785b7b82d7562ffc9cd3efdc98627e5668a2472490" 16 | 17 | [[package]] 18 | name = "build_const" 19 | version = "0.2.1" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "39092a32794787acd8525ee150305ff051b0aa6cc2abaf193924f5ab05425f39" 22 | 23 | [[package]] 24 | name = "byteorder" 25 | version = "1.3.4" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" 28 | 29 | [[package]] 30 | name = "cc" 31 | version = "1.0.60" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "ef611cc68ff783f18535d77ddd080185275713d852c4f5cbb6122c462a7a825c" 34 | 35 | [[package]] 36 | name = "crc" 37 | version = "1.8.1" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "d663548de7f5cca343f1e0a48d14dcfb0e9eb4e079ec58883b7251539fa10aeb" 40 | dependencies = [ 41 | "build_const", 42 | ] 43 | 44 | [[package]] 45 | name = "half" 46 | version = "1.8.2" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" 49 | 50 | [[package]] 51 | name = "inflate" 52 | version = "0.4.5" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "1cdb29978cc5797bd8dcc8e5bf7de604891df2a8dc576973d71a281e916db2ff" 55 | dependencies = [ 56 | "adler32", 57 | ] 58 | 59 | [[package]] 60 | name = "libfuzzer-sys" 61 | version = "0.4.3" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "336244aaeab6a12df46480dc585802aa743a72d66b11937844c61bbca84c991d" 64 | dependencies = [ 65 | "arbitrary", 66 | "cc", 67 | "once_cell", 68 | ] 69 | 70 | [[package]] 71 | name = "lzma-rs" 72 | version = "0.2.0" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "aba8ecb0450dfabce4ad72085eed0a75dffe8f21f7ada05638564ea9db2d7fb1" 75 | dependencies = [ 76 | "byteorder", 77 | "crc", 78 | ] 79 | 80 | [[package]] 81 | name = "memchr" 82 | version = "2.5.0" 83 | source = "registry+https://github.com/rust-lang/crates.io-index" 84 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 85 | 86 | [[package]] 87 | name = "minimal-lexical" 88 | version = "0.2.1" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 91 | 92 | [[package]] 93 | name = "nom" 94 | version = "7.1.1" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" 97 | dependencies = [ 98 | "memchr", 99 | "minimal-lexical", 100 | ] 101 | 102 | [[package]] 103 | name = "once_cell" 104 | version = "1.10.0" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9" 107 | 108 | [[package]] 109 | name = "serde" 110 | version = "1.0.116" 111 | source = "registry+https://github.com/rust-lang/crates.io-index" 112 | checksum = "96fe57af81d28386a513cbc6858332abc6117cfdb5999647c6444b8f43a370a5" 113 | 114 | [[package]] 115 | name = "swf-fixed" 116 | version = "0.1.5" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | checksum = "6b212c20df50b382c442a4098d7d9f1c94f0b040f0f0a5d120fa3a82fa51e302" 119 | dependencies = [ 120 | "serde", 121 | ] 122 | 123 | [[package]] 124 | name = "swf-parser" 125 | version = "0.14.0" 126 | dependencies = [ 127 | "half", 128 | "inflate", 129 | "lzma-rs", 130 | "memchr", 131 | "nom", 132 | "swf-fixed", 133 | "swf-types", 134 | ] 135 | 136 | [[package]] 137 | name = "swf-parser-fuzz" 138 | version = "0.0.0" 139 | dependencies = [ 140 | "libfuzzer-sys", 141 | "swf-parser", 142 | ] 143 | 144 | [[package]] 145 | name = "swf-types" 146 | version = "0.14.0" 147 | source = "registry+https://github.com/rust-lang/crates.io-index" 148 | checksum = "6504ca71457976a3b3f42174a5f2a03798a7c326276816b4037f482245c35d94" 149 | dependencies = [ 150 | "swf-fixed", 151 | ] 152 | -------------------------------------------------------------------------------- /ts/src/test/various.spec.mts: -------------------------------------------------------------------------------- 1 | import { ReadableStream, ReadableByteStream } from "@open-flash/stream"; 2 | import chai from "chai"; 3 | import fs from "fs"; 4 | import { IoType } from "kryo"; 5 | import { JSON_READER } from "kryo-json/json-reader"; 6 | import { JSON_VALUE_WRITER } from "kryo-json/json-value-writer"; 7 | import { Float64Type } from "kryo/float64"; 8 | import { $Uint32 } from "kryo/integer"; 9 | import sysPath from "path"; 10 | import { $ColorTransformWithAlpha } from "swf-types/color-transform-with-alpha"; 11 | import { $Header } from "swf-types/header"; 12 | import { $Matrix } from "swf-types/matrix"; 13 | import { $Rect } from "swf-types/rect"; 14 | import { $SwfSignature } from "swf-types/swf-signature"; 15 | 16 | import { parseColorTransformWithAlpha, parseMatrix, parseRect } from "../lib/parsers/basic-data-types.mjs"; 17 | import { parseHeader, parseSwfSignature } from "../lib/parsers/header.mjs"; 18 | import meta from "./meta.mjs"; 19 | import { readFile, readTextFile } from "./utils.mjs"; 20 | 21 | const PROJECT_ROOT: string = sysPath.join(meta.dirname, ".."); 22 | const SAMPLES_ROOT: string = sysPath.join(PROJECT_ROOT, "..", "tests", "various"); 23 | 24 | for (const group of getSampleGroups()) { 25 | describe(group.name, function () { 26 | for (const sample of getSamplesFromGroup(group.name)) { 27 | it(sample.name, async function () { 28 | const inputBytes: Uint8Array = await readFile(sample.inputPath); 29 | const s: ReadableByteStream = new ReadableStream(inputBytes); 30 | const actualValue: any = group.parser(s); 31 | const actualJson: string = `${JSON.stringify(group.type.write(JSON_VALUE_WRITER, actualValue), null, 2)}\n`; 32 | 33 | // await writeTextFile(sample.valuePath, actualJson); 34 | 35 | chai.assert.isUndefined(group.type.testError!(actualValue)); 36 | 37 | const expectedJson: string = await readTextFile(sample.valuePath); 38 | const expectedValue: any = group.type.read(JSON_READER, expectedJson); 39 | 40 | try { 41 | chai.assert.isTrue(group.type.equals(actualValue, expectedValue)); 42 | } catch (err) { 43 | chai.assert.strictEqual(actualJson, expectedJson); 44 | throw err; 45 | } 46 | }); 47 | } 48 | }); 49 | } 50 | 51 | interface SampleGroup { 52 | name: string; 53 | type: IoType; 54 | 55 | parser(byteStream: ReadableByteStream): T; 56 | } 57 | 58 | function* getSampleGroups(): IterableIterator> { 59 | for (const dirEnt of fs.readdirSync(SAMPLES_ROOT, {withFileTypes: true})) { 60 | if (!dirEnt.isDirectory()) { 61 | continue; 62 | } 63 | const name: string = dirEnt.name; 64 | switch (name) { 65 | case "color-transform-with-alpha": { 66 | yield {name, parser: parseColorTransformWithAlpha, type: $ColorTransformWithAlpha}; 67 | break; 68 | } 69 | case "float16-le": { 70 | yield { 71 | name, 72 | parser: (stream: ReadableByteStream) => stream.readFloat16LE(), 73 | type: new Float64Type(), 74 | }; 75 | break; 76 | } 77 | case "header": { 78 | yield {name, parser: (stream: ReadableByteStream) => parseHeader(stream, 34), type: $Header}; 79 | break; 80 | } 81 | case "matrix": { 82 | yield {name, parser: parseMatrix, type: $Matrix}; 83 | break; 84 | } 85 | case "rect": { 86 | yield {name, parser: parseRect, type: $Rect}; 87 | break; 88 | } 89 | case "swf-signature": { 90 | yield {name, parser: parseSwfSignature, type: $SwfSignature}; 91 | break; 92 | } 93 | case "uint32-leb128": { 94 | yield { 95 | name, 96 | parser: (stream: ReadableByteStream) => stream.readUint32Leb128(), 97 | type: $Uint32, 98 | }; 99 | break; 100 | } 101 | default: 102 | throw new Error(`Unknown sample group: ${name}`); 103 | } 104 | } 105 | } 106 | 107 | interface Sample { 108 | name: string; 109 | inputPath: string; 110 | valuePath: string; 111 | } 112 | 113 | function* getSamplesFromGroup(group: string): IterableIterator { 114 | const groupPath: string = sysPath.join(SAMPLES_ROOT, group); 115 | for (const dirEnt of fs.readdirSync(groupPath, {withFileTypes: true})) { 116 | if (!dirEnt.isDirectory()) { 117 | continue; 118 | } 119 | const testName: string = dirEnt.name; 120 | const testPath: string = sysPath.join(groupPath, testName); 121 | 122 | const inputPath: string = sysPath.join(testPath, "input.bytes"); 123 | const valuePath: string = sysPath.join(testPath, "value.json"); 124 | 125 | yield {name: testName, inputPath, valuePath}; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /rs/src/complete/movie.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use crate::complete::parse_tag; 4 | use crate::streaming::movie::parse_swf_signature; 5 | use crate::streaming::decompress; 6 | use ast::CompressionMethod; 7 | use nom::IResult as NomResult; 8 | use swf_types as ast; 9 | 10 | /// Represents the possible parse errors when parsing an SWF file. 11 | /// 12 | /// Fatal errors can only occur at the beginning of the parsing. Once the header 13 | /// is parsed, the tags are always parsed successfully. Invalid tags produce 14 | /// `Raw` tags but don't prevent the parser from completing: the parser is 15 | /// resilient to invalid (or unknown) tags. 16 | #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] 17 | pub enum SwfParseError { 18 | /// Indicates an invalid SWF signature. 19 | /// 20 | /// The SWF signature corresponds to the first 8 bytes of the movie. 21 | /// This error occurs either if there is not enough data to even parse 22 | /// the signature or if the compression method is invalid. 23 | InvalidSignature, 24 | 25 | /// Indicates that the compression method used by the payload isn't supported. 26 | /// 27 | /// This can only happen when the corresponding Cargo feature is disabled. 28 | UnsupportedCompression(CompressionMethod), 29 | 30 | /// Indicates a failure to decompress the payload. 31 | /// 32 | /// The payload represents all the data following the SWF signature. 33 | /// If the SWF file uses a compressed payload (`Deflate` or `Lzma`), this 34 | /// error is emitted when the decompression fails for any reason. 35 | InvalidPayload, 36 | 37 | /// Indicates an invalid movie header. 38 | /// 39 | /// The movie header corresponds to the first few bytes of the payload. 40 | /// This error occurs if there is not enough data to parse the header. 41 | InvalidHeader, 42 | } 43 | 44 | impl std::error::Error for SwfParseError {} 45 | 46 | impl fmt::Display for SwfParseError { 47 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 48 | match self { 49 | SwfParseError::InvalidSignature => f.write_str("invalid SWF signature"), 50 | SwfParseError::UnsupportedCompression(comp) => { 51 | f.write_str("unsupported SWF compression: ")?; 52 | fmt::Debug::fmt(comp, f) 53 | } 54 | SwfParseError::InvalidPayload => f.write_str("invalid SWF payload"), 55 | SwfParseError::InvalidHeader => f.write_str("invalid SWF header"), 56 | } 57 | } 58 | } 59 | 60 | /// Parses a completely loaded SWF file. 61 | /// 62 | /// See [[SwfParseError]] for details on the possible errors. 63 | /// 64 | /// This function never panics. 65 | pub fn parse_swf(input: &[u8]) -> Result { 66 | let (input, signature) = match parse_swf_signature(input) { 67 | Ok(ok) => ok, 68 | Err(_) => return Err(SwfParseError::InvalidSignature), 69 | }; 70 | 71 | let result = match signature.compression_method { 72 | CompressionMethod::None => decompress::decompress_none(input), 73 | #[cfg(feature="deflate")] 74 | CompressionMethod::Deflate => decompress::decompress_zlib(input), 75 | #[cfg(feature="lzma")] 76 | CompressionMethod::Lzma => decompress::decompress_lzma(input), 77 | #[allow(unreachable_patterns)] 78 | method => return Err(SwfParseError::UnsupportedCompression(method)), 79 | }; 80 | 81 | let (_input, payload) = result.map_err(|_| SwfParseError::InvalidPayload)?; 82 | 83 | // TODO: should we check that the input was fully consumed? 84 | // TODO: check decompressed payload length against signature? 85 | 86 | match parse_movie(&payload, signature.swf_version) { 87 | Ok((_, movie)) => Ok(movie), 88 | Err(_) => Err(SwfParseError::InvalidHeader), 89 | } 90 | } 91 | 92 | /// Parses a completely loaded movie. 93 | /// 94 | /// The movie is the uncompressed payload of the SWF. 95 | fn parse_movie(input: &[u8], swf_version: u8) -> NomResult<&[u8], ast::Movie> { 96 | let (input, header) = parse_header(input, swf_version)?; 97 | let tags = parse_tag_block_string(input, swf_version); 98 | 99 | Ok((&[][..], ast::Movie { header, tags })) 100 | } 101 | 102 | /// Parses the movie header from a completely loaded input. 103 | fn parse_header(input: &[u8], swf_version: u8) -> NomResult<&[u8], ast::Header> { 104 | match crate::streaming::movie::parse_header(input, swf_version) { 105 | Ok(ok) => Ok(ok), 106 | Err(nom::Err::Incomplete(_)) => Err(nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Complete))), 107 | Err(e) => Err(e), 108 | } 109 | } 110 | 111 | /// Parses the string of tags from a completely loaded input. 112 | pub(crate) fn parse_tag_block_string(mut input: &[u8], swf_version: u8) -> Vec { 113 | let mut tags: Vec = Vec::new(); 114 | loop { 115 | input = match parse_tag(input, swf_version) { 116 | (input, Some(tag)) => { 117 | tags.push(tag); 118 | input 119 | } 120 | (_, None) => return tags, 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /rs/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod complete; 2 | mod stream_buffer; 3 | pub mod streaming; 4 | 5 | pub use swf_types; 6 | 7 | pub use complete::tag::parse_tag; 8 | pub use complete::{parse_swf, SwfParseError}; 9 | 10 | #[cfg(test)] 11 | mod tests { 12 | use crate::parse_swf; 13 | use ::swf_types::Movie; 14 | use ::test_generator::test_resources; 15 | use nom::IResult as NomResult; 16 | use std::io::Write; 17 | use std::path::Path; 18 | 19 | #[test_resources("../tests/movies/*/")] 20 | fn test_parse_movie(path: &str) { 21 | use serde::Serialize; 22 | 23 | let path: &Path = Path::new(path); 24 | let _name = path 25 | .components() 26 | .last() 27 | .unwrap() 28 | .as_os_str() 29 | .to_str() 30 | .expect("Failed to retrieve sample name"); 31 | let movie_path = path.join("main.swf"); 32 | let movie_bytes: Vec = ::std::fs::read(movie_path).expect("Failed to read movie"); 33 | 34 | let actual_movie = parse_swf(&movie_bytes).expect("Failed to parse movie"); 35 | 36 | let actual_ast_path = path.join("local-ast.rs.json"); 37 | let actual_ast_file = ::std::fs::File::create(actual_ast_path).expect("Failed to create actual AST file"); 38 | let actual_ast_writer = ::std::io::BufWriter::new(actual_ast_file); 39 | 40 | let mut ser = serde_json_v8::Serializer::pretty(actual_ast_writer); 41 | actual_movie.serialize(&mut ser).expect("Failed to write actual AST"); 42 | ser.into_inner().write_all(b"\n").unwrap(); 43 | 44 | // assert_eq!(remaining_input, &[] as &[u8]); 45 | 46 | let ast_path = path.join("ast.json"); 47 | let ast_file = ::std::fs::File::open(ast_path).expect("Failed to open AST"); 48 | let ast_reader = ::std::io::BufReader::new(ast_file); 49 | let expected_movie = serde_json_v8::from_reader::<_, Movie>(ast_reader).expect("Failed to read AST"); 50 | 51 | assert_eq!(actual_movie, expected_movie); 52 | } 53 | 54 | macro_rules! test_various_parser_impl_any { 55 | ($(#[$meta:meta])* $name:ident<$type:ty>, $parser:path, $check: expr $(,)?) => { 56 | $(#[$meta])* 57 | fn $name(path: &str) { 58 | let path: &Path = Path::new(path); 59 | let _name = path 60 | .components() 61 | .last() 62 | .unwrap() 63 | .as_os_str() 64 | .to_str() 65 | .expect("Failed to retrieve sample name"); 66 | let input_path = path.join("input.bytes"); 67 | let input_bytes: Vec = ::std::fs::read(input_path).expect("Failed to read input"); 68 | 69 | let (remaining_bytes, actual_value): (&[u8], $type) = ($parser)(&input_bytes).expect("Failed to parse"); 70 | 71 | let expected_path = path.join("value.json"); 72 | let expected_file = ::std::fs::File::open(expected_path).expect("Failed to open expected value file"); 73 | let expected_reader = ::std::io::BufReader::new(expected_file); 74 | let expected_value = serde_json_v8::from_reader::<_, $type>(expected_reader).expect("Failed to read AST"); 75 | 76 | #[allow(clippy::redundant_closure_call)] 77 | ($check)(actual_value, expected_value); 78 | assert_eq!(remaining_bytes, &[] as &[u8]); 79 | } 80 | }; 81 | } 82 | 83 | macro_rules! test_various_parser_impl_eq { 84 | ($(#[$meta:meta])* $name:ident<$type:ty>, $parser:path $(,)?) => { 85 | test_various_parser_impl_any!( 86 | $(#[$meta])* $name<$type>, 87 | $parser, 88 | |actual_value, expected_value| { assert_eq!(actual_value, expected_value) }, 89 | ); 90 | }; 91 | } 92 | 93 | macro_rules! test_various_parser_impl_is { 94 | ($(#[$meta:meta])* $name:ident<$type:ty>, $parser:path $(,)?) => { 95 | test_various_parser_impl_any!( 96 | $(#[$meta])* $name<$type>, 97 | $parser, 98 | |actual_value: $type, expected_value: $type| { assert!(crate::swf_types::float_is::Is::is(&actual_value, &expected_value)) }, 99 | ); 100 | }; 101 | } 102 | 103 | test_various_parser_impl_is!( 104 | #[test_resources("../tests/various/float16-le/*/")] test_parse_le_f16, 105 | crate::streaming::basic_data_types::parse_le_f16, 106 | ); 107 | 108 | test_various_parser_impl_eq!( 109 | #[test_resources("../tests/various/matrix/*/")] test_parse_matrix, 110 | crate::streaming::basic_data_types::parse_matrix, 111 | ); 112 | 113 | fn parse_header34(input: &[u8]) -> NomResult<&[u8], swf_types::Header> { 114 | crate::streaming::movie::parse_header(input, 34) 115 | } 116 | 117 | test_various_parser_impl_eq!( 118 | #[test_resources("../tests/various/header/*/")] test_parse_header, 119 | parse_header34, 120 | ); 121 | 122 | test_various_parser_impl_eq!( 123 | #[test_resources("../tests/various/rect/*/")] test_parse_rect, 124 | crate::streaming::basic_data_types::parse_rect, 125 | ); 126 | 127 | test_various_parser_impl_eq!( 128 | #[test_resources("../tests/various/swf-signature/*/")] test_parse_swf_signature, 129 | crate::streaming::movie::parse_swf_signature, 130 | ); 131 | 132 | test_various_parser_impl_eq!( 133 | #[test_resources("../tests/various/uint32-leb128/*/")] test_parse_leb128_u32, 134 | crate::streaming::basic_data_types::parse_leb128_u32, 135 | ); 136 | } 137 | -------------------------------------------------------------------------------- /rs/src/streaming/movie.rs: -------------------------------------------------------------------------------- 1 | use crate::streaming::basic_data_types::{parse_le_ufixed8_p8, parse_rect}; 2 | use crate::streaming::tag::parse_tag; 3 | use crate::streaming::decompress; 4 | use nom::number::streaming::{le_u16 as parse_le_u16, le_u32 as parse_le_u32, le_u8 as parse_u8}; 5 | use nom::{IResult as NomResult, Needed}; 6 | use std::convert::TryFrom; 7 | use swf_types as ast; 8 | 9 | pub fn parse_swf(input: &[u8]) -> NomResult<&[u8], ast::Movie> { 10 | let (input, signature) = parse_swf_signature(input)?; 11 | 12 | let result = match signature.compression_method { 13 | ast::CompressionMethod::None => decompress::decompress_none(input), 14 | ast::CompressionMethod::Deflate => decompress::decompress_zlib(input), 15 | ast::CompressionMethod::Lzma => decompress::decompress_lzma(input), 16 | }; 17 | let (input, payload) = result.unwrap(); 18 | 19 | // TODO: check decompressed payload length against signature? 20 | 21 | match parse_movie(&payload, signature.swf_version) { 22 | // TODO: should we assert that the payload is fully consumed? 23 | Ok((_payload, movie)) => Ok((input, movie)), 24 | Err(nom::Err::Error(e)) => Err(nom::Err::Error(nom::error::Error::new(&[], e.code))), 25 | Err(nom::Err::Failure(e)) => Err(nom::Err::Failure(nom::error::Error::new(&[], e.code))), 26 | Err(nom::Err::Incomplete(n)) => Err(nom::Err::Incomplete(n)), 27 | } 28 | } 29 | 30 | pub fn parse_swf_signature(input: &[u8]) -> NomResult<&[u8], ast::SwfSignature> { 31 | use nom::combinator::map; 32 | 33 | let (input, compression_method) = parse_compression_method(input)?; 34 | let (input, swf_version) = parse_u8(input)?; 35 | let (input, uncompressed_file_length) = map(parse_le_u32, |x| usize::try_from(x).unwrap())(input)?; 36 | 37 | Ok(( 38 | input, 39 | ast::SwfSignature { 40 | compression_method, 41 | swf_version, 42 | uncompressed_file_length, 43 | }, 44 | )) 45 | } 46 | 47 | pub fn parse_compression_method(input: &[u8]) -> NomResult<&[u8], ast::CompressionMethod> { 48 | use nom::bytes::streaming::take; 49 | let (input, tag) = take(3usize)(input)?; 50 | match tag { 51 | b"FWS" => Ok((input, ast::CompressionMethod::None)), 52 | b"CWS" => Ok((input, ast::CompressionMethod::Deflate)), 53 | b"ZWS" => Ok((input, ast::CompressionMethod::Lzma)), 54 | _ => Err(nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Switch))), 55 | } 56 | } 57 | 58 | pub fn parse_header(input: &[u8], swf_version: u8) -> NomResult<&[u8], ast::Header> { 59 | let (input, frame_size) = parse_rect(input)?; 60 | let (input, frame_rate) = parse_le_ufixed8_p8(input)?; 61 | let (input, frame_count) = parse_le_u16(input)?; 62 | 63 | Ok(( 64 | input, 65 | ast::Header { 66 | swf_version, 67 | frame_size, 68 | frame_rate, 69 | frame_count, 70 | }, 71 | )) 72 | } 73 | 74 | pub(crate) fn parse_movie(input: &[u8], swf_version: u8) -> NomResult<&[u8], ast::Movie> { 75 | let (input, header) = parse_header(input, swf_version)?; 76 | let (input, tags) = parse_tag_block_string(input, swf_version)?; 77 | 78 | Ok((input, ast::Movie { header, tags })) 79 | } 80 | 81 | pub(crate) fn parse_tag_block_string(mut input: &[u8], swf_version: u8) -> NomResult<&[u8], Vec> { 82 | let mut result: Vec = Vec::new(); 83 | loop { 84 | input = match parse_tag(input, swf_version) { 85 | Ok((input, Some(tag))) => { 86 | result.push(tag); 87 | input 88 | } 89 | Ok((input, None)) => return Ok((input, result)), 90 | Err(_) => return Err(::nom::Err::Incomplete(Needed::Unknown)), 91 | } 92 | } 93 | } 94 | 95 | #[cfg(test)] 96 | mod tests { 97 | use super::*; 98 | 99 | #[test] 100 | fn test_parse_compression_method() { 101 | assert_eq!( 102 | parse_compression_method(&b"FWS"[..]), 103 | Ok((&[][..], ast::CompressionMethod::None)) 104 | ); 105 | assert_eq!( 106 | parse_compression_method(&b"CWS"[..]), 107 | Ok((&[][..], ast::CompressionMethod::Deflate)) 108 | ); 109 | assert_eq!( 110 | parse_compression_method(&b"ZWS"[..]), 111 | Ok((&[][..], ast::CompressionMethod::Lzma)) 112 | ); 113 | } 114 | 115 | #[test] 116 | fn test_parse_swf_header_signature() { 117 | assert_eq!( 118 | parse_swf_signature(&b"FWS\x0f\x08\x00\x00\x00"[..]), 119 | Ok(( 120 | &[][..], 121 | ast::SwfSignature { 122 | compression_method: ast::CompressionMethod::None, 123 | swf_version: 15u8, 124 | uncompressed_file_length: 8, 125 | } 126 | )) 127 | ); 128 | assert_eq!( 129 | parse_swf_signature(&b"CWS\x0f\x08\x00\x00\x00"[..]), 130 | Ok(( 131 | &[][..], 132 | ast::SwfSignature { 133 | compression_method: ast::CompressionMethod::Deflate, 134 | swf_version: 15u8, 135 | uncompressed_file_length: 8, 136 | } 137 | )) 138 | ); 139 | 140 | assert_eq!( 141 | parse_swf_signature(&b"\x43\x57\x53\x08\xac\x05\x00\x00"[..]), 142 | Ok(( 143 | &[][..], 144 | ast::SwfSignature { 145 | compression_method: ast::CompressionMethod::Deflate, 146 | swf_version: 8u8, 147 | uncompressed_file_length: 1452, 148 | } 149 | )) 150 | ); 151 | } 152 | } 153 | --------------------------------------------------------------------------------