first commit
This commit is contained in:
25
.gitignore
vendored
Normal file
25
.gitignore
vendored
Normal 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
38
CMakeLists.txt
Normal 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
57
README.md
Normal 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
52
build.sh
Executable 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
18
clean.sh
Executable 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
7343
contrib/xxhash.hpp
Normal file
File diff suppressed because it is too large
Load Diff
51
install.sh
Executable file
51
install.sh
Executable 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
73
publish.sh
Executable 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
1
src/.gitkeep
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# This file ensures the src directory is tracked by git.
|
63
src/argparse.cpp
Normal file
63
src/argparse.cpp
Normal 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
12
src/argparse.hpp
Normal 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
372
src/generator.cpp
Normal 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
5
src/generator.hpp
Normal 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
33
src/hash.cpp
Normal 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
5
src/hash.hpp
Normal 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
97
src/main.cpp
Normal 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
1
src/version.hpp.in
Normal file
@@ -0,0 +1 @@
|
|||||||
|
static const char *VERSION = "@PROJECT_VERSION@";
|
186
test.sh
Executable file
186
test.sh
Executable 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
79
test/build_dehydrate_test.sh
Executable 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
BIN
test/dehydrate_test
Executable file
Binary file not shown.
384
test/dehydrate_test.cpp
Normal file
384
test/dehydrate_test.cpp
Normal 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;
|
||||||
|
}
|
188
test/dehydrate_test_data/generated/_original.cpp
Normal file
188
test/dehydrate_test_data/generated/_original.cpp
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
15
test/dehydrate_test_data/generated/_original.hpp
Normal file
15
test/dehydrate_test_data/generated/_original.hpp
Normal 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);
|
||||||
|
}
|
114
test/dehydrate_test_data/generated/_test1.cpp
Normal file
114
test/dehydrate_test_data/generated/_test1.cpp
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
5
test/dehydrate_test_data/generated/_test1.hpp
Normal file
5
test/dehydrate_test_data/generated/_test1.hpp
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <string>
|
||||||
|
namespace recreate_test1 {
|
||||||
|
bool recreate_file(std::string destination_folder);
|
||||||
|
}
|
1
test/dehydrate_test_data/original/small.txt
Normal file
1
test/dehydrate_test_data/original/small.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
x
|
1
test/dehydrate_test_data/original/subdir/nested.txt
Normal file
1
test/dehydrate_test_data/original/subdir/nested.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
This file is in a subdirectory.
|
3
test/dehydrate_test_data/original/test1.txt
Normal file
3
test/dehydrate_test_data/original/test1.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
This is a simple text file.
|
||||||
|
It has multiple lines.
|
||||||
|
Line 3.
|
BIN
test/dehydrate_test_data/original/test2.bin
Executable file
BIN
test/dehydrate_test_data/original/test2.bin
Executable file
Binary file not shown.
3
test/dehydrate_test_data/original/test3.sh
Executable file
3
test/dehydrate_test_data/original/test3.sh
Executable file
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
echo 'Hello from test script'
|
||||||
|
exit 0
|
3
test/dehydrate_test_data/recreated/test1.txt
Normal file
3
test/dehydrate_test_data/recreated/test1.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
This is a simple text file.
|
||||||
|
It has multiple lines.
|
||||||
|
Line 3.
|
1
test/dehydrate_test_data/recreated/tree/small.txt
Normal file
1
test/dehydrate_test_data/recreated/tree/small.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
x
|
@@ -0,0 +1 @@
|
|||||||
|
This file is in a subdirectory.
|
3
test/dehydrate_test_data/recreated/tree/test1.txt
Normal file
3
test/dehydrate_test_data/recreated/tree/test1.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
This is a simple text file.
|
||||||
|
It has multiple lines.
|
||||||
|
Line 3.
|
BIN
test/dehydrate_test_data/recreated/tree/test2.bin
Executable file
BIN
test/dehydrate_test_data/recreated/tree/test2.bin
Executable file
Binary file not shown.
3
test/dehydrate_test_data/recreated/tree/test3.sh
Executable file
3
test/dehydrate_test_data/recreated/tree/test3.sh
Executable file
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
echo 'Hello from test script'
|
||||||
|
exit 0
|
BIN
test/dehydrate_test_data/test_recreate
Executable file
BIN
test/dehydrate_test_data/test_recreate
Executable file
Binary file not shown.
22
test/dehydrate_test_data/test_recreate.cpp
Normal file
22
test/dehydrate_test_data/test_recreate.cpp
Normal 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
14
test/test.sh
Executable 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."
|
Reference in New Issue
Block a user