@braedonsaunders/appkit-desk 0.7.0 → 0.9.0

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.
Files changed (46) hide show
  1. package/README.md +126 -1
  2. package/backend.d.ts +32 -7
  3. package/backend.d.ts.map +1 -1
  4. package/backend.js +107 -524
  5. package/backend.js.map +1 -1
  6. package/host.d.ts +27 -1
  7. package/host.d.ts.map +1 -1
  8. package/host.js +47 -19
  9. package/host.js.map +1 -1
  10. package/index.d.ts +2 -0
  11. package/index.d.ts.map +1 -1
  12. package/index.js +2 -0
  13. package/index.js.map +1 -1
  14. package/machine.d.ts +154 -0
  15. package/machine.d.ts.map +1 -0
  16. package/machine.js +573 -0
  17. package/machine.js.map +1 -0
  18. package/package.json +2 -2
  19. package/plan.d.ts +192 -31
  20. package/plan.d.ts.map +1 -1
  21. package/plan.js +298 -12
  22. package/plan.js.map +1 -1
  23. package/port-relay.d.ts +13 -0
  24. package/port-relay.d.ts.map +1 -0
  25. package/port-relay.js +56 -0
  26. package/port-relay.js.map +1 -0
  27. package/virtualization.d.ts +16 -0
  28. package/virtualization.d.ts.map +1 -0
  29. package/virtualization.js +156 -0
  30. package/virtualization.js.map +1 -0
  31. package/vz/Package.swift +23 -0
  32. package/vz/Sources/DeskNet/Addresses.swift +106 -0
  33. package/vz/Sources/DeskNet/DHCP.swift +111 -0
  34. package/vz/Sources/DeskNet/Fence.swift +57 -0
  35. package/vz/Sources/DeskNet/NetworkStack.swift +547 -0
  36. package/vz/Sources/DeskNet/Packets.swift +355 -0
  37. package/vz/Sources/DeskNet/Plumbing.swift +312 -0
  38. package/vz/Sources/DeskNet/Sockets.swift +221 -0
  39. package/vz/Sources/DeskNet/TCPFlow.swift +448 -0
  40. package/vz/Sources/appkit-desk-vz/Control.swift +152 -0
  41. package/vz/Sources/appkit-desk-vz/Helper.swift +550 -0
  42. package/vz/Sources/appkit-desk-vz/main.swift +38 -0
  43. package/vz/Tests/DeskNetTests/DeskNetTests.swift +426 -0
  44. package/vz/appkit-desk-vz.entitlements +8 -0
  45. package/vz/build.sh +23 -0
  46. package/vz/check/extract-boot.py +221 -0
