Files
klammertext/mac/file.cpp

687 lines
23 KiB
C++
Raw Normal View History

#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;
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)) {
std::cerr << command_name << ": interpreting \""
<< acc << "\" as one filename\n";
result.push_back(acc);
i = j;
found = true;
break;
}
}
if (!found) {
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(
std::vector<std::string> filenames,
std::string prolog, std::string epilog,
std::function <std::string(std::string)> processor)
{
std::string result = prolog;
for (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);
}