diff --git a/CMakeLists.txt b/CMakeLists.txt index be80674..76d3890 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,7 +3,28 @@ project(NitroCopy VERSION 0.0.1 LANGUAGES C ) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +# ----------------------------------------------- +# Tests +# ----------------------------------------------- +include(CTest) +enable_testing() + +# ----------------------------------------------- +# Determine Git-based version +# ----------------------------------------------- +execute_process( + COMMAND git describe --tags --always + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + OUTPUT_VARIABLE GIT_VERSION + OUTPUT_STRIP_TRAILING_WHITESPACE +) + +# Fallback to project version if Git info is unavailable +if(NOT GIT_VERSION) + set(GIT_VERSION ${PROJECT_VERSION}) +endif() # ----------------------------------------------- # Compiler settings @@ -26,17 +47,56 @@ add_compile_options( ) # Debug vs release mode -set(CMAKE_C_FLAGS_DEBUG "-O0 -g -DNITRO_DEBUG") # Disable optimisations (o0) and include debug symbols (-g). -set(CMAKE_C_FLAGS_RELEASE "-O2") # Optimise for speed. +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -D_USE_32BIT_TIME_T") +set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS} -O0 -g -DNITRO_DEBUG") # Disable optimisations (o0) and include debug symbols (-g). +set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS} -O2") # Optimise for speed. # ----------------------------------------------- # Include directories # ----------------------------------------------- include_directories(${CMAKE_SOURCE_DIR}/include) +# ----------------------------------------------- +# Determine Git version +# ----------------------------------------------- +execute_process( + COMMAND git describe --tags --always + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + OUTPUT_VARIABLE GIT_VERSION + OUTPUT_STRIP_TRAILING_WHITESPACE +) + +if(NOT GIT_VERSION) + set(GIT_VERSION ${PROJECT_VERSION}) +endif() + # ----------------------------------------------- # Generate version header # ----------------------------------------------- +# Path to build number file +set(BUILD_NUMBER_FILE "${CMAKE_BINARY_DIR}/build_number.txt") + +# Initialize file if it doesn't exist +if(NOT EXISTS "${BUILD_NUMBER_FILE}") + file(WRITE "${BUILD_NUMBER_FILE}" "0") +endif() + +# Read current build number +file(READ "${BUILD_NUMBER_FILE}" BUILD_NUMBER) +string(STRIP "${BUILD_NUMBER}" BUILD_NUMBER) + +# Increment build number +math(EXPR BUILD_NUMBER "${BUILD_NUMBER} + 1") + +# Write back new build number +file(WRITE "${BUILD_NUMBER_FILE}" "${BUILD_NUMBER}") + +# Export as a cache variable for configure_file +set(NITRO_BUILD_NUMBER "${BUILD_NUMBER}" CACHE INTERNAL "Current build number") +message(STATUS "Build number: ${BUILD_NUMBER}") + +set(VERSION_STRING "${GIT_VERSION} (build ${NITRO_BUILD_NUMBER})") + configure_file( ${CMAKE_SOURCE_DIR}/include/nitro_version.h.in ${CMAKE_BINARY_DIR}/include/nitro_version.h @@ -49,16 +109,14 @@ include_directories(${CMAKE_BINARY_DIR}/include) # ----------------------------------------------- # Source files # ----------------------------------------------- -file(GLOB_RECURSE NITRO_SOURCES "src/*.c") +add_subdirectory(src) # ----------------------------------------------- -# Build application +# Custom target for tagging releases # ----------------------------------------------- -add_executable(NitroCopy ${NITRO_SOURCES}) - -# Link libraries. -target_link_libraries(NitroCopy PRIVATE m) - -if (WIN32) - target_link_options(NitroCopy PRIVATE -static -static-libgcc -static-libstdc++) -endif() +add_custom_target(tag_release + COMMAND git tag -a v${PROJECT_VERSION} -m "Release v${PROJECT_VERSION}" + COMMAND git push --tags + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + COMMENT "Tagging release v${PROJECT_VERSION}" +) diff --git a/compile_commands.json b/compile_commands.json new file mode 120000 index 0000000..25eb4b2 --- /dev/null +++ b/compile_commands.json @@ -0,0 +1 @@ +build/compile_commands.json \ No newline at end of file diff --git a/include/console.h b/include/console.h new file mode 100644 index 0000000..2ea38ac --- /dev/null +++ b/include/console.h @@ -0,0 +1,10 @@ +#ifndef CONSOLE_H +#define CONSOLE_H + +int console_get_width(void); + +void console_move_cursor_up(int rows); + +void console_clear_from_cursor_down(void); + +#endif diff --git a/include/copier.h b/include/copier.h new file mode 100644 index 0000000..490afe2 --- /dev/null +++ b/include/copier.h @@ -0,0 +1,28 @@ +#ifndef COPIER_H +#define COPIER_H + +#include + +typedef enum { + COPY_RESUME, + COPY_OVERWRITE, + COPY_SKIP +} copy_existing_action_t; + +extern copy_existing_action_t g_default_copy_action; + +#define COPY_ACTION_UNSET ((copy_existing_action_t)-1) + +typedef void (*ProgressCallback)(void); + +int copier_copy(const char* src, const char* dest, size_t total_files, size_t total_bytes, size_t* files_copied, size_t* bytes_copied); + +size_t copier_copy_file(const char* src, const char* dest); + +int copier_copy_dir(const char* src_dir, const char* dest_dir, size_t* total_files, size_t* total_bytes); + +int copier_execute(const char* src, const char* dest); + +int copier_get_total_stats(const char* path, size_t* total_files, size_t* total_bytes); + +#endif diff --git a/include/compat.h b/include/file.h similarity index 77% rename from include/compat.h rename to include/file.h index 232d732..8b9aa7e 100644 --- a/include/compat.h +++ b/include/file.h @@ -1,10 +1,8 @@ -#ifndef COMPAT_H -#define COMPAT_H +#ifndef FILE_COMPAT_H +#define FILE_COMPAT_H -#include #include - #if defined(_WIN32) #include @@ -23,9 +21,13 @@ typedef struct { #endif DIR* compat_opendir(const char* path); + struct dirent* compat_readdir(DIR* dir); + int compat_closedir(DIR* dir); + int compat_mkdir(const char* path, int mode); -int compat_get_file_stats(const char* path, struct stat* file_stats); + +int compat_stat(const char* path, struct stat* file_stats); #endif diff --git a/include/libnitro.h b/include/libnitro.h deleted file mode 100644 index b4849f6..0000000 --- a/include/libnitro.h +++ /dev/null @@ -1,34 +0,0 @@ -#ifndef LIBNITRO_H -#define LIBNITRO_H - -/* Opaque handling for holding state. */ -typedef struct NitroCopyState NitroCopyState; - -/* Define status codes. */ -typedef enum { - NITRO_SUCCESS = 0, - NITRO_ERROR_OPENING_FILE, - NITRO_ERROR_DEST_IS_DIR, - NITRO_ERROR_CREATING_DIR, - NITRO_ERROR_IO, - NITRO_ERROR_INVALID_PATH, - NITRO_ERROR_PERMISSION_DENIED, - NITRO_ERROR_GENERAL -} NitroStatus; - - -/* State management functions. */ -NitroCopyState* nitro_init(unsigned int overwrite); -void nitro_destroy(NitroCopyState* state); - -/* Core functionality for copying files and directories. */ -NitroStatus nitro_copy(NitroCopyState* state, const char* src, const char* dest); -NitroStatus nitro_copy_file(NitroCopyState* state, const char* src, const char* dest); -NitroStatus nitro_copy_directory(NitroCopyState* state, const char* src, const char* dest); - -/* Utility functions. */ -NitroStatus nitro_get_total_stats(const char* path, unsigned long* total_size, unsigned long* file_count); -void nitro_update_progress(NitroCopyState* state); -char* nitro_format_bytes(unsigned long bytes); - -#endif diff --git a/include/logging.h b/include/logging.h new file mode 100644 index 0000000..57ca560 --- /dev/null +++ b/include/logging.h @@ -0,0 +1,20 @@ +#ifndef LOGGING_H +#define LOGGING_H + +#define LOG_LEVEL_FATAL 1 +#define LOG_LEVEL_ERROR 2 +#define LOG_LEVEL_WARN 3 +#define LOG_LEVEL_INFO 4 +#define LOG_LEVEL_DEBUG 5 + +void log_fatal(const char* format, ...); + +void log_error(const char* format, ...); + +void log_warn(const char* format, ...); + +void log_info(const char* format, ...); + +void log_debug(const char* format, ...); + +#endif diff --git a/include/nitro_console_compat.h b/include/nitro_console_compat.h deleted file mode 100644 index c7388d0..0000000 --- a/include/nitro_console_compat.h +++ /dev/null @@ -1,6 +0,0 @@ -#ifndef NITRO_CONSOLE_H -#define NITRO_CONSOLE_H - -void nitro_console_clear_line(void); - -#endif diff --git a/include/nitro_debug.h b/include/nitro_debug.h deleted file mode 100644 index 2f49f28..0000000 --- a/include/nitro_debug.h +++ /dev/null @@ -1,33 +0,0 @@ -#ifndef NITRO_DEBUG_H -#define NITRO_DEBUG_H - -#include -#include -#include "nitro_runtime.h" - -#ifdef NITRO_DEBUG - -#ifndef __func__ - #define __func__ "(unknown)" -#endif - -static void nitro_debug(const char* file, int line, const char* func, const char* fmt, ...) { - va_list args; - va_start(args, fmt); - - if(nitro_verbose) { - fprintf(stderr, "[DEBUG] %s:%d:%s(): ", file, line, func); - vfprintf(stderr, fmt, args); - fprintf(stderr, "\n"); - } - - va_end(args); -} - -#define NITRO_DEBUG_LOG(fmt, args) \ - nitro_debug(__FILE__, __LINE__, __func__, fmt, args) -#else -#define NITRO_DEBUG_LOG(fmt, args) ((void)0) -#endif - -#endif diff --git a/include/nitro_runtime.h b/include/nitro_runtime.h deleted file mode 100644 index 04b95f8..0000000 --- a/include/nitro_runtime.h +++ /dev/null @@ -1,11 +0,0 @@ -#ifndef NITRO_RUNTIME_H -#define NITRO_RUNTIME_H - -/* ---------------------------------------- - * Global runtime settings for NitroCopy. - * ---------------------------------------- */ - -/* Verbose logging flag (0 = off, 1 = on) */ -extern int nitro_verbose; - -#endif diff --git a/include/nitro_version.h.in b/include/nitro_version.h.in index 9f25b1f..c904a46 100644 --- a/include/nitro_version.h.in +++ b/include/nitro_version.h.in @@ -3,5 +3,6 @@ #define NITRO_VERSION_MAJOR @PROJECT_VERSION_MAJOR@ #define NITRO_VERSION_MINOR @PROJECT_VERSION_MINOR@ #define NITRO_VERSION_PATCH @PROJECT_VERSION_PATCH@ +#define NITRO_BUILD_NUMBER @NITRO_BUILD_NUMBER@ -#define NITRO_VERSION_STRING "@PROJECT_VERSION@" +#define NITRO_VERSION_STRING "@VERSION_STRING@" diff --git a/include/runtime.h b/include/runtime.h new file mode 100644 index 0000000..a267a92 --- /dev/null +++ b/include/runtime.h @@ -0,0 +1,6 @@ +#ifndef RUNTIME_H +#define RUNTIME_H + +extern int overwrite; + +#endif diff --git a/include/word16.h b/include/word16.h new file mode 100644 index 0000000..c962bf8 --- /dev/null +++ b/include/word16.h @@ -0,0 +1,18 @@ +#ifndef WORD16_H +#define WORD16_H + +#include + +size_t word16_count(size_t length); + +/** + * word16_create(): Create 16 bit words in little endian order, low byte first and high byte second. + * + * @input: The string to create words from. + * @out_words: The pointer to where the words should be stored. + * + * @return THe number of 16-bit words in the input string. + */ +size_t word16_create(const unsigned char* input, size_t length, unsigned short* output); + +#endif diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..5f56a42 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,26 @@ +add_subdirectory(fletcher32) +add_subdirectory(getopt) +add_subdirectory(platform) + +add_library(logging STATIC logging.c) + +add_library(word16 STATIC + word16.c +) + +add_library(copier STATIC + copier.c +) +target_link_libraries(copier PUBLIC + fletcher32 + console + file + logging + m +) + +add_executable(NitroCopy main.c) +target_link_libraries(NitroCopy PUBLIC + getopt + copier +) diff --git a/src/copier.c b/src/copier.c new file mode 100644 index 0000000..128f652 --- /dev/null +++ b/src/copier.c @@ -0,0 +1,467 @@ +#include "copier.h" +#include "file.h" +#include "fletcher32.h" +#include "logging.h" +#include "word16.h" + +#include +#include +#include +#include +#include +#include +#include + +#define CHUNK_SIZE 65535 + +/* + * Prompt the user to choose whether to resume, overwrite or skip the file when it exists. + */ +static copy_existing_action_t prompt_user_action(const char* dst) { + char input[16]; + + if(g_default_copy_action != COPY_ACTION_UNSET) { + return g_default_copy_action; + } + + while(1) { + printf("Destination file \"%s\" exists. Choose action: [R]esume, [O]verwrite, [S]kip: ", dst); + + if(!fgets(input, sizeof(input), stdin)) { + continue; + } + + if(input[0] == 'R' || input[0] == 'r') { + return COPY_RESUME; + } + if(input[0] == 'O' || input[0] == 'o') { + return COPY_OVERWRITE; + } + if(input[0] == 'S' || input[0] == 's') { + return COPY_SKIP; + } + } +} + +static void format_bytes(size_t bytes, char* buffer) { + const char* suffixes[] = {"B", "KB", "MB", "GB"}; + int i = 0; + double value; + + if(bytes == 0) { + sprintf(buffer, "0 B"); + return; + } + + i = (int)floor(log(bytes) / log(1024)); + i = (i < 0) ? 0 : i; + + if((unsigned long) i >= sizeof(suffixes) / sizeof(suffixes[0])) { + i = sizeof(suffixes) / sizeof(suffixes[0]) - 1; + } + + value = (double)bytes / pow(1024, i); + sprintf(buffer, "%.2f %s", value, suffixes[i]); +} + +int copier_execute(const char* src, const char* dest) { + size_t total_files = 0; + size_t total_bytes = 0; + size_t files_copied = 0; + size_t bytes_copied = 0; + char formatted_total_bytes[256]; + char formatted_bytes_copied[256]; + + printf("Counting total number of files to copy...\n"); + + if(copier_get_total_stats(src, &total_files, &total_bytes) != 0) { + fprintf(stderr, "copier_copy(): Failed to calculate total number of files to copy.\n"); + return 1; + } + + if(total_bytes == 0) { + fprintf(stderr, "There's nothing to copy.\n"); + return 1; + } + + format_bytes(total_bytes, formatted_total_bytes); + printf("Counted %lu files (%s)\n", (unsigned long) total_files, formatted_total_bytes); + + copier_copy(src, dest, total_files, total_bytes, &files_copied, &bytes_copied); + + format_bytes(bytes_copied, formatted_bytes_copied); + format_bytes(total_bytes, formatted_total_bytes); + printf("Finished copying files: %lu/%lu (%s/%s)\n", (unsigned long) files_copied, (unsigned long) total_files, formatted_bytes_copied, formatted_total_bytes); + + return 0; +} + +int copier_copy(const char* src, const char* dest, size_t total_files, size_t total_bytes, size_t* total_files_copied, size_t* total_bytes_copied) { + size_t total_progress = 0; + size_t bytes_copied = 0; + char formatted_bytes_copied[256]; + char formatted_total_bytes[256]; + + struct stat stats; + struct dirent* src_file; + DIR* src_dir; + + if(compat_stat(src, &stats) != 0) { + fprintf(stderr, "Failed to acquire stats for %s: %d (%s)\n", src, errno, strerror(errno)); + return 1; + } + + if(S_ISDIR(stats.st_mode)) { + src_dir = compat_opendir(src); + if(!src_dir) { + fprintf(stderr, "Failed to open src dir %s: %d (%s)\n", src, errno, strerror(errno)); + return 1; + } + + if(compat_stat(dest, &stats) != 0 && compat_mkdir(dest, 0755) == -1) { + compat_closedir(src_dir); + fprintf(stderr, "Failed to create directory %s: %d (%s)\n", dest, errno, strerror(errno)); + return 1; + } + + while((src_file = compat_readdir(src_dir)) != NULL) { + char foo[1024]; + char bar[1024]; + + if(strcmp(src_file->d_name, ".") == 0 || strcmp(src_file->d_name, "..") == 0) { + continue; + } + + sprintf(foo, "%s/%s", src, src_file->d_name); + sprintf(bar, "%s/%s", dest, src_file->d_name); + + if(copier_copy(foo, bar, total_files, total_bytes, total_files_copied, total_bytes_copied) != 0) { + return 1; + } + } + + free(src_dir); + } else { + *total_files_copied += 1; + + log_info("Copying file %lu/%lu: %s -> %s\n", *total_files_copied, total_files, src, dest); + + if((bytes_copied = copier_copy_file(src, dest)) >= 0) { + *total_bytes_copied += bytes_copied; + + total_progress = *total_bytes_copied == 0 ? 0 : (int)((double) *total_bytes_copied / (double) total_bytes * 100.0); + + format_bytes(*total_bytes_copied, formatted_bytes_copied); + format_bytes(total_bytes, formatted_total_bytes); + + log_info("-> Total progress: %3d%%, %s/%s\n\n", total_progress, formatted_bytes_copied, formatted_total_bytes); + } else { + return 1; + } + } + + return 0; +} + +/** + * log_progress(): Logs progress once every 10 seconds or when we've copied everything. + */ +static void log_progress(size_t copied, size_t total, time_t start, time_t* last_log_time) { + time_t now = time(NULL); + double elapsed; + double speed; + int percent; + char formatted_copied[64]; + char formatted_total[64]; + + if(difftime(now, *last_log_time) >= 10.0 || copied == total) { + elapsed = difftime(now, start); + speed = elapsed > 0 ? ((double) total / elapsed) : 0.0; + + percent = (int)(((double) copied / (double) total) * 100.0); + format_bytes(copied, formatted_copied); + format_bytes(total, formatted_total); + + log_info("-> File progress: %3d%%, %s/%s, Speed: %.2f MB/s\n", percent, formatted_copied, formatted_total, speed); + + *last_log_time = now; + } +} + +static FILE* open_src_file(const char* src) { + FILE* src_file = fopen(src, "rb"); + if(!src_file) { + fprintf(stderr, "Error opening source file %s: %d (%s)\n", src, errno, strerror(errno)); + } + return src_file; +} + +static FILE* open_dst_file(const char* dst) { + FILE* dst_file; + struct stat dst_stats; + + if(stat(dst, &dst_stats) == 0) { + log_info("Destination file \"%s\" already exists.\n", dst); + } + + dst_file = fopen(dst, "r+b"); + if(!dst_file) { + dst_file = fopen(dst, "w+b"); + if(!dst_file) { + fprintf(stderr, "Error opening destination file: %s: %d (%s)\n", dst, errno, strerror(errno)); + } + } + return dst_file; +} + +/* + * process_chunk(): Read from the source file and write to the destination file. Uses Fletcher-32 to understand whether there's a difference between the source and destination chunks. + * @returns 0 if EOF is reached, -1 if an error occurs, 1 if the chunk is written. + */ +static int process_chunk(FILE* src_file, FILE* dst_file, size_t chunk_index, size_t* bytes_processed) { + unsigned char buffer[CHUNK_SIZE]; + unsigned short words[CHUNK_SIZE / 2 + 1]; + size_t src_bytes; + size_t dst_bytes; + size_t word_count; + unsigned long checksum_src; + unsigned long checksum_dst; + + /* Read from source and calculate checksum */ + src_bytes = fread(buffer, 1, CHUNK_SIZE, src_file); + if(src_bytes == 0) { + *bytes_processed = 0; + return feof(src_file) ? 0 : -1; + } + word_count = word16_create(buffer, src_bytes, words); + checksum_src = fletcher32(words, word_count); + + /* Read from destination and calculate checksum */ + fseek(dst_file, (long)(chunk_index * CHUNK_SIZE), SEEK_SET); + dst_bytes = fread(buffer, 1, src_bytes, dst_file); + word_count = word16_create(buffer, dst_bytes, words); + checksum_dst = fletcher32(words, word_count); + + /* Rewrite the chunk if different */ + if(checksum_src != checksum_dst) { + log_info("Checksum mismatch at chunk index %lu. Rewriting chunk"); + + fseek(src_file, (long)(chunk_index * CHUNK_SIZE), SEEK_SET); + src_bytes = fread(buffer, 1, CHUNK_SIZE, src_file); + + fseek(dst_file, (long)(chunk_index * CHUNK_SIZE), SEEK_SET); + if(fwrite(buffer, 1, src_bytes, dst_file) != src_bytes) { + fprintf(stderr, "Failed to write chunk: %lu: %d (%s)\n", (unsigned long) chunk_index, errno, strerror(errno)); + return -1; + } + fflush(dst_file); + } + + *bytes_processed = src_bytes; + return 1; +} + +/* + * process_chunks(): Main copy loop. + * @returns The total bytes copied. +*/ +static size_t process_chunks(FILE* src_file, FILE* dst_file, const struct stat* src_stats) { + size_t total_bytes_copied = 0; + size_t chunk_index = 0; + size_t bytes_processed; + int status; + + time_t start_time = time(NULL); + time_t last_log_time = start_time; + + while((status = process_chunk(src_file, dst_file, chunk_index, &bytes_processed)) > 0) { + total_bytes_copied += bytes_processed; + log_progress(total_bytes_copied, src_stats->st_size, start_time, &last_log_time); + chunk_index++; + } + + return total_bytes_copied; +} + +static int get_file_stats(const char* path, struct stat* stats) { + if(compat_stat(path, stats) != 0) { + fprintf(stderr, "Failed to stat file %s: %d (%s)\n", path, errno, strerror(errno)); + return -1; + } + return 0; +} + +/* + * determine_resume_chunk(): Determine which chunk to resume the copy operation from. + * @returns The chunk to resume from. + */ +static size_t determine_resume_chunk(FILE* src_file, FILE* dst_file, const char* dst) { + struct stat dst_stats; + size_t resume_chunk = 0; + size_t check_chunk = 0; + size_t dst_size = 0; + int status = 0; + size_t bytes_processed = 0; + + if(stat(dst, &dst_stats) != 0) { + return resume_chunk; + } + + dst_size = dst_stats.st_size; + resume_chunk = dst_size / CHUNK_SIZE; + + if(resume_chunk <= 0) { + return resume_chunk; + } + + check_chunk = resume_chunk - 1; + + fseek(src_file, (long)(check_chunk * CHUNK_SIZE), SEEK_SET); + fseek(dst_file, (long)(check_chunk * CHUNK_SIZE), SEEK_SET); + + status = process_chunk(src_file, dst_file, check_chunk, &bytes_processed); + + if(status < 0) { + fprintf(stderr, "Error verifying chunk %lu\n", (unsigned long) check_chunk); + return 0; /* We'll interpret 0 as a signal to just restart since we couldn't verify the chunk. */ + } + + if(status == 1) { + log_info("Determined resume chunk to be at index %lu\n", check_chunk); + resume_chunk = check_chunk; /* When we encounter a mismatch it's best to roll back to the previous chunk. */ + } + + return resume_chunk; +} + +/* + * reposition_streams_for_resume(): Set the stream positions of the src_file and dst_file streams to resume_chunk. + */ +static void reposition_streams_for_resume(FILE* src_file, FILE* dst_file, size_t resume_chunk) { + long offset = (long)(resume_chunk * CHUNK_SIZE); + fseek(src_file, offset, SEEK_SET); + fseek(dst_file, offset, SEEK_SET); +} + +size_t copier_copy_file(const char* src, const char* dst) { + FILE* src_file; + FILE* dst_file; + struct stat src_stats; + struct stat dst_stats; + size_t copied; + size_t resume_chunk = 0; + + /* Open the src and dst files */ + src_file = open_src_file(src); + if(!src_file) { + return -1; + } + + dst_file = open_dst_file(dst); + if(!dst_file) { + fclose(src_file); + return -1; + } + + /* Get the size of the src file */ + if(get_file_stats(src, &src_stats) != 0) { + fclose(src_file); + fclose(dst_file); + return -1; + } + + if(stat(dst, &dst_stats) == 0) { + copy_existing_action_t action = prompt_user_action(dst); + + switch(action) { + case COPY_SKIP: + fclose(src_file); + fclose(dst_file); + return 0; + case COPY_OVERWRITE: + fclose(dst_file); + dst_file = fopen(dst, "w+b"); + if(!dst_file) { + fclose(src_file); + return -1; + } + resume_chunk = 0; + break; + case COPY_RESUME: + resume_chunk = determine_resume_chunk(src_file, dst_file, dst); + break; + } + } + + reposition_streams_for_resume(src_file, dst_file, resume_chunk); + + /* Process all the chunks until everything is copied */ + copied = process_chunks(src_file, dst_file, &src_stats); + copied += resume_chunk * CHUNK_SIZE; + if(copied > (size_t) src_stats.st_size) { + copied = src_stats.st_size; + } + + /* Close the files */ + fclose(src_file); + fclose(dst_file); + + return copied; +} + +int copier_get_total_stats(const char* src, size_t* total_files, size_t* total_bytes) { + struct stat stats; + struct dirent* entry; + char sub_path[1024]; + + if(!total_files) { + fprintf(stderr, "total_files cannot be NULL\n"); + return 1; + } + + if(!total_bytes) { + fprintf(stderr, "total_bytes cannot be NULL\n"); + return 1; + } + + + if(compat_stat(src, &stats) != 0) { + fprintf(stderr, "Failed to acquire stats for source \"%s\": %d (%s)\n", src, errno, strerror(errno)); + return 1; + } + + if(S_ISDIR(stats.st_mode)) { + DIR *dir = compat_opendir(src); + if(!dir) { + fprintf(stderr, "Failed to open %s: %d (%s)\n", src, errno, strerror(errno)); + return 1; + } + + while ((entry = compat_readdir(dir)) != NULL) { + if(strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { + continue; + } + + sprintf(sub_path, "%s/%s", src, entry->d_name); + if(compat_stat(sub_path, &stats) != 0) { + fprintf(stderr, "Failed to acquire file stats for sub path %s: %d (%s)\n", sub_path, errno, strerror(errno)); + return 1; + } + + if(S_ISDIR(stats.st_mode)) { + copier_get_total_stats(sub_path, total_files, total_bytes); + } else { + *total_files += 1; + *total_bytes += stats.st_size; + } + } + + compat_closedir(dir); + } else { + *total_files += 1; + *total_bytes += stats.st_size; + } + + return 0; +} + diff --git a/src/fletcher32/CMakeLists.txt b/src/fletcher32/CMakeLists.txt new file mode 100644 index 0000000..cb9715a --- /dev/null +++ b/src/fletcher32/CMakeLists.txt @@ -0,0 +1,16 @@ +add_library(fletcher32 STATIC + fletcher32.c +) + +target_include_directories(fletcher32 PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} +) + +target_link_libraries(fletcher32 PUBLIC + word16 +) + +add_executable(test_fletcher32 test_fletcher32.c) +target_link_libraries(test_fletcher32 fletcher32) +add_test(NAME test_fletcher32 COMMAND test_fletcher32) + diff --git a/src/fletcher32/fletcher32.c b/src/fletcher32/fletcher32.c new file mode 100644 index 0000000..9c29f65 --- /dev/null +++ b/src/fletcher32/fletcher32.c @@ -0,0 +1,59 @@ +#include "fletcher32.h" + +int fletcher32_debug = 0; + +unsigned long fletcher32(const unsigned short* data, size_t words_remaining) { + unsigned long sum1 = 0xFFFF; + unsigned long sum2 = 0xFFFF; + size_t word_index = 0; + + if(fletcher32_debug) { + printf("Fletcher-32: Processing %lu words\n", words_remaining); + printf("%-6s %-8s %-10s %-10s\n", "Index", "Word", "Sum1", "Sum2"); + printf("--------------------------------------\n"); + } + + if(words_remaining == 0 || data == NULL) { + return (sum2 << 16) | sum1; + } + + while(words_remaining > 0) { + size_t words_to_process = words_remaining > 360 ? 360 : words_remaining; + words_remaining -= words_to_process; + + while(words_to_process > 0) { + unsigned short word = *data++; + + sum1 += word; + sum2 += sum1; + word_index++; + words_to_process--; + + if(fletcher32_debug) { + printf("%-6lu 0x%04X 0x%08lX 0x%08lX\n", word_index - 1, word, sum1, sum2); + } + } + + if(fletcher32_debug) { + printf("Folding sums to 16 bits:\n"); + printf(" Before: sum1=0x%08lX, sum2=0x%08lX\n", sum1, sum2); + } + + sum1 = (sum1 & 0xFFFF) + (sum1 >> 16); + sum2 = (sum2 & 0xFFFF) + (sum2 >> 16); + + if(fletcher32_debug) { + printf(" After: sum1=0x%08lX, sum2=0x%08lX\n", sum1, sum2); + printf("--------------------------------------\n"); + } + } + + sum1 = (sum1 & 0xFFFF) + (sum1 >> 16); + sum2 = (sum2 & 0xFFFF) + (sum2 >> 16); + + if(fletcher32_debug) { + printf("Final checksum: 0x%08lX\n", (sum2 << 16) | sum1); + } + + return (sum2 << 16) | sum1; +} diff --git a/src/fletcher32/fletcher32.h b/src/fletcher32/fletcher32.h new file mode 100644 index 0000000..602c446 --- /dev/null +++ b/src/fletcher32/fletcher32.h @@ -0,0 +1,14 @@ +#ifndef FLETCHER_32_H +#define FLETCHER_32_H + +#include + +extern int fletcher32_debug; + +/** + * Fletcher-32 operates on 16-bit words, so the data is expected to be of unsigned short. + * In C89 unsigned long is guaranteed to be at least 32 bits. An unsigned int can be 16-bit, so we can't trust that it's any larger. + */ +unsigned long fletcher32(const unsigned short* words, size_t number_of_words); + +#endif diff --git a/src/fletcher32/test_fletcher32.c b/src/fletcher32/test_fletcher32.c new file mode 100644 index 0000000..17cf65e --- /dev/null +++ b/src/fletcher32/test_fletcher32.c @@ -0,0 +1,43 @@ +#include "fletcher32.h" +#include "word16.h" +#include + +struct test_case_t { + const char* name; + const char* input; + unsigned long expected; +}; + +int main(void) { + struct test_case_t test_cases[] = { + {"empty input", NULL, 0xFFFFFFFF}, + {"abcde", "abcde", 0xF04FC729}, + {"abcdef", "abcdef", 0x56502D2A}, + {"abcdefgh", "abcdefgh", 0xEBE19591} + }; + int i, len, exit_code; + + len = sizeof(test_cases) / sizeof(test_cases[0]); + exit_code = 0; + + for(i = 0; i < len; i++) { + size_t number_of_words; + unsigned short data[10]; + unsigned long actual; + + if(test_cases[i].input == NULL) { + actual = fletcher32(NULL, 0); + } else { + number_of_words = word16_count(strlen(test_cases[i].input)); + word16_create((const unsigned char*) test_cases[i].input, test_cases[i].input == NULL ? 0 : strlen(test_cases[i].input), data); + actual = fletcher32(data, number_of_words); + } + + if(test_cases[i].expected != actual) { + fprintf(stderr, "Test \"%s\" failed. Expected %lu, but got %lu\n", test_cases[i].name, test_cases[i].expected, actual); + exit_code = 1; + } + } + + return exit_code; +} diff --git a/src/getopt/CMakeLists.txt b/src/getopt/CMakeLists.txt new file mode 100644 index 0000000..c7bc1d7 --- /dev/null +++ b/src/getopt/CMakeLists.txt @@ -0,0 +1,15 @@ +add_library(getopt STATIC + getopt.c +) + +target_include_directories(getopt PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} +) + +add_executable(test_swap_with_tail test_swap_with_tail.c) +target_link_libraries(test_swap_with_tail PRIVATE getopt) +add_test(NAME test_swap_with_tail COMMAND test_swap_with_tail) + +add_executable(test_getopt test_getopt.c) +target_link_libraries(test_getopt PRIVATE getopt) +add_test(NAME test_getopt COMMAND test_getopt) diff --git a/src/getopt/getopt.c b/src/getopt/getopt.c new file mode 100644 index 0000000..ddc6163 --- /dev/null +++ b/src/getopt/getopt.c @@ -0,0 +1,113 @@ +#include +#include +#include "getopt.h" + +int optind = 1; /* Which argv we're looking at. */ +char* optarg; /* Argument for an option. */ +int opterr = 1; /* Nonzero means print errors. */ +int optopt; /* Current option char. */ + +static char* nextchar; + +#define SWAP_ERROR(msg) \ + do { if (opterr) fprintf(stderr, "Error: %s\n", msg); return 1; } while(0) + +int swap_with_tail(int argc, char* argv[], int n) { + char* non_option; + char* tail; + + if(!argv) SWAP_ERROR("argv is NULL"); + if(argc <= 0) SWAP_ERROR("argc must be greater than zero"); + if(n < 0) SWAP_ERROR("n must be greater than or equal to zero"); + if(n >= argc) SWAP_ERROR("n must be less than argc"); + + non_option = argv[n]; + tail = argv[argc - 1]; + + argv[argc - 1] = non_option; + argv[n] = tail; + + return 0; +} + +int getopt(int argc, char* argv[], const char* optstring) { + const char* p; + + if(optstring == NULL) { + if(opterr) { + fprintf(stderr, "Error: optstring is required\n"); + } + return -1; + } + + if(optind >= argc) { + /* No more arguments */ + return -2; + } + + /* Are we looking at an option? */ + if(argv[optind][0] != '-' || argv[optind][1] == '\0') { + /* By default, getopt() permutes the contents of argv as it scans, so that eventually all the nonoptions are at the end. */ + if(swap_with_tail(argc, argv, optind) != 0) { + return -3; + } + + argc--; + + return getopt(argc, argv, optstring); + } + + /* Are we looking at the special argument "--"? */ + if(argv[optind][1] == '-' && argv[optind][2] == '\0') { + /* The special argument "--" forces an end of option-scanning regardless of the scanning mode. */ + optind++; + return -4; + } + + if(nextchar == NULL) { + /* The character to start with in the current argv */ + nextchar = argv[optind++] + 1; + } + + /* Pull the next char and check if it's valid. */ + optopt = *nextchar++; + + /* Point into optstring for iteration. */ + p = optstring; + + while(*p != '\0') { + if(*p == optopt) { + /* The char is valid. Check if an argument is required. */ + if(p[1] == ':') { + if(*nextchar != '\0') { + /* The argument is attached, example: -Dfoo */ + optarg = nextchar; + nextchar = NULL; + } else if(optind < argc) { + /* The argument is in the next argv, example: -D foo */ + optarg = argv[optind++]; + nextchar = NULL; + } else { + if(opterr) { + fprintf(stderr, "Error: Option \"%c\" requires an argument.\n", optopt); + } + + return ':'; + } + } + + if(nextchar != NULL && *nextchar == '\0') { + nextchar = NULL; + } + + return optopt; + } + p++; + } + + /* Return a question mark if the option char is unknown. */ + if(opterr) { + fprintf(stderr, "Error: \"%c\" is an unknown option.\n", (char) optopt); + } + return '?'; +} diff --git a/src/getopt/getopt.h b/src/getopt/getopt.h new file mode 100644 index 0000000..2f843e7 --- /dev/null +++ b/src/getopt/getopt.h @@ -0,0 +1,13 @@ +#ifndef GETOPT_H +#define GETOPT_H + +extern char* optarg; +extern int optind; +extern int opterr; +extern int optopt; + +int swap_with_tail(int argc, char* argv[], int n); + +int getopt(int argc, char *argv[], const char* optstring); + +#endif diff --git a/src/getopt/test_getopt.c b/src/getopt/test_getopt.c new file mode 100644 index 0000000..5819f32 --- /dev/null +++ b/src/getopt/test_getopt.c @@ -0,0 +1,148 @@ +#include "getopt.h" +#include +#include + +typedef int (*test_fn)(void); + +struct test_case_t { + const char* name; + char expected; + test_fn fn; +}; + +int test_option_with_attached_argument(void) { + int argc = 2; + char* argv[2] = {"foobar", "-ar"}; + char* optstring = "a:"; + int actual = getopt(argc, argv, optstring); + + if(actual != 'a') { + fprintf(stderr, "expected 'a', but got %c\n", (char) actual); + return -1; + } + + if(optarg == NULL) { + fprintf(stderr, "expected optarg to not be NULL, but it was NULL\n"); + return -1; + } + + if(optarg[0] != 'r') { + fprintf(stderr, "expected optarg[0] to be 'r', but it was %c\n", (char) optarg[0]); + return -1; + } + + return 'a'; +} + +int test_option_with_detached_argument(void) { + int argc = 3; + char* argv[3] = {"foobar", "-a", "r"}; + char* optstring = "a:"; + int actual = getopt(argc, argv, optstring); + + if(actual != 'a') { + fprintf(stderr, "expected 'a', but got %c\n", (char) actual); + return actual; + } + + if(optarg == NULL) { + fprintf(stderr, "expected optarg to not be NULL, but it was NULL\n"); + return -1; + } + + if(optarg[0] != 'r') { + fprintf(stderr, "expected optarg[0] to be 'r', but it was %c\n", (char) optarg[0]); + return -1; + } + + return actual; +} + +int test_option_without_argument(void) { + int argc = 2; + char* argv[2] = {"foobar", "-a"}; + char* optstring = "a"; + int actual = getopt(argc, argv, optstring); + + if(actual != 'a') { + fprintf(stderr, "expected 'a', but got %c\n", (char) actual); + return actual; + } + + if(optarg != NULL) { + fprintf(stderr, "expected optarg to be NULL, but it was %s\n", optarg); + return -1; + } + + return actual; +} + +int test_option_and_non_options(void) { + int argc = 5; + char* argv[5] = {"foobar", "foo", "-a", "bar", "-h"}; + char* optstring = "ah"; + int actual = getopt(argc, argv, optstring); + + if(actual != 'a') { + fprintf(stderr, "expected 'a', but got %c\n", (char) actual); + return actual; + } + + if(optarg != NULL) { + fprintf(stderr, "expected optarg to be NULL, but it was %s\n", optarg); + return -1; + } + + if(strcmp(argv[1], "-a") != 0) { + fprintf(stderr, "expected '-a' at argv[1], but it was %s\n", argv[1]); + return -1; + } + + if(strcmp(argv[2], "-h") != 0) { + fprintf(stderr, "expected '-h' at argv[2], but it was %s\n", argv[2]); + return -1; + } + + if(strcmp(argv[3], "foo") != 0) { + fprintf(stderr, "expected 'foo' at argv[3], but it was %s\n", argv[3]); + return -1; + } + + if(strcmp(argv[4], "bar") != 0) { + fprintf(stderr, "expected 'bar' at argv[4], but it was %s\n", argv[4]); + return -1; + } + + return actual; +} + +int main(void) { + struct test_case_t test_cases[] = { + {"option with attached argument", 'a', test_option_with_attached_argument}, + {"option with detached argument", 'a', test_option_with_detached_argument}, + {"option without argument", 'a', test_option_without_argument}, + {"option and non-options", 'a', test_option_and_non_options} + }; + int i, len, exit_code; + + len = sizeof(test_cases) / sizeof(test_cases[0]); + exit_code = 0; + + for(i = 0; i < len; i++) { + int actual; + + /* Reset optind, optopt and optarg between each test execution */ + optind = 1; + optopt = -1; + optarg = NULL; + + actual = test_cases[i].fn(); + + if(test_cases[i].expected != actual) { + fprintf(stderr, "Test \"%s\" failed. Expected %d, but got %d\n", test_cases[i].name, test_cases[i].expected, actual); + exit_code = 1; + } + } + + return exit_code; +} diff --git a/src/getopt/test_swap_with_tail.c b/src/getopt/test_swap_with_tail.c new file mode 100644 index 0000000..0d5f456 --- /dev/null +++ b/src/getopt/test_swap_with_tail.c @@ -0,0 +1,30 @@ +#include "getopt.h" +#include +#include + +int main(void) { + char* argv[3]; + int argc = 3; + int n = 1; + + argv[0] = "foo"; + argv[1] = "bar"; + argv[2] = "baz"; + + if(swap_with_tail(argc, argv, n) != 0) { + fprintf(stderr, "swap_with_tail returned non-zero exit code\n"); + return 1; + } + + if(strcmp(argv[argc - 1], "bar") != 0) { + fprintf(stderr, "Unexpected value at tail position: %s\n", argv[argc - 1]); + return -1; + } + + if(strcmp(argv[n], "baz") != 0) { + fprintf(stderr, "Unexpected value at n position: %s\n", argv[n]); + return -1; + } + + return 0; +} diff --git a/src/libnitro.c b/src/libnitro.c deleted file mode 100644 index c7ae70e..0000000 --- a/src/libnitro.c +++ /dev/null @@ -1,342 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include "libnitro.h" -#include "nitro_console_compat.h" -#include "compat.h" -#include "nitro_debug.h" - -#define BUFFER_SIZE 4096 -#define ERROR_MSG_SIZE 256 - -/* State holder.*/ -struct NitroCopyState { - unsigned long total_size; - unsigned long total_files; - unsigned long bytes_copied; - unsigned long files_processed; - unsigned int overwrite; - char error_msg[ERROR_MSG_SIZE]; -}; - -/* Cross-platform helper function for retrieving file stats. */ -static NitroStatus _nitro_get_file_stats(const char* path, struct stat* file_stats) { -#if defined(_WIN32) - /* Make sure we use the correct struct type. */ - struct _stat64i32 win_stats; - if(_stat64i32(path, &win_stats) != 0) { - return NITRO_ERROR_GENERAL; - } - - /* Manually copy the relevant fields. */ - file_stats->st_mode = win_stats.st_mode; - file_stats->st_size = win_stats.st_size; -#else - if(stat(path, file_stats) != 0) { - return NITRO_ERROR_GENERAL; - } -#endif - - return NITRO_SUCCESS; -} - -void _nitro_print_state(NitroCopyState* state) { - NITRO_DEBUG_LOG("state->total_size = %lu", state->total_size); - NITRO_DEBUG_LOG("state->total_files = %lu", state->total_files); - NITRO_DEBUG_LOG("state->bytes_copied = %lu", state->bytes_copied); - NITRO_DEBUG_LOG("state->files_processed = %lu", state->files_processed); - NITRO_DEBUG_LOG("state->overwrite = %d", state->overwrite); - NITRO_DEBUG_LOG("state->error_msg = \"%s\"", state->error_msg); -} - -/* State constructor. */ -NitroCopyState* nitro_init(unsigned int overwrite) { - NitroCopyState* state = malloc(sizeof(NitroCopyState)); - if(state == NULL) { - return NULL; - } - - state->total_size = 0; - state->total_files = 0; - state->bytes_copied = 0; - state->files_processed = 0; - state->overwrite = overwrite; - state->error_msg[0] = '\0'; /* Initialize error message buffer. */ - - _nitro_print_state(state); - - return state; -} - -/* State destructor. */ -void nitro_destroy(NitroCopyState* state) { - if(state != NULL) { - free(state); - } -} - -NitroStatus nitro_copy(NitroCopyState* state, const char* src, const char* dest) { - struct stat file_stats; - char options[64]; - const char* formatted_size; - const char* formatted_bytes_copied; - - if(_nitro_get_file_stats(src, &file_stats) != NITRO_SUCCESS) { - fprintf(stderr, "Failed to open src file \"%s\": %d (%s)\n", src, errno, strerror(errno)); - return NITRO_ERROR_OPENING_FILE; - } - - if(state->overwrite) { - snprintf(options, sizeof(options), "overwrite = yes"); - } else { - snprintf(options, sizeof(options), "overwrite = no"); - } - - nitro_get_total_stats(src, &state->total_size, &state->total_files); - - formatted_size = nitro_format_bytes(state->total_size); - printf("Files to copy: %lu (%s, %s)\n\n", state->total_files, formatted_size, options); - - if(S_ISDIR(file_stats.st_mode)) { - NitroStatus status = nitro_copy_directory(state, src, dest); - if(status != NITRO_SUCCESS) { - return status; - } - } else { - NitroStatus status = nitro_copy_file(state, src, dest); - if(status != NITRO_SUCCESS) { - return status; - } - } - - _nitro_print_state(state); - - formatted_bytes_copied = nitro_format_bytes(state->bytes_copied); - - printf("\nFinished copying files: %lu/%lu (%s/%s, %s)\n\n", state->files_processed, state->total_files, formatted_bytes_copied, formatted_size, options); - - return NITRO_SUCCESS; -} - -NitroStatus nitro_copy_file(NitroCopyState* state, const char *src, const char *dest) { - struct stat file_stats; - FILE *src_file; - FILE *dest_file; - char buffer[BUFFER_SIZE]; - size_t bytes_read; - - src_file = fopen(src, "rb"); - if(!src_file) { - fprintf(stderr, "Failed to open src file \"%s\": %d (%s)\n", src, errno, strerror(errno)); - return NITRO_ERROR_OPENING_FILE; - } - - if(_nitro_get_file_stats(dest, &file_stats) == 0) { - if(S_ISDIR(file_stats.st_mode)) { - fclose(src_file); - fprintf(stderr, "Destination path \"%s\" is a directory\n", dest); - return NITRO_ERROR_DEST_IS_DIR; - } - - if(!state->overwrite) { - while(1) { - char response[4]; - - printf("Destination file \"%s\" exists already. Overwrite? (y/n): ", dest); - fflush(stdout); - - if(fgets(response, sizeof(response), stdin) != NULL) { - if(response[0] == 'y' || response[0] == 'Y') { - printf("Overwriting file \"%s\"\n", dest); - break; - } - if(response[0] == 'n' || response[0] == 'N') { - return NITRO_SUCCESS; - } - } - printf("Invalid input. Please enter 'y' or 'n'.\n"); - } - } - } - - dest_file = fopen(dest, "wb"); - if(!dest_file) { - fclose(src_file); - fprintf(stderr, "Failed to open or create destination file \"%s\": %d (%s)\n", dest, errno, strerror(errno)); - return NITRO_ERROR_OPENING_FILE; - } - - state->files_processed++; - - while ((bytes_read = fread(buffer, 1, BUFFER_SIZE, src_file)) > 0) { - unsigned int progress; - const char* formatted_bytes_copied; - const char* formatted_total_size; - - if(fwrite(buffer, 1, bytes_read, dest_file) != bytes_read) { - fprintf(stderr, "Failed to write to destination file \"%s\": %d (%s)\n", dest, errno, strerror(errno)); - break; - } - - state->bytes_copied += bytes_read; - progress = (int)((double) state->bytes_copied / state->total_size * 100); - formatted_bytes_copied = nitro_format_bytes(state->bytes_copied); - formatted_total_size = nitro_format_bytes(state->total_size); - - nitro_console_clear_line(); - printf("\rCopying file %lu/%lu: %s -> %s (%3d%%, %s/%s)", state->files_processed, state->total_files, src, dest, progress, formatted_bytes_copied, formatted_total_size); - } - - printf("\n"); - - fclose(src_file); - fclose(dest_file); - - return NITRO_SUCCESS; -} - -NitroStatus nitro_copy_directory(NitroCopyState* state, const char* src, const char* dest) { - DIR *dir_to_copy; - struct stat file_stats; - struct dirent *file_entry; - NitroStatus current_status; - - dir_to_copy = compat_opendir(src); - if(!dir_to_copy) { - fprintf(stderr, "Failed to open src dir \"%s\": %d (%s)\n", src, errno, strerror(errno)); - return NITRO_ERROR_INVALID_PATH; - } - - if(_nitro_get_file_stats(dest, &file_stats) != NITRO_SUCCESS) { - if(compat_mkdir(dest, 0755) == -1) { - compat_closedir(dir_to_copy); - fprintf(stderr, "Failed to create destination directdory \"%s\": %d (%s)\n", dest, errno, strerror(errno)); - return NITRO_ERROR_CREATING_DIR; - } - } else if(!S_ISDIR(file_stats.st_mode)) { - compat_closedir(dir_to_copy); - fprintf(stderr, "Destination path \"%s\" is not a directory\n", dest); - return NITRO_ERROR_INVALID_PATH; - } - - while ((file_entry = compat_readdir(dir_to_copy)) != NULL) { - char entry_source_path[1024]; - char entry_destination_path[1024]; - - if(strcmp(file_entry->d_name, ".") == 0 || strcmp(file_entry->d_name, "..") == 0) { - /* Stay in the current directory, don't go back up. */ - continue; - } - - snprintf(entry_source_path, sizeof(entry_source_path), "%s/%s", src, file_entry->d_name); - snprintf(entry_destination_path, sizeof(entry_destination_path), "%s/%s", dest, file_entry->d_name); - - if(_nitro_get_file_stats(entry_source_path, &file_stats) != NITRO_SUCCESS) { - fprintf(stderr, "Warning: Could not get info for '%s', skipping.\n", src); - continue; - } - - if(S_ISDIR(file_stats.st_mode)) { - current_status = nitro_copy_directory(state, entry_source_path, entry_destination_path); - } else { - current_status = nitro_copy_file(state, entry_source_path, entry_destination_path); - } - - if(current_status != NITRO_SUCCESS) { - break; - } - } - - compat_closedir(dir_to_copy); - return current_status; -} - -NitroStatus nitro_get_total_stats(const char* src, unsigned long* total_size, unsigned long* file_count) { - struct stat file_stats; - struct dirent *entry; - char sub_path[1024]; - - if(_nitro_get_file_stats(src, &file_stats) != NITRO_SUCCESS) { - fprintf(stderr, "Failed to acquire file status for src path \"%s\": %d (%s)\n", src, errno, strerror(errno)); - return NITRO_ERROR_OPENING_FILE; - } - - if(S_ISDIR(file_stats.st_mode)) { - DIR *dir = compat_opendir(src); - if(!dir) { - fprintf(stderr, "Failed to open src dir \"%s\": %d (%s)\n", src, errno, strerror(errno)); - return NITRO_ERROR_OPENING_FILE; - } - - - while ((entry = compat_readdir(dir)) != NULL) { - if(strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { - continue; - } - - snprintf(sub_path, sizeof(sub_path), "%s/%s", src, entry->d_name); - if(_nitro_get_file_stats(sub_path, &file_stats) == 0) { - if(S_ISDIR(file_stats.st_mode)) { - nitro_get_total_stats(sub_path, total_size, file_count); - } else { - *total_size += file_stats.st_size; - *file_count += 1; - } - } - } - - compat_closedir(dir); - } else { - *total_size = file_stats.st_size; - *file_count += 1; - } - - return NITRO_SUCCESS; -} - -void nitro_update_progress(NitroCopyState* state) { - unsigned int progress; - const char* formatted_bytes_copied; - const char* formatted_total_size; - - if(state->total_size == 0) { - return; - } - - progress = (unsigned int)((double) state->bytes_copied / state->total_size * 100); - formatted_bytes_copied = nitro_format_bytes(state->bytes_copied); - formatted_total_size = nitro_format_bytes(state->total_size); - - /* Clear this line before printing the total progress. */ - nitro_console_clear_line(); - printf("\rTotal progress: %d%% (%s/%s)\n", progress, formatted_bytes_copied, formatted_total_size); - printf("\033[A"); - fflush(stdout); -} - -char* nitro_format_bytes(unsigned long bytes) { - char* s = (char*)malloc(sizeof(char) * 20); - const char* suffixes[] = {"B", "KB", "MB", "GB"}; - int i = 0; - double value; - - if(bytes == 0) { - snprintf(s, 20, "0 B"); - return s; - } - - i = (int)floor(log(bytes) / log(1024)); - i = (i < 0) ? 0 : i; - - if((unsigned long) i >= sizeof(suffixes) / sizeof(suffixes[0])) { - i = sizeof(suffixes) / sizeof(suffixes[0]) - 1; - } - - value = (double)bytes / pow(1024, i); - snprintf(s, 20, "%.2f %s", value, suffixes[i]); - - return s; -} diff --git a/src/logging.c b/src/logging.c new file mode 100644 index 0000000..198a57f --- /dev/null +++ b/src/logging.c @@ -0,0 +1,89 @@ +#include "logging.h" + +#include +#include +#include + +#ifndef LOG_LEVEL + #define LOG_LEVEL LOG_LEVEL_DEBUG +#endif + +static void log_msg(int level, const char* format, va_list args) { + time_t rawtime; + struct tm* info; + char timestamp[72]; + const char* level_str; + + if(level > LOG_LEVEL) { + return; + } + + time(&rawtime); + info = localtime(&rawtime); + + sprintf(timestamp, "%04d-%02d-%02d %02d:%02d:%02d", + info->tm_year + 1900, + info->tm_mon + 1, + info->tm_mday, + info->tm_hour, + info->tm_min, + info->tm_sec + ); + + switch(level) { + case LOG_LEVEL_FATAL: + level_str = "FATAL"; + break; + case LOG_LEVEL_ERROR: + level_str = "ERROR"; + break; + case LOG_LEVEL_WARN: + level_str = "WARN"; + break; + case LOG_LEVEL_INFO: + level_str = "INFO"; + break; + case LOG_LEVEL_DEBUG: + level_str = "DEBUG"; + break; + } + + printf("[%s] [%5s] - ", timestamp, level_str); + vprintf(format, args); +} + +void log_fatal(const char* format, ...) { + va_list args; + va_start(args, format); + log_msg(LOG_LEVEL_FATAL, format, args); + va_end(args); +} + +void log_error(const char* format, ...) { + va_list args; + va_start(args, format); + log_msg(LOG_LEVEL_ERROR, format, args); + va_end(args); +} + +void log_warn(const char* format, ...) { + va_list args; + va_start(args, format); + log_msg(LOG_LEVEL_WARN, format, args); + va_end(args); +} + +void log_info(const char* format, ...) { + va_list args; + va_start(args, format); + log_msg(LOG_LEVEL_INFO, format, args); + va_end(args); +} + +void log_debug(const char* format, ...) { + va_list args; + va_start(args, format); + log_msg(LOG_LEVEL_DEBUG, format, args); + va_end(args); +} + diff --git a/src/main.c b/src/main.c index 85a41ff..7f156ca 100644 --- a/src/main.c +++ b/src/main.c @@ -1,44 +1,44 @@ +#include "copier.h" +#include "getopt.h" +#include "nitro_version.h" +#include "runtime.h" + #include #include #include -#include -#include "libnitro.h" -#include "nitro_runtime.h" -#include "nitro_version.h" + +int overwrite = 0; +copy_existing_action_t g_default_copy_action = COPY_ACTION_UNSET; int main(int argc, char *argv[]) { int opt; - int overwrite = 0; - char* src_path = NULL; - char* dest_path = NULL; - NitroCopyState* state = NULL; + char* src_path = NULL; + char* dest_path = NULL; - static struct option long_options[] = { - {"overwrite", no_argument, 0, 'o'}, - {"verbose", no_argument, 0, 'v'}, - {"help", no_argument, 0, 'h'}, - {0, 0, 0, 0} - }; - - while ((opt = getopt_long(argc, argv, "ovVh", long_options, NULL)) != -1) { + while ((opt = getopt(argc, argv, "a:vh")) != -1) { switch(opt) { - case 'o': - overwrite = 1; + case 'a': + if(optarg[0] == 'r') { + g_default_copy_action = COPY_RESUME; + } else if(optarg[0] == 's') { + g_default_copy_action = COPY_SKIP; + } else if(optarg[0] == 'o') { + g_default_copy_action = COPY_OVERWRITE; + } else { + fprintf(stderr, "Invalid argument for -a: %s\n", optarg); + return 1; + } break; case 'v': - nitro_verbose = 1; - break; - case 'V': printf("NitroCopy version: %s\nhttps://github.com/maritims/nitrocopy\n\nWritten by Martin Severin Steffensen.\n", NITRO_VERSION_STRING); return 0; case 'h': printf("NitroCopy %s - A file copy utility for some modern systems and some not so modern systems.\n\n", NITRO_VERSION_STRING); printf("Usage: NitroCopy [OPTIONS] \n"); printf("Options:\n"); - printf(" -o, --overwrite Overwrite destination files without prompting.\n"); - printf(" -v, --verbose Enable verbose logging.\n"); - printf(" -V, --version Show version.\n"); - printf(" -h, --help Display this help message and exit.\n"); + printf(" -a, --action Default action to take when a destination file exists: [r]esume, [s]kip or [o]verwrite.\n"); + printf(" -v, --version Show version.\n"); + printf(" -h, --help Display this help: message and exit.\n"); printf("\nFor more information:\n"); printf(" Documentation: https://github.com/maritims/nitrocopy#readme\n"); @@ -65,10 +65,7 @@ int main(int argc, char *argv[]) { return 1; } - state = nitro_init(overwrite); - if(state == NULL) { - return 1; - } + printf("NitroCopy %s - (overwrite: %d)\n\n", NITRO_VERSION_STRING, overwrite); - return nitro_copy(state, src_path, dest_path); + return copier_execute(src_path, dest_path); } diff --git a/src/nitro_console_compat.c b/src/nitro_console_compat.c deleted file mode 100644 index 4b85613..0000000 --- a/src/nitro_console_compat.c +++ /dev/null @@ -1,38 +0,0 @@ -#include -#include "nitro_console_compat.h" - -#if defined(_WIN32) - #include -#endif - -void nitro_console_clear_line(void) { -#if defined(_WIN32) - HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE); - CONSOLE_SCREEN_BUFFER_INFO csbi; - DWORD written; - DWORD cells; - COORD cursor; - - if(!GetConsoleScreenBufferInfo(hConsole, &csbi)) { - return; - } - - /* This is where we are right now. */ - cursor = csbi.dwCursorPosition; - - /* This is the number of characters to clear. */ - cells = csbi.dwSize.X - cursor.X; - - /* Clear the characters. */ - FillConsoleOutputCharacter(hConsole, ' ', cells, cursor, &written); - - /* Reset the attributes. */ - FillConsoleOutputAttribute(hConsole, csbi.wAttributes, cells, cursor, &written); - - /* Move back to where we were. */ - SetConsoleCursorPosition(hConsole, cursor); -#else - printf("\033[K"); - fflush(stdout); -#endif -} diff --git a/src/nitro_runtime.c b/src/nitro_runtime.c deleted file mode 100644 index 6eb6d93..0000000 --- a/src/nitro_runtime.c +++ /dev/null @@ -1,3 +0,0 @@ -#include "nitro_runtime.h" - -int nitro_verbose = 0; diff --git a/src/platform/CMakeLists.txt b/src/platform/CMakeLists.txt new file mode 100644 index 0000000..922699b --- /dev/null +++ b/src/platform/CMakeLists.txt @@ -0,0 +1,5 @@ +if(WIN32) + add_subdirectory(win9x) +elseif(UNIX) + add_subdirectory(linux) +endif() diff --git a/src/platform/linux/CMakeLists.txt b/src/platform/linux/CMakeLists.txt new file mode 100644 index 0000000..7a3643d --- /dev/null +++ b/src/platform/linux/CMakeLists.txt @@ -0,0 +1,2 @@ +add_library(console STATIC console.c) +add_library(file STATIC file.c) diff --git a/src/platform/linux/console.c b/src/platform/linux/console.c new file mode 100644 index 0000000..f1fe443 --- /dev/null +++ b/src/platform/linux/console.c @@ -0,0 +1,29 @@ +#include "console.h" + +#include +#include +#include +#include + +int console_get_width(void) { + struct winsize ws; + + if(ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == -1) { + perror("ioctl"); + return 1; + } + + return ws.ws_col; +} + +void console_move_cursor_up(int rows) { + int row; + for(row = 0; row < rows; row++) { + printf("\033[A"); + } +} + +void console_clear_from_cursor_down(void) { + printf("\033[J"); +} + diff --git a/src/platform/linux/file.c b/src/platform/linux/file.c new file mode 100644 index 0000000..270d4cd --- /dev/null +++ b/src/platform/linux/file.c @@ -0,0 +1,36 @@ +#include "file.h" + +#include +#include + +DIR* compat_opendir(const char* path) { + return opendir(path); +} + +struct dirent* compat_readdir(DIR* dir) { + if(!dir) { + return NULL; + } + + return readdir(dir); +} + +int compat_closedir(DIR* dir) { + if(!dir) { + return -1; + } + + return closedir(dir); +} + +int compat_mkdir(const char* path, int mode) { + return mkdir(path, mode); +} + +int compat_stat(const char* path, struct stat* file_stats) { + if(stat(path, file_stats) != 0) { + return -1; + } + + return 0; +} diff --git a/src/platform/win9x/CMakeLists.txt b/src/platform/win9x/CMakeLists.txt new file mode 100644 index 0000000..7a3643d --- /dev/null +++ b/src/platform/win9x/CMakeLists.txt @@ -0,0 +1,2 @@ +add_library(console STATIC console.c) +add_library(file STATIC file.c) diff --git a/src/platform/win9x/console.c b/src/platform/win9x/console.c new file mode 100644 index 0000000..0d84242 --- /dev/null +++ b/src/platform/win9x/console.c @@ -0,0 +1,68 @@ +#include "console.h" + +#include +#include +#include +#include + +int console_get_width(void) { + HANDLE hConsole; + CONSOLE_SCREEN_BUFFER_INFO csbi; + + hConsole = GetStdHandle(STD_OUTPUT_HANDLE); + if(hConsole == NULL) { + return 1; + } + + if(!GetConsoleScreenBufferInfo(hConsole, &csbi)) { + return 1; + } + + return csbi.srWindow.Right - csbi.srWindow.Left + 1; +} + +void console_move_cursor_up(int rows) { + HANDLE hConsole; + CONSOLE_SCREEN_BUFFER_INFO csbi; + COORD cursor; + + hConsole = GetStdHandle(STD_OUTPUT_HANDLE); + if(hConsole == NULL) { + return; + } + + if(!GetConsoleScreenBufferInfo(hConsole, &csbi)) { + return; + } + + cursor = csbi.dwCursorPosition; + cursor.Y -= rows; + if(cursor.Y < 0) { + cursor.Y = 0; + } + + SetConsoleCursorPosition(hConsole, cursor); +} + +void console_clear_from_cursor_down(void) { + HANDLE hConsole; + CONSOLE_SCREEN_BUFFER_INFO csbi; + COORD cursor; + DWORD cells; + DWORD written; + + hConsole = GetStdHandle(STD_OUTPUT_HANDLE); + if(hConsole == NULL) { + return; + } + + if(!GetConsoleScreenBufferInfo(hConsole, &csbi)) { + return; + } + + cursor = csbi.dwCursorPosition; + cells = (csbi.dwSize.Y - cursor.Y) * csbi.dwSize.X; + + FillConsoleOutputCharacter(hConsole, ' ', cells, cursor, &written); + FillConsoleOutputAttribute(hConsole, csbi.wAttributes, cells, cursor, &written); +} diff --git a/src/compat.c b/src/platform/win9x/file.c similarity index 72% rename from src/compat.c rename to src/platform/win9x/file.c index 9b0747a..5a3964f 100644 --- a/src/compat.c +++ b/src/platform/win9x/file.c @@ -1,16 +1,13 @@ -#include "compat.h" +#include "file.h" -#if defined(_WIN32) +#include +#include +#include #include #include #include -#else -#include -#include -#endif DIR* compat_opendir(const char* path) { -#if defined(_WIN32) DIR* dir; char search_path[MAX_PATH]; @@ -27,9 +24,6 @@ DIR* compat_opendir(const char* path) { return NULL; } return dir; -#else - return opendir(path); -#endif } struct dirent* compat_readdir(DIR* dir) { @@ -37,7 +31,6 @@ struct dirent* compat_readdir(DIR* dir) { return NULL; } -#if defined(_WIN32) while (dir->is_first_entry || FindNextFile(dir->handle, &(dir->find_data))) { dir->is_first_entry = 0; @@ -51,28 +44,32 @@ struct dirent* compat_readdir(DIR* dir) { } return NULL; -#else - return readdir(dir); -#endif } int compat_closedir(DIR* dir) { if(!dir) { - return -1; + return -1; } -#if defined(_WIN32) + FindClose(dir->handle); free(dir); return 0; -#else - return closedir(dir); -#endif } int compat_mkdir(const char* path, int mode) { -#if defined(_WIN32) return mkdir(path); -#else - return mkdir(path, mode); -#endif +} + +int compat_stat(const char* path, struct stat* file_stats) { + /* Make sure we use the correct struct type. */ + struct _stat64i32 win_stats; + if(_stat64i32(path, &win_stats) != 0) { + return 1; + } + + /* Manually copy the relevant fields. */ + file_stats->st_mode = win_stats.st_mode; + file_stats->st_size = win_stats.st_size; + + return 0; } diff --git a/src/word16.c b/src/word16.c new file mode 100644 index 0000000..d9f1ba1 --- /dev/null +++ b/src/word16.c @@ -0,0 +1,23 @@ +#include "word16.h" + +size_t word16_count(size_t length) { + return (length + 1) / 2; +} + +size_t word16_create(const unsigned char* input, size_t length, unsigned short* output) { + size_t word_count = word16_count(length); + size_t word_index; + + for(word_index = 0; word_index < word_count; word_index++) { + /* The high byte is the second character in a pair, so by multiplying the index we arrive at the character following the one at the current index */ + unsigned char high_byte = (unsigned char) input[word_index * 2]; + unsigned char low_byte = 0; + if(word_index * 2 + 1 < length) { + low_byte = (unsigned char) input[word_index * 2 + 1]; + } + + output[word_index] = ((unsigned short) low_byte << 8) | high_byte; + } + + return word_index; +} diff --git a/tests/tests.c b/tests/tests.c deleted file mode 100644 index e69de29..0000000