first commit

This commit is contained in:
Your Name
2025-08-09 19:13:24 +12:00
commit 52ab41d3d4
39 changed files with 9272 additions and 0 deletions

25
.gitignore vendored Normal file
View File

@@ -0,0 +1,25 @@
# Build directories
build*/
output/
test_temp/
# CMake generated files
CMakeCache.txt
CMakeFiles/
cmake_install.cmake
Makefile
# Compiled executables
dehydrate
*.exe
# IDE files
.vscode/
.idea/
*.swp
*.swo
*~
# OS files
.DS_Store
Thumbs.db

38
CMakeLists.txt Normal file
View File

@@ -0,0 +1,38 @@
cmake_minimum_required(VERSION 3.16)
# Project setup
if(NOT DEFINED PROJECT_NAME)
message(FATAL_ERROR "PROJECT_NAME is not defined. Pass it via -DPROJECT_NAME=<name>")
endif()
string(TIMESTAMP PROJECT_VERSION "%Y.%m%d.%H%M")
project(${PROJECT_NAME} VERSION ${PROJECT_VERSION} LANGUAGES CXX)
# Build configuration
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXE_LINKER_FLAGS "-static")
set(CMAKE_FIND_LIBRARY_SUFFIXES ".a")
set(BUILD_SHARED_LIBS OFF)
set(CMAKE_PREFIX_PATH /usr/local)
# Create executable
file(GLOB_RECURSE SOURCES "src/*.cpp")
add_executable(${PROJECT_NAME} ${SOURCES})
# Configure version.hpp
configure_file("src/version.hpp.in" "src/autogen/version.hpp" @ONLY)
# Pre-build script (optional - remove if not needed)
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/cmake_prebuild.sh")
add_custom_target(run_prebuild_script ALL
COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/cmake_prebuild.sh
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
add_dependencies(${PROJECT_NAME} run_prebuild_script)
endif()
# Include directories
target_include_directories(${PROJECT_NAME} PRIVATE
${CMAKE_CURRENT_BINARY_DIR}/src/autogen
src
contrib)

57
README.md Normal file
View File

@@ -0,0 +1,57 @@
# Dehydrate
## Installation
Automated system-wide installation:
```
curl -fsSL https://gitea.jde.nz/public/dehydrate/releases/download/latest/install.sh | sudo bash
```
To download just the dehydrate executable:
```
curl -fsSL -o dehydrate https://gitea.jde.nz/public/dehydrate/releases/download/latest/dehydrate.amd64 && chmod a+x dehydrate
```
## How it Works
Dehydrate converts existing files to C++ source code which can be used to recreate the original files.
Works on individual files or entire directory trees.
```
Usage: dehydrate [OPTIONS] SOURCE DEST
Options:
-s Silent mode (no output)
-u Update dehydrate to the latest version
Examples:
dehydrate file.txt output/ Creates _file.txt.cpp and _file.txt.hpp in output/
dehydrate src/ output/ Creates _src.cpp and _src.hpp in output/
dehydrate -u Updates dehydrate to the latest version
```
All c++ code produced is in namespace `recreate_{SOURCEFILE|SOURCEFOLDER}`
When the source is a file, the C++ function created is:
```
bool recreate_file(std::string destination_folder);
```
The path is the full path to the destination folder, excluding the filename. The original filename is used.
If a file exists at that location:
The hash of the content is compared, and the file is overwritten if the hash is different, otherwise
left unchanged.
If there is no file:
SOURCEFILE is written in destination_folder.
When the source is a folder, the C++ function created is:
```
bool recreate_tree(std::string destination_folder);
```
The destination_folder (and any needed subfolders) are created if they don't exist.
The contents of the original SOURCEFOLDER are recreated inside destination_folder,
overwriting any existing content in that folder if the hash of the individual file
is different than existing content, or if the file doesn't exist.
In both cases, unless in silent mode, confirmation is displayed for each file, showing
the existing and new hash, and whether the file was updated or not.

52
build.sh Executable file
View File

@@ -0,0 +1,52 @@
#!/bin/bash
set -euo pipefail
# Get script directory - handle different execution contexts
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
PROJECT="$(basename "${SCRIPT_DIR}")"
# Debug output for CI
echo "${PROJECT} build script running from: ${SCRIPT_DIR}"
# handle running locally, or docker in docker via gitea runner.
if [ -n "${GITEA_CONTAINER_NAME:-}" ]; then
echo "We're in a gitea container: ${GITEA_CONTAINER_NAME}"
VOLUME_OPTS=("--volumes-from=${GITEA_CONTAINER_NAME}")
WORKING_DIR=("-w" "${GITHUB_WORKSPACE}/${PROJECT}")
BUILD_DIR="${GITHUB_WORKSPACE}/${PROJECT}/build"
OUTPUT_DIR="${GITHUB_WORKSPACE}/${PROJECT}/output"
else
VOLUME_OPTS=("-v" "${SCRIPT_DIR}:/app")
WORKING_DIR=("-w" "/app")
BUILD_DIR="${SCRIPT_DIR}/build"
OUTPUT_DIR="${SCRIPT_DIR}/output"
fi
# Create output directory
mkdir -p "${OUTPUT_DIR}"
# Run build in container with mounted directories
COMMAND_TO_RUN="cmake -G Ninja -S . -B ./build \
-DCMAKE_BUILD_TYPE=\${CMAKE_BUILD_TYPE} \
-DPROJECT_NAME=${PROJECT} && \
cmake --build ./build"
echo "Building in new docker container"
docker run --rm \
--user "$(id -u):$(id -g)" \
"${VOLUME_OPTS[@]}" \
"${WORKING_DIR[@]}" \
-e CMAKE_BUILD_TYPE="${CMAKE_BUILD_TYPE:-Debug}" \
gitea.jde.nz/public/dropshell-build-base:latest \
bash -c "${COMMAND_TO_RUN}"
# Copy built executable to output directory
if [ -f "${BUILD_DIR}/${PROJECT}" ]; then
cp "${BUILD_DIR}/${PROJECT}" "${OUTPUT_DIR}/"
echo "✓ Build successful - ${PROJECT} copied to ${OUTPUT_DIR}/"
else
echo "✗ Build failed - ${PROJECT} not found in ${BUILD_DIR}/"
exit 1
fi
echo "Build complete"

18
clean.sh Executable file
View File

@@ -0,0 +1,18 @@
#!/bin/bash
set -euo pipefail
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
PROJECT="$(basename "$(dirname "${SCRIPT_DIR}")")"
echo "Cleaning ${PROJECT}..."
# Remove output and build directories
for dir in "output" "build"; do
if [ -d "${SCRIPT_DIR}/${dir}" ]; then
echo "Removing ${dir} directory..."
rm -rf "${SCRIPT_DIR:?}/${dir}"
fi
done
echo "${PROJECT} cleaned successfully"

7343
contrib/xxhash.hpp Normal file

File diff suppressed because it is too large Load Diff

51
install.sh Executable file
View File

@@ -0,0 +1,51 @@
#!/bin/bash
set -e
PROJECT="dehydrate"
# RUN AS ROOT
if [[ $EUID -ne 0 ]]; then
echo "This script must be run as root" >&2
exit 1
fi
# 0. see if we were passed a folder to install to
# -----------------------------------------------------------------------------
INSTALL_DIR="$1"
if [[ -z "$INSTALL_DIR" ]]; then
INSTALL_DIR="/usr/local/bin"
else
if [[ ! -d "$INSTALL_DIR" ]]; then
mkdir -p "$INSTALL_DIR"
fi
fi
echo "Installing $PROJECT to $INSTALL_DIR"
# 1. Determine architecture
# -----------------------------------------------------------------------------
ARCH=$(uname -m)
if [[ "$ARCH" == "x86_64" ]]; then
BIN=$PROJECT.amd64
elif [[ "$ARCH" == "aarch64" || "$ARCH" == "arm64" ]]; then
BIN=$PROJECT.arm64
else
echo "Unsupported architecture: $ARCH" >&2
exit 1
fi
# 3. Download the appropriate binary
# -----------------------------------------------------------------------------
URL="https://gitea.jde.nz/public/$PROJECT/releases/download/latest/$BIN"
echo "Downloading $BIN from $URL to $TMPDIR..."
curl -fsSL -o "$INSTALL_DIR/$PROJECT" "$URL"
# 4. Make it executable
# -----------------------------------------------------------------------------
chmod +x "$INSTALL_DIR/$PROJECT"
# 6. Print success message
# -----------------------------------------------------------------------------
echo "$PROJECT installed successfully to $INSTALL_DIR/$PROJECT (arch $ARCH)"

73
publish.sh Executable file
View File

@@ -0,0 +1,73 @@
#!/bin/bash
set -euo pipefail
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
ARCH=$(uname -m)
PROJECT="dehydrate"
OUTPUT="${SCRIPT_DIR}/output"
function heading() {
# print a heading with a line of dashe
echo "--------------------------------"
echo "$1"
echo "--------------------------------"
}
#--------------------------------------------------------------------------------
heading "Publishing ${PROJECT}"
function die() {
heading "error: $1"
exit 1
}
[[ -n $SOS_WRITE_TOKEN ]] || die "SOS_WRITE_TOKEN not specified"
# clear output dir
rm -rf "${OUTPUT}"
mkdir -p "${OUTPUT}"
#--------------------------------------------------------------------------------
heading "Building ${PROJECT}"
# build release version
export CMAKE_BUILD_TYPE="Release"
"${SCRIPT_DIR}/build.sh"
[ -f "${OUTPUT}/${PROJECT}" ] || die "Build failed."
#--------------------------------------------------------------------------------
SOS="${SCRIPT_DIR}/../sos/sos"
[ -f "${SOS}" ] || die "Failed to find sos"
#--------------------------------------------------------------------------------
heading "Uploading ${PROJECT} to getbin.xyz"
# upload arch-specific binary
"${SOS}" upload "getbin.xyz" "${OUTPUT}/${PROJECT}" "${PROJECT}:latest-${ARCH}"
# upload generic install script (ok if multiple times as we iterate through arch's)
"${SOS}" upload "getbin.xyz" "${SCRIPT_DIR}/install.sh" "${PROJECT}-install:latest"
#--------------------------------------------------------------------------------
heading "Publishing ${PROJECT} as tool to getpkg.xyz"
# Create tool directory structure
TOOLDIR="${OUTPUT}/tool"
mkdir "${TOOLDIR}"
cp "${OUTPUT}/${PROJECT}" "${TOOLDIR}/${PROJECT}"
# Use getpkg to publish the tool
GETPKG="${SCRIPT_DIR}/../getpkg/output/getpkg"
if [ ! -f "$GETPKG" ]; then
GETPKG="${SCRIPT_DIR}/../getpkg/getpkg"
fi
if [ -f "$GETPKG" ]; then
"${GETPKG}" publish "${PROJECT}:${ARCH}" "${TOOLDIR}"
else
echo "Warning: getpkg not found, skipping tool publishing to getpkg.xyz"
fi

1
src/.gitkeep Normal file
View File

@@ -0,0 +1 @@
# This file ensures the src directory is tracked by git.

63
src/argparse.cpp Normal file
View File

@@ -0,0 +1,63 @@
#include "argparse.hpp"
#include <stdexcept>
static const std::string HELP_TEXT = R"(
Converts existing files to C++ source code which can be used to recreate the original files.
Usage: dehydrate [OPTIONS] SOURCE DEST
Options:
-s Silent mode (no output)
-u Update dehydrate to the latest version
-v Show version only
Examples:
dehydrate file.txt output/ Creates _file.txt.cpp and _file.txt.hpp in output/
dehydrate src/ output/ Creates _src.cpp and _src.hpp in output/
dehydrate -u Updates dehydrate to the latest version
dehydrate -v Shows version number
dehydrate version Shows version number
)";
Args parse_args(int argc, char* argv[]) {
Args args;
int idx = 1;
// Check for "version" as first argument (no dash)
if (argc > 1 && std::string(argv[1]) == "version") {
args.version = true;
return args;
}
// Parse flags
while (idx < argc && argv[idx][0] == '-') {
std::string flag = argv[idx];
if (flag == "-s") {
args.silent = true;
} else if (flag == "-u") {
args.update = true;
} else if (flag == "-v") {
args.version = true;
} else {
throw std::runtime_error("Unknown flag: " + flag + "\n\n" + HELP_TEXT);
}
idx++;
}
// If update or version flag is set, return early
if (args.update || args.version) {
return args;
}
// Require source and dest parameters for normal operation
if (argc - idx != 2) {
throw std::runtime_error(HELP_TEXT);
}
args.source = argv[idx];
args.dest = argv[idx + 1];
return args;
}

