Compare commits
13 Commits
Prerelease
...
main
Author | SHA1 | Date | |
---|---|---|---|
1970342ae0 | |||
8edd019ca8 | |||
a49108c4d2 | |||
f36652ad67 | |||
f89cbb96cc | |||
79b9e546ee | |||
021ca575d1 | |||
29bfd46843 | |||
5a88cb6296 | |||
70d44d069a | |||
8cab591f98 | |||
0e17d02451 | |||
44c2ea3a3a |
@@ -1,15 +1,63 @@
|
||||
cmake_minimum_required(VERSION 3.18..3.28)
|
||||
project(ReArchive)
|
||||
|
||||
set(ArchiveProjectVersion 1.1) # The current revision of the project.
|
||||
set(ArchiveFormatVersion 1.0) # The current version of Redacted Software Archive specification.
|
||||
set(ArchiveAppVersion 1.1) # The current version of rsarchive.exe
|
||||
|
||||
|
||||
project(ReArchive
|
||||
VERSION ${ArchiveProjectVersion}
|
||||
LANGUAGES CXX)
|
||||
|
||||
|
||||
if (PROJECT_SOURCE_DIR STREQUAL PROJECT_BINARY_DIR)
|
||||
message(FATAL_ERROR "In-source builds are not allowed")
|
||||
endif()
|
||||
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
|
||||
file(GLOB_RECURSE HEADERS "include/*.h" "include/*.hpp")
|
||||
file(GLOB_RECURSE SOURCES "src/*.c" "src/*.cpp")
|
||||
|
||||
if(UNIX)
|
||||
add_library(ReArchive SHARED ${SOURCES})
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
add_library(ReArchive STATIC ${SOURCES})
|
||||
endif()
|
||||
|
||||
include(cmake/CPM.cmake)
|
||||
|
||||
CPMAddPackage(NAME mcolor
|
||||
URL https://git.redacted.cc/maxine/mcolor/archive/Release-1.zip)
|
||||
|
||||
CPMAddPackage(NAME Endianness
|
||||
URL https://git.redacted.cc/josh/Endianness/archive/Release-1.zip)
|
||||
|
||||
|
||||
target_compile_definitions(ReArchive PUBLIC ARCHIVE_PROJECT_VERSION=${ArchiveProjectVersion})
|
||||
target_compile_definitions(ReArchive PUBLIC ARCHIVE_FORMAT_VERSION=${ArchiveFormatVersion})
|
||||
|
||||
|
||||
set_target_properties(ReArchive PROPERTIES LINKER_LANGUAGE CXX)
|
||||
target_include_directories(ReArchive PUBLIC ${PROJECT_SOURCE_DIR}/include)
|
||||
target_include_directories(ReArchive PRIVATE ${Endianness_SOURCE_DIR}/include)
|
||||
target_link_libraries(ReArchive PRIVATE Endianness)
|
||||
|
||||
add_executable(ReArchive_Demo main.cpp)
|
||||
target_link_libraries(ReArchive_Demo PUBLIC ReArchive)
|
||||
add_executable(rsarchive main.cpp)
|
||||
|
||||
target_compile_definitions(rsarchive PRIVATE ARCHIVE_APP_VERSION=${ArchiveAppVersion})
|
||||
|
||||
target_link_libraries(rsarchive PUBLIC ReArchive)
|
||||
target_include_directories(rsarchive PRIVATE ${mcolor_SOURCE_DIR}/include)
|
||||
target_link_libraries(rsarchive PRIVATE mcolor)
|
||||
|
||||
if(NOT CMAKE_BUILD_TYPE)
|
||||
set(CMAKE_BUILD_TYPE Release CACHE STRING "Choose the build type" FORCE)
|
||||
endif()
|
||||
|
||||
include(GNUInstallDirs)
|
||||
set(CMAKE_INSTALL_PREFIX "/usr" CACHE PATH "Install path prefix" FORCE)
|
||||
install(TARGETS ReArchive DESTINATION ${CMAKE_INSTALL_LIBDIR})
|
||||
install(TARGETS rsarchive DESTINATION ${CMAKE_INSTALL_BINDIR})
|
35
README.md
Normal file
35
README.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Redacted Software Archive
|
||||
|
||||
Yet Another Archival Format :D
|
||||
|
||||
The ReArchive project is a lightweight C++ library designed for creating, managing, and extracting archive files. It provides a foundation for bundling multiple files and directories into a single archive, ideal for game assets (mod distribution), application deployments, and many more tasks. Included with the library is `rsarchive`, a command-line application demonstrating ReArchive's capabilities and providing a general-purpose archive management tool.
|
||||
|
||||
## Features
|
||||
|
||||
* General
|
||||
* Custom bespoke archive format.
|
||||
* Included CLI archive program.
|
||||
* C++20 API for integrating archives into your project.
|
||||
* Public Domain Source Code, Format, & Application.
|
||||
* **ZERO** dependencies. Just C++ and CMake.
|
||||
* Library API Features
|
||||
* Bundle files together for easier distribution.
|
||||
* Integrate easily into your C++ project.
|
||||
* Cross-platform: Designed with Redacted Software signature portability, simplicity, and
|
||||
* Archive Format
|
||||
* Custom, stream-friendly archive format.
|
||||
* Designed for efficiency and extensibility.
|
||||
* Efficient file retrieval even from large archives.
|
||||
* Lightning fast. (See Benchmarks.)
|
||||
* rsarchive Application:
|
||||
* Robust command-line utility.
|
||||
* Create, Extract, Inspect, Modify, and Validate archive files.
|
||||
* Append and remove files individually.
|
||||
* Supported on Windows & Linux.
|
||||
* Designed to be extended and modified.
|
||||
|
||||
|
||||
|
||||
|
||||
## Acknowledgements
|
||||
|
24
cmake/CPM.cmake
Normal file
24
cmake/CPM.cmake
Normal file
@@ -0,0 +1,24 @@
|
||||
# SPDX-License-Identifier: MIT
|
||||
#
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2019-2023 Lars Melchior and contributors
|
||||
|
||||
set(CPM_DOWNLOAD_VERSION 0.38.7)
|
||||
set(CPM_HASH_SUM "83e5eb71b2bbb8b1f2ad38f1950287a057624e385c238f6087f94cdfc44af9c5")
|
||||
|
||||
if(CPM_SOURCE_CACHE)
|
||||
set(CPM_DOWNLOAD_LOCATION "${CPM_SOURCE_CACHE}/cpm/CPM_${CPM_DOWNLOAD_VERSION}.cmake")
|
||||
elseif(DEFINED ENV{CPM_SOURCE_CACHE})
|
||||
set(CPM_DOWNLOAD_LOCATION "$ENV{CPM_SOURCE_CACHE}/cpm/CPM_${CPM_DOWNLOAD_VERSION}.cmake")
|
||||
else()
|
||||
set(CPM_DOWNLOAD_LOCATION "${CMAKE_BINARY_DIR}/cmake/CPM_${CPM_DOWNLOAD_VERSION}.cmake")
|
||||
endif()
|
||||
|
||||
# Expand relative path. This is important if the provided path contains a tilde (~)
|
||||
get_filename_component(CPM_DOWNLOAD_LOCATION ${CPM_DOWNLOAD_LOCATION} ABSOLUTE)
|
||||
|
||||
file(DOWNLOAD
|
||||
https://github.com/cpm-cmake/CPM.cmake/releases/download/v${CPM_DOWNLOAD_VERSION}/CPM.cmake
|
||||
${CPM_DOWNLOAD_LOCATION} EXPECTED_HASH SHA256=${CPM_HASH_SUM}
|
||||
)
|
||||
|
||||
include(${CPM_DOWNLOAD_LOCATION})
|
@@ -8,19 +8,24 @@ namespace ReArchive {
|
||||
/// Creates a new empty archive.
|
||||
/// @param filesystem_path where the archive is to be created.
|
||||
/// @param use_compression whether you'd like the file to use compression.
|
||||
/// @param running_tally If you're keeping track of the file table in your program, Providing it here will update it to reflect our changes.
|
||||
/// @param file_table If you're keeping track of the file table in your program, Providing it here will update it to reflect our changes and speed things up.
|
||||
/// @note use_compression currently does nothing.
|
||||
/// @returns True if success.
|
||||
[[nodiscard]] bool CreateArchive(const std::filesystem::path& archive, bool use_compression = false, FileTable* running_tally = nullptr);
|
||||
[[nodiscard]] bool CreateArchive(const std::filesystem::path& archive, bool use_compression = false, FileTable* file_table = nullptr);
|
||||
|
||||
/// @param archive The archive on the disk.
|
||||
/// @returns std::pair bool, FileTable. bool is success, FileTable is only valid if success.
|
||||
/// @note *Always* check if bool is true before using the file table for anything.
|
||||
[[nodiscard]] std::pair<bool, FileTable> ReadFileTable(const std::filesystem::path& archive);
|
||||
|
||||
/// Add a file to an archive.
|
||||
/// @param archive The archive on the disk.
|
||||
/// @param file_data The raw data of the file to be written.
|
||||
/// @param file_path The std::filesystem::path you would use to retrieve the file from the archive.
|
||||
/// @param byte_count The length of the file in bytes.
|
||||
/// @param running_tally If you're keeping track of the file table in your program, Providing it here will update it to reflect our changes.
|
||||
/// @param file_table If you're keeping track of the file table in your program, Providing it here will update it to reflect our changes and speed things up.
|
||||
/// @returns True if success.
|
||||
[[nodiscard]] bool WriteFile(const std::filesystem::path& archive, const std::filesystem::path& file_path, const unsigned char* file_data, const int64_t& byte_count, FileTable* running_tally = nullptr);
|
||||
[[nodiscard]] bool WriteFile(const std::filesystem::path& archive, const std::filesystem::path& file_path, const unsigned char* file_data, const int64_t& byte_count, FileTable* file_table = nullptr);
|
||||
|
||||
/// Overwrite a file which already exists in the archive.
|
||||
/// @param archive The archive on the disk.
|
||||
@@ -28,19 +33,27 @@ namespace ReArchive {
|
||||
/// @param file_path The std::filesystem::path file in the archive to be overwritten.
|
||||
/// @param byte_count The length of the file in bytes.
|
||||
/// @note It is expected that byte_count will the the same as the file size.
|
||||
/// @param file_table If you're keeping track of the file table in your program, Providing it here will update it to reflect our changes and speed things up.
|
||||
/// @returns True if success.
|
||||
[[nodiscard]] bool OverwriteFile(const std::filesystem::path& archive, const std::filesystem::path& file_path, const unsigned char* file_data, const int64_t& byte_count);
|
||||
[[nodiscard]] bool OverwriteFile(const std::filesystem::path& archive, const std::filesystem::path& file_path, const unsigned char* file_data, const int64_t& byte_count, FileTable* file_table = nullptr);
|
||||
|
||||
/// Remove a file from the archive.
|
||||
/// @param archive The archive on the disk.
|
||||
/// @param file_path The std::filesystem::path file in the archive to be removed.
|
||||
/// @param running_tally If you're keeping track of the file table in your program, Providing it here will update it to reflect our changes.
|
||||
/// @param file_table If you're keeping track of the file table in your program, Providing it here will update it to reflect our changes and speed things up.
|
||||
/// @returns True if success.
|
||||
[[nodiscard]] bool EraseFile(const std::filesystem::path& archive, const std::filesystem::path& file_path, FileTable* running_tally = nullptr);
|
||||
[[nodiscard]] bool EraseFile(const std::filesystem::path& archive, const std::filesystem::path& file_path, FileTable* file_table = nullptr);
|
||||
|
||||
/// Read a file from a given archive
|
||||
/// @param archive The archive on the disk.
|
||||
/// @param file_path The std::filesystem::path you specified for the given file.
|
||||
/// @param file_table If you're keeping track of the file table in your program, Providing it here will update it to reflect our changes and speed things up.
|
||||
/// @note An empty vector is returned in the event that no such file exists or there was an error reading it back.
|
||||
std::vector<unsigned char> ReadFile(const std::filesystem::path& archive, const std::filesystem::path& file_path);
|
||||
std::vector<unsigned char> ReadFile(const std::filesystem::path& archive, const std::filesystem::path& file_path, FileTable* file_table = nullptr);
|
||||
|
||||
|
||||
/// Returns the rsa format version this library is compiled to support.
|
||||
/// @note Future versions of this library may support backward-compatibility.
|
||||
double ArchiveFormatProtocolVersion();
|
||||
}
|
@@ -11,16 +11,19 @@ namespace ReArchive {
|
||||
class ReArchive::FileTable {
|
||||
protected:
|
||||
// count
|
||||
// TODO unordered_set so time to find a particular entry doesn't depend on the length.
|
||||
std::unordered_map<std::filesystem::path, FileEntry> entries;
|
||||
public:
|
||||
void Append(const FileEntry& file_entry);
|
||||
void Remove(const FileEntry& file_entry);
|
||||
[[nodiscard]] std::unordered_map<std::filesystem::path, FileEntry> GetEntries() const { return entries; }
|
||||
[[nodiscard]] bool Contains(const std::filesystem::path& entry ) const { return entries.contains(entry); }
|
||||
[[nodiscard]] std::unordered_map<std::filesystem::path, FileEntry>* GetEntries() { return &entries; }
|
||||
[[nodiscard]] const std::unordered_map<std::filesystem::path, FileEntry>* GetEntries() const { return &entries; }
|
||||
[[nodiscard]] int64_t Count() const { return entries.size(); }
|
||||
public:
|
||||
[[nodiscard]] static std::vector<unsigned char> Serialize(const FileTable& file_table);
|
||||
public:
|
||||
FileTable(const FileTable& rhs) : entries(rhs.entries) {};
|
||||
FileTable(FileTable& rhs) : entries(rhs.entries) {};
|
||||
FileTable() = default;
|
||||
~FileTable() = default;
|
||||
|
||||
|
@@ -3,6 +3,7 @@
|
||||
#include <array>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
#include <Endianness.hpp>
|
||||
|
||||
|
||||
namespace ReArchive {
|
||||
|
270
main.cpp
270
main.cpp
@@ -1,32 +1,254 @@
|
||||
#include <ReArchive/ReArchive.h>
|
||||
#include <iostream>
|
||||
|
||||
int main() {
|
||||
ReArchive::FileTable running_tally;
|
||||
|
||||
if(std::filesystem::exists("test.rsa"))
|
||||
std::filesystem::remove("test.rsa");
|
||||
|
||||
if (!ReArchive::CreateArchive("test.rsa"))
|
||||
return -1;
|
||||
std::string some_string = "some other string0.";
|
||||
if (!ReArchive::WriteFile("test.rsa", "assets/test0.png", reinterpret_cast<const unsigned char *>(some_string.data()), some_string.size(), &running_tally))
|
||||
return -1;
|
||||
|
||||
some_string = "some other string1.";
|
||||
if (!ReArchive::WriteFile("test.rsa", "assets/test1.png", reinterpret_cast<const unsigned char *>(some_string.data()), some_string.size(), &running_tally))
|
||||
return -1;
|
||||
#include <fstream>
|
||||
#include <mcolor.h>
|
||||
|
||||
|
||||
auto retrieved = ReArchive::ReadFile("test.rsa", "assets/test0.png");
|
||||
std::cout << std::string( retrieved.begin(), retrieved.end()) << std::endl;
|
||||
|
||||
if (!ReArchive::EraseFile("test.rsa", "assets/test0.png", &running_tally))
|
||||
return -1;
|
||||
bool GetConfirmation(const std::string& message) {
|
||||
std::string user_input;
|
||||
while (true) {
|
||||
std::cout << message << "(Y/N)"<< std::endl;
|
||||
std::cin >> user_input;
|
||||
|
||||
auto retrieved2 = ReArchive::ReadFile("test.rsa", "assets/test1.png");
|
||||
std::cout << std::string( retrieved2.begin(), retrieved2.end()) << std::endl;
|
||||
if (user_input == "y" || user_input == "Y")
|
||||
return true;
|
||||
|
||||
for (auto& e : running_tally.GetEntries())
|
||||
std::cout << e.second.Path() << std::endl;
|
||||
if (user_input == "n" || user_input == "N")
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<unsigned char> ReadFileFromDisk(const std::filesystem::path& file_to_read) {
|
||||
std::ifstream file(file_to_read, std::ios::binary | std::ios::ate);
|
||||
if (!file)
|
||||
throw std::runtime_error("Failed to open file: " + file_to_read.string());
|
||||
|
||||
std::streamsize size = file.tellg();
|
||||
file.seekg(0, std::ios::beg);
|
||||
|
||||
std::vector<unsigned char> buffer(size);
|
||||
|
||||
if (!file.read(reinterpret_cast<char*>(buffer.data()), size))
|
||||
throw std::runtime_error("Error reading file: " + file_to_read.string());
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
std::string PrettyVersionString()
|
||||
{
|
||||
return std::format("{}Redacted Software Archive Project v{} (rsarchive v{}) (RSA Format v{}){}", Colors::Browns::GoldenRod.ToEscapeCode(), ARCHIVE_PROJECT_VERSION, ARCHIVE_APP_VERSION, ARCHIVE_FORMAT_VERSION, mcolor::AnsiEscapeCodes::ResetAll);
|
||||
}
|
||||
|
||||
bool WriteFileToDisk(const std::vector<unsigned char>& file_data, const std::filesystem::path& destination) {
|
||||
std::ofstream file(destination, std::ios::binary);
|
||||
if (!file)
|
||||
return false;
|
||||
|
||||
file.write(reinterpret_cast<const char*>(file_data.data()), file_data.size());
|
||||
return file.good();
|
||||
}
|
||||
|
||||
void DisplayLicense() {
|
||||
std::cout << Colors::Oranges::Gold.ToEscapeCode();
|
||||
std::cout << "This is free and unencumbered software released into the public domain." << std::endl;
|
||||
std::cout << std::endl;
|
||||
std::cout << "Anyone is free to copy, modify, publish, use, compile, sell, or" << std::endl;
|
||||
std::cout << "distribute this software, either in source code form or as a compiled" << std::endl;
|
||||
std::cout << "binary, for any purpose, commercial or non-commercial, and by any" << std::endl;
|
||||
std::cout << "means." << std::endl;
|
||||
std::cout << std::endl;
|
||||
std::cout << "In jurisdictions that recognize copyright laws, the author or authors" << std::endl;
|
||||
std::cout << "of this software dedicate any and all copyright interest in the" << std::endl;
|
||||
std::cout << "software to the public domain. We make this dedication for the benefit" << std::endl;
|
||||
std::cout << "of the public at large and to the detriment of our heirs and" << std::endl;
|
||||
std::cout << "successors. We intend this dedication to be an overt act of" << std::endl;
|
||||
std::cout << "relinquishment in perpetuity of all present and future rights to this" << std::endl;
|
||||
std::cout << "software under copyright law." << std::endl;
|
||||
std::cout << std::endl;
|
||||
std::cout << "THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND," << std::endl;
|
||||
std::cout << "EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF" << std::endl;
|
||||
std::cout << "MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT." << std::endl;
|
||||
std::cout << "IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR" << std::endl;
|
||||
std::cout << "OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE," << std::endl;
|
||||
std::cout << "ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR" << std::endl;
|
||||
std::cout << "OTHER DEALINGS IN THE SOFTWARE." << mcolor::AnsiEscapeCodes::ResetAll << std::endl;
|
||||
}
|
||||
|
||||
void DisplayHelp() {
|
||||
std::string sep = Colors::DarkGray.ToEscapeCode() + mcolor::AnsiEscapeCodes::Bold + ">>> " + mcolor::AnsiEscapeCodes::ResetAll;
|
||||
std::cout << PrettyVersionString() << std::endl;
|
||||
int col = 45;
|
||||
std::cout.width(col);
|
||||
std::cout << std::left << "-v version: show the version string " << sep << std::left << "-h help: shows this listing" << std::endl;
|
||||
std::cout.width(col);
|
||||
std::cout << std::left << "-L license: show software license " << sep << std::right << "-l list: show all files in-to an archive" << std::endl;
|
||||
std::cout.width(col);
|
||||
std::cout << std::left <<"-x extract: retrieve files from an archive " << sep << std::right << "-a add: put a file in-to an archive" << std::endl;
|
||||
std::cout.width(col);
|
||||
std::cout << std::left << "-c create: make a new, empty archive " << sep << std::right << "-r create an archive from a directory" << std::endl;
|
||||
std::cout.width(col);
|
||||
std::cout << std::left << "-ar add recursive: put all files in a directory in-to an archive" << std::endl;
|
||||
}
|
||||
|
||||
void DisplayArchiveContents(const std::filesystem::path& archive) {
|
||||
auto result = ReArchive::ReadFileTable(archive);
|
||||
|
||||
if (!result.first) {
|
||||
std::cerr << "The specified path is inaccessible or not a valid archive." << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
auto file_table = result.second;
|
||||
std::cout << "path | size (bytes)" << std::endl;
|
||||
for (const auto& e : *file_table.GetEntries())
|
||||
std::cout << e.second.Path() << " " << e.second.Size() << std::endl;
|
||||
|
||||
std::cout << file_table.Count() << " files" << std::endl;
|
||||
}
|
||||
|
||||
void DisplayInvalidParameters() {
|
||||
//std::cerr << "Invalid parameters received. Use -h or 'man rsarchive' for a complete guide." << std::endl;
|
||||
std::cout << Colors::Reds::LightCoral.ToEscapeCode() << "Invalid parameters received." << Colors::White.ToEscapeCode() << " Use -h or 'man rsarchive' for a complete guide." << mcolor::AnsiEscapeCodes::ResetAll << std::endl;
|
||||
}
|
||||
|
||||
void AddFileToArchive(const std::filesystem::path& file_to_add, const std::filesystem::path& archive, ReArchive::FileTable* file_table = nullptr) {
|
||||
if (!std::filesystem::exists(file_to_add)) {
|
||||
std::cerr << "The specified path for the file to be added is inaccessible." << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
if (std::filesystem::is_directory(file_to_add)) {
|
||||
std::cerr << "The specified path for the file(s) to be added is a directory." << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
auto file_data = ReadFileFromDisk(file_to_add);
|
||||
auto result = ReArchive::WriteFile(archive, file_to_add, file_data.data(), file_data.size(), file_table);
|
||||
|
||||
if (!result)
|
||||
std::cerr << "The specified path for the file to be added already exists within the archive." << std::endl;
|
||||
}
|
||||
|
||||
void AddDirectoryToArchive(const std::filesystem::path& directory_to_add, const std::filesystem::path& archive) {
|
||||
if (!std::filesystem::exists(directory_to_add)) {
|
||||
std::cerr << "The specified path for the file(s) to add is inaccessible or does not exist." << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!std::filesystem::is_directory(directory_to_add)) {
|
||||
std::cerr << "The specified path for the file(s) to add is not a directory." << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!std::filesystem::exists(archive)) {
|
||||
std::cerr << "The specified path is inaccessible or not a valid archive." << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
auto result = ReArchive::ReadFileTable(archive);
|
||||
if (!result.first) {
|
||||
std::cerr << "The specified path is inaccessible or not a valid archive." << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto& entry : std::filesystem::recursive_directory_iterator(directory_to_add)) {
|
||||
if (std::filesystem::is_regular_file(entry) && !std::filesystem::is_directory(entry)) {
|
||||
auto entry_relative_path = std::filesystem::relative(entry.path(), std::filesystem::current_path());
|
||||
AddFileToArchive(entry_relative_path, archive, &result.second);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void NewArchiveFromDirectory(const std::filesystem::path& directory_to_add, const std::filesystem::path& archive) {
|
||||
if (std::filesystem::exists(archive)) {
|
||||
std::cerr << "The specified path for the new archive already exists." << std::endl;
|
||||
return;
|
||||
}
|
||||
auto result = ReArchive::CreateArchive(archive, false);
|
||||
if (!result)
|
||||
std::cerr << "The specified path for the new archive already exists." << std::endl;
|
||||
AddDirectoryToArchive(directory_to_add, archive);
|
||||
}
|
||||
|
||||
void ExtractArchive(const std::filesystem::path& archive) {
|
||||
if (!std::filesystem::exists(archive)) {
|
||||
std::cerr << "The specified path is inaccessible or not a valid archive." << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
auto file_table_result = ReArchive::ReadFileTable(archive);
|
||||
if (!file_table_result.first) {
|
||||
std::cerr << "The specified path is inaccessible or not a valid archive." << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
auto result = ReArchive::ReadFileTable(archive);
|
||||
if (!result.first) {
|
||||
std::cerr << "The specified path is inaccessible or not a valid archive." << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto& entry : *file_table_result.second.GetEntries()) {
|
||||
if (std::filesystem::exists(entry.first))
|
||||
if (!GetConfirmation("File " + entry.first.string() + " already exists, overwrite?"))
|
||||
continue;
|
||||
|
||||
std::filesystem::create_directories(std::filesystem::current_path() / entry.first.parent_path());
|
||||
if (!WriteFileToDisk(ReArchive::ReadFile(archive, entry.first, &result.second),std::filesystem::current_path() / entry.first))
|
||||
std::cerr << "The path for writing is inaccessible." << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
|
||||
mcolor::windowsSaneify();
|
||||
|
||||
if (argc == 1)
|
||||
DisplayInvalidParameters();
|
||||
|
||||
if (argc == 2) {
|
||||
if (std::string(argv[1]) == "-v")
|
||||
std::cout << PrettyVersionString() << std::endl;
|
||||
|
||||
else if (std::string(argv[1]) == "-h")
|
||||
DisplayHelp();
|
||||
|
||||
else if (std::string(argv[1]) == "-L")
|
||||
DisplayLicense();
|
||||
|
||||
else
|
||||
DisplayInvalidParameters();
|
||||
}
|
||||
|
||||
else if (argc == 3) {
|
||||
if (std::string(argv[1]) == "-l")
|
||||
DisplayArchiveContents(argv[2]);
|
||||
|
||||
else if (std::string(argv[1]) == "-c") {
|
||||
if(!ReArchive::CreateArchive(argv[2], false))
|
||||
std::cerr << "The specified path for the new archive is inaccessible or already exists." << std::endl;
|
||||
}
|
||||
|
||||
else if (std::string(argv[1]) == "-x") {
|
||||
ExtractArchive(argv[2]);
|
||||
}
|
||||
else
|
||||
DisplayInvalidParameters();
|
||||
}
|
||||
|
||||
else if (argc == 4) {
|
||||
if (std::string(argv[1]) == "-a")
|
||||
AddFileToArchive(argv[2], argv[3]);
|
||||
|
||||
else if (std::string(argv[1]) == "-ar")
|
||||
AddDirectoryToArchive(argv[2], argv[3]);
|
||||
|
||||
else if (std::string(argv[1]) == "-r")
|
||||
NewArchiveFromDirectory(argv[2], argv[3]);
|
||||
else
|
||||
DisplayInvalidParameters();
|
||||
}
|
||||
}
|
@@ -4,6 +4,8 @@
|
||||
#include <ReArchive/types/Header.h>
|
||||
#include <ReArchive/types/FileTable.h>
|
||||
#include <ReArchive/types/FileEntry.h>
|
||||
#include <cassert>
|
||||
#include <Endianness.hpp>
|
||||
|
||||
using ReArchive::Header;
|
||||
using ReArchive::FileTable;
|
||||
@@ -16,61 +18,80 @@ Header GetHeader(const unsigned char* archive) {
|
||||
return h;
|
||||
}
|
||||
|
||||
Header GetHeader(const std::filesystem::path& archive) {
|
||||
std::ifstream file(archive, std::ios::binary);
|
||||
|
||||
if (!file)
|
||||
throw std::runtime_error("Trying to get the header of an archive which doesn't exist?");
|
||||
|
||||
std::vector<unsigned char> buffer(ReArchive::Header::Size());
|
||||
file.read(reinterpret_cast<char *>(buffer.data()), (int64_t) buffer.size());
|
||||
file.close();
|
||||
|
||||
return GetHeader(buffer.data());
|
||||
}
|
||||
|
||||
/// @param header our header.
|
||||
/// @param in Our input stream to the file.
|
||||
/// @note Does not close the input stream.
|
||||
FileTable GetFileTable(const Header& header, std::ifstream& in) {
|
||||
FileTable result;
|
||||
|
||||
std::vector<unsigned char> buffer;
|
||||
|
||||
in.seekg(0, std::ios::end);
|
||||
int64_t file_table_size = in.tellg() - header.FileTableOffset();
|
||||
in.seekg(header.FileTableOffset(), std::ios::beg);
|
||||
buffer.resize(sizeof(int64_t));
|
||||
|
||||
in.read(reinterpret_cast<char *>(buffer.data()), (int64_t) buffer.size());
|
||||
int64_t file_table_entry_count = be64toh(*reinterpret_cast<int64_t*>(buffer.data()));
|
||||
std::vector<unsigned char> buffer(file_table_size);
|
||||
in.read(reinterpret_cast<char *>(buffer.data()), buffer.size());
|
||||
|
||||
if (file_table_entry_count) {
|
||||
// To put us at the first "string size" for each FileEntry.
|
||||
in.seekg(header.FileTableOffset() + 8, std::ios::beg);
|
||||
unsigned char* ptr = buffer.data();
|
||||
Endianness::NetworkToHostOrder(128u);
|
||||
s64 wtf = *reinterpret_cast<const int64_t*>(ptr);
|
||||
int64_t file_table_entry_count = Endianness::NetworkToHostOrder<s64>(wtf);
|
||||
ptr += sizeof(int64_t);
|
||||
|
||||
// for each file entry,
|
||||
FileTable result;
|
||||
for (int64_t i = 0; i < file_table_entry_count; i++) {
|
||||
in.read(reinterpret_cast<char *>(buffer.data()), (int64_t) buffer.size());
|
||||
int64_t string_size = be64toh(*reinterpret_cast<int64_t*>(buffer.data()));
|
||||
// Out of bounds.
|
||||
assert(ptr < (buffer.data() + buffer.size()));
|
||||
|
||||
buffer.resize(string_size);
|
||||
in.read(reinterpret_cast<char *>(buffer.data()), (int64_t) buffer.size());
|
||||
std::string path(buffer.begin(), buffer.end());
|
||||
int64_t string_size = Endianness::NetworkToHostOrder(*reinterpret_cast<const int64_t*>(ptr)); //be64toh(*reinterpret_cast<const int64_t*>(ptr));
|
||||
ptr += sizeof(int64_t);
|
||||
|
||||
buffer.resize(sizeof(int64_t));
|
||||
std::string path(reinterpret_cast<const char*>(ptr), string_size);
|
||||
ptr += string_size;
|
||||
|
||||
in.read(reinterpret_cast<char *>(buffer.data()), (int64_t) buffer.size());
|
||||
int64_t data_size = be64toh(*reinterpret_cast<int64_t*>(buffer.data()));
|
||||
int64_t data_size = Endianness::NetworkToHostOrder(*reinterpret_cast<const int64_t*>(ptr));
|
||||
ptr += sizeof(int64_t);
|
||||
|
||||
in.read(reinterpret_cast<char *>(buffer.data()), (int64_t) buffer.size());
|
||||
int64_t data_offset = be64toh(*reinterpret_cast<int64_t*>(buffer.data()));
|
||||
int64_t data_offset = Endianness::NetworkToHostOrder(*reinterpret_cast<const int64_t*>(ptr));
|
||||
ptr += sizeof(int64_t);
|
||||
|
||||
result.Append(FileEntry(data_size, data_offset, path));
|
||||
}
|
||||
result.Append({ data_size, data_offset, path });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool ReArchive::CreateArchive(const std::filesystem::path& filesystem_path, bool use_compression, FileTable* running_tally) {
|
||||
std::pair<bool, FileTable> ReArchive::ReadFileTable(const std::filesystem::path& archive) {
|
||||
if (!std::filesystem::exists(archive))
|
||||
return {false, {}};
|
||||
|
||||
// Busy-wait.
|
||||
while (locked.contains(archive)) {}
|
||||
locked.insert(archive);
|
||||
|
||||
std::ifstream in(archive, std::ios::binary);
|
||||
if (!in)
|
||||
return {false, {}};
|
||||
|
||||
in.seekg(0, std::ios::end);
|
||||
if (in.tellg() < Header::Size())
|
||||
return {false, {}};
|
||||
in.seekg(0, std::ios::beg);
|
||||
|
||||
std::vector<unsigned char> buffer (Header::Size());
|
||||
in.read(reinterpret_cast<char *>(buffer.data()), (int64_t) buffer.size());
|
||||
if (buffer[0] != 'R' || buffer[1] != 'S' || buffer[2] != 'A')
|
||||
return {false, {}};
|
||||
|
||||
auto header = GetHeader(buffer.data());
|
||||
auto file_table = GetFileTable(header, in);
|
||||
in.close();
|
||||
|
||||
// Remove lock.
|
||||
auto position = locked.find(archive);
|
||||
if (position != locked.end())
|
||||
locked.erase(position);
|
||||
|
||||
return {true, file_table};
|
||||
}
|
||||
|
||||
bool ReArchive::CreateArchive(const std::filesystem::path& filesystem_path, bool use_compression, FileTable* current_file_table) {
|
||||
if (std::filesystem::exists(filesystem_path))
|
||||
return false;
|
||||
|
||||
@@ -89,8 +110,8 @@ bool ReArchive::CreateArchive(const std::filesystem::path& filesystem_path, bool
|
||||
file.write(reinterpret_cast<const char*>(serialized_file_table.data()), (int64_t) serialized_file_table.size());
|
||||
file.close();
|
||||
|
||||
if (running_tally)
|
||||
*running_tally = file_table;
|
||||
if (current_file_table)
|
||||
*current_file_table = file_table;
|
||||
|
||||
// Remove lock.
|
||||
auto position = locked.find(filesystem_path);
|
||||
@@ -100,7 +121,7 @@ bool ReArchive::CreateArchive(const std::filesystem::path& filesystem_path, bool
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ReArchive::WriteFile(const std::filesystem::path& archive, const std::filesystem::path& file_path, const unsigned char* file_data, const int64_t& byte_count, FileTable* running_tally) {
|
||||
bool ReArchive::WriteFile(const std::filesystem::path& archive, const std::filesystem::path& file_path, const unsigned char* file_data, const int64_t& byte_count, FileTable* current_file_table) {
|
||||
if (!std::filesystem::exists(archive))
|
||||
return false;
|
||||
|
||||
@@ -123,11 +144,14 @@ bool ReArchive::WriteFile(const std::filesystem::path& archive, const std::files
|
||||
return false;
|
||||
|
||||
auto header = GetHeader(buffer.data());
|
||||
auto file_table = GetFileTable(header, in);
|
||||
|
||||
auto file_entries = file_table.GetEntries();
|
||||
auto value = file_entries.find(file_path);
|
||||
if (value != file_entries.end())
|
||||
FileTable* file_table;
|
||||
if (current_file_table == nullptr)
|
||||
file_table = new FileTable(GetFileTable(header, in));
|
||||
else
|
||||
file_table = current_file_table;
|
||||
|
||||
if (file_table->Contains(file_path))
|
||||
return false;
|
||||
|
||||
in.close();
|
||||
@@ -139,10 +163,10 @@ bool ReArchive::WriteFile(const std::filesystem::path& archive, const std::files
|
||||
out.seekp(header.FileTableOffset(), std::ios::beg);
|
||||
out.write(reinterpret_cast<const char *>(file_data), byte_count);
|
||||
|
||||
file_table.Append(FileEntry(byte_count, header.FileTableOffset(), file_path));
|
||||
file_table->Append(FileEntry(byte_count, header.FileTableOffset(), file_path));
|
||||
header.FileTableOffset(out.tellp());
|
||||
|
||||
auto new_file_table = FileTable::Serialize(file_table);
|
||||
auto new_file_table = FileTable::Serialize(*file_table);
|
||||
out.write(reinterpret_cast<const char *>(new_file_table.data()), (int64_t) new_file_table.size());
|
||||
|
||||
auto new_header = Header::Serialize(header);
|
||||
@@ -150,18 +174,22 @@ bool ReArchive::WriteFile(const std::filesystem::path& archive, const std::files
|
||||
out.write(reinterpret_cast<const char *>(new_header.data()), (int64_t) new_header.size());
|
||||
out.close();
|
||||
|
||||
if (current_file_table)
|
||||
*current_file_table = *file_table;
|
||||
|
||||
if (current_file_table == nullptr)
|
||||
delete file_table;
|
||||
|
||||
// Remove lock.
|
||||
auto position = locked.find(archive);
|
||||
|
||||
if (position != locked.end())
|
||||
locked.erase(position);
|
||||
|
||||
if (running_tally)
|
||||
*running_tally = file_table;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ReArchive::OverwriteFile(const std::filesystem::path& archive, const std::filesystem::path& file_path, const unsigned char* file_data, const int64_t& byte_count) {
|
||||
bool ReArchive::OverwriteFile(const std::filesystem::path& archive, const std::filesystem::path& file_path, const unsigned char* file_data, const int64_t& byte_count, FileTable* current_file_table) {
|
||||
if (!std::filesystem::exists(archive))
|
||||
return false;
|
||||
|
||||
@@ -184,17 +212,19 @@ bool ReArchive::OverwriteFile(const std::filesystem::path& archive, const std::f
|
||||
return false;
|
||||
|
||||
auto header = GetHeader(buffer.data());
|
||||
auto file_table = GetFileTable(header, in);
|
||||
FileTable* file_table;
|
||||
if (current_file_table == nullptr)
|
||||
file_table = new FileTable(GetFileTable(header, in));
|
||||
else
|
||||
file_table = current_file_table;
|
||||
|
||||
const FileEntry* target = nullptr;
|
||||
auto file_entries = file_table.GetEntries();
|
||||
auto file_entries = file_table->GetEntries();
|
||||
|
||||
auto value = file_entries.find(file_path);
|
||||
if (value != file_entries.end())
|
||||
auto value = file_entries->find(file_path);
|
||||
if (value != file_entries->end())
|
||||
target = &value->second;
|
||||
|
||||
|
||||
|
||||
if (!target)
|
||||
return false;
|
||||
|
||||
@@ -210,6 +240,9 @@ bool ReArchive::OverwriteFile(const std::filesystem::path& archive, const std::f
|
||||
out.write(reinterpret_cast<const char *>(file_data), byte_count);
|
||||
out.close();
|
||||
|
||||
if (current_file_table == nullptr)
|
||||
delete file_table;
|
||||
|
||||
// Remove lock.
|
||||
auto position = locked.find(archive);
|
||||
if (position != locked.end())
|
||||
@@ -218,7 +251,7 @@ bool ReArchive::OverwriteFile(const std::filesystem::path& archive, const std::f
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<unsigned char> ReArchive::ReadFile(const std::filesystem::path& archive, const std::filesystem::path& file_path) {
|
||||
std::vector<unsigned char> ReArchive::ReadFile(const std::filesystem::path& archive, const std::filesystem::path& file_path, FileTable* current_file_table) {
|
||||
if (!std::filesystem::exists(archive))
|
||||
return {};
|
||||
|
||||
@@ -241,23 +274,19 @@ std::vector<unsigned char> ReArchive::ReadFile(const std::filesystem::path& arch
|
||||
return {};
|
||||
|
||||
auto header = GetHeader(buffer.data());
|
||||
auto file_table = GetFileTable(header, in);
|
||||
|
||||
/*
|
||||
for (const auto& e : file_table.GetEntries())
|
||||
if (e.Path() == file_path)
|
||||
target = &e;
|
||||
*/
|
||||
FileTable* file_table;
|
||||
if (current_file_table == nullptr)
|
||||
file_table = new FileTable(GetFileTable(header, in));
|
||||
else
|
||||
file_table = current_file_table;
|
||||
|
||||
const FileEntry* target = nullptr;
|
||||
auto file_entries = file_table.GetEntries();
|
||||
auto file_entries = file_table->GetEntries();
|
||||
|
||||
auto value = file_entries.find(file_path);
|
||||
if (value != file_entries.end())
|
||||
auto value = file_entries->find(file_path);
|
||||
if (value != file_entries->end())
|
||||
target = &value->second;
|
||||
|
||||
|
||||
|
||||
if (!target)
|
||||
return {};
|
||||
|
||||
@@ -266,14 +295,19 @@ for (const auto& e : file_table.GetEntries())
|
||||
in.read(reinterpret_cast<char*>(result.data()), (int64_t) result.size());
|
||||
in.close();
|
||||
|
||||
// delete the one we allocated if one was not passed in.
|
||||
if (current_file_table == nullptr)
|
||||
delete file_table;
|
||||
|
||||
// Remove lock.
|
||||
auto position = locked.find(archive);
|
||||
if (position != locked.end())
|
||||
locked.erase(position);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
double ReArchive::ArchiveFormatProtocolVersion() { return ARCHIVE_FORMAT_VERSION; }
|
||||
|
||||
// I tried to do this several different ways but this seems to be the best approach - Redacted.
|
||||
bool ReArchive::EraseFile(const std::filesystem::path& archive, const std::filesystem::path& file_path, FileTable* running_tally) {
|
||||
|
||||
@@ -301,15 +335,16 @@ bool ReArchive::EraseFile(const std::filesystem::path& archive, const std::files
|
||||
auto current_header = GetHeader(buffer.data());
|
||||
auto current_file_table = GetFileTable(current_header, in);
|
||||
|
||||
// TODO randomize the name more than that.
|
||||
if (!CreateArchive(archive.string() + ".tmp", current_header.Compressed()))
|
||||
return false;
|
||||
|
||||
auto file_entries = current_file_table.GetEntries();
|
||||
auto value = file_entries.find(file_path);
|
||||
if (value != file_entries.end())
|
||||
file_entries.erase(value);
|
||||
auto value = file_entries->find(file_path);
|
||||
if (value != file_entries->end())
|
||||
file_entries->erase(value);
|
||||
|
||||
for (auto& e : file_entries) {
|
||||
for (auto& e : *file_entries) {
|
||||
auto file_buffer = ReadFile(archive, e.first);
|
||||
if (!WriteFile(archive.string() + ".tmp", e.first, file_buffer.data(), (int64_t) file_buffer.size())) {
|
||||
std::filesystem::remove(archive.string() + ".tmp");
|
||||
@@ -329,9 +364,10 @@ bool ReArchive::EraseFile(const std::filesystem::path& archive, const std::files
|
||||
if (position != locked.end())
|
||||
locked.erase(position);
|
||||
|
||||
// TODO read the header from the file we just wrote.
|
||||
if (running_tally)
|
||||
*running_tally = current_file_table;
|
||||
|
||||
if (running_tally) {
|
||||
auto result = ReadFileTable(archive);
|
||||
if (result.first)
|
||||
*running_tally = result.second;
|
||||
}
|
||||
return true;
|
||||
}
|
@@ -1,5 +1,6 @@
|
||||
#include <ReArchive/types/FileEntry.h>
|
||||
#include <cstring>
|
||||
#include <Endianness.hpp>
|
||||
|
||||
|
||||
using namespace ReArchive;
|
||||
@@ -11,18 +12,18 @@ std::vector<unsigned char> FileEntry::Serialize(const FileEntry& file) {
|
||||
std::vector<unsigned char> result(sizeof(int64_t) + path_size + 2 * sizeof(int64_t));
|
||||
unsigned char* ptr = result.data();
|
||||
|
||||
auto network_path_size = htobe64(path_size);
|
||||
auto network_path_size = Endianness::HostToNetworkOrder(path_size);
|
||||
memcpy(ptr, &network_path_size, sizeof(int64_t));
|
||||
ptr += sizeof(int64_t);
|
||||
|
||||
memcpy(ptr, path_string.data(), path_size);
|
||||
ptr += path_size;
|
||||
|
||||
auto network_data_size = htobe64(file.data_size);
|
||||
auto network_data_size = Endianness::HostToNetworkOrder(file.data_size);
|
||||
memcpy(ptr, &network_data_size, sizeof(int64_t));
|
||||
ptr += sizeof(int64_t);
|
||||
|
||||
auto network_data_offset = htobe64(file.data_offset);
|
||||
auto network_data_offset = Endianness::HostToNetworkOrder(file.data_offset);
|
||||
memcpy(ptr, &network_data_offset, sizeof(int64_t));
|
||||
return result;
|
||||
}
|
||||
|
@@ -1,13 +1,11 @@
|
||||
#include <ReArchive/types/FileTable.h>
|
||||
#include <cstring>
|
||||
#include <Endianness.hpp>
|
||||
|
||||
using namespace ReArchive;
|
||||
|
||||
void FileTable::Append(const FileEntry& file_entry) {
|
||||
if (entries.contains(file_entry.Path()))
|
||||
return;
|
||||
|
||||
entries.insert(std::make_pair(file_entry.Path(), file_entry));
|
||||
entries.try_emplace(file_entry.Path(), file_entry);
|
||||
}
|
||||
|
||||
void FileTable::Remove(const FileEntry& file_entry) {
|
||||
@@ -21,15 +19,14 @@ void FileTable::Remove(const FileEntry& file_entry) {
|
||||
|
||||
std::vector<unsigned char> FileTable::Serialize(const FileTable& file_table) {
|
||||
auto files = file_table.GetEntries();
|
||||
int64_t count = files.size();
|
||||
auto network_count = htobe64(count);
|
||||
std::vector<unsigned char> result(reinterpret_cast<unsigned char*>(&network_count),
|
||||
reinterpret_cast<unsigned char*>(&network_count) + sizeof(network_count));
|
||||
int64_t count = files->size();
|
||||
auto network_count = Endianness::HostToNetworkOrder(count);
|
||||
std::vector<unsigned char> result(reinterpret_cast<unsigned char*>(&network_count), reinterpret_cast<unsigned char*>(&network_count) + sizeof(network_count));
|
||||
|
||||
if (files.empty())
|
||||
if (files->empty())
|
||||
return result;
|
||||
|
||||
for (const auto& file : files) {
|
||||
for (const auto& file : *files) {
|
||||
size_t current_size = result.size();
|
||||
auto serialization = FileEntry::Serialize(file.second);
|
||||
|
||||
|
@@ -10,7 +10,7 @@ std::vector<unsigned char> ReArchive::Header::Serialize(const ReArchive::Header&
|
||||
|
||||
size_t current_size = result.size();
|
||||
result.resize(current_size + sizeof(int64_t));
|
||||
auto network_file_table_offset = htobe64(header.file_table_offset);
|
||||
auto network_file_table_offset = Endianness::HostToNetworkOrder(header.file_table_offset);//htobe64(header.file_table_offset);
|
||||
memcpy(result.data() + current_size, &network_file_table_offset, sizeof(int64_t));
|
||||
|
||||
return result;
|
||||
@@ -22,5 +22,5 @@ ReArchive::Header ReArchive::Header::DeSerialize(const unsigned char* serialized
|
||||
use_c = serialized_header[ReArchive::magic.size()];
|
||||
memcpy(&file_table_off, serialized_header + ReArchive::Header::Size() - sizeof(int64_t), sizeof(int64_t));
|
||||
|
||||
return { use_c, (int64_t) be64toh(file_table_off)};
|
||||
return { use_c, (int64_t) Endianness::HostToNetworkOrder(file_table_off)};//be64toh(file_table_off)};
|
||||
}
|
||||
|
Reference in New Issue
Block a user