commit dd03db14d7b11101ce93b173183cb00f35d2e120 Author: Your Name Date: Sat Aug 9 16:44:26 2025 +1200 First commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c056369 --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# Build artifacts +bb64 +bb64.arm64 +bb64.amd64 + +# Musl cross toolchains +x86_64-linux-musl-cross/ +aarch64-linux-musl-cross/ +.musl-cross/ + +# Temporary files +*.tgz +*.tmp +*.swp +*~ + +# Logs +*.log + +# Secrets/tokens +.env +*.token + +# Misc +VERSION \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..a9e6b62 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,69 @@ +{ + "files.associations": { + "*.inja": "jinja-html", + "cctype": "cpp", + "clocale": "cpp", + "cmath": "cpp", + "cstdarg": "cpp", + "cstddef": "cpp", + "cstdio": "cpp", + "cstdlib": "cpp", + "cstring": "cpp", + "ctime": "cpp", + "cwchar": "cpp", + "cwctype": "cpp", + "array": "cpp", + "atomic": "cpp", + "bit": "cpp", + "*.tcc": "cpp", + "bitset": "cpp", + "charconv": "cpp", + "chrono": "cpp", + "compare": "cpp", + "concepts": "cpp", + "condition_variable": "cpp", + "cstdint": "cpp", + "deque": "cpp", + "string": "cpp", + "unordered_map": "cpp", + "vector": "cpp", + "exception": "cpp", + "algorithm": "cpp", + "functional": "cpp", + "iterator": "cpp", + "memory": "cpp", + "memory_resource": "cpp", + "numeric": "cpp", + "optional": "cpp", + "random": "cpp", + "ratio": "cpp", + "string_view": "cpp", + "system_error": "cpp", + "tuple": "cpp", + "type_traits": "cpp", + "utility": "cpp", + "initializer_list": "cpp", + "iomanip": "cpp", + "iosfwd": "cpp", + "iostream": "cpp", + "istream": "cpp", + "limits": "cpp", + "mutex": "cpp", + "new": "cpp", + "numbers": "cpp", + "ostream": "cpp", + "semaphore": "cpp", + "span": "cpp", + "sstream": "cpp", + "stdexcept": "cpp", + "stop_token": "cpp", + "streambuf": "cpp", + "thread": "cpp", + "cinttypes": "cpp", + "typeinfo": "cpp", + "variant": "cpp", + "format": "cpp", + "__nullptr": "cpp", + "codecvt": "cpp" + } +} \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..5267058 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,37 @@ +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=") +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 +add_executable(${PROJECT_NAME} + src/bb64.cpp + src/b64ed.cpp +) + +# Configure version.hpp +configure_file("src/version.hpp.in" "src/autogen/version.hpp" @ONLY) + +# Include directories +target_include_directories(${PROJECT_NAME} PRIVATE + ${CMAKE_CURRENT_BINARY_DIR}/src/autogen + src + ) + +# Link libraries +target_link_libraries(${PROJECT_NAME} PRIVATE + ) + \ No newline at end of file diff --git a/Dockerfile.dropshell-build b/Dockerfile.dropshell-build new file mode 100644 index 0000000..f6fa58c --- /dev/null +++ b/Dockerfile.dropshell-build @@ -0,0 +1,74 @@ +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 only build files first (for better layer caching) +#COPY CMakeLists.txt cmake_prebuild.sh ./ +#COPY src/version.hpp.in src/ + +# Run prebuild script early (this rarely changes) +#RUN bash cmake_prebuild.sh + +# Copy source files (this invalidates cache when source changes) +COPY src/ src/ +COPY CMakeLists.txt ./ + +# Configure project (this step is cached unless CMakeLists.txt changes) +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 (ccache will help here when only some files change) +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 && \ + if [ -f "/build/${PROJECT}" ]; then \ + echo "Found executable at /build/${PROJECT}" && \ + cp "/build/${PROJECT}" "/output/${PROJECT}"; \ + else \ + echo "Executable not found at /build/${PROJECT}, searching..." && \ + find /build -type f -executable -name "*${PROJECT}*" -exec cp {} /output/${PROJECT} \; || \ + find /build -type f -executable -exec cp {} /output/${PROJECT} \; || \ + (echo "Error: Could not find executable for ${PROJECT}" && ls -la /build && exit 1); \ + fi + +# 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} + diff --git a/README.md b/README.md new file mode 100644 index 0000000..a499d27 --- /dev/null +++ b/README.md @@ -0,0 +1,44 @@ +# bb64 + +# Installation + +Automated system-wide installation: +``` +curl -fsSL https://gitea.jde.nz/public/bb64/releases/download/latest/install.sh | bash +``` + +## To download just the bb64 executable: + +``` +curl -fsSL -o bb64 https://gitea.jde.nz/public/bb64/releases/download/latest/bb64.amd64 && chmod a+x bb64 +``` + + +# Use + +Bash Base64, written in C++. + +Uses a custom Base64 character set for bash compatibility, not compatible with other utilities. + +``` +Usage: + bb64 BASE64COMMAND Decodes and runs the command + bb64 -[i|d] BASE64COMMAND Displays the decoded command + bb64 -e COMMAND Encodes the command and prints the result + bb64 -u Updates bb64 to the latest version (uses docker) + bb64 -v Prints the version number + bb64 version Prints the version number +``` + +# Implementation Notes + +bb64 runs the command by replacing the current process, so it ensures that tty, environment +variables etc are all identical for the run command. It works with interactive commands, like +nano or ssh. + +bb64 supports bash scripts, as the command is run as: + `bash -c 'COMMAND'` +Where COMMAND is passed to bash as a single argument. + +If the command is run, the return value is the return value of the command. +If it isn't run, bb64 returns -1. diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..32ce623 --- /dev/null +++ b/build.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +set -euo pipefail + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" +PROJECT="bb64" + +export CMAKE_BUILD_TYPE="Debug" + +rm -rf "${SCRIPT_DIR}/output" +mkdir -p "${SCRIPT_DIR}/output" + +# make sure we have the latest base image. +docker pull gitea.jde.nz/public/dropshell-build-base:latest + +# Build with or without cache based on NO_CACHE environment variable +CACHE_FLAG="" +if [ "${NO_CACHE:-false}" = "true" ]; then + CACHE_FLAG="--no-cache" +fi + +docker build \ + ${CACHE_FLAG} \ + -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}" + diff --git a/clean.sh b/clean.sh new file mode 100755 index 0000000..340cb09 --- /dev/null +++ b/clean.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +set -euo pipefail + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" +PROJECT="bb64" + +echo "Cleaning ${PROJECT}..." + +# Remove output directory +if [ -d "${SCRIPT_DIR}/output" ]; then + echo "Removing output directory..." + rm -rf "${SCRIPT_DIR}/output" +fi + +# Remove Docker images related to this project +echo "Removing Docker images..." +docker images --filter "reference=${PROJECT}-build*" -q | xargs -r docker rmi -f + +# Remove Docker build cache +echo "Pruning Docker build cache..." +docker builder prune -f + +echo "✓ ${PROJECT} cleaned successfully" \ No newline at end of file diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..4eb31d9 --- /dev/null +++ b/install.sh @@ -0,0 +1,69 @@ +#!/bin/bash +set -e + +# Installs bb64 on the local machine. +# 1. determines the architecture of the local machine +# 2. downloads the appropriate bb64 binary from the latest public release on Gitea (https://gitea.jde.nz/public/bb64/releases) +# 3. makes the bb64 binary executable +# 4. moves the bb64 binary to /usr/local/bin +# 5. prints a message to the user + +# 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 + echo "Installing bb64 to $INSTALL_DIR" + if [[ ! -d "$INSTALL_DIR" ]]; then + mkdir -p "$INSTALL_DIR" + fi +fi + +# 0. see if we were passed a user to chown to +# ----------------------------------------------------------------------------- +CHOWN_USER="$2" +if [[ -z "$CHOWN_USER" ]]; then + CHOWN_USER=$(id -u) +fi + +# 1. Determine architecture +# ----------------------------------------------------------------------------- + +ARCH=$(uname -m) +if [[ "$ARCH" == "x86_64" ]]; then + BIN=bb64.amd64 +elif [[ "$ARCH" == "aarch64" || "$ARCH" == "arm64" ]]; then + BIN=bb64.arm64 +else + echo "Unsupported architecture: $ARCH" >&2 + exit 1 +fi + +# 3. Download the appropriate binary to a temp directory +# ----------------------------------------------------------------------------- +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT +URL="https://gitea.jde.nz/public/bb64/releases/download/latest/$BIN" +echo "Downloading $BIN from $URL..." + +curl -fsSL -o "$TMPDIR/bb64" "$URL" + +# 4. Make it executable +# ----------------------------------------------------------------------------- +chmod +x "$TMPDIR/bb64" + +# 5. Move to /usr/local/bin +# ----------------------------------------------------------------------------- +docker run --rm -v "$TMPDIR:/tmp" -v "$INSTALL_DIR:/target" alpine sh -c "cp /tmp/bb64 /target/bb64; chown $CHOWN_USER /target/bb64" +rm "$TMPDIR/bb64" + +# 6. Print success message +# ----------------------------------------------------------------------------- +echo "bb64 installed successfully to $INSTALL_DIR/bb64 (arch $ARCH)" +# echo " " +# echo "Update bb64 with:" +# echo " bb64 -u" +# echo " " +# echo "try it out with:" +# echo " bb64 ZWNobyAiSGVsbG8td29ybGQhIGJiNjQgaXMgd29ya2luZy4i" diff --git a/publish.sh b/publish.sh new file mode 100755 index 0000000..3bf8f0e --- /dev/null +++ b/publish.sh @@ -0,0 +1,209 @@ +#!/bin/bash +set -euo pipefail + +# Publishes bb64 to the Gitea Releases page for the repository. +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" +ARCH=$(uname -m) +PROJECT="bb64" +OUTPUT="${SCRIPT_DIR}/output" + +ARCH_ALIAS="amd64" +if [ "$ARCH" = "aarch64" ]; then + ARCH_ALIAS="arm64" +fi + +# Get version from CMake timestamp +VERSION=$(date +"%Y.%m%d.%H%M") +TAG="v$VERSION" +echo "Building version $VERSION" >&2 + +# build release version +export CMAKE_BUILD_TYPE="Release" + +# Build with or without cache based on NO_CACHE environment variable +CACHE_FLAG="" +if [ "${NO_CACHE:-false}" = "true" ]; then + CACHE_FLAG="--no-cache" +fi + +docker build \ + ${CACHE_FLAG} \ + -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}" + +if [ ! -f "${OUTPUT}/${PROJECT}" ]; then + echo "Build failed." >&2 + exit 1 +fi + +cp "${OUTPUT}/${PROJECT}" "${OUTPUT}/${PROJECT}.${ARCH_ALIAS}" +cp "${OUTPUT}/${PROJECT}" "${OUTPUT}/${PROJECT}.${ARCH}" + +# Find repo info from .git/config +REPO_URL=$(git config --get remote.origin.url) +if [[ ! $REPO_URL =~ gitea ]]; then + echo "Remote origin is not a Gitea repository: $REPO_URL" >&2 + exit 1 +fi +# Extract base URL, owner, and repo +# Example: https://gitea.example.com/username/reponame.git +BASE_URL=$(echo "$REPO_URL" | sed -E 's#(https?://[^/]+)/.*#\1#') +OWNER=$(echo "$REPO_URL" | sed -E 's#.*/([^/]+)/[^/]+(\.git)?$#\1#') +REPO=$(echo "$REPO_URL" | sed -E 's#.*/([^/]+)(\.git)?$#\1#') + +API_URL="$BASE_URL/api/v1/repos/$OWNER/$REPO" + +# Create release +RELEASE_DATA=$(cat <&2 + exit 1 +fi + +# Create and push git tag +echo "Creating git tag $TAG..." + +# Configure git identity if not set (for CI environments) +if ! git config user.email >/dev/null 2>&1; then + git config user.email "ci@gitea.jde.nz" + git config user.name "CI Bot" +fi + +# Check if tag already exists locally +if git rev-parse "$TAG" >/dev/null 2>&1; then + echo "Tag $TAG already exists locally, deleting it first..." + git tag -d "$TAG" +fi + +# Check if tag exists on remote +TAG_EXISTS_ON_REMOTE=false +if git ls-remote --tags origin | grep -q "refs/tags/$TAG"; then + echo "Tag $TAG already exists on remote - this is expected for multi-architecture builds" + echo "Skipping tag creation and proceeding with release attachment..." + TAG_EXISTS_ON_REMOTE=true +else + echo "Creating new tag $TAG..." + git tag -a "$TAG" -m "Release $TAG" + if ! git push origin "$TAG"; then + echo "Failed to push tag $TAG to origin" >&2 + # Try to delete local tag if push failed + git tag -d "$TAG" + exit 1 + fi + echo "Successfully created and pushed tag $TAG" +fi + +echo "Getting or creating release $TAG on Gitea..." + +# First try to get existing release +EXISTING_RELEASE=$(curl -s -X GET "$API_URL/releases/tags/$TAG" \ + -H "Authorization: token $RELEASE_WRITE_TOKEN") + +echo "Existing release check response: $EXISTING_RELEASE" >&2 + +if echo "$EXISTING_RELEASE" | grep -q '"id":[0-9]*'; then + # Release already exists, get its ID + RELEASE_ID=$(echo "$EXISTING_RELEASE" | grep -o '"id":[0-9]*' | head -1 | cut -d: -f2) + echo "Release $TAG already exists with ID: $RELEASE_ID" +else + # Create new release only if tag was just created + if [ "$TAG_EXISTS_ON_REMOTE" = true ]; then + echo "Tag exists on remote but no release found - this shouldn't happen" >&2 + echo "API response was: $EXISTING_RELEASE" >&2 + exit 1 + fi + + echo "Creating new release $TAG on Gitea..." + RELEASE_RESPONSE=$(curl -s -X POST "$API_URL/releases" \ + -H "Content-Type: application/json" \ + -H "Authorization: token $RELEASE_WRITE_TOKEN" \ + -d "$RELEASE_DATA") + + echo "Release API response: $RELEASE_RESPONSE" + + RELEASE_ID=$(echo "$RELEASE_RESPONSE" | grep -o '"id":[0-9]*' | head -1 | cut -d: -f2) + + if [ -z "$RELEASE_ID" ]; then + echo "Failed to create release on Gitea." >&2 + echo "API URL: $API_URL/releases" >&2 + echo "Release data: $RELEASE_DATA" >&2 + exit 1 + fi + + echo "Created new release with ID: $RELEASE_ID" +fi + +# Upload binaries and install.sh +echo "Uploading assets to release..." +for FILE in ${PROJECT}.${ARCH_ALIAS} ${PROJECT}.${ARCH} install.sh; do + if [ -f "output/$FILE" ]; then + filetoupload="output/$FILE" + elif [ -f "$FILE" ]; then + filetoupload="$FILE" + else + echo "Skipping $FILE - not found" + continue + fi + + echo "Uploading $filetoupload as $FILE..." + # Auto-detect content type + ctype=$(file --mime-type -b "$filetoupload") + + UPLOAD_RESPONSE=$(curl -s -X POST "$API_URL/releases/$RELEASE_ID/assets?name=$FILE" \ + -H "Content-Type: $ctype" \ + -H "Authorization: token $RELEASE_WRITE_TOKEN" \ + --data-binary @"$filetoupload") + + if echo "$UPLOAD_RESPONSE" | grep -q '"id"'; then + echo "✓ Uploaded $FILE to release $TAG as $ctype." + else + echo "✗ Failed to upload $FILE. Response: $UPLOAD_RESPONSE" >&2 + exit 1 + fi +done + +echo "Published bb64 version $VERSION to $REPO_URL (tag $TAG) with binaries for $ARCH_ALIAS / $ARCH." + + +#-------------------------------------------------------------------------------- +echo "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 + echo "Publishing ${PROJECT} to getpkg.xyz using ${GETPKG}..." + if "${GETPKG}" publish "${PROJECT}:${ARCH}" "${TOOLDIR}"; then + echo "✓ Successfully published ${PROJECT} to getpkg.xyz" + else + echo "✗ Failed to publish ${PROJECT} to getpkg.xyz" >&2 + exit 1 + fi +else + echo "Warning: getpkg not found at $GETPKG, skipping tool publishing to getpkg.xyz" +fi + +echo "✓ BB64 publish script completed successfully" diff --git a/src/b64ed.cpp b/src/b64ed.cpp new file mode 100644 index 0000000..cec8e1f --- /dev/null +++ b/src/b64ed.cpp @@ -0,0 +1,42 @@ +#include "b64ed.hpp" + +#include + +// Custom base64 encoding/decoding tables +static const std::string custom_base64_chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789+_"; + +std::string base64_encode(const std::string &in) { + std::string out; + int val = 0, valb = -6; + for (unsigned char c : in) { + val = (val << 8) + c; + valb += 8; + while (valb >= 0) { + out.push_back(custom_base64_chars[(val >> valb) & 0x3F]); + valb -= 6; + } + } + if (valb > -6) out.push_back(custom_base64_chars[((val << 8) >> (valb + 8)) & 0x3F]); + while (out.size() % 4) out.push_back('='); + return out; +} + +std::string base64_decode(const std::string &in) { + std::vector T(256, -1); + for (int i = 0; i < 64; i++) T[custom_base64_chars[i]] = i; + std::string out; + int val = 0, valb = -8; + for (unsigned char c : in) { + if (T[c] == -1) break; + val = (val << 6) + T[c]; + valb += 6; + if (valb >= 0) { + out.push_back(char((val >> valb) & 0xFF)); + valb -= 8; + } + } + return out; +} \ No newline at end of file diff --git a/src/b64ed.hpp b/src/b64ed.hpp new file mode 100644 index 0000000..fa71d44 --- /dev/null +++ b/src/b64ed.hpp @@ -0,0 +1,9 @@ +#ifndef B64ED_HPP +#define B64ED_HPP + +#include + +std::string base64_decode(const std::string &in); +std::string base64_encode(const std::string &in); + +#endif diff --git a/src/bb64.cpp b/src/bb64.cpp new file mode 100644 index 0000000..ec8e002 --- /dev/null +++ b/src/bb64.cpp @@ -0,0 +1,208 @@ +#include +#include +#include +#include +#include +#include +#include +#include "version.hpp" +#include "b64ed.hpp" + +// Recursively decode and print if nested bb64 command is found +void recursive_print(const std::string &decoded) +{ + std::cout << std::string(80, '-') << std::endl; + std::cout << decoded << std::endl; + std::cout << std::string(80, '-') << std::endl; + + size_t pos = decoded.find("bb64 "); + if (pos != std::string::npos) + { + std::istringstream iss(decoded.substr(pos)); + std::string cmd, arg; + iss >> cmd >> arg; + if (cmd == "bb64" && !arg.empty()) + { + std::string nested = base64_decode(arg); + std::cout << " "; + std::cout << "nested: " << nested << std::endl; + recursive_print(nested); + } + } +} + +constexpr unsigned int hash(const char *s, int off = 0) +{ + return !s[off] ? 5381 : (hash(s, off + 1) * 33) ^ s[off]; +} + +std::string tidy(const std::string &str) +{ + std::string result; + bool in_whitespace = false; + for (char c : str) + { + // Remove non-printable characters except for whitespace (space, tab, newline, carriage return) + if ((static_cast(c) < 32 && c != ' ' && c != '\t' && c != '\n' && c != '\r') || static_cast(c) == 127) + { + continue; + } + if (c == ' ' || c == '\t' || c == '\n' || c == '\r') + { + if (!in_whitespace) + { + result += ' '; + in_whitespace = true; + } + } + else + { + result += c; + in_whitespace = false; + } + } + // Remove leading whitespace + size_t start = result.find_first_not_of(' '); + if (start == std::string::npos) + return ""; + // Remove trailing whitespace + size_t end = result.find_last_not_of(' '); + return result.substr(start, end - start + 1); +} + + +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_bb64() +{ + // determine path to this executable + std::filesystem::path bb64_path = std::filesystem::canonical("/proc/self/exe"); + std::filesystem::path parent_path = bb64_path.parent_path(); + + // determine the architecture of the system + std::string arch = get_arch(); + + std::string url = "https://gitea.jde.nz/public/bb64/releases/download/latest/bb64." + 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/bb64_temp &&"; + bash_script += " chmod --reference=/target/bb64 /target/bb64_temp &&"; + bash_script += " chown --reference=/target/bb64 /target/bb64_temp &&"; + bash_script += " mv /target/bb64_temp /target/bb64"; + bash_script += "\""; + + std::cout << "Updating " << bb64_path << " 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 decode_and_run(const std::string &encoded) +{ + // Default: decode and run + std::string decoded = base64_decode(encoded); + if (decoded.empty()) + { + std::cerr << "Failed to decode base64 command." << std::endl; + return -1; + } + // Replace current process with bash -c "decoded" + execlp("bash", "bash", "-c", decoded.c_str(), (char *)nullptr); + // If execlp returns, there was an error + std::cerr << "Failed to execute command." << std::endl; + return -1; +} + +int main(int argc, char *argv[]) +{ + if (argc < 2) + { + std::cerr << "bb64 version " << VERSION << ", by J842." << std::endl; + // heredoc for instructions + std::cerr << R"( + +Usage: + bb64 BASE64COMMAND Decodes and runs the command + bb64 -[i|d] BASE64COMMAND Displays the decoded command + + bb64 -e COMMAND Encodes the command and prints the base64 encoded result + bb64 -e Encodes the command provided on stdin and prints the result + + bb64 -u Updates bb64 to the latest version (uses docker) + + bb64 -v Prints the version number + bb64 version Prints the version number + +)" << std::endl; + return -1; + } + + std::string mode = argv[1]; + + if (argc == 2) + { + if (mode == "-u") + return update_bb64(); + else if (mode == "-v" || mode == "version") + { + std::cout << VERSION << std::endl; + return 0; + } + else if (mode == "-e") + { + std::ostringstream oss; + while (std::cin) + { + std::string line; + std::getline(std::cin, line); + oss << line << std::endl; + } + std::string tidier = tidy(oss.str()); + std::cout << base64_encode(tidier) << std::endl; + return 0; + } + else + return decode_and_run(mode); + } + + std::ostringstream oss; + std::string tidier; + + switch (hash(mode.c_str())) + { + case hash("-i"): + case hash("-d"): + std::cout << "Decoding command..." << std::endl + << std::endl; + recursive_print(base64_decode(argv[2])); + break; + case hash("-e"): + for (int i = 2; i < argc; ++i) + oss << (i > 2 ? " " : "") << argv[i]; + tidier = tidy(oss.str()); + std::cout << base64_encode(tidier) << std::endl; + break; + default: + std::cerr << "Invalid mode: " << mode << std::endl; + return -1; + }; +} \ No newline at end of file diff --git a/src/version.hpp.in b/src/version.hpp.in new file mode 100644 index 0000000..d0617bd --- /dev/null +++ b/src/version.hpp.in @@ -0,0 +1 @@ +static const char *VERSION = "@PROJECT_VERSION@"; \ No newline at end of file diff --git a/test.sh b/test.sh new file mode 100755 index 0000000..9aa8e75 --- /dev/null +++ b/test.sh @@ -0,0 +1,135 @@ +#!/bin/bash + +set -euo pipefail + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +PROJECT="bb64" +BB64="$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=$((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" +} + +# Set up trap to ensure cleanup runs +trap cleanup EXIT + +# Create test directory +mkdir -p "$TEST_DIR" + +echo -e "${YELLOW}Running bb64 tests...${NC}\n" + +# Check if bb64 binary exists +if [ ! -f "$BB64" ]; then + echo -e "${RED}Error: bb64 binary not found at $BB64${NC}" + echo "Please run ./build.sh first to build bb64" + exit 1 +fi + +if [ ! -x "$BB64" ]; then + echo -e "${RED}Error: bb64 binary is not executable${NC}" + exit 1 +fi + +echo "Using bb64 binary: $BB64" + +# Test 1: Version command with -v flag +echo "Test 1: Version command (-v flag)" +VERSION_OUTPUT=$("$BB64" -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 with -v flag (YYYY.MMDD.HHMM)" 0 +else + print_test_result "Version format with -v flag (YYYY.MMDD.HHMM)" 1 + echo " Expected: YYYY.MMDD.HHMM format, got: '$VERSION'" +fi + +# Test 2: Version command with 'version' argument +printf "\nTest 2: Version command (version argument)\n" +VERSION_OUTPUT2=$("$BB64" version 2>&1 || true) +# Version output should be just the version number +VERSION2=$(echo "$VERSION_OUTPUT2" | head -n 1) +if [[ "$VERSION2" =~ ^[0-9]{4}\.[0-9]{4}\.[0-9]{4}$ ]]; then + print_test_result "Version format with 'version' argument (YYYY.MMDD.HHMM)" 0 +else + print_test_result "Version format with 'version' argument (YYYY.MMDD.HHMM)" 1 + echo " Expected: YYYY.MMDD.HHMM format, got: '$VERSION2'" +fi + +# Test 3: Both version commands should return the same version +printf "\nTest 3: Version consistency\n" +if [ "$VERSION" = "$VERSION2" ]; then + print_test_result "Both -v and version return same version" 0 +else + print_test_result "Both -v and version return same version" 1 + echo " -v returned: '$VERSION'" + echo " version returned: '$VERSION2'" +fi + +# Test 4: Basic encoding test +echo -e "\nTest 4: Basic encoding test" +TEST_STRING="hello world" +ENCODED_OUTPUT=$("$BB64" -e <<< "$TEST_STRING" 2>&1 || true) +if [ -n "$ENCODED_OUTPUT" ]; then + print_test_result "Basic encoding produces output" 0 +else + print_test_result "Basic encoding produces output" 1 +fi + +# Test 5: Basic decoding test (using -d flag) +echo -e "\nTest 5: Basic decoding test" +# Encode "echo hello" and then decode it +ENCODED_ECHO=$(echo "echo hello" | "$BB64" -e) +if [ -n "$ENCODED_ECHO" ]; then + DECODED_OUTPUT=$("$BB64" -d "$ENCODED_ECHO" 2>&1 || true) + if [[ "$DECODED_OUTPUT" == *"echo hello"* ]]; then + print_test_result "Basic decoding works correctly" 0 + else + print_test_result "Basic decoding works correctly" 1 + echo " Expected to contain 'echo hello', got: '$DECODED_OUTPUT'" + fi +else + print_test_result "Basic decoding works correctly" 1 + echo " Failed to encode test string" +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 \ No newline at end of file