12
src/argparse.hpp Normal file
View File

@@ -0,0 +1,12 @@
#pragma once
#include <string>
struct Args {
bool silent = false;
bool update = false;
bool version = false;
std::string source;
std::string dest;
};
Args parse_args(int argc, char* argv[]);

372
src/generator.cpp Normal file
View File

@@ -0,0 +1,372 @@
#include "generator.hpp"
#include "hash.hpp"
#include <iostream>
#include <fstream>
#include <filesystem>
#include <sstream>
#include <vector>
#include "xxhash.hpp"
#include <sys/stat.h> // For file permissions
#include <cstring> // For strlen
namespace fs = std::filesystem;
static std::string sanitize(const std::string& name) {
std::string out = name;
for (char& c : out) if (!isalnum(c)) c = '_';
return out;
}
static uint64_t fnv1a_64(const void* data, size_t len) {
const uint8_t* p = static_cast<const uint8_t*>(data);
uint64_t h = 0xcbf29ce484222325ULL;
for (size_t i = 0; i < len; ++i)
h = (h ^ p[i]) * 0x100000001b3ULL;
return h;
}
// Base64 encoding function - no dependencies
static std::string base64_encode(const unsigned char* data, size_t len) {
const char* base64_chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
std::string result;
result.reserve((len + 2) / 3 * 4); // Reserve space for the full encoded size
int val = 0, valb = -6;
for (size_t i = 0; i < len; i++) {
val = (val << 8) + data[i];
valb += 8;
while (valb >= 0) {
result.push_back(base64_chars[(val >> valb) & 0x3F]);
valb -= 6;
}
}
if (valb > -6) {
result.push_back(base64_chars[((val << 8) >> (valb + 8)) & 0x3F]);
}
// Add padding
while (result.size() % 4) {
result.push_back('=');
}
return result;
}
// Helper function to output the _recreate_file_ utility function and Base64 decoder
static void output_recreate_file_utility(std::ofstream& cpp) {
cpp << R"cpp(
// Base64 decoding function - no dependencies
static void base64_decode(const char* encoded_data, size_t encoded_len, unsigned char* output, size_t* output_len) {
const char* base64_chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
size_t out_pos = 0;
int val = 0, valb = -8;
for (size_t i = 0; i < encoded_len; i++) {
char c = encoded_data[i];
if (c == '=') break;
// Find position in base64_chars
const char* pos = strchr(base64_chars, c);
if (pos == nullptr) continue; // Skip invalid characters
val = (val << 6) + static_cast<int>(pos - base64_chars);
valb += 6;
if (valb >= 0) {
output[out_pos++] = static_cast<unsigned char>((val >> valb) & 0xFF);
valb -= 8;
}
}
*output_len = out_pos;
}
// Utility function to recreate a file with proper permissions
static bool _recreate_file_(const std::filesystem::path& outpath, uint64_t file_hash, std::filesystem::perms file_perms, const unsigned char* filedata, size_t filedata_len) {
namespace fs = std::filesystem;
bool needs_write = false;
// Check if file exists and has correct hash
if (fs::exists(outpath)) {
// Check content hash
std::ifstream in(outpath, std::ios::binary);
std::ostringstream oss;
oss << in.rdbuf();
std::string data = oss.str();
uint64_t existing_hash = fnv1a_64(data.data(), data.size());
needs_write = existing_hash != file_hash;
} else {
needs_write = true; // File doesn't exist, need to create it
}
bool needs_permission_update = true;
if (!needs_write) { // we always update permissions if the file is written or changed. Othewise we check.
fs::perms current_perms = fs::status(outpath).permissions();
needs_permission_update = current_perms != file_perms;
}
if (needs_write) {
bool existed = fs::exists(outpath);
fs::create_directories(outpath.parent_path());
std::ofstream out(outpath, std::ios::binary);
out.write(reinterpret_cast<const char*>(filedata), filedata_len);
out.close();
// Set the file permissions
fs::permissions(outpath, file_perms);
if (!existed) {
std::cout << "[dehydrate] " << outpath.filename() << ": created\n";
} else {
std::cout << "[dehydrate] " << outpath.filename() << ": updated (hash changed)\n";
}
return true;
}
if (needs_permission_update) {
// Update only permissions
fs::permissions(outpath, file_perms);
std::cout << "[dehydrate] " << outpath.filename() << ": updated (permissions changed)\n";
return true;
}
return false;
}
)cpp";
}
void generate_file_code(const std::string& source, const std::string& destfolder, bool silent) {
fs::path src(source);
fs::path dest(destfolder);
std::string ns = "recreate_" + sanitize(src.stem().string());
std::string cppname = "_" + src.stem().string() + ".cpp";
std::string hppname = "_" + src.stem().string() + ".hpp";
std::string bothname = "_" + src.stem().string() + ".{cpp,hpp}";
fs::create_directories(dest);
std::ifstream in(source, std::ios::binary);
std::ostringstream oss;
oss << in.rdbuf();
std::string filedata = oss.str();
uint64_t hash = fnv1a_64(filedata.data(), filedata.size());
// Get source file permissions
fs::perms src_perms = fs::status(src).permissions();
// Write HPP
std::ofstream hpp(dest / hppname);
hpp << "#pragma once\n#include <string>\nnamespace " << ns << " {\nbool recreate_file(std::string destination_folder);\n}\n";
// Write CPP
std::ofstream cpp(dest / cppname);
cpp << R"cpp(#include <fstream>
#include <filesystem>
#include <string>
#include <iostream>
#include <cstring>
// Tiny dependency-free FNV-1a 64-bit hash
static uint64_t fnv1a_64(const void* data, size_t len) {
const uint8_t* p = static_cast<const uint8_t*>(data);
uint64_t h = 0xcbf29ce484222325ULL;
for (size_t i = 0; i < len; ++i)
h = (h ^ p[i]) * 0x100000001b3ULL;
return h;
}
)cpp";
cpp << "#include \"" << hppname << "\"\n";
cpp << "namespace " << ns << " {\n";
// Output the recreate_file utility function
output_recreate_file_utility(cpp);
// Write recreate_file function with embedded file data
cpp << R"cpp(
bool recreate_file(std::string destination_folder) {
namespace fs = std::filesystem;
fs::path outpath = fs::path(destination_folder) / ")cpp" << src.filename().string() << R"cpp(";
// File data embedded as Base64
static const char filedata_base64[] = )cpp";
// Encode the file data to Base64
std::string base64 = base64_encode(reinterpret_cast<const unsigned char*>(filedata.data()), filedata.size());
// Split into 76-character chunks for readability
const size_t line_length = 76;
for (size_t i = 0; i < base64.length(); i += line_length) {
if (i > 0) cpp << "\n ";
cpp << "\"" << base64.substr(i, std::min(line_length, base64.length() - i)) << "\"";
if (i + line_length < base64.length()) cpp << "\\";
}
cpp << ";\n\n";
// Decode Base64 at runtime
cpp << " // Decode Base64 data\n";
cpp << " size_t decoded_size = (strlen(filedata_base64) * 3) / 4;\n";
cpp << " unsigned char* decoded_data = new unsigned char[decoded_size];\n";
cpp << " size_t actual_size;\n";
cpp << " base64_decode(filedata_base64, strlen(filedata_base64), decoded_data, &actual_size);\n\n";
// Call _recreate_file_ with the decoded data
cpp << " bool result = _recreate_file_(outpath, " << hash << "ULL, "
<< "std::filesystem::perms(" << static_cast<unsigned>(src_perms) << "), "
<< "decoded_data, actual_size);\n";
// Clean up
cpp << " delete[] decoded_data;\n";
cpp << " return result;\n";
cpp << "}\n";
cpp << "}\n";
if (!silent) {
std::cout << "[dehydrate] Generated: " << (dest / bothname) << std::endl;
}
}
// Helper to recursively collect all files in a directory
template<typename F>
void walk_dir(const fs::path& dir, F&& f) {
for (auto& p : fs::recursive_directory_iterator(dir)) {
if (fs::is_regular_file(p)) f(p.path());
}
}
void generate_folder_code(const std::string& source, const std::string& destfolder, bool silent) {
fs::path src(source);
fs::path dest(destfolder);
std::string ns = "recreate_" + sanitize(src.stem().string());
std::string cppname = "_" + src.stem().string() + ".cpp";
std::string hppname = "_" + src.stem().string() + ".hpp";
std::string bothname = "_" + src.stem().string() + ".{cpp,hpp}";
fs::create_directories(dest);
// Collect all files
std::vector<fs::path> files;
walk_dir(src, [&](const fs::path& p) { files.push_back(p); });
// Write HPP
std::ofstream hpp(dest / hppname);
// -------------------------------------------------------------------------
// Generate HPP
hpp << R"hpp(
#pragma once
/*
THIS FILE IS AUTO-GENERATED BY DEHYDRATE.
DO NOT EDIT THIS FILE.
*/
#include <string>
namespace )hpp" << ns << R"hpp( {
bool recreate_tree(std::string destination_folder);
}
)hpp";
// -------------------------------------------------------------------------
// Write CPP
std::ofstream cpp(dest / cppname);
cpp << R"cpp(#include <fstream>
#include <filesystem>
#include <string>
#include <iostream>
#include <cstring>
/*
THIS FILE IS AUTO-GENERATED BY DEHYDRATE.
DO NOT EDIT THIS FILE.
*/
)cpp";
cpp << "#include \"" << hppname << "\"\n";
cpp << "namespace " << ns << " {\n";
cpp << R"cpp(
// Tiny dependency-free FNV-1a 64-bit hash
static uint64_t fnv1a_64(const void* data, size_t len) {
const uint8_t* p = static_cast<const uint8_t*>(data);
uint64_t h = 0xcbf29ce484222325ULL;
for (size_t i = 0; i < len; ++i)
h = (h ^ p[i]) * 0x100000001b3ULL;
return h;
}
)cpp";
// Output the recreate_file utility function
output_recreate_file_utility(cpp);
// Start writing recreate_tree - we'll embed file data directly in function
cpp << R"cpp(
bool recreate_tree(std::string destination_folder) {
namespace fs = std::filesystem;
bool any_written = false;
)cpp";
// Process each file
for (const auto& file : files) {
std::ifstream in(file, std::ios::binary);
std::ostringstream oss;
oss << in.rdbuf();
std::string filedata = oss.str();
uint64_t hash = fnv1a_64(filedata.data(), filedata.size());
fs::perms file_perms = fs::status(file).permissions();
std::string rel = fs::relative(file, src).string();
std::string var = sanitize(rel);
// Start a scope to limit data's lifetime
cpp << " {\n";
cpp << " // File: " << rel << "\n";
cpp << " fs::path outpath = fs::path(destination_folder) / \"" << rel << "\";\n";
// Embed file data as Base64
cpp << " static const char filedata_base64[] = ";
// Encode the file data to Base64
std::string base64 = base64_encode(reinterpret_cast<const unsigned char*>(filedata.data()), filedata.size());
// Split into 76-character chunks for readability
const size_t line_length = 76;
for (size_t i = 0; i < base64.length(); i += line_length) {
if (i > 0) cpp << "\n ";
cpp << "\"" << base64.substr(i, std::min(line_length, base64.length() - i)) << "\"";
if (i + line_length < base64.length()) cpp << "\\";
}
cpp << ";\n\n";
// Decode Base64 at runtime
cpp << " // Decode Base64 data\n";
cpp << " size_t decoded_size = (strlen(filedata_base64) * 3) / 4;\n";
cpp << " unsigned char* decoded_data = new unsigned char[decoded_size];\n";
cpp << " size_t actual_size;\n";
cpp << " base64_decode(filedata_base64, strlen(filedata_base64), decoded_data, &actual_size);\n\n";
// Call _recreate_file_ with the decoded data
cpp << " bool file_written = _recreate_file_(outpath, "
<< hash << "ULL, std::filesystem::perms(" << static_cast<unsigned>(file_perms) << "), "
<< "decoded_data, actual_size);\n";
// Clean up and update flag
cpp << " delete[] decoded_data;\n";
cpp << " any_written = any_written || file_written;\n";
cpp << " }\n"; // Close scope to free memory
}
cpp << " return any_written;\n";
cpp << "}\n";
cpp << "}\n";
if (!silent) {
std::cout << "[dehydrate] Generated: " << (dest / bothname) << std::endl;
}
}

