Files
klammertext/mac/file.cpp

572 lines
19 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;
}
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(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(std::string cache_dir, std::string basename, 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(std::string cache_dir, 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(std::string cache_dir, std::string file_to_cache, 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(
strings_t filenames, std::string output_directory, std::string link_directory)
{
fs::path outdir(output_directory + "/" + link_directory);
fs::create_directories(outdir);
for (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);
}