diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 3eb4d8e..3865e2f 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -4,4 +4,7 @@ target_link_libraries(list PRIVATE minitar) add_executable(untar EXCLUDE_FROM_ALL untar.c) target_link_libraries(untar PRIVATE minitar) -add_custom_target(examples DEPENDS list untar) \ No newline at end of file +add_executable(pack EXCLUDE_FROM_ALL pack.c) +target_link_libraries(pack PRIVATE minitar) + +add_custom_target(examples DEPENDS list untar pack) \ No newline at end of file diff --git a/examples/pack.c b/examples/pack.c new file mode 100644 index 0000000..810ae75 --- /dev/null +++ b/examples/pack.c @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2023, apio. + * + * SPDX-License-Identifier: BSD-2-Clause + * + * pack.c: Example utility which creates a tar archive (POSIX only). + */ + +#define _XOPEN_SOURCE 700 +#include +#include +#include +#include +#include + +int main(int argc, char** argv) +{ + if (argc < 3) + { + fprintf(stderr, "Usage: %s [output] files\n", argv[0]); + return 1; + } + struct minitar_w mp; + if (minitar_open_w(argv[1], &mp, MTAR_OVERWRITE) != 0) + { + perror(argv[1]); + return 1; + } + int exit_status = 0; + struct minitar_entry_metadata metadata; + int arg = 2; + while (arg < argc) + { + FILE* fp = fopen(argv[arg], "r"); + if (!fp) + { + perror("fopen"); + exit_status = 1; + break; + } + + // Get the file length. + fseek(fp, 0, SEEK_END); + size_t length = ftell(fp); + fseek(fp, 0, SEEK_SET); + + char* buf = malloc(length); + fread(buf, 1, length, fp); + if (ferror(fp)) + { + perror("fread"); + exit_status = 1; + break; + } + + struct stat st; + int rc = fstat(fileno(fp), &st); + if (rc < 0) + { + perror("fstat"); + exit_status = 1; + break; + } + + struct minitar_entry_metadata metadata; + strncpy(metadata.path, argv[arg], sizeof(metadata.path)); + metadata.uid = st.st_uid; + metadata.gid = st.st_gid; + metadata.mtime = st.st_mtim.tv_sec; + metadata.size = length; + metadata.type = MTAR_REGULAR; + metadata.mode = st.st_mode & ~S_IFMT; + + rc = minitar_write_file_entry(&mp, &metadata, buf); + free(buf); + + if (rc != 0) + { + perror("write entry failed"); + exit_status = 1; + break; + } + + arg++; + } + minitar_close_w(&mp); + return exit_status; +} \ No newline at end of file diff --git a/minitar.h b/minitar.h index 8a8aeee..d9ccac5 100644 --- a/minitar.h +++ b/minitar.h @@ -17,6 +17,17 @@ struct minitar FILE* stream; }; +struct minitar_w +{ + FILE* stream; +}; + +enum minitar_write_mode +{ + MTAR_APPEND, + MTAR_OVERWRITE +}; + enum minitar_file_type { MTAR_REGULAR, @@ -62,13 +73,17 @@ extern "C" #endif int minitar_open(const char* pathname, struct minitar* out); + int minitar_open_w(const char* pathname, struct minitar_w* out, enum minitar_write_mode mode); int minitar_read_entry(struct minitar* mp, struct minitar_entry* out); + int minitar_write_file_entry(struct minitar_w* mp, const struct minitar_entry_metadata* metadata, char* buf); + int minitar_write_special_entry(struct minitar_w* mp, const struct minitar_entry_metadata* metadata); void minitar_rewind(struct minitar* mp); int minitar_find_by_name(struct minitar* mp, const char* name, struct minitar_entry* out); int minitar_find_by_path(struct minitar* mp, const char* path, struct minitar_entry* out); int minitar_find_any_of(struct minitar* mp, enum minitar_file_type type, struct minitar_entry* out); size_t minitar_read_contents(struct minitar* mp, const struct minitar_entry* entry, char* buf, size_t max); int minitar_close(struct minitar* mp); + int minitar_close_w(struct minitar_w* mp); #ifdef __cplusplus } diff --git a/src/tar.c b/src/tar.c index 7cc8123..ed01004 100644 --- a/src/tar.c +++ b/src/tar.c @@ -11,10 +11,18 @@ #include #include +#ifndef __TINYC__ +#include +#else +#define noreturn _Noreturn +#endif + // all of these are defined in util.c int minitar_read_header(struct minitar*, struct tar_header*); +noreturn void minitar_handle_panic(const char*); int minitar_validate_header(const struct tar_header*); void minitar_parse_metadata_from_tar_header(const struct tar_header*, struct minitar_entry_metadata*); +void minitar_construct_header_from_metadata(struct tar_header*, const struct minitar_entry_metadata*); size_t minitar_align_up_to_block_size(size_t); int minitar_open(const char* pathname, struct minitar* out) @@ -27,11 +35,31 @@ int minitar_open(const char* pathname, struct minitar* out) return 0; } +int minitar_open_w(const char* pathname, struct minitar_w* out, enum minitar_write_mode mode) +{ + const char* mode_string; + switch (mode) + { + case MTAR_APPEND: mode_string = "ab"; break; + case MTAR_OVERWRITE: mode_string = "wb"; break; + default: minitar_handle_panic("mode passed to minitar_open_w is not supported"); + } + FILE* fp = fopen(pathname, mode_string); + if (!fp) return -1; + out->stream = fp; + return 0; +} + int minitar_close(struct minitar* mp) { return fclose(mp->stream); } +int minitar_close_w(struct minitar_w* mp) +{ + return fclose(mp->stream); +} + // Try to read a valid header, and construct an entry from it. If the 512-byte block at the current read offset is not a // valid header, valid is set to 0 so we can try again with the next block. In any other case, valid is set to 1. This // helps distinguish valid return values, EOF, and invalid headers that we should just skip. @@ -74,6 +102,47 @@ int minitar_read_entry(struct minitar* mp, struct minitar_entry* out) return result; } +int minitar_write_file_entry(struct minitar_w* mp, const struct minitar_entry_metadata* metadata, char* buf) +{ + struct minitar_entry_metadata meta = *metadata; + meta.type = MTAR_REGULAR; + + struct tar_header hdr; + minitar_construct_header_from_metadata(&hdr, &meta); + // Write the header. + size_t nwrite = fwrite(&hdr, sizeof(hdr), 1, mp->stream); + if (nwrite == 0 && ferror(mp->stream)) return -1; + + // Write the file data. + nwrite = fwrite(buf, 1, meta.size, mp->stream); + if (nwrite == 0 && ferror(mp->stream)) return -1; + + char zeroes[512]; + memset(zeroes, 0, sizeof(zeroes)); + + // Write as many zeroes as necessary to finish a block. + size_t nzero = minitar_align_up_to_block_size(meta.size) - meta.size; + nwrite = fwrite(zeroes, 1, nzero, mp->stream); + if (nwrite == 0 && ferror(mp->stream)) return -1; + + return 0; +} + +int minitar_write_special_entry(struct minitar_w* mp, const struct minitar_entry_metadata* metadata) +{ + struct minitar_entry_metadata meta = *metadata; + if (meta.type == MTAR_REGULAR) + minitar_handle_panic("Trying to write a special entry, yet MTAR_REGULAR passed as the entry type"); + meta.size = 0; + + struct tar_header hdr; + minitar_construct_header_from_metadata(&hdr, &meta); + size_t nwrite = fwrite(&hdr, sizeof(hdr), 1, mp->stream); + if (nwrite == 0 && ferror(mp->stream)) return -1; + + return 0; +} + void minitar_rewind(struct minitar* mp) { rewind(mp->stream); diff --git a/src/util.c b/src/util.c index 84bf178..5cc7d4c 100644 --- a/src/util.c +++ b/src/util.c @@ -234,6 +234,56 @@ uint32_t minitar_checksum_header(const struct tar_header* hdr) return sum; } +void minitar_construct_header_from_metadata(struct tar_header* hdr, const struct minitar_entry_metadata* metadata) +{ + if (strlen(metadata->path) > 100) + { + minitar_handle_panic("FIXME: pathnames over 100 (using the prefix field) are unsupported for now"); + } + + // We intentionally want strncpy to not write a null terminator here if the path field is 100 bytes long. + strncpy(hdr->name, metadata->path, 100); + + snprintf(hdr->mode, 8, "%.7o", metadata->mode); + snprintf(hdr->uid, 8, "%.7o", metadata->uid); + snprintf(hdr->gid, 8, "%.7o", metadata->gid); + + // snprintf will write the null terminator past the size field. We don't care, as we will overwrite that zero later. + snprintf(hdr->size, 13, "%.12zo", metadata->size); + // Same here. + snprintf(hdr->mtime, 13, "%.12lo", metadata->mtime); + + switch (metadata->type) + { + case MTAR_REGULAR: hdr->typeflag = '0'; break; + case MTAR_HARDLINK: hdr->typeflag = '1'; break; + case MTAR_SYMLINK: hdr->typeflag = '2'; break; + case MTAR_CHRDEV: hdr->typeflag = '3'; break; + case MTAR_BLKDEV: hdr->typeflag = '4'; break; + case MTAR_DIRECTORY: hdr->typeflag = '5'; break; + case MTAR_FIFO: hdr->typeflag = '6'; break; + } + + strncpy(hdr->linkname, metadata->link, 100); + + memcpy(hdr->magic, "ustar", 6); + + hdr->version[0] = '0'; + hdr->version[1] = '0'; + + strncpy(hdr->uname, metadata->uname, 32); + strncpy(hdr->gname, metadata->gname, 32); + + snprintf(hdr->devmajor, 8, "%.7o", metadata->devmajor); + snprintf(hdr->devminor, 8, "%.7o", metadata->devminor); + + memset(hdr->prefix, 0, sizeof(hdr->prefix)); + memset(hdr->padding, 0, sizeof(hdr->padding)); + + uint32_t checksum = minitar_checksum_header(hdr); + snprintf(hdr->chksum, 8, "%.7o", checksum); +} + int minitar_validate_header(const struct tar_header* hdr) { if (hdr->typeflag != '\0' && hdr->typeflag != '0' && hdr->typeflag != '1' && hdr->typeflag != '2' &&