#include #include #include #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(text[i-1]))) && (i + 1 == text.size() || std::isspace(static_cast(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 find_file_recursive(const fs::path& root, const std::string& filename, bool only_one) //, Locator loc) { std::vector 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 find_file_from_roots(const std::vector& roots, const std::string& filename, bool only_one) { (void)K::log(3, filename); std::vector result {}; for (std::string root : roots) { std::vector 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( 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 pathnames_with_extension( const fs::path& dir, const std::string extension) { std::vector 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 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 find_files_with_extension( strings_t find_files_with_extension( const fs::path& root, const std::string& ext, bool case_insensitive) { //std::vector 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 filenames, std::string prolog, std::string epilog, std::function 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 buf1(buffer_size); std::vector 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(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); }