tebako 0.14.0 → 0.15.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.
@@ -0,0 +1,575 @@
1
+ /**
2
+ * @file tpkg.h
3
+ * @brief tebako package manifest (tpkg) — single-header C99 mini-lib
4
+ *
5
+ * Copyright (c) 2026 [Ribose Inc](https://www.ribose.com).
6
+ * All rights reserved.
7
+ * This file is a part of the Tebako project (libtfs).
8
+ *
9
+ * Redistribution and use in source and binary forms, with or without
10
+ * modification, are permitted provided that the following conditions
11
+ * are met:
12
+ * 1. Redistributions of source code must retain the above copyright
13
+ * notice, this list of conditions and the following disclaimer.
14
+ * 2. Redistributions in binary form must reproduce the above copyright
15
+ * notice, this list of conditions and the following disclaimer in the
16
+ * documentation and/or other materials provided with the distribution.
17
+ *
18
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19
+ * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
20
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
21
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR
22
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
23
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
24
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
25
+ * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
26
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
27
+ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
28
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
+ *
30
+ * ============================================================================
31
+ * Manifest trailer v1 — wire layout (all integers little-endian):
32
+ *
33
+ * [payload][slot 0 .. slot n-1 records][trailer header — fixed size, at EOF]
34
+ *
35
+ * trailer header (TPKG_HEADER_SIZE = 166 bytes):
36
+ * offset size field
37
+ * 0 10 magic "TEBAKOTFS\0" (10 bytes, NUL-terminated)
38
+ * 10 4 u32 version (TPKG_VERSION = 1)
39
+ * 14 4 u32 package_flags (bit 0: TPKG_FLAG_LEAN — bootstrap+images
40
+ * only, runtime resolved at run time)
41
+ * 18 4 u32 slot_count (1..TPKG_MAX_SLOTS)
42
+ * 22 8 u64 slot_table_offset (absolute file offset of slot 0 record)
43
+ * 30 128 char runtime_ref[128] (UTF-8, NUL-padded; empty = classic bundle)
44
+ * 158 4 u32 launcher_abi
45
+ * 162 4 u32 header_crc32 — tpkg_crc32() over header bytes [0, 162)
46
+ *
47
+ * slot record (TPKG_SLOT_SIZE = 280 bytes):
48
+ * offset size field
49
+ * 0 8 u64 offset (image start, absolute file offset)
50
+ * 8 8 u64 size (image length in bytes)
51
+ * 16 4 u32 format_id (0=auto(magic), 1=dwarfs, 2=squashfs, 3=zip)
52
+ * 20 4 u32 flags
53
+ * 24 256 char mount_point[256] (UTF-8, NUL-padded)
54
+ *
55
+ * Reader algorithm: read the fixed-size header at EOF - TPKG_HEADER_SIZE,
56
+ * check the magic, verify header_crc32, then read slot_count slot records at
57
+ * slot_table_offset. The table (and therefore the payload) may sit at any —
58
+ * including odd — file offset.
59
+ *
60
+ * Absent vs. corrupt: a file whose last-166-byte window does not start with
61
+ * the 4-byte prefix "TEBA" is reported as TPKG_ERR_NO_TRAILER (a classic
62
+ * bundle without a manifest — not an error condition per se; callers fall
63
+ * back to offset auto-detection). A matching prefix with a mismatching full
64
+ * magic is TPKG_ERR_MAGIC (corrupt trailer); a magic-valid header with a bad
65
+ * crc is TPKG_ERR_CRC.
66
+ *
67
+ * Error handling: all tpkg_* functions (except tpkg_crc32, tpkg_errno and
68
+ * tpkg_strerror) return 0 on success and -1 on failure, with a TPKG_ERR_*
69
+ * code available from tpkg_errno(). The error state is a process-global
70
+ * (ISO C99 has no portable TLS); it is reset at the entry of every public
71
+ * function and is NOT thread-safe.
72
+ *
73
+ * Usage: in exactly one translation unit, #define TPKG_IMPLEMENTATION before
74
+ * #include <tebako/tpkg.h>; every other TU includes it plain. On POSIX the
75
+ * implementation section defines _POSIX_C_SOURCE 200809L if it is not
76
+ * defined yet, so include tpkg.h before any system header in that TU.
77
+ * No dependencies beyond libc.
78
+ * ============================================================================
79
+ */
80
+
81
+ #ifndef TEBAKO_TPKG_H
82
+ #define TEBAKO_TPKG_H
83
+
84
+ #include <stddef.h>
85
+ #include <stdint.h>
86
+
87
+ #ifdef __cplusplus
88
+ extern "C" {
89
+ #endif
90
+
91
+ /* ---- format constants ---------------------------------------------------- */
92
+
93
+ #define TPKG_VERSION 1u
94
+ #define TPKG_MAX_SLOTS 8u
95
+ #define TPKG_HEADER_SIZE 166u
96
+ #define TPKG_SLOT_SIZE 280u
97
+ #define TPKG_MOUNT_POINT_LEN 256u
98
+ #define TPKG_RUNTIME_REF_LEN 128u
99
+ #define TPKG_MAGIC "TEBAKOTFS"
100
+ #define TPKG_MAGIC_LEN 10u /* including the terminating NUL */
101
+ #define TPKG_MAGIC_PREFIX_LEN 4u /* "TEBA": absent-vs-corrupt discriminator */
102
+
103
+ /* package_flags */
104
+ #define TPKG_FLAG_LEAN 0x1u
105
+
106
+ /* format_id */
107
+ #define TPKG_FORMAT_AUTO 0u
108
+ #define TPKG_FORMAT_DWARFS 1u
109
+ #define TPKG_FORMAT_SQUASHFS 2u
110
+ #define TPKG_FORMAT_ZIP 3u
111
+
112
+ /* ---- error codes (returned by tpkg_errno) -------------------------------- */
113
+
114
+ enum {
115
+ TPKG_OK = 0, /* success */
116
+ TPKG_ERR_NO_TRAILER = 1, /* no manifest trailer present (absent — not an error per se) */
117
+ TPKG_ERR_MAGIC = 2, /* magic prefix present but full magic mismatch (corrupt) */
118
+ TPKG_ERR_CRC = 3, /* header_crc32 mismatch (corrupt) */
119
+ TPKG_ERR_IO = 4, /* underlying i/o failure */
120
+ TPKG_ERR_BOUNDS = 5, /* slot table outside file bounds */
121
+ TPKG_ERR_SLOTS = 6, /* slot_count == 0 or > TPKG_MAX_SLOTS */
122
+ TPKG_ERR_INVALID = 7, /* structural validation failure */
123
+ TPKG_ERR_ARG = 8, /* invalid argument (NULL, bad fd) */
124
+ TPKG_ERR_VERSION = 9 /* unsupported manifest version */
125
+ };
126
+
127
+ /* ---- manifest structures -------------------------------------------------- */
128
+
129
+ typedef struct tpkg_slot {
130
+ uint64_t offset;
131
+ uint64_t size;
132
+ uint32_t format_id;
133
+ uint32_t flags;
134
+ char mount_point[TPKG_MOUNT_POINT_LEN];
135
+ } tpkg_slot;
136
+
137
+ typedef struct tpkg_manifest {
138
+ uint32_t version;
139
+ uint32_t package_flags;
140
+ uint32_t slot_count;
141
+ uint32_t launcher_abi;
142
+ char runtime_ref[TPKG_RUNTIME_REF_LEN];
143
+ tpkg_slot slots[TPKG_MAX_SLOTS];
144
+ } tpkg_manifest;
145
+
146
+ /* ---- API ------------------------------------------------------------------ */
147
+
148
+ /* Read the manifest trailer of an open binary (seeks to EOF; the file offset
149
+ * is left unspecified). */
150
+ int tpkg_read_fd(int fd, tpkg_manifest* out);
151
+
152
+ /* Read the manifest trailer from an in-memory image of the binary. */
153
+ int tpkg_read_mem(const void* data, size_t size, tpkg_manifest* out);
154
+
155
+ /* Append the slot table + trailer header to an open binary (at current EOF).
156
+ * The manifest is validated first; a rejected manifest appends nothing. */
157
+ int tpkg_write_fd(int fd, const tpkg_manifest* m);
158
+
159
+ /* Magic-independent structural checks: version supported, slot_count in
160
+ * 1..TPKG_MAX_SLOTS, offset+size non-overflowing, format_id <= TPKG_FORMAT_ZIP,
161
+ * runtime_ref and mount_points NUL-terminated within their fixed fields. */
162
+ int tpkg_validate(const tpkg_manifest* m);
163
+
164
+ /* CRC-32 (zlib polynomial 0xEDB88320, init/xorout 0xFFFFFFFF) — exposed for
165
+ * header_crc32 computation and verification. Pure; does not touch tpkg_errno. */
166
+ uint32_t tpkg_crc32(const void* data, size_t n);
167
+
168
+ /* Code of the last failed (or succeeded) tpkg operation on this process. */
169
+ int tpkg_errno(void);
170
+
171
+ /* Static string for a TPKG_ERR_* code (never NULL). */
172
+ const char* tpkg_strerror(int err);
173
+
174
+ #ifdef __cplusplus
175
+ }
176
+ #endif
177
+
178
+ #endif /* TEBAKO_TPKG_H */
179
+
180
+ /* ~~~~~~~~~~~~~~~~~~~~~~~~~ implementation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
181
+
182
+ #if defined(TPKG_IMPLEMENTATION) && !defined(TPKG_IMPLEMENTATION_DONE)
183
+ #define TPKG_IMPLEMENTATION_DONE
184
+
185
+ #include <stdio.h> /* SEEK_SET / SEEK_END */
186
+ #include <string.h>
187
+
188
+ #if defined(_WIN32)
189
+ #include <io.h>
190
+ typedef __int64 tpkg__off_t;
191
+ typedef int tpkg__ssize_t;
192
+ typedef unsigned int tpkg__io_count_t;
193
+ #define tpkg__lseek _lseeki64
194
+ #define tpkg__read _read
195
+ #define tpkg__write _write
196
+ #else
197
+ #if !defined(_POSIX_C_SOURCE)
198
+ #define _POSIX_C_SOURCE 200809L
199
+ #endif
200
+ #include <sys/types.h>
201
+ #include <unistd.h>
202
+ typedef off_t tpkg__off_t;
203
+ typedef ssize_t tpkg__ssize_t;
204
+ typedef size_t tpkg__io_count_t;
205
+ #define tpkg__lseek lseek
206
+ #define tpkg__read read
207
+ #define tpkg__write write
208
+ #endif
209
+
210
+ /* header field offsets */
211
+ enum {
212
+ TPKG__OFF_MAGIC = 0,
213
+ TPKG__OFF_VERSION = 10,
214
+ TPKG__OFF_PACKAGE_FLAGS = 14,
215
+ TPKG__OFF_SLOT_COUNT = 18,
216
+ TPKG__OFF_TABLE = 22,
217
+ TPKG__OFF_RUNTIME_REF = 30,
218
+ TPKG__OFF_LAUNCHER_ABI = 158,
219
+ TPKG__OFF_CRC32 = 162
220
+ };
221
+
222
+ /* slot record field offsets */
223
+ enum {
224
+ TPKG__REC_OFFSET = 0,
225
+ TPKG__REC_SIZE = 8,
226
+ TPKG__REC_FORMAT = 16,
227
+ TPKG__REC_FLAGS = 20,
228
+ TPKG__REC_MOUNT = 24
229
+ };
230
+
231
+ /* wire sizes are fixed by the format; catch exotic padding at compile time */
232
+ typedef char tpkg__assert_slot_size[(sizeof(tpkg_slot) == TPKG_SLOT_SIZE) ? 1 : -1];
233
+ typedef char tpkg__assert_manifest_size[(sizeof(tpkg_manifest) ==
234
+ 4 * sizeof(uint32_t) + TPKG_RUNTIME_REF_LEN +
235
+ TPKG_MAX_SLOTS * sizeof(tpkg_slot))
236
+ ? 1
237
+ : -1];
238
+
239
+ /* ---- error state ---------------------------------------------------------- */
240
+
241
+ static int tpkg__last_err = TPKG_OK;
242
+
243
+ int tpkg_errno(void)
244
+ {
245
+ return tpkg__last_err;
246
+ }
247
+
248
+ static int tpkg__fail(int code)
249
+ {
250
+ tpkg__last_err = code;
251
+ return -1;
252
+ }
253
+
254
+ const char* tpkg_strerror(int err)
255
+ {
256
+ switch (err) {
257
+ case TPKG_OK:
258
+ return "success";
259
+ case TPKG_ERR_NO_TRAILER:
260
+ return "no tpkg manifest trailer present";
261
+ case TPKG_ERR_MAGIC:
262
+ return "corrupt tpkg trailer magic";
263
+ case TPKG_ERR_CRC:
264
+ return "tpkg trailer header crc32 mismatch";
265
+ case TPKG_ERR_IO:
266
+ return "tpkg i/o error";
267
+ case TPKG_ERR_BOUNDS:
268
+ return "tpkg slot table out of file bounds";
269
+ case TPKG_ERR_SLOTS:
270
+ return "tpkg slot count out of range (1..TPKG_MAX_SLOTS)";
271
+ case TPKG_ERR_INVALID:
272
+ return "invalid tpkg manifest structure";
273
+ case TPKG_ERR_ARG:
274
+ return "invalid tpkg argument";
275
+ case TPKG_ERR_VERSION:
276
+ return "unsupported tpkg manifest version";
277
+ default:
278
+ return "unknown tpkg error";
279
+ }
280
+ }
281
+
282
+ /* ---- CRC-32 (zlib polynomial) -------------------------------------------- */
283
+
284
+ uint32_t tpkg_crc32(const void* data, size_t n)
285
+ {
286
+ const uint8_t* p = (const uint8_t*)data;
287
+ uint32_t crc = 0xFFFFFFFFu;
288
+ size_t i;
289
+ int k;
290
+ for (i = 0; i < n; i++) {
291
+ crc ^= p[i];
292
+ for (k = 0; k < 8; k++) {
293
+ crc = (crc >> 1) ^ (0xEDB88320u & (0u - (crc & 1u)));
294
+ }
295
+ }
296
+ return crc ^ 0xFFFFFFFFu;
297
+ }
298
+
299
+ /* ---- little-endian codec -------------------------------------------------- */
300
+
301
+ static void tpkg__put32(uint8_t* p, uint32_t v)
302
+ {
303
+ p[0] = (uint8_t)(v & 0xFFu);
304
+ p[1] = (uint8_t)((v >> 8) & 0xFFu);
305
+ p[2] = (uint8_t)((v >> 16) & 0xFFu);
306
+ p[3] = (uint8_t)((v >> 24) & 0xFFu);
307
+ }
308
+
309
+ static void tpkg__put64(uint8_t* p, uint64_t v)
310
+ {
311
+ int i;
312
+ for (i = 0; i < 8; i++) {
313
+ p[i] = (uint8_t)((v >> (8 * i)) & 0xFFu);
314
+ }
315
+ }
316
+
317
+ static uint32_t tpkg__get32(const uint8_t* p)
318
+ {
319
+ return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
320
+ }
321
+
322
+ static uint64_t tpkg__get64(const uint8_t* p)
323
+ {
324
+ uint64_t v = 0;
325
+ int i;
326
+ for (i = 0; i < 8; i++) {
327
+ v |= (uint64_t)p[i] << (8 * i);
328
+ }
329
+ return v;
330
+ }
331
+
332
+ /* ---- helpers -------------------------------------------------------------- */
333
+
334
+ static size_t tpkg__strnlen(const char* s, size_t max)
335
+ {
336
+ const char* nul = (const char*)memchr(s, '\0', max);
337
+ return nul ? (size_t)(nul - s) : max;
338
+ }
339
+
340
+ /* copy a C string into a fixed-width field, zero-padding the remainder;
341
+ * callers must have validated NUL-termination within width */
342
+ static void tpkg__put_str(uint8_t* p, const char* s, size_t width)
343
+ {
344
+ size_t len = tpkg__strnlen(s, width);
345
+ memcpy(p, s, len);
346
+ memset(p + len, 0, width - len);
347
+ }
348
+
349
+ static int tpkg__read_full(int fd, void* buf, size_t n)
350
+ {
351
+ uint8_t* p = (uint8_t*)buf;
352
+ while (n > 0) {
353
+ tpkg__ssize_t r = tpkg__read(fd, p, (tpkg__io_count_t)n);
354
+ if (r <= 0) {
355
+ return -1;
356
+ }
357
+ p += (size_t)r;
358
+ n -= (size_t)r;
359
+ }
360
+ return 0;
361
+ }
362
+
363
+ static int tpkg__write_full(int fd, const void* buf, size_t n)
364
+ {
365
+ const uint8_t* p = (const uint8_t*)buf;
366
+ while (n > 0) {
367
+ tpkg__ssize_t r = tpkg__write(fd, p, (tpkg__io_count_t)n);
368
+ if (r <= 0) {
369
+ return -1;
370
+ }
371
+ p += (size_t)r;
372
+ n -= (size_t)r;
373
+ }
374
+ return 0;
375
+ }
376
+
377
+ /* ---- validation ------------------------------------------------------------ */
378
+
379
+ int tpkg_validate(const tpkg_manifest* m)
380
+ {
381
+ uint32_t i;
382
+ tpkg__last_err = TPKG_OK;
383
+ if (m == NULL) {
384
+ return tpkg__fail(TPKG_ERR_ARG);
385
+ }
386
+ if (m->version != TPKG_VERSION) {
387
+ return tpkg__fail(TPKG_ERR_VERSION);
388
+ }
389
+ if (m->slot_count == 0 || m->slot_count > TPKG_MAX_SLOTS) {
390
+ return tpkg__fail(TPKG_ERR_SLOTS);
391
+ }
392
+ if (tpkg__strnlen(m->runtime_ref, TPKG_RUNTIME_REF_LEN) == TPKG_RUNTIME_REF_LEN) {
393
+ return tpkg__fail(TPKG_ERR_INVALID);
394
+ }
395
+ for (i = 0; i < m->slot_count; i++) {
396
+ const tpkg_slot* s = &m->slots[i];
397
+ if (s->size > UINT64_MAX - s->offset) {
398
+ return tpkg__fail(TPKG_ERR_INVALID);
399
+ }
400
+ if (s->format_id > TPKG_FORMAT_ZIP) {
401
+ return tpkg__fail(TPKG_ERR_INVALID);
402
+ }
403
+ if (tpkg__strnlen(s->mount_point, TPKG_MOUNT_POINT_LEN) == TPKG_MOUNT_POINT_LEN) {
404
+ return tpkg__fail(TPKG_ERR_INVALID);
405
+ }
406
+ }
407
+ return 0;
408
+ }
409
+
410
+ /* ---- reader core ------------------------------------------------------------ */
411
+
412
+ /* read exactly n bytes at absolute offset off (off+n guaranteed in bounds) */
413
+ typedef int (*tpkg__readat_fn)(void* ctx, uint64_t off, void* buf, size_t n);
414
+
415
+ static int tpkg__mem_readat(void* ctx, uint64_t off, void* buf, size_t n)
416
+ {
417
+ memcpy(buf, (const uint8_t*)ctx + off, n);
418
+ return 0;
419
+ }
420
+
421
+ static int tpkg__fd_readat(void* ctx, uint64_t off, void* buf, size_t n)
422
+ {
423
+ int fd = *(int*)ctx;
424
+ if (tpkg__lseek(fd, (tpkg__off_t)off, SEEK_SET) == (tpkg__off_t)-1) {
425
+ return -1;
426
+ }
427
+ return tpkg__read_full(fd, buf, n);
428
+ }
429
+
430
+ static int tpkg__read_core(tpkg__readat_fn readat, void* ctx, uint64_t size, tpkg_manifest* out)
431
+ {
432
+ uint8_t hdr[TPKG_HEADER_SIZE];
433
+ uint8_t table[TPKG_MAX_SLOTS * TPKG_SLOT_SIZE];
434
+ uint64_t table_off;
435
+ uint64_t avail;
436
+ uint32_t version;
437
+ uint32_t slot_count;
438
+ uint32_t i;
439
+
440
+ if (size < TPKG_HEADER_SIZE) {
441
+ return tpkg__fail(TPKG_ERR_NO_TRAILER);
442
+ }
443
+ if (readat(ctx, size - TPKG_HEADER_SIZE, hdr, TPKG_HEADER_SIZE) != 0) {
444
+ return tpkg__fail(TPKG_ERR_IO);
445
+ }
446
+
447
+ /* absent vs corrupt: no "TEBA" prefix -> classic bundle, no trailer */
448
+ if (memcmp(hdr + TPKG__OFF_MAGIC, TPKG_MAGIC, TPKG_MAGIC_PREFIX_LEN) != 0) {
449
+ return tpkg__fail(TPKG_ERR_NO_TRAILER);
450
+ }
451
+ if (memcmp(hdr + TPKG__OFF_MAGIC, TPKG_MAGIC, TPKG_MAGIC_LEN) != 0) {
452
+ return tpkg__fail(TPKG_ERR_MAGIC);
453
+ }
454
+ if (tpkg_crc32(hdr, TPKG__OFF_CRC32) != tpkg__get32(hdr + TPKG__OFF_CRC32)) {
455
+ return tpkg__fail(TPKG_ERR_CRC);
456
+ }
457
+
458
+ version = tpkg__get32(hdr + TPKG__OFF_VERSION);
459
+ if (version != TPKG_VERSION) {
460
+ return tpkg__fail(TPKG_ERR_VERSION);
461
+ }
462
+ slot_count = tpkg__get32(hdr + TPKG__OFF_SLOT_COUNT);
463
+ if (slot_count == 0 || slot_count > TPKG_MAX_SLOTS) {
464
+ return tpkg__fail(TPKG_ERR_SLOTS);
465
+ }
466
+
467
+ table_off = tpkg__get64(hdr + TPKG__OFF_TABLE);
468
+ avail = size - TPKG_HEADER_SIZE; /* bytes preceding the header */
469
+ /* overflow-free: table must fit entirely before the header */
470
+ if (table_off > avail || (uint64_t)slot_count > (avail - table_off) / TPKG_SLOT_SIZE) {
471
+ return tpkg__fail(TPKG_ERR_BOUNDS);
472
+ }
473
+ if (readat(ctx, table_off, table, (size_t)slot_count * TPKG_SLOT_SIZE) != 0) {
474
+ return tpkg__fail(TPKG_ERR_IO);
475
+ }
476
+
477
+ memset(out, 0, sizeof *out);
478
+ out->version = version;
479
+ out->package_flags = tpkg__get32(hdr + TPKG__OFF_PACKAGE_FLAGS);
480
+ out->slot_count = slot_count;
481
+ out->launcher_abi = tpkg__get32(hdr + TPKG__OFF_LAUNCHER_ABI);
482
+ memcpy(out->runtime_ref, hdr + TPKG__OFF_RUNTIME_REF, TPKG_RUNTIME_REF_LEN);
483
+ for (i = 0; i < slot_count; i++) {
484
+ const uint8_t* rec = table + (size_t)i * TPKG_SLOT_SIZE;
485
+ tpkg_slot* s = &out->slots[i];
486
+ s->offset = tpkg__get64(rec + TPKG__REC_OFFSET);
487
+ s->size = tpkg__get64(rec + TPKG__REC_SIZE);
488
+ s->format_id = tpkg__get32(rec + TPKG__REC_FORMAT);
489
+ s->flags = tpkg__get32(rec + TPKG__REC_FLAGS);
490
+ memcpy(s->mount_point, rec + TPKG__REC_MOUNT, TPKG_MOUNT_POINT_LEN);
491
+ }
492
+
493
+ /* structural sanity of the parsed manifest (a valid crc does not imply
494
+ * well-formed fields when the trailer was not written by us) */
495
+ return tpkg_validate(out);
496
+ }
497
+
498
+ int tpkg_read_fd(int fd, tpkg_manifest* out)
499
+ {
500
+ tpkg__off_t end;
501
+ tpkg__last_err = TPKG_OK;
502
+ if (fd < 0 || out == NULL) {
503
+ return tpkg__fail(TPKG_ERR_ARG);
504
+ }
505
+ end = tpkg__lseek(fd, 0, SEEK_END);
506
+ if (end == (tpkg__off_t)-1) {
507
+ return tpkg__fail(TPKG_ERR_IO);
508
+ }
509
+ return tpkg__read_core(tpkg__fd_readat, &fd, (uint64_t)end, out);
510
+ }
511
+
512
+ int tpkg_read_mem(const void* data, size_t size, tpkg_manifest* out)
513
+ {
514
+ tpkg__last_err = TPKG_OK;
515
+ if (data == NULL || out == NULL) {
516
+ return tpkg__fail(TPKG_ERR_ARG);
517
+ }
518
+ return tpkg__read_core(tpkg__mem_readat, (void*)data, (uint64_t)size, out);
519
+ }
520
+
521
+ /* ---- writer ----------------------------------------------------------------- */
522
+
523
+ int tpkg_write_fd(int fd, const tpkg_manifest* m)
524
+ {
525
+ uint8_t buf[TPKG_MAX_SLOTS * TPKG_SLOT_SIZE + TPKG_HEADER_SIZE];
526
+ uint8_t* p = buf;
527
+ uint8_t* hdr;
528
+ tpkg__off_t end;
529
+ size_t total;
530
+ uint32_t i;
531
+
532
+ tpkg__last_err = TPKG_OK;
533
+ if (fd < 0 || m == NULL) {
534
+ return tpkg__fail(TPKG_ERR_ARG);
535
+ }
536
+ if (tpkg_validate(m) != 0) {
537
+ return -1; /* tpkg_errno set by tpkg_validate; nothing appended */
538
+ }
539
+
540
+ end = tpkg__lseek(fd, 0, SEEK_END);
541
+ if (end == (tpkg__off_t)-1) {
542
+ return tpkg__fail(TPKG_ERR_IO);
543
+ }
544
+
545
+ /* slot table */
546
+ for (i = 0; i < m->slot_count; i++) {
547
+ const tpkg_slot* s = &m->slots[i];
548
+ tpkg__put64(p + TPKG__REC_OFFSET, s->offset);
549
+ tpkg__put64(p + TPKG__REC_SIZE, s->size);
550
+ tpkg__put32(p + TPKG__REC_FORMAT, s->format_id);
551
+ tpkg__put32(p + TPKG__REC_FLAGS, s->flags);
552
+ tpkg__put_str(p + TPKG__REC_MOUNT, s->mount_point, TPKG_MOUNT_POINT_LEN);
553
+ p += TPKG_SLOT_SIZE;
554
+ }
555
+
556
+ /* trailer header */
557
+ hdr = p;
558
+ memcpy(hdr + TPKG__OFF_MAGIC, TPKG_MAGIC, TPKG_MAGIC_LEN);
559
+ tpkg__put32(hdr + TPKG__OFF_VERSION, m->version);
560
+ tpkg__put32(hdr + TPKG__OFF_PACKAGE_FLAGS, m->package_flags);
561
+ tpkg__put32(hdr + TPKG__OFF_SLOT_COUNT, m->slot_count);
562
+ tpkg__put64(hdr + TPKG__OFF_TABLE, (uint64_t)end);
563
+ tpkg__put_str(hdr + TPKG__OFF_RUNTIME_REF, m->runtime_ref, TPKG_RUNTIME_REF_LEN);
564
+ tpkg__put32(hdr + TPKG__OFF_LAUNCHER_ABI, m->launcher_abi);
565
+ tpkg__put32(hdr + TPKG__OFF_CRC32, tpkg_crc32(hdr, TPKG__OFF_CRC32));
566
+ p += TPKG_HEADER_SIZE;
567
+
568
+ total = (size_t)(p - buf);
569
+ if (tpkg__write_full(fd, buf, total) != 0) {
570
+ return tpkg__fail(TPKG_ERR_IO);
571
+ }
572
+ return 0;
573
+ }
574
+
575
+ #endif /* TPKG_IMPLEMENTATION */
data/lib/tebako/cli.rb CHANGED
@@ -38,6 +38,7 @@ require_relative "cache_manager"
38
38
  require_relative "cli_helpers"
