Просмотр исходного кода

Read authored rule files beside settings.json, and log with a format string

Vendor behaviour is driven by hand-authored text files re-read on every
use, so tuning needs no rebuild. rule_text::Cursor gives them one
grammar: # comments to end of line, blank lines ignored, hex and decimal
fields separated by spaces, each reader asking for fields in its own
order. read_artifact_text reads a file whole beside the DLL and refuses
one too large for its buffer rather than truncating it into a half-rule
that would parse as something else.

core::log::writef formats and emits one event in fixed storage, and
skips formatting entirely for a level the channel refuses.
chnsw 6 дней назад
Родитель
Сommit
14c558bb8b

+ 1 - 0
Sunrise/Sunrise.vcxproj

@@ -1195,6 +1195,7 @@
     <ClInclude Include="src\core\logging\snapshot\internal.h" />
     <ClInclude Include="src\core\logging\view\log_snapshot_view.h" />
     <ClInclude Include="src\core\filesystem\path.h" />
+    <ClInclude Include="src\core\settings\rule_text.h" />
     <ClInclude Include="src\core\filesystem\temporary_sibling.h" />
     <ClInclude Include="src\core\settings\settings.h" />
     <ClInclude Include="src\core\settings\parser.h" />

+ 70 - 0
Sunrise/src/core/filesystem/path.cpp

@@ -2,6 +2,7 @@
 
 #include <Windows.h>
 
