├── LICENSE ├── README.md ├── build.zig ├── build.zig.zon ├── c-api ├── include │ └── stitch.h └── test │ └── c-test.c ├── spec └── README.md └── src ├── lib.zig ├── main.zig └── tests.zig /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023-2025 cryptocode 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 |   4 |   5 | 6 | 7 | Stitch is a tool and library for Zig and C for adding and retrieving resources to and from executables. 8 | 9 | Why not just use `@embedFile` / `#embed`? Stitch serves a different purpose, namely to let build systems, and *users* of your software, create self-contained executables. 10 | 11 | For example, instead of requiring users to install an interpreter and execute `mylisp fib.lisp`, they can simply run `./fib` or `fib.exe` 12 | 13 | Resoures can be anything, such as scripts, images, text, templates config files and other executables. 14 | 15 | ## Some use cases 16 | * Self extracting tools, like an installer 17 | * Create executables for scripts written in your interpreted programming language 18 | * Include a sample config file, which is extracted on first run. The user can then edit this. 19 | * An image in your own format that's able to display itself when executed 20 | 21 | ## Building the project 22 | To build with Zig 0.13, use the `zig-` tag/release. 23 | To build with Zig 0.14 or master, use the main branch (last tested with Zig version `0.15.0-dev.11+5c57e90ff`) 24 | 25 | `zig build` will put a `bin` and `lib` directory in your output folder (e.g. zig-out) 26 | 27 | * bin/stitch is a standalone tool for attaching resources to executables. This can also be done programmatically using the library 28 | * lib/libstitch is a library for reading attached resources from the current executable, and for adding resources to executables (like the standalone tool) 29 | 30 | ## Using the tool 31 | 32 | This example adds two scripts to a Lisp interpreter that supports, through the stitch library, reading embedded scripts: 33 | 34 | ```bash 35 | stitch ./mylisp std.lisp fib.lisp --output fib 36 | 37 | ./fib 8 38 | 21 39 | ``` 40 | 41 | Resources can be named explicitly 42 | 43 | ```bash 44 | stitch ./mylisp std=std.lisp fibonacci=fib.lisp --output fib 45 | ``` 46 | 47 | If a name is not given, the filename (without path) is used. The stitch library supports finding resources by name or index. 48 | 49 | The `--output` flag is optional. By default, resources are added to the original executable (first argument) 50 | ## Stitching programmatically 51 | Let's say you want your interpreted programming language to support producing binaries. 52 | 53 | An easy way to do this is to create an interpreter executable that reads scripts attached to itself using stitch. 54 | 55 | You can provide interpreter binaries for all the OS'es you wanna support, or have the Zig build file do this if your user is building the interpreter. 56 | 57 | In the example below, a Lisp interpreter uses the stitch library to support creating self-contained executables: 58 | 59 | ```bash 60 | ./mylisp --create-exe sql-client.lisp --output sql-client 61 | ``` 62 | The resulting binary can now be executed: 63 | 64 | ``` 65 | ./sql-client 66 | ``` 67 | 68 | You can make the `mylisp` binary understand stitch attachments and then make a copy of it and stitch it with the scripts. Alternatively, you can have separate interpreter binaries specifically for reading stitched scripts. 69 | ## Using the library from C 70 | 71 | Include the `stitch.h` header and link to the library. Here's an example, using the included C test program: 72 | 73 | ```bash 74 | zig build-exe c-api/test/c-test.c -Lzig-out/lib -lstitch -Ic-api/include 75 | ./c-test 76 | ``` 77 | 78 | ## Binary layout 79 | 80 | The binary layout specification can be used by other tools that wants to parse files produced by Stitch, without using the Stitch library. 81 | 82 | [Specification](spec/README.md) 83 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | /// Build the stitch tool, and a library that can be used to read/write resources 4 | pub fn build(b: *std.Build) void { 5 | const target = b.standardTargetOptions(.{}); 6 | const optimize = b.standardOptimizeOption(.{}); 7 | 8 | const stitch_mod = b.addModule("stitch", .{ 9 | .root_source_file = b.path("src/lib.zig"), 10 | }); 11 | 12 | const lib = b.addStaticLibrary(.{ 13 | .name = "stitch", 14 | .root_source_file = b.path("src/lib.zig"), 15 | .target = target, 16 | .optimize = optimize, 17 | }); 18 | b.installArtifact(lib); 19 | 20 | const exe = b.addExecutable(.{ 21 | .name = "stitch", 22 | .root_source_file = b.path("src/main.zig"), 23 | .target = target, 24 | .optimize = optimize, 25 | }); 26 | exe.root_module.addImport("stitch", stitch_mod); 27 | b.installArtifact(exe); 28 | 29 | const run_cmd = b.addRunArtifact(exe); 30 | run_cmd.step.dependOn(b.getInstallStep()); 31 | if (b.args) |args| { 32 | run_cmd.addArgs(args); 33 | } 34 | const run_step = b.step("run", "Run the app"); 35 | run_step.dependOn(&run_cmd.step); 36 | 37 | // Creates a step for unit testing. 38 | const main_tests = b.addTest(.{ 39 | .name = "tests", 40 | .root_source_file = b.path("src/tests.zig"), 41 | .target = target, 42 | .optimize = optimize, 43 | }); 44 | main_tests.root_module.addImport("stitch", stitch_mod); 45 | 46 | const run_main_tests = b.addRunArtifact(main_tests); 47 | const test_step = b.step("test", "Run library tests"); 48 | test_step.dependOn(&run_main_tests.step); 49 | } 50 | -------------------------------------------------------------------------------- /build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = .stitch, 3 | .version = "0.1.0", 4 | .paths = .{ 5 | "build.zig", 6 | "build.zig.zon", 7 | "src/lib.zig", 8 | "src/main.zig", 9 | "src/tests.zig", 10 | "LICENSE", 11 | "README.md", 12 | }, 13 | .fingerprint = 0x284931c8ceb2fd1e, 14 | } 15 | -------------------------------------------------------------------------------- /c-api/include/stitch.h: -------------------------------------------------------------------------------- 1 | // C ABI interface for the stitch library 2 | // Link with libstitch 3 | #pragma once 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | #ifdef __cplusplus 10 | extern "C" { 11 | #endif 12 | 13 | // If a function succeeds, the `error_code` argument is set to STITCH_SUCCESS, otherwise it is set to 14 | // one of the error codes below. The numeric values are guaranteed to not change between 15 | // versions of the library. 16 | // Do not rely on return values for error checking; only check against `error_code` 17 | #define STITCH_SUCCESS 0 18 | #define STITCH_ERROR_UKNOWN 1 19 | #define STITCH_ERROR_OUTPUT_FILE_ALREADY_EXISTS 2 20 | #define STITCH_ERROR_INPUT_FILE_COULD_NOT_OPEN 3 21 | #define STITCH_ERROR_OUTPUT_FILE_COULD_NOT_OPEN 4 22 | #define STITCH_ERROR_INVALID_EXECUTABLE_FORMAT 5 23 | #define STITCH_ERROR_RESOURCE_NOT_FOUND 6 24 | #define STITCH_ERROR_IO_ERROR 7 25 | 26 | // Start a new stitch session for appending resources to an executable. No file writes occur until stitch_writer_commit is called. 27 | // The returned writer session is passed to all other writer functions. 28 | // You must call stitch_deinit to close the session, which also frees memory allocated by the session (including resources) 29 | // Note that `original_executable_path` and `output_executable_path` can be the same, in which case metadata and resources are 30 | // appended to the original executable. The same applies if `output_executable_path` is NULL. 31 | // On error, `error_code` is set to the error code and NULL is returned. 32 | void* stitch_init_writer(const char* original_executable_path, const char* output_executable_path, uint64_t* error_code); 33 | 34 | // Start a new stitch session for reading resources. The returned session is passed to all other applicable functions. 35 | // You must call stitch_deinit to close the session, and free allocated memory. 36 | // If `executable_path` is NULL, the currently running executable is used. This enables executables to read resources from themselves. 37 | // On error, `error_code` is set to the error code and NULL is returned. 38 | void* stitch_init_reader(const char* executable_path, uint64_t* error_code); 39 | 40 | // Close a stitch session returned by `stitch_init_writer` or `stitch_init_reader`. 41 | // Not calling this function will result in memory leaks. 42 | // Calling this function with a NULL pointer is a safe no-op. 43 | void stitch_deinit(void* session); 44 | 45 | // Returns the number of resources in the executable. This may be zero. 46 | uint64_t stitch_reader_get_resource_count(void* reader); 47 | 48 | // Returns the format version of the executable. This is useful for detecting incompatible changes to the stitch format. 49 | uint8_t stitch_reader_get_format_version(void* reader); 50 | 51 | // Returns the index of the resource with the given name. 52 | // On error, `error_code` is set to the error code and UINT64_MAX is returned. 53 | // Error code is STITCH_ERROR_RESOURCE_NOT_FOUND if the resource is not found. 54 | uint64_t stitch_reader_get_resource_index(void* reader, const char* name, uint64_t* error_code); 55 | 56 | // Returns the length in bytes of the resource at the given index 57 | // On error, `error_code` is set to the error code and UINT64_MAX is returned. 58 | // Error code is STITCH_ERROR_RESOURCE_NOT_FOUND if the resource is not found. 59 | uint64_t stitch_reader_get_resource_byte_len(void* reader, uint64_t index, uint64_t* error_code); 60 | 61 | // Returns the data of the resource at the given index 62 | // Use `stitch_reader_get_resource_byte_len` to get the size of the returned resource 63 | // On error, `error_code` is set to the error code and NULL is returned. 64 | // Error code is STITCH_ERROR_RESOURCE_NOT_FOUND if the resource index is invalid. 65 | const char* stitch_reader_get_resource_bytes(void* reader, uint64_t index, uint64_t* error_code); 66 | 67 | // Returns the scratch bytes for the resource, which is all-zeros if not set specifically. 68 | // On error, `error_code` is set to the error code and NULL is returned. 69 | // Error code is STITCH_ERROR_RESOURCE_NOT_FOUND if the resource index is invalid. 70 | const char* stitch_reader_get_scratch_bytes(void* reader, uint64_t index, uint64_t* error_code); 71 | 72 | // Write executable and resources to file. 73 | void stitch_writer_commit(void* writer, uint64_t* error_code); 74 | 75 | // Add a resource to the executable given a relative or absolute path. 76 | // The resource is not written to disk until stitch_writer_commit is called. 77 | // This option reqiores minimal memory usage. 78 | // Returns the index of the resource. 79 | // On error, `error_code` is set to the error code and UINT64_MAX is returned. 80 | uint64_t stitch_writer_add_resource_from_path(void* writer, const char* name, const char* path, uint64_t* error_code); 81 | 82 | // Add a resource to the executable given a buffer of data. 83 | // The buffer is not written to disk until stitch_writer_commit is called. 84 | // The buffer must remain valid until stitch_writer_commit is called. 85 | // Returns the index of the resource. 86 | // On error, `error_code` is set to the error code and UINT64_MAX is returned. 87 | uint64_t stitch_writer_add_resource_from_bytes(void* writer, const char* name, const char* data, uint64_t len, uint64_t* error_code); 88 | 89 | // Set the scratch bytes for a resource, using the index returned by the add_resource... functions. 90 | // The length of `bytes` must be exactly 8 bytes. 91 | // The default scratch bytes is all-zero. 92 | // Returns true if the scratch bytes were set successfully, or false if an error occurs. 93 | void stitch_writer_set_scratch_bytes(void* writer, uint64_t resource_index, const char* bytes, uint64_t* error_code); 94 | 95 | // If an error is produced by an API function, the returned string is a human-readable diagnostic message, 96 | // otherwise NULL is returned. Every API function resets the diagnostic. 97 | // The memory for the returned string is owned by the session and is freed when `stitch_deinit` is called. 98 | char* stitch_get_last_error_diagnostic(void* session); 99 | 100 | // Returns a human-readable diagnostic message for the given error code 101 | // If a valid session is available, use `stitch_get_last_error_diagnostic` instead to get more detailed information. 102 | // The main use case for this function is when a session is not available, i.e when an init function fail. 103 | // The memory for the returned string is owned by the library. 104 | char* stitch_get_error_diagnostic(uint64_t error_code); 105 | 106 | #ifdef __cplusplus 107 | } 108 | #endif -------------------------------------------------------------------------------- /c-api/test/c-test.c: -------------------------------------------------------------------------------- 1 | // Tests the C API 2 | // 3 | // Compile this file into an executable and run it: 4 | // zig build-exe c-api/test/c-test.c -Lzig-out/lib -lstitch -Ic-api/include 5 | // ./c-test 6 | // 7 | // If an error occurs, the test program will print an error message and exit with a non-zero exit code 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | void stitch_test_setup(); 14 | void stitch_test_teardown(); 15 | 16 | int main() { 17 | uint64_t error_code = 0; 18 | 19 | // Create test files 20 | stitch_test_setup(); 21 | 22 | // Create a stitch writer 23 | void* writer = stitch_init_writer(".stitch/executable", ".stitch/new-executable", &error_code); 24 | if (error_code) { 25 | printf("Failed to initialize stitch writer: %" PRIu64 " (%s)\n", error_code, stitch_get_error_diagnostic(error_code)); 26 | return 1; 27 | } 28 | 29 | stitch_writer_add_resource_from_path(writer, "first file", ".stitch/one.txt", &error_code); 30 | if (error_code) { 31 | printf("Failed to add resource from path: %" PRIu64 " (%s)\n", error_code, stitch_get_last_error_diagnostic(writer)); 32 | return 1; 33 | } 34 | stitch_writer_add_resource_from_bytes(writer, "second file", "abcd", 4, &error_code); 35 | if (error_code) { 36 | printf("Failed to add resource from bytes: %" PRIu64 " (%s)\n", error_code, stitch_get_last_error_diagnostic(writer)); 37 | return 1; 38 | } 39 | stitch_writer_set_scratch_bytes(writer, 0, "12345678", &error_code); 40 | if (error_code) { 41 | printf("Failed to set scratch bytes: %" PRIu64 " (%s)\n", error_code, stitch_get_last_error_diagnostic(writer)); 42 | return 1; 43 | } 44 | stitch_writer_commit(writer, &error_code); 45 | if (error_code) { 46 | printf("Failed to commit: %" PRIu64 " (%s)\n", error_code, stitch_get_last_error_diagnostic(writer)); 47 | return 1; 48 | } 49 | 50 | stitch_deinit(writer); 51 | 52 | // Create a stitch reader 53 | void* reader = stitch_init_reader(".stitch/new-executable", &error_code); 54 | if (error_code) { 55 | printf("Failed to initialize stitch. Have you attached resources to this executable yet?\n"); 56 | return 1; 57 | } 58 | 59 | uint64_t count = stitch_reader_get_resource_count(reader); 60 | printf("Resource count is: %" PRIu64 "\n", count); 61 | 62 | uint8_t format_version = stitch_reader_get_format_version(reader); 63 | printf("Format version is: %" PRIu8 "\n", format_version); 64 | 65 | uint64_t index = stitch_reader_get_resource_index(reader, "second file", &error_code); 66 | if (error_code) { 67 | printf("Failed to get index of resource named \"second file\"\n"); 68 | return 1; 69 | } 70 | printf("Index of resource named \"second file\" is: %" PRIu64 "\n", index); 71 | 72 | const char* bytes = stitch_reader_get_resource_bytes(reader, 0, &error_code); 73 | if (error_code) { 74 | printf("Failed to get bytes for resource 0\n"); 75 | return 1; 76 | } 77 | uint64_t len = stitch_reader_get_resource_byte_len(reader, 0, &error_code); 78 | if (error_code) { 79 | printf("Failed to get length of resource 0\n"); 80 | return 1; 81 | } 82 | printf("Resource 0 has length: %" PRIu64 "\n", len); 83 | printf("Bytes: %.*s\n", (int)len, bytes); 84 | 85 | // Get scratch bytes for resource 0 86 | const char* scratch_bytes = stitch_reader_get_scratch_bytes(reader, 0, &error_code); 87 | if (error_code) { 88 | printf("Failed to get scratch bytes for resource 0\n"); 89 | return 1; 90 | } 91 | printf("Scratch bytes for resource 0 are: %.*s\n", 8, scratch_bytes); 92 | 93 | // Clear memory allocated by stitch, including all resource data 94 | // If you need to keep the resources around after deinitializing stitch, you need to copy them first 95 | stitch_deinit(reader); 96 | 97 | // Remove test files 98 | stitch_test_teardown(); 99 | 100 | return 0; 101 | } -------------------------------------------------------------------------------- /spec/README.md: -------------------------------------------------------------------------------- 1 | # Specification 2 | 3 | 4 | 5 | Format version: 1 6 | 7 | This specification can be used by tools to parse and create Stitch executables, without using the Stitch library. 8 | 9 | Resources and metadata are appended to the end of the original executable, according to the specification below. 10 | 11 | Backwards- and forwards compatibility is guaranteed as long as the *eof-magic* is recognized: older parsers will be able to read what they understand from newer format versions, and newer parsers will fully understand older format versions. Any features breaking this guarantee will essentially be a new format, with a new *eof-magic* 12 | 13 | ```ebnf 14 | stitch-executable ::= original-exe resource* index tail 15 | original-exe ::= blob 16 | resource ::= resource-magic blob 17 | index ::= entry-count index-entry* 18 | tail ::= index-offset version eof-magic 19 | 20 | index-entry ::= name resource-type resource-offset byte-length scratch-bytes 21 | name ::= byte-length blob 22 | resource-type ::= u8 23 | resource-offset ::= u64be 24 | scratch-bytes ::= [8]u8 25 | 26 | index-offset ::= u64be 27 | blob ::= [*]u8 28 | byte-length ::= u64be 29 | entry-count ::= u64be 30 | 31 | version ::= u8 32 | resource-magic ::= u64be = 0x18c767a11ea80843 33 | eof-magic ::= u64be = 0xa2a7fdfa0533438f 34 | ``` 35 | A parser is expected to start by reading the 17-byte `tail`: index offset, version and magic. 36 | 37 | If the index offset is 0, then the file doesn't contain any resources but is still a valid Stitch executable. A file shorter than 17 bytes is never a valid Stitch executable. 38 | 39 | If `eof-magic` is recognized, the parser continues by reading the index, given by the index offset. Once the index is read, resources can be read either directly, or on 40 | request by zero-based resource index or resource name. 41 | 42 | ## Notes: 43 | * *offset* is number of bytes from the beginning of the file 44 | * *version* is currently the value 1 45 | * *eof-magic* indicates that this is a Stitch-compliant executable 46 | * *resource-magic* is a marker to help tools verify the that the layout is correct 47 | * *resource-type* is currently the value 1, denoting "blob". This field may gain additional values in the future, to support backwards- and forwards compatibility. 48 | * *scratch-bytes* are 8 freely available bytes, whose interpretation is up to the application. If not set by the application, this field will be initialized to all-zeros. The field can be used for things like file types, permissions, etc. Additional metadata can be prepended manually in the resource. 49 | * *u64be* mean 64-bit integer written in big endian format. Big-endian is used for 3 reasons: a) it's the defacto standard for binary formats, b) it makes debugging outputs easier, c) it prevents buggy implementation assuming native == little (as most systems are little endian) 50 | * Resources are guaranteed to be added in same order as the API calls for adding resources 51 | 52 | ## Diagram 53 | Below is the same specification in diagram form: 54 | ``` 55 | [0] Overall file layout: 56 | 57 | +-------------------+ 58 | | [1] original-exe | 59 | +-------------------+ 60 | | [2] resource | 61 | +-------------------+ 62 | | [2] ... | 63 | +-------------------+ 64 | | [2] resource | 65 | +-------------------+ 66 | | [3] index | 67 | +-------------------+ 68 | | [6] tail | 69 | +-------------------+ 70 | 71 | [1] Original executable: 72 | 73 | +-------+ 74 | | blob | 75 | +-------+ 76 | | [*]u8 | 77 | +-------+ 78 | 79 | [2] Resource: 80 | 81 | +----------------+-------+ 82 | | resource-magic | blob | 83 | +----------------+-------+ 84 | | u64be | [*]u8 | 85 | +----------------+-------+ 86 | 87 | [3] Index: 88 | 89 | +-------------+--------------+---------------+--------------+ 90 | | entry-count | index-entry | ... | index-entry | 91 | +-------------+--------------+---------------+--------------+ 92 | | u64be | [4] | [4] | [4] | 93 | +-------------+--------------+---------------+--------------+ 94 | 95 | [4] Index entry: 96 | 97 | +-------+---------------+------------------+-------------+----------------+ 98 | | name | resource-type | resource-offset | byte-length | scratch-bytes | 99 | +-------+---------------+------------------+-------------+----------------+ 100 | | [5] | u8 | u64be | u64be | [8]u8 | 101 | +-------+---------------+------------------+-------------+----------------+ 102 | 103 | [5] Name: 104 | 105 | +-------------+-------+ 106 | | byte-length | blob | 107 | +-------------+-------+ 108 | | u64be | [*]u8 | 109 | +-------------+-------+ 110 | 111 | [6] Tail: 112 | +---------------+----------+-----------+ 113 | | index-offset | version | eof-magic | 114 | +---------------+----------+-----------+ 115 | | u64be | u8 | u64be | 116 | +---------------+----------+-----------+ 117 | ``` -------------------------------------------------------------------------------- /src/lib.zig: -------------------------------------------------------------------------------- 1 | //! The Stitch library and C wrapper 2 | const std = @import("std"); 3 | const builtin = @import("builtin"); 4 | const testing = std.testing; 5 | const Self = @This(); 6 | 7 | arena: std.heap.ArenaAllocator, 8 | rw: union(enum) { 9 | writer: StitchWriter, 10 | reader: StitchReader, 11 | } = undefined, 12 | 13 | /// This is set whenever a StitchError is returned 14 | diagnostics: ?Diagnostic = null, 15 | 16 | /// The executable to read from, or write to if stitching to the original 17 | org_exe_file: std.fs.File = undefined, 18 | 19 | /// The output executable. If this is null, the resources will be stitched to the original 20 | output_exe_file: ?std.fs.File = null, 21 | 22 | pub const ResourceMagic: u64 = 0x18c767a11ea80843; 23 | pub const EofMagic: u64 = 0xa2a7fdfa0533438f; 24 | pub const StitchVersion: u8 = 0x1; 25 | 26 | const StitchExecutable = struct { 27 | resources: std.ArrayList(Resource), 28 | index: Index, 29 | tail: Tail, 30 | }; 31 | 32 | const Index = struct { 33 | entries: std.ArrayList(IndexEntry), 34 | }; 35 | 36 | const IndexEntry = struct { 37 | name: []const u8, 38 | resource_type: u8, 39 | resource_offset: u64, 40 | byte_length: u64, 41 | scratch_bytes: [8]u8, 42 | }; 43 | 44 | const Tail = struct { 45 | index_offset: u64, 46 | version: u8, 47 | eof_magic: u64, 48 | }; 49 | 50 | const ResourceType = enum { 51 | bytes, 52 | path, 53 | reader, 54 | }; 55 | 56 | const Resource = struct { 57 | magic: u64, 58 | data: union(ResourceType) { 59 | bytes: []const u8, 60 | path: []const u8, 61 | reader: std.fs.File.Reader, 62 | }, 63 | }; 64 | 65 | /// This is the type of error returned by all API functions. No other errors are ever returned. 66 | pub const StitchError = error{ OutputFileAlreadyExists, CouldNotOpenInputFile, CouldNotOpenOutputFile, InvalidExecutableFormat, ResourceNotFound, IoError }; 67 | 68 | /// Diagnostic is available through `getDiagnostics` whenever an error is returned. 69 | pub const Diagnostic = union(std.meta.FieldEnum(StitchError)) { 70 | /// Path to output file that already exists 71 | OutputFileAlreadyExists: []const u8, 72 | /// Could not open input file, typically due to not existing, or lack of read permissions 73 | CouldNotOpenInputFile: []const u8, 74 | /// Could not open output file, typically due to lack of write permissions 75 | CouldNotOpenOutputFile: []const u8, 76 | /// Reason for invalid format 77 | InvalidExecutableFormat: []const u8, 78 | // No resource can be found by the given name or index 79 | ResourceNotFound: union(enum) { 80 | name: []const u8, 81 | index: u64, 82 | }, 83 | // IO error description 84 | IoError: []const u8, 85 | 86 | /// Print a diagnostic error to stderr 87 | pub fn print(self: Diagnostic, str_alloc: std.mem.Allocator) !void { 88 | std.debug.print("{s}\n", .{try self.toOwnedString(str_alloc)}); 89 | } 90 | 91 | /// Return the diagnostic as a string. Caller must free the string. 92 | pub fn toOwnedString(self: Diagnostic, str_alloc: std.mem.Allocator) ![]const u8 { 93 | switch (self) { 94 | .OutputFileAlreadyExists => return try std.fmt.allocPrint(str_alloc, "Output file already exists: {s}\n", .{self.OutputFileAlreadyExists}), 95 | .CouldNotOpenInputFile => return try std.fmt.allocPrint(str_alloc, "Could not open input file: {s}\n", .{self.CouldNotOpenInputFile}), 96 | .CouldNotOpenOutputFile => return try std.fmt.allocPrint(str_alloc, "Could not open output file: {s}\n", .{self.CouldNotOpenOutputFile}), 97 | .InvalidExecutableFormat => return try std.fmt.allocPrint(str_alloc, "Invalid executable format: {s}\n", .{self.InvalidExecutableFormat}), 98 | .ResourceNotFound => switch (self.ResourceNotFound) { 99 | .name => return try std.fmt.allocPrint(str_alloc, "Resource name not found: {s}\n", .{self.ResourceNotFound.name}), 100 | .index => return try std.fmt.allocPrint(str_alloc, "Resource index not found: {d}\n", .{self.ResourceNotFound.index}), 101 | }, 102 | .IoError => return try std.fmt.allocPrint(str_alloc, "IO error: {s}\n", .{self.IoError}), 103 | } 104 | } 105 | 106 | /// Determine if an error is a diagnostic error 107 | pub fn isDiagnostic(err: anyerror) bool { 108 | inline for (std.meta.fields(Diagnostic)) |field| { 109 | if (std.mem.eql(u8, field.name, @errorName(err))) { 110 | return true; 111 | } 112 | } 113 | return false; 114 | } 115 | }; 116 | 117 | /// It is guaranteed that if an error is returned by a public reader or writer session function, 118 | /// the diagnostic will be set. The diagnostic is reset to null at the beginning of each public function. 119 | pub fn getDiagnostics(session: *Self) ?Diagnostic { 120 | return session.diagnostics orelse null; 121 | } 122 | 123 | // Called by all API functions to ensure that diagnostics is set only if a StitchError occurs 124 | fn resetDiagnostics(session: *Self) void { 125 | session.diagnostics = null; 126 | } 127 | 128 | /// Intialize a stitch session for writing. 129 | /// This returns a `StitchWriter`, which can be used to add resources to the input executable. 130 | /// The input and output paths can be the same, in which case resources are appended to the original executable. 131 | pub fn initWriter(allocator: std.mem.Allocator, input_executable_path: []const u8, output_executable_path: []const u8) !StitchWriter { 132 | var session = try allocator.create(Self); 133 | errdefer allocator.destroy(session); 134 | session.* = .{ 135 | .arena = std.heap.ArenaAllocator.init(allocator), 136 | }; 137 | const arena_allocator = session.arena.allocator(); 138 | errdefer session.arena.deinit(); 139 | 140 | const absolute_input_path = std.fs.realpathAlloc(arena_allocator, input_executable_path) catch return StitchError.CouldNotOpenInputFile; 141 | session.org_exe_file = std.fs.openFileAbsolute(absolute_input_path, .{ .mode = .read_write }) catch return StitchError.CouldNotOpenInputFile; 142 | 143 | // Output path does not need to exists; since we use cwd().createFile, path doesn't need to be absolute 144 | // We still attempt realpath to detect if we're stitching on the original 145 | const absolute_output_path = realpathOrOriginal(arena_allocator, output_executable_path) catch return StitchError.CouldNotOpenOutputFile; 146 | const stitch_to_original = std.mem.eql(u8, absolute_input_path, absolute_output_path); 147 | 148 | if (!stitch_to_original) { 149 | session.output_exe_file = std.fs.cwd().createFile(absolute_output_path, .{ .exclusive = true, .truncate = false }) catch |err| switch (err) { 150 | std.fs.File.OpenError.PathAlreadyExists => { 151 | return StitchError.OutputFileAlreadyExists; 152 | }, 153 | else => return err, 154 | }; 155 | } 156 | 157 | session.rw = .{ .writer = StitchWriter.init(session) }; 158 | return session.rw.writer; 159 | } 160 | 161 | /// Same as realpathAlloc, except it returns the original path if the path 162 | /// doesn't exist rather than an error 163 | fn realpathOrOriginal(allocator: std.mem.Allocator, path: []const u8) ![]const u8 { 164 | return std.fs.realpathAlloc(allocator, path) catch |err| { 165 | if (err == error.FileNotFound) { 166 | return path; 167 | } else { 168 | return err; 169 | } 170 | }; 171 | } 172 | 173 | /// Intialize a stitch session for reading 174 | /// This returns a StitchReader, which can be used to read resources from the executable 175 | /// If path is null, the currently running executable will be used 176 | pub fn initReader(allocator: std.mem.Allocator, path: ?[]const u8) !StitchReader { 177 | var session = try allocator.create(Self); 178 | errdefer allocator.destroy(session); 179 | session.* = .{ 180 | .arena = std.heap.ArenaAllocator.init(allocator), 181 | .rw = .{ .reader = StitchReader.init(session) }, 182 | }; 183 | errdefer session.arena.deinit(); 184 | 185 | if (path) |_| { 186 | session.org_exe_file = try std.fs.openFileAbsolute( 187 | try std.fs.realpathAlloc(allocator, path.?), 188 | .{ .mode = .read_only }, 189 | ); 190 | } else { 191 | session.org_exe_file = try std.fs.openSelfExe(.{ .mode = .read_only }); 192 | } 193 | 194 | try session.rw.reader.readMetadata(); 195 | return session.rw.reader; 196 | } 197 | 198 | // Called by a reader or writer's deinit function to free the session resources 199 | fn deinit(session: *Self) void { 200 | session.org_exe_file.close(); 201 | if (session.output_exe_file) |f| f.close(); 202 | var child_allocator = session.arena.child_allocator; 203 | session.arena.deinit(); 204 | child_allocator.destroy(session); 205 | } 206 | 207 | /// Use `initWriter` to create this writer, which allows you to append resources to an executable in 208 | /// a format recognized by `StitchReader` 209 | pub const StitchWriter = struct { 210 | session: *Self = undefined, 211 | exe: StitchExecutable = undefined, 212 | 213 | fn init(session: *Self) StitchWriter { 214 | return .{ 215 | .session = session, 216 | .exe = .{ 217 | .resources = std.ArrayList(Resource).init(session.arena.allocator()), 218 | .index = .{ .entries = std.ArrayList(IndexEntry).init(session.arena.allocator()) }, 219 | .tail = .{ .index_offset = 0, .version = 0, .eof_magic = EofMagic }, 220 | }, 221 | }; 222 | } 223 | 224 | /// Closes the stitch session and frees all resources. 225 | /// This must be called to ensure the writer session is properly closed. 226 | pub fn deinit(writer: *StitchWriter) void { 227 | writer.session.deinit(); 228 | } 229 | 230 | /// Write original executable, index, resources, and tail to output file. 231 | pub fn commit(writer: *StitchWriter) StitchError!void { 232 | // Wrapper to reclassify errors into StitchError.IoError 233 | return commitImpl(writer) catch |err| { 234 | if (!Diagnostic.isDiagnostic(err)) { 235 | writer.session.diagnostics = .{ .IoError = "Unable to commit resources to output file" }; 236 | return StitchError.IoError; 237 | } 238 | return @as(StitchError, @errorCast(err)); 239 | }; 240 | } 241 | 242 | fn commitImpl(writer: *StitchWriter) !void { 243 | writer.session.resetDiagnostics(); 244 | var outfile = writer.session.output_exe_file orelse writer.session.org_exe_file; 245 | var buffered_writer = std.io.bufferedWriter(outfile.writer()); 246 | var counting_writer = std.io.countingWriter(buffered_writer.writer()); 247 | var stream = counting_writer.writer(); 248 | 249 | // Write original executable if we're not stitching to the original, otherwise seek to the end of original 250 | if (writer.session.output_exe_file != null) { 251 | var buffered_reader = std.io.bufferedReader(writer.session.org_exe_file.reader()); 252 | try copyBytes(buffered_reader.reader(), buffered_writer.writer()); 253 | 254 | // Flush to ensure the file length is correct when queried 255 | try buffered_writer.flush(); 256 | } else { 257 | try outfile.seekFromEnd(0); 258 | } 259 | 260 | // No resources = write empty tail 261 | if (writer.exe.resources.items.len == 0) { 262 | try stream.writeInt(u64, 0, .big); 263 | try stream.writeByte(StitchVersion); 264 | try stream.writeInt(u64, EofMagic, .big); 265 | try buffered_writer.flush(); 266 | return; 267 | } 268 | 269 | // This is zero if we're writing not stitching to the original. 270 | // In that case, we set this when we know the length of the first input (which must be the executable) 271 | const exe_file_len = try outfile.getEndPos(); 272 | 273 | // Keeps track of offsets relative to the end of th original executable 274 | // This is used to compute resource indices 275 | var resource_offsets = std.ArrayList(u64).init(writer.session.arena.allocator()); 276 | try resource_offsets.append(exe_file_len); 277 | var resource_lengths = std.ArrayList(u64).init(writer.session.arena.allocator()); 278 | 279 | // Append resources, each prefixed with resource magic 280 | for (writer.exe.resources.items) |*item| { 281 | const written_before = counting_writer.bytes_written; 282 | try stream.writeInt(u64, ResourceMagic, .big); 283 | switch (item.data) { 284 | .bytes => { 285 | try stream.writeAll(item.data.bytes); 286 | }, 287 | .reader => { 288 | try copyBytes(item.data.reader, stream); 289 | }, 290 | .path => { 291 | var file = std.fs.cwd().openFile(item.data.path, .{ .mode = .read_only }) catch |err| switch (err) { 292 | std.fs.File.OpenError.FileNotFound => { 293 | writer.session.diagnostics = .{ .CouldNotOpenInputFile = item.data.path }; 294 | return StitchError.CouldNotOpenInputFile; 295 | }, 296 | else => return err, 297 | }; 298 | defer file.close(); 299 | try copyBytes(file.reader(), stream); 300 | }, 301 | } 302 | try resource_offsets.append(exe_file_len + counting_writer.bytes_written); 303 | try resource_lengths.append(counting_writer.bytes_written - written_before - 8); 304 | } 305 | 306 | const index_offset = exe_file_len + counting_writer.bytes_written; 307 | 308 | // Write the index 309 | try stream.writeInt(u64, writer.exe.index.entries.items.len, .big); 310 | for (writer.exe.index.entries.items, 0..) |*entry, i| { 311 | try stream.writeInt(u64, entry.name.len, .big); 312 | try stream.writeAll(entry.name); 313 | try stream.writeByte(entry.resource_type); 314 | try stream.writeInt(u64, resource_offsets.items[i], .big); 315 | try stream.writeInt(u64, resource_lengths.items[i], .big); 316 | try stream.writeAll(&entry.scratch_bytes); 317 | } 318 | 319 | // Write the tail 320 | try stream.writeInt(u64, index_offset, .big); 321 | try stream.writeByte(StitchVersion); 322 | try stream.writeInt(u64, EofMagic, .big); 323 | try buffered_writer.flush(); 324 | } 325 | 326 | // Copy bytes from a reader to a writer, until EOF 327 | fn copyBytes(reader: anytype, writer: anytype) !void { 328 | var buffered_reader = std.io.bufferedReader(reader); 329 | var instream = buffered_reader.reader(); 330 | read_loop: while (true) { 331 | const byte = instream.readByte() catch |err| switch (err) { 332 | error.EndOfStream => break :read_loop, 333 | else => return err, 334 | }; 335 | try writer.writeByte(byte); 336 | } 337 | } 338 | 339 | /// Set the scratch bytes for a resource, using the index returned by the addResource... functions. 340 | /// The default scratch bytes is all-zero. 341 | pub fn setScratchBytes(writer: *StitchWriter, resource_index: u64, bytes: [8]u8) StitchError!void { 342 | writer.session.resetDiagnostics(); 343 | if (resource_index >= writer.exe.index.entries.items.len) { 344 | writer.session.diagnostics = .{ .ResourceNotFound = .{ .index = resource_index } }; 345 | return StitchError.ResourceNotFound; 346 | } 347 | writer.exe.index.entries.items[resource_index].scratch_bytes = bytes; 348 | } 349 | 350 | /// Reads the file at `path` and adds it to the list of resources to be written 351 | /// This option has minimal memory overhead 352 | /// If name is null, the name of the resource will be the basename of the path 353 | /// Returns the zero-based resource index 354 | pub fn addResourceFromPath(writer: *StitchWriter, name: ?[]const u8, path: []const u8) !u64 { 355 | writer.session.resetDiagnostics(); 356 | try writer.exe.resources.append(Resource{ .magic = ResourceMagic, .data = .{ .path = path } }); 357 | try writer.exe.index.entries.append(IndexEntry{ 358 | .name = if (name != null) name.? else std.fs.path.basename(path), 359 | .resource_type = 0, 360 | .resource_offset = 0, 361 | .byte_length = 0, 362 | .scratch_bytes = [_]u8{0} ** 8, 363 | }); 364 | 365 | return writer.exe.resources.items.len - 1; 366 | } 367 | 368 | /// Adds the reader to the list of resources 369 | /// This option has minimal memory overhead 370 | /// The reader must stay valid until the the Stitch session is closed 371 | /// Returns the zero-based resource index 372 | pub fn addResourceFromReader(writer: *StitchWriter, name: []const u8, reader: std.fs.File.Reader) !u64 { 373 | writer.session.resetDiagnostics(); 374 | try writer.exe.resources.append(Resource{ .magic = ResourceMagic, .data = .{ .reader = reader } }); 375 | try writer.exe.index.entries.append(IndexEntry{ 376 | .name = name, 377 | .resource_type = 0, 378 | .resource_offset = 0, 379 | .byte_length = 0, 380 | .scratch_bytes = [_]u8{0} ** 8, 381 | }); 382 | 383 | return writer.exe.resources.items.len - 1; 384 | } 385 | 386 | /// Adds the slice to the list of resources 387 | /// The provided `data` buffer must stay valid until `commit` is called 388 | /// Returns the zero-based resource index 389 | pub fn addResourceFromSlice(writer: *StitchWriter, name: []const u8, data: []const u8) !u64 { 390 | writer.session.resetDiagnostics(); 391 | try writer.exe.resources.append(Resource{ .magic = ResourceMagic, .data = .{ .bytes = data } }); 392 | try writer.exe.index.entries.append(IndexEntry{ 393 | .name = name, 394 | .resource_type = 0, 395 | .resource_offset = 0, 396 | .byte_length = data.len, 397 | .scratch_bytes = [_]u8{0} ** 8, 398 | }); 399 | 400 | return writer.exe.resources.items.len - 1; 401 | } 402 | }; 403 | 404 | /// Reads the span of a resource, seeking to the start of the resource on first read 405 | /// and returning EOF if reading beyond the end of the resource. 406 | /// Use `StitchReader.getResourceReader` to create this reader. 407 | pub const StitchResourceReader = struct { 408 | underlying_file: std.fs.File, 409 | offset: u64, 410 | limited_reader: std.io.LimitedReader(std.fs.File.Reader), 411 | has_read_yet: bool = false, 412 | 413 | pub const FileError = std.fs.File.Reader.Error || std.fs.File.SeekError; 414 | pub const Reader = std.io.Reader(*StitchResourceReader, FileError, read); 415 | 416 | pub fn read(self: *StitchResourceReader, dest: []u8) FileError!usize { 417 | if (!self.has_read_yet) { 418 | self.has_read_yet = true; 419 | try self.underlying_file.seekTo(self.offset); 420 | } 421 | return self.limited_reader.read(dest); 422 | } 423 | 424 | pub fn reader(self: *StitchResourceReader) Reader { 425 | return .{ .context = self }; 426 | } 427 | }; 428 | 429 | fn stitchResourceReader(underlying_file: std.fs.File, offset: u64, bytes_left: u64) StitchResourceReader { 430 | return .{ .underlying_file = underlying_file, .offset = offset, .limited_reader = std.io.limitedReader(underlying_file.reader(), bytes_left) }; 431 | } 432 | 433 | /// Use `initReader` to create this reader, which allows you to read resources from a stitch file. 434 | pub const StitchReader = struct { 435 | session: *Self, 436 | exe: StitchExecutable = undefined, 437 | 438 | fn init(session: *Self) StitchReader { 439 | return .{ 440 | .session = session, 441 | .exe = .{ 442 | .resources = std.ArrayList(Resource).init(session.arena.allocator()), 443 | .index = .{ .entries = std.ArrayList(IndexEntry).init(session.arena.allocator()) }, 444 | .tail = .{ .index_offset = 0, .version = 0, .eof_magic = EofMagic }, 445 | }, 446 | }; 447 | } 448 | 449 | /// Closes the stitch reader session, freeing all resources 450 | pub fn deinit(reader: *StitchReader) void { 451 | reader.session.deinit(); 452 | } 453 | 454 | pub fn readMetadata(reader: *StitchReader) !void { 455 | reader.session.resetDiagnostics(); 456 | const len = try reader.session.org_exe_file.getEndPos(); 457 | if (len < 17) { 458 | reader.session.diagnostics = .{ .InvalidExecutableFormat = "File too short to contain stitch metadata" }; 459 | return StitchError.InvalidExecutableFormat; 460 | } 461 | 462 | // Read the tail 463 | try reader.session.org_exe_file.seekFromEnd(-17); 464 | var in = reader.session.org_exe_file.reader(); 465 | const index_offset = try in.readInt(u64, .big); 466 | reader.exe.tail.version = try in.readByte(); 467 | reader.exe.tail.eof_magic = try in.readInt(u64, .big); 468 | if (reader.exe.tail.eof_magic != EofMagic) { 469 | reader.session.diagnostics = .{ .InvalidExecutableFormat = "Invalid stitch EOF magic" }; 470 | return StitchError.InvalidExecutableFormat; 471 | } 472 | 473 | // No index means there are no resources 474 | if (index_offset == 0) return; 475 | 476 | // Seek to the index, and read it 477 | var ally = reader.session.arena.allocator(); 478 | try reader.session.org_exe_file.seekTo(index_offset); 479 | const entry_count = try in.readInt(u64, .big); 480 | for (0..entry_count) |_| { 481 | const name_len = try in.readInt(u64, .big); 482 | const name: []const u8 = _: { 483 | if (name_len == 0) break :_ ""; 484 | const buffer = try ally.alloc(u8, name_len); 485 | _ = try in.readAll(buffer); 486 | break :_ buffer; 487 | }; 488 | const resource_type = try in.readByte(); 489 | const resource_offset = try in.readInt(u64, .big); 490 | const byte_length = try in.readInt(u64, .big); 491 | const scratch_bytes = _: { 492 | const buffer = try ally.alloc(u8, 8); 493 | _ = try in.readAll(buffer); 494 | break :_ buffer; 495 | }; 496 | 497 | try reader.exe.index.entries.append(IndexEntry{ 498 | .name = name, 499 | .resource_type = resource_type, 500 | .resource_offset = resource_offset, 501 | .byte_length = byte_length, 502 | .scratch_bytes = scratch_bytes[0..8].*, 503 | }); 504 | } 505 | } 506 | 507 | /// Returns the version of the stitch format used to write the executable 508 | pub fn getFormatVersion(reader: *StitchReader) u8 { 509 | return reader.exe.tail.version; 510 | } 511 | 512 | /// Given a resource name, returns the index of the resource. This can be passed 513 | /// to `getResourceAsSlice` or `getResourceReader` to read the resource. 514 | pub fn getResourceIndex(reader: *StitchReader, name: []const u8) !usize { 515 | reader.session.resetDiagnostics(); 516 | for (reader.exe.index.entries.items, 0..) |entry, index| { 517 | if (std.mem.eql(u8, entry.name, name)) return index; 518 | } 519 | 520 | reader.session.diagnostics = .{ .ResourceNotFound = .{ .name = "Resource not found" } }; 521 | return StitchError.ResourceNotFound; 522 | } 523 | 524 | /// Returns the size of the resource in bytes. 525 | pub fn getResourceSize(reader: *StitchReader, resource_index: usize) !u64 { 526 | reader.session.resetDiagnostics(); 527 | if (resource_index > reader.exe.index.entries.items.len) { 528 | reader.session.diagnostics = .{ .ResourceNotFound = .{ .index = resource_index } }; 529 | return StitchError.ResourceNotFound; 530 | } 531 | return reader.exe.index.entries.items[resource_index].byte_length; 532 | } 533 | 534 | /// Fully reads the resource into memory and returns it. The memory is freed when the session is closed. 535 | pub fn getResourceAsSlice(reader: *StitchReader, resource_index: usize) ![]const u8 { 536 | reader.session.resetDiagnostics(); 537 | if (resource_index > reader.exe.index.entries.items.len) { 538 | reader.session.diagnostics = .{ .ResourceNotFound = .{ .index = resource_index } }; 539 | return StitchError.ResourceNotFound; 540 | } 541 | 542 | var ally = reader.session.arena.allocator(); 543 | 544 | // Get the offset from the index and read the resource 545 | const offset = reader.exe.index.entries.items[resource_index].resource_offset; 546 | const length = reader.exe.index.entries.items[resource_index].byte_length; 547 | 548 | // Seek to the resource and read it 549 | reader.session.org_exe_file.seekTo(offset) catch { 550 | reader.session.diagnostics = .{ .IoError = "Failed to seek to resource" }; 551 | return StitchError.IoError; 552 | }; 553 | 554 | var file_reader = reader.session.org_exe_file.reader(); 555 | 556 | const resource_magic = file_reader.readInt(u64, .big) catch { 557 | reader.session.diagnostics = .{ .IoError = "Failed to read resource magic" }; 558 | return StitchError.IoError; 559 | }; 560 | 561 | if (resource_magic != ResourceMagic) { 562 | reader.session.diagnostics = .{ .InvalidExecutableFormat = "Invalid resource magic" }; 563 | return StitchError.InvalidExecutableFormat; 564 | } 565 | 566 | const buffer = try ally.alloc(u8, length); 567 | _ = file_reader.readAll(buffer) catch { 568 | reader.session.diagnostics = .{ .IoError = "Failed to read resource bytes" }; 569 | return StitchError.IoError; 570 | }; 571 | return buffer; 572 | } 573 | 574 | /// Returns a file reader for the resource. The reader is closed when the session is closed. 575 | /// This option requires the least amount of memory. 576 | pub fn getResourceReader(reader: *StitchReader, resource_index: usize) StitchError!StitchResourceReader { 577 | reader.session.resetDiagnostics(); 578 | if (resource_index > reader.exe.index.entries.items.len) { 579 | reader.session.diagnostics = .{ .ResourceNotFound = .{ .index = resource_index } }; 580 | return StitchError.ResourceNotFound; 581 | } 582 | 583 | // Get offset and length from the index 584 | const offset = reader.exe.index.entries.items[resource_index].resource_offset; 585 | const length = reader.exe.index.entries.items[resource_index].byte_length; 586 | 587 | reader.session.org_exe_file.seekTo(offset) catch { 588 | reader.session.diagnostics = .{ .IoError = "Failed to seek to resource" }; 589 | return StitchError.IoError; 590 | }; 591 | var file_reader = reader.session.org_exe_file.reader(); 592 | const resource_magic = file_reader.readInt(u64, .big) catch { 593 | reader.session.diagnostics = .{ .IoError = "Failed to read resource magic" }; 594 | return StitchError.IoError; 595 | }; 596 | if (resource_magic != ResourceMagic) { 597 | reader.session.diagnostics = .{ .InvalidExecutableFormat = "Invalid resource magic" }; 598 | return StitchError.InvalidExecutableFormat; 599 | } 600 | 601 | return stitchResourceReader(reader.session.org_exe_file, offset + 8, length); 602 | } 603 | 604 | /// Returns the scratch bytes for the resource, which is all-zeros if not set specifically. 605 | pub fn getScratchBytes(reader: *StitchReader, resource_index: usize) ![]const u8 { 606 | reader.session.resetDiagnostics(); 607 | if (resource_index > reader.exe.index.entries.items.len) { 608 | reader.session.diagnostics = .{ .ResourceNotFound = .{ .index = resource_index } }; 609 | return StitchError.ResourceNotFound; 610 | } 611 | 612 | return &reader.exe.index.entries.items[resource_index].scratch_bytes; 613 | } 614 | 615 | /// Returns the total number of resources in the executable. This may be zero. 616 | pub fn getResourceCount(reader: *StitchReader) u64 { 617 | return reader.exe.index.entries.items.len; 618 | } 619 | }; 620 | 621 | /// Returns the path to the currently running executable. 622 | /// It's usually not necessary to call this function directly. 623 | pub fn getSelfPath(session: *Self) StitchError![]const u8 { 624 | return std.fs.selfExeDirPathAlloc(session.arena.allocator()) catch return StitchError.IoError; 625 | } 626 | 627 | /// Reads the entire contents of a file and returns it as a byte slice. 628 | /// The memory is freed when the session is closed. 629 | pub fn readEntireFile(session: *Self, path: []const u8) StitchError![]const u8 { 630 | var arena_allocator = session.arena.allocator(); 631 | errdefer { 632 | session.diagnostics = .{ .IoError = "Failed to read file" }; 633 | } 634 | const absolute_path = std.fs.realpathAlloc(arena_allocator, path) catch return StitchError.IoError; 635 | var file = std.fs.openFileAbsolute(absolute_path, .{ .mode = .read_write }) catch return StitchError.IoError; 636 | defer file.close(); 637 | var reader = file.reader(); 638 | const file_size = file.getEndPos() catch return StitchError.IoError; 639 | const buffer = arena_allocator.alloc(u8, file_size) catch return StitchError.IoError; 640 | _ = reader.readAll(buffer) catch return StitchError.IoError; 641 | return buffer; 642 | } 643 | 644 | // Create a tempoary directory structure with a few files for testing 645 | pub fn testSetup() !void { 646 | // Clean up in case the previous test run terminated early 647 | testTeardown(); 648 | 649 | // Create the directory structure 650 | try std.fs.cwd().makeDir(".stitch"); 651 | try std.fs.cwd().makeDir(".stitch/subdir"); 652 | 653 | { 654 | var file = try std.fs.cwd().createFile(".stitch/executable", .{}); 655 | defer file.close(); 656 | try file.writer().print("Executable bytes goes here", .{}); 657 | } 658 | { 659 | var file = try std.fs.cwd().createFile(".stitch/one.txt", .{}); 660 | defer file.close(); 661 | try file.writer().print("Hello world", .{}); 662 | } 663 | { 664 | var file = try std.fs.cwd().createFile(".stitch/two.txt", .{}); 665 | defer file.close(); 666 | try file.writer().print("Hello\nWorld", .{}); 667 | } 668 | { 669 | var file = try std.fs.cwd().createFile(".stitch/three.txt", .{}); 670 | defer file.close(); 671 | try file.writer().print("A third file", .{}); 672 | } 673 | } 674 | 675 | // Delete the temporary directory structure 676 | pub fn testTeardown() void { 677 | std.fs.cwd().deleteFile(".stitch/executable") catch {}; 678 | std.fs.cwd().deleteFile(".stitch/new-executable") catch {}; 679 | std.fs.cwd().deleteFile(".stitch/one.txt") catch {}; 680 | std.fs.cwd().deleteFile(".stitch/two.txt") catch {}; 681 | std.fs.cwd().deleteFile(".stitch/three.txt") catch {}; 682 | std.fs.cwd().deleteDir(".stitch/subdir") catch {}; 683 | std.fs.cwd().deleteDir(".stitch") catch {}; 684 | } 685 | 686 | // Create a cryptographically unique filename; mostly useful for test purposes 687 | pub fn generateUniqueFileName(allocator: std.mem.Allocator) ![]const u8 { 688 | var output: [16]u8 = undefined; 689 | var secret_seed: [std.Random.DefaultCsprng.secret_seed_length]u8 = undefined; 690 | std.crypto.random.bytes(&secret_seed); 691 | var csprng = std.Random.DefaultCsprng.init(secret_seed); 692 | const random = csprng.random(); 693 | random.bytes(&output); 694 | 695 | // Allocate enough for the hex string, plus the ".tmp" suffix 696 | const buf = try allocator.alloc(u8, output.len * 2 + 4); 697 | return std.fmt.bufPrint(buf, "{s}.tmp", .{std.fmt.fmtSliceHexLower(&output)}); 698 | } 699 | 700 | /// C ABI exported interface. See stitch.h for function-level documentation. 701 | /// 702 | /// This follows the pthreads C API design, where a stitch session is an opaque pointer and all functions 703 | /// are called with the session as the first argument. C ABI clients never deals with structs or enums directly. 704 | /// 705 | /// To interact with stich from C, a writer or reader session must be created. 706 | /// All returned data is owned by the session. Copy any data you need to keep after the session is closed, 707 | /// or keep the session open until you are done with the data. 708 | pub export fn stitch_init_writer(input_executable_path: ?[*:0]const u8, output_executable_path: ?[*:0]const u8, error_code: *u64) callconv(.C) ?*anyopaque { 709 | error_code.* = 0; 710 | if (input_executable_path == null) { 711 | error_code.* = translateError(StitchError.CouldNotOpenInputFile); 712 | return null; 713 | } 714 | const allocator = if (builtin.link_libc) std.heap.c_allocator else std.heap.smp_allocator; 715 | const writer = initWriter(allocator, std.mem.span(input_executable_path.?), if (output_executable_path) |path| std.mem.span(path) else std.mem.span(input_executable_path.?)) catch |err| { 716 | error_code.* = translateError(err); 717 | return null; 718 | }; 719 | return writer.session; 720 | } 721 | 722 | pub export fn stitch_init_reader(executable_path: ?[*:0]const u8, error_code: *u64) callconv(.C) ?*anyopaque { 723 | error_code.* = 0; 724 | if (executable_path == null) { 725 | error_code.* = translateError(StitchError.CouldNotOpenInputFile); 726 | return null; 727 | } 728 | const allocator = if (builtin.link_libc) std.heap.c_allocator else std.heap.smp_allocator; 729 | const reader = initReader(allocator, if (executable_path) |p| std.mem.span(p) else null) catch |err| { 730 | error_code.* = translateError(err); 731 | return null; 732 | }; 733 | return reader.session; 734 | } 735 | 736 | pub export fn stitch_deinit(session: *anyopaque) callconv(.C) void { 737 | fromC(session).deinit(); 738 | } 739 | 740 | pub export fn stitch_reader_get_resource_count(reader: *anyopaque) callconv(.C) u64 { 741 | return fromC(reader).rw.reader.getResourceCount(); 742 | } 743 | 744 | pub export fn stitch_reader_get_format_version(reader: *anyopaque) callconv(.C) u8 { 745 | return fromC(reader).rw.reader.getFormatVersion(); 746 | } 747 | 748 | pub export fn stitch_reader_get_resource_index(reader: *anyopaque, name: [*:0]const u8, error_code: *u64) callconv(.C) u64 { 749 | return fromC(reader).rw.reader.getResourceIndex(std.mem.span(name)) catch |err| { 750 | error_code.* = translateError(err); 751 | return std.math.maxInt(u64); 752 | }; 753 | } 754 | 755 | pub export fn stitch_reader_get_resource_byte_len(reader: *anyopaque, resource_index: u64, error_code: *u64) callconv(.C) u64 { 756 | return fromC(reader).rw.reader.getResourceSize(resource_index) catch |err| { 757 | error_code.* = translateError(err); 758 | return std.math.maxInt(u64); 759 | }; 760 | } 761 | 762 | pub export fn stitch_reader_get_resource_bytes(reader: *anyopaque, resource_index: u64, error_code: *u64) callconv(.C) ?[*]const u8 { 763 | const slice = fromC(reader).rw.reader.getResourceAsSlice(resource_index) catch |err| { 764 | error_code.* = translateError(err); 765 | return null; 766 | }; 767 | return slice.ptr; 768 | } 769 | 770 | pub export fn stitch_reader_get_scratch_bytes(reader: *anyopaque, resource_index: u64, error_code: *u64) callconv(.C) ?[*]const u8 { 771 | error_code.* = 0; 772 | const slice = fromC(reader).rw.reader.getScratchBytes(resource_index) catch |err| { 773 | error_code.* = translateError(err); 774 | return null; 775 | }; 776 | return slice.ptr; 777 | } 778 | 779 | pub export fn stitch_writer_commit(writer: *anyopaque, error_code: *u64) callconv(.C) void { 780 | fromC(writer).rw.writer.commit() catch |err| { 781 | error_code.* = translateError(err); 782 | }; 783 | } 784 | 785 | pub export fn stitch_writer_add_resource_from_path(writer: *anyopaque, name: [*:0]const u8, path: [*:0]const u8, error_code: *u64) callconv(.C) u64 { 786 | return fromC(writer).rw.writer.addResourceFromPath(std.mem.span(name), std.mem.span(path)) catch |err| { 787 | error_code.* = translateError(err); 788 | return std.math.maxInt(u64); 789 | }; 790 | } 791 | 792 | pub export fn stitch_writer_add_resource_from_bytes(writer: *anyopaque, name: [*:0]const u8, bytes: [*]const u8, len: usize, error_code: *u64) callconv(.C) u64 { 793 | return fromC(writer).rw.writer.addResourceFromSlice(std.mem.span(name), bytes[0..len]) catch |err| { 794 | error_code.* = translateError(err); 795 | return std.math.maxInt(u64); 796 | }; 797 | } 798 | 799 | pub export fn stitch_writer_set_scratch_bytes(writer: *anyopaque, resource_index: u64, bytes: [*]const u8, error_code: *u64) callconv(.C) void { 800 | fromC(writer).rw.writer.setScratchBytes(resource_index, bytes[0..8].*) catch |err| { 801 | error_code.* = translateError(err); 802 | }; 803 | } 804 | 805 | pub export fn stitch_read_entire_file(reader_or_writer: *anyopaque, path: [*:0]const u8, error_code: *u64) callconv(.C) ?[*]const u8 { 806 | const s = fromC(reader_or_writer); 807 | switch (s.rw) { 808 | inline else => |rw| { 809 | const slice = rw.session.readEntireFile(std.mem.span(path)) catch |err| { 810 | error_code.* = translateError(err); 811 | return null; 812 | }; 813 | return slice.ptr; 814 | }, 815 | } 816 | return null; 817 | } 818 | 819 | pub export fn stitch_get_last_error_diagnostic(session: ?*anyopaque) callconv(.C) ?[*:0]const u8 { 820 | if (session == null) return "Could not get diagnostic: Invalid session"; 821 | var s = fromC(session.?); 822 | if (s.getDiagnostics()) |d| { 823 | const str = d.toOwnedString(s.arena.allocator()) catch return null; 824 | return s.arena.allocator().dupeZ(u8, str) catch return null; 825 | } else return null; 826 | } 827 | 828 | pub export fn stitch_get_error_diagnostic(error_code: u64) callconv(.C) ?[*:0]const u8 { 829 | switch (error_code) { 830 | 2 => return "Output file already exists", 831 | 3 => return "Could not open input file", 832 | 4 => return "Could not open output file", 833 | 5 => return "Invalid executable format", 834 | 6 => return "Resource not found", 835 | 7 => return "I/O error", 836 | else => return "Unknown error code", 837 | } 838 | } 839 | 840 | pub export fn stitch_test_setup() callconv(.C) void { 841 | testSetup() catch unreachable; 842 | } 843 | 844 | pub export fn stitch_test_teardown() callconv(.C) void { 845 | testTeardown(); 846 | } 847 | 848 | // Convert from a C ABI pointer to a Zig pointer to self 849 | fn fromC(session: *anyopaque) *Self { 850 | return @ptrCast(@alignCast(session)); 851 | } 852 | 853 | // Map Zig errors to C error codes 854 | fn translateError(err: anyerror) u64 { 855 | return switch (err) { 856 | StitchError.OutputFileAlreadyExists => 2, 857 | StitchError.CouldNotOpenInputFile => 3, 858 | StitchError.CouldNotOpenOutputFile => 4, 859 | StitchError.InvalidExecutableFormat => 5, 860 | StitchError.ResourceNotFound => 6, 861 | StitchError.IoError => 7, 862 | else => 1, 863 | }; 864 | } 865 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | //! The Stitch command line tool 2 | const std = @import("std"); 3 | const Stitch = @import("stitch"); 4 | const StitchError = Stitch.StitchError; 5 | 6 | /// The stitch command-line tool, implemented using the stitch library 7 | pub fn main() !u8 { 8 | var gpa = std.heap.GeneralPurposeAllocator(.{ .safety = true }){}; 9 | defer _ = gpa.deinit(); 10 | const backing_allocator = gpa.allocator(); 11 | var arena = std.heap.ArenaAllocator.init(backing_allocator); 12 | defer arena.deinit(); 13 | const allocator = arena.allocator(); 14 | 15 | const cmdline = try Cmdline.parseArgs(allocator); 16 | 17 | // Create a stitcher 18 | var stitcher = Stitch.initWriter(backing_allocator, cmdline.input_files_paths.values()[0], cmdline.output_file_path) catch |err| { 19 | switch (err) { 20 | StitchError.OutputFileAlreadyExists => { 21 | try std.io.getStdErr().writer().print("Output file already exists: {s}\n", .{cmdline.output_file_path}); 22 | return 1; 23 | }, 24 | else => return err, 25 | } 26 | }; 27 | defer stitcher.deinit(); 28 | 29 | // Add resources as specified on the command line 30 | for (cmdline.input_files_paths.values()[1..]) |path| { 31 | _ = try stitcher.addResourceFromPath(null, path); 32 | } 33 | 34 | // Commit changes to file 35 | stitcher.commit() catch |err| { 36 | if (stitcher.session.getDiagnostics()) |diagnostics| { 37 | try diagnostics.print(stitcher.session.arena.allocator()); 38 | } else { 39 | try std.io.getStdErr().writer().print("Error: {s}\n", .{@errorName(err)}); 40 | } 41 | }; 42 | 43 | return 0; 44 | } 45 | 46 | /// Command line parser. The argument syntax is simple enough to do this without an external lib. 47 | /// 48 | /// First argument is the executable to stitch onto. If an output file is specified, the input executable will not be touched. 49 | /// ./stitch /path/to/myexecutable file1.txt file2.txt --output myexecutable 50 | /// 51 | /// Resources can be given a different name than the basename of the file 52 | /// ./stitch my.exe script=main.js lib=lib.js --output my.exe 53 | /// 54 | /// Note that --ouput is optional. If missing, the output file will be the same as the first input file (the executable) 55 | /// ./stitch ./myexecutable file1.txt newname=file2.txt 56 | pub const Cmdline = struct { 57 | const help = 58 | \\Usage: 59 | \\ stitch ... [--output ] 60 | \\ stitch =... [--output ] 61 | \\ stitch --version 62 | \\ stitch --help 63 | \\ 64 | ; 65 | 66 | // Input files to stitch 67 | input_files_paths: std.StringArrayHashMap([]const u8) = undefined, 68 | 69 | // If not specified, the output file will be the same as the first input file 70 | output_file_path: []const u8 = "", 71 | 72 | /// Print usage 73 | fn usage() noreturn { 74 | std.io.getStdErr().writer().print(help, .{}) catch unreachable; 75 | std.process.exit(0); 76 | } 77 | 78 | /// Loop through arguments and extract input files and output name 79 | /// The first input file is the binary onto which the rest of the files are stitched. 80 | /// Thus, at least two inputs must be given. The "--output " argument is required 81 | /// and must appear at the end of the arguments. 82 | fn parseArgs(allocator: std.mem.Allocator) !*Cmdline { 83 | var cmdline = try allocator.create(Cmdline); 84 | cmdline.* = .{ .input_files_paths = std.StringArrayHashMap([]const u8).init(allocator), .output_file_path = "" }; 85 | 86 | var arg_it = try std.process.argsWithAllocator(allocator); 87 | defer arg_it.deinit(); 88 | if (!arg_it.skip()) @panic("Missing process argument"); 89 | 90 | while (arg_it.next()) |arg| { 91 | if (std.mem.startsWith(u8, arg, "--") and !std.mem.eql(u8, arg, "--output") and !std.mem.eql(u8, arg, "--version") and !std.mem.eql(u8, arg, "--help")) { 92 | try std.io.getStdErr().writer().print("Unknown argument: {s}\n\n", .{arg}); 93 | usage(); 94 | } 95 | if (std.mem.eql(u8, arg, "--help")) { 96 | usage(); 97 | } 98 | if (std.mem.eql(u8, arg, "--version")) { 99 | // Format version determines the major version number 100 | try std.io.getStdOut().writer().print("stitch version {d}.0.0\n", .{Stitch.StitchVersion}); 101 | std.process.exit(0); 102 | } 103 | if (std.mem.eql(u8, arg, "--output") or std.mem.eql(u8, arg, "-o")) { 104 | if (arg_it.next()) |output| { 105 | cmdline.output_file_path = output; 106 | if (arg_it.next() != null) { 107 | try std.io.getStdErr().writer().print("The last argument must be --output ", .{}); 108 | usage(); 109 | } 110 | break; 111 | } 112 | } else { 113 | // The filename is stored in the index, so it can be found by name. By using name=path, an alternative name can be given 114 | var it = std.mem.splitScalar(u8, arg, '='); 115 | var name = it.next(); 116 | const second = it.next(); 117 | const path = if (second != null) second.? else name.?; 118 | name = if (second == null) std.fs.path.basename(path) else name; 119 | std.debug.print("name: '{s}', path: '{s}'\n", .{ name.?, path }); 120 | 121 | try cmdline.input_files_paths.put(try allocator.dupe(u8, name.?), try allocator.dupe(u8, path)); 122 | } 123 | } 124 | 125 | if (cmdline.input_files_paths.count() < 2) { 126 | try std.io.getStdErr().writer().print("At least two input files are required\n", .{}); 127 | usage(); 128 | } 129 | 130 | if (cmdline.output_file_path.len == 0) { 131 | cmdline.output_file_path = cmdline.input_files_paths.values()[0]; 132 | } 133 | 134 | return cmdline; 135 | } 136 | }; 137 | -------------------------------------------------------------------------------- /src/tests.zig: -------------------------------------------------------------------------------- 1 | //! Stitch test suite 2 | const std = @import("std"); 3 | const Stitch = @import("stitch"); 4 | const StitchError = Stitch.StitchError; 5 | 6 | test "write to new file, but it exists" { 7 | try Stitch.testSetup(); 8 | defer Stitch.testTeardown(); 9 | 10 | const allocator = std.heap.page_allocator; 11 | try std.testing.expectError(error.OutputFileAlreadyExists, Stitch.initWriter(allocator, ".stitch/one.txt", ".stitch/two.txt")); 12 | } 13 | 14 | test "append resources to new file and read them back" { 15 | try Stitch.testSetup(); 16 | defer Stitch.testTeardown(); 17 | 18 | var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 19 | defer arena.deinit(); 20 | const allocator = arena.allocator(); 21 | 22 | // Create a temporary file, with a random name, and delete it when we're done. 23 | const random_name = try Stitch.generateUniqueFileName(arena.allocator()); 24 | defer std.fs.cwd().deleteFile(random_name) catch unreachable; 25 | 26 | // Create stitch file 27 | { 28 | var writer = try Stitch.initWriter(allocator, ".stitch/executable", random_name); 29 | defer writer.deinit(); 30 | _ = try writer.addResourceFromPath("one", ".stitch/one.txt"); 31 | const index = try writer.addResourceFromPath(null, ".stitch/two.txt"); 32 | try writer.setScratchBytes(index, [8]u8{ 0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01, 0x01, 0x00 }); 33 | 34 | var file = try std.fs.cwd().openFile(".stitch/three.txt", .{}); 35 | defer file.close(); 36 | _ = try writer.addResourceFromReader("from-reader", file.reader()); 37 | _ = try writer.addResourceFromSlice(".stitch/two.txt", "Hello world"); 38 | try writer.commit(); 39 | } 40 | 41 | // Read it back and verify 42 | { 43 | var reader = try Stitch.initReader(allocator, random_name); 44 | defer reader.deinit(); 45 | try std.testing.expectEqual(reader.getFormatVersion(), Stitch.StitchVersion); 46 | try std.testing.expectEqual(reader.getResourceCount(), 4); 47 | 48 | // Test reading a resource fully as a slice 49 | var data = try reader.getResourceAsSlice(0); 50 | try std.testing.expectEqualSlices(u8, data, "Hello world"); 51 | 52 | const two_index = try reader.getResourceIndex("two.txt"); 53 | const scratch_bytes = try reader.getScratchBytes(two_index); 54 | try std.testing.expectEqualSlices(u8, scratch_bytes, &[8]u8{ 0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01, 0x01, 0x00 }); 55 | 56 | // Test reading a resource through a reader 57 | var rr = try reader.getResourceReader(two_index); 58 | data = try rr.reader().readAllAlloc(allocator, std.math.maxInt(u64)); 59 | try std.testing.expectEqualSlices(u8, data, "Hello\nWorld"); 60 | } 61 | } 62 | 63 | test "write executable with no resources" { 64 | try Stitch.testSetup(); 65 | defer Stitch.testTeardown(); 66 | 67 | var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 68 | defer arena.deinit(); 69 | 70 | // Create a temporary file, with a random name, and delete it when we're done. 71 | const random_name = try Stitch.generateUniqueFileName(arena.allocator()); 72 | defer std.fs.cwd().deleteFile(random_name) catch unreachable; 73 | 74 | // Create stitch file 75 | { 76 | const allocator = std.heap.page_allocator; 77 | var writer = try Stitch.initWriter(allocator, ".stitch/one.txt", random_name); 78 | defer writer.deinit(); 79 | try writer.commit(); 80 | } 81 | 82 | // Read it back and verify 83 | { 84 | var reader = try Stitch.initReader(arena.allocator(), random_name); 85 | defer reader.deinit(); 86 | try std.testing.expectEqual(reader.getFormatVersion(), Stitch.StitchVersion); 87 | try std.testing.expectEqual(reader.getResourceCount(), 0); 88 | } 89 | 90 | // Test session utility functions 91 | { 92 | var reader = try Stitch.initReader(arena.allocator(), random_name); 93 | defer reader.deinit(); 94 | 95 | const content = try reader.session.readEntireFile(".stitch/one.txt"); 96 | try std.testing.expectEqualSlices(u8, content, "Hello world"); 97 | try std.testing.expect((try reader.session.getSelfPath()).len > 0); 98 | } 99 | } 100 | 101 | test "read invalid exe, too small" { 102 | try Stitch.testSetup(); 103 | defer Stitch.testTeardown(); 104 | 105 | var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 106 | defer arena.deinit(); 107 | 108 | // Input file too small 109 | { 110 | const random_name = try Stitch.generateUniqueFileName(arena.allocator()); 111 | defer std.fs.cwd().deleteFile(random_name) catch unreachable; 112 | 113 | { 114 | var file = try std.fs.cwd().createFile(random_name, .{}); 115 | defer file.close(); 116 | try file.writer().print("abc", .{}); 117 | } 118 | try std.testing.expectError(StitchError.InvalidExecutableFormat, Stitch.initReader(arena.allocator(), random_name)); 119 | } 120 | 121 | // Bad magic 122 | { 123 | const random_name = try Stitch.generateUniqueFileName(arena.allocator()); 124 | defer std.fs.cwd().deleteFile(random_name) catch unreachable; 125 | 126 | { 127 | var file = try std.fs.cwd().createFile(random_name, .{}); 128 | defer file.close(); 129 | try file.writer().print("1234567890123456712345678901234567", .{}); 130 | } 131 | try std.testing.expectError(StitchError.InvalidExecutableFormat, Stitch.initReader(arena.allocator(), random_name)); 132 | } 133 | } 134 | --------------------------------------------------------------------------------