notso-glb 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- notso_glb/__init__.py +38 -0
- notso_glb/__main__.py +6 -0
- notso_glb/analyzers/__init__.py +20 -0
- notso_glb/analyzers/bloat.py +117 -0
- notso_glb/analyzers/bones.py +100 -0
- notso_glb/analyzers/duplicates.py +71 -0
- notso_glb/analyzers/skinned_mesh.py +47 -0
- notso_glb/analyzers/uv_maps.py +59 -0
- notso_glb/cleaners/__init__.py +23 -0
- notso_glb/cleaners/bones.py +49 -0
- notso_glb/cleaners/duplicates.py +110 -0
- notso_glb/cleaners/mesh.py +183 -0
- notso_glb/cleaners/textures.py +116 -0
- notso_glb/cleaners/uv_maps.py +29 -0
- notso_glb/cleaners/vertex_groups.py +34 -0
- notso_glb/cli.py +330 -0
- notso_glb/exporters/__init__.py +8 -0
- notso_glb/exporters/gltf.py +647 -0
- notso_glb/utils/__init__.py +20 -0
- notso_glb/utils/blender.py +49 -0
- notso_glb/utils/constants.py +41 -0
- notso_glb/utils/gltfpack.py +273 -0
- notso_glb/utils/logging.py +421 -0
- notso_glb/utils/naming.py +24 -0
- notso_glb/wasm/__init__.py +32 -0
- notso_glb/wasm/constants.py +8 -0
- notso_glb/wasm/gltfpack.version +1 -0
- notso_glb/wasm/gltfpack.wasm +0 -0
- notso_glb/wasm/py.typed +0 -0
- notso_glb/wasm/runner.py +137 -0
- notso_glb/wasm/runtime.py +244 -0
- notso_glb/wasm/wasi.py +347 -0
- notso_glb-0.1.0.dist-info/METADATA +150 -0
- notso_glb-0.1.0.dist-info/RECORD +36 -0
- notso_glb-0.1.0.dist-info/WHEEL +4 -0
- notso_glb-0.1.0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""WASM runtime for gltfpack."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .wasi import WasiExit
|
|
9
|
+
from .wasi import WasiFilesystem
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _get_wasm_path() -> Path:
|
|
13
|
+
"""Get path to bundled gltfpack.wasm."""
|
|
14
|
+
return Path(__file__).parent / "gltfpack.wasm"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class GltfpackWasm(WasiFilesystem):
|
|
18
|
+
"""WASM-based gltfpack runner using wasmtime."""
|
|
19
|
+
|
|
20
|
+
def _get_export(self, name: str) -> Any:
|
|
21
|
+
"""Get a named export from the WASM instance."""
|
|
22
|
+
exports: Any = self._instance.exports(self._store) # type: ignore[union-attr]
|
|
23
|
+
return exports[name]
|
|
24
|
+
|
|
25
|
+
def _upload_argv(self, argv: list[str]) -> int:
|
|
26
|
+
"""Upload argument vector to WASM memory."""
|
|
27
|
+
encoded_args = [arg.encode("utf-8") for arg in argv]
|
|
28
|
+
buf_size = len(argv) * 4
|
|
29
|
+
for arg in encoded_args:
|
|
30
|
+
buf_size += len(arg) + 1
|
|
31
|
+
|
|
32
|
+
malloc = self._get_export("malloc")
|
|
33
|
+
buf: int = malloc(self._store, buf_size)
|
|
34
|
+
argp = buf + len(argv) * 4
|
|
35
|
+
|
|
36
|
+
self._refresh_memory()
|
|
37
|
+
assert self._memory_array is not None
|
|
38
|
+
|
|
39
|
+
for i, arg in enumerate(encoded_args):
|
|
40
|
+
self._set_u32(buf + i * 4, argp)
|
|
41
|
+
# Copy string bytes
|
|
42
|
+
for j, b in enumerate(arg):
|
|
43
|
+
self._memory_array[argp + j] = b
|
|
44
|
+
self._set_u8(argp + len(arg), 0)
|
|
45
|
+
argp += len(arg) + 1
|
|
46
|
+
|
|
47
|
+
return buf
|
|
48
|
+
|
|
49
|
+
def _initialize(self) -> None:
|
|
50
|
+
"""Initialize WASM instance with wasmtime."""
|
|
51
|
+
if self._instance is not None:
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
from wasmtime import Engine
|
|
55
|
+
from wasmtime import Func
|
|
56
|
+
from wasmtime import FuncType
|
|
57
|
+
from wasmtime import Linker
|
|
58
|
+
from wasmtime import Module
|
|
59
|
+
from wasmtime import Store
|
|
60
|
+
from wasmtime import ValType
|
|
61
|
+
|
|
62
|
+
engine = Engine()
|
|
63
|
+
self._store = Store(engine)
|
|
64
|
+
wasm_bytes = _get_wasm_path().read_bytes()
|
|
65
|
+
module = Module(engine, wasm_bytes)
|
|
66
|
+
|
|
67
|
+
linker = Linker(engine)
|
|
68
|
+
|
|
69
|
+
# Define WASI functions
|
|
70
|
+
linker.define(
|
|
71
|
+
self._store,
|
|
72
|
+
"wasi_snapshot_preview1",
|
|
73
|
+
"proc_exit",
|
|
74
|
+
Func(self._store, FuncType([ValType.i32()], []), self.wasi_proc_exit),
|
|
75
|
+
)
|
|
76
|
+
linker.define(
|
|
77
|
+
self._store,
|
|
78
|
+
"wasi_snapshot_preview1",
|
|
79
|
+
"fd_close",
|
|
80
|
+
Func(
|
|
81
|
+
self._store,
|
|
82
|
+
FuncType([ValType.i32()], [ValType.i32()]),
|
|
83
|
+
self.wasi_fd_close,
|
|
84
|
+
),
|
|
85
|
+
)
|
|
86
|
+
linker.define(
|
|
87
|
+
self._store,
|
|
88
|
+
"wasi_snapshot_preview1",
|
|
89
|
+
"fd_fdstat_get",
|
|
90
|
+
Func(
|
|
91
|
+
self._store,
|
|
92
|
+
FuncType([ValType.i32(), ValType.i32()], [ValType.i32()]),
|
|
93
|
+
self.wasi_fd_fdstat_get,
|
|
94
|
+
),
|
|
95
|
+
)
|
|
96
|
+
linker.define(
|
|
97
|
+
self._store,
|
|
98
|
+
"wasi_snapshot_preview1",
|
|
99
|
+
"path_open32",
|
|
100
|
+
Func(
|
|
101
|
+
self._store,
|
|
102
|
+
FuncType(
|
|
103
|
+
[ValType.i32()] * 9,
|
|
104
|
+
[ValType.i32()],
|
|
105
|
+
),
|
|
106
|
+
self.wasi_path_open32,
|
|
107
|
+
),
|
|
108
|
+
)
|
|
109
|
+
linker.define(
|
|
110
|
+
self._store,
|
|
111
|
+
"wasi_snapshot_preview1",
|
|
112
|
+
"path_filestat_get",
|
|
113
|
+
Func(
|
|
114
|
+
self._store,
|
|
115
|
+
FuncType([ValType.i32()] * 5, [ValType.i32()]),
|
|
116
|
+
self.wasi_path_filestat_get,
|
|
117
|
+
),
|
|
118
|
+
)
|
|
119
|
+
linker.define(
|
|
120
|
+
self._store,
|
|
121
|
+
"wasi_snapshot_preview1",
|
|
122
|
+
"fd_prestat_get",
|
|
123
|
+
Func(
|
|
124
|
+
self._store,
|
|
125
|
+
FuncType([ValType.i32(), ValType.i32()], [ValType.i32()]),
|
|
126
|
+
self.wasi_fd_prestat_get,
|
|
127
|
+
),
|
|
128
|
+
)
|
|
129
|
+
linker.define(
|
|
130
|
+
self._store,
|
|
131
|
+
"wasi_snapshot_preview1",
|
|
132
|
+
"fd_prestat_dir_name",
|
|
133
|
+
Func(
|
|
134
|
+
self._store,
|
|
135
|
+
FuncType([ValType.i32()] * 3, [ValType.i32()]),
|
|
136
|
+
self.wasi_fd_prestat_dir_name,
|
|
137
|
+
),
|
|
138
|
+
)
|
|
139
|
+
linker.define(
|
|
140
|
+
self._store,
|
|
141
|
+
"wasi_snapshot_preview1",
|
|
142
|
+
"path_remove_directory",
|
|
143
|
+
Func(
|
|
144
|
+
self._store,
|
|
145
|
+
FuncType([ValType.i32()] * 3, [ValType.i32()]),
|
|
146
|
+
self.wasi_path_remove_directory,
|
|
147
|
+
),
|
|
148
|
+
)
|
|
149
|
+
linker.define(
|
|
150
|
+
self._store,
|
|
151
|
+
"wasi_snapshot_preview1",
|
|
152
|
+
"fd_fdstat_set_flags",
|
|
153
|
+
Func(
|
|
154
|
+
self._store,
|
|
155
|
+
FuncType([ValType.i32(), ValType.i32()], [ValType.i32()]),
|
|
156
|
+
self.wasi_fd_fdstat_set_flags,
|
|
157
|
+
),
|
|
158
|
+
)
|
|
159
|
+
linker.define(
|
|
160
|
+
self._store,
|
|
161
|
+
"wasi_snapshot_preview1",
|
|
162
|
+
"fd_seek32",
|
|
163
|
+
Func(
|
|
164
|
+
self._store,
|
|
165
|
+
FuncType([ValType.i32()] * 4, [ValType.i32()]),
|
|
166
|
+
self.wasi_fd_seek32,
|
|
167
|
+
),
|
|
168
|
+
)
|
|
169
|
+
linker.define(
|
|
170
|
+
self._store,
|
|
171
|
+
"wasi_snapshot_preview1",
|
|
172
|
+
"fd_read",
|
|
173
|
+
Func(
|
|
174
|
+
self._store,
|
|
175
|
+
FuncType([ValType.i32()] * 4, [ValType.i32()]),
|
|
176
|
+
self.wasi_fd_read,
|
|
177
|
+
),
|
|
178
|
+
)
|
|
179
|
+
linker.define(
|
|
180
|
+
self._store,
|
|
181
|
+
"wasi_snapshot_preview1",
|
|
182
|
+
"fd_write",
|
|
183
|
+
Func(
|
|
184
|
+
self._store,
|
|
185
|
+
FuncType([ValType.i32()] * 4, [ValType.i32()]),
|
|
186
|
+
self.wasi_fd_write,
|
|
187
|
+
),
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
self._instance = linker.instantiate(self._store, module)
|
|
191
|
+
|
|
192
|
+
# Call constructors
|
|
193
|
+
exports: Any = self._instance.exports(self._store)
|
|
194
|
+
ctors = exports.get("__wasm_call_ctors")
|
|
195
|
+
if ctors:
|
|
196
|
+
ctors(self._store)
|
|
197
|
+
|
|
198
|
+
def pack(
|
|
199
|
+
self,
|
|
200
|
+
input_data: bytes,
|
|
201
|
+
input_name: str = "input.glb",
|
|
202
|
+
output_name: str = "output.glb",
|
|
203
|
+
args: list[str] | None = None,
|
|
204
|
+
) -> tuple[bool, bytes, str]:
|
|
205
|
+
"""
|
|
206
|
+
Run gltfpack on input data.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
input_data: Input GLB/glTF bytes
|
|
210
|
+
input_name: Virtual input filename
|
|
211
|
+
output_name: Virtual output filename
|
|
212
|
+
args: Additional gltfpack arguments
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
Tuple of (success, output_bytes, log_message)
|
|
216
|
+
"""
|
|
217
|
+
self._initialize()
|
|
218
|
+
self._init_fds()
|
|
219
|
+
|
|
220
|
+
self._fs_interface = {input_name: input_data}
|
|
221
|
+
|
|
222
|
+
argv = ["gltfpack", "-i", input_name, "-o", output_name]
|
|
223
|
+
if args:
|
|
224
|
+
argv.extend(args)
|
|
225
|
+
|
|
226
|
+
buf = self._upload_argv(argv)
|
|
227
|
+
|
|
228
|
+
pack_fn = self._get_export("pack")
|
|
229
|
+
try:
|
|
230
|
+
result: int = pack_fn(self._store, len(argv), buf)
|
|
231
|
+
except WasiExit as e:
|
|
232
|
+
# WASI proc_exit was called; treat non-zero as failure
|
|
233
|
+
result = e.exit_code
|
|
234
|
+
|
|
235
|
+
free_fn = self._get_export("free")
|
|
236
|
+
free_fn(self._store, buf)
|
|
237
|
+
|
|
238
|
+
log = self._output_buffer.decode("utf-8", errors="replace")
|
|
239
|
+
|
|
240
|
+
if result != 0:
|
|
241
|
+
return False, b"", log
|
|
242
|
+
|
|
243
|
+
output_data = self._fs_interface.get(output_name, b"")
|
|
244
|
+
return True, output_data, log
|
notso_glb/wasm/wasi.py
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
"""WASI filesystem implementation for gltfpack WASM."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ctypes
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from .constants import WASI_EBADF
|
|
10
|
+
from .constants import WASI_EFAULT
|
|
11
|
+
from .constants import WASI_EINVAL
|
|
12
|
+
from .constants import WASI_EIO
|
|
13
|
+
from .constants import WASI_ENOSYS
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class WasiExit(Exception):
|
|
17
|
+
"""Exception raised when WASI proc_exit is called."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, exit_code: int) -> None:
|
|
20
|
+
self.exit_code = exit_code
|
|
21
|
+
super().__init__(f"WASI process exited with code {exit_code}")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from wasmtime import Instance, Memory, Store
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class WasiFilesystem:
|
|
29
|
+
"""WASI filesystem implementation for in-memory file operations."""
|
|
30
|
+
|
|
31
|
+
def __init__(self) -> None:
|
|
32
|
+
self._store: Store | None = None
|
|
33
|
+
self._instance: Instance | None = None
|
|
34
|
+
self._fs_interface: dict[str, bytes] | None = None
|
|
35
|
+
self._output_buffer: bytearray = bytearray()
|
|
36
|
+
self._fds: dict[int, dict[str, Any]] = {}
|
|
37
|
+
self._memory_array: ctypes.Array | None = None
|
|
38
|
+
|
|
39
|
+
def _init_fds(self) -> None:
|
|
40
|
+
"""Initialize file descriptors."""
|
|
41
|
+
self._output_buffer = bytearray()
|
|
42
|
+
self._fds = {
|
|
43
|
+
1: {"type": "output"}, # stdout
|
|
44
|
+
2: {"type": "output"}, # stderr
|
|
45
|
+
3: {"mount": "/", "path": "/"},
|
|
46
|
+
4: {"mount": "/gltfpack-$pwd", "path": ""},
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
def _next_fd(self) -> int:
|
|
50
|
+
"""Get next available file descriptor."""
|
|
51
|
+
fd = 5
|
|
52
|
+
while fd in self._fds:
|
|
53
|
+
fd += 1
|
|
54
|
+
return fd
|
|
55
|
+
|
|
56
|
+
# Memory access methods
|
|
57
|
+
|
|
58
|
+
def _get_memory(self) -> Memory:
|
|
59
|
+
"""Get WASM memory export."""
|
|
60
|
+
if self._instance is None or self._store is None:
|
|
61
|
+
raise RuntimeError("[ERROR] WASI runtime is not initialized")
|
|
62
|
+
from wasmtime import Memory
|
|
63
|
+
|
|
64
|
+
exports: Any = self._instance.exports(self._store)
|
|
65
|
+
memory = exports["memory"]
|
|
66
|
+
assert isinstance(memory, Memory)
|
|
67
|
+
return memory
|
|
68
|
+
|
|
69
|
+
def _refresh_memory(self) -> None:
|
|
70
|
+
"""Refresh memory array reference (needed after memory growth)."""
|
|
71
|
+
memory = self._get_memory()
|
|
72
|
+
ptr = memory.data_ptr(self._store) # type: ignore[arg-type]
|
|
73
|
+
size = memory.data_len(self._store) # type: ignore[arg-type]
|
|
74
|
+
self._memory_array = (ctypes.c_ubyte * size).from_address(
|
|
75
|
+
ctypes.addressof(ptr.contents)
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def _check_bounds(self, func_name: str, offset: int, length: int) -> int:
|
|
79
|
+
"""Validate memory access bounds, return memory length."""
|
|
80
|
+
self._refresh_memory()
|
|
81
|
+
assert self._memory_array is not None
|
|
82
|
+
mem_len = len(self._memory_array)
|
|
83
|
+
if offset < 0:
|
|
84
|
+
raise ValueError(f"{func_name}: negative offset {offset} (length={length})")
|
|
85
|
+
if offset + length > mem_len:
|
|
86
|
+
raise ValueError(
|
|
87
|
+
f"{func_name}: out of bounds offset={offset} length={length} "
|
|
88
|
+
f"exceeds memory size {mem_len}"
|
|
89
|
+
)
|
|
90
|
+
return mem_len
|
|
91
|
+
|
|
92
|
+
def _get_string(self, offset: int, length: int) -> str:
|
|
93
|
+
"""Read string from WASM memory."""
|
|
94
|
+
self._check_bounds("_get_string", offset, length)
|
|
95
|
+
assert self._memory_array is not None
|
|
96
|
+
return bytes(self._memory_array[offset : offset + length]).decode("utf-8")
|
|
97
|
+
|
|
98
|
+
def _set_u8(self, offset: int, value: int) -> None:
|
|
99
|
+
"""Write uint8 to WASM memory."""
|
|
100
|
+
self._check_bounds("_set_u8", offset, 1)
|
|
101
|
+
assert self._memory_array is not None
|
|
102
|
+
self._memory_array[offset] = value & 0xFF
|
|
103
|
+
|
|
104
|
+
def _set_u32(self, offset: int, value: int) -> None:
|
|
105
|
+
"""Write uint32 (little-endian) to WASM memory."""
|
|
106
|
+
self._check_bounds("_set_u32", offset, 4)
|
|
107
|
+
assert self._memory_array is not None
|
|
108
|
+
val_bytes = value.to_bytes(4, "little")
|
|
109
|
+
for i, b in enumerate(val_bytes):
|
|
110
|
+
self._memory_array[offset + i] = b
|
|
111
|
+
|
|
112
|
+
def _get_u32(self, offset: int) -> int:
|
|
113
|
+
"""Read uint32 (little-endian) from WASM memory."""
|
|
114
|
+
self._check_bounds("_get_u32", offset, 4)
|
|
115
|
+
assert self._memory_array is not None
|
|
116
|
+
return int.from_bytes(bytes(self._memory_array[offset : offset + 4]), "little")
|
|
117
|
+
|
|
118
|
+
# WASI syscall implementations
|
|
119
|
+
|
|
120
|
+
def wasi_proc_exit(self, rval: int) -> None:
|
|
121
|
+
"""WASI proc_exit syscall."""
|
|
122
|
+
raise WasiExit(rval)
|
|
123
|
+
|
|
124
|
+
def wasi_fd_close(self, fd: int) -> int:
|
|
125
|
+
"""WASI fd_close syscall."""
|
|
126
|
+
if fd not in self._fds:
|
|
127
|
+
return WASI_EBADF
|
|
128
|
+
try:
|
|
129
|
+
fd_info = self._fds[fd]
|
|
130
|
+
if "close_data" in fd_info and self._fs_interface is not None:
|
|
131
|
+
name = fd_info.get("name", "")
|
|
132
|
+
data = fd_info["data"][: fd_info["size"]]
|
|
133
|
+
self._fs_interface[name] = bytes(data)
|
|
134
|
+
del self._fds[fd]
|
|
135
|
+
return 0
|
|
136
|
+
except (KeyError, TypeError, IndexError):
|
|
137
|
+
if fd in self._fds:
|
|
138
|
+
del self._fds[fd]
|
|
139
|
+
return WASI_EIO
|
|
140
|
+
|
|
141
|
+
def wasi_fd_fdstat_get(self, fd: int, stat: int) -> int:
|
|
142
|
+
"""WASI fd_fdstat_get syscall."""
|
|
143
|
+
if fd not in self._fds:
|
|
144
|
+
return WASI_EBADF
|
|
145
|
+
# Validate stat buffer can hold fdstat struct (24 bytes)
|
|
146
|
+
self._check_bounds("wasi_fd_fdstat_get", stat, 24)
|
|
147
|
+
fd_info = self._fds[fd]
|
|
148
|
+
# Determine filetype: 2=char device, 3=directory, 4=regular file
|
|
149
|
+
if fd_info.get("type") == "output":
|
|
150
|
+
filetype = 2 # character device (stdout/stderr)
|
|
151
|
+
elif "path" in fd_info:
|
|
152
|
+
filetype = 3 # directory
|
|
153
|
+
else:
|
|
154
|
+
filetype = 4 # regular file
|
|
155
|
+
self._set_u8(stat + 0, filetype)
|
|
156
|
+
self._set_u32(stat + 2, 0)
|
|
157
|
+
self._set_u32(stat + 8, 0)
|
|
158
|
+
self._set_u32(stat + 12, 0)
|
|
159
|
+
self._set_u32(stat + 16, 0)
|
|
160
|
+
self._set_u32(stat + 20, 0)
|
|
161
|
+
return 0
|
|
162
|
+
|
|
163
|
+
def wasi_path_open32(
|
|
164
|
+
self,
|
|
165
|
+
parent_fd: int,
|
|
166
|
+
dirflags: int,
|
|
167
|
+
path: int,
|
|
168
|
+
path_len: int,
|
|
169
|
+
oflags: int,
|
|
170
|
+
fs_rights_base: int,
|
|
171
|
+
fs_rights_inheriting: int,
|
|
172
|
+
fdflags: int,
|
|
173
|
+
opened_fd: int,
|
|
174
|
+
) -> int:
|
|
175
|
+
"""WASI path_open syscall (32-bit variant)."""
|
|
176
|
+
if parent_fd not in self._fds or "path" not in self._fds[parent_fd]:
|
|
177
|
+
return WASI_EBADF
|
|
178
|
+
|
|
179
|
+
file_path = self._fds[parent_fd]["path"] + self._get_string(path, path_len)
|
|
180
|
+
|
|
181
|
+
file_info: dict[str, Any] = {
|
|
182
|
+
"name": file_path,
|
|
183
|
+
"position": 0,
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if oflags & 1: # O_CREAT
|
|
187
|
+
file_info["data"] = bytearray(4096)
|
|
188
|
+
file_info["size"] = 0
|
|
189
|
+
file_info["close_data"] = True
|
|
190
|
+
else:
|
|
191
|
+
if self._fs_interface is None or file_path not in self._fs_interface:
|
|
192
|
+
return WASI_EIO
|
|
193
|
+
file_info["data"] = bytearray(self._fs_interface[file_path])
|
|
194
|
+
file_info["size"] = len(file_info["data"])
|
|
195
|
+
|
|
196
|
+
fd = self._next_fd()
|
|
197
|
+
self._fds[fd] = file_info
|
|
198
|
+
self._set_u32(opened_fd, fd)
|
|
199
|
+
return 0
|
|
200
|
+
|
|
201
|
+
def wasi_path_filestat_get(
|
|
202
|
+
self, parent_fd: int, flags: int, path: int, path_len: int, buf: int
|
|
203
|
+
) -> int:
|
|
204
|
+
"""WASI path_filestat_get syscall."""
|
|
205
|
+
if parent_fd not in self._fds or "path" not in self._fds[parent_fd]:
|
|
206
|
+
return WASI_EBADF
|
|
207
|
+
|
|
208
|
+
name = self._get_string(path, path_len)
|
|
209
|
+
for i in range(64):
|
|
210
|
+
self._set_u8(buf + i, 0)
|
|
211
|
+
|
|
212
|
+
filetype = 3 if name == "." else 4
|
|
213
|
+
self._set_u8(buf + 16, filetype)
|
|
214
|
+
return 0
|
|
215
|
+
|
|
216
|
+
def wasi_fd_prestat_get(self, fd: int, buf: int) -> int:
|
|
217
|
+
"""WASI fd_prestat_get syscall."""
|
|
218
|
+
if fd not in self._fds or "path" not in self._fds[fd]:
|
|
219
|
+
return WASI_EBADF
|
|
220
|
+
|
|
221
|
+
mount = self._fds[fd].get("mount", "").encode("utf-8")
|
|
222
|
+
self._set_u8(buf, 0)
|
|
223
|
+
self._set_u32(buf + 4, len(mount))
|
|
224
|
+
return 0
|
|
225
|
+
|
|
226
|
+
def wasi_fd_prestat_dir_name(self, fd: int, path: int, path_len: int) -> int:
|
|
227
|
+
"""WASI fd_prestat_dir_name syscall."""
|
|
228
|
+
if fd not in self._fds or "path" not in self._fds[fd]:
|
|
229
|
+
return WASI_EBADF
|
|
230
|
+
|
|
231
|
+
mount = self._fds[fd].get("mount", "").encode("utf-8")
|
|
232
|
+
if path_len != len(mount):
|
|
233
|
+
return WASI_EINVAL
|
|
234
|
+
|
|
235
|
+
self._refresh_memory()
|
|
236
|
+
assert self._memory_array is not None
|
|
237
|
+
|
|
238
|
+
# Bounds check: ensure destination range is within memory
|
|
239
|
+
if path < 0 or path + path_len > len(self._memory_array):
|
|
240
|
+
return WASI_EFAULT
|
|
241
|
+
|
|
242
|
+
for i, b in enumerate(mount):
|
|
243
|
+
self._memory_array[path + i] = b
|
|
244
|
+
return 0
|
|
245
|
+
|
|
246
|
+
def wasi_path_remove_directory(
|
|
247
|
+
self, parent_fd: int, path: int, path_len: int
|
|
248
|
+
) -> int:
|
|
249
|
+
"""WASI path_remove_directory syscall."""
|
|
250
|
+
return WASI_EINVAL
|
|
251
|
+
|
|
252
|
+
def wasi_fd_fdstat_set_flags(self, fd: int, flags: int) -> int:
|
|
253
|
+
"""WASI fd_fdstat_set_flags syscall."""
|
|
254
|
+
return WASI_ENOSYS
|
|
255
|
+
|
|
256
|
+
def wasi_fd_seek32(self, fd: int, offset: int, whence: int, newoffset: int) -> int:
|
|
257
|
+
"""WASI fd_seek syscall (32-bit variant)."""
|
|
258
|
+
if fd not in self._fds:
|
|
259
|
+
return WASI_EBADF
|
|
260
|
+
|
|
261
|
+
fd_info = self._fds[fd]
|
|
262
|
+
size = fd_info.get("size", 0)
|
|
263
|
+
|
|
264
|
+
if whence == 0: # SEEK_SET
|
|
265
|
+
new_pos = offset
|
|
266
|
+
elif whence == 1: # SEEK_CUR
|
|
267
|
+
new_pos = fd_info.get("position", 0) + offset
|
|
268
|
+
elif whence == 2: # SEEK_END
|
|
269
|
+
new_pos = size + offset
|
|
270
|
+
else:
|
|
271
|
+
return WASI_EINVAL
|
|
272
|
+
|
|
273
|
+
# Validate position is within valid range [0, size]
|
|
274
|
+
if new_pos < 0 or new_pos > size:
|
|
275
|
+
return WASI_EINVAL
|
|
276
|
+
|
|
277
|
+
fd_info["position"] = new_pos
|
|
278
|
+
self._set_u32(newoffset, new_pos)
|
|
279
|
+
return 0
|
|
280
|
+
|
|
281
|
+
def wasi_fd_read(self, fd: int, iovs: int, iovs_len: int, nread: int) -> int:
|
|
282
|
+
"""WASI fd_read syscall."""
|
|
283
|
+
if fd not in self._fds:
|
|
284
|
+
return WASI_EBADF
|
|
285
|
+
|
|
286
|
+
fd_info = self._fds[fd]
|
|
287
|
+
total_read = 0
|
|
288
|
+
|
|
289
|
+
for i in range(iovs_len):
|
|
290
|
+
buf = self._get_u32(iovs + 8 * i)
|
|
291
|
+
buf_len = self._get_u32(iovs + 8 * i + 4)
|
|
292
|
+
|
|
293
|
+
pos = fd_info.get("position", 0)
|
|
294
|
+
size = fd_info.get("size", 0)
|
|
295
|
+
data = fd_info.get("data", b"")
|
|
296
|
+
|
|
297
|
+
read_len = min(size - pos, buf_len)
|
|
298
|
+
if read_len > 0:
|
|
299
|
+
self._check_bounds("wasi_fd_read", buf, read_len)
|
|
300
|
+
assert self._memory_array is not None
|
|
301
|
+
for j in range(read_len):
|
|
302
|
+
self._memory_array[buf + j] = data[pos + j]
|
|
303
|
+
|
|
304
|
+
fd_info["position"] = pos + read_len
|
|
305
|
+
total_read += read_len
|
|
306
|
+
|
|
307
|
+
self._set_u32(nread, total_read)
|
|
308
|
+
return 0
|
|
309
|
+
|
|
310
|
+
def wasi_fd_write(self, fd: int, iovs: int, iovs_len: int, nwritten: int) -> int:
|
|
311
|
+
"""WASI fd_write syscall."""
|
|
312
|
+
if fd not in self._fds:
|
|
313
|
+
return WASI_EBADF
|
|
314
|
+
|
|
315
|
+
fd_info = self._fds[fd]
|
|
316
|
+
total_written = 0
|
|
317
|
+
|
|
318
|
+
for i in range(iovs_len):
|
|
319
|
+
buf = self._get_u32(iovs + 8 * i)
|
|
320
|
+
buf_len = self._get_u32(iovs + 8 * i + 4)
|
|
321
|
+
|
|
322
|
+
if buf_len > 0:
|
|
323
|
+
self._check_bounds("wasi_fd_write", buf, buf_len)
|
|
324
|
+
assert self._memory_array is not None
|
|
325
|
+
write_data = bytes(self._memory_array[buf : buf + buf_len])
|
|
326
|
+
|
|
327
|
+
if fd_info.get("type") == "output":
|
|
328
|
+
self._output_buffer.extend(write_data)
|
|
329
|
+
else:
|
|
330
|
+
pos = fd_info.get("position", 0)
|
|
331
|
+
data = fd_info.get("data", bytearray())
|
|
332
|
+
|
|
333
|
+
if pos + buf_len > len(data):
|
|
334
|
+
new_len = max(len(data) * 2, pos + buf_len)
|
|
335
|
+
new_data = bytearray(new_len)
|
|
336
|
+
new_data[: len(data)] = data
|
|
337
|
+
fd_info["data"] = new_data
|
|
338
|
+
data = fd_info["data"]
|
|
339
|
+
|
|
340
|
+
data[pos : pos + buf_len] = write_data
|
|
341
|
+
fd_info["position"] = pos + buf_len
|
|
342
|
+
fd_info["size"] = max(fd_info.get("size", 0), pos + buf_len)
|
|
343
|
+
|
|
344
|
+
total_written += buf_len
|
|
345
|
+
|
|
346
|
+
self._set_u32(nwritten, total_written)
|
|
347
|
+
return 0
|