From 5e006e1d09246106f5fa1d3726d44c7b63c8eacb Mon Sep 17 00:00:00 2001 From: Maritim Date: Sun, 21 Sep 2025 21:26:17 +0200 Subject: [PATCH 1/9] Printing in place for POSIX and Win9x --- CMakeLists.txt | 83 ++++++++++++++++++++- include/nitro_console_compat.h | 11 ++- include/nitro_version.h.in | 3 +- src/libnitro.c | 20 +++-- src/nitro_console_compat.c | 130 +++++++++++++++++++++++++++++---- tests/CMakeLists.txt | 14 ++++ tests/cursor_test.c | 16 ++++ 7 files changed, 250 insertions(+), 27 deletions(-) create mode 100644 tests/CMakeLists.txt create mode 100644 tests/cursor_test.c diff --git a/CMakeLists.txt b/CMakeLists.txt index be80674..610105c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,7 +3,22 @@ project(NitroCopy VERSION 0.0.1 LANGUAGES C ) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +# ----------------------------------------------- +# 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 @@ -34,9 +49,47 @@ set(CMAKE_C_FLAGS_RELEASE "-O2") # Optimise for speed. # ----------------------------------------------- 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 @@ -51,14 +104,40 @@ include_directories(${CMAKE_BINARY_DIR}/include) # ----------------------------------------------- file(GLOB_RECURSE NITRO_SOURCES "src/*.c") +# Remove main from the library to avoid double main() +list(FILTER NITRO_SOURCES EXCLUDE REGEX "src/main.c") + +# Build library +add_library(nitro STATIC ${NITRO_SOURCES}) +target_link_libraries(nitro PRIVATE m) + # ----------------------------------------------- # Build application # ----------------------------------------------- -add_executable(NitroCopy ${NITRO_SOURCES}) +add_executable(NitroCopy src/main.c) # Link libraries. -target_link_libraries(NitroCopy PRIVATE m) +target_link_libraries(NitroCopy PRIVATE nitro) if (WIN32) target_link_options(NitroCopy PRIVATE -static -static-libgcc -static-libstdc++) endif() + +# ----------------------------------------------- +# Custom target for tagging releases +# ----------------------------------------------- +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}" +) + +# ----------------------------------------------- +# Tests +# ----------------------------------------------- +include(CTest) +enable_testing() + +# Add tests subdirectory +add_subdirectory(tests) diff --git a/include/nitro_console_compat.h b/include/nitro_console_compat.h index c7388d0..e7f22ab 100644 --- a/include/nitro_console_compat.h +++ b/include/nitro_console_compat.h @@ -1,6 +1,11 @@ -#ifndef NITRO_CONSOLE_H -#define NITRO_CONSOLE_H +#ifndef NITRO_CONSOLE_COMPAT_H +#define NITRO_CONSOLE_COMPAT_H + +int nitro_get_console_width(void); + +void nitro_clear_line(int previous_line_length); + +void nitro_reset_cursor(void); -void nitro_console_clear_line(void); #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/src/libnitro.c b/src/libnitro.c index c7ae70e..432d1eb 100644 --- a/src/libnitro.c +++ b/src/libnitro.c @@ -127,6 +127,7 @@ NitroStatus nitro_copy_file(NitroCopyState* state, const char *src, const char * FILE *dest_file; char buffer[BUFFER_SIZE]; size_t bytes_read; + size_t previous_length; src_file = fopen(src, "rb"); if(!src_file) { @@ -171,10 +172,12 @@ NitroStatus nitro_copy_file(NitroCopyState* state, const char *src, const char * state->files_processed++; + /* Save the cursor position before we start printing progress messages. */ while ((bytes_read = fread(buffer, 1, BUFFER_SIZE, src_file)) > 0) { unsigned int progress; const char* formatted_bytes_copied; const char* formatted_total_size; + char current_line[BUFFER_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)); @@ -186,8 +189,14 @@ NitroStatus nitro_copy_file(NitroCopyState* state, const char *src, const char * 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); + nitro_clear_line(previous_length); + + snprintf(current_line, sizeof(current_line), "Copying file %lu/%lu: %s -> %s (%3d%%, %s/%s)", state->files_processed, state->total_files, src, dest, progress, formatted_bytes_copied, formatted_total_size); + + + printf("%s\n", current_line); + fflush(stdout); + previous_length = strlen(current_line); } printf("\n"); @@ -310,10 +319,11 @@ void nitro_update_progress(NitroCopyState* state) { 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(); + + nitro_clear_line(0); + nitro_reset_cursor(); + printf("\rTotal progress: %d%% (%s/%s)\n", progress, formatted_bytes_copied, formatted_total_size); - printf("\033[A"); fflush(stdout); } diff --git a/src/nitro_console_compat.c b/src/nitro_console_compat.c index 4b85613..d93db29 100644 --- a/src/nitro_console_compat.c +++ b/src/nitro_console_compat.c @@ -1,38 +1,136 @@ +#include #include +#include #include "nitro_console_compat.h" #if defined(_WIN32) #include +#else + #include +#endif + +int nitro_get_console_width(void) { +#if defined(_WIN32) + HANDLE hConsole; + CONSOLE_SCREEN_BUFFER_INFO csbi; + int rows; + + hConsole = GetStdHandle(STD_OUTPUT_HANDLE); + if(hConsole == NULL) { + return 1; + } + + if(!GetConsoleScreenBufferInfo(hConsole, &csbi)) { + return 1; + } + + return csbi.srWindow.Right - csbi.srWindow.Left + 1; +#else + struct winsize ws; + + if(ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == -1) { + perror("ioctl"); + return 1; + } + + return ws.ws_col; #endif +} -void nitro_console_clear_line(void) { +static void _nitro_move_cursor_up(int rows) { #if defined(_WIN32) - HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE); - CONSOLE_SCREEN_BUFFER_INFO csbi; - DWORD written; - DWORD cells; - COORD cursor; + HANDLE hConsole; + CONSOLE_SCREEN_BUFFER_INFO csbi; + COORD cursor; + + hConsole = GetStdHandle(STD_OUTPUT_HANDLE); + if(hConsole == NULL) { + return; + } if(!GetConsoleScreenBufferInfo(hConsole, &csbi)) { return; } - /* This is where we are right now. */ cursor = csbi.dwCursorPosition; + cursor.Y -= rows; + if(cursor.Y < 0) { + cursor.Y = 0; + } + + SetConsoleCursorPosition(hConsole, cursor); +#else + int row; + for(row = 0; row < rows; row++) { + printf("\033[A"); + } +#endif +} - /* This is the number of characters to clear. */ - cells = csbi.dwSize.X - cursor.X; +static void _nitro_clear_from_cursor_down(void) { +#if defined(_WIN32) + HANDLE hConsole; + CONSOLE_SCREEN_BUFFER_INFO csbi; + COORD cursor; + DWORD cells; + DWORD written; - /* Clear the characters. */ - FillConsoleOutputCharacter(hConsole, ' ', cells, cursor, &written); + hConsole = GetStdHandle(STD_OUTPUT_HANDLE); + if(hConsole == NULL) { + return; + } - /* Reset the attributes. */ + 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); +#else + printf("\033[J"); +#endif +} - /* Move back to where we were. */ - SetConsoleCursorPosition(hConsole, cursor); +void nitro_clear_line(int previous_line_length) { + int line_width; + int rows; + + line_width = nitro_get_console_width(); + + if(previous_line_length > 0) { + rows = (previous_line_length + line_width - 1) / line_width; + if(rows > 1) { + _nitro_move_cursor_up(rows); + _nitro_clear_from_cursor_down(); + } + } +} + +void nitro_reset_cursor(void) { +#if defined(_WIN32) + HANDLE hConsole; + CONSOLE_SCREEN_BUFFER_INFO csbi; + + hConsole = GetStdHandle(STD_OUTPUT_HANDLE); + if(hConsole == INVALID_HANDLE_VALUE) { + return; + } + + if(!GetConsoleScreenBufferInfo(hConsole, &csbi)) { + return; + } + + csbi.dwCursorPosition.X = 0; + csbi.dwCursorPosition.Y = csbi.dwCursorPosition.Y - 1; + + SetConsoleCursorPosition(hConsole, csbi.dwCursorPosition); #else - printf("\033[K"); - fflush(stdout); + printf("\r"); #endif } + + + diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..bb6da1d --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,14 @@ +function(add_c_test name) + add_executable(${name} ${name}.c) + target_link_libraries(${name} PRIVATE nitro) + + if(UNIX AND CMAKE_SYSTEM_NAME STREQUAL "Windows") + # Cross build: use wine to run Win32 tests + add_test(NAME ${name} COMMAND wine $) + else() + # Native build: run directly + add_test(NAME ${name} COMMAND ${name}) + endif() +endfunction() + +add_c_test(cursor_test) diff --git a/tests/cursor_test.c b/tests/cursor_test.c new file mode 100644 index 0000000..ddaa892 --- /dev/null +++ b/tests/cursor_test.c @@ -0,0 +1,16 @@ +#include +#include "nitro_console_compat.h" + +#if defined(_WIN32) +#include +#else +#include +#endif + +int main(void) { + const char* msg = "This is a very long line that should wrap multiple times on the console depending on its width."; + printf("%s", msg); + + /*return vc->line_count == 1 ? 0 : 1;*/ + return 0; +} From 622483a03baa66adfde8006e0c268a437ac8fe7d Mon Sep 17 00:00:00 2001 From: Maritim Date: Sun, 21 Sep 2025 21:31:38 +0200 Subject: [PATCH 2/9] Removed unused code --- include/nitro_console_compat.h | 3 --- src/libnitro.c | 1 - src/nitro_console_compat.c | 22 ---------------------- 3 files changed, 26 deletions(-) diff --git a/include/nitro_console_compat.h b/include/nitro_console_compat.h index e7f22ab..abb154c 100644 --- a/include/nitro_console_compat.h +++ b/include/nitro_console_compat.h @@ -5,7 +5,4 @@ int nitro_get_console_width(void); void nitro_clear_line(int previous_line_length); -void nitro_reset_cursor(void); - - #endif diff --git a/src/libnitro.c b/src/libnitro.c index 432d1eb..114262f 100644 --- a/src/libnitro.c +++ b/src/libnitro.c @@ -321,7 +321,6 @@ void nitro_update_progress(NitroCopyState* state) { nitro_clear_line(0); - nitro_reset_cursor(); printf("\rTotal progress: %d%% (%s/%s)\n", progress, formatted_bytes_copied, formatted_total_size); fflush(stdout); diff --git a/src/nitro_console_compat.c b/src/nitro_console_compat.c index d93db29..28afe3f 100644 --- a/src/nitro_console_compat.c +++ b/src/nitro_console_compat.c @@ -109,28 +109,6 @@ void nitro_clear_line(int previous_line_length) { } } -void nitro_reset_cursor(void) { -#if defined(_WIN32) - HANDLE hConsole; - CONSOLE_SCREEN_BUFFER_INFO csbi; - - hConsole = GetStdHandle(STD_OUTPUT_HANDLE); - if(hConsole == INVALID_HANDLE_VALUE) { - return; - } - - if(!GetConsoleScreenBufferInfo(hConsole, &csbi)) { - return; - } - - csbi.dwCursorPosition.X = 0; - csbi.dwCursorPosition.Y = csbi.dwCursorPosition.Y - 1; - - SetConsoleCursorPosition(hConsole, csbi.dwCursorPosition); -#else - printf("\r"); -#endif -} From c739e0744dbedac3156db8aec76c37aaffc82bb9 Mon Sep 17 00:00:00 2001 From: Maritim Date: Sun, 21 Sep 2025 21:32:23 +0200 Subject: [PATCH 3/9] A little clarity --- src/nitro_console_compat.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/nitro_console_compat.c b/src/nitro_console_compat.c index 28afe3f..db30dba 100644 --- a/src/nitro_console_compat.c +++ b/src/nitro_console_compat.c @@ -9,7 +9,7 @@ #include #endif -int nitro_get_console_width(void) { +static int _nitro_get_console_width(void) { #if defined(_WIN32) HANDLE hConsole; CONSOLE_SCREEN_BUFFER_INFO csbi; @@ -98,7 +98,7 @@ void nitro_clear_line(int previous_line_length) { int line_width; int rows; - line_width = nitro_get_console_width(); + line_width = _nitro_get_console_width(); if(previous_line_length > 0) { rows = (previous_line_length + line_width - 1) / line_width; From 273f4e2bb093b51b6cec9a7cad9c7bcabac3ea3d Mon Sep 17 00:00:00 2001 From: Maritim Date: Sun, 21 Sep 2025 21:58:34 +0200 Subject: [PATCH 4/9] Ugly hack for avoiding superfluous newline character between copy statements on Win9x --- src/libnitro.c | 6 ++++-- src/nitro_console_compat.c | 10 ++-------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/libnitro.c b/src/libnitro.c index 114262f..b7a5205 100644 --- a/src/libnitro.c +++ b/src/libnitro.c @@ -116,7 +116,7 @@ NitroStatus nitro_copy(NitroCopyState* state, const char* src, const char* dest) 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); + printf("Finished copying files: %lu/%lu (%s/%s, %s)\n\n", state->files_processed, state->total_files, formatted_bytes_copied, formatted_size, options); return NITRO_SUCCESS; } @@ -172,7 +172,6 @@ NitroStatus nitro_copy_file(NitroCopyState* state, const char *src, const char * state->files_processed++; - /* Save the cursor position before we start printing progress messages. */ while ((bytes_read = fread(buffer, 1, BUFFER_SIZE, src_file)) > 0) { unsigned int progress; const char* formatted_bytes_copied; @@ -199,7 +198,10 @@ NitroStatus nitro_copy_file(NitroCopyState* state, const char *src, const char * previous_length = strlen(current_line); } +#if defined(_WIN32) +#else printf("\n"); +#endif fclose(src_file); fclose(dest_file); diff --git a/src/nitro_console_compat.c b/src/nitro_console_compat.c index db30dba..6f73e70 100644 --- a/src/nitro_console_compat.c +++ b/src/nitro_console_compat.c @@ -102,13 +102,7 @@ void nitro_clear_line(int previous_line_length) { if(previous_line_length > 0) { rows = (previous_line_length + line_width - 1) / line_width; - if(rows > 1) { - _nitro_move_cursor_up(rows); - _nitro_clear_from_cursor_down(); - } + _nitro_move_cursor_up(rows); + _nitro_clear_from_cursor_down(); } } - - - - From c0a8b35458f05ffbd1a993ad4772c125dca12239 Mon Sep 17 00:00:00 2001 From: Maritim Date: Wed, 24 Sep 2025 23:53:05 +0200 Subject: [PATCH 5/9] Cross platform configuration --- CMakeLists.txt | 27 +-- compile_commands.json | 1 + include/nitro_console.h | 10 ++ include/nitro_console_compat.h | 8 - include/nitro_debug.h | 33 ---- include/{compat.h => nitro_file.h} | 18 +- include/nitro_logging.h | 20 +++ src/CMakeLists.txt | 3 + src/app/CMakeLists.txt | 10 ++ src/{ => app}/main.c | 13 +- src/core/CMakeLists.txt | 1 + src/{ => core}/libnitro.c | 164 ++++++++---------- src/core/nitro_logging.c | 88 ++++++++++ src/{ => core}/nitro_runtime.c | 0 src/platform/CMakeLists.txt | 5 + src/platform/linux/CMakeLists.txt | 8 + src/platform/linux/nitro_console.c | 29 ++++ src/platform/linux/nitro_file.c | 36 ++++ src/platform/win9x/CMakeLists.txt | 8 + .../win9x/nitro_console.c} | 51 +----- src/{compat.c => platform/win9x/nitro_file.c} | 51 +++--- 21 files changed, 341 insertions(+), 243 deletions(-) create mode 120000 compile_commands.json create mode 100644 include/nitro_console.h delete mode 100644 include/nitro_console_compat.h delete mode 100644 include/nitro_debug.h rename include/{compat.h => nitro_file.h} (50%) create mode 100644 include/nitro_logging.h create mode 100644 src/CMakeLists.txt create mode 100644 src/app/CMakeLists.txt rename src/{ => app}/main.c (85%) create mode 100644 src/core/CMakeLists.txt rename src/{ => core}/libnitro.c (65%) create mode 100644 src/core/nitro_logging.c rename src/{ => core}/nitro_runtime.c (100%) create mode 100644 src/platform/CMakeLists.txt create mode 100644 src/platform/linux/CMakeLists.txt create mode 100644 src/platform/linux/nitro_console.c create mode 100644 src/platform/linux/nitro_file.c create mode 100644 src/platform/win9x/CMakeLists.txt rename src/{nitro_console_compat.c => platform/win9x/nitro_console.c} (61%) rename src/{compat.c => platform/win9x/nitro_file.c} (62%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 610105c..04154af 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -102,26 +102,7 @@ include_directories(${CMAKE_BINARY_DIR}/include) # ----------------------------------------------- # Source files # ----------------------------------------------- -file(GLOB_RECURSE NITRO_SOURCES "src/*.c") - -# Remove main from the library to avoid double main() -list(FILTER NITRO_SOURCES EXCLUDE REGEX "src/main.c") - -# Build library -add_library(nitro STATIC ${NITRO_SOURCES}) -target_link_libraries(nitro PRIVATE m) - -# ----------------------------------------------- -# Build application -# ----------------------------------------------- -add_executable(NitroCopy src/main.c) - -# Link libraries. -target_link_libraries(NitroCopy PRIVATE nitro) - -if (WIN32) - target_link_options(NitroCopy PRIVATE -static -static-libgcc -static-libstdc++) -endif() +add_subdirectory(src) # ----------------------------------------------- # Custom target for tagging releases @@ -136,8 +117,8 @@ add_custom_target(tag_release # ----------------------------------------------- # Tests # ----------------------------------------------- -include(CTest) -enable_testing() +#include(CTest) +#enable_testing() # Add tests subdirectory -add_subdirectory(tests) +#add_subdirectory(tests) 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/nitro_console.h b/include/nitro_console.h new file mode 100644 index 0000000..9d6d294 --- /dev/null +++ b/include/nitro_console.h @@ -0,0 +1,10 @@ +#ifndef NITRO_CONSOLE_COMPAT_H +#define NITRO_CONSOLE_COMPAT_H + +int nitro_console_get_width(void); + +void nitro_console_move_cursor_up(int rows); + +void nitro_console_clear_from_cursor_down(void); + +#endif diff --git a/include/nitro_console_compat.h b/include/nitro_console_compat.h deleted file mode 100644 index abb154c..0000000 --- a/include/nitro_console_compat.h +++ /dev/null @@ -1,8 +0,0 @@ -#ifndef NITRO_CONSOLE_COMPAT_H -#define NITRO_CONSOLE_COMPAT_H - -int nitro_get_console_width(void); - -void nitro_clear_line(int previous_line_length); - -#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/compat.h b/include/nitro_file.h similarity index 50% rename from include/compat.h rename to include/nitro_file.h index 232d732..d682b98 100644 --- a/include/compat.h +++ b/include/nitro_file.h @@ -1,5 +1,5 @@ -#ifndef COMPAT_H -#define COMPAT_H +#ifndef NITRO_FILE_COMPAT_H +#define NITRO_FILE_COMPAT_H #include #include @@ -22,10 +22,14 @@ typedef struct { #include #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); +DIR* nitro_file_opendir(const char* path); + +struct dirent* nitro_file_readdir(DIR* dir); + +int nitro_file_closedir(DIR* dir); + +int nitro_file_mkdir(const char* path, int mode); + +int nitro_file_stat(const char* path, struct stat* file_stats); #endif diff --git a/include/nitro_logging.h b/include/nitro_logging.h new file mode 100644 index 0000000..cfd2d44 --- /dev/null +++ b/include/nitro_logging.h @@ -0,0 +1,20 @@ +#ifndef NITRO_LOGGING_H +#define NITRO_LOGGING_H + +#define NITRO_LOG_LEVEL_FATAL 1 +#define NITRO_LOG_LEVEL_ERROR 2 +#define NITRO_LOG_LEVEL_WARN 3 +#define NITRO_LOG_LEVEL_INFO 4 +#define NITRO_LOG_LEVEL_DEBUG 5 + +void nitro_log_fatal(const char* format, ...); + +void nitro_log_error(const char* format, ...); + +void nitro_log_warn(const char* format, ...); + +void nitro_log_info(const char* format, ...); + +void nitro_log_debug(const char* format, ...); + +#endif diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..61a6e5e --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,3 @@ +add_subdirectory(core) +add_subdirectory(platform) +add_subdirectory(app) diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt new file mode 100644 index 0000000..3c936c4 --- /dev/null +++ b/src/app/CMakeLists.txt @@ -0,0 +1,10 @@ +add_executable(NitroCopy main.c) + +target_link_libraries(NitroCopy PRIVATE core m) + +if(WIN32) + target_link_libraries(NitroCopy PRIVATE nitro_compat_win9x) + target_link_options(NitroCopy PRIVATE -static -static-libgcc -static-libstdc++) +elseif(UNIX) + target_link_libraries(NitroCopy PRIVATE nitro_compat_linux) +endif() diff --git a/src/main.c b/src/app/main.c similarity index 85% rename from src/main.c rename to src/app/main.c index 85a41ff..85817c1 100644 --- a/src/main.c +++ b/src/app/main.c @@ -3,7 +3,7 @@ #include #include #include "libnitro.h" -#include "nitro_runtime.h" +#include "nitro_logging.h" #include "nitro_version.h" int main(int argc, char *argv[]) { @@ -15,20 +15,16 @@ int main(int argc, char *argv[]) { 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_long(argc, argv, "oh", long_options, NULL)) != -1) { switch(opt) { case 'o': overwrite = 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': @@ -36,8 +32,7 @@ int main(int argc, char *argv[]) { 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(" -v, --version Show version.\n"); printf(" -h, --help Display this help message and exit.\n"); printf("\nFor more information:\n"); @@ -65,6 +60,8 @@ int main(int argc, char *argv[]) { return 1; } + printf("NitroCopy %s - (overwrite: %d)\n\n", NITRO_VERSION_STRING, overwrite); + state = nitro_init(overwrite); if(state == NULL) { return 1; diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt new file mode 100644 index 0000000..b85fa45 --- /dev/null +++ b/src/core/CMakeLists.txt @@ -0,0 +1 @@ +add_library(core STATIC libnitro.c nitro_logging.c nitro_runtime.c) diff --git a/src/libnitro.c b/src/core/libnitro.c similarity index 65% rename from src/libnitro.c rename to src/core/libnitro.c index b7a5205..22faa34 100644 --- a/src/libnitro.c +++ b/src/core/libnitro.c @@ -1,13 +1,14 @@ +#include "nitro_console.h" +#include "nitro_file.h" +#include "nitro_logging.h" +#include "libnitro.h" + #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 @@ -22,37 +23,6 @@ struct NitroCopyState { 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) { @@ -64,28 +34,38 @@ NitroCopyState* nitro_init(unsigned int overwrite) { state->bytes_copied = 0; state->files_processed = 0; state->overwrite = overwrite; - state->error_msg[0] = '\0'; /* Initialize error message buffer. */ - - _nitro_print_state(state); + state->error_msg[0] = '\0'; /* Initialize error message buffer. */ return state; } -/* State destructor. */ void nitro_destroy(NitroCopyState* state) { if(state != NULL) { free(state); } } +void nitro_clear_line(int previous_line_length) { + int line_width; + int rows; + + line_width = nitro_console_get_width(); + + if(previous_line_length > 0) { + rows = (previous_line_length + line_width - 1) / line_width; + nitro_console_move_cursor_up(rows); + nitro_console_clear_from_cursor_down(); + } +} + 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)); + if(nitro_file_stat(src, &file_stats) != 0) { + nitro_log_fatal("Failed to open src file \"%s\": %d (%s)\n", src, errno, strerror(errno)); return NITRO_ERROR_OPENING_FILE; } @@ -95,10 +75,10 @@ NitroStatus nitro_copy(NitroCopyState* state, const char* src, const char* dest) snprintf(options, sizeof(options), "overwrite = no"); } - nitro_get_total_stats(src, &state->total_size, &state->total_files); - + nitro_log_info("Counting total number of files to copy...\n"); + 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); + nitro_log_info("Counted %lu files (%s)\n", state->total_files, formatted_size); if(S_ISDIR(file_stats.st_mode)) { NitroStatus status = nitro_copy_directory(state, src, dest); @@ -112,11 +92,9 @@ NitroStatus nitro_copy(NitroCopyState* state, const char* src, const char* dest) } } - _nitro_print_state(state); - formatted_bytes_copied = nitro_format_bytes(state->bytes_copied); - printf("Finished copying files: %lu/%lu (%s/%s, %s)\n\n", state->files_processed, state->total_files, formatted_bytes_copied, formatted_size, options); + nitro_log_info("Finished copying files: %lu/%lu (%s/%s, %s)\n", state->files_processed, state->total_files, formatted_bytes_copied, formatted_size, options); return NITRO_SUCCESS; } @@ -127,18 +105,22 @@ NitroStatus nitro_copy_file(NitroCopyState* state, const char *src, const char * FILE *dest_file; char buffer[BUFFER_SIZE]; size_t bytes_read; - size_t previous_length; + size_t total_bytes_read; + int last_reported_percentage_block; + int current_percentage_block; + int percentage_block_divisor; + int foo; src_file = fopen(src, "rb"); if(!src_file) { - fprintf(stderr, "Failed to open src file \"%s\": %d (%s)\n", src, errno, strerror(errno)); + nitro_log_error("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(nitro_file_stat(dest, &file_stats) == 0) { if(S_ISDIR(file_stats.st_mode)) { fclose(src_file); - fprintf(stderr, "Destination path \"%s\" is a directory\n", dest); + nitro_log_error("Destination path \"%s\" is a directory\n", dest); return NITRO_ERROR_DEST_IS_DIR; } @@ -166,43 +148,41 @@ NitroStatus nitro_copy_file(NitroCopyState* state, const char *src, const char * 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)); + nitro_log_error("Failed to open or create destination file \"%s\": %d (%s)\n", dest, errno, strerror(errno)); return NITRO_ERROR_OPENING_FILE; } state->files_processed++; + last_reported_percentage_block = -1; + total_bytes_read = 0; + foo = (file_stats.st_size / BUFFER_SIZE) / BUFFER_SIZE; + percentage_block_divisor = 100 / foo; while ((bytes_read = fread(buffer, 1, BUFFER_SIZE, src_file)) > 0) { unsigned int progress; const char* formatted_bytes_copied; const char* formatted_total_size; - char current_line[BUFFER_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)); + nitro_log_error("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); + state->bytes_copied += bytes_read; + total_bytes_read += bytes_read; + progress = (int)((double) total_bytes_read / file_stats.st_size * 100); + current_percentage_block = progress / percentage_block_divisor; - nitro_clear_line(previous_length); + if(current_percentage_block > last_reported_percentage_block) { + formatted_bytes_copied = nitro_format_bytes(state->bytes_copied); + formatted_total_size = nitro_format_bytes(state->total_size); - snprintf(current_line, sizeof(current_line), "Copying file %lu/%lu: %s -> %s (%3d%%, %s/%s)", state->files_processed, state->total_files, src, dest, progress, formatted_bytes_copied, formatted_total_size); - - - printf("%s\n", current_line); - fflush(stdout); - previous_length = strlen(current_line); + nitro_log_info("Copying file %lu/%lu: %s -> %s (%3d%%, %s/%s)\n", state->files_processed, state->total_files, src, dest, progress, formatted_bytes_copied, formatted_total_size); + fflush(stdout); + last_reported_percentage_block = current_percentage_block; + } } -#if defined(_WIN32) -#else - printf("\n"); -#endif - fclose(src_file); fclose(dest_file); @@ -215,25 +195,25 @@ NitroStatus nitro_copy_directory(NitroCopyState* state, const char* src, const c struct dirent *file_entry; NitroStatus current_status; - dir_to_copy = compat_opendir(src); + dir_to_copy = nitro_file_opendir(src); if(!dir_to_copy) { - fprintf(stderr, "Failed to open src dir \"%s\": %d (%s)\n", src, errno, strerror(errno)); + nitro_log_error("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)); + if(nitro_file_stat(dest, &file_stats) != NITRO_SUCCESS) { + if(nitro_file_mkdir(dest, 0755) == -1) { + nitro_file_closedir(dir_to_copy); + nitro_log_error("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); + nitro_file_closedir(dir_to_copy); + nitro_log_error("Destination path \"%s\" is not a directory\n", dest); return NITRO_ERROR_INVALID_PATH; } - while ((file_entry = compat_readdir(dir_to_copy)) != NULL) { + while ((file_entry = nitro_file_readdir(dir_to_copy)) != NULL) { char entry_source_path[1024]; char entry_destination_path[1024]; @@ -245,8 +225,8 @@ NitroStatus nitro_copy_directory(NitroCopyState* state, const char* src, const c 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); + if(nitro_file_stat(entry_source_path, &file_stats) != NITRO_SUCCESS) { + nitro_log_error("Warning: Could not get info for '%s', skipping.\n", src); continue; } @@ -261,35 +241,35 @@ NitroStatus nitro_copy_directory(NitroCopyState* state, const char* src, const c } } - compat_closedir(dir_to_copy); + nitro_file_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]; + 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)); + if(nitro_file_stat(src, &file_stats) != NITRO_SUCCESS) { + nitro_log_error("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); + DIR *dir = nitro_file_opendir(src); if(!dir) { - fprintf(stderr, "Failed to open src dir \"%s\": %d (%s)\n", src, errno, strerror(errno)); + nitro_log_error("Failed to open src dir \"%s\": %d (%s)\n", src, errno, strerror(errno)); return NITRO_ERROR_OPENING_FILE; } - while ((entry = compat_readdir(dir)) != NULL) { + while ((entry = nitro_file_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(nitro_file_stat(sub_path, &file_stats) == 0) { if(S_ISDIR(file_stats.st_mode)) { nitro_get_total_stats(sub_path, total_size, file_count); } else { @@ -299,7 +279,7 @@ NitroStatus nitro_get_total_stats(const char* src, unsigned long* total_size, un } } - compat_closedir(dir); + nitro_file_closedir(dir); } else { *total_size = file_stats.st_size; *file_count += 1; diff --git a/src/core/nitro_logging.c b/src/core/nitro_logging.c new file mode 100644 index 0000000..e84c1ac --- /dev/null +++ b/src/core/nitro_logging.c @@ -0,0 +1,88 @@ +#include +#include +#include "nitro_logging.h" +#include + +#ifndef NITRO_LOG_LEVEL + #define NITRO_LOG_LEVEL NITRO_LOG_LEVEL_DEBUG +#endif + +static void nitro_log_msg(int level, const char* format, va_list args) { + time_t rawtime; + struct tm* info; + char timestamp[30]; + const char* level_str; + + if(level > NITRO_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 NITRO_LOG_LEVEL_FATAL: + level_str = "FATAL"; + break; + case NITRO_LOG_LEVEL_ERROR: + level_str = "ERROR"; + break; + case NITRO_LOG_LEVEL_WARN: + level_str = "WARN"; + break; + case NITRO_LOG_LEVEL_INFO: + level_str = "INFO"; + break; + case NITRO_LOG_LEVEL_DEBUG: + level_str = "DEBUG"; + break; + } + + printf("[%s] [%5s] - ", timestamp, level_str); + vprintf(format, args); +} + +void nitro_log_fatal(const char* format, ...) { + va_list args; + va_start(args, format); + nitro_log_msg(NITRO_LOG_LEVEL_FATAL, format, args); + va_end(args); +} + +void nitro_log_error(const char* format, ...) { + va_list args; + va_start(args, format); + nitro_log_msg(NITRO_LOG_LEVEL_ERROR, format, args); + va_end(args); +} + +void nitro_log_warn(const char* format, ...) { + va_list args; + va_start(args, format); + nitro_log_msg(NITRO_LOG_LEVEL_WARN, format, args); + va_end(args); +} + +void nitro_log_info(const char* format, ...) { + va_list args; + va_start(args, format); + nitro_log_msg(NITRO_LOG_LEVEL_INFO, format, args); + va_end(args); +} + +void nitro_log_debug(const char* format, ...) { + va_list args; + va_start(args, format); + nitro_log_msg(NITRO_LOG_LEVEL_DEBUG, format, args); + va_end(args); +} + diff --git a/src/nitro_runtime.c b/src/core/nitro_runtime.c similarity index 100% rename from src/nitro_runtime.c rename to src/core/nitro_runtime.c 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..eb2e6b0 --- /dev/null +++ b/src/platform/linux/CMakeLists.txt @@ -0,0 +1,8 @@ +add_library(nitro_compat_linux STATIC + nitro_console.c + nitro_file.c +) + +target_include_directories(nitro_compat_linux + PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} +) diff --git a/src/platform/linux/nitro_console.c b/src/platform/linux/nitro_console.c new file mode 100644 index 0000000..a100fbe --- /dev/null +++ b/src/platform/linux/nitro_console.c @@ -0,0 +1,29 @@ +#include "nitro_console.h" + +#include +#include +#include +#include + +int nitro_console_get_width(void) { + struct winsize ws; + + if(ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == -1) { + perror("ioctl"); + return 1; + } + + return ws.ws_col; +} + +void nitro_console_move_cursor_up(int rows) { + int row; + for(row = 0; row < rows; row++) { + printf("\033[A"); + } +} + +void nitro_console_clear_from_cursor_down(void) { + printf("\033[J"); +} + diff --git a/src/platform/linux/nitro_file.c b/src/platform/linux/nitro_file.c new file mode 100644 index 0000000..2ce35e3 --- /dev/null +++ b/src/platform/linux/nitro_file.c @@ -0,0 +1,36 @@ +#include "nitro_file.h" + +#include +#include + +DIR* nitro_file_opendir(const char* path) { + return opendir(path); +} + +struct dirent* nitro_file_readdir(DIR* dir) { + if(!dir) { + return NULL; + } + + return readdir(dir); +} + +int nitro_file_closedir(DIR* dir) { + if(!dir) { + return -1; + } + + return closedir(dir); +} + +int nitro_file_mkdir(const char* path, int mode) { + return mkdir(path, mode); +} + +int nitro_file_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..ad2ba9d --- /dev/null +++ b/src/platform/win9x/CMakeLists.txt @@ -0,0 +1,8 @@ +add_library(nitro_compat_win9x STATIC + nitro_console.c + nitro_file.c +) + +target_include_directories(nitro_compat_win9x + PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} +) diff --git a/src/nitro_console_compat.c b/src/platform/win9x/nitro_console.c similarity index 61% rename from src/nitro_console_compat.c rename to src/platform/win9x/nitro_console.c index 6f73e70..4ea70ec 100644 --- a/src/nitro_console_compat.c +++ b/src/platform/win9x/nitro_console.c @@ -1,16 +1,11 @@ +#include "nitro_console.h" + #include #include #include -#include "nitro_console_compat.h" - -#if defined(_WIN32) - #include -#else - #include -#endif +#include -static int _nitro_get_console_width(void) { -#if defined(_WIN32) +int nitro_console_get_width(void) { HANDLE hConsole; CONSOLE_SCREEN_BUFFER_INFO csbi; int rows; @@ -25,20 +20,9 @@ static int _nitro_get_console_width(void) { } return csbi.srWindow.Right - csbi.srWindow.Left + 1; -#else - struct winsize ws; - - if(ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == -1) { - perror("ioctl"); - return 1; - } - - return ws.ws_col; -#endif } -static void _nitro_move_cursor_up(int rows) { -#if defined(_WIN32) +void nitro_console_move_cursor_up(int rows) { HANDLE hConsole; CONSOLE_SCREEN_BUFFER_INFO csbi; COORD cursor; @@ -59,16 +43,9 @@ static void _nitro_move_cursor_up(int rows) { } SetConsoleCursorPosition(hConsole, cursor); -#else - int row; - for(row = 0; row < rows; row++) { - printf("\033[A"); - } -#endif } -static void _nitro_clear_from_cursor_down(void) { -#if defined(_WIN32) +void nitro_console_clear_from_cursor_down(void) { HANDLE hConsole; CONSOLE_SCREEN_BUFFER_INFO csbi; COORD cursor; @@ -89,20 +66,4 @@ static void _nitro_clear_from_cursor_down(void) { FillConsoleOutputCharacter(hConsole, ' ', cells, cursor, &written); FillConsoleOutputAttribute(hConsole, csbi.wAttributes, cells, cursor, &written); -#else - printf("\033[J"); -#endif -} - -void nitro_clear_line(int previous_line_length) { - int line_width; - int rows; - - line_width = _nitro_get_console_width(); - - if(previous_line_length > 0) { - rows = (previous_line_length + line_width - 1) / line_width; - _nitro_move_cursor_up(rows); - _nitro_clear_from_cursor_down(); - } } diff --git a/src/compat.c b/src/platform/win9x/nitro_file.c similarity index 62% rename from src/compat.c rename to src/platform/win9x/nitro_file.c index 9b0747a..fc80528 100644 --- a/src/compat.c +++ b/src/platform/win9x/nitro_file.c @@ -1,16 +1,13 @@ -#include "compat.h" +#include "nitro_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* nitro_file_opendir(const char* path) { DIR* dir; char search_path[MAX_PATH]; @@ -27,17 +24,13 @@ DIR* compat_opendir(const char* path) { return NULL; } return dir; -#else - return opendir(path); -#endif } -struct dirent* compat_readdir(DIR* dir) { +struct dirent* nitro_file_readdir(DIR* dir) { if(!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) { +int nitro_file_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) +int nitro_file_mkdir(const char* path, int mode) { return mkdir(path); -#else - return mkdir(path, mode); -#endif +} + +int nitro_file_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; } From c8ec7ef310c44f61cf9a0efcf6fafb3123b33c02 Mon Sep 17 00:00:00 2001 From: Maritim Date: Fri, 26 Sep 2025 23:34:50 +0200 Subject: [PATCH 6/9] Some refactoring --- CMakeLists.txt | 6 +- include/console.h | 10 + include/copier.h | 14 + include/copy_process.h | 36 ++ include/file.h | 33 ++ include/libnitro.h | 34 -- include/logging.h | 20 ++ include/nitro_console.h | 10 - include/nitro_file.h | 35 -- include/nitro_logging.h | 20 -- include/nitro_runtime.h | 11 - src/CMakeLists.txt | 20 +- src/app/CMakeLists.txt | 10 - src/copier.c | 278 +++++++++++++++ src/copy_process.c | 80 +++++ src/core/CMakeLists.txt | 1 - src/core/libnitro.c | 333 ------------------ src/core/nitro_runtime.c | 3 - src/{core/nitro_logging.c => logging.c} | 43 +-- src/{app => }/main.c | 13 +- src/platform/linux/CMakeLists.txt | 10 +- .../linux/{nitro_console.c => console.c} | 8 +- src/platform/linux/{nitro_file.c => file.c} | 12 +- src/platform/win9x/CMakeLists.txt | 10 +- .../win9x/{nitro_console.c => console.c} | 9 +- src/platform/win9x/{nitro_file.c => file.c} | 12 +- tests/CMakeLists.txt | 4 +- tests/cursor_test.c | 16 - ...t_should_return_with_initialised_members.c | 14 + tests/tests.c | 0 30 files changed, 561 insertions(+), 544 deletions(-) create mode 100644 include/console.h create mode 100644 include/copier.h create mode 100644 include/copy_process.h create mode 100644 include/file.h delete mode 100644 include/libnitro.h create mode 100644 include/logging.h delete mode 100644 include/nitro_console.h delete mode 100644 include/nitro_file.h delete mode 100644 include/nitro_logging.h delete mode 100644 include/nitro_runtime.h delete mode 100644 src/app/CMakeLists.txt create mode 100644 src/copier.c create mode 100644 src/copy_process.c delete mode 100644 src/core/CMakeLists.txt delete mode 100644 src/core/libnitro.c delete mode 100644 src/core/nitro_runtime.c rename src/{core/nitro_logging.c => logging.c} (54%) rename src/{app => }/main.c (92%) rename src/platform/linux/{nitro_console.c => console.c} (68%) rename src/platform/linux/{nitro_file.c => file.c} (55%) rename src/platform/win9x/{nitro_console.c => console.c} (88%) rename src/platform/win9x/{nitro_file.c => file.c} (84%) delete mode 100644 tests/cursor_test.c create mode 100644 tests/test_copy_process_init_should_return_with_initialised_members.c delete mode 100644 tests/tests.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 04154af..fc561d5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -117,8 +117,8 @@ add_custom_target(tag_release # ----------------------------------------------- # Tests # ----------------------------------------------- -#include(CTest) -#enable_testing() +include(CTest) +enable_testing() # Add tests subdirectory -#add_subdirectory(tests) +add_subdirectory(tests) 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..b912901 --- /dev/null +++ b/include/copier.h @@ -0,0 +1,14 @@ +#ifndef COPIER_H +#define COPIER_H + +#include "copy_process.h" + +int copier_copy(const char* src, const char* dest, copy_process_t* state); + +int copier_copy_file(const char* src, const char* dest, copy_process_t* state); + +int copier_copy_dir(const char* src_dir, const char* dest_dir, copy_process_t* state); + +int copier_get_total_stats(const char* path, copy_process_t* state); + +#endif diff --git a/include/copy_process.h b/include/copy_process.h new file mode 100644 index 0000000..789725f --- /dev/null +++ b/include/copy_process.h @@ -0,0 +1,36 @@ +#ifndef COPY_PROCESS_H +#define COPY_PROCESS_H + +#include + +typedef struct copy_process_t copy_process_t; + +/* Getters */ +size_t copy_process_get_total_size(const copy_process_t* state); + +size_t copy_process_get_total_files(const copy_process_t* state); + +size_t copy_process_get_bytes_copied(const copy_process_t* state); + +size_t copy_process_get_files_processed(const copy_process_t* state); + +unsigned int copy_process_get_overwrite(const copy_process_t* state); + +unsigned int copy_process_get_progress(const copy_process_t* state); + +/* Mutators */ +void copy_process_increase_total_size(copy_process_t* state, size_t size); + +void copy_process_increase_total_files(copy_process_t* state, size_t files); + +void copy_process_increase_bytes_copied(copy_process_t* state, size_t bytes_copied); + +void copy_process_increment_files_processed(copy_process_t* state); + +/* Constructor */ +copy_process_t* copy_process_init(unsigned int overwrite); + +/* Destructor */ +void copy_process_destroy(copy_process_t* state); + +#endif diff --git a/include/file.h b/include/file.h new file mode 100644 index 0000000..8b9aa7e --- /dev/null +++ b/include/file.h @@ -0,0 +1,33 @@ +#ifndef FILE_COMPAT_H +#define FILE_COMPAT_H + +#include + +#if defined(_WIN32) +#include + +struct dirent { + char d_name[MAX_PATH]; +}; + +typedef struct { + HANDLE handle; + WIN32_FIND_DATA find_data; + int is_first_entry; + struct dirent entry; +} DIR; +#else +#include +#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_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.h b/include/nitro_console.h deleted file mode 100644 index 9d6d294..0000000 --- a/include/nitro_console.h +++ /dev/null @@ -1,10 +0,0 @@ -#ifndef NITRO_CONSOLE_COMPAT_H -#define NITRO_CONSOLE_COMPAT_H - -int nitro_console_get_width(void); - -void nitro_console_move_cursor_up(int rows); - -void nitro_console_clear_from_cursor_down(void); - -#endif diff --git a/include/nitro_file.h b/include/nitro_file.h deleted file mode 100644 index d682b98..0000000 --- a/include/nitro_file.h +++ /dev/null @@ -1,35 +0,0 @@ -#ifndef NITRO_FILE_COMPAT_H -#define NITRO_FILE_COMPAT_H - -#include -#include - - -#if defined(_WIN32) -#include - -struct dirent { - char d_name[MAX_PATH]; -}; - -typedef struct { - HANDLE handle; - WIN32_FIND_DATA find_data; - int is_first_entry; - struct dirent entry; -} DIR; -#else -#include -#endif - -DIR* nitro_file_opendir(const char* path); - -struct dirent* nitro_file_readdir(DIR* dir); - -int nitro_file_closedir(DIR* dir); - -int nitro_file_mkdir(const char* path, int mode); - -int nitro_file_stat(const char* path, struct stat* file_stats); - -#endif diff --git a/include/nitro_logging.h b/include/nitro_logging.h deleted file mode 100644 index cfd2d44..0000000 --- a/include/nitro_logging.h +++ /dev/null @@ -1,20 +0,0 @@ -#ifndef NITRO_LOGGING_H -#define NITRO_LOGGING_H - -#define NITRO_LOG_LEVEL_FATAL 1 -#define NITRO_LOG_LEVEL_ERROR 2 -#define NITRO_LOG_LEVEL_WARN 3 -#define NITRO_LOG_LEVEL_INFO 4 -#define NITRO_LOG_LEVEL_DEBUG 5 - -void nitro_log_fatal(const char* format, ...); - -void nitro_log_error(const char* format, ...); - -void nitro_log_warn(const char* format, ...); - -void nitro_log_info(const char* format, ...); - -void nitro_log_debug(const char* format, ...); - -#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/src/CMakeLists.txt b/src/CMakeLists.txt index 61a6e5e..5246a61 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,3 +1,19 @@ -add_subdirectory(core) add_subdirectory(platform) -add_subdirectory(app) + +add_library(logging STATIC logging.c) + +add_library(copier STATIC + copy_process.c + copier.c +) +target_link_libraries(copier PUBLIC + console + file + logging + m +) + +add_executable(NitroCopy main.c) +target_link_libraries(NitroCopy PUBLIC + copier +) diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt deleted file mode 100644 index 3c936c4..0000000 --- a/src/app/CMakeLists.txt +++ /dev/null @@ -1,10 +0,0 @@ -add_executable(NitroCopy main.c) - -target_link_libraries(NitroCopy PRIVATE core m) - -if(WIN32) - target_link_libraries(NitroCopy PRIVATE nitro_compat_win9x) - target_link_options(NitroCopy PRIVATE -static -static-libgcc -static-libstdc++) -elseif(UNIX) - target_link_libraries(NitroCopy PRIVATE nitro_compat_linux) -endif() diff --git a/src/copier.c b/src/copier.c new file mode 100644 index 0000000..eeda9b8 --- /dev/null +++ b/src/copier.c @@ -0,0 +1,278 @@ +#include "copier.h" +#include "copy_process.h" +#include "file.h" +#include "logging.h" + +#include +#include +#include +#include +#include +#include +#include + +#define BUFFER_SIZE 4096 +#define ERROR_MSG_SIZE 256 + +static char* copier_format_bytes(size_t bytes) { + char* s = (char*)malloc(sizeof(char) * 20); + const char* suffixes[] = {"B", "KB", "MB", "GB"}; + int i = 0; + double value; + + if(bytes == 0) { + sprintf(s, "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); + sprintf(s, "%.2f %s", value, suffixes[i]); + + return s; +} + +int copier_copy(const char* src, const char* dest, copy_process_t* copy_process) { + struct stat file_stats; + char options[64]; + const char* formatted_size; + const char* formatted_bytes_copied; + + if(compat_stat(src, &file_stats) != 0) { + log_fatal("Failed to open src file \"%s\": %d (%s)\n", src, errno, strerror(errno)); + return 1; + } + + if(copy_process_get_overwrite(copy_process) == 1) { + sprintf(options, "overwrite = yes"); + } else { + sprintf(options, "overwrite = no"); + } + + log_info("Counting total number of files to copy...\n"); + copier_get_total_stats(src, copy_process); + formatted_size = copier_format_bytes(copy_process_get_total_size(copy_process)); + log_info("Counted %lu files (%s)\n", copy_process_get_total_files(copy_process), formatted_size); + + if(S_ISDIR(file_stats.st_mode)) { + int status = copier_copy_dir(src, dest, copy_process); + if(status != 0) { + return status; + } + } else { + int status = copier_copy_file(src, dest, copy_process); + if(status != 0) { + return status; + } + } + + formatted_bytes_copied = copier_format_bytes(copy_process_get_bytes_copied(copy_process)); + + log_info("Finished copying files: %lu/%lu (%s/%s, %s)\n", copy_process_get_files_processed(copy_process), copy_process_get_total_files(copy_process), formatted_bytes_copied, formatted_size, options); + + return 0; +} + +int copier_copy_file(const char* src, const char* dest, copy_process_t* state) { + struct stat file_stats; + FILE *src_file; + FILE *dest_file; + char buffer[BUFFER_SIZE]; + size_t bytes_read; + size_t total_bytes_read; + time_t start_time; + + src_file = fopen(src, "rb"); + if(!src_file) { + log_error("Failed to open src file: \"%s\": %d (%s)\n", src, errno, strerror(errno)); + return 1; + } + + if(compat_stat(dest, &file_stats) == 0) { + if(S_ISDIR(file_stats.st_mode)) { + fclose(src_file); + log_error("Destination path \"%s\" is a directory\n", dest); + return 2; + } + + if(!copy_process_get_overwrite(state)) { + 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 0; + } + } + printf("Invalid input. Please enter 'y' or 'n'.\n"); + } + } + } + + dest_file = fopen(dest, "wb"); + if(!dest_file) { + fclose(src_file); + log_error("Failed to open or create destination file \"%s\": %d (%s)\n", dest, errno, strerror(errno)); + return 1; + } + + copy_process_increment_files_processed(state); + total_bytes_read = 0; + start_time = time(NULL); + + while ((bytes_read = fread(buffer, 1, BUFFER_SIZE, src_file)) > 0) { + unsigned int progress; + const char* formatted_bytes_copied; + const char* formatted_total_size; + time_t now; + + if(fwrite(buffer, 1, bytes_read, dest_file) != bytes_read) { + log_error("Failed to write to destination file \"%s\": %d (%s)\n", dest, errno, strerror(errno)); + break; + } + + copy_process_increase_bytes_copied(state, bytes_read); + total_bytes_read += bytes_read; + progress = (int)((double) total_bytes_read / file_stats.st_size * 100); + now = time(NULL); + + if(difftime(now, start_time) >= 10.0 || progress == 100.0) { + formatted_bytes_copied = copier_format_bytes(copy_process_get_bytes_copied(state)); + formatted_total_size = copier_format_bytes(copy_process_get_total_size(state)); + + log_info("Copying file %lu/%lu: %s -> %s (%3d%%, %s/%s)\n", copy_process_get_files_processed(state), copy_process_get_total_files(state), src, dest, progress, formatted_bytes_copied, formatted_total_size); + fflush(stdout); + } + } + + fclose(src_file); + fclose(dest_file); + + return 0; +} + +int copier_copy_dir(const char* src, const char* dest, copy_process_t* state) { + DIR *dir_to_copy; + struct stat file_stats; + struct dirent *file_entry; + int current_status; + + dir_to_copy = compat_opendir(src); + if(!dir_to_copy) { + log_error("Failed to open src dir \"%s\": %d (%s)\n", src, errno, strerror(errno)); + return 3; + } + + if(compat_stat(dest, &file_stats) != 0) { + if(compat_mkdir(dest, 0755) == -1) { + compat_closedir(dir_to_copy); + log_error("Failed to create destination directdory \"%s\": %d (%s)\n", dest, errno, strerror(errno)); + return 4; + } + } else if(!S_ISDIR(file_stats.st_mode)) { + compat_closedir(dir_to_copy); + log_error("Destination path \"%s\" is not a directory\n", dest); + return 5; + } + + 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; + } + + sprintf(entry_source_path, "%s/%s", src, file_entry->d_name); + sprintf(entry_destination_path, "%s/%s", dest, file_entry->d_name); + + if(compat_stat(entry_source_path, &file_stats) != 0) { + log_error("Warning: Could not get info for '%s', skipping.\n", src); + continue; + } + + if(S_ISDIR(file_stats.st_mode)) { + current_status = copier_copy_dir(entry_source_path, entry_destination_path, state); + } else { + current_status = copier_copy_file(entry_source_path, entry_destination_path, state); + } + + if(current_status != 0) { + break; + } + } + + compat_closedir(dir_to_copy); + return current_status; +} + +/** + * copy_get_total_stats: Populate the copy process with the total number of files and bytes to copy. + * @src The path to the file or dir being traversed. + * @copy_process A pointer to the current copy process (cannot be NULL). + * + * Returns a code indicating the outcome of the operation: + * 0 = Success. + * 1 = Copy process pointer is NULL. + * 2 = Failed to acquire stats for src. The resource probably doesn't exist. + * 3 = Failed to open src dir. + */ +int copier_get_total_stats(const char* src, copy_process_t* copy_process) { + struct stat file_stats; + struct dirent* entry; + char sub_path[1024]; + + if(!copy_process) { + return 1; + } + + if(compat_stat(src, &file_stats) != 0) { + return 2; + } + + if(S_ISDIR(file_stats.st_mode)) { + DIR *dir = compat_opendir(src); + if(!dir) { + return 3; + } + + + 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, &file_stats) == 0) { + if(S_ISDIR(file_stats.st_mode)) { + copier_get_total_stats(sub_path, copy_process); + } else { + copy_process_increase_total_size(copy_process, file_stats.st_size); + copy_process_increase_total_files(copy_process, 1); + } + } + } + + compat_closedir(dir); + } else { + copy_process_increase_total_size(copy_process, file_stats.st_size); + copy_process_increase_total_files(copy_process, 1); + } + + return 0; +} + diff --git a/src/copy_process.c b/src/copy_process.c new file mode 100644 index 0000000..1c6b977 --- /dev/null +++ b/src/copy_process.c @@ -0,0 +1,80 @@ +#include "copy_process.h" + +#include + +struct copy_process_t { + size_t total_size; + size_t total_files; + size_t bytes_copied; + size_t files_processed; + unsigned int overwrite; +}; + +size_t copy_process_get_total_size(const copy_process_t* state) { + return state ? state->total_size : 0; +} + +size_t copy_process_get_total_files(const copy_process_t* state) { + return state ? state->total_files : 0; +} + +size_t copy_process_get_bytes_copied(const copy_process_t* state) { + return state ? state->bytes_copied : 0; +} + +size_t copy_process_get_files_processed(const copy_process_t* state) { + return state ? state->files_processed : 0; +} + +unsigned int copy_process_get_overwrite(const copy_process_t* state) { + return state ? state->overwrite : 0; +} + +unsigned int copy_process_get_progress(const copy_process_t* state) { + return state && state->total_size > 0 ? (unsigned int)((double) state->bytes_copied / state->total_size * 100) : 0; +} + +void copy_process_increase_total_size(copy_process_t* state, size_t size) { + if(state) { + state->total_size += size; + } +} + +void copy_process_increase_total_files(copy_process_t* state, size_t files) { + if(state) { + state->total_files += files; + } +} + +void copy_process_increase_bytes_copied(copy_process_t* state, size_t bytes_copied) { + if(state) { + state->bytes_copied += bytes_copied; + } +} + +void copy_process_increment_files_processed(copy_process_t* state) { + if(state) { + state->files_processed++; + } +} + +copy_process_t* copy_process_init(unsigned int overwrite) { + copy_process_t* state = malloc(sizeof(copy_process_t)); + if(!state) { + return NULL; + } + + state->total_size = 0; + state->total_files = 0; + state->bytes_copied = 0; + state->files_processed = 0; + state->overwrite = overwrite; + + return state; +} + +void copy_process_destroy(copy_process_t* state) { + if(state) { + free(state); + } +} diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt deleted file mode 100644 index b85fa45..0000000 --- a/src/core/CMakeLists.txt +++ /dev/null @@ -1 +0,0 @@ -add_library(core STATIC libnitro.c nitro_logging.c nitro_runtime.c) diff --git a/src/core/libnitro.c b/src/core/libnitro.c deleted file mode 100644 index 22faa34..0000000 --- a/src/core/libnitro.c +++ /dev/null @@ -1,333 +0,0 @@ -#include "nitro_console.h" -#include "nitro_file.h" -#include "nitro_logging.h" -#include "libnitro.h" - -#include -#include -#include -#include -#include -#include - -#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]; -}; - -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. */ - - return state; -} - -void nitro_destroy(NitroCopyState* state) { - if(state != NULL) { - free(state); - } -} - -void nitro_clear_line(int previous_line_length) { - int line_width; - int rows; - - line_width = nitro_console_get_width(); - - if(previous_line_length > 0) { - rows = (previous_line_length + line_width - 1) / line_width; - nitro_console_move_cursor_up(rows); - nitro_console_clear_from_cursor_down(); - } -} - -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_file_stat(src, &file_stats) != 0) { - nitro_log_fatal("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_log_info("Counting total number of files to copy...\n"); - nitro_get_total_stats(src, &state->total_size, &state->total_files); - formatted_size = nitro_format_bytes(state->total_size); - nitro_log_info("Counted %lu files (%s)\n", state->total_files, formatted_size); - - 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; - } - } - - formatted_bytes_copied = nitro_format_bytes(state->bytes_copied); - - nitro_log_info("Finished copying files: %lu/%lu (%s/%s, %s)\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; - size_t total_bytes_read; - int last_reported_percentage_block; - int current_percentage_block; - int percentage_block_divisor; - int foo; - - src_file = fopen(src, "rb"); - if(!src_file) { - nitro_log_error("Failed to open src file: \"%s\": %d (%s)\n", src, errno, strerror(errno)); - return NITRO_ERROR_OPENING_FILE; - } - - if(nitro_file_stat(dest, &file_stats) == 0) { - if(S_ISDIR(file_stats.st_mode)) { - fclose(src_file); - nitro_log_error("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); - nitro_log_error("Failed to open or create destination file \"%s\": %d (%s)\n", dest, errno, strerror(errno)); - return NITRO_ERROR_OPENING_FILE; - } - - state->files_processed++; - last_reported_percentage_block = -1; - total_bytes_read = 0; - foo = (file_stats.st_size / BUFFER_SIZE) / BUFFER_SIZE; - percentage_block_divisor = 100 / foo; - - 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) { - nitro_log_error("Failed to write to destination file \"%s\": %d (%s)\n", dest, errno, strerror(errno)); - break; - } - - state->bytes_copied += bytes_read; - total_bytes_read += bytes_read; - progress = (int)((double) total_bytes_read / file_stats.st_size * 100); - current_percentage_block = progress / percentage_block_divisor; - - if(current_percentage_block > last_reported_percentage_block) { - formatted_bytes_copied = nitro_format_bytes(state->bytes_copied); - formatted_total_size = nitro_format_bytes(state->total_size); - - nitro_log_info("Copying file %lu/%lu: %s -> %s (%3d%%, %s/%s)\n", state->files_processed, state->total_files, src, dest, progress, formatted_bytes_copied, formatted_total_size); - fflush(stdout); - last_reported_percentage_block = current_percentage_block; - } - } - - 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 = nitro_file_opendir(src); - if(!dir_to_copy) { - nitro_log_error("Failed to open src dir \"%s\": %d (%s)\n", src, errno, strerror(errno)); - return NITRO_ERROR_INVALID_PATH; - } - - if(nitro_file_stat(dest, &file_stats) != NITRO_SUCCESS) { - if(nitro_file_mkdir(dest, 0755) == -1) { - nitro_file_closedir(dir_to_copy); - nitro_log_error("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)) { - nitro_file_closedir(dir_to_copy); - nitro_log_error("Destination path \"%s\" is not a directory\n", dest); - return NITRO_ERROR_INVALID_PATH; - } - - while ((file_entry = nitro_file_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_file_stat(entry_source_path, &file_stats) != NITRO_SUCCESS) { - nitro_log_error("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; - } - } - - nitro_file_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_file_stat(src, &file_stats) != NITRO_SUCCESS) { - nitro_log_error("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 = nitro_file_opendir(src); - if(!dir) { - nitro_log_error("Failed to open src dir \"%s\": %d (%s)\n", src, errno, strerror(errno)); - return NITRO_ERROR_OPENING_FILE; - } - - - while ((entry = nitro_file_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_file_stat(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; - } - } - } - - nitro_file_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); - - - nitro_clear_line(0); - - printf("\rTotal progress: %d%% (%s/%s)\n", progress, formatted_bytes_copied, formatted_total_size); - 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/core/nitro_runtime.c b/src/core/nitro_runtime.c deleted file mode 100644 index 6eb6d93..0000000 --- a/src/core/nitro_runtime.c +++ /dev/null @@ -1,3 +0,0 @@ -#include "nitro_runtime.h" - -int nitro_verbose = 0; diff --git a/src/core/nitro_logging.c b/src/logging.c similarity index 54% rename from src/core/nitro_logging.c rename to src/logging.c index e84c1ac..198a57f 100644 --- a/src/core/nitro_logging.c +++ b/src/logging.c @@ -1,19 +1,20 @@ +#include "logging.h" + #include #include -#include "nitro_logging.h" #include -#ifndef NITRO_LOG_LEVEL - #define NITRO_LOG_LEVEL NITRO_LOG_LEVEL_DEBUG +#ifndef LOG_LEVEL + #define LOG_LEVEL LOG_LEVEL_DEBUG #endif -static void nitro_log_msg(int level, const char* format, va_list args) { +static void log_msg(int level, const char* format, va_list args) { time_t rawtime; struct tm* info; - char timestamp[30]; + char timestamp[72]; const char* level_str; - if(level > NITRO_LOG_LEVEL) { + if(level > LOG_LEVEL) { return; } @@ -30,19 +31,19 @@ static void nitro_log_msg(int level, const char* format, va_list args) { ); switch(level) { - case NITRO_LOG_LEVEL_FATAL: + case LOG_LEVEL_FATAL: level_str = "FATAL"; break; - case NITRO_LOG_LEVEL_ERROR: + case LOG_LEVEL_ERROR: level_str = "ERROR"; break; - case NITRO_LOG_LEVEL_WARN: + case LOG_LEVEL_WARN: level_str = "WARN"; break; - case NITRO_LOG_LEVEL_INFO: + case LOG_LEVEL_INFO: level_str = "INFO"; break; - case NITRO_LOG_LEVEL_DEBUG: + case LOG_LEVEL_DEBUG: level_str = "DEBUG"; break; } @@ -51,38 +52,38 @@ static void nitro_log_msg(int level, const char* format, va_list args) { vprintf(format, args); } -void nitro_log_fatal(const char* format, ...) { +void log_fatal(const char* format, ...) { va_list args; va_start(args, format); - nitro_log_msg(NITRO_LOG_LEVEL_FATAL, format, args); + log_msg(LOG_LEVEL_FATAL, format, args); va_end(args); } -void nitro_log_error(const char* format, ...) { +void log_error(const char* format, ...) { va_list args; va_start(args, format); - nitro_log_msg(NITRO_LOG_LEVEL_ERROR, format, args); + log_msg(LOG_LEVEL_ERROR, format, args); va_end(args); } -void nitro_log_warn(const char* format, ...) { +void log_warn(const char* format, ...) { va_list args; va_start(args, format); - nitro_log_msg(NITRO_LOG_LEVEL_WARN, format, args); + log_msg(LOG_LEVEL_WARN, format, args); va_end(args); } -void nitro_log_info(const char* format, ...) { +void log_info(const char* format, ...) { va_list args; va_start(args, format); - nitro_log_msg(NITRO_LOG_LEVEL_INFO, format, args); + log_msg(LOG_LEVEL_INFO, format, args); va_end(args); } -void nitro_log_debug(const char* format, ...) { +void log_debug(const char* format, ...) { va_list args; va_start(args, format); - nitro_log_msg(NITRO_LOG_LEVEL_DEBUG, format, args); + log_msg(LOG_LEVEL_DEBUG, format, args); va_end(args); } diff --git a/src/app/main.c b/src/main.c similarity index 92% rename from src/app/main.c rename to src/main.c index 85817c1..d590640 100644 --- a/src/app/main.c +++ b/src/main.c @@ -1,17 +1,18 @@ +#include "copier.h" +#include "nitro_version.h" +#include "copy_process.h" + #include #include #include #include -#include "libnitro.h" -#include "nitro_logging.h" -#include "nitro_version.h" int main(int argc, char *argv[]) { int opt; int overwrite = 0; char* src_path = NULL; char* dest_path = NULL; - NitroCopyState* state = NULL; + copy_process_t* state = NULL; static struct option long_options[] = { {"overwrite", no_argument, 0, 'o'}, @@ -62,10 +63,10 @@ int main(int argc, char *argv[]) { printf("NitroCopy %s - (overwrite: %d)\n\n", NITRO_VERSION_STRING, overwrite); - state = nitro_init(overwrite); + state = copy_process_init(overwrite); if(state == NULL) { return 1; } - return nitro_copy(state, src_path, dest_path); + return copier_copy(src_path, dest_path, state); } diff --git a/src/platform/linux/CMakeLists.txt b/src/platform/linux/CMakeLists.txt index eb2e6b0..7a3643d 100644 --- a/src/platform/linux/CMakeLists.txt +++ b/src/platform/linux/CMakeLists.txt @@ -1,8 +1,2 @@ -add_library(nitro_compat_linux STATIC - nitro_console.c - nitro_file.c -) - -target_include_directories(nitro_compat_linux - PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} -) +add_library(console STATIC console.c) +add_library(file STATIC file.c) diff --git a/src/platform/linux/nitro_console.c b/src/platform/linux/console.c similarity index 68% rename from src/platform/linux/nitro_console.c rename to src/platform/linux/console.c index a100fbe..f1fe443 100644 --- a/src/platform/linux/nitro_console.c +++ b/src/platform/linux/console.c @@ -1,11 +1,11 @@ -#include "nitro_console.h" +#include "console.h" #include #include #include #include -int nitro_console_get_width(void) { +int console_get_width(void) { struct winsize ws; if(ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == -1) { @@ -16,14 +16,14 @@ int nitro_console_get_width(void) { return ws.ws_col; } -void nitro_console_move_cursor_up(int rows) { +void console_move_cursor_up(int rows) { int row; for(row = 0; row < rows; row++) { printf("\033[A"); } } -void nitro_console_clear_from_cursor_down(void) { +void console_clear_from_cursor_down(void) { printf("\033[J"); } diff --git a/src/platform/linux/nitro_file.c b/src/platform/linux/file.c similarity index 55% rename from src/platform/linux/nitro_file.c rename to src/platform/linux/file.c index 2ce35e3..270d4cd 100644 --- a/src/platform/linux/nitro_file.c +++ b/src/platform/linux/file.c @@ -1,13 +1,13 @@ -#include "nitro_file.h" +#include "file.h" #include #include -DIR* nitro_file_opendir(const char* path) { +DIR* compat_opendir(const char* path) { return opendir(path); } -struct dirent* nitro_file_readdir(DIR* dir) { +struct dirent* compat_readdir(DIR* dir) { if(!dir) { return NULL; } @@ -15,7 +15,7 @@ struct dirent* nitro_file_readdir(DIR* dir) { return readdir(dir); } -int nitro_file_closedir(DIR* dir) { +int compat_closedir(DIR* dir) { if(!dir) { return -1; } @@ -23,11 +23,11 @@ int nitro_file_closedir(DIR* dir) { return closedir(dir); } -int nitro_file_mkdir(const char* path, int mode) { +int compat_mkdir(const char* path, int mode) { return mkdir(path, mode); } -int nitro_file_stat(const char* path, struct stat* file_stats) { +int compat_stat(const char* path, struct stat* file_stats) { if(stat(path, file_stats) != 0) { return -1; } diff --git a/src/platform/win9x/CMakeLists.txt b/src/platform/win9x/CMakeLists.txt index ad2ba9d..7a3643d 100644 --- a/src/platform/win9x/CMakeLists.txt +++ b/src/platform/win9x/CMakeLists.txt @@ -1,8 +1,2 @@ -add_library(nitro_compat_win9x STATIC - nitro_console.c - nitro_file.c -) - -target_include_directories(nitro_compat_win9x - PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} -) +add_library(console STATIC console.c) +add_library(file STATIC file.c) diff --git a/src/platform/win9x/nitro_console.c b/src/platform/win9x/console.c similarity index 88% rename from src/platform/win9x/nitro_console.c rename to src/platform/win9x/console.c index 4ea70ec..0d84242 100644 --- a/src/platform/win9x/nitro_console.c +++ b/src/platform/win9x/console.c @@ -1,14 +1,13 @@ -#include "nitro_console.h" +#include "console.h" #include #include #include #include -int nitro_console_get_width(void) { +int console_get_width(void) { HANDLE hConsole; CONSOLE_SCREEN_BUFFER_INFO csbi; - int rows; hConsole = GetStdHandle(STD_OUTPUT_HANDLE); if(hConsole == NULL) { @@ -22,7 +21,7 @@ int nitro_console_get_width(void) { return csbi.srWindow.Right - csbi.srWindow.Left + 1; } -void nitro_console_move_cursor_up(int rows) { +void console_move_cursor_up(int rows) { HANDLE hConsole; CONSOLE_SCREEN_BUFFER_INFO csbi; COORD cursor; @@ -45,7 +44,7 @@ void nitro_console_move_cursor_up(int rows) { SetConsoleCursorPosition(hConsole, cursor); } -void nitro_console_clear_from_cursor_down(void) { +void console_clear_from_cursor_down(void) { HANDLE hConsole; CONSOLE_SCREEN_BUFFER_INFO csbi; COORD cursor; diff --git a/src/platform/win9x/nitro_file.c b/src/platform/win9x/file.c similarity index 84% rename from src/platform/win9x/nitro_file.c rename to src/platform/win9x/file.c index fc80528..5a3964f 100644 --- a/src/platform/win9x/nitro_file.c +++ b/src/platform/win9x/file.c @@ -1,4 +1,4 @@ -#include "nitro_file.h" +#include "file.h" #include #include @@ -7,7 +7,7 @@ #include #include -DIR* nitro_file_opendir(const char* path) { +DIR* compat_opendir(const char* path) { DIR* dir; char search_path[MAX_PATH]; @@ -26,7 +26,7 @@ DIR* nitro_file_opendir(const char* path) { return dir; } -struct dirent* nitro_file_readdir(DIR* dir) { +struct dirent* compat_readdir(DIR* dir) { if(!dir) { return NULL; } @@ -46,7 +46,7 @@ struct dirent* nitro_file_readdir(DIR* dir) { return NULL; } -int nitro_file_closedir(DIR* dir) { +int compat_closedir(DIR* dir) { if(!dir) { return -1; } @@ -56,11 +56,11 @@ int nitro_file_closedir(DIR* dir) { return 0; } -int nitro_file_mkdir(const char* path, int mode) { +int compat_mkdir(const char* path, int mode) { return mkdir(path); } -int nitro_file_stat(const char* path, struct stat* file_stats) { +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) { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index bb6da1d..46b7d79 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,6 +1,6 @@ function(add_c_test name) add_executable(${name} ${name}.c) - target_link_libraries(${name} PRIVATE nitro) + target_link_libraries(${name} PRIVATE copier) if(UNIX AND CMAKE_SYSTEM_NAME STREQUAL "Windows") # Cross build: use wine to run Win32 tests @@ -11,4 +11,4 @@ function(add_c_test name) endif() endfunction() -add_c_test(cursor_test) +add_c_test(test_copy_process_init_should_return_with_initialised_members) diff --git a/tests/cursor_test.c b/tests/cursor_test.c deleted file mode 100644 index ddaa892..0000000 --- a/tests/cursor_test.c +++ /dev/null @@ -1,16 +0,0 @@ -#include -#include "nitro_console_compat.h" - -#if defined(_WIN32) -#include -#else -#include -#endif - -int main(void) { - const char* msg = "This is a very long line that should wrap multiple times on the console depending on its width."; - printf("%s", msg); - - /*return vc->line_count == 1 ? 0 : 1;*/ - return 0; -} diff --git a/tests/test_copy_process_init_should_return_with_initialised_members.c b/tests/test_copy_process_init_should_return_with_initialised_members.c new file mode 100644 index 0000000..f49dedb --- /dev/null +++ b/tests/test_copy_process_init_should_return_with_initialised_members.c @@ -0,0 +1,14 @@ +#include "copy_process.h" +#include + +int main(void) { + copy_process_t* copy_process = copy_process_init(0); + + assert(copy_process_get_total_size(copy_process) == 0); + assert(copy_process_get_total_files(copy_process) == 0); + assert(copy_process_get_bytes_copied(copy_process) == 0); + assert(copy_process_get_files_processed(copy_process) == 0); + assert(copy_process_get_overwrite(copy_process) == 0); + + return 0; +} diff --git a/tests/tests.c b/tests/tests.c deleted file mode 100644 index e69de29..0000000 From 1ede6821f8cf7d0f3bb4fd8e578f82fc02613619 Mon Sep 17 00:00:00 2001 From: Maritim Date: Sun, 28 Sep 2025 00:52:46 +0200 Subject: [PATCH 7/9] A bit of refactoring --- CMakeLists.txt | 5 +- include/copier.h | 16 +- include/copy_process.h | 36 -- include/runtime.h | 6 + src/CMakeLists.txt | 1 - src/copier.c | 354 ++++++++++-------- src/copy_process.c | 80 ---- src/main.c | 17 +- tests/CMakeLists.txt | 2 - ...t_should_return_with_initialised_members.c | 14 - 10 files changed, 217 insertions(+), 314 deletions(-) delete mode 100644 include/copy_process.h create mode 100644 include/runtime.h delete mode 100644 src/copy_process.c delete mode 100644 tests/test_copy_process_init_should_return_with_initialised_members.c diff --git a/CMakeLists.txt b/CMakeLists.txt index fc561d5..9b25f15 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -41,8 +41,9 @@ 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 diff --git a/include/copier.h b/include/copier.h index b912901..69e0c94 100644 --- a/include/copier.h +++ b/include/copier.h @@ -1,14 +1,18 @@ #ifndef COPIER_H #define COPIER_H -#include "copy_process.h" - -int copier_copy(const char* src, const char* dest, copy_process_t* state); +#include -int copier_copy_file(const char* src, const char* dest, copy_process_t* state); +typedef void (*ProgressCallback)(void); -int copier_copy_dir(const char* src_dir, const char* dest_dir, copy_process_t* state); +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); -int copier_get_total_stats(const char* path, copy_process_t* state); +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/copy_process.h b/include/copy_process.h deleted file mode 100644 index 789725f..0000000 --- a/include/copy_process.h +++ /dev/null @@ -1,36 +0,0 @@ -#ifndef COPY_PROCESS_H -#define COPY_PROCESS_H - -#include - -typedef struct copy_process_t copy_process_t; - -/* Getters */ -size_t copy_process_get_total_size(const copy_process_t* state); - -size_t copy_process_get_total_files(const copy_process_t* state); - -size_t copy_process_get_bytes_copied(const copy_process_t* state); - -size_t copy_process_get_files_processed(const copy_process_t* state); - -unsigned int copy_process_get_overwrite(const copy_process_t* state); - -unsigned int copy_process_get_progress(const copy_process_t* state); - -/* Mutators */ -void copy_process_increase_total_size(copy_process_t* state, size_t size); - -void copy_process_increase_total_files(copy_process_t* state, size_t files); - -void copy_process_increase_bytes_copied(copy_process_t* state, size_t bytes_copied); - -void copy_process_increment_files_processed(copy_process_t* state); - -/* Constructor */ -copy_process_t* copy_process_init(unsigned int overwrite); - -/* Destructor */ -void copy_process_destroy(copy_process_t* state); - -#endif 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/src/CMakeLists.txt b/src/CMakeLists.txt index 5246a61..e793dd0 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -3,7 +3,6 @@ add_subdirectory(platform) add_library(logging STATIC logging.c) add_library(copier STATIC - copy_process.c copier.c ) target_link_libraries(copier PUBLIC diff --git a/src/copier.c b/src/copier.c index eeda9b8..9f31ab1 100644 --- a/src/copier.c +++ b/src/copier.c @@ -1,7 +1,7 @@ #include "copier.h" -#include "copy_process.h" #include "file.h" #include "logging.h" +#include "runtime.h" #include #include @@ -12,17 +12,15 @@ #include #define BUFFER_SIZE 4096 -#define ERROR_MSG_SIZE 256 -static char* copier_format_bytes(size_t bytes) { - char* s = (char*)malloc(sizeof(char) * 20); +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(s, "0 B"); - return s; + sprintf(buffer, "0 B"); + return; } i = (int)floor(log(bytes) / log(1024)); @@ -33,54 +31,158 @@ static char* copier_format_bytes(size_t bytes) { } value = (double)bytes / pow(1024, i); - sprintf(s, "%.2f %s", value, suffixes[i]); + sprintf(buffer, "%.2f %s", value, suffixes[i]); +} + +/** + * copier_overwrite: Checks whether a file can be and will be overwritten. + * @dest: The path to the file that may or may not be overwritten. + * @copy_process: A pointer to the current copy process (cannot be NULL). + * + * Returns a code indicating whether the file should be overwritten: + * 0 = The file should be overwritten. + * 1 = The file should not be overwritten, or overwriting is not enabled in the ongoing copy process. + * 2 = No stats could be retrieved for the destination path. + * 3 = The destination path represents a dir, not a file. + */ +static int determine_overwrite(const char* dest) { + struct stat stats; + + if(overwrite == 1) { + return 0; + } + + if(compat_stat(dest, &stats) != 0) { + return 0; + } + + if(S_ISDIR(stats.st_mode)) { + fprintf(stderr, "Dest %s is dir\n", dest); + return -1; + } + + while(1) { + char response[4]; - return s; + 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') { + return 0; + } + + if(response[0] == 'n' || response[0] == 'N') { + return 1; + } + } + + fprintf(stderr, "Invalid input. Please enter 'y' or 'n'.\n"); + } } -int copier_copy(const char* src, const char* dest, copy_process_t* copy_process) { - struct stat file_stats; - char options[64]; - const char* formatted_size; - const char* formatted_bytes_copied; +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, &file_stats) != 0) { - log_fatal("Failed to open src file \"%s\": %d (%s)\n", src, errno, strerror(errno)); + if(compat_stat(src, &stats) != 0) { + fprintf(stderr, "Failed to acquire stats for %s: %d (%s)\n", src, errno, strerror(errno)); return 1; } - if(copy_process_get_overwrite(copy_process) == 1) { - sprintf(options, "overwrite = yes"); - } else { - sprintf(options, "overwrite = no"); - } + 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; + } - log_info("Counting total number of files to copy...\n"); - copier_get_total_stats(src, copy_process); - formatted_size = copier_format_bytes(copy_process_get_total_size(copy_process)); - log_info("Counted %lu files (%s)\n", copy_process_get_total_files(copy_process), formatted_size); + 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(S_ISDIR(file_stats.st_mode)) { - int status = copier_copy_dir(src, dest, copy_process); - if(status != 0) { - return status; + if(copier_copy(foo, bar, total_files, total_bytes, total_files_copied, total_bytes_copied) != 0) { + return 1; + } } + + free(src_dir); } else { - int status = copier_copy_file(src, dest, copy_process); - if(status != 0) { - return status; + *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; } } - formatted_bytes_copied = copier_format_bytes(copy_process_get_bytes_copied(copy_process)); + return 0; +} + +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); - log_info("Finished copying files: %lu/%lu (%s/%s, %s)\n", copy_process_get_files_processed(copy_process), copy_process_get_total_files(copy_process), formatted_bytes_copied, formatted_size, options); + 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_file(const char* src, const char* dest, copy_process_t* state) { - struct stat file_stats; +size_t copier_copy_file(const char* src, const char* dest) { + struct stat stats; FILE *src_file; FILE *dest_file; char buffer[BUFFER_SIZE]; @@ -88,189 +190,117 @@ int copier_copy_file(const char* src, const char* dest, copy_process_t* state) { size_t total_bytes_read; time_t start_time; + if(compat_stat(src, &stats) != 0) { + fprintf(stderr, "Failed to acquire stats for %s: %d (%s)\n", dest, errno, strerror(errno)); + return -1; + } + src_file = fopen(src, "rb"); if(!src_file) { - log_error("Failed to open src file: \"%s\": %d (%s)\n", src, errno, strerror(errno)); - return 1; + fprintf(stderr, "Failed to open src %s: %d (%s)\n", src, errno, strerror(errno)); + return -1; } - if(compat_stat(dest, &file_stats) == 0) { - if(S_ISDIR(file_stats.st_mode)) { - fclose(src_file); - log_error("Destination path \"%s\" is a directory\n", dest); - return 2; - } + if(S_ISDIR(stats.st_mode)) { + fclose(src_file); + fprintf(stderr, "Dest %s is a directory\n", dest); + return -1; + } - if(!copy_process_get_overwrite(state)) { - 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 0; - } - } - printf("Invalid input. Please enter 'y' or 'n'.\n"); - } - } - } + if(determine_overwrite(dest) != 0) { + return 0; + } dest_file = fopen(dest, "wb"); if(!dest_file) { fclose(src_file); - log_error("Failed to open or create destination file \"%s\": %d (%s)\n", dest, errno, strerror(errno)); - return 1; + fprintf(stderr, "Failed to open or create destination file \"%s\": %d (%s)\n", dest, errno, strerror(errno)); + return -1; } - copy_process_increment_files_processed(state); - total_bytes_read = 0; start_time = time(NULL); + total_bytes_read = 0; while ((bytes_read = fread(buffer, 1, BUFFER_SIZE, src_file)) > 0) { - unsigned int progress; - const char* formatted_bytes_copied; - const char* formatted_total_size; + unsigned int file_progress; time_t now; + char formatted_total_bytes_read[256]; + char formatted_file_size[256]; if(fwrite(buffer, 1, bytes_read, dest_file) != bytes_read) { - log_error("Failed to write to destination file \"%s\": %d (%s)\n", dest, errno, strerror(errno)); + fprintf(stderr, "Failed to write to destination file \"%s\": %d (%s)\n", dest, errno, strerror(errno)); break; } - copy_process_increase_bytes_copied(state, bytes_read); total_bytes_read += bytes_read; - progress = (int)((double) total_bytes_read / file_stats.st_size * 100); + file_progress = (int)((double) total_bytes_read / stats.st_size * 100); now = time(NULL); - if(difftime(now, start_time) >= 10.0 || progress == 100.0) { - formatted_bytes_copied = copier_format_bytes(copy_process_get_bytes_copied(state)); - formatted_total_size = copier_format_bytes(copy_process_get_total_size(state)); + format_bytes(total_bytes_read, formatted_total_bytes_read); + format_bytes(stats.st_size, formatted_file_size); - log_info("Copying file %lu/%lu: %s -> %s (%3d%%, %s/%s)\n", copy_process_get_files_processed(state), copy_process_get_total_files(state), src, dest, progress, formatted_bytes_copied, formatted_total_size); - fflush(stdout); + if(difftime(now, start_time) >= 10.0 || file_progress == 100.0) { + log_info("-> File progress : %3d%%, %s/%s\n", file_progress, formatted_total_bytes_read, formatted_file_size); } } fclose(src_file); fclose(dest_file); - return 0; + return total_bytes_read; } -int copier_copy_dir(const char* src, const char* dest, copy_process_t* state) { - DIR *dir_to_copy; - struct stat file_stats; - struct dirent *file_entry; - int current_status; - - dir_to_copy = compat_opendir(src); - if(!dir_to_copy) { - log_error("Failed to open src dir \"%s\": %d (%s)\n", src, errno, strerror(errno)); - return 3; - } +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(compat_stat(dest, &file_stats) != 0) { - if(compat_mkdir(dest, 0755) == -1) { - compat_closedir(dir_to_copy); - log_error("Failed to create destination directdory \"%s\": %d (%s)\n", dest, errno, strerror(errno)); - return 4; - } - } else if(!S_ISDIR(file_stats.st_mode)) { - compat_closedir(dir_to_copy); - log_error("Destination path \"%s\" is not a directory\n", dest); - return 5; + if(!total_files) { + fprintf(stderr, "total_files cannot be NULL\n"); + return 1; } - 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; - } - - sprintf(entry_source_path, "%s/%s", src, file_entry->d_name); - sprintf(entry_destination_path, "%s/%s", dest, file_entry->d_name); - - if(compat_stat(entry_source_path, &file_stats) != 0) { - log_error("Warning: Could not get info for '%s', skipping.\n", src); - continue; - } - - if(S_ISDIR(file_stats.st_mode)) { - current_status = copier_copy_dir(entry_source_path, entry_destination_path, state); - } else { - current_status = copier_copy_file(entry_source_path, entry_destination_path, state); - } - - if(current_status != 0) { - break; - } + if(!total_bytes) { + fprintf(stderr, "total_bytes cannot be NULL\n"); + return 1; } - compat_closedir(dir_to_copy); - return current_status; -} -/** - * copy_get_total_stats: Populate the copy process with the total number of files and bytes to copy. - * @src The path to the file or dir being traversed. - * @copy_process A pointer to the current copy process (cannot be NULL). - * - * Returns a code indicating the outcome of the operation: - * 0 = Success. - * 1 = Copy process pointer is NULL. - * 2 = Failed to acquire stats for src. The resource probably doesn't exist. - * 3 = Failed to open src dir. - */ -int copier_get_total_stats(const char* src, copy_process_t* copy_process) { - struct stat file_stats; - struct dirent* entry; - char sub_path[1024]; - - if(!copy_process) { + 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(compat_stat(src, &file_stats) != 0) { - return 2; - } - - if(S_ISDIR(file_stats.st_mode)) { + if(S_ISDIR(stats.st_mode)) { DIR *dir = compat_opendir(src); if(!dir) { - return 3; + 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, &file_stats) == 0) { - if(S_ISDIR(file_stats.st_mode)) { - copier_get_total_stats(sub_path, copy_process); - } else { - copy_process_increase_total_size(copy_process, file_stats.st_size); - copy_process_increase_total_files(copy_process, 1); - } + 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 { - copy_process_increase_total_size(copy_process, file_stats.st_size); - copy_process_increase_total_files(copy_process, 1); + *total_files += 1; + *total_bytes += stats.st_size; } return 0; diff --git a/src/copy_process.c b/src/copy_process.c deleted file mode 100644 index 1c6b977..0000000 --- a/src/copy_process.c +++ /dev/null @@ -1,80 +0,0 @@ -#include "copy_process.h" - -#include - -struct copy_process_t { - size_t total_size; - size_t total_files; - size_t bytes_copied; - size_t files_processed; - unsigned int overwrite; -}; - -size_t copy_process_get_total_size(const copy_process_t* state) { - return state ? state->total_size : 0; -} - -size_t copy_process_get_total_files(const copy_process_t* state) { - return state ? state->total_files : 0; -} - -size_t copy_process_get_bytes_copied(const copy_process_t* state) { - return state ? state->bytes_copied : 0; -} - -size_t copy_process_get_files_processed(const copy_process_t* state) { - return state ? state->files_processed : 0; -} - -unsigned int copy_process_get_overwrite(const copy_process_t* state) { - return state ? state->overwrite : 0; -} - -unsigned int copy_process_get_progress(const copy_process_t* state) { - return state && state->total_size > 0 ? (unsigned int)((double) state->bytes_copied / state->total_size * 100) : 0; -} - -void copy_process_increase_total_size(copy_process_t* state, size_t size) { - if(state) { - state->total_size += size; - } -} - -void copy_process_increase_total_files(copy_process_t* state, size_t files) { - if(state) { - state->total_files += files; - } -} - -void copy_process_increase_bytes_copied(copy_process_t* state, size_t bytes_copied) { - if(state) { - state->bytes_copied += bytes_copied; - } -} - -void copy_process_increment_files_processed(copy_process_t* state) { - if(state) { - state->files_processed++; - } -} - -copy_process_t* copy_process_init(unsigned int overwrite) { - copy_process_t* state = malloc(sizeof(copy_process_t)); - if(!state) { - return NULL; - } - - state->total_size = 0; - state->total_files = 0; - state->bytes_copied = 0; - state->files_processed = 0; - state->overwrite = overwrite; - - return state; -} - -void copy_process_destroy(copy_process_t* state) { - if(state) { - free(state); - } -} diff --git a/src/main.c b/src/main.c index d590640..52967ab 100644 --- a/src/main.c +++ b/src/main.c @@ -1,18 +1,18 @@ #include "copier.h" #include "nitro_version.h" -#include "copy_process.h" +#include "runtime.h" #include #include #include #include +int overwrite = 0; + int main(int argc, char *argv[]) { int opt; - int overwrite = 0; - char* src_path = NULL; - char* dest_path = NULL; - copy_process_t* state = NULL; + char* src_path = NULL; + char* dest_path = NULL; static struct option long_options[] = { {"overwrite", no_argument, 0, 'o'}, @@ -63,10 +63,5 @@ int main(int argc, char *argv[]) { printf("NitroCopy %s - (overwrite: %d)\n\n", NITRO_VERSION_STRING, overwrite); - state = copy_process_init(overwrite); - if(state == NULL) { - return 1; - } - - return copier_copy(src_path, dest_path, state); + return copier_execute(src_path, dest_path); } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 46b7d79..839fc9a 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -10,5 +10,3 @@ function(add_c_test name) add_test(NAME ${name} COMMAND ${name}) endif() endfunction() - -add_c_test(test_copy_process_init_should_return_with_initialised_members) diff --git a/tests/test_copy_process_init_should_return_with_initialised_members.c b/tests/test_copy_process_init_should_return_with_initialised_members.c deleted file mode 100644 index f49dedb..0000000 --- a/tests/test_copy_process_init_should_return_with_initialised_members.c +++ /dev/null @@ -1,14 +0,0 @@ -#include "copy_process.h" -#include - -int main(void) { - copy_process_t* copy_process = copy_process_init(0); - - assert(copy_process_get_total_size(copy_process) == 0); - assert(copy_process_get_total_files(copy_process) == 0); - assert(copy_process_get_bytes_copied(copy_process) == 0); - assert(copy_process_get_files_processed(copy_process) == 0); - assert(copy_process_get_overwrite(copy_process) == 0); - - return 0; -} From 27fed2d8750ad63e5641f3b9b9f053d501099b8b Mon Sep 17 00:00:00 2001 From: Maritim Date: Sun, 28 Sep 2025 21:14:23 +0200 Subject: [PATCH 8/9] Implemented Fletcher-32 for resume functionality --- include/fletcher32.h | 14 +++++++++ include/word16.h | 27 ++++++++++++++++ src/CMakeLists.txt | 12 ++++++++ src/fletcher32.c | 59 +++++++++++++++++++++++++++++++++++ src/word16.c | 26 ++++++++++++++++ tests/CMakeLists.txt | 6 +++- tests/fletcher32_tests.c | 66 ++++++++++++++++++++++++++++++++++++++++ tests/test_runner.c | 34 +++++++++++++++++++++ tests/test_runner.h | 15 +++++++++ 9 files changed, 258 insertions(+), 1 deletion(-) create mode 100644 include/fletcher32.h create mode 100644 include/word16.h create mode 100644 src/fletcher32.c create mode 100644 src/word16.c create mode 100644 tests/fletcher32_tests.c create mode 100644 tests/test_runner.c create mode 100644 tests/test_runner.h diff --git a/include/fletcher32.h b/include/fletcher32.h new file mode 100644 index 0000000..602c446 --- /dev/null +++ b/include/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/include/word16.h b/include/word16.h new file mode 100644 index 0000000..3a6332b --- /dev/null +++ b/include/word16.h @@ -0,0 +1,27 @@ +#ifndef WORD16_H +#define WORD16_H + +#include + +/** + * word16_count(): Count the number of 16 bit words in the input string. + * Add 1 to the length then divide by 2 to ensure that any leftover byte get its own word. + * Without adding 1 an odd-length string would lose its last byte. + * + * @input: The string to count number of words in. + * + * @return The number of 16-bit words in the input string. + */ +size_t word16_count(const char* input); + +/** + * 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 char* input, unsigned short* out_words); + +#endif diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e793dd0..1ed3ef3 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -2,10 +2,22 @@ add_subdirectory(platform) add_library(logging STATIC logging.c) +add_library(word16 STATIC + word16.c +) + +add_library(fletcher32 STATIC + fletcher32.c +) +target_link_libraries(fletcher32 PUBLIC + word16 +) + add_library(copier STATIC copier.c ) target_link_libraries(copier PUBLIC + fletcher32 console file logging diff --git a/src/fletcher32.c b/src/fletcher32.c new file mode 100644 index 0000000..9c29f65 --- /dev/null +++ b/src/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/word16.c b/src/word16.c new file mode 100644 index 0000000..efdb7a4 --- /dev/null +++ b/src/word16.c @@ -0,0 +1,26 @@ +#include "word16.h" +#include + +size_t word16_count(const char* input) { + size_t input_length = strlen(input); + return (input_length + 1) / 2; +} + +size_t word16_create(const char* input, unsigned short* out_words) { + size_t input_length = strlen(input); + size_t word_count = word16_count(input); + 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 < input_length) { + low_byte = (unsigned char) input[word_index * 2 + 1]; + } + + out_words[word_index] = ((unsigned short) low_byte << 8) | high_byte; + } + + return word_index; +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 839fc9a..10828a8 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,5 +1,7 @@ function(add_c_test name) - add_executable(${name} ${name}.c) + add_executable(${name} + test_runner.c + ${name}.c) target_link_libraries(${name} PRIVATE copier) if(UNIX AND CMAKE_SYSTEM_NAME STREQUAL "Windows") @@ -10,3 +12,5 @@ function(add_c_test name) add_test(NAME ${name} COMMAND ${name}) endif() endfunction() + +add_c_test(fletcher32_tests) diff --git a/tests/fletcher32_tests.c b/tests/fletcher32_tests.c new file mode 100644 index 0000000..5fc521f --- /dev/null +++ b/tests/fletcher32_tests.c @@ -0,0 +1,66 @@ +#include "fletcher32.h" +#include "test_runner.h" +#include "word16.h" +#include +#include + + +int test_empty_input(char* out_message_buffer) { + return assert_long_equals(0xFFFFFFFF, fletcher32(NULL, 0), out_message_buffer); +} + +int test_abcde(char* out_message_buffer) { + int result; + size_t number_of_words; + unsigned short* data; + + number_of_words = word16_count("abcde"); + data = (unsigned short*) malloc(sizeof(short) * number_of_words); + word16_create("abcde", data); + + result = assert_long_equals(0xF04FC729, fletcher32(data, number_of_words), out_message_buffer); + + free(data); + return result; +} + +int test_abcdef(char* out_message_buffer) { + int result; + size_t number_of_words; + unsigned short* data; + + number_of_words = word16_count("abcdef"); + data = (unsigned short*) malloc(sizeof(short) * number_of_words); + word16_create("abcdef", data); + + result = assert_long_equals(0x56502D2A, fletcher32(data, number_of_words), out_message_buffer); + + free(data); + return result; +} + +int test_abcdefgh(char* out_message_buffer) { + int result; + size_t number_of_words; + unsigned short* data; + + number_of_words = word16_count("abcdefgh"); + data = (unsigned short*) malloc(sizeof(short) * number_of_words); + word16_create("abcdefgh", data); + + result = assert_long_equals(0xEBE19591, fletcher32(data, number_of_words), out_message_buffer); + + free(data); + return result; +} + +static struct test_case tests[] = { + {"empty input", test_empty_input}, + {"abcde", test_abcde}, + {"abcdef", test_abcdef}, + {"abcdefgh", test_abcdefgh} +}; + +int main(void) { + return run_tests(tests, sizeof(tests) / sizeof(tests[0])); +} diff --git a/tests/test_runner.c b/tests/test_runner.c new file mode 100644 index 0000000..66fc09b --- /dev/null +++ b/tests/test_runner.c @@ -0,0 +1,34 @@ +#include "test_runner.h" +#include + +int assert_long_equals(long expected, long actual, char* out_message_buffer) { + if(expected == actual) { + return 1; + } + + sprintf(out_message_buffer, "expected: %lu, actual: %lu", expected, actual); + return 0; +} + +int run_tests(const struct test_case* tests, int count) { + int i; + int passed = 0; + int failed = 0; + + printf("Running %d tests...\n", count); + + for(i = 0; i < count; i++) { + char out_message_buffer[256]; + int success = tests[i].fn(out_message_buffer); + if(success) { + printf("[PASS] %s\n", tests[i].name); + passed++; + } else { + printf("[FAIL] %s (%s)\n", tests[i].name, out_message_buffer); + failed++; + } + } + + printf("%d passed, %d failed\n", passed, failed); + return failed ? 1 : 0; +} diff --git a/tests/test_runner.h b/tests/test_runner.h new file mode 100644 index 0000000..7539e62 --- /dev/null +++ b/tests/test_runner.h @@ -0,0 +1,15 @@ +#ifndef TEST_RUNNER +#define TEST_RUNNER + +typedef int (*test_fn)(char*); + +struct test_case { + const char* name; + test_fn fn; +}; + +int assert_long_equals(long expected, long actual, char* out_message_buffer); + +int run_tests(const struct test_case* tests, int count); + +#endif From 44f9933052f681b4af6a9850f5e49d6b09cfca9b Mon Sep 17 00:00:00 2001 From: Maritim Date: Sun, 5 Oct 2025 22:45:16 +0200 Subject: [PATCH 9/9] more refactoring and stuff --- CMakeLists.txt | 15 +- include/copier.h | 10 + include/word16.h | 13 +- src/CMakeLists.txt | 10 +- src/copier.c | 375 ++++++++++++++++------- src/fletcher32/CMakeLists.txt | 16 + src/{ => fletcher32}/fletcher32.c | 0 {include => src/fletcher32}/fletcher32.h | 0 src/fletcher32/test_fletcher32.c | 43 +++ src/getopt/CMakeLists.txt | 15 + src/getopt/getopt.c | 113 +++++++ src/getopt/getopt.h | 13 + src/getopt/test_getopt.c | 148 +++++++++ src/getopt/test_swap_with_tail.c | 30 ++ src/main.c | 30 +- src/word16.c | 15 +- tests/CMakeLists.txt | 16 - tests/fletcher32_tests.c | 66 ---- tests/test_runner.c | 34 -- tests/test_runner.h | 15 - 20 files changed, 689 insertions(+), 288 deletions(-) create mode 100644 src/fletcher32/CMakeLists.txt rename src/{ => fletcher32}/fletcher32.c (100%) rename {include => src/fletcher32}/fletcher32.h (100%) create mode 100644 src/fletcher32/test_fletcher32.c create mode 100644 src/getopt/CMakeLists.txt create mode 100644 src/getopt/getopt.c create mode 100644 src/getopt/getopt.h create mode 100644 src/getopt/test_getopt.c create mode 100644 src/getopt/test_swap_with_tail.c delete mode 100644 tests/CMakeLists.txt delete mode 100644 tests/fletcher32_tests.c delete mode 100644 tests/test_runner.c delete mode 100644 tests/test_runner.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 9b25f15..76d3890 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,6 +5,12 @@ project(NitroCopy ) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +# ----------------------------------------------- +# Tests +# ----------------------------------------------- +include(CTest) +enable_testing() + # ----------------------------------------------- # Determine Git-based version # ----------------------------------------------- @@ -114,12 +120,3 @@ add_custom_target(tag_release WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} COMMENT "Tagging release v${PROJECT_VERSION}" ) - -# ----------------------------------------------- -# Tests -# ----------------------------------------------- -include(CTest) -enable_testing() - -# Add tests subdirectory -add_subdirectory(tests) diff --git a/include/copier.h b/include/copier.h index 69e0c94..490afe2 100644 --- a/include/copier.h +++ b/include/copier.h @@ -3,6 +3,16 @@ #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); diff --git a/include/word16.h b/include/word16.h index 3a6332b..c962bf8 100644 --- a/include/word16.h +++ b/include/word16.h @@ -3,16 +3,7 @@ #include -/** - * word16_count(): Count the number of 16 bit words in the input string. - * Add 1 to the length then divide by 2 to ensure that any leftover byte get its own word. - * Without adding 1 an odd-length string would lose its last byte. - * - * @input: The string to count number of words in. - * - * @return The number of 16-bit words in the input string. - */ -size_t word16_count(const char* input); +size_t word16_count(size_t length); /** * word16_create(): Create 16 bit words in little endian order, low byte first and high byte second. @@ -22,6 +13,6 @@ size_t word16_count(const char* input); * * @return THe number of 16-bit words in the input string. */ -size_t word16_create(const char* input, unsigned short* out_words); +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 index 1ed3ef3..5f56a42 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,3 +1,5 @@ +add_subdirectory(fletcher32) +add_subdirectory(getopt) add_subdirectory(platform) add_library(logging STATIC logging.c) @@ -6,13 +8,6 @@ add_library(word16 STATIC word16.c ) -add_library(fletcher32 STATIC - fletcher32.c -) -target_link_libraries(fletcher32 PUBLIC - word16 -) - add_library(copier STATIC copier.c ) @@ -26,5 +21,6 @@ target_link_libraries(copier PUBLIC add_executable(NitroCopy main.c) target_link_libraries(NitroCopy PUBLIC + getopt copier ) diff --git a/src/copier.c b/src/copier.c index 9f31ab1..128f652 100644 --- a/src/copier.c +++ b/src/copier.c @@ -1,7 +1,8 @@ #include "copier.h" #include "file.h" +#include "fletcher32.h" #include "logging.h" -#include "runtime.h" +#include "word16.h" #include #include @@ -11,7 +12,36 @@ #include #include -#define BUFFER_SIZE 4096 +#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"}; @@ -34,52 +64,36 @@ static void format_bytes(size_t bytes, char* buffer) { sprintf(buffer, "%.2f %s", value, suffixes[i]); } -/** - * copier_overwrite: Checks whether a file can be and will be overwritten. - * @dest: The path to the file that may or may not be overwritten. - * @copy_process: A pointer to the current copy process (cannot be NULL). - * - * Returns a code indicating whether the file should be overwritten: - * 0 = The file should be overwritten. - * 1 = The file should not be overwritten, or overwriting is not enabled in the ongoing copy process. - * 2 = No stats could be retrieved for the destination path. - * 3 = The destination path represents a dir, not a file. - */ -static int determine_overwrite(const char* dest) { - struct stat stats; +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]; - if(overwrite == 1) { - return 0; - } + printf("Counting total number of files to copy...\n"); - if(compat_stat(dest, &stats) != 0) { - return 0; - } - - if(S_ISDIR(stats.st_mode)) { - fprintf(stderr, "Dest %s is dir\n", dest); - return -1; + 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; } - while(1) { - char response[4]; + if(total_bytes == 0) { + fprintf(stderr, "There's nothing to copy.\n"); + return 1; + } - printf("Destination file \"%s\" exists already. Overwrite? (y/n): ", dest); - fflush(stdout); + format_bytes(total_bytes, formatted_total_bytes); + printf("Counted %lu files (%s)\n", (unsigned long) total_files, formatted_total_bytes); - - if(fgets(response, sizeof(response), stdin) != NULL) { - if(response[0] == 'y' || response[0] == 'Y') { - return 0; - } + copier_copy(src, dest, total_files, total_bytes, &files_copied, &bytes_copied); - if(response[0] == 'n' || response[0] == 'N') { - return 1; - } - } + 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); - fprintf(stderr, "Invalid input. Please enter 'y' or 'n'.\n"); - } + 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) { @@ -149,105 +163,250 @@ int copier_copy(const char* src, const char* dest, size_t total_files, size_t to return 0; } -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]; +/** + * 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]; - printf("Counting total number of files to copy...\n"); + if(difftime(now, *last_log_time) >= 10.0 || copied == total) { + elapsed = difftime(now, start); + speed = elapsed > 0 ? ((double) total / elapsed) : 0.0; - 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; + 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; } +} - if(total_bytes == 0) { - fprintf(stderr, "There's nothing to copy.\n"); - return 1; +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); } - format_bytes(total_bytes, formatted_total_bytes); - printf("Counted %lu files (%s)\n", (unsigned long) total_files, formatted_total_bytes); + 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; +} - copier_copy(src, dest, total_files, total_bytes, &files_copied, &bytes_copied); +/* + * 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); + } - 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); + *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; } -size_t copier_copy_file(const char* src, const char* dest) { - struct stat stats; - FILE *src_file; - FILE *dest_file; - char buffer[BUFFER_SIZE]; - size_t bytes_read; - size_t total_bytes_read; - time_t start_time; +/* + * 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; + } - if(compat_stat(src, &stats) != 0) { - fprintf(stderr, "Failed to acquire stats for %s: %d (%s)\n", dest, errno, strerror(errno)); - return -1; + dst_size = dst_stats.st_size; + resume_chunk = dst_size / CHUNK_SIZE; + + if(resume_chunk <= 0) { + return resume_chunk; } - src_file = fopen(src, "rb"); - if(!src_file) { - fprintf(stderr, "Failed to open src %s: %d (%s)\n", src, errno, strerror(errno)); + 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; - } + } - if(S_ISDIR(stats.st_mode)) { + dst_file = open_dst_file(dst); + if(!dst_file) { fclose(src_file); - fprintf(stderr, "Dest %s is a directory\n", dest); return -1; } - if(determine_overwrite(dest) != 0) { - return 0; - } - - 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)); + /* Get the size of the src file */ + if(get_file_stats(src, &src_stats) != 0) { + fclose(src_file); + fclose(dst_file); return -1; - } - - start_time = time(NULL); - total_bytes_read = 0; + } - while ((bytes_read = fread(buffer, 1, BUFFER_SIZE, src_file)) > 0) { - unsigned int file_progress; - time_t now; - char formatted_total_bytes_read[256]; - char formatted_file_size[256]; + if(stat(dst, &dst_stats) == 0) { + copy_existing_action_t action = prompt_user_action(dst); - 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; + 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; } + } - total_bytes_read += bytes_read; - file_progress = (int)((double) total_bytes_read / stats.st_size * 100); - now = time(NULL); - - format_bytes(total_bytes_read, formatted_total_bytes_read); - format_bytes(stats.st_size, formatted_file_size); + reposition_streams_for_resume(src_file, dst_file, resume_chunk); - if(difftime(now, start_time) >= 10.0 || file_progress == 100.0) { - log_info("-> File progress : %3d%%, %s/%s\n", file_progress, formatted_total_bytes_read, formatted_file_size); - } + /* 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(dest_file); + fclose(dst_file); - return total_bytes_read; + return copied; } int copier_get_total_stats(const char* src, size_t* total_files, size_t* total_bytes) { 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.c b/src/fletcher32/fletcher32.c similarity index 100% rename from src/fletcher32.c rename to src/fletcher32/fletcher32.c diff --git a/include/fletcher32.h b/src/fletcher32/fletcher32.h similarity index 100% rename from include/fletcher32.h rename to src/fletcher32/fletcher32.h 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/main.c b/src/main.c index 52967ab..7f156ca 100644 --- a/src/main.c +++ b/src/main.c @@ -1,29 +1,33 @@ #include "copier.h" +#include "getopt.h" #include "nitro_version.h" #include "runtime.h" #include #include #include -#include -int overwrite = 0; +int overwrite = 0; +copy_existing_action_t g_default_copy_action = COPY_ACTION_UNSET; int main(int argc, char *argv[]) { int opt; char* src_path = NULL; char* dest_path = NULL; - static struct option long_options[] = { - {"overwrite", no_argument, 0, 'o'}, - {"help", no_argument, 0, 'h'}, - {0, 0, 0, 0} - }; - - while ((opt = getopt_long(argc, argv, "oh", 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': printf("NitroCopy version: %s\nhttps://github.com/maritims/nitrocopy\n\nWritten by Martin Severin Steffensen.\n", NITRO_VERSION_STRING); @@ -32,9 +36,9 @@ int main(int argc, char *argv[]) { 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(" -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(" -h, --help Display this help: message and exit.\n"); printf("\nFor more information:\n"); printf(" Documentation: https://github.com/maritims/nitrocopy#readme\n"); diff --git a/src/word16.c b/src/word16.c index efdb7a4..d9f1ba1 100644 --- a/src/word16.c +++ b/src/word16.c @@ -1,25 +1,22 @@ #include "word16.h" -#include -size_t word16_count(const char* input) { - size_t input_length = strlen(input); - return (input_length + 1) / 2; +size_t word16_count(size_t length) { + return (length + 1) / 2; } -size_t word16_create(const char* input, unsigned short* out_words) { - size_t input_length = strlen(input); - size_t word_count = word16_count(input); +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 < input_length) { + if(word_index * 2 + 1 < length) { low_byte = (unsigned char) input[word_index * 2 + 1]; } - out_words[word_index] = ((unsigned short) low_byte << 8) | high_byte; + output[word_index] = ((unsigned short) low_byte << 8) | high_byte; } return word_index; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt deleted file mode 100644 index 10828a8..0000000 --- a/tests/CMakeLists.txt +++ /dev/null @@ -1,16 +0,0 @@ -function(add_c_test name) - add_executable(${name} - test_runner.c - ${name}.c) - target_link_libraries(${name} PRIVATE copier) - - if(UNIX AND CMAKE_SYSTEM_NAME STREQUAL "Windows") - # Cross build: use wine to run Win32 tests - add_test(NAME ${name} COMMAND wine $) - else() - # Native build: run directly - add_test(NAME ${name} COMMAND ${name}) - endif() -endfunction() - -add_c_test(fletcher32_tests) diff --git a/tests/fletcher32_tests.c b/tests/fletcher32_tests.c deleted file mode 100644 index 5fc521f..0000000 --- a/tests/fletcher32_tests.c +++ /dev/null @@ -1,66 +0,0 @@ -#include "fletcher32.h" -#include "test_runner.h" -#include "word16.h" -#include -#include - - -int test_empty_input(char* out_message_buffer) { - return assert_long_equals(0xFFFFFFFF, fletcher32(NULL, 0), out_message_buffer); -} - -int test_abcde(char* out_message_buffer) { - int result; - size_t number_of_words; - unsigned short* data; - - number_of_words = word16_count("abcde"); - data = (unsigned short*) malloc(sizeof(short) * number_of_words); - word16_create("abcde", data); - - result = assert_long_equals(0xF04FC729, fletcher32(data, number_of_words), out_message_buffer); - - free(data); - return result; -} - -int test_abcdef(char* out_message_buffer) { - int result; - size_t number_of_words; - unsigned short* data; - - number_of_words = word16_count("abcdef"); - data = (unsigned short*) malloc(sizeof(short) * number_of_words); - word16_create("abcdef", data); - - result = assert_long_equals(0x56502D2A, fletcher32(data, number_of_words), out_message_buffer); - - free(data); - return result; -} - -int test_abcdefgh(char* out_message_buffer) { - int result; - size_t number_of_words; - unsigned short* data; - - number_of_words = word16_count("abcdefgh"); - data = (unsigned short*) malloc(sizeof(short) * number_of_words); - word16_create("abcdefgh", data); - - result = assert_long_equals(0xEBE19591, fletcher32(data, number_of_words), out_message_buffer); - - free(data); - return result; -} - -static struct test_case tests[] = { - {"empty input", test_empty_input}, - {"abcde", test_abcde}, - {"abcdef", test_abcdef}, - {"abcdefgh", test_abcdefgh} -}; - -int main(void) { - return run_tests(tests, sizeof(tests) / sizeof(tests[0])); -} diff --git a/tests/test_runner.c b/tests/test_runner.c deleted file mode 100644 index 66fc09b..0000000 --- a/tests/test_runner.c +++ /dev/null @@ -1,34 +0,0 @@ -#include "test_runner.h" -#include - -int assert_long_equals(long expected, long actual, char* out_message_buffer) { - if(expected == actual) { - return 1; - } - - sprintf(out_message_buffer, "expected: %lu, actual: %lu", expected, actual); - return 0; -} - -int run_tests(const struct test_case* tests, int count) { - int i; - int passed = 0; - int failed = 0; - - printf("Running %d tests...\n", count); - - for(i = 0; i < count; i++) { - char out_message_buffer[256]; - int success = tests[i].fn(out_message_buffer); - if(success) { - printf("[PASS] %s\n", tests[i].name); - passed++; - } else { - printf("[FAIL] %s (%s)\n", tests[i].name, out_message_buffer); - failed++; - } - } - - printf("%d passed, %d failed\n", passed, failed); - return failed ? 1 : 0; -} diff --git a/tests/test_runner.h b/tests/test_runner.h deleted file mode 100644 index 7539e62..0000000 --- a/tests/test_runner.h +++ /dev/null @@ -1,15 +0,0 @@ -#ifndef TEST_RUNNER -#define TEST_RUNNER - -typedef int (*test_fn)(char*); - -struct test_case { - const char* name; - test_fn fn; -}; - -int assert_long_equals(long expected, long actual, char* out_message_buffer); - -int run_tests(const struct test_case* tests, int count); - -#endif