5
src/generator.hpp Normal file
View File

@@ -0,0 +1,5 @@
#pragma once
#include <string>
void generate_file_code(const std::string& source, const std::string& destfolder, bool silent);
void generate_folder_code(const std::string& source, const std::string& destfolder, bool silent);

33
src/hash.cpp Normal file
View File

@@ -0,0 +1,33 @@
#include "hash.hpp"
#define XXH_INLINE_ALL
#include "xxhash.hpp"
#include <fstream>
#include <sstream>
#include <iomanip>
static std::string to_hex64(uint64_t value) {
std::ostringstream oss;
oss << std::hex << std::setw(16) << std::setfill('0') << value;
return oss.str();
}
std::string hash_data(const std::string& data) {
uint64_t h = XXH3_64bits(data.data(), data.size());
return to_hex64(h);
}
std::string hash_file(const std::string& path) {
std::ifstream file(path, std::ios::binary);
if (!file) return "";
XXH64_state_t* state = XXH64_createState();
XXH64_reset(state, 0);
char buf[4096];
while (file) {
file.read(buf, sizeof(buf));
std::streamsize n = file.gcount();
if (n > 0) XXH64_update(state, buf, static_cast<size_t>(n));
}
uint64_t h = XXH64_digest(state);
XXH64_freeState(state);
return to_hex64(h);
}

