A snapshot of the development tree. The substantial changes since the last one:
COMMAND OUTPUT POLICY. The three commands display text in exactly three cases,
and each owns a stream: LOGGING under "-v" greater than 0 and an ERROR before
termination go to STDERR; OUTPUT THE USER ASKED FOR goes to STDOUT. For ktext
that output is a document, so "ktext doc.kt -d | ..." is now safe -- logging
used to share the stream and land inside the document. A bare command prints
its usage and succeeds rather than failing. Colour is emitted only to a
terminal, per stream, and NO_COLOR is honoured.
"-v 1" reports every decision whose outcome you could not have read off your own
input: the klammerset that was loaded and from which file, a font's directory, a
":files" name's file, how "-o" was expanded. Higher levels are the trace.
The commands no longer warn and continue: an anomaly is an error, described with
its location. Two exceptions remain, each for a stated reason -- a condition
that is expected and temporary by design, and a judgment that is a heuristic
rather than exact.
@cond IS NOW A TRUE SPECIAL FORM, resolved at APPLICATION time rather than when
the file is read. Two consequences for a writer:
* a state variable reaches the predicate. "@@@state Flag :value true @@@
@cond *Flag* | T | F @" renders "T"; it used to see the literal "*Flag*" and
silently take the false branch. The document now behaves like a klammer
body, whose arguments are bound before its conditionals are decided.
* nothing in a discarded branch happens -- it is not read, not evaluated, not
expanded. An @eval in the branch not taken used to run anyway.
Its predicate relation is total and strict: true, True, 1; false, False, 0, and
empty; anything else is an error at the @cond rather than silently false.
@eval REACHING OUTSIDE. ":shell" and ":haskell" now keep the command's standard
error out of the document (it appears under "-v 1") and treat a nonzero exit as
an error naming what the command reported. A command that exits nonzero on
purpose -- "grep" finding no match -- says so with "|| true".
KLAMMER SETS. Several combine: "--klammersets a b c" loads all three in the
order given, sharing one namespace, with the definition modes deciding
collisions. "none" means none and may not be combined with other symbols. A
klammerset with symbol X is declared in a file X/X.k, which is what lets two
sets require the same third set without loading it twice.
TESTS. Four new suites: the kdiag command's interface, the @eval primitive's
contract with the outside world, and verbosity at both tiers. Three suites
that could not run on macOS at all now do.
Assembled from dev commit 6c8ee6c22fca.
714 lines
24 KiB
C++
714 lines
24 KiB
C++
#include <fstream>
|
|
#include <algorithm>
|
|
#include <cstring>
|
|
|
|
#include "file.h"
|
|
#include "error.h"
|
|
#include "log.h"
|
|
#include "util.h"
|
|
#include "show.h"
|
|
|
|
std::string file_basename(const std::string& filename)
|
|
{
|
|
fs::path p(filename);
|
|
return p.stem().string();
|
|
}
|
|
|
|
std::string extension(const std::string& filename)
|
|
{
|
|
auto pos = filename.find_last_of(".");
|
|
if (pos != std::string::npos)
|
|
return filename.substr(pos + 1);
|
|
return "";
|
|
}
|
|
|
|
std::string file_directory(const std::string& filename)
|
|
{
|
|
fs::path p(filename);
|
|
return p.parent_path().string();
|
|
}
|
|
|
|
std::string absolute_pathname(const std::string& filename, const std::string& base)
|
|
{
|
|
fs::path p(filename);
|
|
if (filename.empty()) {
|
|
return fs::current_path().string();
|
|
}
|
|
if (!base.empty()) {
|
|
// Resolve filename relative to base's directory
|
|
fs::path b(base);
|
|
fs::path base_dir = fs::is_directory(b) ? b : b.parent_path();
|
|
p = base_dir / p;
|
|
}
|
|
return fs::absolute(p).string();
|
|
}
|
|
|
|
std::string relative_pathname(const std::string& filename)
|
|
{
|
|
/*
|
|
fs::path relative_to_current(const fs::path& input,
|
|
const fs::path& currentFile) {
|
|
const auto base = fs::absolute(currentFile).parent_path();
|
|
return fs::relative(fs::absolute(input), base);
|
|
*/
|
|
|
|
fs::path p(absolute_pathname(filename)); // + "/" + filename);
|
|
auto relpath = fs::relative(p, fs::current_path());
|
|
std::cout << "Absolute: " << fs::current_path() << " " << p << "->" << relpath << "\n";
|
|
|
|
return relpath.string();
|
|
}
|
|
|
|
size_t count_substrings(const std::string& text, const std::string& substring) {
|
|
size_t count = 0;
|
|
size_t pos = 0;
|
|
while ((pos = text.find(substring, pos)) != std::string::npos) {
|
|
count++;
|
|
pos += substring.length();
|
|
}
|
|
return count;
|
|
}
|
|
|
|
fs::path relative_to_cwd(const fs::path& input)
|
|
{
|
|
const auto base = fs::current_path();
|
|
std::error_code ec;
|
|
auto abs_input = fs::weakly_canonical(input, ec);
|
|
if (ec) abs_input = fs::absolute(input);
|
|
auto rel = fs::relative(abs_input, base, ec);
|
|
if (ec) rel = abs_input.lexically_relative(base);
|
|
auto result = rel.empty() ? abs_input : rel;
|
|
if (count_substrings(result.string(), "../") > 3) {
|
|
result = input;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
|
|
std::string relative_pathname(const std::string& filename, std::string base)
|
|
{
|
|
fs::path p(absolute_pathname(filename));
|
|
auto relpath = relative(p, base);
|
|
relpath = relpath.lexically_normal();
|
|
return relpath.string();
|
|
}
|
|
|
|
bool file_exists(const std::string& pathname, bool error_if_not, bool is_directory)
|
|
{
|
|
fs::path p(pathname);
|
|
bool exists = fs::exists(p);
|
|
bool regular = fs::is_regular_file(p);
|
|
bool directory = fs::is_directory(p);
|
|
bool valid = exists and (regular or directory);
|
|
|
|
std::string filetype = is_directory ? "Directory " : "File";
|
|
Locator no_source("", -1, -1);
|
|
if (error_if_not and not valid) {
|
|
if (not exists)
|
|
throw File_error(filetype + " '" + pathname + "' does not exist", no_source);
|
|
else if (not is_directory and not regular)
|
|
throw File_error(filetype + " '" + pathname + "' is not a regular text file", no_source);
|
|
else if (is_directory and regular)
|
|
throw File_error(filetype + " '" + pathname + "' is not a directory", no_source);
|
|
}
|
|
return valid;
|
|
}
|
|
|
|
|
|
// Filename lists (see file.h): standalone-"/" separation, existence-rescue
|
|
// grouping, and tilde expansion.
|
|
|
|
std::string expand_tilde(const std::string& path)
|
|
{
|
|
if (path == "~" || (path.size() > 1 && path[0] == '~' && path[1] == '/')) {
|
|
std::string home = get_env_var("HOME");
|
|
if (!home.empty()) {
|
|
return home + path.substr(1);
|
|
}
|
|
}
|
|
return path;
|
|
}
|
|
|
|
static bool filename_exists(const std::string& name, const std::string& base_dir)
|
|
{
|
|
std::string resolved = expand_tilde(name);
|
|
if (!base_dir.empty() && !fs::path(resolved).is_absolute()) {
|
|
resolved = base_dir + "/" + resolved;
|
|
}
|
|
return fs::is_regular_file(resolved);
|
|
}
|
|
|
|
strings_t group_filename_tokens(const strings_t& tokens, const std::string& base_dir)
|
|
{
|
|
strings_t result {};
|
|
if (std::find(tokens.begin(), tokens.end(), "/") != tokens.end()) {
|
|
// Deterministic form: standalone "/" separates the filenames; the
|
|
// tokens between separators form one name. No existence checks.
|
|
strings_t group {};
|
|
for (const std::string& t : tokens) {
|
|
if (t == "/") {
|
|
if (!group.empty()) result.push_back(join(group));
|
|
group.clear();
|
|
} else {
|
|
group.push_back(t);
|
|
}
|
|
}
|
|
if (!group.empty()) result.push_back(join(group));
|
|
} else {
|
|
// Rescue: a token naming an existing file stands alone; one that
|
|
// does not is joined with following tokens until the accumulated
|
|
// name exists. A name that never resolves is kept as given, so the
|
|
// missing-file error downstream reports what the user wrote.
|
|
size_t i = 0;
|
|
bool reported = false; // at most one "tried" report per list
|
|
while (i < tokens.size()) {
|
|
if (filename_exists(tokens[i], base_dir)) {
|
|
result.push_back(tokens[i]);
|
|
++i;
|
|
continue;
|
|
}
|
|
std::string acc = tokens[i];
|
|
size_t j = i + 1;
|
|
bool found = false;
|
|
while (j < tokens.size()) {
|
|
acc += " " + tokens[j];
|
|
++j;
|
|
if (filename_exists(acc, base_dir)) {
|
|
// A successful regrouping is a DERIVED value -- the
|
|
// command completed a name the input left ambiguous -- so
|
|
// it is reported at "-v 1" and not at every run. Silence
|
|
// is safe here because a rescue only succeeds when the
|
|
// joined name names an existing file: a mistyped name
|
|
// fails rather than resolving to something else.
|
|
K::log(1, "interpreting", q_(acc), "as one filename");
|
|
result.push_back(acc);
|
|
i = j;
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!found) {
|
|
// Nothing resolved. The groupings that were TRIED are what
|
|
// the user needs at verbosity 0, because the downstream error
|
|
// names only the first token and cannot say why the others
|
|
// were not joined to it. Recorded on the name so the caller
|
|
// can report them with the missing-file error.
|
|
strings_t tried {};
|
|
std::string acc2 = tokens[i];
|
|
tried.push_back(q_(acc2));
|
|
for (size_t k = i + 1; k < tokens.size(); ++k) {
|
|
acc2 += " " + tokens[k];
|
|
tried.push_back(q_(acc2));
|
|
}
|
|
// Once: the first unresolved token's list already shows
|
|
// every joining from that point on, and one report per
|
|
// leftover word buries it.
|
|
if (tried.size() > 1 && !reported) {
|
|
std::cerr << command_name << ": no file matches "
|
|
<< q_(tokens[i]) << "; tried "
|
|
<< join(tried, ", ") << "\n";
|
|
reported = true;
|
|
}
|
|
result.push_back(tokens[i]);
|
|
++i;
|
|
}
|
|
}
|
|
}
|
|
for (std::string& name : result) {
|
|
name = expand_tilde(name);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
strings_t resolve_filename_list(const std::string& text, const std::string& base_dir)
|
|
{
|
|
// Split on standalone "/" at the string level first, so a name's inner
|
|
// spacing survives exactly; without a separator, fall back to
|
|
// whitespace tokens and the rescue in group_filename_tokens().
|
|
auto standalone_slash = [&](size_t i) {
|
|
return text[i] == '/'
|
|
&& (i == 0 || std::isspace(static_cast<unsigned char>(text[i-1])))
|
|
&& (i + 1 == text.size() || std::isspace(static_cast<unsigned char>(text[i+1])));
|
|
};
|
|
bool has_separator = false;
|
|
for (size_t i = 0; i < text.size(); ++i) {
|
|
if (standalone_slash(i)) {
|
|
has_separator = true;
|
|
break;
|
|
}
|
|
}
|
|
if (has_separator) {
|
|
strings_t result {};
|
|
std::string part {};
|
|
for (size_t i = 0; i < text.size(); ++i) {
|
|
if (standalone_slash(i)) {
|
|
part = trim(part);
|
|
if (!part.empty()) result.push_back(expand_tilde(part));
|
|
part.clear();
|
|
} else {
|
|
part += text[i];
|
|
}
|
|
}
|
|
part = trim(part);
|
|
if (!part.empty()) result.push_back(expand_tilde(part));
|
|
return result;
|
|
}
|
|
return group_filename_tokens(word_split(text), base_dir);
|
|
}
|
|
|
|
std::string string_from_file(const std::string& pathname, bool strip_surrounding_whitespace)
|
|
{
|
|
std::regex klammertext_filename_re { R"(.*\.kt?)" };
|
|
|
|
fs::path p(pathname);
|
|
std::string result {};
|
|
if (fs::exists(p)) {
|
|
if (fs::is_regular_file(p)) {
|
|
std::ifstream stream { pathname };
|
|
if (!stream.is_open()) {
|
|
throw File_error("Error opening file \"" + pathname + "\"", Locator("", -1, -1));
|
|
} else {
|
|
std::ostringstream buffer {};
|
|
stream >> std::noskipws >> buffer.rdbuf();
|
|
if (stream.fail() && !stream.eof()) {
|
|
throw File_error("Error reading file \"" + pathname + "\"", Locator("", -1, -1));
|
|
} else {
|
|
result = buffer.str();
|
|
|
|
result = string_replace(result, "\r\n", ""); // Urgh.
|
|
|
|
|
|
if (std::regex_match(pathname, klammertext_filename_re)) {
|
|
//std::cout << "Read Klammertext source file: " << pathname << "\n";
|
|
// result = encode(result);
|
|
} else {
|
|
//std::cout << "Read file: " << pathname << "\n";
|
|
}
|
|
|
|
if (strip_surrounding_whitespace)
|
|
result = trim(result);
|
|
return result;
|
|
}
|
|
}
|
|
} else {
|
|
throw File_error("File \"" + pathname + "\" is not a regular text file", Locator("", -1, -1));
|
|
}
|
|
} else {
|
|
throw File_error("File '" + pathname + "' does not exist", Locator("", -1, -1));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
|
|
void string_to_file(const std::string& pathname, std::string contents)
|
|
{
|
|
fs::create_directories(file_directory(fs::absolute(pathname)));
|
|
std::ofstream out(pathname);
|
|
if (!out) {
|
|
throw File_error("Could not write file " + pathname);
|
|
}
|
|
out << contents;
|
|
out.close();
|
|
}
|
|
|
|
|
|
strings_t get_subdirectories(const std::string& s, std::regex name_match_re)
|
|
{
|
|
strings_t result {};
|
|
for (auto& p : fs::recursive_directory_iterator(s)) {
|
|
std::smatch match {};
|
|
//std::cout << "Check dir: " << p.path().string() << "\n";
|
|
std::string base = file_basename(p.path().string());
|
|
if (fs::is_directory(p) and std::regex_match(base, match, name_match_re))
|
|
result.push_back(p.path().string());
|
|
}
|
|
return result;
|
|
}
|
|
|
|
strings_t get_files_in_directory(const std::string& dir)
|
|
{
|
|
strings_t result {};
|
|
try {
|
|
for (const auto& entry : fs::directory_iterator(dir)) {
|
|
if (entry.is_regular_file()) {
|
|
// std::cout << entry.path().filename() << std::endl;
|
|
result.push_back(entry.path().filename());
|
|
}
|
|
}
|
|
} catch (const fs::filesystem_error& ex) {
|
|
std::cerr << "Error: " << ex.what() << std::endl;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
|
|
std::string find_file(const std::string& basename, strings_t search_path, bool error_if_not_found)
|
|
{
|
|
bool found = false;
|
|
std::string pathname { "" };
|
|
for (const std::string& s : search_path) {
|
|
pathname = s + "/" + basename;
|
|
if (file_exists(pathname)) {
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (error_if_not_found and not found) {
|
|
std::stringstream ss {};
|
|
std::sort(search_path.begin(), search_path.end());
|
|
ss << "File with basename \"" << basename << "\" not found in search path:\n "
|
|
<< join(search_path, "\n ");
|
|
throw File_error(ss.str(), Locator(), false);
|
|
}
|
|
return pathname;
|
|
}
|
|
|
|
|
|
std::vector<fs::path>
|
|
find_file_recursive(const fs::path& root, const std::string& filename, bool only_one) //, Locator loc)
|
|
{
|
|
std::vector<fs::path> result {};
|
|
if (!fs::exists(root) || !fs::is_directory(root)) {
|
|
return result;
|
|
}
|
|
for (const auto& entry : fs::recursive_directory_iterator(root)) {
|
|
// msg() << "Entry: " << entry.path().string() << "\n";
|
|
if (entry.is_regular_file() && entry.path().filename() == filename) {
|
|
result.push_back(entry.path());
|
|
}
|
|
}
|
|
if (only_one && result.size() > 1) {
|
|
std::stringstream ss {};
|
|
ss << "More than one file named " + q_(filename) + " found:\n";
|
|
for (auto f : result) {
|
|
ss << " " << f << "\n";
|
|
}
|
|
std::cerr << ss.str();
|
|
throw File_error("More than one file named " + q_(filename) + " found");
|
|
}
|
|
return result;
|
|
}
|
|
|
|
|
|
std::vector<fs::path>
|
|
find_file_from_roots(const std::vector<std::string>& roots, const std::string& filename, bool only_one)
|
|
{
|
|
(void)K::log(3, filename);
|
|
std::vector<fs::path> result {};
|
|
for (std::string root : roots) {
|
|
std::vector<fs::path> filenames = find_file_recursive(root, filename, only_one);
|
|
for (auto f : filenames) {
|
|
if (std::ranges::count(result, f) == 0) {
|
|
result.push_back(f);
|
|
}
|
|
}
|
|
// result.insert(result.end(), filenames.begin(), filenames.end());
|
|
}
|
|
if (result.empty()) {
|
|
throw File_error("File \"" + filename + "\" not found");
|
|
}
|
|
if (only_one && result.size() > 1) {
|
|
std::stringstream ss {};
|
|
ss << "More than one file named " + q_(filename) + " found:\n";
|
|
for (auto f : result) {
|
|
ss << " " << f << "\n";
|
|
}
|
|
std::cerr << ss.str();
|
|
throw File_error("More than one file named " + q_(filename) + " found");
|
|
}
|
|
for (auto f : result) {
|
|
if (!fs::exists(f)) {
|
|
throw File_error("File \"" + filename + "\" does not exist");
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
fs::path klammertext_filename(const std::string& basename, bool error_if_missing, bool make_directory_if_missing)
|
|
{
|
|
std::string home { get_env_var("KLAMMERTEXT_HOME") };
|
|
std::string result = home + "/" + basename;
|
|
if (make_directory_if_missing)
|
|
fs::create_directory(file_directory(result));
|
|
if (error_if_missing and !file_exists(result, false, true)) {
|
|
throw File_error(
|
|
"Klammertext file does not exist: " + result);
|
|
}
|
|
return fs::path(result);
|
|
}
|
|
|
|
strings_t sks_dirs()
|
|
{
|
|
std::string home { get_env_var("KLAMMERTEXT_HOME") };
|
|
strings_t result = get_subdirectories(home + "/sks", std::regex(R"([a-zA-Z]\w*)"));
|
|
result.emplace(result.begin(), home + "/sks/kutil");
|
|
// result.push_back(home + "/doc/handbook"); // Not included in container yet
|
|
return result;
|
|
}
|
|
|
|
strings_t get_sks_directories(const std::string& s, bool include_argument)
|
|
{
|
|
strings_t result;
|
|
if (include_argument)
|
|
result.push_back(s);
|
|
for (auto& p : fs::recursive_directory_iterator(s)) {
|
|
auto basename = file_basename(p.path().string());
|
|
auto parent = p.path().parent_path();
|
|
if (fs::is_directory(p)
|
|
and basename[0] != '_'
|
|
and basename != "css"
|
|
and basename != "sty"
|
|
and basename != "js"
|
|
//and basename != "font"
|
|
and parent != "font"
|
|
and parent != "fonts")
|
|
result.push_back(p.path().string());
|
|
}
|
|
return result;
|
|
}
|
|
|
|
std::string cache_directory(const std::string& subdirectory, std::string parent_directory)
|
|
{
|
|
if (parent_directory.empty()) {
|
|
// /dev/shm is a fast RAM-backed tmpfs on Linux; it does not exist on
|
|
// macOS, so fall back to the platform temp directory there.
|
|
if (fs::is_directory("/dev/shm")) {
|
|
parent_directory = "/dev/shm";
|
|
} else {
|
|
parent_directory = fs::temp_directory_path().string();
|
|
}
|
|
}
|
|
std::string result = parent_directory + "/_klammertext_cache/" + subdirectory;
|
|
// msg() << "Cache directory: " << result << "\n";
|
|
return result;
|
|
}
|
|
|
|
|
|
std::time_t to_time_t(const fs::file_time_type& ftime)
|
|
{
|
|
auto sctp = std::chrono::time_point_cast<std::chrono::system_clock::duration>(
|
|
ftime - fs::file_time_type::clock::now() + std::chrono::system_clock::now());
|
|
return std::chrono::system_clock::to_time_t(sctp);
|
|
}
|
|
|
|
bool in_modification_order(std::string filename1, std::string filename2)
|
|
{
|
|
if ((!file_exists(filename1)) || (!file_exists(filename2))) {
|
|
return false;
|
|
} else {
|
|
auto time1 = fs::last_write_time(fs::path(filename1));
|
|
auto time2 = fs::last_write_time(fs::path(filename2));
|
|
return time1 < time2;
|
|
}
|
|
}
|
|
|
|
void write_to_cache(const std::string& cache_dir, const std::string& basename, const std::string& text)
|
|
{
|
|
if (!file_exists(cache_dir)) {
|
|
//std::cout << "Creating cache directory: " << cache_dir << "\n";
|
|
fs::create_directories(cache_dir);
|
|
}
|
|
// msg() << "Writing file to cache: " << basename << "\n";
|
|
std::string output_filename = cache_dir + "/" + basename;
|
|
string_to_file(output_filename, text);
|
|
}
|
|
|
|
std::string read_from_cache(const std::string& cache_dir, const std::string& basename)
|
|
{
|
|
std::string input_filename = cache_dir + "/" + basename;
|
|
// msg() << "Reading file from cache: " << input_filename << "\n";
|
|
return string_from_file(input_filename);
|
|
}
|
|
|
|
bool cache_requires_update(const std::string& cache_dir, const std::string& file_to_cache, const std::string& basename)
|
|
{
|
|
std::string cache_filename = cache_dir + "/" + basename;
|
|
return !in_modification_order(file_to_cache, cache_filename);
|
|
}
|
|
|
|
|
|
std::vector<fs::path> pathnames_with_extension(
|
|
const fs::path& dir, const std::string extension)
|
|
{
|
|
std::vector<fs::path> files;
|
|
for (const auto& entry : fs::recursive_directory_iterator(dir)) {
|
|
if (!entry.is_regular_file()) continue;
|
|
auto ext = entry.path().extension().string();
|
|
std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
|
|
if (ext == "." + extension) files.push_back(entry.path());
|
|
}
|
|
return files;
|
|
}
|
|
|
|
|
|
/*
|
|
std::string find_file(const fs::path& root, const std::string& name)
|
|
{
|
|
//std::vector<fs::path> matches;
|
|
strings_t matches {};
|
|
|
|
auto normalize = [&](std::string s) {
|
|
if (!case_insensitive) return s;
|
|
std::transform(s.begin(), s.end(), s.begin(),
|
|
[](unsigned char c) { return std::tolower(c); });
|
|
return s;
|
|
};
|
|
|
|
const std::string norm_ext = normalize("." + ext);
|
|
|
|
for (const auto& entry : fs::recursive_directory_iterator(root)) {
|
|
if (!entry.is_regular_file())
|
|
continue;
|
|
//std::string entry_ext = normalize(entry.path().extension().string());
|
|
if (entry == name) {
|
|
matches.push_back(entry.path().string());
|
|
}
|
|
}
|
|
return matches[0];
|
|
}
|
|
*/
|
|
|
|
|
|
// std::vector<fs::path> find_files_with_extension(
|
|
strings_t find_files_with_extension(
|
|
const fs::path& root, const std::string& ext, bool case_insensitive)
|
|
{
|
|
//std::vector<fs::path> matches;
|
|
strings_t matches {};
|
|
|
|
auto normalize = [&](std::string s) {
|
|
if (!case_insensitive) return s;
|
|
std::transform(s.begin(), s.end(), s.begin(),
|
|
[](unsigned char c) { return std::tolower(c); });
|
|
return s;
|
|
};
|
|
|
|
const std::string norm_ext = normalize("." + ext);
|
|
|
|
for (const auto& entry : fs::recursive_directory_iterator(root)) {
|
|
if (!entry.is_regular_file())
|
|
continue;
|
|
std::string entry_ext = normalize(entry.path().extension().string());
|
|
if (entry_ext == norm_ext)
|
|
matches.push_back(entry.path().string());
|
|
}
|
|
|
|
return matches;
|
|
}
|
|
|
|
std::string combine_files(
|
|
const std::vector<std::string>& filenames,
|
|
const std::string& prolog, const std::string& epilog,
|
|
const std::function <std::string(std::string)>& processor)
|
|
{
|
|
std::string result = prolog;
|
|
for (const std::string& f : filenames) {
|
|
result += "\n/* " + file_basename(f) + " */\n";
|
|
result += string_from_file(f);
|
|
}
|
|
result += "\n" + epilog + "\n";
|
|
if (processor) {
|
|
result = processor(result);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
bool files_differ(const fs::path& p1,
|
|
const fs::path& p2,
|
|
std::size_t buffer_size) // 64 KiB
|
|
{
|
|
// 1. Check existence and type
|
|
if (!fs::exists(p1) || !fs::exists(p2)) return true;
|
|
if (!fs::is_regular_file(p1) || !fs::is_regular_file(p2)) return true;
|
|
|
|
// 2. Compare sizes
|
|
auto s1 = fs::file_size(p1);
|
|
auto s2 = fs::file_size(p2);
|
|
if (s1 != s2) return true;
|
|
|
|
// 3. Open both files in binary mode
|
|
std::ifstream f1(p1, std::ios::binary);
|
|
std::ifstream f2(p2, std::ios::binary);
|
|
if (!f1 || !f2) return true; // treat I/O error as "different"
|
|
|
|
// 4. Compare in chunks
|
|
std::vector<char> buf1(buffer_size);
|
|
std::vector<char> buf2(buffer_size);
|
|
|
|
while (f1 && f2) {
|
|
f1.read(buf1.data(), buffer_size);
|
|
f2.read(buf2.data(), buffer_size);
|
|
|
|
std::streamsize r1 = f1.gcount();
|
|
std::streamsize r2 = f2.gcount();
|
|
if (r1 != r2) return true; // should not happen if sizes equal
|
|
|
|
if (r1 == 0) break; // EOF both
|
|
|
|
if (std::memcmp(buf1.data(), buf2.data(), static_cast<std::size_t>(r1)) != 0)
|
|
return true;
|
|
}
|
|
return false; // no difference found
|
|
}
|
|
|
|
// Copy a single file with an explicit binary read/write stream, forcing the
|
|
// destination to be world-readable. Deliberately NOT std::filesystem::copy_file
|
|
// or fs::copy: under Apple's `container` runtime the HTML output directory is a
|
|
// virtiofs bind mount, and libstdc++'s copy_file/copy create the destination
|
|
// with openat(O_WRONLY|O_CREAT|O_TRUNC, 0200) — a mode lacking the owner-read
|
|
// bit, which virtiofs rejects with EACCES (apple/container #1344, an OS-level
|
|
// Virtualization.framework bug), leaving a 0-byte --w------- file and aborting
|
|
// output. A stream copy creates the destination owner-readable and works
|
|
// identically on virtiofs, on in-VM filesystems, and under Docker. Used for
|
|
// every file Klammertext writes into the output tree (fonts, CSS, JS, ...).
|
|
void copy_file_stream(const fs::path& src, const fs::path& dst)
|
|
{
|
|
{
|
|
std::ifstream in(src, std::ios::binary);
|
|
if (!in)
|
|
throw File_error("Cannot read file for copy:\n " + src.string());
|
|
|
|
std::ofstream out(dst, std::ios::binary | std::ios::trunc);
|
|
if (!out)
|
|
throw File_error("Cannot create output file:\n " + dst.string());
|
|
|
|
// Guard against the empty-source failbit quirk of rdbuf insertion.
|
|
if (in.peek() != std::ifstream::traits_type::eof())
|
|
out << in.rdbuf();
|
|
out.flush();
|
|
|
|
if (!out || in.bad())
|
|
throw File_error("Failed to copy file:\n " + src.string()
|
|
+ "\n -> " + dst.string());
|
|
}
|
|
|
|
fs::permissions(dst,
|
|
fs::perms::owner_read | fs::perms::owner_write |
|
|
fs::perms::group_read | fs::perms::others_read,
|
|
fs::perm_options::replace);
|
|
}
|
|
|
|
void copy_preserving_basename(
|
|
const strings_t& filenames, const std::string& output_directory,
|
|
const std::string& link_directory)
|
|
{
|
|
fs::path outdir(output_directory + "/" + link_directory);
|
|
fs::create_directories(outdir);
|
|
for (const std::string& filename : filenames) {
|
|
fs::path pname(filename);
|
|
auto out_path = outdir / pname.filename();
|
|
// Preserve the previous copy_options::update_existing behavior: skip
|
|
// when the destination already exists and is no older than the source.
|
|
if (fs::exists(out_path) &&
|
|
fs::last_write_time(out_path) >= fs::last_write_time(pname))
|
|
continue;
|
|
copy_file_stream(pname, out_path);
|
|
}
|
|
}
|
|
|
|
fs::path resolve_relative_to(const fs::path& relative, const fs::path& base)
|
|
{
|
|
fs::path base_dir = is_directory(base) ? base : base.parent_path();
|
|
return fs::weakly_canonical(base_dir / relative);
|
|
}
|