Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d47064bd07 | ||
|
|
3da354c59e | ||
|
|
ce607b157a | ||
|
|
841fa3850a |
+40
@@ -0,0 +1,40 @@
|
||||
# Common build directories used in the source tree
|
||||
/build*
|
||||
/.build*
|
||||
/*_build*
|
||||
/*-build*
|
||||
/out
|
||||
|
||||
# Temporary files
|
||||
*~
|
||||
\#*\#
|
||||
.\#*
|
||||
[._]*.s[a-v][a-z]
|
||||
[._]*.sw[a-p]
|
||||
[._]s[a-v][a-z]
|
||||
[._]sw[a-p]
|
||||
|
||||
# OS specific files
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
|
||||
# CMake supports some configuration files to allow users a convenient way to pre-define
|
||||
# sets of configuration options:
|
||||
# https://cmake.org/cmake/help/latest/manual/cmake-presets.7.html
|
||||
/CMakeUserPresets.json
|
||||
|
||||
# Visual Studio (note that CMakePresets.json is recommended instead of
|
||||
# CMakeSettings.json for Visual Studio going forward)
|
||||
# https://learn.microsoft.com/en-us/cpp/build/cmake-presets-vs
|
||||
# https://learn.microsoft.com/en-us/cpp/build/customize-cmake-settings
|
||||
/.vs
|
||||
.vscode
|
||||
/CMakeSettings.json
|
||||
|
||||
#Qt Creator
|
||||
CMakeLists.txt.user
|
||||
|
||||
# Misc
|
||||
.clang-format
|
||||
|
||||
|
||||
+8
-8
@@ -36,17 +36,11 @@
|
||||
# Minimum required version of CMake
|
||||
cmake_minimum_required(VERSION 3.18)
|
||||
|
||||
# set CMake project name
|
||||
project(STRCLEAR)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
include_directories(
|
||||
${CMAKE_CURRENT_BINARY_DIR}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
)
|
||||
|
||||
if(NOT DEFINED BIN_DIR)
|
||||
set(BIN_DIR bin)
|
||||
endif(NOT DEFINED BIN_DIR)
|
||||
@@ -54,13 +48,20 @@ endif(NOT DEFINED BIN_DIR)
|
||||
include(CheckCXXCompilerFlag)
|
||||
check_cxx_compiler_flag(-O3 O3_COMPILER_FLAG)
|
||||
|
||||
find_package(Threads REQUIRED)
|
||||
|
||||
add_executable(strclear strclear.cpp)
|
||||
target_include_directories(strclear PRIVATE
|
||||
${CMAKE_CURRENT_BINARY_DIR}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
)
|
||||
target_link_libraries(strclear PRIVATE Threads::Threads)
|
||||
if (O3_COMPILER_FLAG)
|
||||
# If we have the O3 flag, use it
|
||||
target_compile_options(strclear PRIVATE "-O3")
|
||||
endif (O3_COMPILER_FLAG)
|
||||
install(TARGETS strclear DESTINATION ${BIN_DIR})
|
||||
|
||||
option(STRCLEAR_BUILD_TESTS "Build strclear tests" ON)
|
||||
if (STRCLEAR_BUILD_TESTS)
|
||||
enable_testing()
|
||||
add_subdirectory(tests)
|
||||
@@ -72,4 +73,3 @@ endif ()
|
||||
# indent-tabs-mode: t
|
||||
# End:
|
||||
# ex: shiftwidth=2 tabstop=8
|
||||
|
||||
|
||||
-31
@@ -51,34 +51,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
|
||||
strclear also uses strnstr, which has the following license:
|
||||
|
||||
Copyright (c) 2005-2018 Rich Felker
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
This is a length limited version of strstr, based on the OpenBSD
|
||||
implementation:
|
||||
|
||||
OpenBSD: strstr.c,v 1.9 2020/04/16 12:37:52 claudio Exp
|
||||
https://github.com/libressl-portable/openbsd/blob/master/src/lib/libc/string/strstr.c
|
||||
|
||||
|
||||
@@ -1,6 +1,27 @@
|
||||
strclear is a basic utility with two main jobs - clearing strings in binary
|
||||
files, and replacing them in text files. It is used as part of the bundling
|
||||
process with BRL-CAD, to help make sure we don't have stray build-time paths
|
||||
accidentally resulting in programs working when they shouldn't by referencing
|
||||
build-directory resources.
|
||||
strclear
|
||||
========
|
||||
|
||||
`strclear` removes or replaces embedded strings in files. BRL-CAD uses it
|
||||
when staging third party dependencies, primarily to remove build-time paths
|
||||
from binaries and to rewrite paths in text metadata such as CMake package files
|
||||
and pkg-config files.
|
||||
|
||||
Supported modes:
|
||||
|
||||
* `strclear -B <file>` classifies one file. It returns `0` for binary input
|
||||
and `1` for text input, matching the historical BRL-CAD CMake contract.
|
||||
* `strclear --classify [--files <list> | <file> ...]` emits one JSON Lines
|
||||
record per input path: `{"type":"TEXT","path":"..."}` or
|
||||
`{"type":"BINARY","path":"..."}`.
|
||||
* `strclear [-v] <file> <target> [replacement]` rewrites one file. Binary
|
||||
files clear matching bytes to NUL by default; text files replace `target`
|
||||
with `replacement`, or remove `target` if no replacement is supplied.
|
||||
* `strclear [-v] --files <list> <target> [replacement]` applies the same
|
||||
operation to each path listed in the file.
|
||||
* `--text-only` skips binary inputs, and `--binary-only` skips text inputs.
|
||||
* `-p` expands a filesystem path target into common path forms before matching.
|
||||
* `-b -c` and `-r` are accepted for compatibility with existing BRL-CAD CMake
|
||||
logic. `-b -c` forces binary clearing, and `-r` forces text replacement.
|
||||
|
||||
The `--classify` output is intentionally JSONL so build systems can parse it
|
||||
without ambiguous path escaping rules.
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
Right now this tool is one file per operation, but it
|
||||
would be better to be able to feed in a list of files.
|
||||
|
||||
We could process them in parallel to determine text or
|
||||
binary, and perform the appropriate operation on them.
|
||||
|
||||
Can still keep single file modes for manual operations
|
||||
or testing, but since the parent build should be able
|
||||
to feed us all the files at once it would simplify the
|
||||
parent build logic to be able to just hand us the list
|
||||
and take care of it. Should also speed things up a lot
|
||||
since parallel processing ought to be an option.
|
||||
+521
-183
@@ -34,110 +34,264 @@
|
||||
*/
|
||||
/** @file strclear.cpp
|
||||
*
|
||||
* Two modes for this tool:
|
||||
* This tool has two primary jobs:
|
||||
*
|
||||
* Given a binary (or text) file and a string, replace any instances of the
|
||||
* string in the binary with null chars.
|
||||
* Given a binary file and a string, replace any instances of the string in the
|
||||
* binary with null chars (or a different character specified by an option.)
|
||||
*
|
||||
* Given a text file, a target string and a replacement string, replace all
|
||||
* instances of the target string with the replacement string.
|
||||
* TODO: For the moment, the replacement string can't contain the target string.
|
||||
* Given a text file, a target string and an optional replacement string,
|
||||
* replace all instances of the target string with the replacement string (or
|
||||
* remove the target string if there is no replacement string - it is
|
||||
* "replaced" with the empty string.)
|
||||
*
|
||||
* The --classify mode emits JSON Lines records for build systems that need to
|
||||
* batch text/binary detection.
|
||||
*/
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cctype>
|
||||
#include <condition_variable>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <iterator>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <queue>
|
||||
#include <sstream>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
#include "cxxopts.hpp"
|
||||
|
||||
extern "C" char *
|
||||
strnstr(const char *h, const char *n, size_t hlen);
|
||||
struct process_opts {
|
||||
bool binary_only = false;
|
||||
bool binary_test_mode = false;
|
||||
bool clear_mode = false;
|
||||
bool classify_mode = false;
|
||||
bool force_binary = false;
|
||||
bool force_text = false;
|
||||
bool path_mode = false;
|
||||
bool text_only = false;
|
||||
bool verbose = false;
|
||||
char clear_char = '\0';
|
||||
std::vector<std::string> tgt_strs;
|
||||
std::string replace_str;
|
||||
};
|
||||
|
||||
inline std::vector<std::string> expand_path_forms(const std::string &input) {
|
||||
static std::string
|
||||
json_escape(const std::string &input)
|
||||
{
|
||||
std::ostringstream out;
|
||||
for (unsigned char c : input) {
|
||||
switch (c) {
|
||||
case '"':
|
||||
out << "\\\"";
|
||||
break;
|
||||
case '\\':
|
||||
out << "\\\\";
|
||||
break;
|
||||
case '\b':
|
||||
out << "\\b";
|
||||
break;
|
||||
case '\f':
|
||||
out << "\\f";
|
||||
break;
|
||||
case '\n':
|
||||
out << "\\n";
|
||||
break;
|
||||
case '\r':
|
||||
out << "\\r";
|
||||
break;
|
||||
case '\t':
|
||||
out << "\\t";
|
||||
break;
|
||||
default:
|
||||
if (c < 0x20) {
|
||||
out << "\\u" << std::hex << std::setw(4) << std::setfill('0') << (int)c << std::dec;
|
||||
} else {
|
||||
out << c;
|
||||
}
|
||||
}
|
||||
}
|
||||
return out.str();
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
class WorkQueue {
|
||||
std::queue<T> q;
|
||||
std::mutex m;
|
||||
std::condition_variable cv;
|
||||
bool closed = false;
|
||||
|
||||
public:
|
||||
bool push(T v) {
|
||||
std::lock_guard<std::mutex> lg(m);
|
||||
if (closed) return false;
|
||||
q.push(std::move(v));
|
||||
cv.notify_one();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool pop(T &out) {
|
||||
std::unique_lock<std::mutex> lk(m);
|
||||
cv.wait(lk, [&]{ return closed || !q.empty(); });
|
||||
if (q.empty()) return false;
|
||||
out = std::move(q.front());
|
||||
q.pop();
|
||||
return true;
|
||||
}
|
||||
|
||||
void close() {
|
||||
std::lock_guard<std::mutex> lg(m);
|
||||
closed = true;
|
||||
cv.notify_all();
|
||||
}
|
||||
};
|
||||
|
||||
inline std::vector<std::string>
|
||||
expand_path_forms(const std::string &input) {
|
||||
namespace fs = std::filesystem;
|
||||
std::vector<std::string> forms;
|
||||
if (!input.length())
|
||||
return forms;
|
||||
forms.push_back(input); // Always include original
|
||||
|
||||
auto add_form = [&](const fs::path &p) {
|
||||
std::string s = p.string();
|
||||
if (!s.empty())
|
||||
forms.push_back(s);
|
||||
};
|
||||
|
||||
auto add_partial_canonical = [&](const fs::path &p) {
|
||||
std::error_code ec;
|
||||
fs::path probe = fs::absolute(p, ec);
|
||||
if (ec)
|
||||
probe = p;
|
||||
|
||||
fs::path suffix;
|
||||
while (!probe.empty() && !fs::exists(probe, ec)) {
|
||||
fs::path parent = probe.parent_path();
|
||||
if (parent == probe)
|
||||
break;
|
||||
if (suffix.empty())
|
||||
suffix = probe.filename();
|
||||
else
|
||||
suffix = probe.filename() / suffix;
|
||||
probe = parent;
|
||||
ec.clear();
|
||||
}
|
||||
|
||||
if (probe.empty() || !fs::exists(probe, ec))
|
||||
return;
|
||||
|
||||
ec.clear();
|
||||
fs::path canon = fs::canonical(probe, ec);
|
||||
if (ec || canon.empty())
|
||||
return;
|
||||
|
||||
if (!suffix.empty())
|
||||
canon /= suffix;
|
||||
add_form(canon);
|
||||
};
|
||||
|
||||
try {
|
||||
fs::path p(input);
|
||||
if (fs::exists(p) && (fs::is_regular_file(p) || fs::is_symlink(p) || fs::is_directory(p))) {
|
||||
// Absolute form
|
||||
std::error_code ec;
|
||||
auto abs = fs::absolute(p, ec).string();
|
||||
if (!abs.empty() && abs != input) forms.push_back(abs);
|
||||
fs::path p(input);
|
||||
|
||||
// Canonical form (resolves symlinks, may fail if not accessible)
|
||||
ec.clear();
|
||||
auto canon = fs::canonical(p, ec).string();
|
||||
if (!ec && !canon.empty() && canon != input && canon != abs) forms.push_back(canon);
|
||||
// Always include the original spelling, plus cheap normalized forms.
|
||||
add_form(p);
|
||||
std::error_code ec;
|
||||
fs::path abs = fs::absolute(p, ec);
|
||||
if (!ec)
|
||||
add_form(abs);
|
||||
add_form(p.lexically_normal());
|
||||
if (!ec)
|
||||
add_form(abs.lexically_normal());
|
||||
|
||||
// Normalized (lexically, does not resolve symlinks)
|
||||
auto norm = p.lexically_normal().string();
|
||||
if (!norm.empty() && norm != input && norm != abs && norm != canon) forms.push_back(norm);
|
||||
}
|
||||
ec.clear();
|
||||
if (fs::exists(p, ec)) {
|
||||
fs::path canon = fs::canonical(p, ec);
|
||||
if (!ec)
|
||||
add_form(canon);
|
||||
}
|
||||
|
||||
add_partial_canonical(p);
|
||||
} catch (...) {
|
||||
// Ignore errors (broken symlink, permission denied, etc).
|
||||
// Ignore errors (broken symlink, permission denied, etc).
|
||||
}
|
||||
|
||||
// Match longer paths first so shorter path spellings do not partially
|
||||
// replace longer ones.
|
||||
std::sort(forms.begin(), forms.end(),
|
||||
[](const std::string &a, const std::string &b) {
|
||||
if (a.size() != b.size())
|
||||
return a.size() > b.size(); // longer first
|
||||
return a > b; // lexicographical descending
|
||||
}
|
||||
);
|
||||
forms.erase(std::unique(forms.begin(), forms.end()), forms.end());
|
||||
|
||||
return forms;
|
||||
}
|
||||
|
||||
int
|
||||
process_binary(std::string &fname, std::vector<std::string> &target_strs, char clear_char, bool verbose, bool path_mode)
|
||||
inline std::vector<std::string>
|
||||
expand_target_strings(const std::vector<std::string> &targets, bool path_mode)
|
||||
{
|
||||
std::vector<std::string> expanded;
|
||||
if (!path_mode) {
|
||||
expanded = targets;
|
||||
} else {
|
||||
for (const auto &target : targets) {
|
||||
std::vector<std::string> forms = expand_path_forms(target);
|
||||
expanded.insert(expanded.end(), forms.begin(), forms.end());
|
||||
}
|
||||
}
|
||||
|
||||
std::sort(expanded.begin(), expanded.end(),
|
||||
[](const std::string &a, const std::string &b) {
|
||||
if (a.size() != b.size())
|
||||
return a.size() > b.size();
|
||||
return a > b;
|
||||
});
|
||||
expanded.erase(std::unique(expanded.begin(), expanded.end()), expanded.end());
|
||||
return expanded;
|
||||
}
|
||||
|
||||
int
|
||||
process_binary(const std::string &fname, process_opts &p)
|
||||
{
|
||||
// Keep track of how many times we change the file - that's our return code
|
||||
int change_cnt = 0;
|
||||
|
||||
// Read binary contents
|
||||
std::ifstream input_fs;
|
||||
input_fs.open(fname, std::ios::binary);
|
||||
if (!input_fs.is_open()) {
|
||||
std::cerr << "Unable to open file " << fname << "\n";
|
||||
return -1;
|
||||
return change_cnt;
|
||||
}
|
||||
std::vector<char> bin_contents(std::istreambuf_iterator<char>(input_fs), {});
|
||||
input_fs.close();
|
||||
|
||||
std::set<std::string> tgt_strs;
|
||||
if (path_mode) {
|
||||
for (const auto& t : target_strs) {
|
||||
auto expanded = expand_path_forms(t);
|
||||
tgt_strs.insert(expanded.begin(), expanded.end());
|
||||
}
|
||||
} else {
|
||||
tgt_strs.insert(target_strs.begin(), target_strs.end());
|
||||
}
|
||||
|
||||
// Set up vectors of target and array of null chars
|
||||
int grcnt = 0;
|
||||
std::set<std::string>::iterator t_it;
|
||||
for (t_it = tgt_strs.begin(); t_it != tgt_strs.end(); ++t_it) {
|
||||
std::vector<char> search_chars(t_it->begin(), t_it->end());
|
||||
std::vector<char> null_chars(search_chars.size(), clear_char);
|
||||
// Process all target strings
|
||||
for (const auto &tgt : p.tgt_strs) {
|
||||
std::vector<char> search_chars(tgt.begin(), tgt.end());
|
||||
std::vector<char> null_chars(search_chars.size(), p.clear_char);
|
||||
|
||||
// Find instances of target string in binary, and replace any we find
|
||||
auto position = bin_contents.begin();
|
||||
int rcnt = 0;
|
||||
while ((position = std::search(position, bin_contents.end(), search_chars.begin(), search_chars.end())) != bin_contents.end()) {
|
||||
std::copy(null_chars.begin(), null_chars.end(), position);
|
||||
rcnt++;
|
||||
if (verbose && rcnt == 1)
|
||||
std::cout << fname << ":\n";
|
||||
if (verbose) {
|
||||
std::string cchar(1, clear_char);
|
||||
if (clear_char == '\0')
|
||||
cchar = std::string("\\0");
|
||||
std::cout << "\tclearing instance #" << rcnt << " of " << *t_it << " with the '" << cchar << "' char\n";
|
||||
}
|
||||
position += search_chars.size();
|
||||
// For clear ops we count by decrementing so we know what to print
|
||||
// - a file is only cleared or replaced, not both, and a binary
|
||||
// file can only be cleared.
|
||||
change_cnt--;
|
||||
}
|
||||
grcnt += rcnt;
|
||||
}
|
||||
if (!grcnt)
|
||||
if (change_cnt == 0)
|
||||
return 0;
|
||||
|
||||
// If we changed the contents, write them back out
|
||||
@@ -145,61 +299,53 @@ process_binary(std::string &fname, std::vector<std::string> &target_strs, char c
|
||||
output_fs.open(fname, std::ios::binary);
|
||||
if (!output_fs.is_open()) {
|
||||
std::cerr << "Unable to write updated file contents for " << fname << "\n";
|
||||
return -1;
|
||||
return change_cnt;
|
||||
}
|
||||
|
||||
std::copy(bin_contents.begin(), bin_contents.end(), std::ostreambuf_iterator<char>(output_fs));
|
||||
output_fs.close();
|
||||
|
||||
return 0;
|
||||
return change_cnt;
|
||||
}
|
||||
|
||||
int
|
||||
process_text(std::string &fname, std::string &target_str, std::string &replace_str, bool verbose, bool path_mode)
|
||||
process_text(const std::string &fname, process_opts &p)
|
||||
{
|
||||
// Make sure the replacement doesn't contain the target. If we need that
|
||||
// we'll have to be more sophisticated about the replacement logic, but
|
||||
// for now just be simple
|
||||
auto loop_check = std::search(replace_str.begin(), replace_str.end(), target_str.begin(), target_str.end());
|
||||
if (loop_check != replace_str.end()) {
|
||||
std::cerr << "Replacement string \"" << replace_str << "\" contains target string \"" << target_str << "\" - unsupported.\n";
|
||||
return -1;
|
||||
}
|
||||
|
||||
std::set<std::string> tgt_strs;
|
||||
if (path_mode) {
|
||||
auto expanded = expand_path_forms(target_str);
|
||||
tgt_strs.insert(expanded.begin(), expanded.end());
|
||||
} else {
|
||||
tgt_strs.insert(target_str);
|
||||
}
|
||||
// Keep track of how many times we change the file - that's our return code
|
||||
int change_cnt = 0;
|
||||
|
||||
// Read text contents
|
||||
std::ifstream input_fs(fname);
|
||||
if (!input_fs.is_open()) {
|
||||
std::cerr << "Unable to open file " << fname << "\n";
|
||||
return change_cnt;
|
||||
}
|
||||
std::stringstream fbuffer;
|
||||
fbuffer << input_fs.rdbuf();
|
||||
std::string nfile_contents = fbuffer.str();
|
||||
input_fs.close();
|
||||
if (!nfile_contents.length())
|
||||
return 0;
|
||||
return change_cnt;
|
||||
|
||||
// For replace ops we count by incrementing and clear opts we count by
|
||||
// decrementing, so we know what to print in the tally. A file is only
|
||||
// cleared or replaced, not both, and which it is depends on the
|
||||
// replacement string.
|
||||
int rincr = (p.replace_str.length()) ? 1 : -1;
|
||||
|
||||
// Use index and std::string::find for O(N) replacement
|
||||
bool changed = false;
|
||||
std::set<std::string>::iterator t_it;
|
||||
for (t_it = tgt_strs.begin(); t_it != tgt_strs.end(); ++t_it) {
|
||||
for (const auto &tgt : p.tgt_strs) {
|
||||
// If we're replacing a string with itself, that's a no-op
|
||||
if (tgt == p.replace_str)
|
||||
continue;
|
||||
size_t pos = 0;
|
||||
int rcnt = 0;
|
||||
while ((pos = nfile_contents.find(*t_it, pos)) != std::string::npos) {
|
||||
nfile_contents.replace(pos, t_it->size(), replace_str);
|
||||
rcnt++;
|
||||
changed = true;
|
||||
if (verbose && rcnt == 1)
|
||||
std::cout << fname << ":\n";
|
||||
if (verbose)
|
||||
std::cout << "\treplacing instance #" << rcnt << " of " << *t_it << " with " << replace_str << "\n";
|
||||
pos += replace_str.size();
|
||||
while ((pos = nfile_contents.find(tgt, pos)) != std::string::npos) {
|
||||
nfile_contents.replace(pos, tgt.size(), p.replace_str);
|
||||
pos += p.replace_str.size();
|
||||
change_cnt += rincr;
|
||||
}
|
||||
}
|
||||
if (!changed)
|
||||
if (change_cnt == 0)
|
||||
return 0;
|
||||
|
||||
// If we changed the contents, write them back out
|
||||
@@ -207,15 +353,15 @@ process_text(std::string &fname, std::string &target_str, std::string &replace_s
|
||||
output_fs.open(fname, std::ios::trunc);
|
||||
if (!output_fs.is_open()) {
|
||||
std::cerr << "Unable to write updated file contents for " << fname << "\n";
|
||||
return -1;
|
||||
exit(-1);
|
||||
}
|
||||
output_fs << nfile_contents;
|
||||
output_fs.close();
|
||||
return 0;
|
||||
return change_cnt;
|
||||
}
|
||||
|
||||
// Text vs. binary file heuristic (generated with GPT-4.1 assistance)
|
||||
bool is_binary(std::ifstream &file, size_t max_check = 4096, double nontext_threshold = 0.1)
|
||||
bool
|
||||
is_binary(std::ifstream &file, size_t max_check = 4096, double nontext_threshold = 0.1)
|
||||
{
|
||||
size_t n_read = 0;
|
||||
size_t n_nontext = 0;
|
||||
@@ -225,10 +371,10 @@ bool is_binary(std::ifstream &file, size_t max_check = 4096, double nontext_thre
|
||||
// Null byte: almost always binary
|
||||
if (c == '\0')
|
||||
return true;
|
||||
// Accept printable ASCII (32–126), CR, LF, TAB, FF
|
||||
// Accept printable ASCII, CR, LF, TAB and FF.
|
||||
if ((c >= 32 && c <= 126) || c == '\n' || c == '\r' || c == '\t' || c == '\f')
|
||||
continue;
|
||||
// Accept valid 8-bit UTF-8 lead bytes (for text, this is not 100% but helps)
|
||||
// Treat common UTF-8 lead bytes as text unless other bytes dominate.
|
||||
if ((unsigned char)c >= 0xC2 && (unsigned char)c <= 0xF4)
|
||||
continue;
|
||||
n_nontext++;
|
||||
@@ -237,35 +383,85 @@ bool is_binary(std::ifstream &file, size_t max_check = 4096, double nontext_thre
|
||||
if (n_read == 0)
|
||||
return false; // empty file: treat as text
|
||||
|
||||
// If more than 10% non-text, guess binary
|
||||
return (double)n_nontext / n_read > nontext_threshold;
|
||||
}
|
||||
|
||||
void
|
||||
process_files(std::map<std::string, std::atomic<int>> &op_tally, std::set<std::string> &files, process_opts &p)
|
||||
{
|
||||
if (files.empty() || !p.tgt_strs.size())
|
||||
return;
|
||||
|
||||
unsigned int num_threads = (unsigned int)(0.5 * (double)std::thread::hardware_concurrency());
|
||||
if (num_threads == 0)
|
||||
num_threads = 4;
|
||||
num_threads = std::min(num_threads, (unsigned int)files.size());
|
||||
|
||||
// Pre-populate op_tally entries so worker threads only update atomics.
|
||||
for (const auto &fname : files) {
|
||||
(void)op_tally[fname]; // default-construct atomic<int> (0)
|
||||
op_tally[fname].store(0, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
WorkQueue<std::string> wq;
|
||||
for (const auto &fname : files) {
|
||||
bool ok = wq.push(fname);
|
||||
(void)ok; // with our usage we haven't called close yet; ok should be true
|
||||
}
|
||||
wq.close();
|
||||
|
||||
auto worker = [&]() {
|
||||
std::string fname;
|
||||
while (wq.pop(fname)) {
|
||||
std::ifstream check_fs(fname, std::ios::binary);
|
||||
if (!check_fs.is_open()) {
|
||||
std::cerr << "Error: unable to open " << fname << "\n";
|
||||
op_tally[fname].store(0, std::memory_order_relaxed);
|
||||
continue;
|
||||
}
|
||||
bool binary_mode = (p.force_binary) ? true : is_binary(check_fs);
|
||||
check_fs.close();
|
||||
|
||||
int result = 0;
|
||||
if (binary_mode && p.force_text) {
|
||||
std::cerr << "Error: string replacement indicated, but file is binary: " << fname << "\n";
|
||||
op_tally[fname].store(0, std::memory_order_relaxed);
|
||||
continue;
|
||||
}
|
||||
if (binary_mode && !p.text_only)
|
||||
result = process_binary(fname, p);
|
||||
if (!binary_mode && !p.binary_only)
|
||||
result = process_text(fname, p);
|
||||
op_tally[fname].store(result, std::memory_order_relaxed);
|
||||
}
|
||||
};
|
||||
|
||||
std::vector<std::thread> threads;
|
||||
threads.reserve(num_threads);
|
||||
for (unsigned int i = 0; i < num_threads; ++i)
|
||||
threads.emplace_back(worker);
|
||||
|
||||
for (auto &t : threads)
|
||||
t.join();
|
||||
}
|
||||
|
||||
int
|
||||
main(int argc, const char *argv[])
|
||||
{
|
||||
bool binary_mode = false;
|
||||
bool binary_test_mode = false;
|
||||
bool clear_mode = false;
|
||||
char clear_char = '\0';
|
||||
bool swap_mode = false;
|
||||
bool text_mode = false;
|
||||
bool path_mode = false;
|
||||
bool verbose = false;
|
||||
process_opts p;
|
||||
bool legacy_binary_mode = false;
|
||||
std::string file_list;
|
||||
|
||||
cxxopts::Options options(argv[0],
|
||||
"A program to clear or replace strings in files.\n"
|
||||
"Clear or replace strings in files.\n"
|
||||
"\n"
|
||||
"When the -p option is activated, a target string supplied for clearing\n"
|
||||
"or replacement appears to be a filesystem path (e.g., an existing file\n"
|
||||
"or directory), this tool will automatically search for and operate on\n"
|
||||
"all recognized forms of that path within the file. This includes:\n"
|
||||
" - the original path string as supplied\n"
|
||||
" - its absolute path form\n"
|
||||
" - its canonical (fully resolved, with symlinks removed) form\n"
|
||||
" - its normalized (syntactically simplified) form\n"
|
||||
"This ensures that both relative and absolute references, as well as\n"
|
||||
"symlinked and normalized forms of the same file, are detected and processed.\n"
|
||||
"strclear -B <filename>\n"
|
||||
"strclear --classify [--files <filelist> | <filename> ...]\n"
|
||||
"strclear <filename> <target_str> [replacement_str]\n"
|
||||
"strclear -f <filelist> <target_str> [replacement_str]\n"
|
||||
"\n"
|
||||
"-B returns 0 for binary input and 1 for text input.\n"
|
||||
"--classify emits one JSON record per input path.\n"
|
||||
);
|
||||
|
||||
std::vector<std::string> nonopts;
|
||||
@@ -275,38 +471,40 @@ main(int argc, const char *argv[])
|
||||
options
|
||||
.set_width(70)
|
||||
.add_options()
|
||||
("B,is_binary","Test the file to see if it is a binary file.)", cxxopts::value<bool>(binary_test_mode))
|
||||
("b,binary", "Treat the input file as binary. (Note that only string clearing is supported with binary files.)", cxxopts::value<bool>(binary_mode))
|
||||
("c,clear", "Replace strings in files by overwriting a specified character (defaults to NULL.) Note: If the target string is a path and -p is specified, all equivalent path forms are processed.", cxxopts::value<bool>(clear_mode))
|
||||
("clear_char", "Specify a character to use when clearing strings in files", cxxopts::value<char>(clear_char))
|
||||
("r,replace", "Replace one string with another (text mode only). Note: If the target string is a path and -p is specified, all equivalent path forms are processed.", cxxopts::value<bool>(swap_mode))
|
||||
("p,paths", "Expand a target string that is a file path into all recognized forms (original, absolute, canonical, normalized) for searching and replacing/clearing.", cxxopts::value<bool>(path_mode))
|
||||
("t,text", "Refuse to run unless the input file is a text file.", cxxopts::value<bool>(text_mode))
|
||||
("v,verbose", "Verbose reporting during processing", cxxopts::value<bool>(verbose))
|
||||
("h,help", "Print help")
|
||||
("B,is_binary", "Test one file: return 0 for binary input and 1 for text input.", cxxopts::value<bool>(p.binary_test_mode))
|
||||
("t,text", "Compatibility alias: force text replacement mode.", cxxopts::value<bool>(p.force_text))
|
||||
("text-only", "Skip inputs that are binary files.", cxxopts::value<bool>(p.text_only))
|
||||
("b,binary", "Compatibility alias: force binary clear mode.", cxxopts::value<bool>(legacy_binary_mode))
|
||||
("binary-only", "Skip inputs that are text files.", cxxopts::value<bool>(p.binary_only))
|
||||
("c,clear", "Compatibility option for binary clear mode.", cxxopts::value<bool>(p.clear_mode))
|
||||
("classify", "Classify files as TEXT or BINARY, one JSON record per path.", cxxopts::value<bool>(p.classify_mode))
|
||||
("r,replace", "Compatibility option for text replacement mode.", cxxopts::value<bool>())
|
||||
("f,files", "Provide a list of files to process.", cxxopts::value<std::string>(file_list))
|
||||
("clear-char", "Specify a character to use when clearing strings in files", cxxopts::value<char>(p.clear_char))
|
||||
("clear_char", "Specify a character to use when clearing strings in files", cxxopts::value<char>(p.clear_char))
|
||||
("p,paths", "Expand target strings that are file paths into recognized forms (original, absolute, canonical, normalized).", cxxopts::value<bool>(p.path_mode))
|
||||
("v,verbose", "Verbose reporting during processing", cxxopts::value<bool>(p.verbose))
|
||||
("h,help", "Print help")
|
||||
;
|
||||
auto result = options.parse(argc, argv);
|
||||
|
||||
nonopts = result.unmatched();
|
||||
|
||||
// Do we want help?
|
||||
if (result.count("help")) {
|
||||
std::cout << options.help({""}) << std::endl;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (binary_mode && text_mode) {
|
||||
std::cerr << "Error: need to specify either binary or text mode, not both\n";
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!clear_mode && !swap_mode && !binary_test_mode) {
|
||||
std::cerr << "Error: need to specify either clear mode (-c), replace mode (-r), or binary file test mode (-B)\n";
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (clear_mode && swap_mode) {
|
||||
std::cerr << "Error: need to specify either clear or replace mode, not both\n";
|
||||
return -1;
|
||||
nonopts = result.unmatched();
|
||||
if (legacy_binary_mode)
|
||||
p.force_binary = true;
|
||||
if (p.force_binary)
|
||||
p.binary_only = false;
|
||||
if (result.count("replace")) {
|
||||
if (p.clear_mode) {
|
||||
std::cerr << "Error: need to specify either clear or replace mode, not both.\n";
|
||||
return -1;
|
||||
}
|
||||
p.force_text = true;
|
||||
}
|
||||
}
|
||||
catch (const cxxopts::exceptions::exception& e)
|
||||
@@ -315,70 +513,211 @@ main(int argc, const char *argv[])
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Unless the goal is strictly to test file type, we need at least two args
|
||||
if (nonopts.size() < 2 && !binary_test_mode) {
|
||||
/////////////////////////////////////////
|
||||
// Do some option checking and validation
|
||||
/////////////////////////////////////////
|
||||
|
||||
if (p.classify_mode && p.binary_test_mode) {
|
||||
std::cerr << "Error: specify either -B or --classify, not both.\n";
|
||||
return -1;
|
||||
}
|
||||
if (p.classify_mode && (legacy_binary_mode || p.clear_mode || p.force_text || p.text_only || p.binary_only || p.path_mode)) {
|
||||
std::cerr << "Error: --classify cannot be combined with rewrite mode options.\n";
|
||||
return -1;
|
||||
}
|
||||
|
||||
// binary_only ∩ text_only == NULL set
|
||||
if ((p.binary_only || p.force_binary) && (p.text_only || p.force_text)) {
|
||||
std::cerr << "Error: can specify binary-only or text-only, not both.\n";
|
||||
std::cout << options.help({""}) << std::endl;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// If we only have a filename and a single string, the only thing we can
|
||||
// do is treat the file as binary and replace the string
|
||||
if (nonopts.size() == 2 && swap_mode) {
|
||||
std::cerr << "Error: string replacement mode indicated, but no replacement specified\n";
|
||||
// If we're testing whether a file is binary, we only take one argument
|
||||
if (p.binary_test_mode && nonopts.size() != 1) {
|
||||
std::cerr << "Error: -B accepts exactly one file path as input.\n";
|
||||
std::cout << options.help({""}) << std::endl;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// For swap_mode, we need exactly 3 nonopts (file, target, replace)
|
||||
if (swap_mode && nonopts.size() != 3) {
|
||||
std::cerr << "Error: replacing string in text file - need file, target string and replacement string as arguments.\n";
|
||||
return -1;
|
||||
}
|
||||
|
||||
std::string fname(nonopts[0]);
|
||||
|
||||
// Determine if the file is a binary or text file, if we've not been told
|
||||
// to treat it as binary explicitly with -b. If we've been told text mode
|
||||
// we still check to make sure we really have a text file before processing.
|
||||
if (!binary_mode) {
|
||||
std::ifstream check_fs(fname, std::ios::binary);
|
||||
if (p.binary_test_mode) {
|
||||
std::ifstream check_fs(nonopts[0], std::ios::binary);
|
||||
if (!check_fs.is_open()) {
|
||||
std::cerr << "Error: unable to open " << fname << "\n";
|
||||
std::cerr << "Unable to open file " << nonopts[0] << "\n";
|
||||
return -1;
|
||||
}
|
||||
binary_mode = is_binary(check_fs);
|
||||
bool binary_mode = is_binary(check_fs);
|
||||
check_fs.close();
|
||||
return binary_mode ? 0 : 1;
|
||||
}
|
||||
|
||||
// If all we're supposed to do is determine the type, return success (0) if
|
||||
// the file is binary, else 1
|
||||
if (binary_test_mode)
|
||||
return (binary_mode) ? 0 : 1;
|
||||
if (p.classify_mode) {
|
||||
std::vector<std::string> files;
|
||||
if (file_list.length()) {
|
||||
if (nonopts.size()) {
|
||||
std::cerr << "Error: specify either --files or file paths for --classify, not both.\n";
|
||||
return -1;
|
||||
}
|
||||
std::ifstream instream(file_list);
|
||||
if (!instream.is_open()) {
|
||||
std::cerr << "Error: Could not open " << file_list << "\n";
|
||||
return -1;
|
||||
}
|
||||
std::string line;
|
||||
while (std::getline(instream, line)) {
|
||||
if (line.length())
|
||||
files.push_back(line);
|
||||
}
|
||||
instream.close();
|
||||
} else {
|
||||
for (const auto &fname : nonopts)
|
||||
files.push_back(fname);
|
||||
}
|
||||
if (files.empty()) {
|
||||
std::cerr << "Error: --classify needs at least one file path.\n";
|
||||
return -1;
|
||||
}
|
||||
int ret = 0;
|
||||
for (const auto &fname : files) {
|
||||
std::ifstream check_fs(fname, std::ios::binary);
|
||||
if (!check_fs.is_open()) {
|
||||
std::cerr << "Unable to open file " << fname << "\n";
|
||||
ret = -1;
|
||||
continue;
|
||||
}
|
||||
bool binary_mode = is_binary(check_fs);
|
||||
check_fs.close();
|
||||
std::cout << "{\"type\":\"" << (binary_mode ? "BINARY" : "TEXT") << "\",\"path\":\"" << json_escape(fname) << "\"}\n";
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
if (binary_mode && swap_mode) {
|
||||
std::cerr << "Error: string replacement indicated, but file is binary\n";
|
||||
// Everything else needs at least a filename or file list and a target string
|
||||
if ((file_list.length() && !p.binary_only && !p.force_binary) && (nonopts.size() != 1 && nonopts.size() != 2)) {
|
||||
std::cerr << "Error: when using a file list we need a target string and (optionally) a replacement string.\n";
|
||||
std::cout << options.help({""}) << std::endl;
|
||||
return -1;
|
||||
}
|
||||
if ((file_list.length() && (p.binary_only || p.force_binary)) && nonopts.empty()) {
|
||||
std::cerr << "Error: binary file-list processing needs at least one target string.\n";
|
||||
return -1;
|
||||
}
|
||||
if ((!file_list.length() && !p.binary_only && !p.force_binary) && (nonopts.size() != 2 && nonopts.size() != 3)) {
|
||||
std::cerr << "Error: we need a file, a target string and (optionally) a replacement string.\n";
|
||||
std::cout << options.help({""}) << std::endl;
|
||||
return -1;
|
||||
}
|
||||
if ((!file_list.length() && p.binary_only) && (nonopts.size() != 2)) {
|
||||
std::cerr << "Error: when in binary-only mode we only accept a filename, a target string and (optionally) a --clear-char character.\n";
|
||||
return -1;
|
||||
}
|
||||
if ((!file_list.length() && p.force_binary) && (nonopts.size() < 2)) {
|
||||
std::cerr << "Error: when in binary mode we need a filename and one or more target strings.\n";
|
||||
return -1;
|
||||
}
|
||||
if (p.force_text && !file_list.length() && nonopts.size() != 3) {
|
||||
std::cerr << "Error: replacing string in text file - need file, target string and replacement string as arguments.\n";
|
||||
return -1;
|
||||
}
|
||||
if (p.force_text && file_list.length() && nonopts.size() != 2) {
|
||||
std::cerr << "Error: replacing strings in a text file list needs target string and replacement string arguments.\n";
|
||||
return -1;
|
||||
}
|
||||
|
||||
// If we're in binary mode we're just nulling out the target string(s).
|
||||
if (binary_mode) {
|
||||
std::vector<std::string> target_strs;
|
||||
for (size_t i = 1; i < nonopts.size(); i++) {
|
||||
if (nonopts[i].length())
|
||||
target_strs.push_back(nonopts[i]);
|
||||
std::set<std::string> files;
|
||||
std::string target_str;
|
||||
if (!file_list.length()) {
|
||||
files.insert(nonopts[0]);
|
||||
target_str = nonopts[1];
|
||||
if (p.force_binary) {
|
||||
for (size_t i = 1; i < nonopts.size(); i++)
|
||||
p.tgt_strs.push_back(nonopts[i]);
|
||||
} else {
|
||||
p.replace_str = (nonopts.size() > 2) ? std::string(nonopts[2]) : std::string("");
|
||||
}
|
||||
if (!target_strs.size()) {
|
||||
std::cerr << "Target strings cannot be empty.\n";
|
||||
} else {
|
||||
std::ifstream instream(file_list);
|
||||
if (!instream.is_open()) {
|
||||
std::cerr << "Error: Could not open " << file_list << "\n";
|
||||
return -1;
|
||||
}
|
||||
return process_binary(fname, target_strs, clear_char, verbose, path_mode);
|
||||
std::string line;
|
||||
while (std::getline(instream, line))
|
||||
files.insert(line);
|
||||
instream.close();
|
||||
|
||||
target_str = nonopts[0];
|
||||
if (p.force_binary || p.binary_only) {
|
||||
for (size_t i = 0; i < nonopts.size(); i++)
|
||||
p.tgt_strs.push_back(nonopts[i]);
|
||||
} else if (!p.binary_only) {
|
||||
p.replace_str = (nonopts.size() > 1) ? std::string(nonopts[1]) : std::string("");
|
||||
}
|
||||
}
|
||||
|
||||
std::string target_str(nonopts[1]);
|
||||
if (!target_str.length()) {
|
||||
std::cerr << "Target string cannot be empty.\n";
|
||||
if (!p.force_binary && !target_str.length()) {
|
||||
std::cerr << "Error: empty target string supplied\n";
|
||||
return -1;
|
||||
}
|
||||
std::string replace_str = (swap_mode) ? std::string(nonopts[2]) : std::string("");
|
||||
return process_text(fname, target_str, replace_str, verbose, path_mode);
|
||||
|
||||
if (!p.force_binary && !(file_list.length() && p.binary_only)) {
|
||||
p.tgt_strs.clear();
|
||||
p.tgt_strs.push_back(target_str);
|
||||
}
|
||||
p.tgt_strs = expand_target_strings(p.tgt_strs, p.path_mode);
|
||||
|
||||
std::map<std::string, std::atomic<int>> op_tally;
|
||||
process_files(op_tally, files, p);
|
||||
|
||||
if (p.verbose) {
|
||||
|
||||
// Verify we did something on some file before we print anything
|
||||
bool did_op = false;
|
||||
for (const auto &kv : op_tally) {
|
||||
if (kv.second.load(std::memory_order_relaxed) != 0) {
|
||||
did_op = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If we did something interesting, report it - otherwise just note
|
||||
// that nothing happened.
|
||||
if (did_op) {
|
||||
std::string cchar(1, p.clear_char);
|
||||
if (p.clear_char == '\0')
|
||||
cchar = std::string("\\0");
|
||||
|
||||
std::cout << "Summary:\n";
|
||||
std::cout << " Original target string: " << target_str << "\n";
|
||||
if (p.path_mode) {
|
||||
std::cout << " Expanded path targets: \n";
|
||||
for (const auto &t : p.tgt_strs) {
|
||||
if (t == target_str)
|
||||
continue;
|
||||
std::cout << " : " << t << "\n";
|
||||
}
|
||||
}
|
||||
if (p.clear_char != '\0')
|
||||
std::cout << " Clear char: " << cchar << "\n";
|
||||
if (p.replace_str.length())
|
||||
std::cout << " Replacement string: " << p.replace_str << "\n";
|
||||
|
||||
std::cout << "----------Processed Paths-------\n";
|
||||
|
||||
// print tally
|
||||
for (const auto &kv : op_tally) {
|
||||
int v = kv.second.load(std::memory_order_relaxed);
|
||||
if (!v)
|
||||
continue;
|
||||
std::cout << kv.first << ": ";
|
||||
if (v < 0) {
|
||||
std::cout << " cleared " << -1*v << " instances\n";
|
||||
} else {
|
||||
std::cout << "replaced " << v << " instances\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Local Variables:
|
||||
@@ -389,4 +728,3 @@ main(int argc, const char *argv[])
|
||||
// c-file-style: "stroustrup"
|
||||
// End:
|
||||
// ex: shiftwidth=4 tabstop=8
|
||||
|
||||
|
||||
+167
-183
@@ -2,234 +2,218 @@
|
||||
# strclear tests
|
||||
###################################################
|
||||
|
||||
# Counting characters in a binary file is a little outside
|
||||
# CMake's wheelhouse, so add a utility to do it.
|
||||
add_executable(charcnt charcnt.cpp)
|
||||
|
||||
# Directory for test artifacts
|
||||
set(TESTDIR "${CMAKE_CURRENT_BINARY_DIR}/testfiles")
|
||||
file(MAKE_DIRECTORY "${TESTDIR}")
|
||||
|
||||
# Helper function to compare file contents
|
||||
function(assert_file_contains file expected_text)
|
||||
file(READ "${file}" file_content)
|
||||
string(FIND "${file_content}" "${expected_text}" found)
|
||||
if(found EQUAL -1)
|
||||
message(FATAL_ERROR "File ${file} does not contain expected text: '${expected_text}'\nActual content:\n${file_content}")
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
# Helper function to assert file does NOT contain text
|
||||
function(assert_file_not_contains file unexpected_text)
|
||||
file(READ "${file}" file_content)
|
||||
string(FIND "${file_content}" "${unexpected_text}" found)
|
||||
if(NOT found EQUAL -1)
|
||||
message(FATAL_ERROR "File ${file} still contains text: '${unexpected_text}'\nActual content:\n${file_content}")
|
||||
endif()
|
||||
endfunction()
|
||||
# We need to use this rather than cmake_path(NORMALIZE) to resolve symlinks.
|
||||
# Do this AFTER file(MAKE_DIRECTORY) is called.
|
||||
file(REAL_PATH "${TESTDIR}" TESTDIR)
|
||||
|
||||
# --- Test 1: Non-filesystem string replace ---
|
||||
set(TEXT1 "This is a test string foo and bar.\nAnother foo here.")
|
||||
set(FILE1 "${TESTDIR}/plain.txt")
|
||||
file(WRITE "${FILE1}" "${TEXT1}")
|
||||
|
||||
# Set up a template to use as input. Since the contents are altered, the test
|
||||
# needs to stage it each time - that's why we write it out with the .in suffix.
|
||||
set(PLAINFILE "plain.txt")
|
||||
file(WRITE "${TESTDIR}/${PLAINFILE}.in" "This is a test string foo and bar.\nAnother foo here.")
|
||||
add_test(NAME BasicStringReplace
|
||||
COMMAND strclear -r "${FILE1}" foo baz
|
||||
)
|
||||
set_tests_properties(BasicStringReplace PROPERTIES
|
||||
FIXTURES_SETUP basic_replace
|
||||
)
|
||||
|
||||
add_test(NAME VerifyBasicStringReplace
|
||||
COMMAND ${CMAKE_COMMAND} -Dfile=${FILE1} -Dexpected=baz -P "${CMAKE_CURRENT_LIST_DIR}/verify_contains.cmake"
|
||||
)
|
||||
set_tests_properties(VerifyBasicStringReplace PROPERTIES
|
||||
FIXTURES_REQUIRED basic_replace
|
||||
COMMAND ${CMAKE_COMMAND} -DSDIR=${CMAKE_CURRENT_SOURCE_DIR}/testdata -DTDIR=${TESTDIR} -DTFILE=${PLAINFILE}
|
||||
-DTNUM=1 -DTGTSTR=foo -DEXPSTR=CLEARED -DEXPCNT=2 -DSTRCLEAR=$<TARGET_FILE:strclear> -DSTRCLEAR_OPTS=-v
|
||||
-P "${CMAKE_CURRENT_SOURCE_DIR}/run_test.cmake"
|
||||
)
|
||||
|
||||
# --- Test 2: Path expansion (filesystem) ---
|
||||
set(REL_PATH "relpath.txt")
|
||||
set(ABS_PATH "${TESTDIR}/abspath.txt")
|
||||
set(FILE2 "${TESTDIR}/withpaths.txt")
|
||||
|
||||
get_filename_component(ABS_PATH_FOR_FILE "${ABS_PATH}" ABSOLUTE)
|
||||
set(TEXT2 "Here is a path: ${REL_PATH}\nAnd here is another: ${ABS_PATH_FOR_FILE}\n")
|
||||
file(WRITE "${FILE2}" "${TEXT2}")
|
||||
|
||||
file(WRITE "${TESTDIR}/${REL_PATH}" "dummy")
|
||||
file(WRITE "${ABS_PATH}" "dummy")
|
||||
# Set up the template for the to-be-altered file. For this test we want to
|
||||
# point to a file that is present - we can just target the plain.in file from
|
||||
# the first test, since that is already placed on the filesystem by CMake.
|
||||
set(WITHPATHSFILE "withpaths.txt")
|
||||
get_filename_component(ABS_PATH_FOR_FILE "${TESTDIR}/${PLAINFILE}.in" ABSOLUTE)
|
||||
set(WITHPATHS_TXT "Relative: ${PLAINFILE}.in\nAbsolute: ${ABS_PATH_FOR_FILE}\n")
|
||||
file(WRITE "${TESTDIR}/${WITHPATHSFILE}.in" "${WITHPATHS_TXT}")
|
||||
|
||||
add_test(NAME PathExpansionReplace
|
||||
COMMAND strclear -r -p "${FILE2}" "${REL_PATH}" CLEARED
|
||||
)
|
||||
set_tests_properties(PathExpansionReplace PROPERTIES
|
||||
FIXTURES_SETUP path_replace
|
||||
)
|
||||
|
||||
add_test(NAME VerifyPathExpansionReplace
|
||||
COMMAND ${CMAKE_COMMAND} -Dfile=${FILE2} -Dexpected=CLEARED -P "${CMAKE_CURRENT_LIST_DIR}/verify_contains.cmake"
|
||||
)
|
||||
set_tests_properties(VerifyPathExpansionReplace PROPERTIES
|
||||
FIXTURES_REQUIRED path_replace
|
||||
COMMAND ${CMAKE_COMMAND} -DSDIR=${CMAKE_CURRENT_SOURCE_DIR}/testdata -DTDIR=${TESTDIR} -DTFILE=${WITHPATHSFILE}
|
||||
-DTNUM=2 -DTGTSTR=${PLAINFILE}.in -DEXPSTR=CLEARED -DEXPCNT=2 -DSTRCLEAR=$<TARGET_FILE:strclear> -DSTRCLEAR_OPTS=-v\;-p
|
||||
-P "${CMAKE_CURRENT_SOURCE_DIR}/run_test.cmake"
|
||||
)
|
||||
|
||||
# --- Test 3: Symlink path expansion ---
|
||||
set(SRC_FILE "${TESTDIR}/target.txt")
|
||||
set(SYM_PATH "${TESTDIR}/symlink.txt")
|
||||
set(FILE3 "${TESTDIR}/withsymlink.txt")
|
||||
set(TEXT3 "Original: ${SRC_FILE}\nSymlink: ${SYM_PATH}\n")
|
||||
file(WRITE "${FILE3}" "${TEXT3}")
|
||||
file(WRITE "${SRC_FILE}" "dummy")
|
||||
# IFF the OS supports making symlinks, we can do this test - find out
|
||||
set(SYMFILE "symlink.txt")
|
||||
file(CREATE_LINK "${TESTDIR}/${PLAINFILE}.in" "${TESTDIR}/${SYMFILE}" SYMBOLIC RESULT symlink_ret)
|
||||
if(symlink_ret EQUAL 0)
|
||||
# Set up the template for the to-be-altered file.
|
||||
set(WITHSYMFILE "withsymlink.txt")
|
||||
set(WITHSYMLINK_TXT "Original: ${TESTDIR}/${PLAINFILE}.in\nSymlink: ${TESTDIR}/${SYMFILE}\n")
|
||||
file(WRITE "${TESTDIR}/${WITHSYMFILE}.in" "${WITHSYMLINK_TXT}")
|
||||
|
||||
file(CREATE_LINK "${SRC_FILE}" "${SYM_PATH}" SYMBOLIC)
|
||||
|
||||
add_test(NAME SymlinkPathExpansionReplace
|
||||
COMMAND strclear -r -p "${FILE3}" "${SYM_PATH}" "CLEAN"
|
||||
)
|
||||
set_tests_properties(SymlinkPathExpansionReplace PROPERTIES
|
||||
FIXTURES_SETUP symlink_replace
|
||||
)
|
||||
|
||||
add_test(NAME VerifySymlinkPathExpansionReplace
|
||||
COMMAND ${CMAKE_COMMAND} -Dfile=${FILE3} -Dexpected=CLEAN -P "${CMAKE_CURRENT_LIST_DIR}/verify_contains.cmake"
|
||||
)
|
||||
set_tests_properties(VerifySymlinkPathExpansionReplace PROPERTIES
|
||||
FIXTURES_REQUIRED symlink_replace
|
||||
)
|
||||
add_test(NAME SymlinkPathExpansionReplace
|
||||
COMMAND ${CMAKE_COMMAND} -DSDIR=${CMAKE_CURRENT_SOURCE_DIR}/testdata -DTDIR=${TESTDIR} -DTFILE=${WITHSYMFILE}
|
||||
-DTNUM=3 -DTGTSTR=${SYMFILE} -DEXPSTR=CLEARED -DEXPCNT=2 -DSTRCLEAR=$<TARGET_FILE:strclear> -DSTRCLEAR_OPTS=-v\;-p
|
||||
-P "${CMAKE_CURRENT_SOURCE_DIR}/run_test.cmake"
|
||||
)
|
||||
endif()
|
||||
|
||||
# --- Test 4: No Replacement Occurs ---
|
||||
set(FILE_NOMATCH "${TESTDIR}/no_match.txt")
|
||||
set(TEXT_NOMATCH "Just a normal text file.")
|
||||
file(WRITE "${FILE_NOMATCH}" "${TEXT_NOMATCH}")
|
||||
|
||||
set(NOMATCHFILE "no_match.txt")
|
||||
file(WRITE "${TESTDIR}/${NOMATCHFILE}.in" "Just a normal text file.")
|
||||
add_test(NAME NoReplacementIfNoMatch
|
||||
COMMAND strclear -r "${FILE_NOMATCH}" "foo" "bar"
|
||||
)
|
||||
set_tests_properties(NoReplacementIfNoMatch PROPERTIES
|
||||
FIXTURES_SETUP no_replace
|
||||
COMMAND ${CMAKE_COMMAND} -DSDIR=${CMAKE_CURRENT_SOURCE_DIR}/testdata -DTDIR=${TESTDIR} -DTFILE=${NOMATCHFILE}
|
||||
-DTNUM=4 -DTGTSTR=foo -DEXPSTR=CLEARED -DEXPCNT=0 -DSTRCLEAR=$<TARGET_FILE:strclear> -DSTRCLEAR_OPTS=-v
|
||||
-P "${CMAKE_CURRENT_SOURCE_DIR}/run_test.cmake"
|
||||
)
|
||||
|
||||
add_test(NAME VerifyNoReplacementIfNoMatch
|
||||
COMMAND ${CMAKE_COMMAND} -Dfile=${FILE_NOMATCH} -Dexpected=${TEXT_NOMATCH} -P "${CMAKE_CURRENT_LIST_DIR}/verify_contains.cmake"
|
||||
)
|
||||
set_tests_properties(VerifyNoReplacementIfNoMatch PROPERTIES
|
||||
FIXTURES_REQUIRED no_replace
|
||||
)
|
||||
|
||||
# --- Test 5: Multiple Occurrences ---
|
||||
set(FILE_MULTI "${TESTDIR}/multi.txt")
|
||||
set(TEXT_MULTI "foo bar foo baz foo")
|
||||
file(WRITE "${FILE_MULTI}" "${TEXT_MULTI}")
|
||||
|
||||
add_test(NAME MultipleReplacements
|
||||
COMMAND strclear -r "${FILE_MULTI}" foo CLEARED
|
||||
)
|
||||
set_tests_properties(MultipleReplacements PROPERTIES
|
||||
FIXTURES_SETUP multi_replace
|
||||
)
|
||||
|
||||
add_test(NAME VerifyMultipleReplacements
|
||||
COMMAND ${CMAKE_COMMAND} -Dfile=${FILE_MULTI} -Dexpected=CLEARED -P "${CMAKE_CURRENT_LIST_DIR}/verify_contains.cmake"
|
||||
)
|
||||
set_tests_properties(VerifyMultipleReplacements PROPERTIES
|
||||
FIXTURES_REQUIRED multi_replace
|
||||
# --- Test 5: Many Occurrences ---
|
||||
set(MULTIFILE "multi.txt")
|
||||
file(WRITE "${TESTDIR}/${MULTIFILE}.in" "foo bar foo baz ")
|
||||
foreach(i RANGE 100)
|
||||
file(APPEND "${TESTDIR}/${MULTIFILE}.in" "foo ")
|
||||
endforeach()
|
||||
add_test(NAME ManyOccurrences
|
||||
COMMAND ${CMAKE_COMMAND} -DSDIR=${CMAKE_CURRENT_SOURCE_DIR}/testdata -DTDIR=${TESTDIR} -DTFILE=${MULTIFILE}
|
||||
-DTNUM=5 -DTGTSTR=foo -DEXPSTR=CLEARED -DEXPCNT=103 -DSTRCLEAR=$<TARGET_FILE:strclear> -DSTRCLEAR_OPTS=-v
|
||||
-P "${CMAKE_CURRENT_SOURCE_DIR}/run_test.cmake"
|
||||
)
|
||||
|
||||
# --- Test 6: Empty Search String (should fail) ---
|
||||
set(FILE_EMPTYSEARCH "${TESTDIR}/emptysearch.txt")
|
||||
file(WRITE "${FILE_EMPTYSEARCH}" "some text")
|
||||
|
||||
add_test(NAME EmptySearchString
|
||||
COMMAND strclear -r "${FILE_EMPTYSEARCH}" "" replacement
|
||||
)
|
||||
set_tests_properties(EmptySearchString PROPERTIES
|
||||
WILL_FAIL TRUE
|
||||
COMMAND ${CMAKE_COMMAND} -DSDIR=${CMAKE_CURRENT_SOURCE_DIR}/testdata -DTDIR=${TESTDIR} -DTFILE=${NOMATCHFILE}
|
||||
-DTNUM=6 -DTGTSTR= -DEXPSTR=CLEARED -DEXPCNT=0 -DSTRCLEAR=$<TARGET_FILE:strclear> -DSTRCLEAR_OPTS=-v
|
||||
-P "${CMAKE_CURRENT_SOURCE_DIR}/run_test.cmake"
|
||||
)
|
||||
|
||||
# --- Test 7: File with Only Search String ---
|
||||
set(FILE_ONLYSEARCH "${TESTDIR}/onlysearch.txt")
|
||||
file(WRITE "${FILE_ONLYSEARCH}" "foo")
|
||||
|
||||
set(ONLYFILE "onlysearch.txt")
|
||||
file(WRITE "${TESTDIR}/${ONLYFILE}.in" "foo")
|
||||
add_test(NAME OnlySearchString
|
||||
COMMAND strclear -r "${FILE_ONLYSEARCH}" foo gone
|
||||
)
|
||||
set_tests_properties(OnlySearchString PROPERTIES
|
||||
FIXTURES_SETUP onlysearch_replace
|
||||
COMMAND ${CMAKE_COMMAND} -DSDIR=${CMAKE_CURRENT_SOURCE_DIR}/testdata -DTDIR=${TESTDIR} -DTFILE=${ONLYFILE}
|
||||
-DTNUM=7 -DTGTSTR=foo -DEXPSTR=CLEARED -DEXPCNT=1 -DSTRCLEAR=$<TARGET_FILE:strclear> -DSTRCLEAR_OPTS=-v
|
||||
-P "${CMAKE_CURRENT_SOURCE_DIR}/run_test.cmake"
|
||||
)
|
||||
|
||||
add_test(NAME VerifyOnlySearchString
|
||||
COMMAND ${CMAKE_COMMAND} -Dfile=${FILE_ONLYSEARCH} -Dexpected=gone -P "${CMAKE_CURRENT_LIST_DIR}/verify_contains.cmake"
|
||||
)
|
||||
set_tests_properties(VerifyOnlySearchString PROPERTIES
|
||||
FIXTURES_REQUIRED onlysearch_replace
|
||||
)
|
||||
|
||||
# --- Test 8: Large File Replacement ---
|
||||
set(FILE_LARGE "${TESTDIR}/large.txt")
|
||||
set(SEARCH_LARGE "foo")
|
||||
set(REPLACE_LARGE "bar")
|
||||
file(WRITE "${FILE_LARGE}" "")
|
||||
foreach(i RANGE 1 1000)
|
||||
file(APPEND "${FILE_LARGE}" "foo this is line ${i}\n")
|
||||
# --- Test 8: Large(ish) File Replacement ---
|
||||
set(LARGEFILE "large.txt")
|
||||
file(WRITE "${TESTDIR}/${LARGEFILE}.in" "Autogenerated large file:\n")
|
||||
foreach(i RANGE 1000)
|
||||
file(APPEND "${TESTDIR}/${LARGEFILE}.in" "Line foo: foo${i}foo\n")
|
||||
endforeach()
|
||||
|
||||
add_test(NAME LargeFileReplacement
|
||||
COMMAND strclear -r "${FILE_LARGE}" foo bar
|
||||
)
|
||||
set_tests_properties(LargeFileReplacement PROPERTIES
|
||||
FIXTURES_SETUP large_replace
|
||||
add_test(NAME LargeFile
|
||||
COMMAND ${CMAKE_COMMAND} -DSDIR=${CMAKE_CURRENT_SOURCE_DIR}/testdata -DTDIR=${TESTDIR} -DTFILE=${LARGEFILE}
|
||||
-DTNUM=8 -DTGTSTR=foo -DEXPSTR=CLEARED -DEXPCNT=3003 -DQUIET_RUN=1 -DSTRCLEAR=$<TARGET_FILE:strclear> -DSTRCLEAR_OPTS=-v
|
||||
-P "${CMAKE_CURRENT_SOURCE_DIR}/run_test.cmake"
|
||||
)
|
||||
|
||||
add_test(NAME VerifyLargeFileReplacement
|
||||
COMMAND ${CMAKE_COMMAND} -Dfile=${FILE_LARGE} -Dexpected=bar -P "${CMAKE_CURRENT_LIST_DIR}/verify_contains.cmake"
|
||||
)
|
||||
set_tests_properties(VerifyLargeFileReplacement PROPERTIES
|
||||
FIXTURES_REQUIRED large_replace
|
||||
# --- Test 9: Binary File Alteration ---
|
||||
set(BINARYFILE "binary_file.bin")
|
||||
file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/${BINARYFILE}" DESTINATION ${TESTDIR})
|
||||
file(RENAME "${TESTDIR}/${BINARYFILE}" "${TESTDIR}/${BINARYFILE}.in")
|
||||
add_test(NAME BinaryFile
|
||||
COMMAND ${CMAKE_COMMAND} -DSDIR=${CMAKE_CURRENT_SOURCE_DIR}/testdata -DTDIR=${TESTDIR} -DTFILE=${BINARYFILE}
|
||||
-DTNUM=9 -DTGTSTR=foo -DEXPSTR=NULL -DEXPCNT=6 -DQUIET_RUN=1 -DSTRCLEAR=$<TARGET_FILE:strclear> -DSTRCLEAR_OPTS=-v
|
||||
-DCHARCNT=$<TARGET_FILE:charcnt>
|
||||
-P "${CMAKE_CURRENT_SOURCE_DIR}/run_test.cmake"
|
||||
)
|
||||
|
||||
# --- Test 9: Unicode/UTF-8 Replacement ---
|
||||
# Disabled for now - we don't support Unicode yet
|
||||
#set(FILE_UTF8 "${TESTDIR}/utf8.txt")
|
||||
## Note: 💧 is a multi-byte character; CMake will encode as UTF-8
|
||||
#file(WRITE "${FILE_UTF8}" "αβγ foo δει foo ζ")
|
||||
#add_test(NAME UnicodeReplacement
|
||||
# COMMAND strclear -r "${FILE_UTF8}" foo 💧
|
||||
#)
|
||||
#set_tests_properties(UnicodeReplacement PROPERTIES
|
||||
# FIXTURES_SETUP unicode_replace
|
||||
#)
|
||||
#
|
||||
#add_test(NAME VerifyUnicodeReplacement
|
||||
# COMMAND ${CMAKE_COMMAND} -Dfile=${FILE_UTF8} -Dexpected=💧 -P "${CMAKE_CURRENT_LIST_DIR}/verify_contains.cmake"
|
||||
#)
|
||||
#set_tests_properties(VerifyUnicodeReplacement PROPERTIES
|
||||
# FIXTURES_REQUIRED unicode_replace
|
||||
#)
|
||||
|
||||
# --- Test 10: Read-only File (should fail) ---
|
||||
set(FILE_RO "${TESTDIR}/readonly.txt")
|
||||
file(WRITE "${FILE_RO}" "foo bar")
|
||||
execute_process(COMMAND chmod a-w "${FILE_RO}")
|
||||
|
||||
add_test(NAME ReadOnlyFile
|
||||
COMMAND strclear -r "${FILE_RO}" foo bar
|
||||
)
|
||||
set_tests_properties(ReadOnlyFile PROPERTIES
|
||||
WILL_FAIL TRUE
|
||||
# --- Test 10: Binary File Alteration, alternate character ---
|
||||
set(CLEAR_CHAR "X")
|
||||
add_test(NAME BinaryFileClearChar
|
||||
COMMAND ${CMAKE_COMMAND} -DSDIR=${CMAKE_CURRENT_SOURCE_DIR}/testdata -DTDIR=${TESTDIR} -DTFILE=${BINARYFILE}
|
||||
-DTNUM=10 -DTGTSTR=b -DEXPSTR=NULL -DEXPCHAR=${CLEAR_CHAR} -DEXPCNT=2 -DQUIET_RUN=1 -DSTRCLEAR=$<TARGET_FILE:strclear> -DSTRCLEAR_OPTS=-v\;--clear-char=${CLEAR_CHAR}
|
||||
-DCHARCNT=$<TARGET_FILE:charcnt>
|
||||
-P "${CMAKE_CURRENT_SOURCE_DIR}/run_test.cmake"
|
||||
)
|
||||
|
||||
# --- Test 11: Help and Version Output ---
|
||||
# --- Test 11: Mixed Input List via --files option ---
|
||||
set(MIXTXTFILE "mix_input_list.txt")
|
||||
file(WRITE "${TESTDIR}/${MIXTXTFILE}.in" "This is a text input for mixed lists. foo.")
|
||||
set(MIXBINFILE "mix_input_list.bin")
|
||||
file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/${BINARYFILE}" DESTINATION ${TESTDIR})
|
||||
file(RENAME "${TESTDIR}/${BINARYFILE}" "${TESTDIR}/${MIXBINFILE}.in")
|
||||
|
||||
add_test(NAME MixedList
|
||||
COMMAND ${CMAKE_COMMAND} -DSDIR=${CMAKE_CURRENT_SOURCE_DIR}/testdata
|
||||
-DTDIR=${TESTDIR} -DTNUM=11 -DCPYCNT=1 -DQUIET_RUN=1
|
||||
-DTBINFILE=${MIXBINFILE} -DEXPNULLCNT=6 -DCHARCNT=$<TARGET_FILE:charcnt>
|
||||
-DTTXTFILE=${MIXTXTFILE} -DTGTSTR=foo -DEXPSTR=CLEARED -DEXPCNT=1
|
||||
-DSTRCLEAR=$<TARGET_FILE:strclear> -DSTRCLEAR_OPTS=-v
|
||||
-P "${CMAKE_CURRENT_SOURCE_DIR}/run_mixed_list.cmake"
|
||||
)
|
||||
|
||||
# --- Test 12: Parallel Processing (large mixed list, stress test) ---
|
||||
add_test(NAME ParallelProcessing
|
||||
COMMAND ${CMAKE_COMMAND} -DSDIR=${CMAKE_CURRENT_SOURCE_DIR}/testdata
|
||||
-DTDIR=${TESTDIR} -DTNUM=12 -DCPYCNT=50 -DQUIET_RUN=1
|
||||
-DTBINFILE=${MIXBINFILE} -DEXPNULLCNT=6 -DCHARCNT=$<TARGET_FILE:charcnt>
|
||||
-DTTXTFILE=${MIXTXTFILE} -DTGTSTR=foo -DEXPSTR=CLEARED -DEXPCNT=1
|
||||
-DSTRCLEAR=$<TARGET_FILE:strclear> -DSTRCLEAR_OPTS=-v
|
||||
-P "${CMAKE_CURRENT_SOURCE_DIR}/run_mixed_list.cmake"
|
||||
)
|
||||
|
||||
# --- Test 13: Text file only filtering from a mixed list ---
|
||||
add_test(NAME TextOnlyFiltering
|
||||
COMMAND ${CMAKE_COMMAND} -DSDIR=${CMAKE_CURRENT_SOURCE_DIR}/testdata
|
||||
-DTDIR=${TESTDIR} -DTNUM=13 -DCPYCNT=1 -DQUIET_RUN=1
|
||||
-DTBINFILE=${MIXBINFILE} -DEXPNULLCNT=3 -DCHARCNT=$<TARGET_FILE:charcnt>
|
||||
-DTTXTFILE=${MIXTXTFILE} -DTGTSTR=foo -DEXPSTR=CLEARED -DEXPCNT=1
|
||||
-DSTRCLEAR=$<TARGET_FILE:strclear> -DSTRCLEAR_OPTS=-v\;--text-only
|
||||
-P "${CMAKE_CURRENT_SOURCE_DIR}/run_mixed_list.cmake"
|
||||
)
|
||||
|
||||
# --- Test 14: Binary file only filtering from a mixed list ---
|
||||
add_test(NAME BinaryOnlyFiltering
|
||||
COMMAND ${CMAKE_COMMAND} -DSDIR=${CMAKE_CURRENT_SOURCE_DIR}/testdata
|
||||
-DTDIR=${TESTDIR} -DTNUM=14 -DCPYCNT=1 -DQUIET_RUN=1
|
||||
-DTBINFILE=${MIXBINFILE} -DEXPNULLCNT=6 -DCHARCNT=$<TARGET_FILE:charcnt>
|
||||
-DTTXTFILE=${MIXTXTFILE} -DTGTSTR=foo -DEXPSTR=CLEARED -DEXPCNT=0
|
||||
-DSTRCLEAR=$<TARGET_FILE:strclear> -DSTRCLEAR_OPTS=-v\;--binary-only
|
||||
-P "${CMAKE_CURRENT_SOURCE_DIR}/run_mixed_list.cmake"
|
||||
)
|
||||
|
||||
|
||||
# --- Test 15: Binary/text classification contracts ---
|
||||
add_test(NAME ClassifyModes
|
||||
COMMAND ${CMAKE_COMMAND}
|
||||
-DTDIR=${TESTDIR}
|
||||
-DBINARY_SRC=${CMAKE_CURRENT_SOURCE_DIR}/${BINARYFILE}
|
||||
-DSTRCLEAR=$<TARGET_FILE:strclear>
|
||||
-P "${CMAKE_CURRENT_SOURCE_DIR}/run_classify.cmake"
|
||||
)
|
||||
|
||||
# --- Test 16: BRL-CAD compatibility options ---
|
||||
add_test(NAME CompatibilityModes
|
||||
COMMAND ${CMAKE_COMMAND}
|
||||
-DTDIR=${TESTDIR}
|
||||
-DBINARY_SRC=${CMAKE_CURRENT_SOURCE_DIR}/${BINARYFILE}
|
||||
-DCHARCNT=$<TARGET_FILE:charcnt>
|
||||
-DSTRCLEAR=$<TARGET_FILE:strclear>
|
||||
-P "${CMAKE_CURRENT_SOURCE_DIR}/run_compat_modes.cmake"
|
||||
)
|
||||
|
||||
# --- Test 17: Help output ---
|
||||
add_test(NAME StrclearHelp
|
||||
COMMAND strclear --help
|
||||
COMMAND $<TARGET_FILE:strclear> -v --help
|
||||
)
|
||||
set_tests_properties(StrclearHelp PROPERTIES
|
||||
PASS_REGULAR_EXPRESSION "usage|help|clear"
|
||||
PASS_REGULAR_EXPRESSION "usage|help|clear"
|
||||
)
|
||||
|
||||
# --- Test 12: Nonexistent File (should fail) ---
|
||||
set(NONEXISTENT_FILE "${TESTDIR}/does_not_exist.txt")
|
||||
# --- Test 18: Nonexistent file ---
|
||||
add_test(NAME NonexistentFile
|
||||
COMMAND strclear -r "${NONEXISTENT_FILE}" foo bar
|
||||
COMMAND $<TARGET_FILE:strclear> -v "${TESTDIR}/does_not_exist.txt" foo bar
|
||||
)
|
||||
set_tests_properties(NonexistentFile PROPERTIES
|
||||
WILL_FAIL TRUE
|
||||
|
||||
# --- Test 19: Read-only file should fail ---
|
||||
# Make a read-only
|
||||
set(READONLYFILE "${TESTDIR}/readonly.txt")
|
||||
file(WRITE "${READONLYFILE}" "Read_only")
|
||||
file(CHMOD "${READONLYFILE}" PERMISSIONS OWNER_READ GROUP_READ WORLD_READ)
|
||||
add_test(NAME ReadOnlyFile
|
||||
COMMAND $<TARGET_FILE:strclear> -v "${READONLYFILE}" Read_only Written
|
||||
)
|
||||
set_tests_properties(ReadOnlyFile PROPERTIES WILL_FAIL TRUE)
|
||||
|
||||
|
||||
# Local Variables:
|
||||
# tab-width: 8
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,66 @@
|
||||
/* C H A R C N T . C P P
|
||||
* BRL-CAD
|
||||
*
|
||||
* Published in 2025 by the United States Government.
|
||||
* This work is in the public domain.
|
||||
*/
|
||||
|
||||
#include <climits>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <cstring>
|
||||
|
||||
int
|
||||
main(int argc, const char **argv)
|
||||
{
|
||||
if (argc != 2 && argc != 3) {
|
||||
std::cerr << "Usage: charcnt <filename> [char]\n";
|
||||
return -1;
|
||||
}
|
||||
|
||||
char c = '\0';
|
||||
if (argc == 3) {
|
||||
if (strlen(argv[2]) != 1) {
|
||||
std::cerr << "Error - second argument (if present) must be a single char\n";
|
||||
return -1;
|
||||
} else {
|
||||
c = argv[2][0];
|
||||
}
|
||||
}
|
||||
|
||||
std::ifstream fs(argv[1], std::ios::binary);
|
||||
if (!fs.is_open()) {
|
||||
std::cerr << "Error: Could not open file " << argv[1] << "\n";
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Iterate, and count all the nulls
|
||||
unsigned long long nc = 0;
|
||||
char byte;
|
||||
while (fs.get(byte))
|
||||
nc = (byte == c) ? nc + 1 : nc;
|
||||
fs.close();
|
||||
|
||||
std::string rstr;
|
||||
if (c == '\0') {
|
||||
rstr = std::string("null");
|
||||
} else {
|
||||
rstr = c;
|
||||
}
|
||||
|
||||
std::cout << "Found " << nc << " " << rstr << " characters\n";
|
||||
if (nc > INT_MAX)
|
||||
std::cout << "Error - more than " << INT_MAX << " chars found!\n";
|
||||
|
||||
return (int)nc;
|
||||
}
|
||||
|
||||
// Local Variables:
|
||||
// tab-width: 8
|
||||
// mode: C++
|
||||
// c-basic-offset: 4
|
||||
// indent-tabs-mode: t
|
||||
// c-file-style: "stroustrup"
|
||||
// End:
|
||||
// ex: shiftwidth=4 tabstop=8 cino=N-s
|
||||
@@ -0,0 +1,40 @@
|
||||
set(TEXT_FILE "${TDIR}/classify_text.txt")
|
||||
set(BINARY_FILE "${TDIR}/classify_binary.bin")
|
||||
set(FILE_LIST "${TDIR}/classify_files.txt")
|
||||
|
||||
file(WRITE "${TEXT_FILE}" "plain text\n")
|
||||
execute_process(COMMAND ${CMAKE_COMMAND} -E copy_if_different "${BINARY_SRC}" "${BINARY_FILE}")
|
||||
file(WRITE "${FILE_LIST}" "${TEXT_FILE}\n${BINARY_FILE}\n")
|
||||
|
||||
execute_process(
|
||||
COMMAND "${STRCLEAR}" -B "${TEXT_FILE}"
|
||||
RESULT_VARIABLE text_result
|
||||
)
|
||||
if(NOT text_result EQUAL 1)
|
||||
message(FATAL_ERROR "-B should return 1 for text files, got ${text_result}")
|
||||
endif()
|
||||
|
||||
execute_process(
|
||||
COMMAND "${STRCLEAR}" -B "${BINARY_FILE}"
|
||||
RESULT_VARIABLE binary_result
|
||||
)
|
||||
if(NOT binary_result EQUAL 0)
|
||||
message(FATAL_ERROR "-B should return 0 for binary files, got ${binary_result}")
|
||||
endif()
|
||||
|
||||
execute_process(
|
||||
COMMAND "${STRCLEAR}" --classify --files "${FILE_LIST}"
|
||||
RESULT_VARIABLE classify_result
|
||||
OUTPUT_VARIABLE classify_output
|
||||
)
|
||||
if(NOT classify_result EQUAL 0)
|
||||
message(FATAL_ERROR "--classify failed: ${classify_result}")
|
||||
endif()
|
||||
|
||||
string(FIND "${classify_output}" "\"type\":\"TEXT\"" text_type)
|
||||
string(FIND "${classify_output}" "\"path\":\"${TEXT_FILE}\"" text_path)
|
||||
string(FIND "${classify_output}" "\"type\":\"BINARY\"" binary_type)
|
||||
string(FIND "${classify_output}" "\"path\":\"${BINARY_FILE}\"" binary_path)
|
||||
if(text_type EQUAL -1 OR text_path EQUAL -1 OR binary_type EQUAL -1 OR binary_path EQUAL -1)
|
||||
message(FATAL_ERROR "Unexpected --classify output:\n${classify_output}")
|
||||
endif()
|
||||
@@ -0,0 +1,33 @@
|
||||
set(TEXT_FILE "${TDIR}/compat_replace.txt")
|
||||
set(BINARY_FILE "${TDIR}/compat_clear.bin")
|
||||
|
||||
file(WRITE "${TEXT_FILE}" "foo one foo two\n")
|
||||
execute_process(
|
||||
COMMAND "${STRCLEAR}" -r "${TEXT_FILE}" "foo" "bar"
|
||||
RESULT_VARIABLE replace_result
|
||||
)
|
||||
if(NOT replace_result EQUAL 0)
|
||||
message(FATAL_ERROR "Compatibility -r mode failed: ${replace_result}")
|
||||
endif()
|
||||
|
||||
file(READ "${TEXT_FILE}" text_content)
|
||||
if(NOT "${text_content}" STREQUAL "bar one bar two\n")
|
||||
message(FATAL_ERROR "Compatibility -r mode produced unexpected text:\n${text_content}")
|
||||
endif()
|
||||
|
||||
execute_process(COMMAND ${CMAKE_COMMAND} -E copy_if_different "${BINARY_SRC}" "${BINARY_FILE}")
|
||||
execute_process(
|
||||
COMMAND "${STRCLEAR}" -b -c "${BINARY_FILE}" "foo" "/path/not/present"
|
||||
RESULT_VARIABLE clear_result
|
||||
)
|
||||
if(NOT clear_result EQUAL 0)
|
||||
message(FATAL_ERROR "Compatibility -b -c mode failed: ${clear_result}")
|
||||
endif()
|
||||
|
||||
execute_process(
|
||||
COMMAND "${CHARCNT}" "${BINARY_FILE}"
|
||||
RESULT_VARIABLE null_count
|
||||
)
|
||||
if(NOT null_count EQUAL 6)
|
||||
message(FATAL_ERROR "Compatibility -b -c mode expected 6 NUL chars, found ${null_count}")
|
||||
endif()
|
||||
@@ -0,0 +1,69 @@
|
||||
# Reset input files
|
||||
set(TF "${TDIR}/${TTXTFILE}${TNUM}")
|
||||
set(BF "${TDIR}/${TBINFILE}${TNUM}")
|
||||
foreach (i RANGE ${CPYCNT})
|
||||
execute_process(COMMAND ${CMAKE_COMMAND} -E copy_if_different "${TDIR}/${TTXTFILE}.in" "${TF}_${i}")
|
||||
execute_process(COMMAND ${CMAKE_COMMAND} -E copy_if_different "${TDIR}/${TBINFILE}.in" "${BF}_${i}")
|
||||
endforeach()
|
||||
|
||||
# report original file contents
|
||||
if (NOT QUIET_RUN)
|
||||
file(READ "${TF}" orig_file_content)
|
||||
message("Original text file contents:\n\n${orig_file_content}\n")
|
||||
execute_process(
|
||||
COMMAND "${CHARCNT}" "${BF}"
|
||||
RESULT_VARIABLE count
|
||||
)
|
||||
message("Original binary null char count: ${count}")
|
||||
endif()
|
||||
|
||||
# assemble file list to pass to strclear
|
||||
set(flist)
|
||||
file(REMOVE "${TDIR}/${TNUM}.txt")
|
||||
foreach (i RANGE ${CPYCNT})
|
||||
file(APPEND ${TDIR}/${TNUM}.txt "${TF}_${i}\n")
|
||||
file(APPEND ${TDIR}/${TNUM}.txt "${BF}_${i}\n")
|
||||
endforeach()
|
||||
|
||||
# Run strclear
|
||||
message("WORKING_DIRECTORY ${TDIR}")
|
||||
message("${STRCLEAR} ${STRCLEAR_OPTS} --files ${TDIR}/${TNUM}.txt ${TGTSTR} ${EXPSTR}")
|
||||
execute_process(
|
||||
COMMAND "${STRCLEAR}" ${STRCLEAR_OPTS} --files ${TDIR}/${TNUM}.txt "${TGTSTR}" ${EXPSTR}
|
||||
WORKING_DIRECTORY ${TDIR}
|
||||
)
|
||||
|
||||
# Check file contents
|
||||
file(STRINGS ${TDIR}/${TNUM}.txt ilist)
|
||||
foreach (f ${ilist})
|
||||
if (NOT EXISTS ${f})
|
||||
message(FATAL_ERROR "file ${f} not found")
|
||||
endif()
|
||||
if ("${f}" MATCHES ".*bin.*")
|
||||
execute_process(
|
||||
COMMAND ${CHARCNT} ${f}
|
||||
RESULT_VARIABLE bcount
|
||||
OUTPUT_VARIABLE bout
|
||||
)
|
||||
if(NOT ${bcount} EQUAL ${EXPNULLCNT})
|
||||
message(FATAL_ERROR "File ${f} does not contain ${EXPNULLCNT} null chars - found ${bcount}")
|
||||
endif()
|
||||
else()
|
||||
file(READ ${f} file_content)
|
||||
# Match all occurrences of CLEARED and store them in a list
|
||||
string(REPLACE "\n" "" file_content "${file_content}")
|
||||
string(REGEX MATCHALL "${EXPSTR}" MATCHES "${file_content}")
|
||||
list(LENGTH MATCHES tcount)
|
||||
|
||||
if(NOT ${tcount} EQUAL ${EXPCNT})
|
||||
message(FATAL_ERROR "File ${f} does not contain expected text: ${EXPCNT}x'${EXPSTR}'\nActual content:\n${file_content}")
|
||||
endif()
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
# Local Variables:
|
||||
# tab-width: 8
|
||||
# mode: cmake
|
||||
# indent-tabs-mode: t
|
||||
# End:
|
||||
# ex: shiftwidth=2 tabstop=8
|
||||
@@ -0,0 +1,64 @@
|
||||
# Reset input file
|
||||
set(TF "${TDIR}/${TFILE}${TNUM}")
|
||||
execute_process(COMMAND ${CMAKE_COMMAND} -E copy_if_different "${TDIR}/${TFILE}.in" "${TF}")
|
||||
|
||||
# report original file contents
|
||||
if (NOT QUIET_RUN)
|
||||
file(READ "${TF}" orig_file_content)
|
||||
message("Original file contents:\n\n${orig_file_content}\n")
|
||||
endif()
|
||||
|
||||
# Run strclear
|
||||
message("WORKING_DIRECTORY ${TDIR}")
|
||||
message("${STRCLEAR} ${STRCLEAR_OPTS} ${TF} ${TGTSTR} ${EXPSTR}")
|
||||
execute_process(
|
||||
COMMAND "${STRCLEAR}" ${STRCLEAR_OPTS} "${TF}" "${TGTSTR}" ${EXPSTR}
|
||||
WORKING_DIRECTORY ${TDIR}
|
||||
)
|
||||
|
||||
# Check output file contents
|
||||
file(READ "${TF}" file_content)
|
||||
|
||||
# If we were assigned a charcnt executable, look for NULL characters.
|
||||
# Otherwise, count EXPSTR instances
|
||||
if (CHARCNT)
|
||||
message("${CHARCNT} ${TF} ${EXPCHAR}")
|
||||
execute_process(
|
||||
COMMAND "${CHARCNT}" "${TF}" ${EXPCHAR}
|
||||
RESULT_VARIABLE count
|
||||
)
|
||||
if (count LESS 0)
|
||||
message(FATAL_ERROR "charcnt unable to run successfully\n")
|
||||
endif()
|
||||
else()
|
||||
# Match all occurrences of CLEARED and store them in a list
|
||||
string(REPLACE "\n" "" file_content "${file_content}")
|
||||
string(REGEX MATCHALL "${EXPSTR}" MATCHES "${file_content}")
|
||||
list(LENGTH MATCHES count)
|
||||
endif()
|
||||
|
||||
file(READ "${TF}" file_content)
|
||||
if(NOT ${count} EQUAL ${EXPCNT})
|
||||
if (CHARCNT)
|
||||
set(nstr)
|
||||
if (NOT EXPCHAR)
|
||||
set(nstr "null")
|
||||
else()
|
||||
set(nstr "${EXPCHAR}")
|
||||
endif()
|
||||
message(FATAL_ERROR "File ${TF} does not contain ${EXPCNT} null chars - found ${count}")
|
||||
else()
|
||||
message(FATAL_ERROR "File ${TF} does not contain expected text: ${EXPCNT}x'${EXPSTR}'\nActual content:\n${file_content}")
|
||||
endif()
|
||||
else()
|
||||
if (NOT QUIET_RUN)
|
||||
message("Final file contents:\n\n${file_content}\n")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Local Variables:
|
||||
# tab-width: 8
|
||||
# mode: cmake
|
||||
# indent-tabs-mode: t
|
||||
# End:
|
||||
# ex: shiftwidth=2 tabstop=8
|
||||
@@ -1,12 +0,0 @@
|
||||
if(NOT DEFINED file)
|
||||
message(FATAL_ERROR "Must define -Dfile argument")
|
||||
endif()
|
||||
if(NOT DEFINED expected)
|
||||
message(FATAL_ERROR "Must define -Dexpected argument")
|
||||
endif()
|
||||
|
||||
file(READ "${file}" file_content)
|
||||
string(FIND "${file_content}" "${expected}" found)
|
||||
if(found EQUAL -1)
|
||||
message(FATAL_ERROR "File ${file} does not contain expected text: '${expected}'\nActual content:\n${file_content}")
|
||||
endif()
|
||||
Reference in New Issue
Block a user