├── 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