diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..7ed8115 --- /dev/null +++ b/.clang-format @@ -0,0 +1,79 @@ +AccessModifierOffset: -4 +AlignAfterOpenBracket: Align +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlines: Right +AlignOperands: false +AlignTrailingComments: false +AllowAllParametersOfDeclarationOnNextLine: false +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: None +AllowShortIfStatementsOnASingleLine: false +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: Yes +BinPackArguments: true +BinPackParameters: true +BreakBeforeBraces: Custom +BraceWrapping: + AfterEnum: true + AfterClass: true + AfterControlStatement: true + AfterFunction: true + AfterNamespace: true + AfterStruct: true + AfterUnion: true + AfterExternBlock: true + BeforeCatch: true + BeforeElse: true + IndentBraces: false + SplitEmptyFunction: false + SplitEmptyRecord: false + SplitEmptyNamespace: false +BreakBeforeBinaryOperators: NonAssignment +BreakBeforeTernaryOperators: true +BreakConstructorInitializers: BeforeComma +BreakInheritanceList: BeforeComma +BreakStringLiterals: true +ColumnLimit: 80 +CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: false +ConstructorInitializerIndentWidth: 4 +Cpp11BracedListStyle: false +DerivePointerAlignment: false +FixNamespaceComments: true +ForEachMacros: ['ILIST_FOREACH', 'ILIST_FOREACH_ENTRY'] +IncludeBlocks: Regroup +IncludeCategories: + - Regex: '<.*>' + Priority: 1 + - Regex: '.*' + Priority: 2 +IndentCaseLabels: false +IndentPPDirectives: AfterHash +IndentWidth: 4 +IndentWrappedFunctionNames: false +KeepEmptyLinesAtTheStartOfBlocks: false +Language: Cpp +NamespaceIndentation: All +PointerAlignment: Right +ReflowComments: true +SortIncludes: true +SortUsingDeclarations: false +SpaceAfterCStyleCast: false +SpaceAfterTemplateKeyword: true +SpaceBeforeAssignmentOperators: true +SpaceBeforeCpp11BracedList: false +SpaceBeforeCtorInitializerColon: true +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: true +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: false +SpacesInCStyleCastParentheses: false +SpacesInContainerLiterals: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +TabWidth: 4 +UseTab: Never diff --git a/epitar/Makefile b/epitar/Makefile index b3430bf..10068ef 100644 --- a/epitar/Makefile +++ b/epitar/Makefile @@ -21,13 +21,18 @@ $(TARGET): $(OBJS) $(CC) -o $@ $^ $(LDFLAGS) $(LDLIBS) @echo $(OBJS) +all: $(OBJS) + $(CC) -o $(TARGET) $^ $(LDFLAGS) $(LDLIBS) + debug: CFLAGS += $(DBG_CFLAGS) debug: LDFLAGS += $(DBG_LDFLAGS) debug: $(OBJS) $(CC) -o $(TARGET) $^ $(LDFLAGS) $(LDLIBS) -check: - dash ./tests/run.sh +check: $(TARGET) + bash ./tests/functional/old_testsuite.sh + bash ./tests/functional/run.sh + bash ./tests/functional/warning.sh clean: $(RM) $(TARGET) diff --git a/epitar/src/main.c b/epitar/src/main.c index 3e1f5d9..92171d5 100644 --- a/epitar/src/main.c +++ b/epitar/src/main.c @@ -1,6 +1,58 @@ #include "tar/archive.h" #include "tar/extract.h" #include "utils/config.h" +#include "utils/errors.h" + +static int handle_err_code(enum error_code err, struct config *config) +{ + int ret_code = 0; + switch (err) + { + case SUCCESS: + ret_code = 0; + break; + + case BAD_CHECKSUM: + puts("epitar: bad checksum"); + ret_code = 2; + break; + + case NOT_IMPLEMENTED: + puts("epitar: Function not implemented"); + ret_code = 2; + break; + + case FILE_NOT_FOUND: + if (config->mode == ARCHIVE) + { + // Rest of code should have printed the first part of the line + printf(" to tarball %s\n", config->archive_file); + ret_code = 3; + break; + } + __attribute__((fallthrough)); + + case EMPTY_ARCHIVE: + if (config->mode == ARCHIVE) + { + puts("epitar: cowardly refusing to create an empty archive"); + ret_code = 1; + } + else + { + printf("epitar: error extracting tarball %s\n", + config->archive_file); + ret_code = 3; + } + break; + + default: + printf("epitar: error extracting tarball %s\n", config->archive_file); + ret_code = 3; + break; + } + return ret_code; +} int main(int argc, char **argv) { @@ -31,22 +83,7 @@ int main(int argc, char **argv) err = archive(config.archive_file, config.input_files, config.verbose); } - switch (err) - { - case SUCCESS: - return 0; - - // case FILE_NOT_FOUND: - // case UNKNOWN_ERROR: - // printf("epitar: error extracting tarball %s\n", config.archive_file); - // return 3; - - case BAD_CHECKSUM: - puts("epitar: bad checksum"); - return 2; - - default: - printf("epitar: error extracting tarball %s\n", config.archive_file); - return 3; - } + int ret_code = handle_err_code(err, &config); + destroy_config(&config); + return ret_code; } diff --git a/epitar/src/tar/archive.c b/epitar/src/tar/archive.c index 014bcd1..ed0b247 100644 --- a/epitar/src/tar/archive.c +++ b/epitar/src/tar/archive.c @@ -1,8 +1,235 @@ #include "archive.h" -enum error_code archive(char *archive_name, char **files, bool verbose) { - (void) archive_name; - (void) files; - (void) verbose; - return UNKNOWN_ERROR; +#include +#include +#include +#include +#include +#include + +#include "../utils/filesystem.h" +#include "tar.h" + +static size_t getfilesize(FILE *file) +{ + fseek(file, 0L, SEEK_END); + size_t file_size = ftell(file); + fseek(file, 0L, SEEK_SET); // Go back + return file_size; +} + +/** + * @brief Creates and returns the header for the file located at `file_path` and + * with content `file_stream` + * @param file_stream Pointer to the content of the file (as a stream) if it's a + * file. If it's NULL file will be considered as a directory + */ +static struct ustar_header create_header(FILE *file_stream, char *file_path) +{ + struct ustar_header header = { 0 }; + + // Name + strncpy(header.name, file_path, TNAMELEN); + // Magic tag + strncpy(header.magic, USTAR_MAGIC, TMAGLEN); + // Versions + memcpy(header.version, TVERSION, TVERSLEN); + + // uid, gid, mtime and mode + struct stat st; + if (stat(file_path, &st) == -1) + { + printf("Could not stat file %s\n", file_path); + return header; + } + // TODO links + // UID + snprintf(header.uid, sizeof(header.uid), "%07o", st.st_uid); + // GID + snprintf(header.gid, sizeof(header.gid), "%07o", st.st_gid); + // Mtime + snprintf(header.mtime, sizeof(header.mtime), "%011lo", st.st_mtime); + // Mode + snprintf(header.mode, sizeof(header.mode), "%07o", st.st_mode & 0777); + + // User name + struct passwd *pw = getpwuid(st.st_uid); + if (pw != NULL) + strncpy(header.uname, pw->pw_name, 32); + // Group name + struct group *gr = getgrgid(st.st_gid); + if (gr != NULL) + strncpy(header.gname, gr->gr_name, 32); + + // File or directory + if (file_stream == NULL) + { + // === Directory + header.name[strlen(header.name)] = '/'; + // Type + header.typeflag = DIRTYPE; + // Size + snprintf(header.size, 12, "%011o", 0); + } + else + { + // === File + // Size + snprintf(header.size, 12, "%011lo", st.st_size); + // Type + header.typeflag = REGTYPE; + } + + // Checksum + union tar_header header_union = { .ustar = header }; + compute_checksum(&header_union); + return header_union.ustar; +} + +/** + * @brief Adds a file (header + content) to the archive stream + * @param archive The target archive stream + * @param file_path The path where the file to be archived is located + * @return SUCCESS or the corresponding error code + */ +static enum error_code archivefile(FILE *archive, char *file_path, bool verbose) +{ + if (verbose) + puts(file_path); + + FILE *file = fopen(file_path, "r"); + if (file == NULL) + { + printf("unable to add file %s", file_path); + return FILE_NOT_FOUND; + } + + // Create header (ustar) + struct ustar_header header = create_header(file, file_path); + size_t file_size = getfilesize(file); + + // Write header + size_t nwritten = fwrite(&header, sizeof(struct ustar_header), 1, archive); + if (nwritten < 1) + { + fclose(file); + return CANNOT_CREATE_TARGET; + } + + // Copy file content + fcat(file, archive, file_size); + + // Add padding + size_t total_size = + (((file_size + BLOCK_SIZE - 1) / BLOCK_SIZE) * BLOCK_SIZE); + size_t padding = total_size - file_size; + + char empty_buf[BLOCK_SIZE] = { 0 }; + size_t remaining = padding; + nwritten = fwrite(empty_buf, sizeof(char), remaining, archive); + if (nwritten < remaining) + { + fclose(file); + return CANNOT_CREATE_TARGET; + } + + return SUCCESS; +} + +/** + * @brief Archives a hole directory (recursively) + * @warn Assumes that the director path is valid + * @return SUCCESS or the corresponding error + */ +static enum error_code archivedir(FILE *archive, char *dir_path, bool verbose) +{ + // Add the directory itself + if (verbose) + printf("%s/\n", dir_path); + + // Header + struct ustar_header header = create_header(NULL, dir_path); + size_t nwritten = fwrite(&header, sizeof(struct ustar_header), 1, archive); + if (nwritten < 1) + return CANNOT_CREATE_TARGET; + + // Archive members + char *filename; + while ((filename = listdirectory(dir_path)) != NULL) + { + enum error_code err; + enum error_code isdir = isdirectory(filename); + if (isdir == FILE_NOT_FOUND) + { + // Does not exists + printf("unable to add file %s", dir_path); + err = FILE_NOT_FOUND; + } + else if (isdir == SUCCESS) + { + // Directory + err = archivedir(archive, filename, verbose); + } + else + { + // File + err = archivefile(archive, filename, verbose); + } + + if (err != SUCCESS) + { + freedirectory(); + return err; + } + } + + freedirectory(); // Just in case + return SUCCESS; +} + +enum error_code archive(char *archive_name, char **files, bool verbose) +{ + if (archive_name == NULL || files == NULL) + return UNKNOWN_ERROR; + if (*files == NULL) + return EMPTY_ARCHIVE; + + FILE *archive_stream = fopen(archive_name, "w+"); + + char **current_file = files; + while (*current_file != NULL) + { + enum error_code err; + enum error_code isdir = isdirectory(*current_file); + if (isdir == FILE_NOT_FOUND) + { + // Does not exists + printf("unable to add file %s", *current_file); + err = FILE_NOT_FOUND; + } + else if (isdir == SUCCESS) + { + // Directory + err = archivedir(archive_stream, *current_file, verbose); + } + else + { + // File + err = archivefile(archive_stream, *current_file, verbose); + } + + if (err != SUCCESS) + { + fclose(archive_stream); + return err; + } + + current_file++; + } + + // Terminating NULL blocks + char empty_buf[BLOCK_SIZE * 2] = { 0 }; + fwrite(empty_buf, sizeof(char), BLOCK_SIZE * 2, archive_stream); + fclose(archive_stream); + return SUCCESS; } diff --git a/epitar/src/tar/archive.h b/epitar/src/tar/archive.h index 546f4ff..f9dd21f 100644 --- a/epitar/src/tar/archive.h +++ b/epitar/src/tar/archive.h @@ -1,10 +1,10 @@ #ifndef ARCHIVE_H #define ARCHIVE_H -#include "../utils/errors.h" - #include +#include "../utils/errors.h" + /* @brief Archives the listed `files` into `archive_name` * @return 0 on success, the corresponding error code otherwise */ diff --git a/epitar/src/tar/extract.c b/epitar/src/tar/extract.c index 1de4b9d..28f2677 100644 --- a/epitar/src/tar/extract.c +++ b/epitar/src/tar/extract.c @@ -5,6 +5,7 @@ #include #include "../utils/filesystem.h" +#include "tar.h" /** * @@ -13,7 +14,6 @@ */ static enum error_code extract_ustar(FILE *stream, struct ustar_header *header) { - struct file_stats stats = { .name = header->name, .length = strtol(header->size, NULL, 8), .content_stream = stream, @@ -50,9 +50,14 @@ static enum error_code extract_ustar(FILE *stream, struct ustar_header *header) static enum error_code extract_unixtar(FILE *stream, struct unixtar_header *header) { - (void)stream; - (void)header; - return NOT_IMPLEMENTED; + struct file_stats stats = { .name = header->name, + .length = strtol(header->size, NULL, 8), + .content_stream = stream, + .mode = header->mode, + .uid = header->uid, + .gid = header->gid, + .mtime = header->mtime }; + return create_file(&stats); } /** @@ -70,14 +75,14 @@ static enum error_code extract_file(FILE *content, union tar_header *header, if ((ustar_header = get_ustar_header(header)) != NULL) { if (verbose) - printf("./%s\n", ustar_header->name); + printf("%s\n", ustar_header->name); return extract_ustar(content, ustar_header); } // UNIXTAR else if ((unixtar_header = get_unixtar_header(header)) != NULL) { if (verbose) - printf("./%s", unixtar_header->name); + printf("%s\n", unixtar_header->name); return extract_unixtar(content, unixtar_header); } // Unsupported format @@ -113,6 +118,7 @@ enum error_code extract(char *archive_name, bool verbose) if (bytes_read < sizeof(union tar_header)) { read_state = INVALID_FORMAT; // Unexpected EOF + continue; } // Check if empty @@ -130,6 +136,12 @@ enum error_code extract(char *archive_name, bool verbose) got_empty_block = false; } + if (!check_checksum(&header)) + { + fclose(stream); + return BAD_CHECKSUM; + } + // Extract file enum error_code err = extract_file(stream, &header, verbose); if (err != SUCCESS) @@ -140,7 +152,8 @@ enum error_code extract(char *archive_name, bool verbose) // Calculate how many padding bytes need to be skipped size_t file_size = strtol(header.ustar.size, NULL, 8); - size_t padded_size = ((file_size + BLOCK_SIZE - 1) / BLOCK_SIZE) * BLOCK_SIZE; + size_t padded_size = + ((file_size + BLOCK_SIZE - 1) / BLOCK_SIZE) * BLOCK_SIZE; size_t skip_bytes = padded_size - file_size; if (skip_bytes > 0 && fseek(stream, skip_bytes, SEEK_CUR) != 0) diff --git a/epitar/src/tar/tar.c b/epitar/src/tar/tar.c index 5defa56..ee9fd7a 100644 --- a/epitar/src/tar/tar.c +++ b/epitar/src/tar/tar.c @@ -1,6 +1,6 @@ #include "tar.h" -#include +#include #include struct unixtar_header *get_unixtar_header(union tar_header *header) @@ -26,34 +26,58 @@ bool check_checksum(union tar_header *header) { struct ustar_header *ustar_header; struct unixtar_header *unixtar_header; - int actual_checksum; + char actual_checksum[TCHKSUMLEN]; // USTAR if ((ustar_header = get_ustar_header(header)) != NULL) { - actual_checksum = atoi(ustar_header->chksum); + strcpy(actual_checksum, ustar_header->chksum); + return strcmp(actual_checksum, header->ustar.chksum) == 0; } // UNIXTAR else if ((unixtar_header = get_unixtar_header(header)) != NULL) { - actual_checksum = atoi(unixtar_header->chksum); + strcpy(actual_checksum, unixtar_header->chksum); + return strcmp(actual_checksum, header->unixtar.chksum) == 0; } // Unsupported format else { return false; } - - int computed_checksum = compute_checksum(header); - return computed_checksum == actual_checksum; } -int compute_checksum(union tar_header *header) +void compute_checksum(union tar_header *header) { - char *block = header->raw_block; - int sum = 0; + // Set to spaces + struct unixtar_header *unixtar_header = get_unixtar_header(header); + struct ustar_header *ustar_header = get_ustar_header(header); + if (ustar_header) + { + memset(ustar_header->chksum, ' ', TCHKSUMLEN); + } + else if (unixtar_header) + { + memset(unixtar_header->chksum, ' ', TCHKSUMLEN); + } + + // Compute + unsigned char *block = header->raw_block; + unsigned int sum = 0; for (int i = 0; i < BLOCK_SIZE; i++) sum += block[i]; - return sum; + // Update checksum + if (ustar_header) + { + snprintf(ustar_header->chksum, TCHKSUMLEN - 1, "%06o", sum); + ustar_header->chksum[TCHKSUMLEN - 2] = '\0'; + ustar_header->chksum[TCHKSUMLEN - 1] = ' '; + } + else if (unixtar_header) + { + snprintf(unixtar_header->chksum, TCHKSUMLEN - 1, "%06o", sum); + unixtar_header->chksum[TCHKSUMLEN - 2] = '\0'; + unixtar_header->chksum[TCHKSUMLEN - 1] = ' '; + } } diff --git a/epitar/src/tar/tar.h b/epitar/src/tar/tar.h index ebf660c..82dab5f 100644 --- a/epitar/src/tar/tar.h +++ b/epitar/src/tar/tar.h @@ -40,6 +40,11 @@ #define TOWRITE 00002 /* write by other */ #define TOEXEC 00001 /* execute/search by other */ +// Non standard definitions +#define TNAMELEN 100 /* Length of the name field */ +#define TSIZELEN 12 /* Length of the size field */ +#define TCHKSUMLEN 8 /* Length of the size field */ + // === Structures struct ustar_header @@ -60,6 +65,7 @@ struct ustar_header char devmajor[8]; char devminor[8]; char prefix[155]; + char padding[12]; }; struct unixtar_header @@ -71,7 +77,7 @@ struct unixtar_header char size[12]; char mtime[12]; char chksum[8]; - char link[1]; + char link; char linkname[100]; char padding[255]; // Should be empty }; @@ -80,7 +86,7 @@ union tar_header { struct unixtar_header unixtar; struct ustar_header ustar; - char raw_block[BLOCK_SIZE]; + unsigned char raw_block[BLOCK_SIZE]; }; // === Functions @@ -97,13 +103,14 @@ struct unixtar_header *get_unixtar_header(union tar_header *header); struct ustar_header *get_ustar_header(union tar_header *header); /* @brief checks if file has not been corrupted via checksum + * @warn Alters header checksum with the new one * @return true if member file is valid, false otherwise */ bool check_checksum(union tar_header *header); -/* @brief computes the checksum of the given file +/* @brief computes the checksum of the given header and updates it * @return the checksum */ -int compute_checksum(union tar_header *header); +void compute_checksum(union tar_header *header); #endif // TAR_H diff --git a/epitar/src/utils/config.c b/epitar/src/utils/config.c index e2d82c6..a46a5a7 100644 --- a/epitar/src/utils/config.c +++ b/epitar/src/utils/config.c @@ -81,7 +81,7 @@ enum arg_error_code args_handler(int argc, char **argv, struct config *config) // Parse option flags int opt; - while ((opt = getopt(argc, argv, "cxvh")) != -1) + while ((opt = getopt(argc, argv, ":cvxh")) != -1) { int err = handle_opt(opt, config); if (err == ARG_HELP) @@ -190,7 +190,7 @@ void print_invalid_option(void) puts("Try './epitar -h' for more information"); } -void config_destroy(struct config *config) +void destroy_config(struct config *config) { if (config == NULL) return; @@ -203,5 +203,5 @@ void config_destroy(struct config *config) // Note: archive_file points to argv, so we don't free it - free(config); + // free(config); } diff --git a/epitar/src/utils/config.h b/epitar/src/utils/config.h index c429c96..a79bfc9 100644 --- a/epitar/src/utils/config.h +++ b/epitar/src/utils/config.h @@ -55,7 +55,7 @@ void print_invalid_option(void); */ bool validate_config(struct config *cfg); -/* +/* @brief frees all data allocated by config */ void destroy_config(struct config *config); diff --git a/epitar/src/utils/errors.h b/epitar/src/utils/errors.h index eaf81ea..aeb20c3 100644 --- a/epitar/src/utils/errors.h +++ b/epitar/src/utils/errors.h @@ -6,11 +6,15 @@ enum error_code SUCCESS = 0, PENDING, UNKNOWN_ERROR, + GENERIC_ERROR, FILE_NOT_FOUND, BAD_CHECKSUM, INVALID_FORMAT, CANNOT_CREATE_TARGET, TARGET_ALREADY_EXISTS, + NO_TARGET_PROVIDED, + EMPTY_ARCHIVE, + CANNOT_STAT, NOT_IMPLEMENTED }; diff --git a/epitar/src/utils/filesystem.c b/epitar/src/utils/filesystem.c index aac2b70..9116034 100644 --- a/epitar/src/utils/filesystem.c +++ b/epitar/src/utils/filesystem.c @@ -1,8 +1,8 @@ #define _POSIX_C_SOURCE 200809L #include "filesystem.h" -#include "errors.h" +#include #include #include #include @@ -11,9 +11,15 @@ #include #include +#include "errors.h" + +static char *current_dir_path = NULL; +static DIR *current_dir = NULL; + enum error_code create_directory(struct file_stats *stats) { - if (stats == NULL || stats->name == NULL || stats->mode == NULL || stats->uid == NULL || stats->gid == NULL || stats->mtime == NULL) + if (stats == NULL || stats->name == NULL || stats->mode == NULL + || stats->uid == NULL || stats->gid == NULL || stats->mtime == NULL) return UNKNOWN_ERROR; // Create dir struct stat st = { 0 }; @@ -26,17 +32,13 @@ enum error_code create_directory(struct file_stats *stats) } else { - return TARGET_ALREADY_EXISTS; + // return TARGET_ALREADY_EXISTS; + return SUCCESS; } // Set stats - enum error_code errc; - errc = setowner(stats->name, stats->uid, stats->gid); - if (errc != SUCCESS) - return errc; - errc = setmtime(stats->name, stats->mtime); - if (errc != SUCCESS) - return errc; + setowner(stats->name, stats->uid, stats->gid); + setmtime(stats->name, stats->mtime); return SUCCESS; } @@ -51,17 +53,12 @@ enum error_code create_link(struct file_stats *stats, char *target) return CANNOT_CREATE_TARGET; // Set stats - enum error_code errc; - errc = setowner(stats->name, stats->uid, stats->gid); - if (errc != SUCCESS) - return errc; - errc = setmtime(stats->name, stats->mtime); - if (errc != SUCCESS) - return errc; + setowner(stats->name, stats->uid, stats->gid); + setmtime(stats->name, stats->mtime); // May change the target mode instead of the limk itsel - // also lchmod is not available on all systems so just ignoring this property here - // errc = setmode(stats->name, stats->mode); - // if (errc != SUCCESS) + // also lchmod is not available on all systems so just ignoring this + // property here errc = setmode(stats->name, stats->mode); if (errc != + // SUCCESS) // return errc; return SUCCESS; @@ -83,31 +80,15 @@ enum error_code create_file(struct file_stats *stats) if (file_stream == NULL) return CANNOT_CREATE_TARGET; // Copy content - char buffer[4096]; - size_t remaining = stats->length; - while (remaining > 0) { - size_t to_read = (remaining < sizeof(buffer)) ? remaining : sizeof(buffer); - size_t bytes_read = fread(buffer, 1, to_read, stats->content_stream); - if (bytes_read == 0) - break; // Reached EOF - fwrite(buffer, 1, bytes_read, file_stream); - remaining -= bytes_read; - } + fcat(stats->content_stream, file_stream, stats->length); // Close file fclose(file_stream); // Set stats - enum error_code errc; - errc = setowner(stats->name, stats->uid, stats->gid); - if (errc != SUCCESS) - return errc; - errc = setmtime(stats->name, stats->mtime); - if (errc != SUCCESS) - return errc; - errc = setmode(stats->name, stats->mode); - if (errc != SUCCESS) - return errc; + setowner(stats->name, stats->uid, stats->gid); + setmtime(stats->name, stats->mtime); + setmode(stats->name, stats->mode); return SUCCESS; } @@ -115,13 +96,13 @@ enum error_code create_file(struct file_stats *stats) enum error_code setowner(char *path, char *uid, char *gid) { if (path == NULL || uid == NULL || gid == NULL) - return UNKNOWN_ERROR; + return UNKNOWN_ERROR; - uid_t uid_val = strtol(uid, NULL, 10); - gid_t gid_val = strtol(gid, NULL, 10); + uid_t uid_val = strtol(uid, NULL, 10); + gid_t gid_val = strtol(gid, NULL, 10); - int err = chown(path, uid_val, gid_val); - return err == 0 ? SUCCESS : CANNOT_CREATE_TARGET; + int err = chown(path, uid_val, gid_val); + return err == 0 ? SUCCESS : CANNOT_CREATE_TARGET; } enum error_code setmtime(char *path, char *mtime) @@ -155,3 +136,76 @@ enum error_code setmode(char *path, char *mode) int err = chmod(path, mode_val); return err == 0 ? SUCCESS : CANNOT_CREATE_TARGET; } + +enum error_code isdirectory(char *path) +{ + struct stat st; + if (stat(path, &st) == -1) + return FILE_NOT_FOUND; + return S_ISREG(st.st_mode) ? GENERIC_ERROR : SUCCESS; +} + +char *listdirectory(char *path) +{ + struct dirent *de; + + // Check for path changes + if (current_dir == NULL || current_dir_path == NULL + || strcmp(current_dir_path, path) != 0) + { + // Close previous dir + freedirectory(); + // Open new dir + current_dir_path = strdup(path); + current_dir = opendir(current_dir_path); + if (current_dir == NULL) + { + freedirectory(); + return NULL; + } + } + + // Get following file + de = readdir(current_dir); + if (de == NULL) + { + freedirectory(); + return NULL; + } + + // Check for '.' or '..' + if (strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0) + return listdirectory(path); + + return de->d_name; +} + +void freedirectory(void) +{ + if (current_dir != NULL) + { + closedir(current_dir); + current_dir = NULL; + } + if (current_dir_path != NULL) + { + free(current_dir_path); + current_dir_path = NULL; + } +} + +void fcat(FILE *src_stream, FILE *dst_stream, size_t size) +{ + char buffer[4096]; + size_t remaining = size; + while (remaining > 0) + { + size_t to_read = + (remaining < sizeof(buffer)) ? remaining : sizeof(buffer); + size_t bytes_read = fread(buffer, 1, to_read, src_stream); + if (bytes_read == 0) + break; // Reached EOF + fwrite(buffer, bytes_read, 1, dst_stream); + remaining -= bytes_read; + } +} diff --git a/epitar/src/utils/filesystem.h b/epitar/src/utils/filesystem.h index 6b16787..160c800 100644 --- a/epitar/src/utils/filesystem.h +++ b/epitar/src/utils/filesystem.h @@ -21,8 +21,44 @@ enum error_code create_directory(struct file_stats *stats); enum error_code create_link(struct file_stats *stats, char *target); enum error_code create_file(struct file_stats *stats); +void fcat(FILE *src_stream, FILE *dst_stream, size_t size); + enum error_code setowner(char *path, char *uid, char *gid); enum error_code setmtime(char *path, char *mtime); enum error_code setmode(char *path, char *mode); +/** + * @brief Retrieves the file informations at path and build the corresponding + * file_stats struct + * @arg path The given path + * @arg stats Pointer to where the function should write file informations + * @return FILE_NOT_FOUND if doesn's exists, GENERIC_ERROR if . + */ +enum error_code getstats(char *path, struct file_stats *stats); + +/** + * @brief Check if given path exists and is a directory + * @option path The given path + * @return FILE_NOT_FOUND if doesn's exists, GENERIC_ERROR if it is not a + * directory, SUCCESS otherwise. + */ +enum error_code isdirectory(char *path); + +/** + * @brief Lists a directory content. Returns a new member name after each + * iteration and NULL when there are no more to list. + * @arg path The directory path + * @return The current item name or NULL + * @warn The return item statically allocated and freed after finishing to list + * the directory or when freedirectory() is called. + */ +char *listdirectory(char *path); + +/** + * @brief Closes the last opened (and not closed) directory and frees the + * related allocated memory + * @warn Use this function if you only partially listed a directory previously. + */ +void freedirectory(void); + #endif // FILESYSTEM_H diff --git a/epitar/src/utils/misc.c b/epitar/src/utils/misc.c deleted file mode 100644 index 0271150..0000000 --- a/epitar/src/utils/misc.c +++ /dev/null @@ -1,3 +0,0 @@ -#include "utils.h" - -#include diff --git a/epitar/src/utils/misc.h b/epitar/src/utils/misc.h deleted file mode 100644 index 3d20ecc..0000000 --- a/epitar/src/utils/misc.h +++ /dev/null @@ -1,6 +0,0 @@ -#ifndef MISC_H -#define MISC_H - -void print_str_array(const char **input_files); - -#endif /* MISC_H */ diff --git a/epitar/tests/functional/old_testsuite.sh b/epitar/tests/functional/old_testsuite.sh new file mode 100644 index 0000000..5e16a27 --- /dev/null +++ b/epitar/tests/functional/old_testsuite.sh @@ -0,0 +1,91 @@ +#!/bin/sh + +# Old testsuite but still pertinent + +# Colors +GREEN='\033[0;32m' +RED='\033[0;31m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +PASSED=0 +FAILED=0 +TOTAL=0 + +# Ensure executable exists +if [ ! -x "./epitar" ]; then + echo -e "${RED}Error: ./epitar not found or not executable.${NC}" + exit 1 +fi + +# assert "test name" "command" "expected_exit_code" "expected_output_substring" +assert() { + ((TOTAL++)) + local label=$1 + local cmd=$2 + local expected_code=$3 + local expected_msg=$4 + + echo -n "Test $TOTAL: $label... " + + # Run + output=$(eval "$cmd" 2>&1) + exit_code=$? + + if [ "$exit_code" -eq "$expected_code" ] && [[ "$output" == *"$expected_msg"* ]]; then + echo -e "${GREEN}PASS${NC}" + ((PASSED++)) + else + echo -e "${RED}FAIL${NC}" + echo -e " Expected code: $expected_code, Got: $exit_code" + echo -e " Expected msg contains: '$expected_msg'" + echo -e " Actual output: '$output'" + ((FAILED++)) + fi +} + +# === Clean Environment === +rm -f *.tar tmp_file1 tmp_file2 +touch tmp_file1 tmp_file2 +echo "corrupt data" > bad_checksum.tar + +# === Testsuite === + +echo -e "${BLUE}Starting epitar test suite...${NC}\n" + +# Help and Basic Usage +assert "Help flag -h" "./epitar -h" 0 "Usage: epitar -[xcvh] []" +assert "No arguments" "./epitar" 1 "invalid or missing option" +assert "Invalid flag -z" "./epitar -z" 1 "invalid or missing option" +assert "Invalid flag -a" "./epitar -a toto.tar titi" 1 "invalid or missing option" + +# Creation Errors (-c) +assert "Refuse empty archive" "./epitar -c empty.tar" 1 "cowardly refusing" +assert "Missing archive name for -c" "./epitar -c" 1 "invalid or missing option" +assert "Archive adding itself" "./epitar -c self.tar self.tar" 0 "is the archive; not dumped" +assert "Unable to add missing file" "./epitar -cv missing.tar non_existent_file" 3 "unable to add file" + +# Extraction Errors (-x) +assert "Bad checksum" "./epitar -x bad_checksum.tar" 2 "bad checksum" +assert "Extraction of non-existent file" "./epitar -x nowhere.tar" 3 "error extracting tarball" +assert "Missing archive name for -x" "./epitar -x" 1 "invalid or missing option" + +# Combinations & Complex flags +assert "Multiple invalid flags" "./epitar -xz archive.tar" 1 "invalid or missing option" +assert "Help priority" "./epitar -h -x nonexistent.tar" 0 "Usage:" +assert "Multiple files for creation (failure case)" "./epitar -c fail.tar tmp_file1 missing_file" 3 "unable to add file" + +# Path and Environment edge cases +assert "Relative path to non-existent" "./epitar -x ./subdir/fake.tar" 3 "error extracting tarball" +assert "Creation with dot (.)" "./epitar -c current.tar ." 0 "current.tar is the archive; not dumped" +assert "List content (hypothetical -v without -c or -x)" "./epitar -v" 1 "invalid or missing option" +assert "Wrong order of arguments" "./epitar archive.tar -c" 1 "invalid or missing option" + +# === Summary === +echo -e "\n------------------------------------" +echo -e "Results: ${GREEN}$PASSED Passed${NC}, ${RED}$FAILED Failed${NC} / $TOTAL Total" +echo -e "------------------------------------" + +# Cleanup +rm -f *.tar tmp_file1 tmp_file2 + diff --git a/epitar/tests/functional/run.sh b/epitar/tests/functional/run.sh new file mode 100644 index 0000000..70b07f4 --- /dev/null +++ b/epitar/tests/functional/run.sh @@ -0,0 +1,345 @@ +#!/bin/sh + +# Testsuitator (mon bien-aimé) le retour <3 + +################### +# Variables # +################### + +executable="$BIN_PATH" +ref_executable="tar" + +total_tests=0 +errors_count=0 +timeouts_count=0 + +# Create temp directory for tests +test_dir=$(mktemp -d) +trap "rm -rf $test_dir" EXIT + + +################## +# Colors # +################## + +# Reset +Color_Off='\033[0m' # Text Reset + +# Regular Colors +Black='\033[0;30m' # Black +Red='\033[0;31m' # Red +Green='\033[0;32m' # Green +Yellow='\033[0;33m' # Yellow +Blue='\033[0;34m' # Blue +Purple='\033[0;35m' # Purple +Cyan='\033[0;36m' # Cyan +White='\033[0;37m' # White + +# Bold +BBlack='\033[1;30m' # Black +BRed='\033[1;31m' # Red +BGreen='\033[1;32m' # Green +BYellow='\033[1;33m' # Yellow +BBlue='\033[1;34m' # Blue +BPurple='\033[1;35m' # Purple +BCyan='\033[1;36m' # Cyan +BWhite='\033[1;37m' # White + + +################## +# Wrappers # +################## + +# @arg test name +# @arg command +check_result() { + local test_name="$1" + local actual_code="$2" + local ref_code="$3" + + test_failed=0 + + # Check return code + if [[ "$actual_code" -eq 124 ]]; then + echo -e $BRed "TIMEOUT" $Color_Off + echo -e ' ' "on '$test_name'" + echo -e ' ' "Expected code $ref_code but got $actual_code" + ((timeouts_count++)) + test_failed=1 + elif [[ "$actual_code" -ne "$ref_code" ]]; then + echo -e $BRed "FAILED" $Color_Off + echo -e ' ' "on '$test_name'" + echo -e ' ' "Expected code $ref_code but got $actual_code" + test_failed=1 + else + echo -e $BGreen "OK" $Color_Off + fi + + if [[ "$test_failed" -eq 1 ]]; then + ((errors_count++)) + fi +} + +# @arg test name +# @arg test command +test_archive_creation() { + local test_name="$1" + local archive_name="$2" + local input_files="$3" + + echo -e $BBlue "========== $test_name ==========" $Color_Off + + # Create test files + local test_subdir="$test_dir/$test_name" + mkdir -p "$test_subdir" + cd "$test_subdir" + + # Prepare input files + local file_list="" + for file in $input_files; do + echo "content of $file" > "$file" + file_list="$file_list $file" + done + + # Test with epitar + echo -e -n $Blue "= [ARCHIVE]" $Color_Off " " + timeout 2 $executable -c "$archive_name" $file_list > /dev/null 2>&1 + actual_code=$? + + if [[ $actual_code -eq 0 && -f "$archive_name" ]]; then + echo -e $BGreen "OK" $Color_Off + else + echo -e $BRed "FAILED" $Color_Off + ((errors_count++)) + fi + ((total_tests++)) + + # Test extraction + if [[ -f "$archive_name" ]]; then + echo -e -n $Blue "= [EXTRACT] " $Color_Off " " + extract_dir="extract_test" + mkdir "$extract_dir" + cd "$extract_dir" + timeout 2 $executable -x "../$archive_name" > /dev/null 2>&1 + actual_code=$? + + if [[ $actual_code -eq 0 ]]; then + # Check if files were extracted + all_extracted=1 + for file in $file_list; do + if [[ ! -f "$file" ]]; then + all_extracted=0 + fi + done + if [[ $all_extracted -eq 1 ]]; then + echo -e $BGreen "OK" $Color_Off + else + echo -e $BRed "FAILED (files not extracted)" $Color_Off + ((errors_count++)) + fi + else + echo -e $BRed "FAILED (exit code $actual_code)" $Color_Off + ((errors_count++)) + fi + ((total_tests++)) + cd .. + fi + + cd / + echo -e "\n" +} + +# @arg test name +# @arg files to test +test_verbose_output() { + local test_name="$1" + local files="$2" + + echo -e $BBlue "========== $test_name ==========" $Color_Off + + local test_subdir="$test_dir/${test_name}_verbose" + mkdir -p "$test_subdir" + cd "$test_subdir" + + # Create test files + for file in $files; do + echo "test content" > "$file" + done + + echo -e -n $Blue "= [VERBOSE]" $Color_Off " " + archive="test_verbose.tar" + timeout 2 $executable -cv "$archive" $files > output.txt 2>&1 + actual_code=$? + + # Check if output contains filenames + local all_found=1 + for file in $files; do + if ! grep -q "$file" output.txt; then + all_found=0 + fi + done + + if [[ $actual_code -eq 0 && $all_found -eq 1 ]]; then + echo -e $BGreen "OK" $Color_Off + else + echo -e $BRed "FAILED" $Color_Off + ((errors_count++)) + fi + ((total_tests++)) + + cd / + echo -e "\n" +} + +# @arg test name +test_help() { + local test_name="$1" + + echo -e $BBlue "========== $test_name ==========" $Color_Off + + echo -e -n $Blue "= [HELP]" $Color_Off " " + timeout 1 $executable -h > /dev/null 2>&1 + actual_code=$? + + if [[ $actual_code -eq 0 ]]; then + echo -e $BGreen "OK" $Color_Off + else + echo -e $BRed "FAILED" $Color_Off + ((errors_count++)) + fi + ((total_tests++)) + + echo -e "\n" +} + +# @arg test name +test_invalid_args() { + local test_name="$1" + local args="$2" + + echo -e $BBlue "========== $test_name ==========" $Color_Off + + echo -e -n $Blue "= [INVALID]" $Color_Off " " + timeout 1 $executable $args > /dev/null 2>&1 + actual_code=$? + + # Should fail + if [[ $actual_code -ne 0 ]]; then + echo -e $BGreen "OK (correctly rejected)" $Color_Off + else + echo -e $BRed "FAILED (should have been rejected)" $Color_Off + ((errors_count++)) + fi + ((total_tests++)) + + echo -e "\n" +} + +summarize() { + # Compute statistics + (( passed_tests = $total_tests - $errors_count )) + if [[ $total_tests -gt 0 ]]; then + (( tests_percentage = 100 * $passed_tests / $total_tests )) + else + tests_percentage=0 + fi + + # Adapt color depending on results + if [[ tests_percentage -gt 80 ]]; then + coverage_color=$BGreen + elif [[ tests_percentage -gt 50 ]]; then + coverage_color=$BYellow + else + coverage_color=$BRed + fi + + # Print + echo -e $BWhite "\n\n""===========" $BWhite"Summary"$Color_Off "\n" + echo -e " Passed $coverage_color$passed_tests/$total_tests$Color_Off tests ($coverage_color$tests_percentage%$Color_Off)" + echo -e " Got $timeouts_count timeout(s)" + if [ "$OUTPUT_FILE" != "" ]; then + echo $tests_percentage > "$OUTPUT_FILE" + fi + echo +} + + +################# +# Tests # +################# + +echo -e "\n\n""===$BGreen TestsuitatorX 2.0 Ultra Pro Max+ 365 Premium Gris Sidéral" "\n\n"$Color_Off + +echo -e "\n$BBlue=== Basic Operations ===$Color_Off" +test_archive_creation "Single file" "single.tar" "file1.txt" +test_archive_creation "Multiple files" "multi.tar" "file1.txt file2.txt file3.txt" +test_archive_creation "Files with spaces" "spaces.tar" "file_with_underscore.txt" + +echo -e "\n$BBlue=== Verbose Output ===$Color_Off" +test_verbose_output "Verbose creation" "file1.txt file2.txt" + +echo -e "\n$BBlue=== Help and Invalid Arguments ===$Color_Off" +test_help "Help flag" +test_invalid_args "No arguments" "" +test_invalid_args "Invalid flag" "-z" +test_invalid_args "Invalid flagS" "-zbk" +test_invalid_args "Invalid position" "test.tar -h" +test_invalid_args "Invalid flags and position" "test.tar -z" +test_invalid_args "Invalid flags and position mix" "test.tar -zhx" +test_invalid_args "Invalid position" "test.tar -z" +test_invalid_args "Invalid flag mix" "-zh" +test_invalid_args "Invalid flag mix 2" "-hnh" +test_invalid_args "Invalid flag mix 3" "-cnc" +test_invalid_args "Invalid flag mix 3" "-hhhz" +test_invalid_args "Invalid flag mix 3" "-hnc" +test_invalid_args "Invalid flag mix 3" "-cch" +test_invalid_args "Invalid flag mix 3" "-cnc" +test_invalid_args "Valid flags multiple" "-cc" +test_invalid_args "Incompatible flag" "-cx" +test_invalid_args "Missing op flag" "-v" +test_invalid_args "Missing archive (c)" "-c" +test_invalid_args "Missing archive (x)" "-x" +test_invalid_args "Extract with files" "-x archive.tar file.txt" + +echo -e "\n$BBlue=== Edge Cases ===$Color_Off" + +# Test with empty archive +test_subdir="$test_dir/empty_archive" +mkdir -p "$test_subdir" +cd "$test_subdir" +echo -e $BBlue "========== Empty archive ==========" $Color_Off +echo -e -n $Blue "= [CREATE_EMPTY]" $Color_Off " " +touch dummy +timeout 2 $executable -c "empty.tar" dummy > /dev/null 2>&1 +actual_code=$? +rm dummy +if [[ $actual_code -eq 0 ]]; then + echo -e $BGreen "OK" $Color_Off +else + echo -e $BRed "FAILED" $Color_Off + ((errors_count++)) +fi +((total_tests++)) +cd / +echo -e "\n" + +# Test combined flags +test_subdir="$test_dir/combined_flags" +mkdir -p "$test_subdir" +cd "$test_subdir" +echo -e $BBlue "========== Combined flags (-cv) ==========" $Color_Off +echo -e -n $Blue "= [COMBINED]" $Color_Off " " +echo "content" > test.txt +timeout 2 $executable -cv "test.tar" test.txt > /dev/null 2>&1 +actual_code=$? +if [[ $actual_code -eq 0 && -f "test.tar" ]]; then + echo -e $BGreen "OK" $Color_Off +else + echo -e $BRed "FAILED" $Color_Off + ((errors_count++)) +fi +((total_tests++)) +cd / +echo -e "\n" + +summarize diff --git a/epitar/tests/functional/warning.sh b/epitar/tests/functional/warning.sh new file mode 100644 index 0000000..6b66c3b --- /dev/null +++ b/epitar/tests/functional/warning.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +Color_Off='\033[0m' # Text Reset +BYellow='\033[1;33m' # Yellow + +echo +echo -e $BYellow "WARNING:" $Color_Off " Two testsuites have been run, don't just check the last summary" +echo -e $BYellow "WARNING:" $Color_Off " Archive/Extract tests will be partly hidden if their first part doesn't succeed" +echo " (So if you want the accurate number of tests just add the number of failed tests to the total)"