@@ -0,0 +1,221 @@
1
+ #!/usr/bin/env python3
2
+ """Copy a Debian disk image's kernel and initrd out of its ext4 root, read-only.
3
+
4
+ extract-boot.py <disk.raw> <out-dir>
5
+
6
+ Writes <out-dir>/Image (an uncompressed arm64 Image, which is what
7
+ VZLinuxBootLoader boots), <out-dir>/initrd, and <out-dir>/release (the kernel
8
+ version). The kernel and initrd come from the image itself because that is the
9
+ only place Debian publishes the initrd that matches the image's modules.
10
+
11
+ macOS cannot mount ext4, so this reads the filesystem directly: GPT, the
12
+ superblock, group descriptors, inodes, extent trees and directories. It only
13
+ reads, and it supports exactly what a stock Debian cloud image uses.
14
+ """
15
+ import gzip
16
+ import os
17
+ import shutil
18
+ import struct
19
+ import subprocess
20
+ import sys
21
+ import zlib
22
+
23
+ SECTOR = 512
24
+ EXT4_MAGIC = 0xEF53
25
+ EXTENTS_FLAG = 0x80000
26
+ INLINE_DATA_FLAG = 0x10000000
27
+ INCOMPAT_64BIT = 0x80
28
+ S_IFMT = 0o170000
29
+ S_IFDIR = 0o040000
30
+ S_IFLNK = 0o120000
31
+
32
+
33
+ def partitions(image):
34
+ image.seek(SECTOR)
35
+ header = image.read(92)
36
+ if header[:8] != b"EFI PART":
37
+ return [0]
38
+ entries_lba, count, size = struct.unpack_from("<QII", header, 72)
39
+ offsets = []
40
+ for index in range(count):
41
+ image.seek(entries_lba * SECTOR + index * size)
42
+ entry = image.read(size)
43
+ if entry[:16] == b"\0" * 16:
44
+ continue
45
+ first = struct.unpack_from("<Q", entry, 32)[0]
46
+ offsets.append(first * SECTOR)
47
+ return offsets
48
+
49
+
50
+ class Ext4:
51
+ def __init__(self, image, offset):
52
+ self.image = image
53
+ self.offset = offset
54
+ superblock = self.read(1024, 1024)
55
+ if struct.unpack_from("<H", superblock, 0x38)[0] != EXT4_MAGIC:
56
+ raise ValueError("not ext4")
57
+ self.block_size = 1024 << struct.unpack_from("<I", superblock, 0x18)[0]
58
+ self.first_data_block = struct.unpack_from("<I", superblock, 0x14)[0]
59
+ self.inodes_per_group = struct.unpack_from("<I", superblock, 0x28)[0]
60
+ self.inode_size = struct.unpack_from("<H", superblock, 0x58)[0]
61
+ incompat = struct.unpack_from("<I", superblock, 0x60)[0]
62
+ self.is64 = bool(incompat & INCOMPAT_64BIT)
63
+ self.desc_size = struct.unpack_from("<H", superblock, 0xFE)[0] if self.is64 else 32
64
+ self.gdt = (self.first_data_block + 1) * self.block_size
65
+
66
+ def read(self, position, length):
67
+ self.image.seek(self.offset + position)
68
+ data = self.image.read(length)
69
+ if len(data) != length:
70
+ raise ValueError("read past the end of the image")
71
+ return data
72
+
73
+ def inode(self, number):
74
+ group, index = divmod(number - 1, self.inodes_per_group)
75
+ desc = self.read(self.gdt + group * self.desc_size, self.desc_size)
76
+ table = struct.unpack_from("<I", desc, 0x8)[0]
77
+ if self.is64 and self.desc_size >= 64:
78
+ table |= struct.unpack_from("<I", desc, 0x28)[0] << 32
79
+ return self.read(table * self.block_size + index * self.inode_size, self.inode_size)
80
+
81
+ @staticmethod
82
+ def mode(inode):
83
+ return struct.unpack_from("<H", inode, 0)[0]
84
+
85
+ @staticmethod
86
+ def size(inode):
87
+ low, high = struct.unpack_from("<I", inode, 0x4)[0], struct.unpack_from("<I", inode, 0x6C)[0]
88
+ return high << 32 | low
89
+
90
+ def extents(self, node):
91
+ magic, entries, _, depth = struct.unpack_from("<HHHH", node, 0)
92
+ if magic != 0xF30A:
93
+ raise ValueError("bad extent header")
94
+ for index in range(entries):
95
+ entry = node[12 + index * 12: 24 + index * 12]
96
+ if depth == 0:
97
+ logical, length, start_high, start_low = struct.unpack("<IHHI", entry)
98
+ uninitialized = length > 32768
99
+ if uninitialized:
100
+ length -= 32768
101
+ yield logical, length, start_high << 32 | start_low, uninitialized
102
+ else:
103
+ _, leaf_low, leaf_high = struct.unpack_from("<IIH", entry, 0)
104
+ child = self.read((leaf_high << 32 | leaf_low) * self.block_size, self.block_size)
105
+ yield from self.extents(child)
106
+
107
+ def contents(self, inode):
108
+ flags = struct.unpack_from("<I", inode, 0x20)[0]
109
+ size = self.size(inode)
110
+ if flags & INLINE_DATA_FLAG:
111
+ raise ValueError("inline data is not supported")
112
+ if not flags & EXTENTS_FLAG:
113
+ raise ValueError("block-mapped files are not supported")
114
+ data = bytearray(size)
115
+ for logical, length, physical, uninitialized in self.extents(inode[0x28:0x28 + 60]):
116
+ begin = logical * self.block_size
117
+ if begin >= size or uninitialized:
118
+ continue
119
+ count = min(length * self.block_size, size - begin)
120
+ data[begin:begin + count] = self.read(physical * self.block_size, count)
121
+ return bytes(data)
122
+
123
+ def entries(self, inode):
124
+ data = self.contents(inode)
125
+ position = 0
126
+ while position + 8 <= len(data):
127
+ number, record, name_length = struct.unpack_from("<IHB", data, position)
128
+ if record < 8:
129
+ break
130
+ if number:
131
+ yield data[position + 8: position + 8 + name_length].decode("utf-8", "replace"), number
132
+ position += record
133
+
134
+ def lookup(self, path, follow=True, depth=0):
135
+ if depth > 16:
136
+ raise ValueError("too many symbolic links")
137
+ number = 2
138
+ parts = [part for part in path.split("/") if part]
139
+ for index, part in enumerate(parts):
140
+ directory = self.inode(number)
141
+ if self.mode(directory) & S_IFMT != S_IFDIR:
142
+ raise FileNotFoundError(path)
143
+ found = dict(self.entries(directory)).get(part)
144
+ if found is None:
145
+ raise FileNotFoundError(path)
146
+ number = found
147
+ node = self.inode(number)
148
+ last = index == len(parts) - 1
149
+ if self.mode(node) & S_IFMT == S_IFLNK and (follow or not last):
150
+ target = self.symlink(node)
151
+ base = "/" + "/".join(parts[:index])
152
+ rest = "/".join(parts[index + 1:])
153
+ joined = target if target.startswith("/") else os.path.join(base, target)
154
+ return self.lookup(os.path.normpath(joined + ("/" + rest if rest else "")), follow, depth + 1)
155
+ return self.inode(number)
156
+
157
+ def symlink(self, inode):
158
+ size = self.size(inode)
159
+ flags = struct.unpack_from("<I", inode, 0x20)[0]
160
+ if size < 60 and not flags & EXTENTS_FLAG:
161
+ return inode[0x28:0x28 + size].decode()
162
+ return self.contents(inode).decode()
163
+
164
+
165
+ def uncompressed_image(kernel):
166
+ """Debian's arm64 vmlinuz is a gzip'd Image or an EFI zboot image."""
167
+ if kernel[:2] == b"\x1f\x8b":
168
+ return gzip.decompress(kernel)
169
+ if kernel[:2] == b"MZ" and kernel[4:8] == b"zimg":
170
+ start, length = struct.unpack_from("<II", kernel, 8)
171
+ kind = kernel[24:32].rstrip(b"\0").decode()
172
+ payload = kernel[start:start + length]
173
+ if kind == "gzip":
174
+ return zlib.decompress(payload, 16 + zlib.MAX_WBITS)
175
+ if kind == "zstd":
176
+ zstd = shutil.which("zstd")
177
+ if not zstd:
178
+ raise SystemExit("this kernel is zstd-compressed EFI zboot; install zstd to unpack it")
179
+ return subprocess.run([zstd, "-dc"], input=payload, check=True, capture_output=True).stdout
180
+ raise SystemExit(f"unsupported EFI zboot compression: {kind}")
181
+ if kernel[0x38:0x3C] == b"ARM\x64":
182
+ return kernel
183
+ raise SystemExit("the kernel is neither an arm64 Image, a gzip'd Image, nor EFI zboot")
184
+
185
+
186
+ def main():
187
+ if len(sys.argv) != 3:
188
+ raise SystemExit(__doc__)
189
+ disk, out = sys.argv[1], sys.argv[2]
190
+ with open(disk, "rb") as image:
191
+ for offset in partitions(image):
192
+ try:
193
+ fs = Ext4(image, offset)
194
+ boot = dict(fs.entries(fs.lookup("/boot")))
195
+ except (ValueError, FileNotFoundError):
196
+ continue
197
+ kernels = sorted(name for name in boot if name.startswith("vmlinuz-"))
198
+ if not kernels:
199
+ continue
200
+ release = kernels[-1][len("vmlinuz-"):]
201
+ if f"initrd.img-{release}" not in boot:
202
+ raise SystemExit(f"/boot has vmlinuz-{release} but no matching initrd")
203
+ kernel = fs.contents(fs.lookup(f"/boot/vmlinuz-{release}"))
204
+ initrd = fs.contents(fs.lookup(f"/boot/initrd.img-{release}"))
205
+ image_bytes = uncompressed_image(kernel)
206
+ if image_bytes[0x38:0x3C] != b"ARM\x64":
207
+ raise SystemExit("the unpacked kernel is not an arm64 Image")
208
+ os.makedirs(out, exist_ok=True)
209
+ with open(os.path.join(out, "Image"), "wb") as handle:
210
+ handle.write(image_bytes)
211
+ with open(os.path.join(out, "initrd"), "wb") as handle:
212
+ handle.write(initrd)
213
+ with open(os.path.join(out, "release"), "w") as handle:
214
+ handle.write(release + "\n")
215
+ print(release)
216
+ return
217
+ raise SystemExit("no ext4 partition with a kernel in /boot was found")
218
+
219
+
220
+ if __name__ == "__main__":
221
+ main()