5
src/hash.hpp Normal file
View File

@@ -0,0 +1,5 @@
#pragma once
#include <string>
std::string hash_file(const std::string& path);
std::string hash_data(const std::string& data);

97
src/main.cpp Normal file
View File

@@ -0,0 +1,97 @@
#include <iostream>
#include <filesystem>
#include <unistd.h>
#include "argparse.hpp"
#include "generator.hpp"
#include "version.hpp"
std::string get_arch()
{
// determine the architecture of the system
std::string arch;
#ifdef __aarch64__
arch = "arm64";
#elif __x86_64__
arch = "amd64";
#endif
return arch;
}
int update()
{
// determine path to this executable
std::filesystem::path exepath = std::filesystem::canonical("/proc/self/exe");
std::filesystem::path parent_path = exepath.parent_path();
std::string project_name = exepath.filename().string();
// determine the architecture of the system
std::string arch = get_arch();
std::string url = "https://gitea.jde.nz/public/"+project_name+"/releases/download/latest/"+project_name+"." + arch;
// download new version, preserve permissions and ownership
std::string bash_script;
bash_script += "docker run --rm -v "+parent_path.string()+":/target";
bash_script += " gitea.jde.nz/public/debian-curl:latest";
bash_script += " sh -c \"";
bash_script += " curl -fsSL " + url + " -o /target/"+project_name+"_temp &&";
bash_script += " chmod --reference=/target/"+project_name+" /target/"+project_name+"_temp &&";
bash_script += " chown --reference=/target/"+project_name+" /target/"+project_name+"_temp &&";
bash_script += " mv /target/"+project_name+"_temp /target/"+project_name;
bash_script += "\"";
std::cout << "Updating " << exepath << " to the latest " << arch << " version." << std::endl;
// std::cout << "bash_script: " << std::endl
// << bash_script << std::endl;
// run the bash script
execlp("bash", "bash", "-c", bash_script.c_str(), (char *)nullptr);
std::cerr << "Failed to execute command." << std::endl;
return -1;
}
int main(int argc, char* argv[]) {
try {
Args args = parse_args(argc, argv);
// Handle version request (output only version)
if (args.version) {
std::cout << VERSION << std::endl;
return 0;
}
// Handle update request
if (args.update) {
std::cout << "Dehydrate version " << VERSION << std::endl;
return update();
}
// Show version for normal operations (unless silent)
if (!args.silent) {
std::cout << "Dehydrate version " << VERSION << std::endl;
}
std::filesystem::path src(args.source);
if (!std::filesystem::exists(src)) {
std::cerr << "Source does not exist: " << args.source << std::endl;
return 1;
}
if (std::filesystem::is_regular_file(src)) {
generate_file_code(args.source, args.dest, args.silent);
} else if (std::filesystem::is_directory(src)) {
generate_folder_code(args.source, args.dest, args.silent);
} else {
std::cerr << "Source is neither a file nor a directory: " << args.source << std::endl;
return 1;
}
} catch (const std::exception& ex) {
std::cerr << ex.what() << std::endl;
return 1;
}
return 0;
}

1
src/version.hpp.in Normal file
View File

@@ -0,0 +1 @@
static const char *VERSION = "@PROJECT_VERSION@";

186
test.sh Executable file
View File