+#include <cstdint>
 #include <cstring>
 
 namespace sunrise::core::path {
@@ -63,6 +64,40 @@ bool artifact_directory(void* module, Buffer& output) noexcept {
     return attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0;
 }
 
+/** Resolves one Sunrise-owned file beside this DLL. */
+bool artifact_file(std::wstring_view relative, Buffer& output) noexcept {
+    HMODULE self{};
+    // From this function's own address, so it names the DLL rather than the host executable. The
+    // two differ: the game sits in the install root and this module in `bin\x64`.
+    if (GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS
+                               | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
+                           reinterpret_cast<LPCWSTR>(&artifact_file),
+                           &self)
+            == FALSE
+        || self == nullptr) {
+        return false;
+    }
+    if (!artifact_directory(self, output)) {
+        return false;
+    }
+    // Create each directory named before the file, so `exports\x.txt` works on a fresh install.
+    std::size_t start = 0;
+    for (std::size_t index = 0; index < relative.size(); ++index) {
+        if (relative[index] != L'\\') {
+            continue;
+        }
+        if (!append(output, L"\\") || !append(output, relative.substr(start, index - start))) {
+            return false;
+        }
+        if (CreateDirectoryW(output.chars.data(), nullptr) == FALSE
+            && GetLastError() != ERROR_ALREADY_EXISTS) {
+            return false;
+        }
+        start = index + 1;
+    }
+    return append(output, L"\\") && append(output, relative.substr(start));
+}
+
 /** Appends a path suffix without exceeding fixed storage. */
 bool append(Buffer& path, std::wstring_view suffix) noexcept {
     if (path.length + suffix.size() >= path.chars.size()) {
@@ -74,4 +109,39 @@ bool append(Buffer& path, std::wstring_view suffix) noexcept {
     return true;
 }
 
+/** Reads one Sunrise-owned text file whole, into caller storage, terminated. */
+bool read_artifact_text(std::wstring_view relative, std::span<char> text) noexcept {
+    if (text.empty()) {
+        return false;
+    }
+    text[0] = '\0';
+    Buffer file{};
+    if (!artifact_file(relative, file)) {
+        return false;
+    }
+    const HANDLE handle = CreateFileW(file.chars.data(), GENERIC_READ, FILE_SHARE_READ, nullptr,
+                                      OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
+    if (handle == INVALID_HANDLE_VALUE) {
+        return false;
+    }
+    // The size is measured before anything is read, so a file too large for the caller's storage
+    // is refused outright rather than read in part. Half a rule would parse as a whole one, which
+    // is worse than having no rule at all. One byte is kept for the terminator, so a file that
+    // exactly fills the rest still reads whole.
+    LARGE_INTEGER size{};
+    DWORD read = 0;
+    const bool measured = GetFileSizeEx(handle, &size) != FALSE;
+    const bool fits = measured && size.QuadPart >= 0
+                      && static_cast<std::uint64_t>(size.QuadPart) < text.size();
+    const bool ok = fits
+                    && ReadFile(handle, text.data(), static_cast<DWORD>(text.size() - 1), &read,
+                                nullptr) != FALSE;
+    (void)CloseHandle(handle);
+    if (!ok || read == 0) {
+        return false;
+    }
+    text[read] = '\0';
+    return true;
+}
+
 } // namespace sunrise::core::path

+ 28 - 0
Sunrise/src/core/filesystem/path.h

@@ -2,6 +2,7 @@
 
 #include <array>
 #include <cstddef>
+#include <span>
 #include <string_view>
 
 namespace sunrise::core::path {
@@ -24,7 +25,34 @@ struct Buffer {
 /** Resolves and creates the one Sunrise-owned generated-artifact directory. */
 [[nodiscard]] bool artifact_directory(void* module, Buffer& output) noexcept;
 
+/**
+ * Resolves one Sunrise-owned file beside this DLL, creating any directory it needs.
+ *
+ * Callers get `<directory holding steam_api64.dll>\Sunrise\<relative>`, which is where
+ * `settings.json`, `logs` and `cache` already live. This finds the module from its own address, so
+ * nothing has to thread a handle down, and moving the whole install needs no rebuild.
+ *
+ * @param relative File name, optionally with one leading subdirectory such as `exports\x.txt`.
+ * @param output Receives the full path.
+ * @return True when the path fits and every directory in it exists or was created.
+ */
+[[nodiscard]] bool artifact_file(std::wstring_view relative, Buffer& output) noexcept;
+
 /** Appends one suffix without exceeding fixed path storage. */
 [[nodiscard]] bool append(Buffer& path, std::wstring_view suffix) noexcept;
 
+/**
+ * Reads one Sunrise-owned text file whole, into caller storage, terminated.
+ *
+ * Every authored rule file is read this way: opened fresh each time it is consulted, so editing one
+ * takes effect without a relaunch, and never partially - a file too large for the caller's storage
+ * is refused rather than truncated into a half-rule that would parse as something else.
+ *
+ * @param relative File name, resolved the same way `artifact_file` resolves one.
+ * @param text Caller storage. Receives the file's bytes followed by a terminating NUL, so one byte
+ *        of it is always spent on the terminator.
+ * @return True only when the file opened, fitted, and held at least one byte.
+ */
+[[nodiscard]] bool read_artifact_text(std::wstring_view relative, std::span<char> text) noexcept;
+
 } // namespace sunrise::core::path

+ 18 - 0
Sunrise/src/core/logging/log.cpp

@@ -5,6 +5,7 @@
 #include <algorithm>
 #include <array>
 #include <atomic>
+#include <cstdarg>
 #include <cstdio>
 #include <cstring>
 #include <mutex>
@@ -250,6 +251,23 @@ void write(Channel channel, Level level, std::string_view event) noexcept {
     snapshot::internal::record(channel, level, std::string_view(line.data(), snapshotLength));
 }
 
+/** Formats and emits one structured event when allowed by the channel threshold. */
+void writef(Channel channel, Level level, const char* format, ...) noexcept {
+    if (format == nullptr || !accepts(channel, level)) {
+        return;
+    }
+    std::array<char, kLineCapacity> line{};
+    va_list arguments;
+    va_start(arguments, format);
+    const int count = std::vsnprintf(line.data(), line.size(), format, arguments);
+    va_end(arguments);
+    if (count <= 0) {
+        return;
+    }
+    const std::size_t length = (std::min)(static_cast<std::size_t>(count), line.size() - 1);
+    write(channel, level, std::string_view(line.data(), length));
+}
+
 /** Formats and emits one debug event carrying a duration in the ms field. */
 void write_elapsed(Channel channel,
                    std::string_view event,

+ 13 - 0
Sunrise/src/core/logging/log.h

@@ -51,6 +51,19 @@ void shutdown() noexcept;
 /** Emits one structured event when allowed by the channel threshold. */
 void write(Channel channel, Level level, std::string_view event) noexcept;
 
+/**
+ * Formats and emits one structured event when allowed by the channel threshold.
+ *
+ * The line is built in `kLineCapacity` storage and truncated to fit, which is what every caller
+ * that spelled out its own array and `snprintf` did by hand. Nothing is formatted for a level the
+ * channel refuses, so a debug line costs nothing when debug is off.
+ *
+ * @param channel Subsystem owning the event.
+ * @param level Severity of the event.
+ * @param format printf-style format; `%s` arguments must be NUL-terminated.
+ */
+void writef(Channel channel, Level level, const char* format, ...) noexcept;
+
 /**
  * Emits one debug event carrying a duration in the ms field.
  * Timing is diagnostic, so it stays off at the levels a normal run uses.

+ 132 - 0
Sunrise/src/core/settings/rule_text.h

@@ -0,0 +1,132 @@
+#pragma once
+
+#include <cstddef>
+#include <cstdint>
+
+namespace sunrise::core::rule_text {
+
+/**
+ * Storage one rule file is read into. Far larger than any authored file needs, because a file too
+ * large for its buffer is refused whole rather than read in part, and refusal silently turns off
+ * whatever the file configures.
+ */
+inline constexpr std::size_t kRuleTextCapacity = 65536;
+
+/** Line comments run from this character to the end of the line. */
+inline constexpr char kCommentMark = '#';
+
+/** @return True for a character `read_hex` accepts. */
+[[nodiscard]] constexpr bool is_hex_digit(char value) noexcept {
+    return (value >= '0' && value <= '9') || (value >= 'a' && value <= 'f')
+           || (value >= 'A' && value <= 'F');
+}
+
+/** @return The value of one hex digit, or 0 for any other character. */
+[[nodiscard]] constexpr std::uint32_t hex_value(char value) noexcept {
+    if (value >= '0' && value <= '9') {
+        return static_cast<std::uint32_t>(value - '0');
+    }
+    if (value >= 'a' && value <= 'f') {
+        return static_cast<std::uint32_t>(value - 'a') + 10U;
+    }
+    if (value >= 'A' && value <= 'F') {
+        return static_cast<std::uint32_t>(value - 'A') + 10U;
+    }
+    return 0;
+}
+
+/**
+ * Reads one authored rule file, a field at a time.
+ *
+ * Every rule file Sunrise authors has the same shape: `#` comments to end of line, blank lines
+ * ignored, and rules made of hex and decimal fields separated by spaces. Each reader knows its own
+ * field order and asks for fields in that order, so a hex field and a decimal one are never
+ * confused - "100" is 256 read as hex and 100 read as decimal, and only the caller knows which the
+ * file meant.
+ *
+ * Nothing here allocates or throws. The cursor borrows the caller's text and never runs past its
+ * terminating NUL.
+ */
+class Cursor final {
+public:
+    /** @param text NUL-terminated rule text, borrowed for the cursor's lifetime. */
+    explicit constexpr Cursor(const char* text) noexcept : at_{text} {}
+
+    /**
+     * @return True while the cursor sits on a field of the line it is already reading. A leading
+     * minus counts only in front of a digit, so a negative field is never read back as positive.
+     */
+    [[nodiscard]] constexpr bool at_field() const noexcept {
+        return is_hex_digit(*at_) || (*at_ == '-' && at_[1] >= '0' && at_[1] <= '9');
+    }
+
+    /**
+     * Advances to the next rule field, stepping over comments, blank lines and separators.
+     * @return True when a field was found, false at the end of the text.
+     */
+    constexpr bool seek_field() noexcept {
+        while (*at_ != '\0') {
+            if (*at_ == kCommentMark) {
+                while (*at_ != '\0' && *at_ != '\n') {
+                    ++at_;
+                }
+                continue;
+            }
+            if (at_field()) {
+                return true;
+            }
+            ++at_;
+        }
+        return false;
+    }
+
+    /** @return One hex field, and steps past it and any spaces after it. */
+    constexpr std::uint32_t read_hex() noexcept {
+        std::uint32_t value = 0;
+        while (is_hex_digit(*at_)) {
+            value = (value * 16U) + hex_value(*at_);
+            ++at_;
+        }
+        skip_spaces();
+        return value;
+    }
+
+    /**
+     * @return One decimal field, and steps past it and any spaces after it.
+     *
+     * Digits accumulate wide and saturate. A field longer than the type can hold is a malformed
+     * file rather than a rule, and overflowing a signed accumulator to find that out is undefined
+     * behaviour, so the value stops at the end of the range instead of wrapping into a small or
+     * negative one that would read as a plausible rule.
+     */
+    constexpr std::int32_t read_decimal() noexcept {
+        const bool negative = *at_ == '-';
+        if (negative) {
+            ++at_;
+        }
+        constexpr std::int64_t kCeiling = 0x7FFFFFFF;
+        std::int64_t value = 0;
+        while (*at_ >= '0' && *at_ <= '9') {
+            if (value <= kCeiling) {
+                value = (value * 10) + (*at_ - '0');
+            }
+            ++at_;
+        }
+        skip_spaces();
+        if (value > kCeiling) {
+            value = kCeiling;
+        }
+        return static_cast<std::int32_t>(negative ? -value : value);
+    }
+
+private:
+    constexpr void skip_spaces() noexcept {
+        while (*at_ == ' ' || *at_ == '\t') {
+            ++at_;
+        }
+    }
+
+    const char* at_;
+};
+
+} // namespace sunrise::core::rule_text