39
39
  require_relative "error"
40
40
  require_relative "ruby_version"
41
+ require_relative "runtime_manager"
41
42
  require_relative "scenario_manager"
42
43
  require_relative "version"
43
44
 
@@ -45,6 +46,78 @@ require_relative "version"
45
46
  # Implementation of tebako command-line interface
46
47
  module Tebako
47
48
  DEFAULT_TEBAFILE = ".tebako.yml"
49
+
50
+ # 'tebako cache' subcommands: machine-wide prebuilt runtime package cache
51
+ class CacheCli < Thor
52
+ package_name "tebako cache"
53
+
54
+ desc "list", "List cached tebako runtime packages with sizes and ages"
55
+ def list
56
+ entries = runtime_manager.entries
57
+ return puts empty_message if entries.empty?
58
+
59
+ entries.each { |entry| puts entry_line(entry) }
60
+ puts total_line(entries)
61
+ end
62
+
63
+ desc "prune", "Remove cached tebako runtime packages"
64
+ method_option :all, type: :boolean, default: false, desc: "Remove all cached runtime packages"
65
+ method_option :"older-than", type: :string,
66
+ desc: "Remove runtime packages installed more than N days ago (e.g. 30d)"
67
+ def prune
68
+ removed = do_prune
69
+ return if removed.nil?
70
+
71
+ removed.each { |name| puts "Removed #{name}" }
72
+ puts "#{removed.size} cached runtime package(s) removed"
73
+ end
74
+
75
+ no_commands do
76
+ def runtime_manager
77
+ Tebako::RuntimeManager.new
78
+ end
79
+
80
+ def empty_message
81
+ "Runtime package cache is empty (#{File.join(runtime_manager.cache_root, "runtimes")})"
82
+ end
83
+
84
+ def entry_line(entry)
85
+ format("%<name>-44s %<size>9s %<age>s", name: entry[:name], size: human_size(entry[:size_bytes]),
86
+ age: human_age(entry[:installed_at]))
87
+ end
88
+
89
+ def total_line(entries)
90
+ format("%<label>-44s %<size>9s", label: "Total (#{entries.size} package(s))",
91
+ size: human_size(entries.sum { |entry| entry[:size_bytes] }))
92
+ end
93
+ end
94
+
95
+ no_commands do
96
+ def do_prune
97
+ return runtime_manager.prune(all: true) if options[:all]
98
+
99
+ match = /\A(?<days>\d+)d?\z/.match(options[:"older-than"].to_s)
100
+ return runtime_manager.prune(older_than_days: match[:days].to_i) if match
101
+
102
+ puts "Nothing to do: pass --all or --older-than Nd"
103
+ nil
104
+ end
105
+
106
+ def human_size(bytes)
107
+ format("%.1f MB", bytes / (1024.0 * 1024))
108
+ end
109
+
110
+ def human_age(installed_at)
111
+ age = Time.now - installed_at
112
+ if age < 3600 then "#{(age / 60).floor}m ago"
113
+ elsif age < 86_400 then "#{(age / 3600).floor}h ago"
114
+ else
115
+ "#{(age / 86_400).floor}d ago"
116
+ end
117
+ end
118
+ end
119
+ end
120
+
48
121
  # Tebako packager front-end
