test: Update 19 files
All checks were successful
Build-Test-Publish / build (linux/amd64) (push) Successful in 34s
Build-Test-Publish / build (linux/arm64) (push) Successful in 44s
Build-Test-Publish / test-install-from-scratch (linux/amd64) (push) Successful in 8s
Build-Test-Publish / test-install-from-scratch (linux/arm64) (push) Successful in 8s

This commit is contained in:
Your Name 2025-06-22 23:10:39 +12:00
parent 0065a41012
commit dd0fc37798
19 changed files with 8379 additions and 0 deletions

25
dehydrate/.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
dehydrate/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
include
contrib)

View File

@ -0,0 +1,66 @@
ARG IMAGE_TAG
FROM gitea.jde.nz/public/dropshell-build-base:latest AS builder
ARG PROJECT
ARG CMAKE_BUILD_TYPE=Debug
# Set working directory
WORKDIR /app
SHELL ["/bin/bash", "-c"]
# Create cache directories
RUN mkdir -p /ccache
# Set up ccache
ENV CCACHE_DIR=/ccache
ENV CCACHE_COMPILERCHECK=content
ENV CCACHE_MAXSIZE=2G
# Copy build files
COPY CMakeLists.txt ./
COPY src/version.hpp.in src/
# Copy source files
COPY src/ src/
COPY include/ include/
COPY contrib/ contrib/
# Configure project
RUN --mount=type=cache,target=/ccache \
--mount=type=cache,target=/build \
mkdir -p /build && \
cmake -G Ninja -S /app -B /build \
-DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
-DCMAKE_EXE_LINKER_FLAGS="-fuse-ld=mold -static -g" \
-DCMAKE_CXX_FLAGS="-g -fno-omit-frame-pointer" \
-DCMAKE_C_FLAGS="-g -fno-omit-frame-pointer" \
-DPROJECT_NAME="${PROJECT}" \
-DCMAKE_STRIP=OFF \
${CMAKE_TOOLCHAIN_FILE:+-DCMAKE_TOOLCHAIN_FILE=$CMAKE_TOOLCHAIN_FILE}
# Build project
RUN --mount=type=cache,target=/ccache \
--mount=type=cache,target=/build \
cmake --build /build
# Copy the built executable to a regular directory for the final stage
RUN --mount=type=cache,target=/build \
mkdir -p /output && \
find /build -type f -executable -name "*${PROJECT}*" -exec cp {} /output/${PROJECT} \; || \
find /build -type f -executable -exec cp {} /output/${PROJECT} \;
# if we're a release build, then run upx on the binary.
RUN if [ "${CMAKE_BUILD_TYPE}" = "Release" ]; then \
upx /output/${PROJECT}; \
fi
# Final stage that only contains the binary
FROM scratch AS project
ARG PROJECT
# Copy the actual binary from the regular directory
COPY --from=builder /output/${PROJECT} /${PROJECT}

57
dehydrate/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.

24
dehydrate/build.sh Executable file
View File

@ -0,0 +1,24 @@
#!/bin/bash
set -euo pipefail
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
export CMAKE_BUILD_TYPE="Debug"
rm -rf "${SCRIPT_DIR}/output"
mkdir -p "${SCRIPT_DIR}/output"
PROJECT="dehydrate"
# make sure we have the latest base image.
docker pull gitea.jde.nz/public/dropshell-build-base:latest
docker build \
-t "${PROJECT}-build" \
-f "${SCRIPT_DIR}/Dockerfile.dropshell-build" \
--build-arg PROJECT="${PROJECT}" \
--build-arg CMAKE_BUILD_TYPE="${CMAKE_BUILD_TYPE}" \
--output "${SCRIPT_DIR}/output" \
"${SCRIPT_DIR}"

7343
dehydrate/contrib/xxhash.hpp Normal file

File diff suppressed because it is too large Load Diff

View File

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

View File

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

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);

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);

51
dehydrate/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)"

80
dehydrate/publish.sh Executable file
View File

@ -0,0 +1,80 @@
#!/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"
docker build \
-t "${PROJECT}-build" \
-f "${SCRIPT_DIR}/Dockerfile.dropshell-build" \
--build-arg PROJECT="${PROJECT}" \
--build-arg CMAKE_BUILD_TYPE="${CMAKE_BUILD_TYPE}" \
--output "${OUTPUT}" \
"${SCRIPT_DIR}"
[ -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
dehydrate/src/.gitkeep Normal file
View File

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

View File

@ -0,0 +1,45 @@
#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
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
)";
Args parse_args(int argc, char* argv[]) {
Args args;
int idx = 1;
// Check for silent flag
if (idx < argc && std::string(argv[idx]) == "-s") {
args.silent = true;
idx++;
}
// Check for update flag
if (idx < argc && std::string(argv[idx]) == "-u") {
args.update = true;
idx++;
return args; // No need for source and dest parameters when updating
}
// 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;
}

372
dehydrate/src/generator.cpp Normal file
View File

@ -0,0 +1,372 @@
#include "generator.hpp"
#include "../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;
}
}

33
dehydrate/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);
}

86
dehydrate/src/main.cpp Normal file
View File

@ -0,0 +1,86 @@
#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 {
std::cout << "Dehydrate version " << VERSION << std::endl;
Args args = parse_args(argc, argv);
// Handle update request
if (args.update) {
return update();
}
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;
}

View File

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

135
dehydrate/test.sh Executable file
View File

@ -0,0 +1,135 @@
#!/bin/bash
# Don't use set -e because we want to continue even if tests fail
set -uo pipefail
PROJECT="dehydrate"
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
DEHYDRATE="${SCRIPT_DIR}/output/${PROJECT}"
TEST_DIR="${SCRIPT_DIR}/test_temp"
# 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++))
else
echo -e "${RED}${NC} $test_name"
((TESTS_FAILED++))
fi
}
# Function to cleanup test artifacts
cleanup() {
echo -e "\n${YELLOW}Cleaning up test artifacts...${NC}"
rm -rf "$TEST_DIR"
}
# 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"
# 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"
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 shows version in help output)
echo "Test 1: Version command"
VERSION_OUTPUT=$("$DEHYDRATE" 2>&1 || true)
# Extract version from the beginning of help output
VERSION=$(echo "$VERSION_OUTPUT" | head -n 1 | sed 's/Dehydrate version //')
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)
echo -e "\nTest 2: Help command"
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_OUTPUT=$("$DEHYDRATE" "$TEST_SRC_DIR" "$TEST_DIR" 2>&1 || true)
# 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
echo -e "\n${YELLOW}Test Summary:${NC}"
echo -e "Tests passed: ${GREEN}${TESTS_PASSED}${NC}"
echo -e "Tests failed: ${RED}${TESTS_FAILED}${NC}"
if [ "$TESTS_FAILED" -eq 0 ]; then
echo -e "\n${GREEN}All tests passed!${NC}"
exit 0
else
echo -e "\n${RED}Some tests failed!${NC}"
exit 1
fi