#include #include #include #include #include #include #include #include #ifdef _WIN32 #define NOMINMAX #include #else #include #include #endif namespace faset { namespace { void validate_utf8_path(std::string_view text) { for (std::size_t i = 0; i < text.size();) { const auto first = static_cast(text[i++]); require(first != 0, "path.encoding", "NUL in filesystem path"); if (first < 0x80) continue; unsigned remaining = first >= 0xc2 && first <= 0xdf ? 1 : first >= 0xe0 && first <= 0xef ? 2 : first >= 0xf0 && first <= 0xf4 ? 3 : 0; require(remaining != 0 && i + remaining <= text.size(), "path.encoding", "Invalid UTF-8 path"); const unsigned minimum = remaining == 1 ? 0x80 : remaining == 2 ? 0x800 : 0x10000; unsigned code = first & ((1u << (6 - remaining)) - 1u); while (remaining--) { const auto next = static_cast(text[i++]); require((next & 0xc0) == 0x80, "path.encoding", "Invalid UTF-8 path"); code = (code << 6) | (next & 0x3f); } require(code >= minimum && code <= 0x10ffff && !(code >= 0xd800 && code <= 0xdfff), "path.encoding", "Invalid UTF-8 path"); } } std::string utf8_bytes(const std::u8string& value) { return {reinterpret_cast(value.data()), value.size()}; } } // namespace std::filesystem::path path_from_utf8(std::string_view text) { validate_utf8_path(text); return std::filesystem::path(std::u8string(text.begin(), text.end())); } std::string path_to_utf8(const std::filesystem::path& path) { return utf8_bytes(path.u8string()); } std::string generic_path_to_utf8(const std::filesystem::path& path) { return utf8_bytes(path.generic_u8string()); } std::filesystem::path native_io_path(const std::filesystem::path& path) { #ifdef _WIN32 auto normalized = std::filesystem::absolute(path).lexically_normal(); normalized.make_preferred(); const auto& native = normalized.native(); if (native.starts_with(LR"(\\?\)") || native.starts_with(LR"(\\.\)")) return normalized; if (native.starts_with(LR"(\\)")) return std::filesystem::path(std::wstring(LR"(\\?\UNC\)") + native.substr(2)); return std::filesystem::path(std::wstring(LR"(\\?\)") + native); #else return path; #endif } #ifdef _WIN32 int run_utf8_main(int argc, wchar_t** argv, int (*entry)(int, char**)) noexcept { try { std::vector arguments; arguments.reserve(static_cast(argc)); for (int i = 0; i < argc; ++i) { const auto length = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, argv[i], -1, nullptr, 0, nullptr, nullptr); require(length > 0, "cli.encoding", "Invalid Unicode command-line argument"); std::string value(static_cast(length), '\0'); require(WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, argv[i], -1, value.data(), length, nullptr, nullptr) == length, "cli.encoding", "Cannot convert command-line argument to UTF-8"); value.pop_back(); arguments.push_back(std::move(value)); } std::vector pointers; pointers.reserve(arguments.size() + 1); for (auto& value : arguments) pointers.push_back(value.data()); pointers.push_back(nullptr); return entry(argc, pointers.data()); } catch (const std::exception& error) { std::cerr << "Command line failed: " << error.what() << '\n'; return 1; } } #endif std::string new_id() { static std::mutex mutex; static std::random_device random; std::array bytes{}; { std::lock_guard lock(mutex); for (auto& byte : bytes) byte = static_cast(random()); } bytes[6] = (bytes[6] & 0x0f) | 0x40; bytes[8] = (bytes[8] & 0x3f) | 0x80; constexpr char hex[] = "0123456789abcdef"; std::string result; result.reserve(36); for (std::size_t i = 0; i < bytes.size(); ++i) { if (i == 4 || i == 6 || i == 8 || i == 10) result += '-'; result += hex[bytes[i] >> 4]; result += hex[bytes[i] & 15]; } return result; } std::string read_text(const std::filesystem::path& path) { std::ifstream stream(native_io_path(path), std::ios::binary); require(bool(stream), "io.open", "Cannot open file: " + path_to_utf8(path)); std::string value((std::istreambuf_iterator(stream)), {}); require(!stream.bad(), "io.read", "Cannot read file: " + path_to_utf8(path)); return value; } Json read_json(const std::filesystem::path& path) { try { return Json::parse(read_text(path)); } catch (const Json::exception& error) { throw Error("format.json", "Invalid JSON in " + path_to_utf8(path), {{"reason", error.what()}}); } } void atomic_write(const std::filesystem::path& path, std::string_view bytes) { const auto parent = path.has_parent_path() ? path.parent_path() : std::filesystem::path("."); std::filesystem::create_directories(native_io_path(parent)); auto temporary = path; temporary += ".tmp-" + new_id(); // Append ASCII to the native path without a narrow round trip. try { #ifdef _WIN32 HANDLE file = CreateFileW(native_io_path(temporary).c_str(), GENERIC_WRITE, 0, nullptr, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, nullptr); require(file != INVALID_HANDLE_VALUE, "io.create", "Cannot create temporary file"); bool ok = true; std::size_t offset = 0; while (offset < bytes.size()) { DWORD written = 0; const auto count = static_cast(std::min(bytes.size() - offset, 1u << 30)); if (!WriteFile(file, bytes.data() + offset, count, &written, nullptr) || written == 0) { ok = false; break; } offset += written; } ok = FlushFileBuffers(file) && ok; CloseHandle(file); require(ok, "io.write", "Cannot flush temporary file"); require(MoveFileExW(native_io_path(temporary).c_str(), native_io_path(path).c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) != 0, "io.replace", "Cannot publish file: " + path_to_utf8(path)); #else const int fd = ::open(temporary.c_str(), O_WRONLY | O_CREAT | O_EXCL, 0644); require(fd >= 0, "io.create", "Cannot create temporary file"); bool ok = true; std::size_t offset = 0; while (offset < bytes.size()) { const auto count = ::write(fd, bytes.data() + offset, bytes.size() - offset); if (count < 0 && errno == EINTR) continue; if (count <= 0) { ok = false; break; } offset += static_cast(count); } ok = (::fsync(fd) == 0) && ok; const auto closed = ::close(fd); ok = ok && (closed == 0); require(ok, "io.write", "Cannot flush temporary file"); std::filesystem::rename(temporary, path); const int directory = ::open(parent.c_str(), O_RDONLY | O_DIRECTORY); if (directory >= 0) { ::fsync(directory); ::close(directory); } #endif } catch (...) { std::error_code ignored; std::filesystem::remove(native_io_path(temporary), ignored); throw; } } void atomic_write_json(const std::filesystem::path& path, const Json& value) { atomic_write(path, value.dump(2) + "\n"); } std::filesystem::path project_path(const std::filesystem::path& root, const std::filesystem::path& relative) { require(!relative.is_absolute(), "path.outside_project", "Expected a path relative to the project"); const auto canonical = std::filesystem::weakly_canonical(root); const auto target = std::filesystem::weakly_canonical(canonical / relative); auto a = canonical.begin(), b = target.begin(); for (; a != canonical.end(); ++a, ++b) require(b != target.end() && *a == *b, "path.outside_project", "Path escapes the project root"); return target; } } // namespace faset