@@ -0,0 +1,186 @@
#!/bin/bash
set -euo pipefail
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
PROJECT="dehydrate"
# Handle running locally or in Gitea runner
if [ -n "${GITEA_CONTAINER_NAME:-}" ]; then
echo "Running in Gitea CI environment"
echo "GITHUB_WORKSPACE: ${GITHUB_WORKSPACE}"
echo "Current directory: $(pwd)"
OUTPUT_DIR="${GITHUB_WORKSPACE}/dehydrate/output"
TEST_DIR="${GITHUB_WORKSPACE}/dehydrate/test_temp"
else
OUTPUT_DIR="${SCRIPT_DIR}/output"
TEST_DIR="${SCRIPT_DIR}/test_temp"
fi
DEHYDRATE="${OUTPUT_DIR}/${PROJECT}"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Test counters
TESTS_PASSED=0
TESTS_FAILED=0
# Function to print test results
print_test_result() {
local test_name="$1"
local result="$2"
if [ "$result" -eq 0 ]; then
echo -e "${GREEN}${NC} $test_name"
TESTS_PASSED=$((TESTS_PASSED + 1))
else
echo -e "${RED}${NC} $test_name"
TESTS_FAILED=$((TESTS_FAILED + 1))
fi
}
# Function to cleanup test artifacts
cleanup() {
echo -e "\n${YELLOW}Cleaning up test artifacts...${NC}"
rm -rf "$TEST_DIR"
echo -e "\nDone.\n"
}
# Set up trap to ensure cleanup runs
trap cleanup EXIT
# Create test directory
mkdir -p "$TEST_DIR"
echo -e "${YELLOW}Running dehydrate tests...${NC}\n"
# Debug output
echo "Looking for dehydrate at: $DEHYDRATE"
echo "Workspace structure:"
ls -la "${GITHUB_WORKSPACE}" 2>/dev/null || echo "Workspace not found"
echo "Dehydrate directory contents:"
ls -la "${GITHUB_WORKSPACE}/dehydrate" 2>/dev/null || echo "Dehydrate directory not found"
echo "Output directory contents:"
ls -la "$OUTPUT_DIR" 2>/dev/null || echo "Output directory not found"
# Check if dehydrate binary exists
if [ ! -f "$DEHYDRATE" ]; then
echo -e "${RED}Error: dehydrate binary not found at $DEHYDRATE${NC}"
echo "Please run ./build.sh first to build dehydrate"
if [ -n "${GITEA_CONTAINER_NAME:-}" ]; then
echo "Checking if build directory exists..."
BUILD_DIR="${GITHUB_WORKSPACE}/dehydrate/build"
if [ -d "$BUILD_DIR" ]; then
echo "Build directory exists, checking contents:"
ls -la "$BUILD_DIR"
else
echo "Build directory $BUILD_DIR does not exist"
fi
fi
exit 1
fi
if [ ! -x "$DEHYDRATE" ]; then
echo -e "${RED}Error: dehydrate binary is not executable${NC}"
exit 1
fi
echo "Using dehydrate binary: $DEHYDRATE"
# Test 1: Version command (dehydrate -v shows version only)
echo "Test 1: Version command"
VERSION_OUTPUT=$("$DEHYDRATE" -v 2>&1 || true)
# Version output should be just the version number
VERSION=$(echo "$VERSION_OUTPUT" | head -n 1)
if [[ "$VERSION" =~ ^[0-9]{4}\.[0-9]{4}\.[0-9]{4}$ ]]; then
print_test_result "Version format (YYYY.MMDD.HHMM)" 0
else
print_test_result "Version format (YYYY.MMDD.HHMM)" 1
fi
# Test 2: Help command (shows help when no args provided)
printf "\nTest 2: Help command\n"
HELP_OUTPUT=$("$DEHYDRATE" 2>&1 || true)
if [[ "$HELP_OUTPUT" =~ "Usage: dehydrate" ]] && [[ "$HELP_OUTPUT" =~ "Converts existing files" ]]; then
print_test_result "Help command output" 0
else
print_test_result "Help command output" 1
fi
# Test 3: Basic dehydration test
echo -e "\nTest 3: Basic dehydration test"
# Create a test source directory
TEST_SRC_DIR="${TEST_DIR}/test_src"
mkdir -p "$TEST_SRC_DIR"
echo "int main() { return 0; }" > "$TEST_SRC_DIR/main.cpp"
echo "#include <iostream>" > "$TEST_SRC_DIR/header.hpp"
# Run dehydrate on the test source
"$DEHYDRATE" -s "$TEST_SRC_DIR" "$TEST_DIR"
# Dehydrate creates files with pattern _<source_dir_name>.{cpp,hpp}
if [ -f "$TEST_DIR/_test_src.hpp" ] && [ -f "$TEST_DIR/_test_src.cpp" ]; then
print_test_result "Basic dehydration creates output files" 0
else
print_test_result "Basic dehydration creates output files" 1
fi
# Test 4: Test the generated files have valid syntax
echo -e "\nTest 4: Test generated files syntax"
if [ -f "$TEST_DIR/_test_src.hpp" ] && [ -f "$TEST_DIR/_test_src.cpp" ]; then
# Check that the header file has the expected namespace declaration
if grep -q "namespace recreate_test_src" "$TEST_DIR/_test_src.hpp"; then
# Check that the cpp file includes the header
if grep -q "#include \"_test_src.hpp\"" "$TEST_DIR/_test_src.cpp"; then
# Check that the function is declared
if grep -q "recreate_tree" "$TEST_DIR/_test_src.hpp"; then
print_test_result "Generated files have correct structure" 0
else
print_test_result "Generated files have correct structure" 1
fi
else
print_test_result "Generated files have correct structure" 1
fi
else
print_test_result "Generated files have correct structure" 1
fi
else
print_test_result "Generated files have correct structure" 1
fi
cleanup
# Print summary for basic tests
echo -e "\n${YELLOW}Basic Test Summary:${NC}"
echo -e "Tests passed: ${GREEN}${TESTS_PASSED}${NC}"
echo -e "Tests failed: ${RED}${TESTS_FAILED}${NC}"
# Run comprehensive tests if basic tests passed
if [ "$TESTS_FAILED" -eq 0 ]; then
echo -e "\n${YELLOW}Running comprehensive tests...${NC}"
if [ -f "$SCRIPT_DIR/test/test.sh" ]; then
cd "$SCRIPT_DIR/test"
./test.sh
COMPREHENSIVE_RESULT=$?
cd "$SCRIPT_DIR"
if [ "$COMPREHENSIVE_RESULT" -eq 0 ]; then
echo -e "\n${GREEN}All tests passed!${NC}"
exit 0
else
echo -e "\n${RED}Comprehensive tests failed!${NC}"
exit 1
fi
else
echo -e "${YELLOW}Warning: Comprehensive test suite not found${NC}"
echo -e "\n${GREEN}Basic tests passed!${NC}"
exit 0
fi
else
echo -e "\n${RED}Basic tests failed! Skipping comprehensive tests.${NC}"
exit 1
fi

79
test/build_dehydrate_test.sh Executable file
View File

@@ -0,0 +1,79 @@
#!/bin/bash
# Get the directory where this script is located
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
PROJECT_DIR="$( cd "$SCRIPT_DIR/.." && pwd )"
cd "$SCRIPT_DIR" || exit 1
# Clean up old test data and any existing binaries
# Force removal with chmod to handle permission issues
if [ -d dehydrate_test_data ]; then
chmod -R u+w dehydrate_test_data 2>/dev/null || true
rm -rf dehydrate_test_data
fi
rm -f dehydrate_test
# Build the test program using Docker
# The Docker container supports both amd64 and arm64 architectures
echo "Building dehydrate test executable..."
# Use docker cp approach since volume mounting may not work in CI
CONTAINER_NAME="dehydrate-test-build-$$"
# Start container in detached mode
docker run -d --name "$CONTAINER_NAME" \
gitea.jde.nz/public/dropshell-build-base:latest \
sleep 60
# Copy source file into container
docker cp dehydrate_test.cpp "$CONTAINER_NAME":/dehydrate_test.cpp
# Compile in container
docker exec "$CONTAINER_NAME" bash -c "
echo 'Compiling dehydrate test...'
if ! g++ -std=c++23 -static /dehydrate_test.cpp -o /dehydrate_test; then
echo 'ERROR: Compilation failed'
exit 1
fi
# Verify binary was created
if [ ! -f /dehydrate_test ]; then
echo 'ERROR: Binary was not created'
exit 1
fi
# Quick architecture check
if ! file /dehydrate_test | grep -q 'executable'; then
echo 'ERROR: Generated file is not an executable'
file /dehydrate_test
exit 1
fi
echo 'Compilation successful'
"
# Copy binary back to host
docker cp "$CONTAINER_NAME":/dehydrate_test ./dehydrate_test
# Clean up container
docker rm -f "$CONTAINER_NAME"
# Check if compilation succeeded
if [ ! -f "./dehydrate_test" ]; then
echo "Error: Failed to compile dehydrate_test - binary not found"
echo "Files in current directory:"
ls -la
exit 1
fi
# Fix ownership of the binary (created by Docker as root)
if [ "$(stat -c %u ./dehydrate_test)" != "$(id -u)" ]; then
# Use Docker to change ownership to current user
docker run --rm -v "$PROJECT_DIR":/workdir -w /workdir/test \
gitea.jde.nz/public/dropshell-build-base:latest \
chown "$(id -u):$(id -g)" dehydrate_test
fi
# Run the test
./dehydrate_test

BIN
test/dehydrate_test Executable file

Binary file not shown.

384
test/dehydrate_test.cpp Normal file
View File