49
122
  class Cli < Thor # rubocop:disable Metrics/ClassLength
50
123
  package_name "Tebako"
@@ -82,6 +155,9 @@ module Tebako
82
155
  print Digest::SHA256.hexdigest [File.read(File.join(source, "CMakeLists.txt")), Tebako::VERSION].join
83
156
  end
84
157
 
158
+ desc "cache SUBCOMMAND", "Manage the machine-wide cache of prebuilt tebako runtime packages"
159
+ subcommand "cache", Tebako::CacheCli
160
+
85
161
  CWD_DESCRIPTION = <<~DESC
86
162
  Current working directory for packaged application. This directory shall be specified relative to root.
87
163
  #{" " * 65}# If this parameter is not set, the application will start in the current directory of the host file system.
@@ -97,6 +173,17 @@ module Tebako
97
173
  #{" " * 65}# If this parameter is not set, the application will start in the current directory of the host file system.
98
174
  DESC
99
175
 
176
+ RUNTIME_DESCRIPTION = <<~DESC
177
+ Runtime provenance: 'prebuilt' resolves/downloads a prebuilt tebako runtime package and stitches the
178
+ #{" " * 65}# application image onto it (default for the 'bundle' mode); 'source' keeps the Stage-2 source build.
179
+ #{" " * 65}# Modes other than 'bundle' always build from source.
180
+ DESC
181
+
182
+ IMAGE_DESCRIPTION = <<~DESC
183
+ Additional image to stitch into the package, '<path>:<mount-point>'; repeatable, mount points
184
+ #{" " * 65}# must be distinct. Prebuilt runtime only.
185
+ DESC
186
+
100
187
  desc "press", "Press tebako image"
101
188
  method_option :cwd, type: :string, aliases: "-c", required: false, desc: CWD_DESCRIPTION
102
189
  method_option :"log-level", type: :string, aliases: "-l", required: false, enum: %w[error warn debug trace],
@@ -113,6 +200,10 @@ module Tebako
113
200
  method_option :mode, type: :string, aliases: "-m", required: false, enum: %w[bundle both runtime application],
114
201
  desc: "Tebako press mode, 'bundle' by default"
115
202
  method_option :ref, type: :string, aliases: "-u", required: false, desc: REF_DESCRIPTION
203
+ method_option :runtime, type: :string, required: false, enum: %w[prebuilt source], desc: RUNTIME_DESCRIPTION
204
+ method_option :"build-runtime", type: :boolean, required: false,
205
+ desc: "Build the runtime from source (alias for '--runtime source')"
206
+ method_option :image, type: :array, required: false, desc: IMAGE_DESCRIPTION
116
207
 
117
208
  def press
118
209
  validate_press_options