From 482ac6d9494276b2c30ab2194e49d52045cb9e4a Mon Sep 17 00:00:00 2001 From: apio Date: Sat, 4 Feb 2023 19:02:08 +0100 Subject: [PATCH] feat: Add write support This uses a new struct (struct minitar_w). This struct is initialized using the minitar_open_w function, and deleted using minitar_close_w. The functions to write to an archive are minitar_write_file_entry() and minitar_write_special_entry(). The difference is that regular files have content, while the rest (special entries) do not. This commit also adds a new example program (pack), which can create tar archives from files (no directory support though). Archives created using pack and minitar can be read and extracted using GNU tar!! Documentation is coming in another commit. --- examples/CMakeLists.txt | 5 ++- examples/pack.c | 88 +++++++++++++++++++++++++++++++++++++++++ minitar.h | 15 +++++++ src/tar.c | 69 ++++++++++++++++++++++++++++++++ src/util.c | 50 +++++++++++++++++++++++ 5 files changed, 226 insertions(+), 1 deletion(-) create mode 100644 examples/pack.c 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' &&