@@ -0,0 +1,384 @@
#include <iostream>
#include <fstream>
#include <filesystem>
#include <cstring>
#include <vector>
#include <sstream>
#include <iomanip>
#include <sys/wait.h>
#include <unistd.h>
#include <algorithm>
namespace fs = std::filesystem;
// Helper function to execute a command and capture its output
bool execute_command(const std::string& cmd, std::string& output) {
FILE* pipe = popen(cmd.c_str(), "r");
if (!pipe) return false;
char buffer[128];
output.clear();
while (fgets(buffer, sizeof(buffer), pipe) != nullptr) {
output += buffer;
}
int status = pclose(pipe);
return WIFEXITED(status) && WEXITSTATUS(status) == 0;
}
// Helper function to calculate file hash (simple checksum for testing)
std::string calculate_file_hash(const fs::path& filepath) {
std::ifstream file(filepath, std::ios::binary);
if (!file.is_open()) return "";
std::ostringstream oss;
oss << file.rdbuf();
std::string content = oss.str();
// Simple hash for demonstration
size_t hash = 0;
for (char c : content) {
hash = hash * 31 + static_cast<unsigned char>(c);
}
std::stringstream ss;
ss << std::hex << hash;
return ss.str();
}
// Helper function to compare two files
bool compare_files(const fs::path& file1, const fs::path& file2) {
// Compare existence
if (!fs::exists(file1) || !fs::exists(file2)) {
std::cout << "File existence mismatch: " << file1 << " vs " << file2 << std::endl;
return false;
}
// Compare size
if (fs::file_size(file1) != fs::file_size(file2)) {
std::cout << "File size mismatch: " << file1 << " (" << fs::file_size(file1)
<< ") vs " << file2 << " (" << fs::file_size(file2) << ")" << std::endl;
return false;
}
// Compare content
std::ifstream f1(file1, std::ios::binary);
std::ifstream f2(file2, std::ios::binary);
std::string content1((std::istreambuf_iterator<char>(f1)), std::istreambuf_iterator<char>());
std::string content2((std::istreambuf_iterator<char>(f2)), std::istreambuf_iterator<char>());
if (content1 != content2) {
std::cout << "File content mismatch: " << file1 << " vs " << file2 << std::endl;
return false;
}
// Compare permissions
auto perms1 = fs::status(file1).permissions();
auto perms2 = fs::status(file2).permissions();
if (perms1 != perms2) {
std::cout << "File permissions mismatch: " << file1
<< " (" << static_cast<unsigned>(perms1) << ") vs "
<< file2 << " (" << static_cast<unsigned>(perms2) << ")" << std::endl;
return false;
}
return true;
}
// Helper function to compare directories recursively
bool compare_directories(const fs::path& dir1, const fs::path& dir2) {
std::vector<fs::path> files1, files2;
// Collect all files from dir1
for (const auto& entry : fs::recursive_directory_iterator(dir1)) {
if (fs::is_regular_file(entry)) {
files1.push_back(fs::relative(entry.path(), dir1));
}
}
// Collect all files from dir2
for (const auto& entry : fs::recursive_directory_iterator(dir2)) {
if (fs::is_regular_file(entry)) {
files2.push_back(fs::relative(entry.path(), dir2));
}
}
// Sort for comparison
std::sort(files1.begin(), files1.end());
std::sort(files2.begin(), files2.end());
// Check if same files exist
if (files1 != files2) {
std::cout << "Directory structure mismatch!" << std::endl;
return false;
}
// Compare each file
bool all_match = true;
for (const auto& rel_path : files1) {
if (!compare_files(dir1 / rel_path, dir2 / rel_path)) {
all_match = false;
}
}
return all_match;
}
int main() {
std::cout << "=== Dehydrate Test Program ===" << std::endl;
// Create test directory structure
fs::path test_root = "dehydrate_test_data";
fs::path original_dir = test_root / "original";
fs::path generated_dir = test_root / "generated";
fs::path recreated_dir = test_root / "recreated";
// Clean up any existing test data
if (fs::exists(test_root)) {
try {
fs::remove_all(test_root);
} catch (const fs::filesystem_error& e) {
// If removal fails due to permissions, try to fix permissions first
std::cerr << "Warning: Failed to remove test directory, attempting to fix permissions: " << e.what() << std::endl;
try {
// Try to make files writable
for (auto& entry : fs::recursive_directory_iterator(test_root)) {
if (entry.is_regular_file()) {
fs::permissions(entry.path(), fs::perms::owner_write, fs::perm_options::add);
}
}
fs::remove_all(test_root);
} catch (const fs::filesystem_error& e2) {
std::cerr << "Error: Could not clean up test directory: " << e2.what() << std::endl;
std::cerr << "Please manually remove: " << test_root << std::endl;
// Continue anyway - the test might still work
}
}
}
// Create directories
fs::create_directories(original_dir / "subdir");
fs::create_directories(generated_dir);
fs::create_directories(recreated_dir);
std::cout << "\n1. Creating test files..." << std::endl;
// Create test file 1: Simple text file
{
std::ofstream file(original_dir / "test1.txt");
file << "This is a simple text file.\nIt has multiple lines.\nLine 3.";
file.close();
fs::permissions(original_dir / "test1.txt", fs::perms::owner_read | fs::perms::owner_write);
}
// Create test file 2: Binary file with special characters
{
std::ofstream file(original_dir / "test2.bin", std::ios::binary);
unsigned char binary_data[] = {0x00, 0xFF, 0x42, 0x13, 0x37, 0xDE, 0xAD, 0xBE, 0xEF};
file.write(reinterpret_cast<char*>(binary_data), sizeof(binary_data));
file.close();
fs::permissions(original_dir / "test2.bin", fs::perms::owner_all);
}
// Create test file 3: Executable script
{
std::ofstream file(original_dir / "test3.sh");
file << "#!/bin/bash\necho 'Hello from test script'\nexit 0";
file.close();
fs::permissions(original_dir / "test3.sh",
fs::perms::owner_all | fs::perms::group_read | fs::perms::group_exec);
}
// Create test file 4: File in subdirectory
{
std::ofstream file(original_dir / "subdir" / "nested.txt");
file << "This file is in a subdirectory.";
file.close();
fs::permissions(original_dir / "subdir" / "nested.txt", fs::perms::owner_read);
}
// Create test file 5: Very small file
{
std::ofstream file(original_dir / "small.txt");
file << "x"; // Single character to avoid empty file
file.close();
fs::permissions(original_dir / "small.txt", fs::perms::owner_read | fs::perms::owner_write);
}
std::cout << "Created test files in: " << original_dir << std::endl;
// Build dehydrate if not already built
std::cout << "\n2. Building dehydrate tool..." << std::endl;
std::string output;
if (!fs::exists("../output/dehydrate")) {
if (!execute_command("cd .. && ./build.sh", output)) {
std::cerr << "Failed to build dehydrate!" << std::endl;
return 1;
}
}
// Test single file dehydration
std::cout << "\n3. Testing single file dehydration..." << std::endl;
{
std::string cmd = "../output/dehydrate -s " +
(original_dir / "test1.txt").string() + " " +
generated_dir.string();
if (!execute_command(cmd, output)) {
std::cerr << "Failed to dehydrate single file!" << std::endl;
return 1;
}
// Check if generated files exist
if (!fs::exists(generated_dir / "_test1.cpp") ||
!fs::exists(generated_dir / "_test1.hpp")) {
std::cerr << "Generated files not found!" << std::endl;
return 1;
}
std::cout << "Generated: _test1.cpp and _test1.hpp" << std::endl;
}
// Test directory dehydration
std::cout << "\n4. Testing directory dehydration..." << std::endl;
{
std::string cmd = "../output/dehydrate -s " +
original_dir.string() + " " +
generated_dir.string();
if (!execute_command(cmd, output)) {
std::cerr << "Failed to dehydrate directory!" << std::endl;
return 1;
}
// Check if generated files exist
if (!fs::exists(generated_dir / "_original.cpp") ||
!fs::exists(generated_dir / "_original.hpp")) {
std::cerr << "Generated directory files not found!" << std::endl;
return 1;
}
std::cout << "Generated: _original.cpp and _original.hpp" << std::endl;
}
// Create test program that uses the generated code
std::cout << "\n5. Creating test program to recreate files..." << std::endl;
{
std::ofstream test_prog(test_root / "test_recreate.cpp");
test_prog << R"cpp(
#include <iostream>
#include "generated/_test1.hpp"
#include "generated/_original.hpp"
int main() {
std::cout << "Testing file recreation..." << std::endl;
// Test single file recreation
std::cout << "Recreating single file..." << std::endl;
if (recreate_test1::recreate_file("recreated")) {
std::cout << "Single file recreation returned true" << std::endl;
}
// Test directory recreation
std::cout << "Recreating directory tree..." << std::endl;
if (recreate_original::recreate_tree("recreated/tree")) {
std::cout << "Directory recreation returned true" << std::endl;
}
return 0;
}
)cpp";
}
// Compile the test program
std::cout << "\n6. Compiling recreation test program..." << std::endl;
{
std::string cmd = "cd " + test_root.string() +
" && g++ -std=c++23 -static -I. test_recreate.cpp generated/_test1.cpp generated/_original.cpp" +
" -o test_recreate";
if (!execute_command(cmd, output)) {
std::cerr << "Failed to compile test program!" << std::endl;
std::cerr << "Output: " << output << std::endl;
return 1;
}
}
// Run the recreation test
std::cout << "\n7. Running recreation test..." << std::endl;
{
std::string cmd = "cd " + test_root.string() + " && ./test_recreate";
if (!execute_command(cmd, output)) {
std::cerr << "Failed to run recreation test!" << std::endl;
return 1;
}
std::cout << output << std::endl;
}
// Compare results
std::cout << "\n8. Comparing original and recreated files..." << std::endl;
// Compare single file
std::cout << "\nComparing single file recreation:" << std::endl;
if (compare_files(original_dir / "test1.txt", recreated_dir / "test1.txt")) {
std::cout << "✓ Single file matches!" << std::endl;
} else {
std::cout << "✗ Single file does NOT match!" << std::endl;
}
// Compare directory tree
std::cout << "\nComparing directory tree recreation:" << std::endl;
if (compare_directories(original_dir, recreated_dir / "tree")) {
std::cout << "✓ Directory tree matches!" << std::endl;
} else {
std::cout << "✗ Directory tree does NOT match!" << std::endl;
}
// Test re-running recreation (should detect no changes needed)
std::cout << "\n9. Testing re-run (should detect no changes)..." << std::endl;
{
std::string cmd = "cd " + test_root.string() + " && ./test_recreate";
if (!execute_command(cmd, output)) {
std::cerr << "Failed to re-run recreation test!" << std::endl;
return 1;
}
std::cout << output << std::endl;
}
// Modify a file and test update detection
std::cout << "\n10. Testing update detection..." << std::endl;
{
// Modify the recreated file
std::ofstream file(recreated_dir / "test1.txt");
file << "Modified content";
file.close();
// Re-run recreation
std::string cmd = "cd " + test_root.string() + " && ./test_recreate";
if (!execute_command(cmd, output)) {
std::cerr << "Failed to test update!" << std::endl;
return 1;
}
std::cout << output << std::endl;
// Verify it was restored
if (compare_files(original_dir / "test1.txt", recreated_dir / "test1.txt")) {
std::cout << "✓ File correctly restored after modification!" << std::endl;
} else {
std::cout << "✗ File NOT restored correctly!" << std::endl;
}
}
std::cout << "\n=== Test Complete ===" << std::endl;
std::cout << "✓ All dehydrate tests passed successfully!" << std::endl;
return 0;
}

