TARFS 0.1.5
Read-only TAR filesystem for ESP32
Loading...
Searching...
No Matches
tar.c
Go to the documentation of this file.
1/*
2 * TARFS - Immutable (read-only) filesystem for embedded systems.
3 *
4 * Copyright (c) 2026 Viacheslav Logunov
5 * SPDX-License-Identifier: MMIT
6 *
7 * Author:
8 * Viacheslav Logunov <vvb333007@gmail.com>
9 *
10 * Project:
11 * https://github.com/vvb333007/tarfs
12 */
13
14
15#include <stdint.h>
16#include <stdio.h>
17#include <stddef.h>
18#include <string.h>
19#include <stdlib.h>
20#include <stdbool.h>
21
22#include "os.h"
23#include "fs.h"
24#include "tar.h"
25#include "hash.h"
26
27/* TAR CONCEPTS:
28 * Checked TAR String == "CTS", a byte sequence within a TAR archive terminated
29 * by '\0', '\r', or '\n'.
30 * Unchecked TAR String == "UTS", a byte sequence that may not be terminated.
31 * C string == a byte sequence terminated by '\0'.
32 *
33 * Strings stored in a TAR archive may or may not include a terminating
34 * character. For this reason, TARFS provides a set of string helper
35 * functions that mimic the behavior of the standard string.h functions.
36 *
37 * When operating on an Unchecked TAR String (UTS), the caller must provide
38 * a pointer to the end of the string (UTS limit).
39 */
40
41
42/*
43 * Compare an CTS/UTS with a CTS.
44 * s1 - UTS/CTS
45 * s1_end - UTS limit (or NULL for CTS)
46 * s2 - CTS
47 */
48int tar_strcmp(const char *s1, const char *s1_end, const char *s2) {
49
50 while (1) {
51
52
53 if (s1_end != NULL && s1 >= s1_end)
54 break;
55
56 unsigned char c1 = (unsigned char)*s1;
57 unsigned char c2 = (unsigned char)*s2;
58
59 if (c1 == '\0' || c1 == '\r' || c1 == '\n')
60 c1 = '\0';
61
62 if (c2 == '\0' || c2 == '\r' || c2 == '\n')
63 c2 = '\0';
64
65 if (c1 != c2)
66 return (int)c1 - (int)c2;
67
68 if (c1 == '\0')
69 return 0;
70
71 ++s1;
72 ++s2;
73 }
74
75 /* Reached the explicit end of the TAR field.
76 * Treat it as an implicit end-of-string.
77 */
78 unsigned char c2 = (unsigned char)*s2;
79
80 if (c2 == '\r' || c2 == '\n')
81 c2 = '\0';
82
83 return -(int)c2;
84}
85
86/*
87 * strncmp() for two Checked TAR Strings
88 *
89 */
90int tar_strncmp(const char *s1, const char *s2, size_t len) {
91
92 while (len > 0) {
93
94 unsigned char c1 = (unsigned char)*s1;
95 unsigned char c2 = (unsigned char)*s2;
96
97 if (c1 == '\0' || c1 == '\r' || c1 == '\n')
98 c1 = '\0';
99
100 if (c2 == '\0' || c2 == '\r' || c2 == '\n')
101 c2 = '\0';
102
103 if (c1 != c2)
104 return (int)c1 - (int)c2;
105
106 if (c1 == '\0')
107 return 0;
108
109 ++s1;
110 ++s2;
111 --len;
112 }
113
114 /* Reached the explicit end of the TAR field.
115 * Treat it as an implicit end-of-string.
116 */
117 unsigned char c2 = (unsigned char)*s2;
118
119 if (c2 == '\r' || c2 == '\n')
120 c2 = '\0';
121
122 return -(int)c2;
123}
124
125
126/*
127 * Return the length of an Unchecked TAR string.
128 * s1 - UTS/CTS
129 * s1_en - UTS limit (or NULL for CTS)
130 */
131int tar_strlen(const char *s1, const char *s1_end) {
132
133 const char *c = s1;
134
135 while ( true ) {
136 if (s1_end != NULL && c >= s1_end)
137 break;
138 if (*c == 0 || *c == '\r' || *c == '\n')
139 break;
140 c++;
141 }
142
143 s1_end = c;
144
145 return s1_end - s1;
146
147}
148
149/*
150 * Copy CTS to a buffer, finalize it with NUL, creating a C string
151 *
152 */
153void tar_strcpy(char *dst, const char *src) {
154
155 while ( true ) {
156 if (*src == 0 || *src == '\r' || *src == '\n')
157 break;
158 *dst++ = *src++;
159 }
160 *dst = '\0';
161}
162
163
164/*
165 * Duplicate an CTS or UTS as a special NUL-NUL-terminated C string.
166 *
167 * The returned buffer contains one additional byte after the terminating
168 * NUL, allowing a single character (typically '/') to be appended without
169 * reallocating. This is used during link resolution.
170 */
171char *tar_strdup1(const char *s1, const char *s1_end) {
172
173 size_t len = tar_strlen(s1, s1_end);
174
175 char *buf = tarfs_os_malloc(len + 1 + 1);
176
177 if (buf != NULL) {
178 memcpy(buf,s1,len);
179 buf[len+0] = 0;
180 buf[len+1] = 0;
181 }
182
183 return buf;
184}
185
186
187
188/* Parse an octal number from a TAR field. This is the only function which accepts UTS+len
189 * instead of UTS+UTS_end
190 * p - UTS
191 * max_len - UTS limit.
192 */
193uint32_t tar_octal(const char *p, size_t max_len) {
194
195 uint32_t value = 0;
196
197 /* Skip leading spaces */
198 while(max_len > 0 && *p == ' ') {
199 p++;
200 max_len--;
201 }
202
203 /* shift-add approach */
204 while (max_len > 0 && *p >= '0' && *p <= '7' ) {
205
206 value = (value << 3) | (*p - '0');
207 p++;
208 max_len--;
209 }
210 return value;
211}
212
213
214/* Calculate and verify the header checksum.
215 *
216 * TAR checksum rules require the checksum field itself to be treated
217 * as eight ASCII spaces while the checksum is being calculated.
218 *
219 * The checksum field occupies bytes [148..156).
220 */
221uint32_t tar_hdrsum(tarhdr_t const * hdr) {
222
223 uint8_t const *p = (uint8_t const *)hdr;
224 uint32_t calc = 0; /* checksum calculated from header contents */
225
226 for (size_t i = 0; i < sizeof(tarhdr_t); i++)
227 if (i >= 148 && i < (148 + 8))
228 calc += ' ';
229 else
230 calc += p[i];
231
232 return calc;
233}
234
235
236
249bool tar_badhdr(tarhdr_t const * hdr) {
250
251
252 uint32_t calc = 0, /* checksum calculated from header contents */
253 expc; /* checksum stored in the TAR header */
254
255 /* Quick sanity check.
256 *
257 * If the entry type is unknown, the header is either corrupted or
258 * describes a TAR feature we do not support. In either case, treat
259 * it as an unreadable entry and skip it.
260 */
261 switch (hdr->type) {
262 case TART_AFILE:
263 case TART_PAX:
264 case TART_PAX_G:
265 case TART_FILE:
266 case TART_HARDLINK:
267 case TART_SYMLINK:
268 case TART_CHRDEV:
269 case TART_BLKDEV:
270 case TART_DIR:
271 case TART_FIFO:
272 case TART_CONT:
273 /* Supported TAR entry type. */
274 break;
275 default:
276 return true;
277 };
278
279 /* Verify the reserved padding byte.
280 *
281 * POSIX TAR requires this byte to be zero. We also rely on this
282 * guarantee because it ensures that the preceding 'prefix' field
283 * is NUL-terminated.
284 */
285 if (hdr->zero != 0)
286 return true;
287
288 calc = tar_hdrsum(hdr);
289 expc = tar_octal(hdr->checksum, sizeof(hdr->checksum));
290
291 /* All zero TAR header will pass all these checks. We consider all-zero headers as invalid
292 * zero size entry (these can be found at the end of the tar archive, but this doesnt matter)
293 */
294 if (expc != calc || (calc == 0 && expc == 0))
295 return true;
296
297
298 return false;
299}
300
306int tar_getnino(const uint8_t *tar_start, size_t tar_length) {
307
308 uint32_t size;
309 size_t off = 0;
310 int bad = 0, files = 0, dirs = 0, links = 0, bad_total = 0, hdr_no = 0;
311 uintptr_t tar_end = (uintptr_t )((const uint8_t *)tar_start + tar_length);
312
313 while (off + sizeof(tarhdr_t) <= tar_length) {
314
315 const tarhdr_t *hdr = (const tarhdr_t *)(tar_start + off);
316
317 if (tar_badhdr(hdr)) {
318bad_header:
319 bad++;
320 off += sizeof(tarhdr_t);
321 continue;
322 }
323
324 if (bad) {
325 bad_total += bad;
326 bad = 0;
327 }
328
329 size = tar_octal(hdr->size, sizeof(hdr->size));
330
331 /* Check if size is sane: current pointer + 512 bytes + size must be < tar_end */
332 if (((uintptr_t)(hdr + 1)) + size >= tar_end) {
333 goto bad_header;
334 }
335
336 switch(hdr->type) {
337 case TART_AFILE:
338 case TART_CONT:
339 case TART_FILE: files++; break;
340
341 case TART_SYMLINK:
342 case TART_HARDLINK: links++; break;
343
344 case TART_DIR: dirs++; break;
345
346 default: break;
347 /* all-zero headers are filtered here */
348 };
349
350
351 off += sizeof(tarhdr_t);
352
353
354 /* Real size is 512 bytes aligned */
355 off += ((size_t)size + 511) & ~511u;
356
357 hdr_no++;
358 }
359
360 unsigned int total = files + links + dirs;
361
362 log("headers processed: %u, number of bad metadata headers: %u\r\n", hdr_no, bad_total);
363 log("# of inodes calculated: %u (%u files, %u dirs, %u links\r\n",total, files, dirs, links);
364
365 return total;
366}
367
372bool tar_rootdir(const uint8_t *tar_start, size_t tar_length, char *base_dir, size_t out_len) {
373
374 uint32_t size;
375 size_t off = 0;
376 uintptr_t tar_end = (uintptr_t )((const uint8_t *)tar_start + tar_length);
377
378 while (off + sizeof(tarhdr_t) <= tar_length) {
379
380 const tarhdr_t *hdr = (const tarhdr_t *)(tar_start + off);
381
382 size = 0;
383 if (!tar_badhdr(hdr)) {
384
385 size = tar_octal(hdr->size, sizeof(hdr->size));
386
387 if (((uintptr_t)(hdr + 1)) + size < tar_end) {
388
389
390 if (hdr->type == TART_DIR) {
391 if (hdr->name[0]) {
392
393 /* Check if guessed root is a valid mountpoint name (latin1, proper length )*/
394 int l = tar_strlen(hdr->name, &hdr->name[0] + sizeof(hdr->name));
395 if (l < sizeof(hdr->name) && l <= tarfs_os_mp_maxlen() && l < out_len) {
396 memcpy(base_dir, hdr->name, l - 1 );
397 base_dir[l-1] = 0;
398 return true;
399 }
400 }
401 }
402
403 } else
404 size = 0;
405 }
406
407 off += (((size_t)size + 511) & ~511u) + sizeof(tarhdr_t);
408 }
409 log("failed to guess root directory\r\n");
410 return false;
411}
412
413#if CONFIG_TARFS_LOG
414/* Displays string `buf`, which may or may not end with NUL: the line end
415 * markers are \r, \n and \0.
416 *
417 * We dont go past the `end` pointer even if we didnt find any line
418 * terminator from the list above
419 */
420void tar_print(const char *buf, const char *end) {
421
422 int len = 0;
423 const char *text = buf;
424
425 if (end == NULL)
426 end = (const char *)(-1);
427
428 /* Calculate remaining string length */
429 for (; (text + len) < end; len++) {
430 if (text[len] == '\0' || text[len] == '\r' || text[len] == '\n' )
431 break;
432 }
433
434 /* XXX: get rid of stack buffer here */
435 char b[len + 1];
436 memcpy(b,text,len);
437 b[len] = 0;
438
439 printf("%s",b);
440}
441#endif
442
443
444#if TARSUM_BUILD && CONFIG_TARFS_INTEGRITY
449int tar_addsum(uint8_t *tar_start, size_t tar_length) {
450
451 int inserted = 0;
452 uint32_t size;
453 size_t off = 0;
454 bool bad = 0;
455 uintptr_t tar_end = (uintptr_t )(tar_start + tar_length);
456
457 while (off + sizeof(tarhdr_t) <= tar_length) {
458
459 tarhdr_t *hdr = (tarhdr_t *)(tar_start + off);
460
461 if (tar_badhdr(hdr)) {
462
463bad_header:
464 if (!bad)
465 bad = true;
466
467 off += sizeof(tarhdr_t);
468 continue;
469 }
470
471 bad = 0;
472 size = tar_octal(hdr->size, sizeof(hdr->size));
473
474 /* Check if size is sane: current pointer + 512 bytes + size must be < tar_end */
475 if (((uintptr_t)(hdr + 1)) + size >= tar_end)
476 goto bad_header;
477
478
479 /* For entries having data (including PAX headers) we calculate CRC64 and inject it
480 * into header->padding[] field.
481 *
482 */
483 if (size > 0) {
484 uint32_t new_sum;
485 uint8_t octet;
486 char tmp[9] = { ' ',' ',' ',' ',' ',' ',' ',' ',' '};
487 void const *data = (void *)(hdr + 1);
488 uint64_t icv;
489
490 /* calculate integrity check value */
491 icv = hash64(0, data, size);
492
493 /* inject it byte by byte in Little Endian byte order */
494 for (int i = 0; i < 8; i++) {
495 octet = icv & 0xff;
496 icv >>= 8;
497 hdr->digest[i] = octet;
498 }
499
500 /* insert CRC type signature (CRC64)*/
501 hdr->md[0] = 'C';
502 hdr->md[1] = '6';
503 hdr->md[2] = '4';
504
505 hdr->zero = 0;
506
507 /* calculate new header checksum */
508 new_sum = tar_hdrsum(hdr);
509
510 /* inject it into header in octal ASCII form. */
511 snprintf(tmp, sizeof(tmp), "%-8o", new_sum);
512 memcpy(hdr->checksum, tmp, 8);
513 inserted++;
514 }
515
516 /* Real size is 512 bytes aligned */
517 off += sizeof(tarhdr_t) + (((size_t)size + 511) & ~511u);
518 }
519
520 return inserted;
521}
522#endif /* TARSUM_BUILD */
523
524
529bool tar_baddata(struct tarhdr const *hdr, size_t size) {
530
531#if CONFIG_TARFS_INTEGRITY
532 uint64_t icv_calc, icv_hdr;
533
534 /* CRC64 is verified only for entries containing data (including PAX
535 * headers). The computed CRC is compared against the value stored in
536 * hdr->padding[]. Empty headers (size == 0) are ignored.
537 */
538 if (size > 0) {
539
540 void const *data = (void *)(hdr + 1);
541
542 /* calculate integrity check value */
543 icv_calc = hash64(0, data, size);
544 memcpy(&icv_hdr, hdr->digest, 8);
545
546#if CONFIG_TARFS_BIG_ENDIAN
547 /* ICV is stored in little-endian byte order */
548 icv_hdr = __builtin_bswap64(icv_hdr);
549#endif
550 return icv_hdr != icv_calc;
551 }
552#endif /* #if CONFIG_TARFS_INTEGRITY */
553
554 return false;
555}
556
557
#define log(Format_,...)
Definition fs.h:445
uint64_t hash64(uint64_t prev_crc, void const *buffer0, size_t buf_len)
CRC-64/ECMA-182 algoritm.
Definition hash.c:163
size_t tarfs_os_mp_maxlen()
Return the maximum mount point name length supported by the platform.
Definition os_esp32.c:175
void * tarfs_os_malloc(size_t size)
Memory allocation backend.
Definition os_esp32.c:187
For TAR files with modified PADDING field (see tarsum.c TARFS Checksum Utility): The type and meaning...
Definition tar.h:55
void tar_strcpy(char *dst, const char *src)
Definition tar.c:153
int tar_strncmp(const char *s1, const char *s2, size_t len)
Definition tar.c:90
char * tar_strdup1(const char *s1, const char *s1_end)
Duplicate a TAR string as a regular NUL-terminated C string.
Definition tar.c:171
uint32_t tar_octal(const char *p, size_t max_len)
Definition tar.c:193
bool tar_badhdr(tarhdr_t const *hdr)
Validate a TAR header.
Definition tar.c:249
int tar_getnino(const uint8_t *tar_start, size_t tar_length)
Quick run through the tarfile to count number of inodes we have to create.
Definition tar.c:306
uint32_t tar_hdrsum(tarhdr_t const *hdr)
Definition tar.c:221
int tar_strcmp(const char *s1, const char *s1_end, const char *s2)
Compare an UTS/CTS to a CTS.
Definition tar.c:48
int tar_strlen(const char *s1, const char *s1_end)
Return the length of a TAR string.
Definition tar.c:131
bool tar_baddata(struct tarhdr const *hdr, size_t size)
Verify CRC64 checksum stored in a TAR archive, if present.
Definition tar.c:529
bool tar_rootdir(const uint8_t *tar_start, size_t tar_length, char *base_dir, size_t out_len)
Detect the archive root directory.
Definition tar.c:372
const char size[12]
Definition tar.h:5