diff --git a/.vscode/settings.json b/.vscode/settings.json index adee848..2e64a28 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -83,6 +83,12 @@ "variant": "cpp", "format": "cpp", "stdfloat": "cpp", - "__nullptr": "cpp" + "__nullptr": "cpp", + "*.ipp": "cpp", + "__hash_table": "cpp", + "__split_buffer": "cpp", + "__tree": "cpp", + "queue": "cpp", + "stack": "cpp" } } \ No newline at end of file diff --git a/dropshell-tool/CMakeLists.txt b/dropshell-tool/CMakeLists.txt index 4f89eb3..9938db7 100644 --- a/dropshell-tool/CMakeLists.txt +++ b/dropshell-tool/CMakeLists.txt @@ -1,6 +1,7 @@ cmake_minimum_required(VERSION 3.10) set(PROJECT_EXE_NAME dropshell-tool) project(${PROJECT_EXE_NAME} VERSION 1.0.0 LANGUAGES CXX) +cmake_policy(SET CMP0135 NEW) # Force static linking globally set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static") @@ -9,7 +10,15 @@ set(BUILD_SHARED_LIBS OFF CACHE BOOL "Build shared libraries" FORCE) set(CMAKE_POSITION_INDEPENDENT_CODE OFF) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -static") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -static") -set(ZLIB_USE_STATIC_LIBS "ON") +set(ZLIB_USE_STATIC_LIBS ON) + +# Ensure zlib is built as static only +set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) +set(ZLIB_BUILD_SHARED OFF CACHE BOOL "" FORCE) +set(ZLIB_BUILD_STATIC ON CACHE BOOL "" FORCE) +# Disable zlib examples and minigzip +set(ZLIB_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) +set(ZLIB_BUILD_MINIGZIP OFF CACHE BOOL "" FORCE) set(CMAKE_CXX_STANDARD 23) set(CMAKE_C_STANDARD 23) @@ -86,18 +95,23 @@ FetchContent_MakeAvailable(cpptrace) FetchContent_Declare( nlohmann_json GIT_REPOSITORY https://github.com/nlohmann/json.git - GIT_TAG v3.11.3 + GIT_TAG v3.12.0 ) FetchContent_MakeAvailable(nlohmann_json) -find_package(OpenSSL REQUIRED) -target_link_libraries(${PROJECT_EXE_NAME} PRIVATE OpenSSL::SSL OpenSSL::Crypto) +# Fetch and build zlib from source (static) +FetchContent_Declare( + zlib + URL https://zlib.net/zlib-1.3.1.tar.gz +) +FetchContent_MakeAvailable(zlib) # Link libraries target_link_libraries(${PROJECT_EXE_NAME} PRIVATE libassert::assert cpptrace::cpptrace nlohmann_json::nlohmann_json + zlibstatic ) # Set static linking flags @@ -109,3 +123,9 @@ find_package(OpenSSL REQUIRED) target_compile_definitions(${PROJECT_EXE_NAME} PRIVATE CPPHTTPLIB_OPENSSL_SUPPORT) target_link_libraries(${PROJECT_EXE_NAME} PRIVATE OpenSSL::SSL OpenSSL::Crypto) +# --- MUSL CROSS-COMPILATION --- +# To ensure all dependencies (including zlib) are built with musl, set your toolchain file or: +# set(CMAKE_C_COMPILER "/path/to/musl-gcc") +# set(CMAKE_CXX_COMPILER "/path/to/musl-g++") +# BEFORE the project() call above. + diff --git a/dropshell-tool/src/ArchiveManager.cpp b/dropshell-tool/src/ArchiveManager.cpp index 983034c..19cb13f 100644 --- a/dropshell-tool/src/ArchiveManager.cpp +++ b/dropshell-tool/src/ArchiveManager.cpp @@ -1,24 +1,212 @@ -#include "ArchiveManager.hpp" #include +#include +#include +#include +#include +#include +#include +#include + +#include "tar_to_stream.hpp" +#include "ArchiveManager.hpp" + +namespace fs = std::filesystem; + +namespace { +constexpr size_t TAR_BLOCK_SIZE = 512; + +// --- Minimal tar extraction logic for unpacking and config extraction --- +struct TarHeader { + char name[100]; + char mode[8]; + char uid[8]; + char gid[8]; + char size[12]; + char mtime[12]; + char chksum[8]; + char typeflag; + char linkname[100]; + char magic[6]; + char version[2]; + char uname[32]; + char gname[32]; + char devmajor[8]; + char devminor[8]; + char prefix[155]; + char pad[12]; +}; + +size_t tarFileSize(const TarHeader* hdr) { + return std::strtol(hdr->size, nullptr, 8); +} + +std::string tarFileName(const TarHeader* hdr) { + std::string name(hdr->name); + if (hdr->prefix[0]) { + std::string prefix(hdr->prefix); + return prefix + "/" + name; + } + return name; +} + +bool readTar(const std::vector& tarData, const std::string& outDir, std::string* configJson = nullptr) { + size_t pos = 0; + while (pos + sizeof(TarHeader) <= tarData.size()) { + const TarHeader* hdr = reinterpret_cast(&tarData[pos]); + if (hdr->name[0] == '\0') break; + size_t filesize = tarFileSize(hdr); + std::string filename = tarFileName(hdr); + size_t fileStart = pos + sizeof(TarHeader); + if (configJson && filename == "dropshell-tool-config.json") { + *configJson = std::string(reinterpret_cast(&tarData[fileStart]), filesize); + } else if (!outDir.empty()) { + fs::path outPath = fs::path(outDir) / filename; + fs::create_directories(outPath.parent_path()); + std::ofstream ofs(outPath, std::ios::binary); + ofs.write(reinterpret_cast(&tarData[fileStart]), filesize); + } + pos = fileStart + filesize; + pos += (TAR_BLOCK_SIZE - (filesize % TAR_BLOCK_SIZE)) % TAR_BLOCK_SIZE; + } + return true; +} + +bool extractConfigJson(const std::vector& tarData, std::string& outJson) { + return readTar(tarData, "", &outJson); +} + +bool replaceConfigJson(std::vector& tarData, const std::string& json) { + // Remove old config, add new one at end + std::vector newTar; + size_t pos = 0; + bool replaced = false; + while (pos + sizeof(TarHeader) <= tarData.size()) { + const TarHeader* hdr = reinterpret_cast(&tarData[pos]); + if (hdr->name[0] == '\0') break; + size_t filesize = tarFileSize(hdr); + std::string filename = tarFileName(hdr); + size_t fileStart = pos + sizeof(TarHeader); + if (filename != "dropshell-tool-config.json") { + newTar.insert(newTar.end(), &tarData[pos], &tarData[fileStart + filesize]); + size_t pad = (TAR_BLOCK_SIZE - (filesize % TAR_BLOCK_SIZE)) % TAR_BLOCK_SIZE; + newTar.insert(newTar.end(), pad, 0); + } else { + replaced = true; + } + pos = fileStart + filesize; + pos += (TAR_BLOCK_SIZE - (filesize % TAR_BLOCK_SIZE)) % TAR_BLOCK_SIZE; + } + // Add new config + // Use tar_to_stream to add the config file + std::ostringstream oss(std::ios::binary); + oss.write(reinterpret_cast(newTar.data()), newTar.size()); + std::string uname = "root"; + std::string gname = "root"; + tar_to_stream_properties props = { + "dropshell-tool-config.json", + std::as_bytes(std::span(json.data(), json.size())), + 0u, "644", 0u, 0u, uname, gname + }; + tar_to_stream(oss, std::move(props)); + tar_to_stream_tail(oss); + std::string tarStr = oss.str(); + tarData.assign(tarStr.begin(), tarStr.end()); + return true; +} + +// --- zlib helpers --- +bool gzipCompress(const std::string& in, std::vector& out) { + z_stream strm{}; + deflateInit2(&strm, Z_BEST_COMPRESSION, Z_DEFLATED, 15 + 16, 8, Z_DEFAULT_STRATEGY); + out.resize(compressBound(in.size())); + strm.next_in = reinterpret_cast(const_cast(in.data())); + strm.avail_in = in.size(); + strm.next_out = out.data(); + strm.avail_out = out.size(); + int ret = deflate(&strm, Z_FINISH); + if (ret != Z_STREAM_END) { deflateEnd(&strm); return false; } + out.resize(strm.total_out); + deflateEnd(&strm); + return true; +} + +bool gzipDecompress(const std::string& inPath, std::vector& out) { + std::ifstream ifs(inPath, std::ios::binary); + if (!ifs) return false; + ifs.seekg(0, std::ios::end); + size_t insize = ifs.tellg(); + ifs.seekg(0, std::ios::beg); + std::vector inbuf(insize); + ifs.read(reinterpret_cast(inbuf.data()), insize); + z_stream strm{}; + inflateInit2(&strm, 15 + 16); + out.resize(insize * 10); // crude guess + strm.next_in = inbuf.data(); + strm.avail_in = inbuf.size(); + strm.next_out = out.data(); + strm.avail_out = out.size(); + int ret = inflate(&strm, Z_FINISH); + if (ret != Z_STREAM_END) { inflateEnd(&strm); return false; } + out.resize(strm.total_out); + inflateEnd(&strm); + return true; +} + +bool gzipDecompressToTar(const std::string& inPath, std::vector& tarData) { + return gzipDecompress(inPath, tarData); +} + +bool gzipCompressToFile(const std::string& tarData, const std::string& outPath) { + std::vector gz; + if (!gzipCompress(tarData, gz)) return false; + std::ofstream ofs(outPath, std::ios::binary); + if (!ofs) return false; + ofs.write(reinterpret_cast(gz.data()), gz.size()); + return ofs.good(); +} +} ArchiveManager::ArchiveManager() {} bool ArchiveManager::pack(const std::string& folderPath, const std::string& archivePath) { - // TODO: Implement packing logic - return false; + // Use tar_to_stream to create tar in memory + std::ostringstream tarStream(std::ios::binary); + for (auto& p : fs::recursive_directory_iterator(folderPath)) { + if (!fs::is_regular_file(p)) continue; + std::ifstream ifs(p.path(), std::ios::binary); + if (!ifs) return false; + std::vector data((std::istreambuf_iterator(ifs)), {}); + std::string uname = "root"; + std::string gname = "root"; + tar_to_stream_properties props = { + fs::relative(p.path(), folderPath).generic_string(), + std::as_bytes(std::span(data.data(), data.size())), + 0u, "644", 0u, 0u, uname, gname + }; + tar_to_stream(tarStream, std::move(props)); + } + tar_to_stream_tail(tarStream); + std::string tarData = tarStream.str(); + return gzipCompressToFile(tarData, archivePath); } bool ArchiveManager::unpack(const std::string& archivePath, const std::string& outDir) { - // TODO: Implement unpacking logic - return false; + std::vector tarData; + if (!gzipDecompressToTar(archivePath, tarData)) return false; + return readTar(tarData, outDir); } bool ArchiveManager::readConfigJson(const std::string& archivePath, std::string& outJson) { - // TODO: Implement config extraction logic - return false; + std::vector tarData; + if (!gzipDecompressToTar(archivePath, tarData)) return false; + return extractConfigJson(tarData, outJson); } bool ArchiveManager::writeConfigJson(const std::string& archivePath, const std::string& json) { - // TODO: Implement config writing logic - return false; + std::vector tarData; + if (!gzipDecompressToTar(archivePath, tarData)) return false; + if (!replaceConfigJson(tarData, json)) return false; + // Write back to archivePath + std::string tarStr(reinterpret_cast(tarData.data()), tarData.size()); + return gzipCompressToFile(tarStr, archivePath); } \ No newline at end of file diff --git a/dropshell-tool/src/main.cpp b/dropshell-tool/src/main.cpp index e96a212..f6c48ae 100644 --- a/dropshell-tool/src/main.cpp +++ b/dropshell-tool/src/main.cpp @@ -173,7 +173,9 @@ int publish_tool(int argc, char* argv[]) { std::cerr << "dropshell-tool-config.json not found in " << folder << std::endl; return 1; } - std::filesystem::path archivePath = std::filesystem::path(folder) / (labeltag + ".tgz"); + std::filesystem::path archivePath = std::filesystem::path(home) / ".tmp" / (labeltag + ".tgz"); + std::filesystem::create_directories(archivePath.parent_path()); + ArchiveManager archiver; if (!archiver.pack(folder, archivePath.string())) { std::cerr << "Failed to create archive." << std::endl; diff --git a/dropshell-tool/src/tar_to_stream.hpp b/dropshell-tool/src/tar_to_stream.hpp new file mode 100644 index 0000000..6adebc7 --- /dev/null +++ b/dropshell-tool/src/tar_to_stream.hpp @@ -0,0 +1,100 @@ +#pragma once + +#include +#include +#include +#include +#include + +struct tar_to_stream_properties { + /// Properties of the file to enter into the stream + std::string const &filename; /// name of the file to write + std::span data; /// the location of the file's contents in memory + uint64_t mtime{0u}; /// file modification time, in seconds since epoch + std::string filemode{"644"}; /// file mode + unsigned int uid{0u}; /// file owner user ID + unsigned int gid{0u}; /// file owner group ID + std::string const &uname{"root"}; /// file owner username + std::string const &gname{"root"}; /// file owner group name +}; + +template +void tar_to_stream(T &stream, /// stream to write to, e.g. ostream or ofstream + tar_to_stream_properties &&file) { /// properties of the file to enter into the stream + /// Read a "file" in memory, and write it as a TAR archive to the stream + struct { // offset + char name[100]{}; // 0 filename + char mode[8]{}; // 100 file mode: 0000644 etc + char uid[8]{}; // 108 user id, ascii representation of octal value: "0001750" (for UID 1000) + char gid[8]{}; // 116 group id, ascii representation of octal value: "0001750" (for GID 1000) + char size[12]{}; // 124 file size, ascii representation of octal value + char mtime[12]{"00000000000"}; // 136 modification time, seconds since epoch + char chksum[8]{' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '}; // 148 checksum: six octal bytes followed by null and ' '. Checksum is the octal sum of all bytes in the header, with chksum field set to 8 spaces. + char typeflag{'0'}; // 156 '0' + char linkname[100]{}; // 157 null bytes when not a link + char magic[6]{'u', 's', 't', 'a', 'r', ' '}; // 257 format: Unix Standard TAR: "ustar ", not null-terminated + char version[2]{" "}; // 263 " " + char uname[32]{}; // 265 user name + char gname[32]{}; // 297 group name + char devmajor[8]{}; // 329 null bytes + char devminor[8]{}; // 337 null bytes + char prefix[155]{}; // 345 null bytes + char padding[12]{}; // 500 padding to reach 512 block size + } header; // 512 + + file.filemode.insert(file.filemode.begin(), 7 - file.filemode.length(), '0'); // zero-pad the file mode + + std::strncpy(header.name, file.filename.c_str(), sizeof(header.name ) - 1); // leave one char for the final null + std::strncpy(header.mode, file.filemode.c_str(), sizeof(header.mode ) - 1); + std::strncpy(header.uname, file.uname.c_str(), sizeof(header.uname) - 1); + std::strncpy(header.gname, file.gname.c_str(), sizeof(header.gname) - 1); + + sprintf(header.size, "%011lo", file.data.size()); + sprintf(header.mtime, "%011llo", file.mtime); + sprintf(header.uid, "%07o", file.uid); + sprintf(header.gid, "%07o", file.gid); + + { + unsigned int checksum_value = 0; + for(size_t i{0}; i != sizeof(header); ++i) { + checksum_value += reinterpret_cast(&header)[i]; + } + sprintf(header.chksum, "%06o", checksum_value); + } + + size_t const padding{(512u - file.data.size() % 512) & 511u}; + stream << std::string_view{header.name, sizeof(header)} + << std::string_view{reinterpret_cast(file.data.data()), file.data.size()} + << std::string(padding, '\0'); +} + +template +[[deprecated("Use tar_to_stream_properties as argument: tar_to_stream(stream, {...}) - this allows use of designated initialisers and cleaner code. Refer to tar_to_stream's README for example usage")]] +void tar_to_stream(T &stream, /// stream to write to, e.g. ostream or ofstream + std::string const &filename, /// name of the file to write + char const *data_ptr, /// pointer to the data in this archive segment + size_t data_size, /// size of the data + uint64_t mtime = 0u, /// file modification time, in seconds since epoch + std::string filemode = "644", /// file mode + unsigned int uid = 0u, /// file owner user ID + unsigned int gid = 0u, /// file owner group ID + std::string const &uname = "root", /// file owner username + std::string const &gname = "root") { /// file owner group name + /// Explicit argument constructor, for backwards compatibility + tar_to_stream(stream, tar_to_stream_properties{ + .filename{filename}, + .data{std::as_bytes(std::span{data_ptr, data_size})}, + .mtime{mtime}, + .filemode{filemode}, + .uid{uid}, + .gid{gid}, + .uname{uname}, + .gname{gname}, + }); +} + +template +void tar_to_stream_tail(T &stream, unsigned int tail_length = 512u * 2u) { + /// TAR archives expect a tail of null bytes at the end - min of 512 * 2, but implementations often add more + stream << std::string(tail_length, '\0'); +} diff --git a/dropshell-tool/tool/dropshell-tool b/dropshell-tool/tool/dropshell-tool new file mode 100755 index 0000000..3920b50 Binary files /dev/null and b/dropshell-tool/tool/dropshell-tool differ