View File

@@ -0,0 +1,188 @@
#include <fstream>
#include <filesystem>
#include <string>
#include <iostream>
#include <cstring>
/*
THIS FILE IS AUTO-GENERATED BY DEHYDRATE.
DO NOT EDIT THIS FILE.
*/
#include "_original.hpp"
namespace recreate_original {
// Tiny dependency-free FNV-1a 64-bit hash
static uint64_t fnv1a_64(const void* data, size_t len) {
const uint8_t* p = static_cast<const uint8_t*>(data);
uint64_t h = 0xcbf29ce484222325ULL;
for (size_t i = 0; i < len; ++i)
h = (h ^ p[i]) * 0x100000001b3ULL;
return h;
}
// Base64 decoding function - no dependencies
static void base64_decode(const char* encoded_data, size_t encoded_len, unsigned char* output, size_t* output_len) {
const char* base64_chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
size_t out_pos = 0;
int val = 0, valb = -8;
for (size_t i = 0; i < encoded_len; i++) {
char c = encoded_data[i];
if (c == '=') break;
// Find position in base64_chars
const char* pos = strchr(base64_chars, c);
if (pos == nullptr) continue; // Skip invalid characters
val = (val << 6) + static_cast<int>(pos - base64_chars);
valb += 6;
if (valb >= 0) {
output[out_pos++] = static_cast<unsigned char>((val >> valb) & 0xFF);
valb -= 8;
}
}
*output_len = out_pos;
}
// Utility function to recreate a file with proper permissions
static bool _recreate_file_(const std::filesystem::path& outpath, uint64_t file_hash, std::filesystem::perms file_perms, const unsigned char* filedata, size_t filedata_len) {
namespace fs = std::filesystem;
bool needs_write = false;
// Check if file exists and has correct hash
if (fs::exists(outpath)) {
// Check content hash
std::ifstream in(outpath, std::ios::binary);
std::ostringstream oss;
oss << in.rdbuf();
std::string data = oss.str();
uint64_t existing_hash = fnv1a_64(data.data(), data.size());
needs_write = existing_hash != file_hash;
} else {
needs_write = true; // File doesn't exist, need to create it
}
bool needs_permission_update = true;
if (!needs_write) { // we always update permissions if the file is written or changed. Othewise we check.
fs::perms current_perms = fs::status(outpath).permissions();
needs_permission_update = current_perms != file_perms;
}
if (needs_write) {
bool existed = fs::exists(outpath);
fs::create_directories(outpath.parent_path());
std::ofstream out(outpath, std::ios::binary);
out.write(reinterpret_cast<const char*>(filedata), filedata_len);
out.close();
// Set the file permissions
fs::permissions(outpath, file_perms);
if (!existed) {
std::cout << "[dehydrate] " << outpath.filename() << ": created\n";
} else {
std::cout << "[dehydrate] " << outpath.filename() << ": updated (hash changed)\n";
}
return true;
}
if (needs_permission_update) {
// Update only permissions
fs::permissions(outpath, file_perms);
std::cout << "[dehydrate] " << outpath.filename() << ": updated (permissions changed)\n";
return true;
}
return false;
}
bool recreate_tree(std::string destination_folder) {
namespace fs = std::filesystem;
bool any_written = false;
{
// File: small.txt
fs::path outpath = fs::path(destination_folder) / "small.txt";
static const char filedata_base64[] = "eA==";
// Decode Base64 data
size_t decoded_size = (strlen(filedata_base64) * 3) / 4;
unsigned char* decoded_data = new unsigned char[decoded_size];
size_t actual_size;
base64_decode(filedata_base64, strlen(filedata_base64), decoded_data, &actual_size);
bool file_written = _recreate_file_(outpath, 12638214688346347271ULL, std::filesystem::perms(384), decoded_data, actual_size);
delete[] decoded_data;
any_written = any_written || file_written;
}
{
// File: test2.bin
fs::path outpath = fs::path(destination_folder) / "test2.bin";
static const char filedata_base64[] = "AP9CEzferb7v";
// Decode Base64 data
size_t decoded_size = (strlen(filedata_base64) * 3) / 4;
unsigned char* decoded_data = new unsigned char[decoded_size];
size_t actual_size;
base64_decode(filedata_base64, strlen(filedata_base64), decoded_data, &actual_size);
bool file_written = _recreate_file_(outpath, 10042072622899139650ULL, std::filesystem::perms(448), decoded_data, actual_size);
delete[] decoded_data;
any_written = any_written || file_written;
}
{
// File: test3.sh
fs::path outpath = fs::path(destination_folder) / "test3.sh";
static const char filedata_base64[] = "IyEvYmluL2Jhc2gKZWNobyAnSGVsbG8gZnJvbSB0ZXN0IHNjcmlwdCcKZXhpdCAw";
// Decode Base64 data
size_t decoded_size = (strlen(filedata_base64) * 3) / 4;
unsigned char* decoded_data = new unsigned char[decoded_size];
size_t actual_size;
base64_decode(filedata_base64, strlen(filedata_base64), decoded_data, &actual_size);
bool file_written = _recreate_file_(outpath, 14335927320996074478ULL, std::filesystem::perms(488), decoded_data, actual_size);
delete[] decoded_data;
any_written = any_written || file_written;
}
{
// File: test1.txt
fs::path outpath = fs::path(destination_folder) / "test1.txt";
static const char filedata_base64[] = "VGhpcyBpcyBhIHNpbXBsZSB0ZXh0IGZpbGUuCkl0IGhhcyBtdWx0aXBsZSBsaW5lcy4KTGluZSAz"\
"Lg==";
// Decode Base64 data
size_t decoded_size = (strlen(filedata_base64) * 3) / 4;
unsigned char* decoded_data = new unsigned char[decoded_size];
size_t actual_size;
base64_decode(filedata_base64, strlen(filedata_base64), decoded_data, &actual_size);
bool file_written = _recreate_file_(outpath, 11900461415522640014ULL, std::filesystem::perms(384), decoded_data, actual_size);
delete[] decoded_data;
any_written = any_written || file_written;
}
{
// File: subdir/nested.txt
fs::path outpath = fs::path(destination_folder) / "subdir/nested.txt";
static const char filedata_base64[] = "VGhpcyBmaWxlIGlzIGluIGEgc3ViZGlyZWN0b3J5Lg==";
// Decode Base64 data
size_t decoded_size = (strlen(filedata_base64) * 3) / 4;
unsigned char* decoded_data = new unsigned char[decoded_size];
size_t actual_size;
base64_decode(filedata_base64, strlen(filedata_base64), decoded_data, &actual_size);
bool file_written = _recreate_file_(outpath, 14153000318456068100ULL, std::filesystem::perms(256), decoded_data, actual_size);
delete[] decoded_data;
any_written = any_written || file_written;
}
return any_written;
}
}

