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
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:
parent
0065a41012
commit
dd0fc37798
25
dehydrate/.gitignore
vendored
Normal file
25
dehydrate/.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
dehydrate/CMakeLists.txt
Normal file
38
dehydrate/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
|
||||
include
|
||||
contrib)
|
66
dehydrate/Dockerfile.dropshell-build
Normal file
66
dehydrate/Dockerfile.dropshell-build
Normal 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
57
dehydrate/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.
|
24
dehydrate/build.sh
Executable file
24
dehydrate/build.sh
Executable 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
7343
dehydrate/contrib/xxhash.hpp
Normal file
File diff suppressed because it is too large
Load Diff
1
dehydrate/include/.gitkeep
Normal file
1
dehydrate/include/.gitkeep
Normal file
@ -0,0 +1 @@
|
||||
# This file ensures the include directory is tracked by git.
|
11
dehydrate/include/argparse.hpp
Normal file
11
dehydrate/include/argparse.hpp
Normal 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[]);
|
5
dehydrate/include/generator.hpp
Normal file
5
dehydrate/include/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);
|
5
dehydrate/include/hash.hpp
Normal file
5
dehydrate/include/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);
|
51
dehydrate/install.sh
Executable file
51
dehydrate/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)"
|
80
dehydrate/publish.sh
Executable file
80
dehydrate/publish.sh
Executable 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
1
dehydrate/src/.gitkeep
Normal file
@ -0,0 +1 @@
|
||||
# This file ensures the src directory is tracked by git.
|
45
dehydrate/src/argparse.cpp
Normal file
45
dehydrate/src/argparse.cpp
Normal 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
372
dehydrate/src/generator.cpp
Normal 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
33
dehydrate/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);
|
||||
}
|
86
dehydrate/src/main.cpp
Normal file
86
dehydrate/src/main.cpp
Normal 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;
|
||||
}
|
1
dehydrate/src/version.hpp.in
Normal file
1
dehydrate/src/version.hpp.in
Normal file
@ -0,0 +1 @@
|
||||
static const char *VERSION = "@PROJECT_VERSION@";
|
135
dehydrate/test.sh
Executable file
135
dehydrate/test.sh
Executable 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
|
Loading…
x
Reference in New Issue
Block a user