diff --git a/.gitignore b/.gitignore index 92216fc..ac44fcf 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,6 @@ TestResults/ *.tmp *.bak *.b64.txt +BinaryConverter-source.zip +BinaryConverter-source.zip.certutil.txt +native/build/ diff --git a/Dokumentation.md b/Dokumentation.md index 1e7249a..67c6092 100644 --- a/Dokumentation.md +++ b/Dokumentation.md @@ -95,6 +95,11 @@ BinaryConverter/ Program.cs Properties/ AssemblyInfo.cs + native/ + Makefile + README.md + src/ + binaryconverter.cpp .editorconfig .gitattributes .gitignore @@ -102,6 +107,19 @@ BinaryConverter/ Readme.md ``` +## Native Linux-Version + +Unter `native/` liegt eine C++17-Implementierung fuer Linux. Sie erzeugt eine native Arch-Linux-Binary und verwendet OpenSSL/libcrypto fuer SHA-256. Das Transferformat ist identisch zur C#-Version, sodass Dateien auf Windows encoded und auf Linux decoded werden koennen oder umgekehrt. + +Build: + +```bash +cd native +make +``` + +Die Binary liegt danach unter `native/build/binaryconverter`. + ## Gitea-Hinweise -Das Repository enthaelt keine Build-Artefakte. `bin/`, `obj/`, Visual-Studio-Userdateien, Logs und temporaere Dateien sind ueber `.gitignore` ausgeschlossen. +Das Repository enthaelt keine Build-Artefakte. `bin/`, `obj/`, `native/build/`, Visual-Studio-Userdateien, Logs und temporaere Dateien sind ueber `.gitignore` ausgeschlossen. diff --git a/Readme.md b/Readme.md index 8486258..40f0586 100644 --- a/Readme.md +++ b/Readme.md @@ -32,6 +32,25 @@ Die EXE liegt danach hier: BinaryConverter\bin\Release\BinaryConverter.exe ``` +Native Linux-Version fuer Arch Linux: + +```bash +cd native +make +``` + +Die native Binary liegt danach hier: + +```text +native/build/binaryconverter +``` + +Voraussetzungen unter Arch Linux: + +```bash +sudo pacman -S --needed base-devel openssl pkgconf +``` + ## Nutzung Encode: @@ -46,6 +65,13 @@ Decode: BinaryConverter.exe -Decode Out.txt PowerShell.exe ``` +Unter Linux wird dieselbe Syntax mit der nativen Binary verwendet: + +```bash +./native/build/binaryconverter -Encode input.bin -Output Out.txt +./native/build/binaryconverter -Decode Out.txt output.bin +``` + Alternative Decode-Syntax: ```powershell diff --git a/native/Makefile b/native/Makefile new file mode 100644 index 0000000..661bb2c --- /dev/null +++ b/native/Makefile @@ -0,0 +1,28 @@ +CXX ?= g++ +PKG_CONFIG ?= pkg-config +PREFIX ?= /usr/local + +TARGET := binaryconverter +SRC := src/binaryconverter.cpp +BUILD_DIR := build +BIN := $(BUILD_DIR)/$(TARGET) + +CXXFLAGS ?= -O3 -DNDEBUG -march=native +CPPFLAGS += -std=c++17 -Wall -Wextra -Wpedantic -D_FILE_OFFSET_BITS=64 +LIBCRYPTO_CFLAGS := $(shell $(PKG_CONFIG) --cflags libcrypto 2>/dev/null) +LIBCRYPTO_LIBS := $(shell $(PKG_CONFIG) --libs libcrypto 2>/dev/null) +LDLIBS += $(if $(LIBCRYPTO_LIBS),$(LIBCRYPTO_LIBS),-lcrypto) + +.PHONY: all clean install + +all: $(BIN) + +$(BIN): $(SRC) + @mkdir -p $(BUILD_DIR) + $(CXX) $(CPPFLAGS) $(CXXFLAGS) $(LIBCRYPTO_CFLAGS) -o $@ $< $(LDFLAGS) $(LDLIBS) + +install: $(BIN) + install -Dm755 $(BIN) "$(DESTDIR)$(PREFIX)/bin/$(TARGET)" + +clean: + rm -rf $(BUILD_DIR) diff --git a/native/README.md b/native/README.md new file mode 100644 index 0000000..47b105b --- /dev/null +++ b/native/README.md @@ -0,0 +1,65 @@ +# BinaryConverter Native fuer Linux + +Diese Variante ist eine native C++17-Implementierung fuer Linux, getestet fuer Arch Linux. Sie ist formatkompatibel zur vorhandenen C#/.NET-Framework-Version: Encode-Dateien koennen zwischen Windows und Linux gegenseitig verarbeitet werden. + +## Voraussetzungen + +Arch Linux: + +```bash +sudo pacman -S --needed base-devel openssl pkgconf +``` + +## Build + +```bash +cd native +make +``` + +Die Binary liegt danach hier: + +```text +native/build/binaryconverter +``` + +Optional generischere Binary ohne CPU-spezifische Optimierung: + +```bash +make clean +make CXXFLAGS="-O3 -DNDEBUG" +``` + +## Nutzung + +Encode: + +```bash +./build/binaryconverter -Encode input.bin -Output transfer.txt +``` + +Decode: + +```bash +./build/binaryconverter -Decode transfer.txt output.bin +``` + +Mit Logfile: + +```bash +./build/binaryconverter -Encode input.bin -Output transfer.txt -Log binaryconverter.log +``` + +Vorhandene Ausgabedatei ueberschreiben: + +```bash +./build/binaryconverter -Decode transfer.txt output.bin -Force +``` + +## Verhalten + +- Streaming-Verarbeitung ohne komplette Dateien in den RAM zu laden +- Base64-Zeilen mit 76 Zeichen +- temporaere Ausgabedatei im Zielverzeichnis und atomarer Rename nach Erfolg +- Decode prueft `SourceLength` und `SHA256`, wenn die Metadaten vorhanden sind +- Exitcodes wie die C#-Version: `0` Erfolg, `1` Laufzeit-/Datenfehler, `2` Argumentfehler diff --git a/native/src/binaryconverter.cpp b/native/src/binaryconverter.cpp new file mode 100644 index 0000000..144f6eb --- /dev/null +++ b/native/src/binaryconverter.cpp @@ -0,0 +1,1087 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace fs = std::filesystem; + +namespace { + +constexpr std::size_t RawReadBufferSize = 1024 * 1024; +constexpr std::size_t EncodeBlockSize = 57 * 4096; +constexpr std::size_t Base64LineLength = 76; +constexpr std::string_view BeginMarker = "-----BEGIN BINARYCONVERTER BASE64-----"; +constexpr std::string_view EndMarker = "-----END BINARYCONVERTER BASE64-----"; +constexpr std::string_view Version = "1.0.0-native"; + +enum class OperationMode { + None, + Encode, + Decode, +}; + +struct Options { + OperationMode mode = OperationMode::None; + std::string input_path; + std::string output_path; + std::string log_path; + bool force = false; + bool quiet = false; + bool no_progress = false; + bool show_help = false; +}; + +struct DecodeMetadata { + std::optional source_length; + std::string sha256; +}; + +class BinaryConverterError : public std::runtime_error { +public: + using std::runtime_error::runtime_error; +}; + +std::string to_lower(std::string_view value) +{ + std::string result; + result.reserve(value.size()); + for (unsigned char ch : value) { + result.push_back(static_cast(std::tolower(ch))); + } + return result; +} + +bool equals_ignore_case(std::string_view left, std::string_view right) +{ + return to_lower(left) == to_lower(right); +} + +bool starts_with_ignore_case(std::string_view value, std::string_view prefix) +{ + if (value.size() < prefix.size()) { + return false; + } + return equals_ignore_case(value.substr(0, prefix.size()), prefix); +} + +bool is_option(std::string_view value, std::string_view name) +{ + if (value.size() < 2) { + return false; + } + + if (value[0] == '/') { + return equals_ignore_case(value.substr(1), name); + } + + if (value[0] == '-') { + std::size_t offset = value.size() > 1 && value[1] == '-' ? 2 : 1; + return equals_ignore_case(value.substr(offset), name); + } + + return false; +} + +bool is_known_option(std::string_view value) +{ + return is_option(value, "Encode") || + is_option(value, "Decode") || + is_option(value, "Output") || + is_option(value, "Out") || + is_option(value, "O") || + is_option(value, "Log") || + is_option(value, "Force") || + is_option(value, "Overwrite") || + is_option(value, "Quiet") || + is_option(value, "NoProgress") || + is_option(value, "Help") || + value == "/?" || + value == "-?"; +} + +std::string trim(std::string_view value) +{ + std::size_t begin = 0; + while (begin < value.size() && std::isspace(static_cast(value[begin]))) { + begin++; + } + + std::size_t end = value.size(); + while (end > begin && std::isspace(static_cast(value[end - 1]))) { + end--; + } + + return std::string(value.substr(begin, end - begin)); +} + +std::string shell_basename(std::string_view path) +{ + std::string text(path); + const auto pos = text.find_last_of("/\\"); + if (pos == std::string::npos) { + return text; + } + return text.substr(pos + 1); +} + +std::string format_bytes(std::uintmax_t value) +{ + static constexpr std::array units = {"B", "KB", "MB", "GB", "TB"}; + double size = static_cast(value); + std::size_t unit = 0; + while (size >= 1024.0 && unit < units.size() - 1) { + size /= 1024.0; + unit++; + } + + std::ostringstream stream; + stream << std::fixed << std::setprecision(unit == 0 ? 0 : 2) << size << ' ' << units[unit]; + return stream.str(); +} + +std::string now_local_for_log() +{ + const auto now = std::chrono::system_clock::now(); + const auto millis = std::chrono::duration_cast(now.time_since_epoch()) % 1000; + const std::time_t now_time = std::chrono::system_clock::to_time_t(now); + + std::tm local_time {}; + localtime_r(&now_time, &local_time); + + std::ostringstream stream; + stream << std::put_time(&local_time, "%Y-%m-%d %H:%M:%S") + << '.' << std::setw(3) << std::setfill('0') << millis.count(); + return stream.str(); +} + +std::string now_utc_iso8601() +{ + const auto now = std::chrono::system_clock::now(); + const auto micros = std::chrono::duration_cast(now.time_since_epoch()) % 1000000; + const std::time_t now_time = std::chrono::system_clock::to_time_t(now); + + std::tm utc_time {}; + gmtime_r(&now_time, &utc_time); + + std::ostringstream stream; + stream << std::put_time(&utc_time, "%Y-%m-%dT%H:%M:%S") + << '.' << std::setw(6) << std::setfill('0') << micros.count() << 'Z'; + return stream.str(); +} + +class Logger { +public: + Logger(bool quiet, const std::string& log_path) + : quiet_(quiet) + { + if (!log_path.empty()) { + const fs::path path(log_path); + if (path.has_parent_path()) { + fs::create_directories(path.parent_path()); + } + file_.open(path, std::ios::out | std::ios::app); + if (!file_) { + throw BinaryConverterError("Could not open log file: " + log_path); + } + } + } + + void info(const std::string& message) { write("INFO", message, false); } + void ok(const std::string& message) { write("OK", message, false); } + void warn(const std::string& message) { write("WARN", message, false); } + void error(const std::string& message) { write("ERROR", message, true); } + void debug(const std::string& message) { write("DEBUG", message, false); } + +private: + void write(std::string_view level, const std::string& message, bool error) + { + const std::string line = now_local_for_log() + " [" + std::string(level) + "] " + message; + if (!quiet_ || error) { + (error ? std::cerr : std::cout) << line << '\n'; + } + if (file_) { + file_ << line << '\n'; + file_.flush(); + } + } + + bool quiet_; + std::ofstream file_; +}; + +class ProgressReporter { +public: + ProgressReporter(std::string operation, std::uintmax_t total, bool disabled, Logger& logger) + : operation_(std::move(operation)), + total_(total), + disabled_(disabled), + logger_(logger), + started_(std::chrono::steady_clock::now()), + last_report_(started_ - std::chrono::seconds(2)) + { + } + + void report(std::uintmax_t current) + { + if (disabled_) { + return; + } + + const auto now = std::chrono::steady_clock::now(); + if (now - last_report_ < std::chrono::seconds(1)) { + return; + } + + last_report_ = now; + logger_.info(build_message(current, now)); + } + + void done(std::uintmax_t current) + { + if (!disabled_) { + logger_.info(build_message(current, std::chrono::steady_clock::now())); + } + } + +private: + std::string build_message(std::uintmax_t current, std::chrono::steady_clock::time_point now) const + { + std::ostringstream stream; + const double percent = total_ > 0 ? (static_cast(current) * 100.0) / static_cast(total_) : 100.0; + const double seconds = std::max(0.001, std::chrono::duration(now - started_).count()); + const auto bytes_per_second = static_cast(static_cast(current) / seconds); + + stream << operation_ << ": " << format_bytes(current) << " / " << format_bytes(total_) + << " (" << std::fixed << std::setprecision(1) << percent << "%, " + << format_bytes(bytes_per_second) << "/s)"; + return stream.str(); + } + + std::string operation_; + std::uintmax_t total_; + bool disabled_; + Logger& logger_; + std::chrono::steady_clock::time_point started_; + std::chrono::steady_clock::time_point last_report_; +}; + +class Sha256 { +public: + Sha256() + : context_(EVP_MD_CTX_new()) + { + if (context_ == nullptr || EVP_DigestInit_ex(context_, EVP_sha256(), nullptr) != 1) { + throw BinaryConverterError("Could not initialize SHA-256."); + } + } + + Sha256(const Sha256&) = delete; + Sha256& operator=(const Sha256&) = delete; + + ~Sha256() + { + EVP_MD_CTX_free(context_); + } + + void update(const unsigned char* data, std::size_t length) + { + if (length == 0) { + return; + } + if (EVP_DigestUpdate(context_, data, length) != 1) { + throw BinaryConverterError("SHA-256 update failed."); + } + } + + std::string final_hex() + { + std::array digest {}; + unsigned int length = 0; + if (EVP_DigestFinal_ex(context_, digest.data(), &length) != 1) { + throw BinaryConverterError("SHA-256 finalization failed."); + } + + std::ostringstream stream; + for (unsigned int i = 0; i < length; i++) { + stream << std::hex << std::setw(2) << std::setfill('0') << static_cast(digest[i]); + } + return stream.str(); + } + +private: + EVP_MD_CTX* context_; +}; + +std::string compute_sha256(const fs::path& path, std::uintmax_t total_bytes, const Options& options, Logger& logger) +{ + std::ifstream input(path, std::ios::binary); + if (!input) { + throw BinaryConverterError("Could not open input file: " + path.string()); + } + + Sha256 sha256; + std::vector buffer(RawReadBufferSize); + std::uintmax_t processed = 0; + ProgressReporter progress("Hash", total_bytes, options.no_progress, logger); + + while (input) { + input.read(reinterpret_cast(buffer.data()), static_cast(buffer.size())); + const auto read = input.gcount(); + if (read > 0) { + sha256.update(buffer.data(), static_cast(read)); + processed += static_cast(read); + progress.report(processed); + } + } + + if (!input.eof()) { + throw BinaryConverterError("Failed while reading input file: " + path.string()); + } + + progress.done(processed); + return sha256.final_hex(); +} + +class Base64LineWriter { +public: + explicit Base64LineWriter(std::ostream& output) + : output_(output) + { + } + + void write(std::string_view text) + { + for (char ch : text) { + output_.put(ch); + line_length_++; + if (line_length_ == Base64LineLength) { + output_.put('\n'); + line_length_ = 0; + } + } + } + + void finish() + { + if (line_length_ != 0) { + output_.put('\n'); + line_length_ = 0; + } + } + +private: + std::ostream& output_; + std::size_t line_length_ = 0; +}; + +class Base64Encoder { +public: + explicit Base64Encoder(Base64LineWriter& writer) + : writer_(writer) + { + } + + void update(const unsigned char* data, std::size_t length) + { + std::size_t offset = 0; + + if (carry_length_ > 0) { + while (carry_length_ < 3 && offset < length) { + carry_[carry_length_++] = data[offset++]; + } + if (carry_length_ == 3) { + encode_triplet(carry_.data(), 3); + carry_length_ = 0; + } + } + + while (offset + 3 <= length) { + encode_triplet(data + offset, 3); + offset += 3; + } + + while (offset < length) { + carry_[carry_length_++] = data[offset++]; + } + } + + void finish() + { + if (carry_length_ > 0) { + encode_triplet(carry_.data(), carry_length_); + carry_length_ = 0; + } + writer_.finish(); + } + +private: + void encode_triplet(const unsigned char* data, std::size_t length) + { + static constexpr char alphabet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + char encoded[4]; + + const unsigned int b0 = data[0]; + const unsigned int b1 = length > 1 ? data[1] : 0; + const unsigned int b2 = length > 2 ? data[2] : 0; + + encoded[0] = alphabet[b0 >> 2]; + encoded[1] = alphabet[((b0 & 0x03U) << 4) | (b1 >> 4)]; + encoded[2] = length > 1 ? alphabet[((b1 & 0x0FU) << 2) | (b2 >> 6)] : '='; + encoded[3] = length > 2 ? alphabet[b2 & 0x3FU] : '='; + writer_.write(std::string_view(encoded, sizeof(encoded))); + } + + Base64LineWriter& writer_; + std::array carry_ {}; + std::size_t carry_length_ = 0; +}; + +bool is_base64_char(char ch) +{ + return (ch >= 'A' && ch <= 'Z') || + (ch >= 'a' && ch <= 'z') || + (ch >= '0' && ch <= '9') || + ch == '+' || + ch == '/' || + ch == '='; +} + +int base64_value(char ch) +{ + if (ch >= 'A' && ch <= 'Z') { + return ch - 'A'; + } + if (ch >= 'a' && ch <= 'z') { + return ch - 'a' + 26; + } + if (ch >= '0' && ch <= '9') { + return ch - '0' + 52; + } + if (ch == '+') { + return 62; + } + if (ch == '/') { + return 63; + } + return -1; +} + +class Base64Decoder { +public: + Base64Decoder(std::ostream& output, Sha256& sha256) + : output_(output), + sha256_(sha256) + { + } + + void append(std::string_view text, int line_number) + { + for (char ch : text) { + if (std::isspace(static_cast(ch))) { + continue; + } + if (!is_base64_char(ch)) { + throw BinaryConverterError("Invalid Base64 character at line " + std::to_string(line_number) + ": '" + std::string(1, ch) + "'."); + } + if (finished_by_padding_) { + throw BinaryConverterError("Unexpected Base64 data after padding near line " + std::to_string(line_number) + "."); + } + + quartet_[quartet_length_++] = ch; + if (quartet_length_ == 4) { + decode_quartet(line_number); + quartet_length_ = 0; + } + } + } + + void finish(int line_number) + { + if (quartet_length_ != 0) { + throw BinaryConverterError("Base64 payload length is invalid near line " + std::to_string(line_number) + "."); + } + } + + std::uintmax_t decoded_bytes() const + { + return decoded_bytes_; + } + +private: + void decode_quartet(int line_number) + { + const char c0 = quartet_[0]; + const char c1 = quartet_[1]; + const char c2 = quartet_[2]; + const char c3 = quartet_[3]; + + if (c0 == '=' || c1 == '=') { + throw BinaryConverterError("Base64 padding is invalid near line " + std::to_string(line_number) + "."); + } + if (c2 == '=' && c3 != '=') { + throw BinaryConverterError("Base64 padding is invalid near line " + std::to_string(line_number) + "."); + } + + const int b0 = base64_value(c0); + const int b1 = base64_value(c1); + const int b2 = c2 == '=' ? 0 : base64_value(c2); + const int b3 = c3 == '=' ? 0 : base64_value(c3); + + if (b0 < 0 || b1 < 0 || b2 < 0 || b3 < 0) { + throw BinaryConverterError("Base64 payload is invalid near line " + std::to_string(line_number) + "."); + } + + unsigned char decoded[3]; + decoded[0] = static_cast((b0 << 2) | (b1 >> 4)); + decoded[1] = static_cast(((b1 & 0x0F) << 4) | (b2 >> 2)); + decoded[2] = static_cast(((b2 & 0x03) << 6) | b3); + + std::size_t length = 3; + if (c2 == '=') { + length = 1; + finished_by_padding_ = true; + } else if (c3 == '=') { + length = 2; + finished_by_padding_ = true; + } + + output_.write(reinterpret_cast(decoded), static_cast(length)); + if (!output_) { + throw BinaryConverterError("Failed while writing decoded output."); + } + sha256_.update(decoded, length); + decoded_bytes_ += length; + } + + std::ostream& output_; + Sha256& sha256_; + std::array quartet_ {}; + std::size_t quartet_length_ = 0; + bool finished_by_padding_ = false; + std::uintmax_t decoded_bytes_ = 0; +}; + +std::string base64_encode_text(std::string_view text) +{ + static constexpr char alphabet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + std::string encoded; + encoded.reserve(((text.size() + 2) / 3) * 4); + + const auto* data = reinterpret_cast(text.data()); + for (std::size_t offset = 0; offset < text.size(); offset += 3) { + const std::size_t remaining = text.size() - offset; + const std::size_t length = std::min(3, remaining); + const unsigned int b0 = data[offset]; + const unsigned int b1 = length > 1 ? data[offset + 1] : 0; + const unsigned int b2 = length > 2 ? data[offset + 2] : 0; + + encoded.push_back(alphabet[b0 >> 2]); + encoded.push_back(alphabet[((b0 & 0x03U) << 4) | (b1 >> 4)]); + encoded.push_back(length > 1 ? alphabet[((b1 & 0x0FU) << 2) | (b2 >> 6)] : '='); + encoded.push_back(length > 2 ? alphabet[b2 & 0x3FU] : '='); + } + + return encoded; +} + +std::string make_temp_path(const fs::path& destination) +{ + const auto now = std::chrono::steady_clock::now().time_since_epoch().count(); + std::ostringstream suffix; + suffix << ".tmp." << getpid() << '.' << now; + return destination.string() + suffix.str(); +} + +void ensure_parent_directory(const fs::path& output_path) +{ + if (output_path.has_parent_path()) { + fs::create_directories(output_path.parent_path()); + } +} + +void ensure_output_can_be_written(const fs::path& output_path, bool force) +{ + if (output_path.empty()) { + throw BinaryConverterError("Output path is missing."); + } + if (fs::exists(output_path) && !force) { + throw BinaryConverterError("Output file already exists. Use -Force to overwrite: " + output_path.string()); + } +} + +void move_or_replace(const fs::path& source, const fs::path& destination) +{ + std::error_code error; + fs::rename(source, destination, error); + if (error) { + throw BinaryConverterError("Could not move temporary output into place: " + error.message()); + } +} + +void try_delete(const fs::path& path) +{ + std::error_code ignored; + if (!path.empty()) { + fs::remove(path, ignored); + } +} + +bool looks_like_binaryconverter_envelope_line(std::string_view trimmed_line) +{ + return trimmed_line == BeginMarker || + trimmed_line == EndMarker || + starts_with_ignore_case(trimmed_line, "# BinaryConverter-"); +} + +void parse_metadata_line(const std::string& line, DecodeMetadata& metadata) +{ + std::string text = trim(std::string_view(line).substr(1)); + const auto separator = text.find(':'); + if (separator == std::string::npos) { + return; + } + + const std::string key = trim(std::string_view(text).substr(0, separator)); + const std::string value = trim(std::string_view(text).substr(separator + 1)); + + if (equals_ignore_case(key, "SourceLength")) { + try { + std::size_t parsed_chars = 0; + const auto parsed = std::stoull(value, &parsed_chars, 10); + if (parsed_chars == value.size()) { + metadata.source_length = parsed; + } + } catch (const std::exception&) { + } + } else if (equals_ignore_case(key, "SHA256")) { + metadata.sha256 = value; + } +} + +void validate_decoded_output(const DecodeMetadata& metadata, std::uintmax_t decoded_bytes, const std::string& actual_hash, Logger& logger) +{ + if (metadata.source_length.has_value()) { + if (*metadata.source_length != decoded_bytes) { + throw BinaryConverterError("Decoded file length does not match metadata. Expected " + + std::to_string(*metadata.source_length) + " bytes, got " + + std::to_string(decoded_bytes) + " bytes."); + } + logger.info("Length check: OK (" + std::to_string(decoded_bytes) + " bytes)"); + } else { + logger.warn("No source length metadata found; length validation skipped."); + } + + if (!metadata.sha256.empty()) { + if (!equals_ignore_case(metadata.sha256, actual_hash)) { + throw BinaryConverterError("SHA-256 check failed. Expected " + metadata.sha256 + ", got " + actual_hash + "."); + } + logger.info("SHA-256 check: OK (" + actual_hash + ")"); + } else { + logger.warn("No SHA-256 metadata found; hash validation skipped. Actual SHA-256: " + actual_hash); + } +} + +void encode_file(const Options& options, Logger& logger) +{ + const fs::path input_path(options.input_path); + const fs::path output_path(options.output_path); + + if (!fs::exists(input_path)) { + throw BinaryConverterError("Input file was not found: " + input_path.string()); + } + if (!fs::is_regular_file(input_path)) { + throw BinaryConverterError("Input path is not a regular file: " + input_path.string()); + } + + ensure_output_can_be_written(output_path, options.force); + ensure_parent_directory(output_path); + + const auto input_size = fs::file_size(input_path); + logger.info("Reading input to calculate SHA-256."); + const std::string sha256 = compute_sha256(input_path, input_size, options, logger); + logger.info("SHA-256: " + sha256); + logger.info("Writing Base64 transfer file."); + + const fs::path temp_output_path(make_temp_path(output_path)); + + try { + std::ifstream input(input_path, std::ios::binary); + if (!input) { + throw BinaryConverterError("Could not open input file: " + input_path.string()); + } + + std::ofstream output(temp_output_path, std::ios::binary | std::ios::trunc); + if (!output) { + throw BinaryConverterError("Could not create temporary output file: " + temp_output_path.string()); + } + + output << "# BinaryConverter-Format: 1\n"; + output << "# Encoding: Base64\n"; + output << "# SourceFileNameBase64: " << base64_encode_text(shell_basename(input_path.string())) << '\n'; + output << "# SourceLength: " << input_size << '\n'; + output << "# SHA256: " << sha256 << '\n'; + output << "# CreatedUtc: " << now_utc_iso8601() << '\n'; + output << "# LineLength: " << Base64LineLength << '\n'; + output << BeginMarker << '\n'; + + Base64LineWriter line_writer(output); + Base64Encoder encoder(line_writer); + std::vector buffer(EncodeBlockSize); + std::uintmax_t processed = 0; + ProgressReporter progress("Encode", input_size, options.no_progress, logger); + + while (input) { + input.read(reinterpret_cast(buffer.data()), static_cast(buffer.size())); + const auto read = input.gcount(); + if (read > 0) { + encoder.update(buffer.data(), static_cast(read)); + processed += static_cast(read); + progress.report(processed); + } + } + + if (!input.eof()) { + throw BinaryConverterError("Failed while reading input file: " + input_path.string()); + } + + encoder.finish(); + output << EndMarker << '\n'; + if (!output) { + throw BinaryConverterError("Failed while writing transfer file: " + temp_output_path.string()); + } + + progress.done(processed); + output.close(); + move_or_replace(temp_output_path, output_path); + } catch (...) { + try_delete(temp_output_path); + throw; + } + + logger.info("Transfer file size: " + format_bytes(fs::file_size(output_path))); +} + +void decode_file(const Options& options, Logger& logger) +{ + const fs::path input_path(options.input_path); + const fs::path output_path(options.output_path); + + if (!fs::exists(input_path)) { + throw BinaryConverterError("Input file was not found: " + input_path.string()); + } + if (!fs::is_regular_file(input_path)) { + throw BinaryConverterError("Input path is not a regular file: " + input_path.string()); + } + + ensure_output_can_be_written(output_path, options.force); + ensure_parent_directory(output_path); + + const auto input_size = fs::file_size(input_path); + const fs::path temp_output_path(make_temp_path(output_path)); + DecodeMetadata metadata; + + try { + std::ifstream input(input_path, std::ios::binary); + if (!input) { + throw BinaryConverterError("Could not open transfer file: " + input_path.string()); + } + + std::ofstream output(temp_output_path, std::ios::binary | std::ios::trunc); + if (!output) { + throw BinaryConverterError("Could not create temporary output file: " + temp_output_path.string()); + } + + Sha256 sha256; + Base64Decoder decoder(output, sha256); + ProgressReporter progress("Decode input", input_size, options.no_progress, logger); + logger.info("Reading Base64 transfer file."); + + bool saw_begin_marker = false; + bool saw_end_marker = false; + bool inside_payload = false; + std::string line; + int line_number = 0; + + while (std::getline(input, line)) { + line_number++; + const auto position = input.tellg(); + const auto current_position = position < std::istream::pos_type(0) + ? input_size + : static_cast(position); + progress.report(current_position); + + std::string trimmed = trim(line); + if (trimmed.empty()) { + continue; + } + + if (trimmed.front() == '#') { + parse_metadata_line(trimmed, metadata); + continue; + } + + if (trimmed == BeginMarker) { + saw_begin_marker = true; + inside_payload = true; + continue; + } + + if (trimmed == EndMarker) { + saw_end_marker = true; + decoder.finish(line_number); + inside_payload = false; + break; + } + + if (saw_begin_marker && !inside_payload) { + throw BinaryConverterError("Unexpected data after end marker at line " + std::to_string(line_number) + "."); + } + + if (saw_begin_marker || !looks_like_binaryconverter_envelope_line(trimmed)) { + decoder.append(trimmed, line_number); + } + } + + if (input.bad()) { + throw BinaryConverterError("Failed while reading transfer file: " + input_path.string()); + } + + if (saw_begin_marker && !saw_end_marker) { + throw BinaryConverterError("Transfer file is incomplete: missing end marker."); + } + + if (!saw_end_marker) { + decoder.finish(line_number); + } + + output.flush(); + if (!output) { + throw BinaryConverterError("Failed while writing decoded output: " + temp_output_path.string()); + } + + const std::string actual_hash = sha256.final_hex(); + progress.done(input_size); + validate_decoded_output(metadata, decoder.decoded_bytes(), actual_hash, logger); + + output.close(); + move_or_replace(temp_output_path, output_path); + logger.info("Decoded file size: " + format_bytes(decoder.decoded_bytes())); + } catch (...) { + try_delete(temp_output_path); + throw; + } +} + +void print_usage() +{ + std::cout << "BinaryConverter native - streaming binary transfer helper\n\n"; + std::cout << "Usage:\n"; + std::cout << " binaryconverter -Encode -Output [-Force] [-Log ]\n"; + std::cout << " binaryconverter -Decode [-Force] [-Log ]\n"; + std::cout << " binaryconverter -Decode -Output [-Force] [-Log ]\n\n"; + std::cout << "Options:\n"; + std::cout << " -Encode Encode a binary file to a copy/paste friendly Base64 text file.\n"; + std::cout << " -Decode Decode a transfer text file back to binary.\n"; + std::cout << " -Output Output path.\n"; + std::cout << " -Force Overwrite an existing output file.\n"; + std::cout << " -Log Optional log file path.\n"; + std::cout << " -NoProgress Disable periodic progress lines.\n"; + std::cout << " -Quiet Only write errors to the console.\n"; + std::cout << " -Help Show this help.\n"; +} + +bool parse_options(int argc, char** argv, Options& options, std::string& error) +{ + std::vector positionals; + + if (argc <= 1) { + options.show_help = true; + return true; + } + + for (int i = 1; i < argc; i++) { + const std::string arg = argv[i]; + + if (is_option(arg, "Help") || arg == "/?" || arg == "-?") { + options.show_help = true; + return true; + } + + if (is_option(arg, "Encode")) { + if (options.mode != OperationMode::None) { + error = "Specify only one operation: -Encode or -Decode."; + return false; + } + options.mode = OperationMode::Encode; + if (i + 1 < argc && !is_known_option(argv[i + 1])) { + options.input_path = argv[++i]; + } + continue; + } + + if (is_option(arg, "Decode")) { + if (options.mode != OperationMode::None) { + error = "Specify only one operation: -Encode or -Decode."; + return false; + } + options.mode = OperationMode::Decode; + if (i + 1 < argc && !is_known_option(argv[i + 1])) { + options.input_path = argv[++i]; + } + continue; + } + + if (is_option(arg, "Output") || is_option(arg, "Out") || is_option(arg, "O")) { + if (i + 1 >= argc) { + error = "-Output requires a path."; + return false; + } + options.output_path = argv[++i]; + continue; + } + + if (is_option(arg, "Log")) { + if (i + 1 >= argc) { + error = "-Log requires a path."; + return false; + } + options.log_path = argv[++i]; + continue; + } + + if (is_option(arg, "Force") || is_option(arg, "Overwrite")) { + options.force = true; + continue; + } + + if (is_option(arg, "Quiet")) { + options.quiet = true; + continue; + } + + if (is_option(arg, "NoProgress")) { + options.no_progress = true; + continue; + } + + if (!arg.empty() && arg.front() == '-') { + error = "Unknown option: " + arg; + return false; + } + + positionals.push_back(arg); + } + + if (options.mode == OperationMode::None) { + error = "Missing operation. Use -Encode or -Decode."; + return false; + } + + if (options.input_path.empty() && !positionals.empty()) { + options.input_path = positionals.front(); + positionals.erase(positionals.begin()); + } + + if (options.output_path.empty() && !positionals.empty()) { + options.output_path = positionals.front(); + positionals.erase(positionals.begin()); + } + + if (!positionals.empty()) { + error = "Too many positional arguments."; + return false; + } + + if (options.input_path.empty()) { + error = "Missing input path."; + return false; + } + + if (options.output_path.empty()) { + if (options.mode == OperationMode::Encode) { + options.output_path = shell_basename(options.input_path) + ".b64.txt"; + } else { + error = "Missing output path for decode."; + return false; + } + } + + return true; +} + +const char* mode_name(OperationMode mode) +{ + switch (mode) { + case OperationMode::Encode: + return "Encode"; + case OperationMode::Decode: + return "Decode"; + case OperationMode::None: + return "None"; + } + return "None"; +} + +} // namespace + +int main(int argc, char** argv) +{ + Options options; + std::string parse_error; + + if (!parse_options(argc, argv, options, parse_error)) { + std::cerr << parse_error << "\n\n"; + print_usage(); + return 2; + } + + if (options.show_help) { + print_usage(); + return 0; + } + + try { + Logger logger(options.quiet, options.log_path); + logger.info(std::string("BinaryConverter ") + std::string(Version)); + logger.info(std::string("Operation: ") + mode_name(options.mode)); + logger.info("Input: " + fs::absolute(options.input_path).string()); + logger.info("Output: " + fs::absolute(options.output_path).string()); + + if (options.mode == OperationMode::Encode) { + encode_file(options, logger); + } else { + decode_file(options, logger); + } + + logger.ok("Finished successfully."); + return 0; + } catch (const std::exception& ex) { + try { + Logger logger(options.quiet, options.log_path); + logger.error(ex.what()); + logger.debug(std::string("errno=") + std::to_string(errno) + " " + std::strerror(errno)); + } catch (...) { + std::cerr << ex.what() << '\n'; + } + return 1; + } +}