View File

@@ -0,0 +1,15 @@
#pragma once
/*
THIS FILE IS AUTO-GENERATED BY DEHYDRATE.
DO NOT EDIT THIS FILE.
*/
#include <string>
namespace recreate_original {
bool recreate_tree(std::string destination_folder);
}

View File

@@ -0,0 +1,114 @@
#include <fstream>
#include <filesystem>
#include <string>
#include <iostream>
#include <cstring>
// Tiny dependency-free FNV-1a 64-bit hash
static uint64_t fnv1a_64(const void* data, size_t len) {
const uint8_t* p = static_cast<const uint8_t*>(data);
uint64_t h = 0xcbf29ce484222325ULL;
for (size_t i = 0; i < len; ++i)
h = (h ^ p[i]) * 0x100000001b3ULL;
return h;
}
#include "_test1.hpp"
namespace recreate_test1 {
// Base64 decoding function - no dependencies
static void base64_decode(const char* encoded_data, size_t encoded_len, unsigned char* output, size_t* output_len) {
const char* base64_chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
size_t out_pos = 0;
int val = 0, valb = -8;
for (size_t i = 0; i < encoded_len; i++) {
char c = encoded_data[i];
if (c == '=') break;
// Find position in base64_chars
const char* pos = strchr(base64_chars, c);
if (pos == nullptr) continue; // Skip invalid characters
val = (val << 6) + static_cast<int>(pos - base64_chars);
valb += 6;
if (valb >= 0) {
output[out_pos++] = static_cast<unsigned char>((val >> valb) & 0xFF);
valb -= 8;
}
}
*output_len = out_pos;
}
// Utility function to recreate a file with proper permissions
static bool _recreate_file_(const std::filesystem::path& outpath, uint64_t file_hash, std::filesystem::perms file_perms, const unsigned char* filedata, size_t filedata_len) {
namespace fs = std::filesystem;
bool needs_write = false;
// Check if file exists and has correct hash
if (fs::exists(outpath)) {
// Check content hash
std::ifstream in(outpath, std::ios::binary);
std::ostringstream oss;
oss << in.rdbuf();
std::string data = oss.str();
uint64_t existing_hash = fnv1a_64(data.data(), data.size());
needs_write = existing_hash != file_hash;
} else {
needs_write = true; // File doesn't exist, need to create it
}
bool needs_permission_update = true;
if (!needs_write) { // we always update permissions if the file is written or changed. Othewise we check.
fs::perms current_perms = fs::status(outpath).permissions();
needs_permission_update = current_perms != file_perms;
}
if (needs_write) {
bool existed = fs::exists(outpath);
fs::create_directories(outpath.parent_path());
std::ofstream out(outpath, std::ios::binary);
out.write(reinterpret_cast<const char*>(filedata), filedata_len);
out.close();
// Set the file permissions
fs::permissions(outpath, file_perms);
if (!existed) {
std::cout << "[dehydrate] " << outpath.filename() << ": created\n";
} else {
std::cout << "[dehydrate] " << outpath.filename() << ": updated (hash changed)\n";
}
return true;
}
if (needs_permission_update) {
// Update only permissions
fs::permissions(outpath, file_perms);
std::cout << "[dehydrate] " << outpath.filename() << ": updated (permissions changed)\n";
return true;
}
return false;
}
bool recreate_file(std::string destination_folder) {
namespace fs = std::filesystem;
fs::path outpath = fs::path(destination_folder) / "test1.txt";
// File data embedded as Base64
static const char filedata_base64[] = "VGhpcyBpcyBhIHNpbXBsZSB0ZXh0IGZpbGUuCkl0IGhhcyBtdWx0aXBsZSBsaW5lcy4KTGluZSAz"\
"Lg==";
// Decode Base64 data
size_t decoded_size = (strlen(filedata_base64) * 3) / 4;
unsigned char* decoded_data = new unsigned char[decoded_size];
size_t actual_size;
base64_decode(filedata_base64, strlen(filedata_base64), decoded_data, &actual_size);
bool result = _recreate_file_(outpath, 11900461415522640014ULL, std::filesystem::perms(384), decoded_data, actual_size);
delete[] decoded_data;
return result;
}
}

View File

@@ -0,0 +1,5 @@
#pragma once
#include <string>
namespace recreate_test1 {
bool recreate_file(std::string destination_folder);
}

View File

@@ -0,0 +1 @@
x

View File

@@ -0,0 +1 @@
This file is in a subdirectory.

View File

@@ -0,0 +1,3 @@
This is a simple text file.
It has multiple lines.
Line 3.

Binary file not shown.

View File

@@ -0,0 +1,3 @@
#!/bin/bash
echo 'Hello from test script'
exit 0

View File

@@ -0,0 +1,3 @@
This is a simple text file.
It has multiple lines.
Line 3.

View File

@@ -0,0 +1 @@
x

View File

@@ -0,0 +1 @@
This file is in a subdirectory.

View File

@@ -0,0 +1,3 @@
This is a simple text file.
It has multiple lines.
Line 3.

Binary file not shown.

View File

@@ -0,0 +1,3 @@
#!/bin/bash
echo 'Hello from test script'
exit 0

Binary file not shown.

View File

@@ -0,0 +1,22 @@
#include <iostream>
#include "generated/_test1.hpp"
#include "generated/_original.hpp"
int main() {
std::cout << "Testing file recreation..." << std::endl;
// Test single file recreation
std::cout << "Recreating single file..." << std::endl;
if (recreate_test1::recreate_file("recreated")) {
std::cout << "Single file recreation returned true" << std::endl;
}
// Test directory recreation
std::cout << "Recreating directory tree..." << std::endl;
if (recreate_original::recreate_tree("recreated/tree")) {
std::cout << "Directory recreation returned true" << std::endl;
}
return 0;
}

14
test/test.sh Executable file
View File

@@ -0,0 +1,14 @@
#!/bin/bash
set -euo pipefail
# Simple script to run the dehydrate tests
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
cd "$SCRIPT_DIR"
echo "Running dehydrate tests..."
./build_dehydrate_test.sh
echo "Dehydrate tests complete."