├── .github └── workflows │ └── test.yml ├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── Fixtures ├── .gitignore ├── Entrypoints │ ├── I64ImportTransformerTests.swift │ └── StackOverflowSanitizerTests.swift ├── Makefile ├── build │ └── .gitkeep ├── index.js ├── package-lock.json ├── package.json ├── stack_sanitizer_support.c └── webpack.config.js ├── IntegrationTests ├── .gitignore ├── .swiftpm │ └── xcode │ │ └── package.xcworkspace │ │ └── contents.xcworkspacedata ├── Package.resolved ├── Package.swift ├── README.md └── Tests │ └── IntegrationTests │ ├── I64ImportTransformerTests.swift │ ├── StackOverflowSanitizerTests.swift │ └── misc.swift ├── LICENSE ├── Makefile ├── Package.swift ├── README.md ├── Sources ├── WasmTransformer │ ├── BinaryFormat.swift │ ├── ByteEncodable.swift │ ├── InputByteStream.swift │ ├── LEB128.swift │ ├── OutputWriter.swift │ ├── Readers │ │ ├── CodeSectionReader.swift │ │ ├── ElementSectionReader.swift │ │ ├── FunctionSectionReader.swift │ │ ├── ImportSectionReader.swift │ │ ├── ModuleReader.swift │ │ ├── TypeSectionReader.swift │ │ └── VectorSectionReader.swift │ ├── Sections.swift │ ├── Trampoline.swift │ ├── Transformers │ │ ├── CustomSectionStripper.swift │ │ ├── I64ImportTransformer.swift │ │ ├── SizeProfiler.swift │ │ ├── StackOverflowSanitizer+Fixtures.swift │ │ └── StackOverflowSanitizer.swift │ └── WasmTransformer.swift └── wasm-trans │ └── main.swift ├── Tests ├── LinuxMain.swift └── WasmTransformerTests │ ├── CustomSectionStripperTests.swift │ ├── I64ImportTransformerTests.swift │ ├── InputByteStreamTests.swift │ ├── IntegrationTests.swift │ ├── SectionsInfoTests.swift │ ├── StackOverflowSanitizerTests.swift │ └── misc.swift └── Tools └── GenerateStackOverflowSanitizerSupport.swift /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: [push] 3 | 4 | jobs: 5 | build-fixtures: 6 | runs-on: macos-latest 7 | strategy: 8 | matrix: 9 | toolchain-version: 10 | - swift-wasm-5.3.0-RELEASE 11 | - swift-wasm-5.4.0-RELEASE 12 | - swift-wasm-5.5.0-RELEASE 13 | - swift-wasm-5.6.0-RELEASE 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Install SwiftWasm toolchain 18 | run: | 19 | VERSION=${{ matrix.toolchain-version }} 20 | TOOLCHAIN_URL="https://github.com/swiftwasm/swift/releases/download/$VERSION/$VERSION-macos_x86_64.pkg" 21 | wget $TOOLCHAIN_URL 22 | installer -target CurrentUserHomeDirectory -pkg $VERSION-macos_x86_64.pkg 23 | echo "SWIFT_TOOLCHAIN=$HOME/Library/Developer/Toolchains/$VERSION.xctoolchain/usr" >> $GITHUB_ENV 24 | - name: Setup fixtures 25 | run: | 26 | npm install 27 | npm run build 28 | make all 29 | working-directory: Fixtures 30 | - uses: actions/upload-artifact@v2 31 | with: 32 | name: test-fixtures-${{ matrix.toolchain-version }} 33 | path: Fixtures/build 34 | 35 | ubuntu-unit-tests: 36 | runs-on: ubuntu-latest 37 | needs: build-fixtures 38 | strategy: 39 | matrix: 40 | toolchain-version: 41 | - swift-wasm-5.3.0-RELEASE 42 | - swift-wasm-5.4.0-RELEASE 43 | - swift-wasm-5.5.0-RELEASE 44 | - swift-wasm-5.6.0-RELEASE 45 | steps: 46 | - uses: actions/checkout@v2 47 | - uses: actions/download-artifact@v2 48 | with: 49 | name: test-fixtures-${{ matrix.toolchain-version }} 50 | path: Fixtures/build 51 | - name: Install dependencies 52 | run: | 53 | curl -L -o wabt.tar.gz https://github.com/WebAssembly/wabt/releases/download/1.0.19/wabt-1.0.19-ubuntu.tar.gz 54 | tar xzvf wabt.tar.gz 55 | sudo cp wabt-*/bin/* /usr/local/bin 56 | - name: Run unit tests 57 | run: swift test --enable-test-discovery 58 | 59 | macos-unit-tests: 60 | runs-on: macos-12 61 | needs: build-fixtures 62 | strategy: 63 | matrix: 64 | xcode: ["Xcode_14.0", "Xcode_13.4.1", "Xcode_13.2.1"] 65 | toolchain-version: 66 | - swift-wasm-5.3.0-RELEASE 67 | - swift-wasm-5.4.0-RELEASE 68 | - swift-wasm-5.5.0-RELEASE 69 | - swift-wasm-5.6.0-RELEASE 70 | 71 | steps: 72 | - uses: actions/checkout@v2 73 | - uses: actions/download-artifact@v2 74 | with: 75 | name: test-fixtures-${{ matrix.toolchain-version }} 76 | path: Fixtures/build 77 | - name: Select Xcode toolchain 78 | run: sudo xcode-select -s /Applications/${{ matrix.xcode }}.app/Contents/Developer 79 | - name: Install dependencies 80 | run: brew install wabt 81 | - name: Run unit tests 82 | run: swift test 83 | 84 | integration-tests: 85 | runs-on: macos-12 86 | needs: build-fixtures 87 | strategy: 88 | fail-fast: false 89 | matrix: 90 | toolchain-version: 91 | - swift-wasm-5.3.0-RELEASE 92 | - swift-wasm-5.4.0-RELEASE 93 | - swift-wasm-5.5.0-RELEASE 94 | - swift-wasm-5.6.0-RELEASE 95 | steps: 96 | - uses: actions/checkout@v2 97 | - uses: actions/download-artifact@v2 98 | with: 99 | name: test-fixtures-${{ matrix.toolchain-version }} 100 | path: Fixtures/build 101 | - name: Select Xcode toolchain 102 | run: sudo xcode-select -s /Applications/Xcode_13.2.1.app/Contents/Developer/ 103 | - name: Install dependencies 104 | run: | 105 | sudo pip3 install selenium 106 | - run: swift test 107 | working-directory: IntegrationTests 108 | env: 109 | PYTHON_VERSION: 3 110 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | Package.resolved 7 | .vscode 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Fixtures/.gitignore: -------------------------------------------------------------------------------- 1 | /build/* 2 | !.gitkeep 3 | /node_modules 4 | -------------------------------------------------------------------------------- /Fixtures/Entrypoints/I64ImportTransformerTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | let date = Date() 4 | print("timeIntervalSince1970 is \(date.timeIntervalSince1970)") 5 | -------------------------------------------------------------------------------- /Fixtures/Entrypoints/StackOverflowSanitizerTests.swift: -------------------------------------------------------------------------------- 1 | struct LargeBox0 { 2 | let value0: UInt64 = 0xdeadbeef 3 | let value1: UInt64 = 0xdeadbeef 4 | let value2: UInt64 = 0xdeadbeef 5 | let value3: UInt64 = 0xdeadbeef 6 | let value4: UInt64 = 0xdeadbeef 7 | let value5: UInt64 = 0xdeadbeef 8 | let value6: UInt64 = 0xdeadbeef 9 | let value7: UInt64 = 0xdeadbeef 10 | } 11 | 12 | struct LargeBox { 13 | let value0 = LargeBox0() 14 | let value1 = LargeBox0() 15 | let value2 = LargeBox0() 16 | let value3 = LargeBox0() 17 | let value4 = LargeBox0() 18 | let value5 = LargeBox0() 19 | let value6 = LargeBox0() 20 | let value7 = LargeBox0() 21 | } 22 | 23 | func causeStackOverflow(box: LargeBox) { 24 | let box = LargeBox() 25 | causeStackOverflow(box: box) 26 | } 27 | 28 | causeStackOverflow(box: LargeBox()) 29 | -------------------------------------------------------------------------------- /Fixtures/Makefile: -------------------------------------------------------------------------------- 1 | ifndef SWIFT_TOOLCHAIN 2 | $(error Please set SWIFT_TOOLCHAIN env variable) 3 | endif 4 | 5 | SWIFTC = ${SWIFT_TOOLCHAIN}/bin/swiftc 6 | CLANG = ${SWIFT_TOOLCHAIN}/bin/clang 7 | LLVM_AR = ${SWIFT_TOOLCHAIN}/bin/llvm-ar 8 | 9 | .PHONY: all 10 | all: build/I64ImportTransformerTests.wasm build/StackOverflowSanitizerTests.wasm 11 | 12 | build/%.wasm: Entrypoints/%.swift 13 | $(SWIFTC) -target wasm32-unknown-wasi \ 14 | -sdk "${SWIFT_TOOLCHAIN}/share/wasi-sysroot" \ 15 | -I "${SWIFT_TOOLCHAIN}/lib/swift/wasi/wasm32" \ 16 | -lFoundation \ 17 | -lCoreFoundation \ 18 | -lBlocksRuntime \ 19 | -licui18n \ 20 | -luuid \ 21 | $< -o $@ 22 | 23 | build/StackOverflowSanitizerTests.wasm: Entrypoints/StackOverflowSanitizerTests.swift build/stack_sanitizer_support.o 24 | $(SWIFTC) -target wasm32-unknown-wasi \ 25 | -sdk "${SWIFT_TOOLCHAIN}/share/wasi-sysroot" \ 26 | -I "${SWIFT_TOOLCHAIN}/lib/swift/wasi/wasm32" \ 27 | -lFoundation \ 28 | -lCoreFoundation \ 29 | -lBlocksRuntime \ 30 | -licui18n \ 31 | -luuid \ 32 | build/stack_sanitizer_support.o \ 33 | $< -o $@ 34 | 35 | build/stack_sanitizer_support.o: stack_sanitizer_support.c 36 | $(CLANG) -c -target wasm32-unknown-wasi $< \ 37 | -o build/stack_sanitizer_support.o 38 | -------------------------------------------------------------------------------- /Fixtures/build/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swiftwasm/WasmTransformer/d04b31f61b6f528a9a96ebfe4fa4275e333eba82/Fixtures/build/.gitkeep -------------------------------------------------------------------------------- /Fixtures/index.js: -------------------------------------------------------------------------------- 1 | import { WASI } from "@wasmer/wasi"; 2 | import { WasmFs } from "@wasmer/wasmfs"; 3 | 4 | const wrapWASI = (wasiObject) => { 5 | for (const key in wasiObject.wasiImport) { 6 | const func = wasiObject.wasiImport[key] 7 | wasiObject.wasiImport[key] = function() { 8 | console.log(`[tracing] WASI.${key}`); 9 | return Reflect.apply(func, undefined, arguments); 10 | } 11 | } 12 | // PATCH: @wasmer-js/wasi@0.x forgets to call `refreshMemory` in `clock_res_get`, 13 | // which writes its result to memory view. Without the refresh the memory view, 14 | // it accesses a detached array buffer if the memory is grown by malloc. 15 | // But they wasmer team discarded the 0.x codebase at all and replaced it with 16 | // a new implementation written in Rust. The new version 1.x is really unstable 17 | // and not production-ready as far as katei investigated in Apr 2022. 18 | // So override the broken implementation of `clock_res_get` here instead of 19 | // fixing the wasi polyfill. 20 | // Reference: https://github.com/wasmerio/wasmer-js/blob/55fa8c17c56348c312a8bd23c69054b1aa633891/packages/wasi/src/index.ts#L557 21 | const original_clock_res_get = wasiObject.wasiImport["clock_res_get"]; 22 | wasiObject.wasiImport["clock_res_get"] = (clockId, resolution) => { 23 | wasiObject.refreshMemory(); 24 | return original_clock_res_get(clockId, resolution) 25 | }; 26 | return wasiObject.wasiImport; 27 | } 28 | 29 | window.I64ImportTransformerTests = async (bytes) => { 30 | const wasmFs = new WasmFs(); 31 | const wasi = new WASI({ 32 | args: [], env: {}, 33 | bindings: { 34 | ...WASI.defaultBindings, 35 | fs: wasmFs.fs, 36 | } 37 | }); 38 | 39 | const importObject = { 40 | wasi_snapshot_preview1: wrapWASI(wasi), 41 | }; 42 | 43 | const module = await WebAssembly.compile(bytes); 44 | const instance = await WebAssembly.instantiate(module, importObject); 45 | wasi.start(instance); 46 | }; 47 | 48 | window.StackOverflowSanitizerTests = async (bytes) => { 49 | const wasmFs = new WasmFs(); 50 | const wasi = new WASI({ 51 | args: [], env: {}, 52 | bindings: { 53 | ...WASI.defaultBindings, 54 | fs: wasmFs.fs, 55 | } 56 | }); 57 | 58 | const importObject = { 59 | wasi_snapshot_preview1: wrapWASI(wasi), 60 | __stack_sanitizer: { 61 | report_stack_overflow: () => { 62 | throw new Error("CATCH_STACK_OVERFLOW"); 63 | } 64 | } 65 | }; 66 | 67 | const module = await WebAssembly.compile(bytes); 68 | const instance = await WebAssembly.instantiate(module, importObject); 69 | wasi.start(instance); 70 | } 71 | -------------------------------------------------------------------------------- /Fixtures/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fixtures", 3 | "main": "index.js", 4 | "dependencies": { 5 | "@wasmer/wasi": "^0.12.0", 6 | "@wasmer/wasmfs": "^0.12.0" 7 | }, 8 | "scripts": { 9 | "build": "webpack --config webpack.config.js" 10 | }, 11 | "devDependencies": { 12 | "webpack": "^4.42.0", 13 | "webpack-cli": "^3.3.11" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Fixtures/stack_sanitizer_support.c: -------------------------------------------------------------------------------- 1 | __attribute__((__import_module__("__stack_sanitizer"), 2 | __import_name__("report_stack_overflow"))) 3 | void __stack_sanitizer_report_stack_overflow(void); 4 | 5 | __attribute__((visibility("hidden"))) 6 | void __dummy() { 7 | __stack_sanitizer_report_stack_overflow(); 8 | } 9 | -------------------------------------------------------------------------------- /Fixtures/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mode: "development", 3 | entry: './index.js', 4 | output: { 5 | filename: 'bundle.js', 6 | path: __dirname + "/build", 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /IntegrationTests/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | -------------------------------------------------------------------------------- /IntegrationTests/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /IntegrationTests/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "PythonKit", 6 | "repositoryURL": "https://github.com/pvieito/PythonKit.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "8de2a3f1f8c1388e9fca84f192f96821d9ccd43d", 10 | "version": null 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /IntegrationTests/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "IntegrationTests", 7 | dependencies: [ 8 | .package(url: "https://github.com/pvieito/PythonKit.git", .revision("8de2a3f1f8c1388e9fca84f192f96821d9ccd43d")), 9 | .package(name: "WasmTransformer", path: "../"), 10 | ], 11 | targets: [ 12 | .testTarget( 13 | name: "IntegrationTests", 14 | dependencies: ["WasmTransformer", "PythonKit"]), 15 | ] 16 | ) 17 | -------------------------------------------------------------------------------- /IntegrationTests/README.md: -------------------------------------------------------------------------------- 1 | # IntegrationTests 2 | 3 | A description of this package. 4 | -------------------------------------------------------------------------------- /IntegrationTests/Tests/IntegrationTests/I64ImportTransformerTests.swift: -------------------------------------------------------------------------------- 1 | import WasmTransformer 2 | import Foundation 3 | import XCTest 4 | import PythonKit 5 | 6 | final class I64ImportTransformerTests: XCTestCase { 7 | 8 | let binaryPath = buildPath.appendingPathComponent("I64ImportTransformerTests.wasm") 9 | 10 | func lowerI64Imports(_ input: URL) throws -> URL { 11 | let transformer = I64ImportTransformer() 12 | var inputStream = try InputByteStream(from: binaryPath) 13 | var writer = InMemoryOutputWriter() 14 | try transformer.transform(&inputStream, writer: &writer) 15 | 16 | let (url, wasmFileHandle) = makeTemporaryFile(suffix: ".wasm") 17 | wasmFileHandle.write(Data(writer.bytes())) 18 | return url 19 | } 20 | 21 | func testIntegration() throws { 22 | let html = try createCheckHtml(embedding: lowerI64Imports(binaryPath)) 23 | let (tmpHtmlURL, htmlFileHandle) = makeTemporaryFile(suffix: ".html") 24 | htmlFileHandle.write(html.data(using: .utf8)!) 25 | 26 | 27 | // Ensure that no exception happen 28 | let time = Python.import("time") 29 | let webdriver = Python.import("selenium.webdriver") 30 | 31 | let driver = webdriver.Safari() 32 | driver.set_page_load_timeout(120) 33 | driver.get(tmpHtmlURL.absoluteString) 34 | time.sleep(5) 35 | driver.execute_async_script("await window.I64ImportTransformerTests(wasmBytes);arguments[0]();") 36 | time.sleep(5) 37 | driver.quit() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /IntegrationTests/Tests/IntegrationTests/StackOverflowSanitizerTests.swift: -------------------------------------------------------------------------------- 1 | import WasmTransformer 2 | import Foundation 3 | import XCTest 4 | import PythonKit 5 | 6 | final class StackOverflowSanitizerTests: XCTestCase { 7 | 8 | let binaryPath = buildPath.appendingPathComponent("StackOverflowSanitizerTests.wasm") 9 | 10 | func instrumentStackSanitizer(_ input: URL) throws -> URL { 11 | let transformer = StackOverflowSanitizer() 12 | var inputStream = try InputByteStream(from: binaryPath) 13 | var writer = InMemoryOutputWriter() 14 | try transformer.transform(&inputStream, writer: &writer) 15 | 16 | let (url, wasmFileHandle) = makeTemporaryFile(suffix: ".wasm") 17 | wasmFileHandle.write(Data(writer.bytes())) 18 | return url 19 | } 20 | 21 | func testIntegration() throws { 22 | let wasm = try instrumentStackSanitizer(binaryPath) 23 | let html = try createCheckHtml(embedding: wasm) 24 | let (tmpHtmlURL, htmlFileHandle) = makeTemporaryFile(suffix: ".html") 25 | htmlFileHandle.write(html.data(using: .utf8)!) 26 | 27 | 28 | // Ensure that no exception happen 29 | let time = Python.import("time") 30 | let webdriver = Python.import("selenium.webdriver") 31 | 32 | let driver = webdriver.Safari() 33 | driver.set_page_load_timeout(120) 34 | driver.get(tmpHtmlURL.absoluteString) 35 | time.sleep(5) 36 | let entryScript = "await window.StackOverflowSanitizerTests(wasmBytes);arguments[0]();" 37 | XCTAssertThrowsError( 38 | try driver.execute_async_script.throwing.dynamicallyCall(withArguments: entryScript) 39 | ) { error in 40 | let description = String(describing: error) 41 | XCTAssertTrue(description.contains("CATCH_STACK_OVERFLOW"), description) 42 | } 43 | driver.quit() 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /IntegrationTests/Tests/IntegrationTests/misc.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | let buildPath = URL(fileURLWithPath: #filePath) 4 | .deletingLastPathComponent() 5 | .deletingLastPathComponent() 6 | .deletingLastPathComponent() 7 | .deletingLastPathComponent() 8 | .appendingPathComponent("Fixtures/build") 9 | 10 | func makeTemporaryFile(suffix: String) -> (URL, FileHandle) { 11 | let tempdir = URL(fileURLWithPath: NSTemporaryDirectory()) 12 | let templatePath = tempdir.appendingPathComponent("wasm-transformer.XXXXXX\(suffix)") 13 | var template = [UInt8](templatePath.path.utf8).map { Int8($0) } + [Int8(0)] 14 | let fd = mkstemps(&template, Int32(suffix.utf8.count)) 15 | if fd == -1 { 16 | fatalError("Failed to create temp directory") 17 | } 18 | let url = URL(fileURLWithPath: String(cString: template)) 19 | let handle = FileHandle(fileDescriptor: fd, closeOnDealloc: true) 20 | return (url, handle) 21 | } 22 | 23 | let bundleJSPath = buildPath.appendingPathComponent("bundle.js") 24 | func createCheckHtml(embedding binaryPath: URL) throws -> String { 25 | let wasmBytes = try Data(contentsOf: binaryPath) 26 | return try """ 27 | 37 | """ 38 | } 39 | 40 | import WasmTransformer 41 | 42 | extension InputByteStream { 43 | init(from url: URL) throws { 44 | let bytes = try Array(Data(contentsOf: url)) 45 | self.init(bytes: bytes) 46 | } 47 | } 48 | 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2020 WasmTransformer contributors 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | update-fixture: Sources/WasmTransformer/Transformers/StackOverflowSanitizer+Fixtures.swift 2 | 3 | Fixtures/build/stack_sanitizer_support.o: 4 | $(MAKE) -C Fixtures build/stack_sanitizer_support.o 5 | Sources/WasmTransformer/Transformers/StackOverflowSanitizer+Fixtures.swift: Fixtures/build/stack_sanitizer_support.o Tools/GenerateStackOverflowSanitizerSupport.swift 6 | swift ./Tools/GenerateStackOverflowSanitizerSupport.swift Fixtures/build/stack_sanitizer_support.o > Sources/WasmTransformer/Transformers/StackOverflowSanitizer+Fixtures.swift 7 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.4 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "WasmTransformer", 7 | products: [ 8 | .library( 9 | name: "WasmTransformer", 10 | targets: ["WasmTransformer"] 11 | ), 12 | ], 13 | dependencies: [ 14 | .package(url: "https://github.com/apple/swift-argument-parser", .exact("1.0.0")), 15 | ], 16 | targets: [ 17 | .executableTarget(name: "wasm-trans", dependencies: [ 18 | "WasmTransformer", 19 | .product(name: "ArgumentParser", package: "swift-argument-parser"), 20 | ]), 21 | .target(name: "WasmTransformer", dependencies: []), 22 | .testTarget(name: "WasmTransformerTests", dependencies: ["WasmTransformer"]), 23 | ] 24 | ) 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WasmTransformer 2 | 3 | ![Test](https://github.com/swiftwasm/swift-wasm-transformer/workflows/Test/badge.svg) 4 | 5 | A package which provides transformation operation for WebAssembly binary. Inspired by [Rust implementation](https://github.com/wasmerio/wasmer-js/tree/master/crates/wasm_transformer) 6 | 7 | ## Available transformations 8 | 9 | ### `lowerI64Imports` 10 | 11 | 12 | ```swift 13 | public func lowerI64Imports(_ input: [UInt8]) throws -> [UInt8] 14 | ``` 15 | 16 | Inserts trampoline functions for imports that have i64 params or returns. This is useful for running Wasm modules in browsers that do not support JavaScript BigInt -> Wasm i64 integration. Especially in the case for i64 WASI Imports. 17 | 18 | 19 | ### `stripCustomSections` 20 | 21 | ```swift 22 | public func stripCustomSections(_ input: [UInt8]) throws -> [UInt8] 23 | ``` 24 | 25 | Strip all custom sections from input WebAssembly binary. 26 | 27 | 28 | ## Testing 29 | 30 | 1. Set environment variable `SWIFT_TOOLCHAIN` to the path to your SwiftWasm toolchain. 31 | e.g. `$HOME/Library/Developer/Toolchains/swift-wasm-5.7.3-RELEASE.xctoolchain/usr` 32 | 2. Set up testing fixtures by: `(cd ./Fixtures/ && npm install && npm run build && make all)` 33 | 3. Run `swift test` 34 | 35 | -------------------------------------------------------------------------------- /Sources/WasmTransformer/BinaryFormat.swift: -------------------------------------------------------------------------------- 1 | let magic: [UInt8] = [0x00, 0x61, 0x73, 0x6D] 2 | let version: [UInt8] = [0x01, 0x00, 0x00, 0x00] 3 | 4 | let LIMITS_HAS_MAX_FLAG: UInt8 = 0x1 5 | let LIMITS_IS_SHARED_FLAG: UInt8 = 0x2 6 | 7 | public enum SectionType: UInt8 { 8 | case custom = 0 9 | case type = 1 10 | case `import` = 2 11 | case function = 3 12 | case table = 4 13 | case memory = 5 14 | case global = 6 15 | case export = 7 16 | case start = 8 17 | case element = 9 18 | case code = 10 19 | case data = 11 20 | case dataCount = 12 21 | } 22 | 23 | public enum ValueType: UInt8, Equatable { 24 | case i32 = 0x7F 25 | case i64 = 0x7E 26 | case f32 = 0x7D 27 | case f64 = 0x7C 28 | } 29 | 30 | enum ExternalKind: UInt8, Equatable { 31 | case `func` = 0 32 | case table = 1 33 | case memory = 2 34 | case global = 3 35 | case except = 4 36 | } 37 | 38 | enum ConstOpcode: UInt8 { 39 | case i32Const = 0x41 40 | case i64Const = 0x42 41 | case f32Const = 0x43 42 | case f64Const = 0x44 43 | } 44 | 45 | 46 | enum BlockType { 47 | case empty 48 | } 49 | 50 | let END_INST_OPCODE: UInt8 = 0x0B 51 | enum Opcode: Equatable { 52 | case unreachable 53 | case end 54 | case localGet(UInt32) 55 | case localSet(UInt32) 56 | case globalSet(UInt32) 57 | case `if`(BlockType) 58 | case call(UInt32) 59 | case i32Const(Int32) 60 | case i32LtS 61 | case i32WrapI64 62 | case unknown([UInt8]) 63 | 64 | func serialize() -> [UInt8] { 65 | switch self { 66 | case .unreachable: return [0x00] 67 | case .end: return [END_INST_OPCODE] 68 | case let .localGet(localIndex): 69 | return [0x20] + encodeULEB128(localIndex) 70 | case let .localSet(localIndex): 71 | return [0x21] + encodeULEB128(localIndex) 72 | case let .globalSet(globalIndex): 73 | return [0x24] + encodeULEB128(globalIndex) 74 | case .if(.empty): 75 | return [0x04, 0x40] 76 | case let .call(funcIndex): 77 | return [0x10] + encodeULEB128(funcIndex) 78 | case let .i32Const(value): 79 | return [0x41] + encodeSLEB128(value) 80 | case .i32LtS: return [0x48] 81 | case .i32WrapI64: return [0xA7] 82 | case let .unknown(bytes): return bytes 83 | } 84 | } 85 | } 86 | 87 | public struct FuncSignature { 88 | public let params: [ValueType] 89 | public let results: [ValueType] 90 | } 91 | -------------------------------------------------------------------------------- /Sources/WasmTransformer/ByteEncodable.swift: -------------------------------------------------------------------------------- 1 | protocol ByteEncodable { 2 | func encode(to writer: inout Writer) throws 3 | } 4 | 5 | extension FuncSignature: ByteEncodable { 6 | func encode(to writer: inout Writer) throws { 7 | try writer.writeByte(0x60) 8 | try writer.writeResultTypes(params) 9 | try writer.writeResultTypes(results) 10 | } 11 | } 12 | 13 | extension Import: ByteEncodable { 14 | func encode(to writer: inout Writer) throws { 15 | try writer.writeString(module) 16 | try writer.writeString(field) 17 | switch descriptor { 18 | case .function(let sigIndex): 19 | try writer.writeByte(ExternalKind.func.rawValue) 20 | try writer.writeBytes(encodeULEB128(sigIndex)) 21 | case .table(let rawBytes): 22 | try writer.writeByte(ExternalKind.table.rawValue) 23 | try writer.writeBytes(rawBytes) 24 | case .memory(let rawBytes): 25 | try writer.writeByte(ExternalKind.memory.rawValue) 26 | try writer.writeBytes(rawBytes) 27 | case .global(let rawBytes): 28 | try writer.writeByte(ExternalKind.global.rawValue) 29 | try writer.writeBytes(rawBytes) 30 | } 31 | } 32 | } 33 | 34 | extension SignatureIndex: ByteEncodable { 35 | func encode(to writer: inout Writer) throws { 36 | try writer.writeBytes(encodeULEB128(value)) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/WasmTransformer/InputByteStream.swift: -------------------------------------------------------------------------------- 1 | public struct InputByteStream { 2 | public private(set) var offset: Int 3 | public let bytes: ArraySlice 4 | public var isEOF: Bool { 5 | offset >= bytes.endIndex 6 | } 7 | 8 | public init(bytes: ArraySlice) { 9 | self.bytes = bytes 10 | self.offset = bytes.startIndex 11 | } 12 | 13 | public init(bytes: [UInt8]) { 14 | self.init(bytes: bytes[...]) 15 | } 16 | 17 | mutating func readHeader() throws -> ArraySlice { 18 | let maybeMagic = read(4) 19 | guard maybeMagic.elementsEqual(magic) else { 20 | throw Error.badMagic(maybeMagic) 21 | } 22 | let version = read(4) 23 | return version 24 | } 25 | 26 | mutating func readSectionInfo() throws -> SectionInfo { 27 | let startOffset = offset 28 | let rawType = readUInt8() 29 | guard let type = SectionType(rawValue: rawType) else { 30 | throw Error.unexpectedSection(rawType) 31 | } 32 | let size = Int(readVarUInt32()) 33 | let contentStart = offset 34 | 35 | return .init( 36 | startOffset: startOffset, 37 | contentStart: contentStart, 38 | type: type, 39 | size: size 40 | ) 41 | } 42 | 43 | public mutating func seek(_ offset: Int) { 44 | self.offset = offset 45 | } 46 | 47 | public mutating func skip(_ length: Int) { 48 | offset += length 49 | } 50 | 51 | public mutating func read(_ length: Int) -> ArraySlice { 52 | let result = bytes[offset ..< offset + length] 53 | offset += length 54 | return result 55 | } 56 | 57 | mutating func readUInt8() -> UInt8 { 58 | let byte = read(1) 59 | return byte[byte.startIndex] 60 | } 61 | 62 | public mutating func readVarUInt32() -> UInt32 { 63 | let (value, advanced) = decodeULEB128(bytes[offset...], UInt32.self) 64 | offset += advanced 65 | return value 66 | } 67 | 68 | mutating func consumeULEB128(_: T.Type) where T: UnsignedInteger, T: FixedWidthInteger { 69 | let (_, advanced) = decodeULEB128(bytes[offset...], T.self) 70 | offset += advanced 71 | } 72 | 73 | enum Error: Swift.Error { 74 | case badMagic(ArraySlice) 75 | case invalidValueType(UInt8) 76 | case invalidExternalKind(UInt8) 77 | case unsupportedExternalKind(ExternalKind) 78 | case expectConstOpcode(UInt8) 79 | case expectI32Const(ConstOpcode) 80 | case unexpectedOpcode(UInt8) 81 | case unexpectedSection(UInt8) 82 | case expectEnd 83 | } 84 | 85 | /// https://webassembly.github.io/spec/core/binary/types.html#result-types 86 | mutating func readResultTypes() throws -> [ValueType] { 87 | let count = readVarUInt32() 88 | var resultTypes: [ValueType] = [] 89 | for _ in 0 ..< count { 90 | let rawType = readUInt8() 91 | guard let type = ValueType(rawValue: rawType) else { 92 | throw Error.invalidValueType(rawType) 93 | } 94 | resultTypes.append(type) 95 | } 96 | return resultTypes 97 | } 98 | 99 | mutating func readExternalKind() throws -> ExternalKind { 100 | let rawKind = readUInt8() 101 | guard let kind = ExternalKind(rawValue: rawKind) else { 102 | throw Error.invalidExternalKind(rawKind) 103 | } 104 | return kind 105 | } 106 | 107 | typealias Consumer = (ArraySlice) throws -> Void 108 | 109 | mutating func consumeString(consumer: Consumer? = nil) rethrows { 110 | let start = offset 111 | let length = Int(readVarUInt32()) 112 | offset += length 113 | try consumer?(bytes[start ..< offset]) 114 | } 115 | 116 | /// https://webassembly.github.io/spec/core/binary/values.html#names 117 | mutating func readString() -> String { 118 | let length = Int(readVarUInt32()) 119 | let strStart = offset 120 | offset += length 121 | return String(decoding: bytes[strStart ..< offset], as: UTF8.self) 122 | } 123 | 124 | mutating func readImportDescriptor() throws -> ImportDescriptor { 125 | let kind = try readExternalKind() 126 | switch kind { 127 | case .func: return .function(readVarUInt32()) 128 | case .table: return .table(rawBytes: consumeTable()) 129 | case .memory: return .memory(rawBytes: consumeMemory()) 130 | case .global: return .global(rawBytes: consumeGlobalHeader()) 131 | case .except: throw Error.unsupportedExternalKind(kind) 132 | } 133 | } 134 | 135 | /// https://webassembly.github.io/spec/core/binary/modules.html#import-section 136 | mutating func readImport() throws -> Import { 137 | let module = readString() 138 | let field = readString() 139 | let desc = try readImportDescriptor() 140 | return Import(module: module, field: field, descriptor: desc) 141 | } 142 | 143 | /// https://webassembly.github.io/spec/core/binary/types.html#value-types 144 | mutating func readFuncType() throws -> FuncSignature { 145 | let params = try readResultTypes() 146 | let results = try readResultTypes() 147 | return FuncSignature( 148 | params: params, results: results 149 | ) 150 | } 151 | /// https://webassembly.github.io/spec/core/binary/types.html#table-types 152 | mutating func consumeTable() -> ArraySlice { 153 | let start = offset 154 | _ = readUInt8() // element type 155 | let hasMax = readUInt8() != 0 156 | _ = readVarUInt32() // initial 157 | if hasMax { 158 | _ = readVarUInt32() // max 159 | } 160 | return bytes[start ..< offset] 161 | } 162 | 163 | /// https://webassembly.github.io/spec/core/binary/types.html#memory-types 164 | mutating func consumeMemory() -> ArraySlice { 165 | let start = offset 166 | let flags = readUInt8() 167 | let hasMax = (flags & LIMITS_HAS_MAX_FLAG) != 0 168 | _ = readVarUInt32() // initial 169 | if hasMax { 170 | _ = readVarUInt32() // max 171 | } 172 | return bytes[start ..< offset] 173 | } 174 | 175 | /// https://webassembly.github.io/spec/core/binary/types.html#global-types 176 | mutating func consumeGlobalHeader() -> ArraySlice { 177 | let start = offset 178 | _ = readUInt8() // value type 179 | _ = readUInt8() // mutable 180 | return bytes[start ..< offset] 181 | } 182 | 183 | mutating func consumeI32InitExpr() throws -> ArraySlice { 184 | let start = offset 185 | let code = readUInt8() 186 | guard let constOp = ConstOpcode(rawValue: code) else { 187 | throw Error.expectConstOpcode(code) 188 | } 189 | switch constOp { 190 | case .i32Const: 191 | _ = readVarUInt32() 192 | case .f32Const, .f64Const, .i64Const: 193 | throw Error.expectI32Const(constOp) 194 | } 195 | let opcode = readUInt8() 196 | guard opcode == END_INST_OPCODE else { 197 | throw Error.expectEnd 198 | } 199 | return bytes[start ..< offset] 200 | } 201 | 202 | /// https://webassembly.github.io/spec/core/binary/modules.html#binary-local 203 | @discardableResult 204 | mutating func consumeLocals(consumer: Consumer? = nil) throws -> UInt32 { 205 | let start = offset 206 | let count = readVarUInt32() 207 | for _ in 0.. (count: UInt32, rawBytes: ArraySlice) { 215 | let start = offset 216 | let count = readVarUInt32() // n 217 | _ = readUInt8() // value type 218 | return (count, bytes[start ..< offset]) 219 | } 220 | 221 | mutating func consumeBlockType() { 222 | let head = bytes[offset] 223 | let length: Int 224 | switch head { 225 | case 0x40: 226 | length = 1 227 | case _ where ValueType(rawValue: head) != nil: 228 | length = 1 229 | default: 230 | (_, length) = decodeSLEB128(bytes, Int64.self) 231 | } 232 | offset += length 233 | } 234 | 235 | mutating func consumeBrTable() { 236 | let count = readVarUInt32() 237 | for _ in 0 ..< count { 238 | _ = readVarUInt32() 239 | } 240 | _ = readVarUInt32() 241 | } 242 | 243 | mutating func consumeMemoryArg() { 244 | _ = readVarUInt32() 245 | _ = readVarUInt32() 246 | } 247 | 248 | mutating func consumeInst(code: UInt8) throws { 249 | switch code { 250 | // https://webassembly.github.io/spec/core/binary/instructions.html#control-instructions 251 | case 0x00, 0x01: break 252 | case 0x02, 0x03, 0x04: consumeBlockType() 253 | case 0x05, 0x0B: break 254 | case 0x0C, 0x0D: _ = readVarUInt32() // label index 255 | case 0x0E: consumeBrTable() 256 | case 0x0F: break 257 | case 0x10: _ = readVarUInt32() 258 | case 0x11: 259 | _ = readVarUInt32() // type index 260 | _ = readUInt8() // 0x00 261 | 262 | // https://webassembly.github.io/spec/core/binary/instructions.html#parametric-instructions 263 | case 0x1A, 0x1B: break 264 | 265 | // https://webassembly.github.io/spec/core/binary/instructions.html#variable-instructions 266 | case 0x20 ... 0x24: _ = readVarUInt32() // local index 267 | 268 | // https://webassembly.github.io/spec/core/binary/instructions.html#memory-instructions 269 | case 0x28 ... 0x3E: consumeMemoryArg() 270 | case 0x3F, 0x40: _ = readUInt8() // 0x00 271 | 272 | // https://webassembly.github.io/spec/core/binary/instructions.html#numeric-instructions 273 | case 0x41: consumeULEB128(UInt32.self) 274 | case 0x42: consumeULEB128(UInt64.self) 275 | case 0x43: _ = read(4) 276 | case 0x44: _ = read(8) 277 | case 0x45 ... 0xC4: break 278 | case 0xFC: _ = readVarUInt32() 279 | default: 280 | throw Error.unexpectedOpcode(code) 281 | } 282 | } 283 | 284 | mutating func readGlobalSet() throws -> UInt32? { 285 | let rawCode = readUInt8() 286 | switch rawCode { 287 | // https://webassembly.github.io/spec/core/binary/instructions.html#variable-instructions 288 | case 0x24: 289 | return readVarUInt32() 290 | default: 291 | try consumeInst(code: rawCode) 292 | return nil 293 | } 294 | } 295 | 296 | mutating func readCallInst() throws -> UInt32? { 297 | let rawCode = readUInt8() 298 | switch rawCode { 299 | // https://webassembly.github.io/spec/core/binary/instructions.html#control-instructions 300 | case 0x10: 301 | return readVarUInt32() 302 | default: 303 | try consumeInst(code: rawCode) 304 | return nil 305 | } 306 | } 307 | } 308 | -------------------------------------------------------------------------------- /Sources/WasmTransformer/LEB128.swift: -------------------------------------------------------------------------------- 1 | func decodeULEB128(_ bytes: ArraySlice, _: T.Type) -> (value: T, offset: Int) 2 | where T: UnsignedInteger, T: FixedWidthInteger 3 | { 4 | var index: Int = bytes.startIndex 5 | var value: T = 0 6 | var shift: UInt = 0 7 | var byte: UInt8 8 | repeat { 9 | byte = bytes[index] 10 | index += 1 11 | value |= T(byte & 0x7F) << shift 12 | shift += 7 13 | } while byte >= 128 14 | return (value, index - bytes.startIndex) 15 | } 16 | 17 | func decodeSLEB128(_ bytes: ArraySlice, _: T.Type) -> (value: T, offset: Int) 18 | where T: SignedInteger, T: FixedWidthInteger 19 | { 20 | var index: Int = bytes.startIndex 21 | var value: T = 0 22 | var shift: UInt = 0 23 | var byte: UInt8 24 | repeat { 25 | byte = bytes[index] 26 | index += 1 27 | value |= T(byte & 0x7F) << shift 28 | shift += 7 29 | } while byte >= 128 30 | if byte & 0x40 != 0 { 31 | value |= T(-1) << shift 32 | } 33 | return (value, index - bytes.startIndex) 34 | } 35 | 36 | func encodeULEB128(_ value: T, padTo: Int? = nil) -> [UInt8] 37 | where T: UnsignedInteger, T: FixedWidthInteger 38 | { 39 | var value = value 40 | var length = 0 41 | var results: [UInt8] = [] 42 | var needPad: Bool { 43 | guard let padTo = padTo else { return false } 44 | return length < padTo 45 | } 46 | repeat { 47 | var byte = UInt8(value & 0x7F) 48 | value >>= 7 49 | length += 1 50 | if value != 0 || needPad { 51 | byte |= 0x80 52 | } 53 | results.append(byte) 54 | } while value != 0 55 | 56 | if let padTo = padTo, length < padTo { 57 | while length < padTo - 1 { 58 | results.append(0x80) 59 | length += 1 60 | } 61 | results.append(0x00) 62 | } 63 | return results 64 | } 65 | 66 | func encodeSLEB128(_ value: T, padTo: Int? = nil) -> [UInt8] 67 | where T: SignedInteger, T: FixedWidthInteger 68 | { 69 | var value = value 70 | var length = 0 71 | var results: [UInt8] = [] 72 | var hasMore: Bool 73 | var needPad: Bool { 74 | guard let padTo = padTo else { return false } 75 | return length < padTo 76 | } 77 | repeat { 78 | var byte = UInt8(value & 0x7F) 79 | value >>= 7 80 | length += 1 81 | hasMore = !((value == 0 && (byte & 0x40) == 0) || (value == -1 && (byte & 0x40) != 0)) 82 | if hasMore || needPad { 83 | byte |= 0x80 84 | } 85 | results.append(byte) 86 | } while hasMore 87 | 88 | if let padTo = padTo, length < padTo { 89 | let padValue: UInt8 = value < 0 ? 0x7F : 0x00 90 | while length < padTo - 1 { 91 | results.append(padValue | 0x80) 92 | length += 1 93 | } 94 | results.append(padValue) 95 | } 96 | return results 97 | } 98 | -------------------------------------------------------------------------------- /Sources/WasmTransformer/OutputWriter.swift: -------------------------------------------------------------------------------- 1 | public protocol OutputWriter { 2 | func writeByte(_ byte: UInt8) throws 3 | func writeBytes(_ bytes: S) throws where S.Element == UInt8 4 | } 5 | 6 | extension OutputWriter { 7 | func writeString(_ value: String) throws { 8 | let bytes = value.utf8 9 | try writeBytes(encodeULEB128(UInt32(bytes.count))) 10 | try writeBytes(bytes) 11 | } 12 | 13 | /// https://webassembly.github.io/spec/core/binary/types.html#result-types 14 | func writeResultTypes(_ types: [ValueType]) throws { 15 | try writeBytes(encodeULEB128(UInt32(types.count))) 16 | for type in types { 17 | try writeByte(type.rawValue) 18 | } 19 | } 20 | 21 | mutating func writeSection(_ type: SectionType, bodyWriter: (inout InMemoryOutputWriter) throws -> T) throws -> T { 22 | try writeByte(type.rawValue) 23 | var buffer = InMemoryOutputWriter() 24 | let result = try bodyWriter(&buffer) 25 | try writeBytes(encodeULEB128(UInt32(buffer.bytes().count))) 26 | try writeBytes(buffer.bytes()) 27 | return result 28 | } 29 | 30 | mutating func writeVectorSection( 31 | type: SectionType, 32 | reader: Reader, extras: [Reader.Item] = [] 33 | ) throws where Reader.Item: ByteEncodable { 34 | let count = reader.count + UInt32(extras.count) 35 | try self.writeSection(type) { buffer in 36 | try buffer.writeBytes(encodeULEB128(count)) 37 | for result in reader { 38 | let entry = try result.get() 39 | try entry.encode(to: &buffer) 40 | } 41 | for extra in extras { 42 | try extra.encode(to: &buffer) 43 | } 44 | } 45 | } 46 | 47 | mutating func writeVectorSection( 48 | type: SectionType, 49 | count: UInt32, 50 | writeItems: (inout InMemoryOutputWriter) throws -> Void 51 | ) throws { 52 | try self.writeSection(type) { buffer in 53 | try buffer.writeBytes(encodeULEB128(count)) 54 | try writeItems(&buffer) 55 | } 56 | } 57 | 58 | mutating func writeVectorSection( 59 | type: SectionType, 60 | items: [Item] = [] 61 | ) throws { 62 | let count = UInt32(items.count) 63 | try self.writeSection(type) { buffer in 64 | try buffer.writeBytes(encodeULEB128(count)) 65 | for extra in items { 66 | try extra.encode(to: &buffer) 67 | } 68 | } 69 | } 70 | } 71 | 72 | public class InMemoryOutputWriter: OutputWriter { 73 | private var _bytes: [UInt8] = [] 74 | 75 | public init(reservingCapacity capacity: Int = 0) { 76 | _bytes.reserveCapacity(capacity) 77 | } 78 | 79 | public func writeByte(_ byte: UInt8) throws { 80 | _bytes.append(byte) 81 | } 82 | 83 | public func writeBytes(_ newBytes: S) throws where S : Sequence, S.Element == UInt8 { 84 | _bytes.append(contentsOf: newBytes) 85 | } 86 | 87 | public func bytes() -> [UInt8] { _bytes } 88 | } 89 | -------------------------------------------------------------------------------- /Sources/WasmTransformer/Readers/CodeSectionReader.swift: -------------------------------------------------------------------------------- 1 | public struct FunctionBody { 2 | public internal(set) var input: InputByteStream 3 | public let size: UInt32 4 | public let endOffset: Int 5 | 6 | func locals() -> LocalsReader { 7 | LocalsReader(input: input) 8 | } 9 | } 10 | 11 | struct LocalsReader { 12 | var input: InputByteStream 13 | let count: UInt32 14 | 15 | init(input: InputByteStream) { 16 | self.input = input 17 | self.count = self.input.readVarUInt32() 18 | } 19 | 20 | mutating func read() throws -> (count: UInt32, rawBytes: ArraySlice) { 21 | input.consumeLocal() 22 | } 23 | 24 | func operators() -> InputByteStream { 25 | input 26 | } 27 | } 28 | 29 | public struct CodeSectionReader: VectorSectionReader { 30 | var input: InputByteStream 31 | public let count: UInt32 32 | 33 | init(input: InputByteStream) { 34 | self.input = input 35 | self.count = self.input.readVarUInt32() 36 | } 37 | 38 | public mutating func read() throws -> FunctionBody { 39 | let size = input.readVarUInt32() 40 | let body = FunctionBody(input: input, size: size, 41 | endOffset: input.offset + Int(size)) 42 | input.skip(Int(size)) 43 | return body 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/WasmTransformer/Readers/ElementSectionReader.swift: -------------------------------------------------------------------------------- 1 | public struct ElementSegment { 2 | let flags: UInt32 3 | let initExpr: ArraySlice 4 | var items: ElementItemsReader 5 | } 6 | 7 | struct FunctionIndex: Equatable { 8 | let value: UInt32 9 | } 10 | 11 | struct ElementItemsReader { 12 | var input: InputByteStream 13 | let count: UInt32 14 | 15 | mutating func read() -> FunctionIndex { 16 | FunctionIndex(value: input.readVarUInt32()) 17 | } 18 | } 19 | 20 | public struct ElementSectionReader: VectorSectionReader { 21 | var input: InputByteStream 22 | public let count: UInt32 23 | 24 | init(input: InputByteStream) { 25 | self.input = input 26 | self.count = self.input.readVarUInt32() 27 | } 28 | 29 | public mutating func read() throws -> ElementSegment { 30 | let flags = input.readVarUInt32() 31 | let initExpr = try input.consumeI32InitExpr() 32 | let count = input.readVarUInt32() 33 | return ElementSegment( 34 | flags: flags, 35 | initExpr: initExpr, 36 | items: ElementItemsReader(input: input, count: count) 37 | ) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/WasmTransformer/Readers/FunctionSectionReader.swift: -------------------------------------------------------------------------------- 1 | public struct SignatureIndex: Equatable { 2 | public let value: UInt32 3 | } 4 | 5 | public struct FunctionSectionReader: VectorSectionReader { 6 | var input: InputByteStream 7 | public let count: UInt32 8 | 9 | init(input: InputByteStream) { 10 | self.input = input 11 | self.count = self.input.readVarUInt32() 12 | } 13 | 14 | public mutating func read() throws -> SignatureIndex { 15 | return SignatureIndex(value: input.readVarUInt32()) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/WasmTransformer/Readers/ImportSectionReader.swift: -------------------------------------------------------------------------------- 1 | public struct Import { 2 | public var module: String 3 | public var field: String 4 | public var descriptor: ImportDescriptor 5 | } 6 | 7 | public enum ImportDescriptor { 8 | case function(UInt32) 9 | case table(rawBytes: ArraySlice) 10 | case memory(rawBytes: ArraySlice) 11 | case global(rawBytes: ArraySlice) 12 | } 13 | 14 | public struct ImportSectionReader: VectorSectionReader { 15 | var input: InputByteStream 16 | public let count: UInt32 17 | 18 | init(input: InputByteStream) { 19 | self.input = input 20 | self.count = self.input.readVarUInt32() 21 | } 22 | 23 | public mutating func read() throws -> Import { 24 | try input.readImport() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/WasmTransformer/Readers/ModuleReader.swift: -------------------------------------------------------------------------------- 1 | public enum ModuleSection { 2 | case type(TypeSectionReader) 3 | case `import`(ImportSectionReader) 4 | case function(FunctionSectionReader) 5 | case element(ElementSectionReader) 6 | case code(CodeSectionReader) 7 | case rawSection(type: SectionType, content: ArraySlice) 8 | } 9 | 10 | public struct ModuleReader { 11 | enum Error: Swift.Error { 12 | case invalidMagic 13 | } 14 | 15 | var input: InputByteStream 16 | 17 | public init(input: InputByteStream) { 18 | self.input = input 19 | } 20 | 21 | public var isEOF: Bool { input.isEOF } 22 | 23 | mutating func readHeader() throws -> ArraySlice { 24 | return try input.readHeader() 25 | } 26 | 27 | public mutating func readSection() throws -> ModuleSection { 28 | let sectionInfo = try input.readSectionInfo() 29 | defer { 30 | input.seek(sectionInfo.endOffset) 31 | } 32 | switch sectionInfo.type { 33 | case .type: 34 | return .type(TypeSectionReader(input: input)) 35 | case .import: 36 | return .import(ImportSectionReader(input: input)) 37 | case .function: 38 | return .function(FunctionSectionReader(input: input)) 39 | case .element: 40 | return .element(ElementSectionReader(input: input)) 41 | case .code: 42 | return .code(CodeSectionReader(input: input)) 43 | default: 44 | return .rawSection( 45 | type: sectionInfo.type, 46 | content: input.bytes[sectionInfo.contentRange] 47 | ) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/WasmTransformer/Readers/TypeSectionReader.swift: -------------------------------------------------------------------------------- 1 | public struct TypeSectionReader: VectorSectionReader { 2 | enum Error: Swift.Error { 3 | case unsupportedTypeDefKind(UInt8) 4 | } 5 | 6 | var input: InputByteStream 7 | public let count: UInt32 8 | 9 | init(input: InputByteStream) { 10 | self.input = input 11 | count = self.input.readVarUInt32() 12 | } 13 | 14 | public mutating func read() throws -> FuncSignature { 15 | let rawKind = input.readUInt8() 16 | switch rawKind { 17 | case 0x60: 18 | return try input.readFuncType() 19 | default: 20 | throw Error.unsupportedTypeDefKind(rawKind) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/WasmTransformer/Readers/VectorSectionReader.swift: -------------------------------------------------------------------------------- 1 | public protocol VectorSectionReader: Sequence where Element == Result { 2 | associatedtype Item 3 | var count: UInt32 { get } 4 | mutating func read() throws -> Item 5 | } 6 | 7 | public struct VectorSectionIterator: IteratorProtocol { 8 | private(set) var reader: Reader 9 | private(set) var left: UInt32 10 | init(reader: Reader, count: UInt32) { 11 | self.reader = reader 12 | self.left = count 13 | } 14 | private var end: Bool = false 15 | public mutating func next() -> Reader.Element? { 16 | guard !end else { return nil } 17 | guard left != 0 else { return nil } 18 | let result = Result(catching: { try reader.read() }) 19 | left -= 1 20 | switch result { 21 | case .success: return result 22 | case .failure: 23 | end = true 24 | return result 25 | } 26 | } 27 | } 28 | 29 | extension VectorSectionReader where Iterator == VectorSectionIterator { 30 | __consuming public func makeIterator() -> VectorSectionIterator { 31 | VectorSectionIterator(reader: self, count: count) 32 | } 33 | 34 | public func collect() throws -> [Item] { 35 | var items: [Item] = [] 36 | items.reserveCapacity(Int(count)) 37 | for result in self { 38 | try items.append(result.get()) 39 | } 40 | return items 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/WasmTransformer/Sections.swift: -------------------------------------------------------------------------------- 1 | public struct SectionInfo: Equatable { 2 | public let startOffset: Int 3 | public let contentStart: Int 4 | public var endOffset: Int { 5 | contentStart + size 6 | } 7 | public var contentRange: Range { 8 | contentStart.. (Trampoline, Int)? { 40 | trampolineByBaseFuncIndex[index] 41 | } 42 | 43 | typealias Iterator = Array.Iterator 44 | func makeIterator() -> Iterator { 45 | trampolines.makeIterator() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/WasmTransformer/Transformers/CustomSectionStripper.swift: -------------------------------------------------------------------------------- 1 | 2 | public struct CustomSectionStripper: Transformer { 3 | 4 | private let stripIf: (_ name: String) -> Bool 5 | 6 | /// Create a new transformer of `CustomSectionStripper` 7 | /// - Parameter stripIf: The closure accepting the custom section name and 8 | /// returning `true` if it should strip the section. 9 | public init( 10 | stripIf: @escaping (_ name: String) -> Bool = { _ in true } 11 | ) { 12 | self.stripIf = stripIf 13 | } 14 | 15 | public let metadata = TransformerMetadata( 16 | name: "strip-custom-section", description: "Strips custom sections from the wasm module" 17 | ) 18 | 19 | public func transform(_ input: inout InputByteStream, writer: inout Writer) throws { 20 | let version = try input.readHeader() 21 | try writer.writeBytes(magic) 22 | try writer.writeBytes(version) 23 | 24 | while !input.isEOF { 25 | let section = try input.readSectionInfo() 26 | 27 | let shouldStrip: Bool 28 | if section.type == .custom { 29 | let name = input.readString() 30 | shouldStrip = stripIf(name) 31 | } else { 32 | shouldStrip = false 33 | } 34 | input.seek(section.endOffset) 35 | if !shouldStrip { 36 | try writer.writeBytes(input.bytes[section.startOffset.. Bool 11 | 12 | public init(shouldLower: @escaping (Import) -> Bool = { _ in true }) { 13 | self.shouldLower = shouldLower 14 | } 15 | 16 | public let metadata = TransformerMetadata( 17 | name: "i64-to-i32-lowering", 18 | description: "Replaces all i64 imports with i32 imports" 19 | ) 20 | 21 | public func transform( 22 | _ input: inout InputByteStream, 23 | writer: inout Writer 24 | ) throws { 25 | var moduleReader = ModuleReader(input: input) 26 | let version = try moduleReader.readHeader() 27 | try writer.writeBytes(magic) 28 | try writer.writeBytes(version) 29 | 30 | var importedFunctionCount = 0 31 | var trampolines = Trampolines() 32 | do { 33 | // Phase 1. Scan Type and Import sections to determine import records 34 | // which will be lowered. 35 | var copyingSections: [(type: SectionType, content: ArraySlice)] = [] 36 | var typeSection: [FuncSignature] = [] 37 | var replacements: [ImportFuncReplacement] = [] 38 | var importSection: ImportSectionReader? 39 | Phase1: while !moduleReader.isEOF { 40 | switch try moduleReader.readSection() { 41 | case .type(let reader): 42 | typeSection = try reader.collect() 43 | case .import(var reader): 44 | importSection = reader 45 | importedFunctionCount = try scan( 46 | importSection: &reader, 47 | typeSection: &typeSection, replacements: &replacements, 48 | trampolines: &trampolines 49 | ) 50 | break Phase1 51 | case .rawSection(type: .custom, content: let content): 52 | copyingSections.append((type: .custom, content: content)) 53 | case .element: 54 | throw Error.unexpectedSection(SectionType.element.rawValue) 55 | case .function(_): 56 | throw Error.unexpectedSection(SectionType.function.rawValue) 57 | case .code(_): 58 | throw Error.unexpectedSection(SectionType.code.rawValue) 59 | case .rawSection(let type, _): 60 | throw Error.unexpectedSection(type.rawValue) 61 | } 62 | } 63 | 64 | // Phase 2. Write out Type and Import section based on scanned results. 65 | try writer.writeVectorSection(type: .type, items: typeSection) 66 | if var importSection = importSection { 67 | try writer.writeVectorSection(type: .import, count: importSection.count) { writer in 68 | for index in 0 ..< importSection.count { 69 | var entry = try importSection.read() 70 | switch entry.descriptor { 71 | case .function: 72 | if let replacement = replacements.first(where: { $0.index == index }) { 73 | entry.descriptor = .function(UInt32(replacement.toTypeIndex)) 74 | } 75 | default: break 76 | } 77 | try entry.encode(to: &writer) 78 | } 79 | } 80 | } 81 | 82 | for section in copyingSections { 83 | try writer.writeSection(section.type) { buffer in 84 | try buffer.writeBytes(section.content) 85 | } 86 | } 87 | } 88 | 89 | // After here, we can emit binary sequentially 90 | 91 | var originalFuncCount: Int? 92 | while !moduleReader.isEOF { 93 | switch try moduleReader.readSection() { 94 | case .type, .import: 95 | fatalError("unreachable") 96 | case .function(let reader): 97 | // Phase 3. Write out Func section and add trampoline signatures. 98 | try writer.writeVectorSection(type: .function, reader: reader, extras: trampolines.map { 99 | SignatureIndex(value: UInt32($0.fromSignatureIndex)) 100 | }) 101 | originalFuncCount = Int(reader.count) + importedFunctionCount 102 | case .element(var reader): 103 | // Phase 4. Read Elem section and rewrite i64 functions with trampoline functions. 104 | guard let originalFuncCount = originalFuncCount else { 105 | throw Error.expectFunctionSection 106 | } 107 | try transformElemSection( 108 | input: &reader, writer: &writer, 109 | trampolines: trampolines, originalFuncCount: originalFuncCount 110 | ) 111 | case .code(var reader): 112 | // Phase 5. Read Code section and rewrite i64 function calls with trampoline function call. 113 | // And add trampoline functions at the tail 114 | guard let originalFuncCount = originalFuncCount else { 115 | throw Error.expectFunctionSection 116 | } 117 | try transformCodeSection( 118 | input: &reader, writer: &writer, 119 | trampolines: trampolines, originalFuncCount: originalFuncCount 120 | ) 121 | case .rawSection(let type, let content): 122 | try writer.writeSection(type) { buffer in 123 | try buffer.writeBytes(content) 124 | } 125 | } 126 | } 127 | } 128 | 129 | /// https://webassembly.github.io/spec/core/binary/modules.html#import-section 130 | /// Returns a count of imported functions 131 | func scan(importSection: inout ImportSectionReader, 132 | typeSection: inout [FuncSignature], 133 | replacements: inout [ImportFuncReplacement], 134 | trampolines: inout Trampolines) throws -> Int 135 | { 136 | var importFuncCount = 0 137 | for (index, result) in importSection.enumerated() { 138 | let entry = try result.get() 139 | switch entry.descriptor { 140 | case .function(let sigIndex): 141 | let signatureIndex = Int(sigIndex) 142 | let signature = typeSection[signatureIndex] 143 | defer { importFuncCount += 1 } 144 | guard signature.hasI64Param(), shouldLower(entry) else { continue } 145 | let toTypeIndex = typeSection.count 146 | let toSignature = signature.lowered() 147 | typeSection.append(toSignature) 148 | replacements.append( 149 | (index: Int(index), toTypeIndex: toTypeIndex) 150 | ) 151 | trampolines.add( 152 | importIndex: importFuncCount, from: signature, 153 | fromIndex: signatureIndex, to: toSignature 154 | ) 155 | case .table, .memory, .global: break 156 | } 157 | } 158 | return importFuncCount 159 | } 160 | } 161 | 162 | private func transformCodeSection( 163 | input: inout CodeSectionReader, writer: inout Writer, 164 | trampolines: Trampolines, originalFuncCount: Int) throws 165 | { 166 | let count = input.count 167 | let newCount = count + UInt32(trampolines.count) 168 | try writer.writeVectorSection(type: .code, count: newCount) { writer in 169 | for _ in 0 ..< input.count { 170 | let body = try input.read() 171 | let bodyBuffer = try replaceFunctionCall(body: body) { funcIndex in 172 | guard let (_, trampolineIndex) = trampolines.trampoline(byBaseFuncIndex: Int(funcIndex)) else { return nil } 173 | return UInt32(originalFuncCount + trampolineIndex) 174 | } 175 | let newSize = bodyBuffer.count 176 | try writer.writeBytes(encodeULEB128(UInt32(newSize))) 177 | try writer.writeBytes(bodyBuffer) 178 | } 179 | for trampoline in trampolines { 180 | try trampoline.write(to: writer) 181 | } 182 | } 183 | } 184 | private func replaceFunctionCall( 185 | body: FunctionBody, 186 | replace: (_ funcIndex: UInt32) -> UInt32? 187 | ) throws -> [UInt8] { 188 | var locals = body.locals() 189 | var bodyBuffer: [UInt8] = [] 190 | bodyBuffer.reserveCapacity(Int(body.size)) 191 | 192 | bodyBuffer.append(contentsOf: encodeULEB128(locals.count)) 193 | for _ in 0 ..< locals.count { 194 | try bodyBuffer.append(contentsOf: locals.read().rawBytes) 195 | } 196 | 197 | var operators = locals.operators() 198 | var nonCallInstStart = operators.offset 199 | while operators.offset < body.endOffset { 200 | let nonCallInstEnd = operators.offset 201 | guard let funcIndex = try operators.readCallInst(), 202 | let newFuncIndex = replace(funcIndex) else { 203 | continue 204 | } 205 | bodyBuffer.append(contentsOf: operators.bytes[nonCallInstStart..( 217 | input: inout ElementSectionReader, writer: inout Writer, 218 | trampolines: Trampolines, originalFuncCount: Int 219 | ) throws { 220 | try writer.writeVectorSection(type: .element, count: input.count) { writer in 221 | for result in input { 222 | var entry = try result.get() 223 | try writer.writeBytes(encodeULEB128(entry.flags)) 224 | try writer.writeBytes(entry.initExpr) 225 | try writer.writeBytes(encodeULEB128(entry.items.count)) 226 | for _ in 0.. FuncSignature { 240 | func transform(_ type: ValueType) -> ValueType { 241 | if case .i64 = type { return .i32 } 242 | else { return type } 243 | } 244 | return FuncSignature( 245 | params: params.map(transform), 246 | results: results 247 | ) 248 | } 249 | 250 | func hasI64Param() -> Bool { 251 | for param in params { 252 | if param == ValueType.i64 { 253 | return true 254 | } 255 | } 256 | return false 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /Sources/WasmTransformer/Transformers/SizeProfiler.swift: -------------------------------------------------------------------------------- 1 | public extension InputByteStream { 2 | mutating func readSectionsInfo() throws -> [SectionInfo] { 3 | precondition(offset == bytes.startIndex) 4 | _ = try readHeader() 5 | 6 | var result = [SectionInfo]() 7 | while !isEOF { 8 | let section = try readSectionInfo() 9 | result.append(section) 10 | skip(section.size) 11 | 12 | } 13 | return result 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/WasmTransformer/Transformers/StackOverflowSanitizer+Fixtures.swift: -------------------------------------------------------------------------------- 1 | // GENERATED FROM Tools/GenerateStackOverflowSanitizerSupport.swift 2 | 3 | extension StackOverflowSanitizer { 4 | public static let supportObjectFile: [UInt8] = [ 5 | 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x84, 0x80, 0x80, 0x80, 0x00, 0x01, 0x60, 0x00, 0x00, 0x02, 0xc2, 0x80, 0x80, 0x80, 0x00, 0x02, 0x03, 0x65, 0x6e, 0x76, 0x0f, 0x5f, 0x5f, 0x6c, 0x69, 0x6e, 0x65, 0x61, 0x72, 0x5f, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x02, 0x00, 0x00, 0x11, 0x5f, 0x5f, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x5f, 0x73, 0x61, 0x6e, 0x69, 0x74, 0x69, 0x7a, 0x65, 0x72, 0x15, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x5f, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x5f, 0x6f, 0x76, 0x65, 0x72, 0x66, 0x6c, 0x6f, 0x77, 0x00, 0x00, 0x03, 0x82, 0x80, 0x80, 0x80, 0x00, 0x01, 0x00, 0x0a, 0x8b, 0x80, 0x80, 0x80, 0x00, 0x01, 0x09, 0x00, 0x10, 0x80, 0x80, 0x80, 0x80, 0x00, 0x0f, 0x0b, 0x00, 0xc6, 0x80, 0x80, 0x80, 0x00, 0x07, 0x6c, 0x69, 0x6e, 0x6b, 0x69, 0x6e, 0x67, 0x02, 0x08, 0xb7, 0x80, 0x80, 0x80, 0x00, 0x02, 0x00, 0x04, 0x01, 0x07, 0x5f, 0x5f, 0x64, 0x75, 0x6d, 0x6d, 0x79, 0x00, 0x50, 0x00, 0x27, 0x5f, 0x5f, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x5f, 0x73, 0x61, 0x6e, 0x69, 0x74, 0x69, 0x7a, 0x65, 0x72, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x5f, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x5f, 0x6f, 0x76, 0x65, 0x72, 0x66, 0x6c, 0x6f, 0x77, 0x00, 0x90, 0x80, 0x80, 0x80, 0x00, 0x0a, 0x72, 0x65, 0x6c, 0x6f, 0x63, 0x2e, 0x43, 0x4f, 0x44, 0x45, 0x03, 0x01, 0x00, 0x04, 0x01, 0x00, 0x89, 0x81, 0x80, 0x80, 0x00, 0x09, 0x70, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x65, 0x72, 0x73, 0x01, 0x0c, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x65, 0x64, 0x2d, 0x62, 0x79, 0x01, 0x0f, 0x53, 0x77, 0x69, 0x66, 0x74, 0x57, 0x61, 0x73, 0x6d, 0x20, 0x63, 0x6c, 0x61, 0x6e, 0x67, 0x5f, 0x31, 0x30, 0x2e, 0x30, 0x2e, 0x30, 0x20, 0x28, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x73, 0x77, 0x69, 0x66, 0x74, 0x77, 0x61, 0x73, 0x6d, 0x2f, 0x6c, 0x6c, 0x76, 0x6d, 0x2d, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x2e, 0x67, 0x69, 0x74, 0x20, 0x31, 0x36, 0x35, 0x64, 0x38, 0x61, 0x62, 0x32, 0x64, 0x31, 0x39, 0x62, 0x65, 0x63, 0x63, 0x30, 0x64, 0x39, 0x61, 0x39, 0x63, 0x62, 0x33, 0x31, 0x39, 0x32, 0x39, 0x65, 0x30, 0x39, 0x36, 0x63, 0x38, 0x30, 0x36, 0x37, 0x34, 0x36, 0x65, 0x38, 0x29 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /Sources/WasmTransformer/Transformers/StackOverflowSanitizer.swift: -------------------------------------------------------------------------------- 1 | 2 | public struct StackOverflowSanitizer: Transformer { 3 | enum Error: Swift.Error { 4 | case expectFunctionSection 5 | case supportLibraryNotLinked 6 | case expectTypeSection 7 | case invalidFunctionIndex 8 | case invalidTypeIndex 9 | } 10 | 11 | public init() {} 12 | 13 | public let metadata = TransformerMetadata( 14 | name: "stack-sanitizer", 15 | description: "Sanitize stack overflow assuming --stack-first and stack pointer is placed at globals[0]" 16 | ) 17 | 18 | public func transform(_ input: inout InputByteStream, writer: inout Writer) throws { 19 | let version = try input.readHeader() 20 | try writer.writeBytes(magic) 21 | try writer.writeBytes(version) 22 | var importCount: UInt32? 23 | var reportFuncIndex: UInt32? 24 | var assertSigIndex: UInt32? 25 | var assertFuncIndex: UInt32? 26 | 27 | while !input.isEOF { 28 | let sectionInfo = try input.readSectionInfo() 29 | 30 | switch sectionInfo.type { 31 | case .type: 32 | let reader = TypeSectionReader(input: input) 33 | assertSigIndex = reader.count 34 | let assertSignature = FuncSignature( 35 | params: [.i32], results: [.i32] 36 | ) 37 | try writer.writeVectorSection( 38 | type: .type, reader: reader, extras: [assertSignature] 39 | ) 40 | case .import: 41 | let reader = ImportSectionReader(input: input) 42 | importCount = reader.count 43 | let entries = try reader.lazy.map { try $0.get() } 44 | .filter { 45 | guard case .function(_) = $0.descriptor else { return false } 46 | return true 47 | } 48 | reportFuncIndex = entries.firstIndex(where: { 49 | return $0.module == "__stack_sanitizer" && $0.field == "report_stack_overflow" 50 | }) 51 | .map(UInt32.init) 52 | 53 | try writer.writeBytes( 54 | input.bytes[sectionInfo.startOffset ..< sectionInfo.endOffset] 55 | ) 56 | case .function: 57 | let reader = FunctionSectionReader(input: input) 58 | assertFuncIndex = (importCount ?? 0) + reader.count 59 | guard let assertSigIndex = assertSigIndex else { 60 | throw Error.expectTypeSection 61 | } 62 | try writer.writeVectorSection( 63 | type: .function, reader: reader, extras: [ 64 | SignatureIndex(value: assertSigIndex) 65 | ] 66 | ) 67 | case .code: 68 | guard let reportFuncIndex = reportFuncIndex else { 69 | throw Error.supportLibraryNotLinked 70 | } 71 | guard let assertFuncIndex = assertFuncIndex else { 72 | throw Error.expectFunctionSection 73 | } 74 | var reader = CodeSectionReader(input: input) 75 | try transformCodeSection( 76 | input: &reader, writer: &writer, 77 | reportFuncIndex: reportFuncIndex, 78 | assertFuncIndex: assertFuncIndex 79 | ) 80 | default: 81 | try writer.writeBytes( 82 | input.bytes[sectionInfo.startOffset ..< sectionInfo.endOffset] 83 | ) 84 | } 85 | input.skip(sectionInfo.size) 86 | } 87 | } 88 | 89 | func transformCodeSection( 90 | input: inout CodeSectionReader, 91 | writer: inout Writer, 92 | reportFuncIndex: UInt32, 93 | assertFuncIndex: UInt32 94 | ) throws { 95 | try writer.writeVectorSection(type: .code, count: input.count + 1) { writer in 96 | for _ in 0 ..< input.count { 97 | let body = try input.read() 98 | try transformFunction( 99 | input: body, writer: writer, 100 | assertFuncIndex: assertFuncIndex 101 | ) 102 | } 103 | try emitStackPointerAssert(reportFuncIndex: reportFuncIndex, writer: writer) 104 | } 105 | } 106 | 107 | func emitStackPointerAssert(reportFuncIndex: UInt32, writer: OutputWriter) throws { 108 | var bodyBuffer: [UInt8] = [] 109 | bodyBuffer.append(0x00) // local decl count 110 | let opcode = [ 111 | Opcode.localGet(0), 112 | Opcode.i32Const(0), 113 | Opcode.i32LtS, 114 | Opcode.if(.empty), 115 | Opcode.call(reportFuncIndex), 116 | Opcode.end, 117 | Opcode.localGet(0), 118 | Opcode.end, 119 | ] 120 | bodyBuffer.append(contentsOf: opcode.flatMap { $0.serialize() }) 121 | try writer.writeBytes(encodeULEB128(UInt32(bodyBuffer.count))) 122 | try writer.writeBytes(bodyBuffer) 123 | } 124 | 125 | func transformFunction(input: FunctionBody, writer: OutputWriter, 126 | assertFuncIndex: UInt32) throws { 127 | let oldSize = Int(input.size) 128 | var bodyBuffer: [UInt8] = [] 129 | bodyBuffer.reserveCapacity(oldSize) 130 | 131 | var locals = input.locals() 132 | 133 | bodyBuffer.append(contentsOf: encodeULEB128(locals.count)) 134 | for _ in 0 ..< locals.count { 135 | try bodyBuffer.append(contentsOf: locals.read().rawBytes) 136 | } 137 | 138 | var operators = locals.operators() 139 | var lazyChunkStart = operators.offset 140 | while operators.offset < input.endOffset { 141 | let lazyChunkEnd = operators.offset 142 | let rawCode = operators.readUInt8() 143 | func flushLazyChunk() { 144 | bodyBuffer.append(contentsOf: operators.bytes[lazyChunkStart..(_ input: inout InputByteStream, writer: inout Writer) throws 9 | } 10 | 11 | public func lowerI64Imports( 12 | _ input: [UInt8], 13 | shouldLower: @escaping (Import) -> Bool = { _ in true } 14 | ) throws -> [UInt8] { 15 | let transformer = I64ImportTransformer(shouldLower: shouldLower) 16 | var inputStream = InputByteStream(bytes: input) 17 | var writer = InMemoryOutputWriter() 18 | try transformer.transform(&inputStream, writer: &writer) 19 | return writer.bytes() 20 | } 21 | 22 | public func stripCustomSections(_ input: [UInt8]) throws -> [UInt8] { 23 | let transformer = CustomSectionStripper() 24 | var inputStream = InputByteStream(bytes: input) 25 | var writer = InMemoryOutputWriter() 26 | try transformer.transform(&inputStream, writer: &writer) 27 | return writer.bytes() 28 | } 29 | -------------------------------------------------------------------------------- /Sources/wasm-trans/main.swift: -------------------------------------------------------------------------------- 1 | import WasmTransformer 2 | import ArgumentParser 3 | import Foundation 4 | 5 | struct TransformerOption: EnumerableFlag, CustomStringConvertible { 6 | let transformer: Transformer 7 | 8 | var description: String { 9 | transformer.metadata.name 10 | } 11 | 12 | static func name(for value: TransformerOption) -> NameSpecification { 13 | .long 14 | } 15 | 16 | static func help(for value: TransformerOption) -> ArgumentHelp? { 17 | ArgumentHelp(value.transformer.metadata.description) 18 | } 19 | 20 | static var allCases: [TransformerOption] { 21 | [ 22 | I64ImportTransformer(), 23 | CustomSectionStripper(), 24 | StackOverflowSanitizer() 25 | ] 26 | .map(TransformerOption.init(transformer: )) 27 | } 28 | 29 | static func == (lhs: TransformerOption, rhs: TransformerOption) -> Bool { 30 | return lhs.description == rhs.description 31 | } 32 | } 33 | 34 | struct WasmTrans: ParsableCommand { 35 | 36 | @Argument 37 | var input: String 38 | @Option(name: .shortAndLong, help: "Output file") 39 | var output: String 40 | 41 | @Flag 42 | var passes: [TransformerOption] = [] 43 | 44 | mutating func run() throws { 45 | var nextBytes = try Array(Data(contentsOf: URL(fileURLWithPath: input))) 46 | for pass in passes { 47 | var inputStream = InputByteStream(bytes: nextBytes) 48 | var writer = InMemoryOutputWriter(reservingCapacity: nextBytes.count) 49 | try pass.transformer.transform(&inputStream, writer: &writer) 50 | nextBytes = writer.bytes() 51 | } 52 | try Data(nextBytes).write(to: URL(fileURLWithPath: output)) 53 | } 54 | } 55 | 56 | WasmTrans.main() 57 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | fatalError("Use `swift test --enable-test-discovery` to run tests") 2 | -------------------------------------------------------------------------------- /Tests/WasmTransformerTests/CustomSectionStripperTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import WasmTransformer 3 | 4 | private func transformWat(_ input: String, stripIf: @escaping (_ name: String) -> Bool = { _ in true }) throws -> URL { 5 | let inputWasm = compileWat(input, options: ["--debug-names"]) 6 | let transformer = CustomSectionStripper(stripIf: stripIf) 7 | var inputStream = try InputByteStream(from: inputWasm) 8 | var writer = InMemoryOutputWriter() 9 | try transformer.transform(&inputStream, writer: &writer) 10 | 11 | let (url, handle) = makeTemporaryFile() 12 | handle.write(Data(writer.bytes())) 13 | return url 14 | } 15 | 16 | final class CustomSectionStripperTests: XCTestCase { 17 | static let wat = """ 18 | (module 19 | (func $add (result i32) 20 | (i32.add 21 | (i32.const 1) 22 | (i32.const 1) 23 | ) 24 | ) 25 | ) 26 | """ 27 | func testStripDebugSection() throws { 28 | let wat = Self.wat 29 | do { 30 | let original = compileWat(wat, options: ["--debug-names"]) 31 | let output = wasmObjdump(original, args: ["--header"]) 32 | let expectedCustomSection = #"Custom start=0x00000020 end=0x00000032 (size=0x00000012) "name""# 33 | XCTAssertTrue(output.contains(expectedCustomSection)) 34 | } 35 | do { 36 | let url = try transformWat(wat) 37 | let output = wasmObjdump(url, args: ["--header"]) 38 | XCTAssertFalse(output.contains("Custom start=")) 39 | } 40 | } 41 | 42 | func testStripIf() throws { 43 | let wat = Self.wat 44 | do { 45 | let original = compileWat(wat, options: ["--debug-names"]) 46 | let output = wasmObjdump(original, args: ["--header"]) 47 | let expectedCustomSection = #"Custom start=0x00000020 end=0x00000032 (size=0x00000012) "name""# 48 | XCTAssertTrue(output.contains(expectedCustomSection)) 49 | } 50 | do { 51 | let url = try transformWat(wat, stripIf: { $0 != "name" }) 52 | let output = wasmObjdump(url, args: ["--header"]) 53 | XCTAssertTrue(output.contains("Custom start=")) 54 | } 55 | do { 56 | let url = try transformWat(wat, stripIf: { $0 == "name" }) 57 | let output = wasmObjdump(url, args: ["--header"]) 58 | XCTAssertFalse(output.contains("Custom start=")) 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Tests/WasmTransformerTests/I64ImportTransformerTests.swift: -------------------------------------------------------------------------------- 1 | @testable import WasmTransformer 2 | import XCTest 3 | 4 | private func transformWat(_ input: String, shouldLower: @escaping (Import) -> Bool = { _ in true }) throws -> URL { 5 | let inputWasm = compileWat(input) 6 | let transformer = I64ImportTransformer(shouldLower: shouldLower) 7 | var inputStream = try InputByteStream(from: inputWasm) 8 | var writer = InMemoryOutputWriter() 9 | try transformer.transform(&inputStream, writer: &writer) 10 | 11 | let (url, handle) = makeTemporaryFile() 12 | handle.write(Data(writer.bytes())) 13 | return url 14 | } 15 | 16 | final class I64ImportTransformerTests: XCTestCase { 17 | 18 | func testI64ParamsImport() throws { 19 | let wat = """ 20 | (module 21 | (import "foo" "bar" (func (param i64))) 22 | ) 23 | """ 24 | let url = try transformWat(wat) 25 | let output = wasmObjdump(url, args: ["--details"]) 26 | let expectedTypes = """ 27 | Type[2]: 28 | - type[0] (i64) -> nil 29 | - type[1] (i32) -> nil 30 | """ 31 | XCTAssertTrue(output.contains(expectedTypes)) 32 | let expectedImport = """ 33 | Import[1]: 34 | - func[0] sig=1 <- foo.bar 35 | """ 36 | XCTAssertContains(output, contains: expectedImport) 37 | } 38 | 39 | func testI64ResultImport() throws { 40 | let wat = """ 41 | (module 42 | (import "foo" "bar" (func (result i64))) 43 | ) 44 | """ 45 | let url = try transformWat(wat) 46 | let output = wasmObjdump(url, args: ["--details"]) 47 | // No transformation happens since small to large cast is valid without BigInt support 48 | let expectedTypes = """ 49 | Type[1]: 50 | - type[0] () -> i64 51 | """ 52 | XCTAssertTrue(output.contains(expectedTypes)) 53 | let expectedImport = """ 54 | Import[1]: 55 | - func[0] sig=0 <- foo.bar 56 | """ 57 | XCTAssertContains(output, contains: expectedImport) 58 | } 59 | 60 | func testSelectLoweringImport() throws { 61 | let wat = """ 62 | (module 63 | (import "foo" "bar" (func (param i64))) 64 | (import "fizz" "bar" (func (param i64))) 65 | ) 66 | """ 67 | let url = try transformWat(wat) { $0.module == "fizz" } 68 | let output = wasmObjdump(url, args: ["--details"]) 69 | let expectedTypes = """ 70 | Type[2]: 71 | - type[0] (i64) -> nil 72 | - type[1] (i32) -> nil 73 | """ 74 | XCTAssertTrue(output.contains(expectedTypes)) 75 | let expectedImport = """ 76 | Import[2]: 77 | - func[0] sig=0 <- foo.bar 78 | - func[1] sig=1 <- fizz.bar 79 | """ 80 | XCTAssertContains(output, contains: expectedImport) 81 | } 82 | 83 | func testI64ImportCall() throws { 84 | typealias TestCase = ( 85 | line: UInt, wat: String, expectedTypes: String?, expectedImport: String?, expectedCode: String? 86 | ) 87 | 88 | let testCases: [TestCase] = [ 89 | (line: #line, 90 | wat: """ 91 | (module 92 | (import "foo" "bar" (func (param i64))) 93 | (func (call 0 (i64.const 0))) 94 | ) 95 | """, 96 | expectedTypes: """ 97 | Type[3]: 98 | - type[0] (i64) -> nil 99 | - type[1] () -> nil 100 | - type[2] (i32) -> nil 101 | """, 102 | expectedImport: """ 103 | Import[1]: 104 | - func[0] sig=2 <- foo.bar 105 | """, 106 | expectedCode: """ 107 | 00002c func[1]: 108 | 00002d: 42 00 | i64.const 0 109 | 00002f: 10 02 | call 2 110 | 000031: 0b | end 111 | 000033 func[2]: 112 | 000034: 20 00 | local.get 0 113 | 000036: a7 | i32.wrap_i64 114 | 000037: 10 00 | call 0 115 | 000039: 0b | end 116 | """ 117 | ), 118 | (line: #line, 119 | wat: """ 120 | (module 121 | (import "foo" "bar" (func (param i64) (param i32) (param i64))) 122 | (func (call 0 (i64.const 0) (i32.const 0) (i64.const 0))) 123 | ) 124 | """, 125 | expectedTypes: """ 126 | Type[3]: 127 | - type[0] (i64, i32, i64) -> nil 128 | - type[1] () -> nil 129 | - type[2] (i32, i32, i32) -> nil 130 | """, 131 | expectedImport: "func[0] sig=2 <- foo.bar", 132 | expectedCode: """ 133 | 000030 func[1]: 134 | 000031: 42 00 | i64.const 0 135 | 000033: 41 00 | i32.const 0 136 | 000035: 42 00 | i64.const 0 137 | 000037: 10 02 | call 2 138 | 000039: 0b | end 139 | 00003b func[2]: 140 | 00003c: 20 00 | local.get 0 141 | 00003e: a7 | i32.wrap_i64 142 | 00003f: 20 01 | local.get 1 143 | 000041: 20 02 | local.get 2 144 | 000043: a7 | i32.wrap_i64 145 | 000044: 10 00 | call 0 146 | 000046: 0b | end 147 | """ 148 | ), 149 | ] 150 | 151 | for testCase in testCases { 152 | let url = try transformWat(testCase.wat) 153 | let summary = wasmObjdump(url, args: ["--details"]) 154 | if let expectedTypes = testCase.expectedTypes { 155 | XCTAssertTrue(summary.contains(expectedTypes), "\(summary) doesn't contains \(expectedTypes)", line: testCase.line) 156 | } 157 | if let expectedImport = testCase.expectedImport { 158 | XCTAssertTrue(summary.contains(expectedImport), "\(summary) doesn't contains \(expectedImport)", line: testCase.line) 159 | } 160 | let disassemble = wasmObjdump(url, args: ["--disassemble"]) 161 | if let expectedCode = testCase.expectedCode { 162 | XCTAssertTrue(disassemble.contains(expectedCode), "\(disassemble) doesn't contains \(expectedCode)", line: testCase.line) 163 | } 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /Tests/WasmTransformerTests/InputByteStreamTests.swift: -------------------------------------------------------------------------------- 1 | @testable import WasmTransformer 2 | import XCTest 3 | 4 | final class InputByteStreamTests: XCTestCase { 5 | let buildPath = URL(fileURLWithPath: #filePath) 6 | .deletingLastPathComponent() 7 | .deletingLastPathComponent() 8 | .deletingLastPathComponent() 9 | .appendingPathComponent("Fixtures/build") 10 | 11 | func testReadCallInst() { 12 | measureMetrics([.wallClockTime], automaticallyStartMeasuring: false) { 13 | var input = try! InputByteStream(from: buildPath.appendingPathComponent("I64ImportTransformerTests.wasm")) 14 | _ = try! input.readHeader() 15 | readUntilCode: while !input.isEOF { 16 | let sectionInfo = try! input.readSectionInfo() 17 | 18 | switch sectionInfo.type { 19 | case .code: 20 | break readUntilCode 21 | default: 22 | input.skip(sectionInfo.size) 23 | } 24 | } 25 | 26 | let count = Int(input.readVarUInt32()) 27 | self.startMeasuring() 28 | for _ in 0 ..< count { 29 | let oldSize = Int(input.readVarUInt32()) 30 | let bodyEnd = input.offset + oldSize 31 | var bodyBuffer: [UInt8] = [] 32 | bodyBuffer.reserveCapacity(oldSize) 33 | 34 | try! input.consumeLocals(consumer: { 35 | bodyBuffer.append(contentsOf: $0) 36 | }) 37 | 38 | while input.offset < bodyEnd { 39 | _ = try! input.readCallInst() 40 | } 41 | } 42 | self.stopMeasuring() 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Tests/WasmTransformerTests/IntegrationTests.swift: -------------------------------------------------------------------------------- 1 | @testable import WasmTransformer 2 | import XCTest 3 | 4 | class IntegrationTests: XCTestCase { 5 | func testSwiftFoundationDate() throws { 6 | let binaryPath = URL(fileURLWithPath: #filePath) 7 | .deletingLastPathComponent() 8 | .deletingLastPathComponent() 9 | .deletingLastPathComponent() 10 | .appendingPathComponent("Fixtures/build/I64ImportTransformerTests.wasm") 11 | 12 | let transformer = I64ImportTransformer() 13 | var inputStream = try InputByteStream(from: binaryPath) 14 | var writer = InMemoryOutputWriter() 15 | try transformer.transform(&inputStream, writer: &writer) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Tests/WasmTransformerTests/SectionsInfoTests.swift: -------------------------------------------------------------------------------- 1 | @testable import WasmTransformer 2 | import XCTest 3 | 4 | final class SectionsInfoTests: XCTestCase { 5 | func testSectionsInfo() throws { 6 | let wat = """ 7 | (module 8 | (func $add (result i32) 9 | (i32.add 10 | (i32.const 1) 11 | (i32.const 1) 12 | ) 13 | ) 14 | ) 15 | """ 16 | 17 | let binaryURL = compileWat(wat, options: ["--debug-names"]) 18 | var input = try InputByteStream(bytes: [UInt8](Data(contentsOf: binaryURL))) 19 | try XCTAssertEqual( 20 | input.readSectionsInfo(), 21 | [ 22 | .init(startOffset: 8, contentStart: 10, type: .type, size: 5), 23 | .init(startOffset: 15, contentStart: 17, type: .function, size: 2), 24 | .init(startOffset: 19, contentStart: 21, type: .code, size: 9), 25 | .init(startOffset: 30, contentStart: 32, type: .custom, size: 18), 26 | ] 27 | ) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Tests/WasmTransformerTests/StackOverflowSanitizerTests.swift: -------------------------------------------------------------------------------- 1 | @testable import WasmTransformer 2 | import XCTest 3 | 4 | final class StackOverflowSanitizerTests: XCTestCase { 5 | 6 | func testTransformFunction() throws { 7 | let wat = """ 8 | (module 9 | (import "__stack_sanitizer" "report_stack_overflow" (func)) 10 | (global (mut i32) (i32.const 0)) 11 | (func $bar 12 | call $foo 13 | ) 14 | (func $foo) 15 | (func $llvm_func 16 | i32.const 0 17 | global.set 0 18 | ) 19 | ) 20 | """ 21 | 22 | let binaryURL = compileWat(wat) 23 | var input = try InputByteStream(bytes: [UInt8](Data(contentsOf: binaryURL))) 24 | 25 | var writer = InMemoryOutputWriter() 26 | let transformer = StackOverflowSanitizer() 27 | try transformer.transform(&input, writer: &writer) 28 | let (url, handle) = makeTemporaryFile() 29 | handle.write(Data(writer.bytes())) 30 | let disassemble = wasmObjdump(url, args: ["--disassemble"]) 31 | let expected = """ 32 | 00005b func[3]: 33 | 00005c: 41 00 | i32.const 0 34 | 00005e: 10 04 | call 4 35 | 000060: 24 00 | global.set 0 36 | 000062: 0b | end 37 | 000064 func[4]: 38 | 000065: 20 00 | local.get 0 39 | 000067: 41 00 | i32.const 0 40 | 000069: 48 | i32.lt_s 41 | 00006a: 04 40 | if 42 | 00006c: 10 00 | call 0 <__stack_sanitizer.report_stack_overflow> 43 | 00006e: 0b | end 44 | 00006f: 20 00 | local.get 0 45 | 000071: 0b | end 46 | """ 47 | XCTAssertContains(disassemble, contains: expected) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Tests/WasmTransformerTests/misc.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | func findExecutable(_ name: String) -> String { 4 | guard let pathsString = ProcessInfo.processInfo.environment["PATH"] else { 5 | fatalError("PATH is not set in environment") 6 | } 7 | let paths = pathsString.split(separator: ":") 8 | for path in paths { 9 | let candidate = URL(fileURLWithPath: String(path)).appendingPathComponent(name) 10 | if FileManager.default.isExecutableFile(atPath: candidate.path) { 11 | return candidate.path 12 | } 13 | } 14 | fatalError("Command '\(name)' not found in PATH") 15 | } 16 | 17 | @discardableResult 18 | func exec(_ launchPath: String, _ arguments: [String]) -> String? { 19 | let process = Process() 20 | process.launchPath = launchPath 21 | process.arguments = arguments 22 | let stdoutPipe = Pipe() 23 | process.standardOutput = stdoutPipe 24 | process.launch() 25 | process.waitUntilExit() 26 | assert(process.terminationStatus == 0) 27 | let stdoutData = stdoutPipe.fileHandleForReading.readDataToEndOfFile() 28 | guard let stdout = String(data: stdoutData, encoding: .utf8) else { 29 | return nil 30 | } 31 | return stdout 32 | } 33 | 34 | func makeTemporaryFile() -> (URL, FileHandle) { 35 | let tempdir = URL(fileURLWithPath: NSTemporaryDirectory()) 36 | let templatePath = tempdir.appendingPathComponent("wasm-transformer.XXXXXX") 37 | var template = [UInt8](templatePath.path.utf8).map { Int8($0) } + [Int8(0)] 38 | let fd = mkstemp(&template) 39 | if fd == -1 { 40 | fatalError("Failed to create temp directory") 41 | } 42 | let url = URL(fileURLWithPath: String(cString: template)) 43 | let handle = FileHandle(fileDescriptor: fd, closeOnDealloc: true) 44 | return (url, handle) 45 | } 46 | 47 | func createFile(_ content: String) -> URL { 48 | let (url, handle) = makeTemporaryFile() 49 | handle.write(content.data(using: .utf8)!) 50 | return url 51 | } 52 | 53 | func compileWat(_ content: String, options: [String] = []) -> URL { 54 | let module = createFile(content) 55 | let (output, _) = makeTemporaryFile() 56 | exec(findExecutable("wat2wasm"), [module.path, "-o", output.path] + options) 57 | return output 58 | } 59 | 60 | func wasmObjdump(_ input: URL, args: [String]) -> String { 61 | exec(findExecutable("wasm-objdump"), [input.path] + args)! 62 | } 63 | 64 | 65 | @testable import WasmTransformer 66 | 67 | extension WasmTransformer.InputByteStream { 68 | init(from url: URL) throws { 69 | let bytes = try Array(Data(contentsOf: url)) 70 | self.init(bytes: bytes) 71 | } 72 | } 73 | 74 | import XCTest 75 | 76 | func XCTAssertContains(_ value: String, contains: String, file: StaticString = #file, line: UInt = #line) { 77 | XCTAssertTrue(value.contains(contains), "'\(value)'\n does not contain \n'\(contains)'", file: file, line: line) 78 | } 79 | -------------------------------------------------------------------------------- /Tools/GenerateStackOverflowSanitizerSupport.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | guard CommandLine.arguments.count == 2 else { 4 | print("Usage: \(CommandLine.arguments[0]) ") 5 | exit(1) 6 | } 7 | 8 | let objectFile = try Data(contentsOf: URL(fileURLWithPath: CommandLine.arguments[1])) 9 | 10 | let sourceCode = """ 11 | // GENERATED FROM Tools/GenerateStackOverflowSanitizerSupport.swift 12 | 13 | extension StackOverflowSanitizer { 14 | public static let supportObjectFile: [UInt8] = [ 15 | \(objectFile.map { String(format: "0x%02x", $0) }.joined(separator: ", ")) 16 | ] 17 | } 18 | """ 19 | 20 | print(sourceCode) 21 | --------------------------------------------------------------------------------