From d0b17d48e742274e4d240c7fc5cd034bef6cf795 Mon Sep 17 00:00:00 2001 From: Kittycannon Date: Fri, 7 Aug 2026 20:08:28 -0600 Subject: [PATCH] audited all the library using gemini, openssl support is still pending --- src/ckitty/memory/array.hpp | 159 ++++++++++-------- src/ckitty/memory/bits.hpp | 138 +++++++++++++--- src/ckitty/memory/buffers.hpp | 265 ++++++++++++++--------------- src/ckitty/memory/data_view.hpp | 131 +++++++++++---- src/ckitty/memory/iterators.hpp | 128 ++++++++++++-- src/ckitty/memory/primitives.hpp | 63 ++++--- src/ckitty/memory/ptr.hpp | 145 ++++++++++++++++ src/ckitty/memory/range.hpp | 135 +++++++++++++++ src/ckitty/strings/unicode.hpp | 266 ++++++++++++++++++++++++++++++ src/ckitty/strings/utils.hpp | 165 +++++++++++++----- src/ckitty/system/chrono.hpp | 114 +++++++++++-- src/ckitty/system/environment.hpp | 208 ++++++++++++++++------- src/ckitty/system/error.hpp | 170 ++++++++++++++----- 13 files changed, 1631 insertions(+), 456 deletions(-) create mode 100644 src/ckitty/memory/ptr.hpp create mode 100644 src/ckitty/memory/range.hpp diff --git a/src/ckitty/memory/array.hpp b/src/ckitty/memory/array.hpp index b0450a4..2dd64d8 100644 --- a/src/ckitty/memory/array.hpp +++ b/src/ckitty/memory/array.hpp @@ -1,108 +1,129 @@ #pragma once #include +#include + +#include +#include +#include namespace ckitty { /** - * @brief Represents a dynamically created, - * fixed size container. Useful to move data - * around, when the data itself has a fixed size. - * - * C++ does not have a standard library equivalent. - * Closest to it are vector and std::array. However, - * vector does not have a constant length and array - * is not dynamically created. + * @brief Represents a dynamically allocated, fixed-size array container + * using shared ownership semantics. + * + * @tparam T Element type stored in the array. */ template class array { + public: + // Standard STL type definitions + using value_type = T; + using size_type = usize; + using difference_type = usize; + using reference = T&; + using const_reference = const T&; + using pointer = T*; + using const_pointer = const T*; + using iterator = T*; + using const_iterator = const T*; + protected: - ptr _data; - isize _length; + usize _length{ 0 }; - public: - + private: /** - * Inherits a pointer, and takes ownership of it. + * @brief Private constructor used by factory methods to avoid unnecessary allocations. */ - static array inherit(T* data, isize length) { - array arr = array(); - arr._data = ptr(data); - arr._length = length; - return arr; - } - - /** - * @brief Borrows a modifiable part of a buffer to - * this array. The array does not delete it. - */ - static array borrow(T* data, isize length) { - array arr = array(); - arr._data = ptr(data, []([[maybe_unused]] T* _ptr) {}); - arr._length = length; - return arr; + array(ptr data, const usize length) noexcept + : _data(std::move(data)), _length(length) { } public: + /** + * @brief Takes ownership of a raw pointer. + */ + static array inherit(T* data, const usize length) { + return array(ptr(data), length); + } - array(const isize length = 0) - : _data(new T[length]), _length(length) {} + /** + * @brief Borrows a buffer without taking deletion ownership. + */ + static array borrow(T* data, const usize length) { + return array(ptr(data, []([[maybe_unused]] T* p) {}), length); + } + public: + /** + * @brief Construct a new fixed-size array (value-initialized). + * @param length Number of elements to allocate. + */ + explicit array(const usize length = 0) + : _data(length > 0 ? new T[length]() : nullptr), _length(length) { + } + + /** + * @brief Construct array by copying from a range [begin, end). + */ array(const T* begin, const T* end) - : _data(new T[end - begin]), _length(isize(end - begin)) { - std::copy(begin, end, this->begin()); + : _data(end > begin ? new T[end - begin] : nullptr), + _length(end > begin ? static_cast(end - begin) : 0) { + if (_length > 0) { + std::copy(begin, end, _data.get()); + } } - array(const array& copy) : _data(copy._data), _length(copy._length) {} + // Shared ownership copy constructor and assignment + array(const array& copy) noexcept = default; + array& operator=(const array& copy) noexcept = default; - array(array&& move) : _data(std::move(move._data)), _length(move._length) { - // move._data = nullptr - move._length = 0; + // Move constructor and assignment + array(array&& move) noexcept + : _data(std::move(move._data)), _length(std::exchange(move._length, 0)) { } - array& operator=(const array& copy) { - this->_data = copy._data; - this->_length = copy._length; + array& operator=(array&& move) noexcept { + if (this != &move) { + _data = std::move(move._data); + _length = std::exchange(move._length, 0); + } return *this; } - array& operator=(array&& move) { - this->_data = std::move(move._data); - this->_length = move._length; - // move._data = nullptr - move._length = 0; - return *this; + ~array() = default; + + // Element Access + [[nodiscard]] T& operator[](const usize i) noexcept { + assert(i >= 0 && i < _length && "Index out of bounds"); + return _data.get()[i]; } - T& operator[](const isize i) { - return *(_data.get() + i); + [[nodiscard]] const T& operator[](const usize i) const noexcept { + assert(i >= 0 && i < _length && "Index out of bounds"); + return _data.get()[i]; } - const T& operator[](const isize i) const { - return *(_data.get() + i); - } + // Capacity + [[nodiscard]] usize length() const noexcept { return _length; } + [[nodiscard]] usize size() const noexcept { return _length; } + [[nodiscard]] bool empty() const noexcept { return _length == 0; } - isize length() const { - return _length; - } + // Data pointer + [[nodiscard]] T* data() noexcept { return _data.get(); } + [[nodiscard]] const T* data() const noexcept { return _data.get(); } - T* begin() { - return _data.get(); - } + // Iterators + [[nodiscard]] T* begin() noexcept { return _data.get(); } + [[nodiscard]] T* end() noexcept { return _data.get() + _length; } - T* end() { - return _data.get() + _length; - } - - const T* begin() const { - return _data.get(); - } - - const T* end() const { - return _data.get() + _length; - } + [[nodiscard]] const T* begin() const noexcept { return _data.get(); } + [[nodiscard]] const T* end() const noexcept { return _data.get() + _length; } + [[nodiscard]] const T* cbegin() const noexcept { return _data.get(); } + [[nodiscard]] const T* cend() const noexcept { return _data.get() + _length; } }; -} \ No newline at end of file +} // namespace ckitty \ No newline at end of file diff --git a/src/ckitty/memory/bits.hpp b/src/ckitty/memory/bits.hpp index 1453271..371f12f 100644 --- a/src/ckitty/memory/bits.hpp +++ b/src/ckitty/memory/bits.hpp @@ -1,58 +1,148 @@ #pragma once -#include #include +#include +#include #include namespace ckitty { + /** + * @brief Endianness specification for packing and unpacking operations. + */ + enum class endian { + little, + big + }; + namespace bits { /** - * Unpacks a value into bytes. + * @brief Unpacks an integral value into an array of bytes. + * @tparam T Integral type to unpack. + * @param value The value to convert into bytes. + * @param order Byte order (default: endian::little). */ template - inline ckitty::array unpack(const T value, bool endianness) { - const size bytelen = sizeof(T); - ckitty::array bytes(bytelen); - for(size i = 0; i < bytelen; i++) { - size j = endianness ? bytelen - i - 1 : i; - bytes[i] = (value >> (j * 8)) & 0xFF; + [[nodiscard]] inline ckitty::array unpack(const T value, const endian order = endian::little) { + static_assert(std::is_integral_v, "bits::unpack requires an integral type"); + + constexpr usize bytelen = sizeof(T); + ckitty::array bytes(static_cast(bytelen)); + + for (usize i = 0; i < bytelen; ++i) { + const usize shift_bytes = (order == endian::big) ? (bytelen - 1 - i) : i; + bytes[static_cast(i)] = static_cast((value >> (shift_bytes * 8)) & 0xFF); } + return bytes; } - + /** - * Packs bytes into a value. + * @brief Packs a raw byte buffer into an integral value. + * @tparam T Integral destination type. + * @param bytes Pointer to the byte buffer. + * @param length Number of bytes available in the buffer. + * @param order Byte order (default: endian::little). */ template - inline T pack(const u8* bytes, bool endianness) { - const size bytelen = sizeof(T); - T type(0); - for(size i = 0; i < bytelen; i++) { - size j = endianness ? bytelen - i - 1 : i; - type |= T(bytes[i] << (j * 8)) & 0xFF; + [[nodiscard]] inline T pack(const u8* bytes, const usize length, const endian order = endian::little) noexcept { + static_assert(std::is_integral_v, "bits::pack requires an integral type"); + + if (!bytes) { + return T(0); } - return type; + + constexpr usize bytelen = sizeof(T); + const usize count = (length < bytelen) ? length : bytelen; + T result{0}; + + for (usize i = 0; i < count; ++i) { + const usize shift_bytes = (order == endian::big) ? (bytelen - 1 - i) : i; + result |= (static_cast(bytes[i]) << (shift_bytes * 8)); + } + + return result; } + /** + * @brief Packs a ckitty::array of bytes into an integral value. + */ template - inline bool read(const T value, isize bitno) { - return (value >> bitno) & 1; + [[nodiscard]] inline T pack(const ckitty::array& bytes, const endian order = endian::little) noexcept { + return pack(bytes.begin(), static_cast(bytes.length()), order); } + /** + * @brief Reads a single bit state at the specified index. + */ template - inline T write(const T value, isize bitno, bool bitval) { - size mask = T(1) << bitno; + [[nodiscard]] inline bool read(const T value, const usize bitno) noexcept { + static_assert(std::is_integral_v, "bits::read requires an integral type"); + return (value >> bitno) & static_cast(1); + } + + /** + * @brief Sets or clears a single bit at the specified index, returning the modified value. + */ + template + [[nodiscard]] inline T write(const T value, const usize bitno, const bool bitval) noexcept { + static_assert(std::is_integral_v, "bits::write requires an integral type"); + const T mask = static_cast(1) << bitno; return bitval ? (value | mask) : (value & ~mask); } + /** + * @brief Modifies a bit in-place at the specified index. + */ template - inline void write_r(T& value, isize bitno, bool bitval) { + inline void write_inplace(T& value, const usize bitno, const bool bitval) noexcept { value = write(value, bitno, bitval); } - } + /** + * @brief Sets a bit to 1 at the specified index. + */ + template + [[nodiscard]] inline T set(const T value, const usize bitno) noexcept { + return write(value, bitno, true); + } -} + /** + * @brief Clears a bit to 0 at the specified index. + */ + template + [[nodiscard]] inline T clear(const T value, const usize bitno) noexcept { + return write(value, bitno, false); + } + + /** + * @brief Inverts a bit at the specified index. + */ + template + [[nodiscard]] inline T toggle(const T value, const usize bitno) noexcept { + static_assert(std::is_integral_v, "bits::toggle requires an integral type"); + return value ^ (static_cast(1) << bitno); + } + + /** + * @brief Reverses the byte order of an integral value. + */ + template + [[nodiscard]] inline T byteswap(const T value) noexcept { + static_assert(std::is_integral_v, "bits::byteswap requires an integral type"); + constexpr usize bytelen = sizeof(T); + T result{0}; + + for (usize i = 0; i < bytelen; ++i) { + const u8 byte = static_cast((value >> (i * 8)) & 0xFF); + result |= (static_cast(byte) << ((bytelen - 1 - i) * 8)); + } + + return result; + } + + } // namespace bits + +} // namespace ckitty \ No newline at end of file diff --git a/src/ckitty/memory/buffers.hpp b/src/ckitty/memory/buffers.hpp index d505f56..8c88229 100644 --- a/src/ckitty/memory/buffers.hpp +++ b/src/ckitty/memory/buffers.hpp @@ -2,70 +2,87 @@ #include #include +#include + +#include +#include +#include +#include #include -#include #include +#include +#include namespace ckitty { /** - * Define a buffer as an array of u8. + * @brief A byte buffer represented as a contiguous fixed-size array of u8. */ using buffer = array; namespace buffers { /** - * @brief The sole purpose of this function - * is to stop me from confusing the signs. + * @brief Checks whether a given index falls within bounds. */ - inline bool fits(isize index, isize length) { - return index < length; + [[nodiscard]] constexpr inline bool fits(const isize index, const isize length) noexcept { + return index >= 0 && index < length; } /** - * @brief Returns true if some data fits in the - * provided space. + * @brief Checks if a requested payload size fits starting at `offset`. */ - inline bool fits(isize offset, isize length, isize test) { - return (offset < length) && ((length - offset) >= test); + [[nodiscard]] constexpr inline bool fits(const isize offset, const isize length, const isize test_length) noexcept { + return offset >= 0 && test_length >= 0 && length >= 0 && + offset <= length && test_length <= (length - offset); } - inline data_view view(const buffer& buff) { + /** + * @brief Creates a data_view over an existing buffer. + */ + [[nodiscard]] inline data_view view(const buffer& buff) noexcept { return data_view(buff.begin(), buff.end()); } /** - * @brief Simple match agains data view. + * @brief Creates a data_view over a string. */ - inline bool matches(data_view d1, data_view d2) { - if(d1.length() != d2.length()) return false; - for(isize i = 0; i < d1.length(); i++) { - if(d1[i] != d2[i]) return false; - } - return true; + [[nodiscard]] inline data_view view(std::string_view str) noexcept { + return data_view(str); } /** - * @brief Matches if the data starts with a prefix + * @brief Tests if two byte views contain identical content. */ - inline bool startsWith(data_view d1, data_view pre) { - if(d1.length() < pre.length()) return false; - for(isize i = 0; i < pre.length(); i++) { - if(d1[i] != pre[i]) return false; - } - return true; + [[nodiscard]] constexpr inline bool matches(const data_view d1, const data_view d2) noexcept { + return d1 == d2; } - inline bool find(data_view data, data_view test, isize& index) { - // loop until we find match - for(isize off = 0; off < data.length(); off++) { - // fits? - if(!fits(off, data.length(), test.length())) return false; + /** + * @brief Checks if a view starts with the given prefix. + */ + [[nodiscard]] constexpr inline bool starts_with(const data_view data, const data_view prefix) noexcept { + if (data.length() < prefix.length()) return false; + return data.sub_off(0, prefix.length()) == prefix; + } - // test for equality - if(matches(data_view(data.begin() + off, test.length()), test)) { - index = off; + /** + * @brief Searches for a subview pattern inside data. + * @param data Buffer view to search within. + * @param test Pattern view to locate. + * @param[out] out_index Set to the match start index if found. + * @return true if pattern was found, false otherwise. + */ + [[nodiscard]] inline bool find(const data_view data, const data_view test, isize& out_index) noexcept { + if (test.empty()) { + out_index = 0; + return true; + } + + const isize max_offset = data.length() - test.length(); + for (isize off = 0; off <= max_offset; ++off) { + if (data.sub_off(off, test.length()) == test) { + out_index = off; return true; } } @@ -73,135 +90,97 @@ namespace ckitty { } /** - * @brief Checks if an index is in between - * min and max, inclusively. + * @brief Checks if an index is within [min, max] inclusively. */ - inline bool in_range(isize min, isize index, isize max) { - return min <= index && index <= max; + [[nodiscard]] constexpr inline bool in_range(const isize min_val, const isize index, const isize max_val) noexcept { + return min_val <= index && index <= max_val; } /** - * @brief Moves the buffer to a new - * one, and restores the size in the - * previous one. + * @brief Moves buffer ownership out while replacing the source buffer with a fresh allocation. */ - inline buffer moveRealocate(buffer& buff) { - isize len = buff.length(); - buffer out = std::move(buff); + [[nodiscard]] inline buffer move_and_reallocate(buffer& buff) { + const isize len = buff.length(); + buffer old_buffer = std::move(buff); buff = buffer(len); - return out; + return old_buffer; } - inline data_view view(std::string& str) { - return data_view(reinterpret_cast(str.c_str()), str.length()); + /** + * @brief Copies data from a data_view into a buffer at `index`, advancing `index`. + */ + inline void copy(buffer& buff, isize& index, const data_view src) noexcept { + assert(index >= 0 && "Index cannot be negative"); + assert(index + src.length() <= buff.length() && "Buffer overflow during copy operation"); + + std::copy(src.begin(), src.end(), buff.begin() + index); + index += src.length(); } - inline void copy(buffer& buff, isize& index, const std::string& str) { - std::copy(str.begin(), str.end(), buff.begin() + index); - index += str.size(); + /** + * @brief Construct a new owned buffer populated with bytes from a string. + */ + [[nodiscard]] inline buffer from(std::string_view str) { + return buffer(reinterpret_cast(str.data()), + reinterpret_cast(str.data() + str.size())); } - inline void copy(buffer& buff, isize& index, const buffer& data) { - std::copy(data.begin(), data.end(), buff.begin() + index); - index += data.length(); - } + /** + * @brief Prints a formatted hexadecimal and ASCII view of a buffer to a stream. + */ + inline void hexdump(const data_view view, std::ostream& os, std::string_view prefix = "") { + constexpr char hex_lookup[] = "0123456789ABCDEF"; - inline void hexdump(data_view view, std::ostream& os, std::string prefix = "") { - // format: - // FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF - // - - char chars[] = { - '0', '1', '2', '3', - '4', '5', '6', '7', - '8', '9', 'A', 'B', - 'C', 'D', 'E', 'F' - }; - - // how many rows chars to use? - double logv = std::log(std::max(1, view.length())); - double spaces = std::floor(logv / std::log(16) + 1); - isize rowchars = std::max(2, isize(spaces)) - 1; - - // print header - os << prefix << std::string(rowchars, ' ') << " X0 X1 X2 X3 X4 X5 X6 X7 X8 X9 XA XB XC XD XE XF | -------- --------\n"; - - // check zero data - if(view.length() == 0) { - os << prefix << std::string(rowchars, ' ') << " (NO DATA)\n"; - os.flush(); + if (view.empty()) { + os << prefix << "(NO DATA)\n"; return; } - // add prefix - os << prefix << "\n"; - - // loop through the bytes - isize index = 0, row = 0; - while(index < view.length()) { - // ascii buffer - std::string ascii; - - // add header - os << prefix; - os << std::hex << std::setfill('0') << std::setw(int(rowchars)) << row << std::dec << "X "; - - // print a line of the characters - for(int g = 0; g < 2; g++) { - // print half a row - for(int i = 0; i < 8; i++) { - // end of file? - u8 value, high, low; - - // compute vars - if(index < view.length()) { - // get the byte value - value = view[index++]; - high = (value >> 4) & 0xF; - low = (value >> 0) & 0xF; - - // log value - os << chars[high] << chars[low]; - } else { - os << " "; - value = ' '; - } - - // add to ascii? - char ch = char(value); - if(!std::isgraph(ch)) ch = ' '; - ascii += ch; - - // space condition - if(i < 7) { - os << ' '; - continue; - } - if(g == 0) { - os << " "; - ascii += " "; - continue; - } - } - // <- - } - - // add the "ASCII" version - os << " | " << ascii; - row++; - - // end of line - os << '\n'; - os.flush(); + // Calculate address width in hex digits integer-wise + isize max_addr = view.length(); + int addr_width = 2; + while (max_addr > 0xFF) { + addr_width += 2; + max_addr >>= 8; + } + + // Print Header + os << prefix << std::string(addr_width, ' ') + << " X0 X1 X2 X3 X4 X5 X6 X7 X8 X9 XA XB XC XD XE XF | ASCII\n"; + + const isize total = view.length(); + for (isize i = 0; i < total; i += 16) { + os << prefix; + os << std::hex << std::setfill('0') << std::setw(addr_width) << i << std::dec << " "; + + std::string ascii_part; + ascii_part.reserve(16); + + for (isize j = 0; j < 16; ++j) { + const isize idx = i + j; + if (idx < total) { + const u8 byte = view[idx]; + os << hex_lookup[(byte >> 4) & 0x0F] << hex_lookup[byte & 0x0F]; + + const unsigned char ubyte = static_cast(byte); + ascii_part += std::isgraph(ubyte) ? static_cast(ubyte) : '.'; + } else { + os << " "; + } + + if (j == 7) { + os << " "; + } else if (j < 15) { + os << ' '; + } + } + + os << " | " << ascii_part << '\n'; } - // undo manips os << std::setfill(' '); } - inline buffer from(const std::string& str) { - return buffer(reinterpret_cast(str.begin().base()), reinterpret_cast(str.end().base())); - } + } // namespace buffers - } - -} \ No newline at end of file +} // namespace ckitty \ No newline at end of file diff --git a/src/ckitty/memory/data_view.hpp b/src/ckitty/memory/data_view.hpp index c286667..6548038 100644 --- a/src/ckitty/memory/data_view.hpp +++ b/src/ckitty/memory/data_view.hpp @@ -1,55 +1,128 @@ #pragma once +#include #include +#include +#include +#include +#include +#include + namespace ckitty { /** - * @brief Represents a piece of a shared buffer that - * is not owned by this object. + * @brief A non-owning view over a contiguous sequence of immutable bytes. + * Lightweight wrapper equivalent to a std::span. */ class data_view { - private: + public: + using value_type = u8; + using size_type = isize; + using difference_type = isize; + using reference = const u8&; + using const_reference = const u8&; + using pointer = const u8*; + using const_pointer = const u8*; + using iterator = const u8*; + using const_iterator = const u8*; - const u8* _begin; - const u8* _end; + private: + const u8* _begin{ nullptr }; + const u8* _end{ nullptr }; public: + // Constructors + constexpr data_view() noexcept = default; - data_view(const u8* begin, const u8* end) - : _begin(begin), _end(end) {} - - data_view(const u8* data, const isize length) - : _begin(data), _end(data + length) {} - - data_view(const std::string& str) - : _begin(reinterpret_cast(str.begin().base())), - _end(reinterpret_cast(str.end().base())) {} - - const u8* begin() const { - return _begin; + constexpr data_view(const u8* begin, const u8* end) noexcept + : _begin(begin), _end(end) { + assert((_begin <= _end) && "Invalid range: begin > end"); } - const u8* end() const { - return _end; + constexpr data_view(const u8* data, const isize length) noexcept + : _begin(data), _end(data&& length > 0 ? data + length : data) { + assert(length >= 0 && "Length cannot be negative"); } - u8 operator[](isize i) const { - return *(_begin + i); + // String / String View interoperability + data_view(const std::string& str) noexcept + : _begin(reinterpret_cast(str.data())), + _end(reinterpret_cast(str.data() + str.size())) { } - isize length() const { - return isize(_end - _begin); + constexpr data_view(std::string_view sv) noexcept + : _begin(reinterpret_cast(sv.data())), + _end(reinterpret_cast(sv.data() + sv.size())) { } - const data_view sub_off(isize offset, isize length) const { - return data_view(_begin + offset, _begin + offset + length); + // Construct from char array / C-string with explicit length + constexpr data_view(const char* data, const isize length) noexcept + : data_view(reinterpret_cast(data), length) { } - const data_view sub_abs(isize begin, isize end) const { - return data_view(_begin + begin, _begin + end); + // Interoperability with ckitty::array + data_view(const ckitty::array& arr) noexcept + : _begin(arr.begin()), _end(arr.end()) { + } + + // Default copy/move semantics + constexpr data_view(const data_view&) noexcept = default; + constexpr data_view(data_view&&) noexcept = default; + constexpr data_view& operator=(const data_view&) noexcept = default; + constexpr data_view& operator=(data_view&&) noexcept = default; + ~data_view() = default; + + // Element Access + [[nodiscard]] constexpr u8 operator[](const isize i) const noexcept { + assert(i >= 0 && i < length() && "Index out of bounds"); + return _begin[i]; + } + + [[nodiscard]] constexpr const u8* data() const noexcept { return _begin; } + + // Iterators + [[nodiscard]] constexpr const u8* begin() const noexcept { return _begin; } + [[nodiscard]] constexpr const u8* end() const noexcept { return _end; } + [[nodiscard]] constexpr const u8* cbegin() const noexcept { return _begin; } + [[nodiscard]] constexpr const u8* cend() const noexcept { return _end; } + + // Capacity + [[nodiscard]] constexpr isize length() const noexcept { return static_cast(_end - _begin); } + [[nodiscard]] constexpr isize size() const noexcept { return length(); } + [[nodiscard]] constexpr bool empty() const noexcept { return _begin == _end; } + + // Subviews + /** + * @brief Returns a subview starting at `offset` with `length` bytes. + */ + [[nodiscard]] constexpr data_view sub_off(const isize offset, const isize length) const noexcept { + assert(offset >= 0 && offset <= this->length() && "Offset out of bounds"); + assert(length >= 0 && (offset + length) <= this->length() && "Length out of bounds"); + return data_view(_begin + offset, length); + } + + /** + * @brief Returns a subview in range [_begin + start, _begin + end). + */ + [[nodiscard]] constexpr data_view sub_abs(const isize start, const isize end) const noexcept { + assert(start >= 0 && start <= length() && "Start index out of bounds"); + assert(end >= start && end <= length() && "End index out of bounds"); + return data_view(_begin + start, _begin + end); + } + + // Comparison Operators + [[nodiscard]] friend constexpr bool operator==(const data_view& lhs, const data_view& rhs) noexcept { + if (lhs.length() != rhs.length()) return false; + for (isize i = 0; i < lhs.length(); ++i) { + if (lhs[i] != rhs[i]) return false; + } + return true; + } + + [[nodiscard]] friend constexpr bool operator!=(const data_view& lhs, const data_view& rhs) noexcept { + return !(lhs == rhs); } - }; -} \ No newline at end of file +} // namespace ckitty \ No newline at end of file diff --git a/src/ckitty/memory/iterators.hpp b/src/ckitty/memory/iterators.hpp index bfd4a38..d79f95b 100644 --- a/src/ckitty/memory/iterators.hpp +++ b/src/ckitty/memory/iterators.hpp @@ -1,33 +1,135 @@ #pragma once #include + #include +#include +#include +#include namespace ckitty { namespace it { - template - bool contains(const T& cont, const V& value) { - return std::find(cont.begin(), cont.end(), value) != cont.end(); + namespace detail { + // SFINAE helper to check for member .find() or .contains() + template + struct has_member_find : std::false_type {}; + + template + struct has_member_find().find(std::declval()))>> : std::true_type {}; + + template + struct has_member_erase : std::false_type {}; + + template + struct has_member_erase().erase(std::declval()))>> : std::true_type {}; + } // namespace detail + + /** + * @brief Checks if a container contains a specific value. + * Automatically dispatches to member .find() / .contains() if available (O(1) / O(log N)), + * falling back to standard linear search O(N) for sequence containers. + */ + template + [[nodiscard]] inline bool contains(const Container& cont, const Value& value) { + if constexpr (detail::has_member_find::value) { + return cont.find(value) != cont.end(); + } else { + return std::find(cont.begin(), cont.end(), value) != cont.end(); + } } - template - bool erase(T& cont, const V& value) { + /** + * @brief Checks if any element in a container satisfies a predicate. + */ + template + [[nodiscard]] inline bool contains_if(const Container& cont, Predicate&& pred) { + return std::find_if(cont.begin(), cont.end(), std::forward(pred)) != cont.end(); + } + + /** + * @brief Finds the zero-based index of the first occurrence of a value. + * @return Index if found, or -1 (isize) if not found. + */ + template + [[nodiscard]] inline isize index_of(const Container& cont, const Value& value) { + auto it = std::find(cont.begin(), cont.end(), value); + if (it == cont.end()) return -1; + return static_cast(std::distance(cont.begin(), it)); + } + + /** + * @brief Erases the FIRST occurrence of a value from a sequence container. + * @return true if an element was erased, false otherwise. + */ + template + inline bool erase_first(Container& cont, const Value& value) { auto it = std::find(cont.begin(), cont.end(), value); if (it == cont.end()) return false; cont.erase(it); return true; } - template - bool map_erase(T& map, const V& value) { - auto it = map.find(value); - if (it == map.end()) return false; - map.erase(it); - return true; + /** + * @brief Erases ALL occurrences of a value from a container. + * Dispatches to member .erase(key) for associative maps/sets, or erase-remove for sequences. + * @return Number of elements removed. + */ + template + inline usize erase_all(Container& cont, const Value& value) { + if constexpr (detail::has_member_erase::value) { + return static_cast(cont.erase(value)); + } else { + const auto old_size = cont.size(); + cont.erase(std::remove(cont.begin(), cont.end(), value), cont.end()); + return static_cast(old_size - cont.size()); + } } - } + /** + * @brief Erases elements that satisfy a predicate. + * @return Number of elements removed. + */ + template + inline usize erase_if(Container& cont, Predicate&& pred) { + const auto old_size = cont.size(); + cont.erase(std::remove_if(cont.begin(), cont.end(), std::forward(pred)), cont.end()); + return static_cast(old_size - cont.size()); + } -} + /** + * @brief Specialized single-key erase for maps and sets (single lookup O(log N) / O(1)). + */ + template + inline bool map_erase(Map& map, const Key& key) { + return map.erase(key) > 0; + } + + /** + * @brief Returns true if all elements satisfy the predicate. + */ + template + [[nodiscard]] inline bool all_of(const Container& cont, Predicate&& pred) { + return std::all_of(cont.begin(), cont.end(), std::forward(pred)); + } + + /** + * @brief Returns true if any element satisfies the predicate. + */ + template + [[nodiscard]] inline bool any_of(const Container& cont, Predicate&& pred) { + return std::any_of(cont.begin(), cont.end(), std::forward(pred)); + } + + /** + * @brief Returns true if no elements satisfy the predicate. + */ + template + [[nodiscard]] inline bool none_of(const Container& cont, Predicate&& pred) { + return std::none_of(cont.begin(), cont.end(), std::forward(pred)); + } + + } // namespace it + +} // namespace ckitty \ No newline at end of file diff --git a/src/ckitty/memory/primitives.hpp b/src/ckitty/memory/primitives.hpp index 281daa2..d8f385f 100644 --- a/src/ckitty/memory/primitives.hpp +++ b/src/ckitty/memory/primitives.hpp @@ -1,40 +1,57 @@ #pragma once +#include #include -#include -#include #include +#include +#include #include #include #include -#include +#include namespace ckitty { - typedef std::uint8_t u8; - typedef std::uint16_t u16; - typedef std::uint32_t u32; - typedef std::uint64_t u64; - - typedef std::int8_t i8; - typedef std::int16_t i16; - typedef std::int32_t i32; - typedef std::int64_t i64; + // Fixed-width integer types + using u8 = std::uint8_t; + using u16 = std::uint16_t; + using u32 = std::uint32_t; + using u64 = std::uint64_t; - typedef std::size_t isize; - - typedef float f32; - typedef double f64; + using i8 = std::int8_t; + using i16 = std::int16_t; + using i32 = std::int32_t; + using i64 = std::int64_t; + + // Pointer-sized integer types + using usize = std::size_t; // Unsigned size type + using isize = std::ptrdiff_t; // Signed size type + + // Floating-point types + using f32 = float; + using f64 = double; + + // Common standard library type aliases + using string = std::string; - using std::vector; - using std::map; using std::deque; + using std::map; using std::optional; + using std::vector; + + // Smart pointer type aliases + + template> + using uptr = std::unique_ptr; - template using ptr = std::shared_ptr; - template using uptr = std::unique_ptr; - template using wptr = std::weak_ptr; + template + using sptr = std::shared_ptr; - template using ilist = std::initializer_list; + template + using wptr = std::weak_ptr; -} \ No newline at end of file + // Utility aliases + template + using ilist = std::initializer_list; + +} // namespace ckitty \ No newline at end of file diff --git a/src/ckitty/memory/ptr.hpp b/src/ckitty/memory/ptr.hpp new file mode 100644 index 0000000..9fed180 --- /dev/null +++ b/src/ckitty/memory/ptr.hpp @@ -0,0 +1,145 @@ +#pragma once + +#include +#include +#include +#include + +namespace ckitty { + + /** + * @brief Non-atomic, reference-counted smart pointer (equivalent to Rust's Rc). + * Designed for single-threaded usage or single threads protected by external mutexes. + * + * @tparam T Type of object managed. Supports array types like ptr. + */ + template + class ptr { + public: + using element_type = std::remove_extent_t; + using deleter_type = std::function; + + private: + struct ControlBlock { + element_type* raw_ptr{nullptr}; + std::size_t ref_count{1}; + deleter_type deleter; + + ControlBlock(element_type* p, deleter_type d) + : raw_ptr(p), ref_count(1), deleter(std::move(d)) {} + }; + + ControlBlock* _cb{nullptr}; + + void release() noexcept { + if (_cb) { + --_cb->ref_count; + if (_cb->ref_count == 0) { + if (_cb->deleter) { + _cb->deleter(_cb->raw_ptr); + } else { + if constexpr (std::is_array_v) { + delete[] _cb->raw_ptr; + } else { + delete _cb->raw_ptr; + } + } + delete _cb; + } + _cb = nullptr; + } + } + + public: + constexpr ptr() noexcept = default; + constexpr ptr(std::nullptr_t) noexcept {} + + /** + * @brief Takes ownership of a raw pointer. + * @param raw Pointer to manage. + * @param deleter Custom cleanup function (optional). + */ + explicit ptr(element_type* raw, deleter_type deleter = nullptr) { + if (raw) { + _cb = new ControlBlock(raw, std::move(deleter)); + } + } + + ~ptr() { + release(); + } + + // Copy semantics: non-atomic ref-count increment + ptr(const ptr& other) noexcept : _cb(other._cb) { + if (_cb) { + ++_cb->ref_count; + } + } + + ptr& operator=(const ptr& other) noexcept { + if (this != &other) { + release(); + _cb = other._cb; + if (_cb) { + ++_cb->ref_count; + } + } + return *this; + } + + // Move semantics: transfer ownership without ref-count change + ptr(ptr&& other) noexcept : _cb(other._cb) { + other._cb = nullptr; + } + + ptr& operator=(ptr&& other) noexcept { + if (this != &other) { + release(); + _cb = std::exchange(other._cb, nullptr); + } + return *this; + } + + // Accessors + [[nodiscard]] element_type* get() const noexcept { + return _cb ? _cb->raw_ptr : nullptr; + } + + [[nodiscard]] std::size_t use_count() const noexcept { + return _cb ? _cb->ref_count : 0; + } + + [[nodiscard]] explicit operator bool() const noexcept { + return get() != nullptr; + } + + // Pointer dereference operators + [[nodiscard]] element_type& operator*() const noexcept { + return *get(); + } + + [[nodiscard]] element_type* operator->() const noexcept { + return get(); + } + + // Array indexing operator (enabled only for array types T[]) + template>> + [[nodiscard]] element_type& operator[](const std::ptrdiff_t index) const noexcept { + return get()[index]; + } + + void reset(element_type* raw = nullptr, deleter_type deleter = nullptr) { + release(); + if (raw) { + _cb = new ControlBlock(raw, std::move(deleter)); + } + } + }; + + // Helper function equivalent to std::make_shared + template + [[nodiscard]] ptr make_ptr(Args&&... args) { + return ptr(new T(std::forward(args)...)); + } + +} // namespace ckitty \ No newline at end of file diff --git a/src/ckitty/memory/range.hpp b/src/ckitty/memory/range.hpp new file mode 100644 index 0000000..012f49b --- /dev/null +++ b/src/ckitty/memory/range.hpp @@ -0,0 +1,135 @@ +#pragma once + +#include + +#include +#include +#include +#include + +namespace ckitty { + + /** + * @brief Lazy sequence generator that mimics Python's range() function. + * Evaluates values on the fly without pre-allocating memory. + * + * @tparam T Integral element type (defaults to isize). + */ + template + class range { + static_assert(std::is_integral_v, "ckitty::range requires an integral type"); + + private: + T _start{0}; + T _stop{0}; + T _step{1}; + + public: + class iterator { + public: + using iterator_category = std::forward_iterator_tag; + using value_type = T; + using difference_type = isize; + using pointer = const T*; + using reference = const T&; + + private: + T _current{0}; + T _step{1}; + + public: + constexpr iterator() noexcept = default; + constexpr iterator(const T current, const T step) noexcept + : _current(current), _step(step) {} + + [[nodiscard]] constexpr T operator*() const noexcept { return _current; } + + constexpr iterator& operator++() noexcept { + _current += _step; + return *this; + } + + constexpr iterator operator++(int) noexcept { + iterator temp = *this; + _current += _step; + return temp; + } + + [[nodiscard]] constexpr friend bool operator==(const iterator& lhs, const iterator& rhs) noexcept { + if (lhs._step > 0) return lhs._current >= rhs._current; + if (lhs._step < 0) return lhs._current <= rhs._current; + return true; + } + + [[nodiscard]] constexpr friend bool operator!=(const iterator& lhs, const iterator& rhs) noexcept { + return !(lhs == rhs); + } + }; + + public: + /** + * @brief Construct range(stop) -> [0, stop) with step = 1. + */ + constexpr explicit range(const T stop) noexcept + : _start(0), _stop(stop), _step(1) {} + + /** + * @brief Construct range(start, stop, step = 1). + */ + constexpr range(const T start, const T stop, const T step = 1) noexcept + : _start(start), _stop(stop), _step(step) { + assert(step != 0 && "ckitty::range step cannot be zero"); + } + + // Accessors + [[nodiscard]] constexpr T start() const noexcept { return _start; } + [[nodiscard]] constexpr T stop() const noexcept { return _stop; } + [[nodiscard]] constexpr T step() const noexcept { return _step; } + + // Iterators + [[nodiscard]] constexpr iterator begin() const noexcept { return iterator(_start, _step); } + [[nodiscard]] constexpr iterator end() const noexcept { return iterator(_stop, _step); } + + // Capacity + [[nodiscard]] constexpr isize size() const noexcept { + if (_step > 0 && _start < _stop) { + return static_cast((_stop - _start + _step - 1) / _step); + } + if (_step < 0 && _start > _stop) { + return static_cast((_start - _stop - _step - 1) / (-_step)); + } + return 0; + } + + [[nodiscard]] constexpr isize length() const noexcept { return size(); } + [[nodiscard]] constexpr bool empty() const noexcept { return size() == 0; } + + // Sequence Access + [[nodiscard]] constexpr T operator[](const isize index) const noexcept { + assert(index >= 0 && index < size() && "ckitty::range index out of bounds"); + return static_cast(_start + (index * _step)); + } + + // Membership check (O(1)) + [[nodiscard]] constexpr bool contains(const T value) const noexcept { + if (_step > 0) { + return value >= _start && value < _stop && ((value - _start) % _step == 0); + } + if (_step < 0) { + return value <= _start && value > _stop && ((_start - value) % (-_step) == 0); + } + return false; + } + }; + + // Class Template Argument Deduction (CTAD) + template + range(T) -> range; + + template + range(T, T) -> range; + + template + range(T, T, T) -> range; + +} // namespace ckitty \ No newline at end of file diff --git a/src/ckitty/strings/unicode.hpp b/src/ckitty/strings/unicode.hpp index e69de29..d3953e5 100644 --- a/src/ckitty/strings/unicode.hpp +++ b/src/ckitty/strings/unicode.hpp @@ -0,0 +1,266 @@ +#pragma once + + +#include + +#include +#include + +namespace ckitty { + + namespace unicode { + + // --------------------- // + // UTF-8 Sequence Length // + // --------------------- // + + constexpr isize seqlen(u8 c) { + if ((c & 0x80) == 0x00) return 1; + if ((c & 0xE0) == 0xC0) return 2; + if ((c & 0xF0) == 0xE0) return 3; + if ((c & 0xF8) == 0xF0) return 4; + return 0; + } + + constexpr bool isCont(u8 c) { + return (c & 0xC0) == 0x80; + } + + // ----------------- // + // UTF-8 into UTF-32 // + // ----------------- // + + inline bool decodeArr(const char* src, isize chlen, u32& out) { + // Check character length + if (chlen < 1 || chlen > 4) return false; + + // map of masks, starts at 1 + static constexpr u8 firstMask[5] = { + 0x00, // unused + 0x7F, // 0xxxxxxx + 0x1F, // 110xxxxx + 0x0F, // 1110xxxx + 0x07 // 11110xxx + }; + + u32 result = u8(src[0]) & firstMask[chlen]; + for (isize i = 1; i < chlen; ++i) { + if (!isCont(u8(src[i]))) return false; + result = (result << 6) | (u8(src[i]) & 0x3F); + } + + // Security & Constraints Validation + // So, as it turns out, you can define UTF-8 + // characters to be longer than they should. + // This protects against the (albeit niche) hack + // of sending sensitive chars like "\", "<", etc. + // and have a parser not target them. + // So take this: ALWAYS use UTF-32 to make comparisons! + // Byte-sequences are NOT to be trusted NEVER. + // Still, this ensures it's DOUBLE correct. + if (chlen == 2 && result < 0x80) return false; // Overlong 2-byte + if (chlen == 3 && result < 0x0800) return false; // Overlong 3-byte + if (chlen == 4 && result < 0x10000) return false; // Overlong 4-byte + if (result >= 0xD800 && result <= 0xDFFF) return false; // UTF-16 Surrogates + if (result > 0x10FFFF) return false; // Out of Unicode bounds + + out = result; + return true; + } + + inline isize decode(const char* src, isize len, u32& out) { + if (len <= 0) return 0; + + isize m = seqlen(u8(src[0])); + if (m == 0 || m > len) return 0; + + if (decodeArr(src, m, out)) return m; + return 0; + } + + inline bool toUTF32(std::string_view str, std::u32string& out) { + isize _i = 0; + isize csize = str.size(); + const char* data = str.data(); + + while (_i < csize) { + u32 codepoint; + isize _size = decode(data + _i, csize - _i, codepoint); + if (_size == 0) return false; + + out += char32_t(codepoint); + _i += _size; + } + + return _i == csize; + } + + // ----------------- // + // UTF-32 into UTF-8 // + // ----------------- // + + inline void append_utf32_to_utf8(u32 code_point, std::string& out) { + if (code_point <= 0x7F) { + // 1-byte sequence (ASCII) + out.push_back(static_cast(code_point)); + } else if (code_point <= 0x7FF) { + // 2-byte sequence + out.push_back(static_cast(0xC0 | ((code_point >> 6) & 0x1F))); + out.push_back(static_cast(0x80 | (code_point & 0x3F))); + } else if (code_point <= 0xFFFF) { + // 3-byte sequence + // Filter out surrogate pairs (U+D800 to U+DFFF) as they are invalid Unicode scalar values + if (code_point >= 0xD800 && code_point <= 0xDFFF) { + code_point = 0xFFFD; // Replacement character + } + out.push_back(static_cast(0xE0 | ((code_point >> 12) & 0x0F))); + out.push_back(static_cast(0x80 | ((code_point >> 6) & 0x3F))); + out.push_back(static_cast(0x80 | (code_point & 0x3F))); + } else if (code_point <= 0x10FFFF) { + // 4-byte sequence + out.push_back(static_cast(0xF0 | ((code_point >> 18) & 0x07))); + out.push_back(static_cast(0x80 | ((code_point >> 12) & 0x3F))); + out.push_back(static_cast(0x80 | ((code_point >> 6) & 0x3F))); + out.push_back(static_cast(0x80 | (code_point & 0x3F))); + } else { + // Code point out of Unicode range -> insert UTF-8 replacement character U+FFFD + append_utf32_to_utf8(0xFFFD, out); + } + } + + inline std::string toUTF8(u32 cp) { + std::string s; + append_utf32_to_utf8(cp, s); + return s; + } + + inline std::string toUTF8(const std::u32string& str) { + std::string s; + for (u32 ch : str) append_utf32_to_utf8(ch, s); + return s; + } + + // ----------------- // + // STRINGS // + // ----------------- // + + inline const char* getControlCharName(u8 c) { + static const char* names[32] = { + "NUL", "SOH", "STX", "ETX", "EOT", "ENQ", "ACK", "BEL", + "BS", "HT", "LF", "VT", "FF", "CR", "SO", "SI", + "DLE", "DC1", "DC2", "DC3", "DC4", "NAK", "SYN", "ETB", + "CAN", "EM", "SUB", "ESC", "FS", "GS", "RS", "US" + }; + if (c < 32) return names[c]; + if (c == 127) return "DEL"; + return nullptr; + } + + struct pos { + isize byteoff; + isize line; + isize col; + pos(isize _byteoff = 0, isize _line = 1, isize _col = 1) + : byteoff(_byteoff), line(_line), col(_col) { + } + }; + + inline void hexdump_utf8(const char* data, isize length, pos at, std::ostream& ostr) { + auto old_flags = ostr.flags(); + auto old_fill = ostr.fill(); + + isize i = 0; + while (i < length) { + u8 lead = u8(data[i]); + isize m = seqlen(lead); + + // Byte, Line, Col + ostr << std::setfill('0') << std::hex << std::uppercase; + ostr << "0x" << std::setw(8) << (at.byteoff + i); + + ostr << std::setfill(' ') << std::dec; + ostr << " (" << std::setw(3) << at.line << ", " << std::setw(3) << at.col << ") : "; + + // 1. Invalid Lead Byte handling + if (m == 0) { + ostr << std::setfill('0') << std::hex << std::uppercase; + ostr << "0x" << std::setw(2) << u32(lead); + ostr << std::setw(15) << std::setfill(' ') << " "; // Pad to match normal spacing + ostr << " : INVALID LEAD\n"; + if (lead == '\n') { at.line++; at.col = 1; } else { at.col++; } + i++; + continue; + } + + // 2. Truncated Sequence handling (Not enough bytes left in buffer) + if ((i + m) > length) { + isize available = length - i; + + // Print what we can see + for (isize j = 0; j < available; ++j) { + ostr << "0x" << std::setw(2) << std::setfill('0') << std::hex << int(u8(data[i + j])) << " "; + } + // Fill missing bytes with ?? + for (isize j = available; j < m; ++j) { + ostr << "0x?? "; + } + + // Pad the rest of the column width + isize spaces_to_print = 20 - (m * 5); + if (spaces_to_print) ostr << std::setw(i32(spaces_to_print)) << std::setfill(' ') << " "; + ostr << ": TRUNC SEQ\n"; + + // Update row tracking based on what we actually processed + for (isize j = 0; j < available; ++j) { + if (u8(data[i + j]) == '\n') { at.line++; at.col = 1; } else { at.col++; } + } + i += available; + continue; + } + + // 3. Fully Available Sequence Processing + u32 codepoint = 0; + bool valid = decodeArr(data + i, m, codepoint); + + for (isize j = 0; j < m; ++j) { + ostr << "0x" << std::setw(2) << std::setfill('0') << std::hex << int(u8(data[i + j])) << " "; + } + + isize spaces_to_print = 20 - (m * 5); + if (spaces_to_print) ostr << std::setw(i32(spaces_to_print)) << std::setfill(' ') << " "; + + if (valid) { + ostr << ": U+" << std::setw(5) << std::setfill('0') << std::uppercase << std::hex << codepoint << " "; + + // Check for control characters + const char* ctrlName = getControlCharName(lead); // Multi-byte UTF-8 can't be ASCII control chars + if (m == 1 && ctrlName != nullptr) { + ostr << "(" << ctrlName << ")\n"; + } else { + ostr.write(data + i, i64(m)); + ostr << "\n"; + } + + // Track position updates + if (m == 1 && lead == '\n') { + at.line++; + at.col = 1; + } else { + at.col++; + } + } else { + ostr << ": INVALID SEQ\n"; + at.col++; + } + + i += m; + } + + ostr.flags(old_flags); + ostr.fill(old_fill); + at.byteoff += length; + } + + } + +} \ No newline at end of file diff --git a/src/ckitty/strings/utils.hpp b/src/ckitty/strings/utils.hpp index 8ead2c7..2a6c056 100644 --- a/src/ckitty/strings/utils.hpp +++ b/src/ckitty/strings/utils.hpp @@ -1,78 +1,159 @@ #pragma once #include -#include + #include #include -#include +#include +#include +#include +#include namespace ckitty { namespace strings { - // trim spaces + // --- In-place Trimming --- - inline void ltrim(std::string &s) { - s.erase(s.begin(), std::find_if(s.begin(), s.end(), - [](unsigned char ch) { + /** + * @brief Trims leading whitespace in-place. + */ + inline void ltrim(std::string& s) { + s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) { return !std::isspace(ch); })); } - - inline void rtrim(std::string &s) { - s.erase(std::find_if(s.rbegin(), s.rend(), - [](unsigned char ch) { + + /** + * @brief Trims trailing whitespace in-place. + */ + inline void rtrim(std::string& s) { + s.erase(std::find_if(s.rbegin(), s.rend(), [](unsigned char ch) { return !std::isspace(ch); }).base(), s.end()); } - inline void trim(std::string& str) { - ltrim(str); - rtrim(str); + /** + * @brief Trims leading and trailing whitespace in-place without double shifting. + */ + inline void trim(std::string& s) { + auto first = std::find_if(s.begin(), s.end(), [](unsigned char ch) { + return !std::isspace(ch); + }); + if (first == s.end()) { + s.clear(); + return; + } + auto last = std::find_if(s.rbegin(), s.rend(), [](unsigned char ch) { + return !std::isspace(ch); + }).base(); + + s.assign(first, last); } - inline std::string trimc(const std::string& str) { - std::string copy = str; - trim(copy); - return copy; + // --- Non-mutating Trimming (Returns std::string_view / std::string) --- + + /** + * @brief Returns a string_view with leading and trailing whitespace removed (Zero Allocations). + */ + [[nodiscard]] constexpr inline std::string_view trim_view(std::string_view sv) noexcept { + const auto start = sv.find_first_not_of(" \t\n\r\f\v"); + if (start == std::string_view::npos) return {}; + const auto end = sv.find_last_not_of(" \t\n\r\f\v"); + return sv.substr(start, end - start + 1); } - inline std::string ltrimc(const std::string& str) { - std::string copy = str; - ltrim(copy); - return copy; + [[nodiscard]] inline std::string trimmed(std::string_view sv) { + return std::string(trim_view(sv)); } - inline std::string rtrimc(const std::string& str) { - std::string copy = str; - rtrim(copy); - return copy; + [[nodiscard]] inline std::string ltrimmed(std::string_view sv) { + const auto start = sv.find_first_not_of(" \t\n\r\f\v"); + return (start == std::string_view::npos) ? "" : std::string(sv.substr(start)); } - // ascii upper & lower - - inline void upper(std::string& str) { - std::transform(str.begin(), str.end(), str.begin(), ::toupper); + [[nodiscard]] inline std::string rtrimmed(std::string_view sv) { + const auto end = sv.find_last_not_of(" \t\n\r\f\v"); + return (end == std::string_view::npos) ? "" : std::string(sv.substr(0, end + 1)); } - inline void lower(std::string& str) { - std::transform(str.begin(), str.end(), str.begin(), ::tolower); + // --- Case Conversion --- + + /** + * @brief Converts string to uppercase in-place (ASCII safe). + */ + inline void upper(std::string& str) noexcept { + std::transform(str.begin(), str.end(), str.begin(), [](unsigned char c) { + return static_cast(std::toupper(c)); + }); } - inline std::string copy_upper(const std::string& str) { - std::string copy = str; - upper(copy); - return copy; + /** + * @brief Converts string to lowercase in-place (ASCII safe). + */ + inline void lower(std::string& str) noexcept { + std::transform(str.begin(), str.end(), str.begin(), [](unsigned char c) { + return static_cast(std::tolower(c)); + }); } - inline std::string copy_lower(const std::string& str) { - std::string copy = str; - lower(copy); - return copy; + [[nodiscard]] inline std::string to_upper(std::string_view sv) { + std::string result(sv); + upper(result); + return result; } - // ... + [[nodiscard]] inline std::string to_lower(std::string_view sv) { + std::string result(sv); + lower(result); + return result; + } - } + // --- Common Utilities --- -} \ No newline at end of file + [[nodiscard]] constexpr inline bool starts_with(std::string_view sv, std::string_view prefix) noexcept { + return sv.length() >= prefix.length() && sv.substr(0, prefix.length()) == prefix; + } + + [[nodiscard]] constexpr inline bool ends_with(std::string_view sv, std::string_view suffix) noexcept { + return sv.length() >= suffix.length() && sv.substr(sv.length() - suffix.length()) == suffix; + } + + /** + * @brief Splits a string by a delimiter into a list of string_views. + */ + [[nodiscard]] inline std::vector split(std::string_view sv, char delimiter) { + std::vector tokens; + std::size_t start = 0; + std::size_t end = sv.find(delimiter); + + while (end != std::string_view::npos) { + tokens.push_back(sv.substr(start, end - start)); + start = end + 1; + end = sv.find(delimiter, start); + } + tokens.push_back(sv.substr(start)); + return tokens; + } + + /** + * @brief Joins a container of string-like objects into a single string. + */ + template + [[nodiscard]] inline std::string join(const Container& container, std::string_view delimiter) { + std::string result; + auto it = container.begin(); + if (it != container.end()) { + result += *it; + ++it; + } + for (; it != container.end(); ++it) { + result += delimiter; + result += *it; + } + return result; + } + + } // namespace strings + +} // namespace ckitty \ No newline at end of file diff --git a/src/ckitty/system/chrono.hpp b/src/ckitty/system/chrono.hpp index 890005c..73d0f37 100644 --- a/src/ckitty/system/chrono.hpp +++ b/src/ckitty/system/chrono.hpp @@ -1,42 +1,122 @@ #pragma once #include + #include -#include -#include +#include +#include #include #include +#include +#include namespace ckitty { namespace chrono { - using namespace std::chrono; + namespace detail { + /** + * @brief Thread-safe cross-platform wrapper for localtime. + */ + inline bool safe_localtime(const std::time_t& time, std::tm& result) noexcept { +#if defined(_WIN32) + return localtime_s(&result, &time) == 0; +#else + return localtime_r(&time, &result) != nullptr; +#endif + } + } // namespace detail - inline u64 millis() { - auto now = high_resolution_clock::now().time_since_epoch(); - return duration_cast(now).count(); + // --- Monotonic & System Timestamps --- + + /** + * @brief Returns system wall-clock epoch time in milliseconds. + */ + [[nodiscard]] inline u64 millis() noexcept { + const auto now = std::chrono::system_clock::now().time_since_epoch(); + return static_cast(std::chrono::duration_cast(now).count()); } - inline u64 micros() { - auto now = high_resolution_clock::now().time_since_epoch(); - return duration_cast(now).count(); + /** + * @brief Returns system wall-clock epoch time in microseconds. + */ + [[nodiscard]] inline u64 micros() noexcept { + const auto now = std::chrono::system_clock::now().time_since_epoch(); + return static_cast(std::chrono::duration_cast(now).count()); } - // Date Time String // Could be done better but this is neat too + /** + * @brief Returns monotonic time in nanoseconds (ideal for benchmarks). + */ + [[nodiscard]] inline u64 steady_nanos() noexcept { + const auto now = std::chrono::steady_clock::now().time_since_epoch(); + return static_cast(std::chrono::duration_cast(now).count()); + } - inline std::string dateTimeString(const std::string& format, const std::tm time) { + // --- Date & Time Formatting --- + + /** + * @brief Formats a std::tm structure according to a format string. + */ + [[nodiscard]] inline std::string date_time_string(std::string_view format, const std::tm& time) { std::ostringstream oss; - oss << std::put_time(&time, format.c_str()); + oss << std::put_time(&time, std::string(format).c_str()); return oss.str(); } - inline std::string dateTimeString(const std::string& format = "%d-%m-%Y %H-%M-%S") { + /** + * @brief Formats current local time according to a strftime format string. + */ + [[nodiscard]] inline std::string date_time_string(std::string_view format = "%Y-%m-%d %H:%M:%S") { const std::time_t t = std::time(nullptr); - std::tm tm = *std::localtime(&t); - return dateTimeString(format, tm); + std::tm tm_buf{}; + if (!detail::safe_localtime(t, tm_buf)) { + return ""; + } + return date_time_string(format, tm_buf); } - } + // Backward-compatible camelCase alias + [[nodiscard]] inline std::string dateTimeString(std::string_view format = "%Y-%m-%d %H:%M:%S") { + return date_time_string(format); + } -} \ No newline at end of file + // --- Stopwatch Utility --- + + /** + * @brief High-precision monotonic timer for benchmarking and measuring elapsed time. + */ + class Stopwatch { + private: + std::chrono::steady_clock::time_point _start{std::chrono::steady_clock::now()}; + + public: + constexpr Stopwatch() noexcept = default; + + /// Resets the starting time point to now. + void reset() noexcept { + _start = std::chrono::steady_clock::now(); + } + + /// Returns elapsed time in milliseconds. + [[nodiscard]] double elapsed_ms() const noexcept { + const auto end = std::chrono::steady_clock::now(); + return std::chrono::duration(end - _start).count(); + } + + /// Returns elapsed time in microseconds. + [[nodiscard]] double elapsed_us() const noexcept { + const auto end = std::chrono::steady_clock::now(); + return std::chrono::duration(end - _start).count(); + } + + /// Returns elapsed time in seconds. + [[nodiscard]] double elapsed_sec() const noexcept { + const auto end = std::chrono::steady_clock::now(); + return std::chrono::duration(end - _start).count(); + } + }; + + } // namespace chrono + +} // namespace ckitty \ No newline at end of file diff --git a/src/ckitty/system/environment.hpp b/src/ckitty/system/environment.hpp index bb4b9e4..7a6561e 100644 --- a/src/ckitty/system/environment.hpp +++ b/src/ckitty/system/environment.hpp @@ -2,67 +2,161 @@ #include +#include +#include + +// --- OS & Platform Preprocessor Detection --- + +#if defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(__NT__) + #define CKITTY_OS_WINDOWS 1 + #define CKITTY_DEVICE_DESKTOP 1 +#elif defined(__APPLE__) + #include + #if defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE + #define CKITTY_OS_IOS 1 + #define CKITTY_DEVICE_MOBILE 1 + #elif defined(TARGET_OS_MAC) && TARGET_OS_MAC + #define CKITTY_OS_MACOS 1 + #define CKITTY_DEVICE_DESKTOP 1 + #else + #error "Unknown Apple platform" + #endif +#elif defined(__ANDROID__) + #define CKITTY_OS_ANDROID 1 + #define CKITTY_DEVICE_MOBILE 1 +#elif defined(__linux__) || defined(__unix__) + #define CKITTY_OS_LINUX 1 + #define CKITTY_DEVICE_DESKTOP 1 +#elif defined(__EMSCRIPTEN__) || defined(__wasm__) || defined(__wasm) + #define CKITTY_OS_WASM 1 + #define CKITTY_DEVICE_BROWSER 1 +#else + #error "Unknown operating system or target environment." +#endif + +// --- CPU Architecture Detection --- + +#if defined(__x86_64__) || defined(_M_X64) + #define CKITTY_ARCH_X86_64 1 +#elif defined(__i386__) || defined(_M_IX86) + #define CKITTY_ARCH_X86 1 +#elif defined(__aarch64__) || defined(_M_ARM64) + #define CKITTY_ARCH_ARM64 1 +#elif defined(__arm__) || defined(_M_ARM) + #define CKITTY_ARCH_ARM 1 +#elif defined(__wasm32__) || defined(__wasm64__) + #define CKITTY_ARCH_WASM 1 +#else + #define CKITTY_ARCH_UNKNOWN 1 +#endif + +// --- Build Mode Detection --- + +#if defined(NDEBUG) && !defined(_DEBUG) + #define CKITTY_BUILD_RELEASE 1 +#else + #define CKITTY_BUILD_DEBUG 1 +#endif + namespace ckitty { namespace environment { -#if defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(__NT__) - //define something for Windows (32-bit and 64-bit, this part is common) - // Do not bother to differentiate 32 bits from 64 bits. - inline const std::string OS_NAME = "Windows"; - inline const std::string DEVICE = "Desktop"; - inline const std::string CODE = "desktop/windows"; - #define CKITTY_OS_WINDOWS - #define CKITTY_DEVICE_DESKTOP -#elif __APPLE__ - #include - // iOS, tvOS, or watchOS Simulator - // #elif TARGET_OS_MACCATALYST - // Mac's Catalyst (ports iOS API into Mac, like UIKit). - #if defined(TARGET_IPHONE_SIMULATOR) || defined(TARGET_OS_IPHONE) - // iOS, tvOS, or watchOS device - inline const std::string OS_NAME = "iOS"; - inline const std::string DEVICE = "Mobile"; - inline const std::string CODE = "mobile/ios"; - #define CKITTY_OS_IOS - #define CKITTY_DEVICE_MOBILE - #elif TARGET_OS_MAC - // Other kinds of Apple platforms - inline const std::string OS_NAME = "MacOS"; - inline const std::string DEVICE = "Desktop"; - inline const std::string CODE = "desktop/macos"; - #define CKITTY_OS_MACOS - #define CKITTY_DEVICE_DESKTOP - #else - # error "Unknown Apple platform" - #endif -#elif __ANDROID__ - // Below __linux__ check should be enough to handle Android, - // but something may be unique to Android. - // all unices not caught above - inline const std::string OS_NAME = "Android"; - inline const std::string DEVICE = "Mobile"; - inline const std::string CODE = "mobile/android"; - #define CKITTY_OS_ANDROID - #define CKITTY_DEVICE_MOBILE -#elif defined(__linux__) || defined(__unix__) - // linux / unix - inline const std::string OS_NAME = "Linux"; - inline const std::string DEVICE = "Desktop"; - inline const std::string CODE = "desktop/linux"; - #define CKITTY_OS_LINUX - #define CKITTY_DEVICE_DESKTOP -#elif defined(__EMSCRIPTEN__) || defined(__wasm__) || defined(__wasm) - // WASM - inline const std::string OS_NAME = "WASM"; - inline const std::string DEVICE = "Browser"; - inline const std::string CODE = "browser/wasm"; - #define CKITTY_OS_WASM - #define CKITTY_DEVICE_BROWSER -#else -# error "Unknown compiler/system." + // --- OS String Constants --- + +#if defined(CKITTY_OS_WINDOWS) + constexpr std::string_view OS_NAME = "Windows"; + constexpr std::string_view DEVICE = "Desktop"; + constexpr std::string_view CODE = "desktop/windows"; +#elif defined(CKITTY_OS_IOS) + constexpr std::string_view OS_NAME = "iOS"; + constexpr std::string_view DEVICE = "Mobile"; + constexpr std::string_view CODE = "mobile/ios"; +#elif defined(CKITTY_OS_MACOS) + constexpr std::string_view OS_NAME = "MacOS"; + constexpr std::string_view DEVICE = "Desktop"; + constexpr std::string_view CODE = "desktop/macos"; +#elif defined(CKITTY_OS_ANDROID) + constexpr std::string_view OS_NAME = "Android"; + constexpr std::string_view DEVICE = "Mobile"; + constexpr std::string_view CODE = "mobile/android"; +#elif defined(CKITTY_OS_LINUX) + constexpr std::string_view OS_NAME = "Linux"; + constexpr std::string_view DEVICE = "Desktop"; + constexpr std::string_view CODE = "desktop/linux"; +#elif defined(CKITTY_OS_WASM) + constexpr std::string_view OS_NAME = "WASM"; + constexpr std::string_view DEVICE = "Browser"; + constexpr std::string_view CODE = "browser/wasm"; #endif - } + // --- Compile-Time Boolean Flags (C++17 if constexpr ready) --- -} \ No newline at end of file +#if defined(CKITTY_OS_WINDOWS) + constexpr bool is_windows = true; +#else + constexpr bool is_windows = false; +#endif + +#if defined(CKITTY_OS_MACOS) + constexpr bool is_macos = true; +#else + constexpr bool is_macos = false; +#endif + +#if defined(CKITTY_OS_IOS) + constexpr bool is_ios = true; +#else + constexpr bool is_ios = false; +#endif + +#if defined(CKITTY_OS_LINUX) + constexpr bool is_linux = true; +#else + constexpr bool is_linux = false; +#endif + +#if defined(CKITTY_OS_ANDROID) + constexpr bool is_android = true; +#else + constexpr bool is_android = false; +#endif + +#if defined(CKITTY_OS_WASM) + constexpr bool is_wasm = true; +#else + constexpr bool is_wasm = false; +#endif + + // Device Type Flags + constexpr bool is_desktop = (is_windows || is_macos || is_linux); + constexpr bool is_mobile = (is_ios || is_android); + constexpr bool is_browser = is_wasm; + + // Architecture Flags +#if defined(CKITTY_ARCH_X86_64) + constexpr bool is_x86_64 = true; + constexpr std::string_view ARCH_NAME = "x86_64"; +#else + constexpr bool is_x86_64 = false; +#endif + +#if defined(CKITTY_ARCH_ARM64) + constexpr bool is_arm64 = true; + constexpr std::string_view ARCH_NAME = "arm64"; +#else + constexpr bool is_arm64 = false; +#endif + + // Build Mode Flags +#if defined(CKITTY_BUILD_DEBUG) + constexpr bool is_debug = true; + constexpr bool is_release = false; +#else + constexpr bool is_debug = false; + constexpr bool is_release = true; +#endif + + } // namespace environment + +} // namespace ckitty \ No newline at end of file diff --git a/src/ckitty/system/error.hpp b/src/ckitty/system/error.hpp index 3cc12d1..4ab1ad2 100644 --- a/src/ckitty/system/error.hpp +++ b/src/ckitty/system/error.hpp @@ -1,62 +1,154 @@ #pragma once #include + +#include +#include +#include +#include +#include #include -#include +#include +#include +#include +#include +#include +#include namespace ckitty { - struct error { + // --- Single Stack Frame --- - i32 code; + struct error_frame { + i32 code{0}; std::string message; + std::source_location location; - error(i32 code = 0, const std::string& msg = "") - : code(code), message(msg) { + error_frame(i32 code, std::string msg, std::source_location loc = std::source_location::current()) + : code(code), message(std::move(msg)), location(loc) {} + }; + + // --- Stacked Error Representation --- + + class error { + private: + std::vector _frames; + + public: + error() = default; + + /// Construct a single error frame with automatic source location capture + error(i32 code, std::string message, std::source_location loc = std::source_location::current()) { + _frames.emplace_back(code, std::move(message), loc); } - operator bool() const { - return code; + [[nodiscard]] bool empty() const noexcept { return _frames.empty(); } + [[nodiscard]] explicit operator bool() const noexcept { return !_frames.empty(); } + + [[nodiscard]] i32 code() const noexcept { + return _frames.empty() ? 0 : _frames.front().code; } - operator std::string() const { - std::ostringstream oss; - oss << "[" << std::hex << std::showbase << std::uppercase << std::setw(2) << std::setfill('0') << code << "] "; - oss << (message.empty() ? "(no info)" : message); - return oss.str(); - } + [[nodiscard]] const std::vector& frames() const noexcept { return _frames; } - error& operator+=(const error& err) { - auto err1 = (*this) + err; - code = err1.code; - message = err1.message; + /// Pushes a new contextual frame to the top of the stack + error& context(std::string_view message, std::source_location loc = std::source_location::current()) { + _frames.emplace_back(code(), std::string(message), loc); return *this; } - error operator+(const error& err) const { - // If this error has no code (no error), return the other error - if (code == 0) { - return err; + /// Formats the error stack into a multi-line visual trace + [[nodiscard]] std::string to_string() const { + if (_frames.empty()) return "[OK] No Error"; + + std::ostringstream oss; + oss << "Error Stack Trace (Root Code: 0x" << std::hex << code() << std::dec << "):\n"; + + for (size_t i = 0; i < _frames.size(); ++i) { + const auto& f = _frames[i]; + oss << " [" << i << "] " << f.message << "\n"; + oss << " at " << f.location.file_name() << ":" + << f.location.line() << " in " << f.location.function_name() << "\n"; } - - // If the other error has no code, return this error - if (err.code == 0) { - return *this; - } - - // Both errors have codes, combine them - error result = *this; - - // Combine messages - if (!result.message.empty()) { - result.message += "\n "; - } - - // add next error to the message - result.message += err.operator std::string(); - return result; + return oss.str(); } + explicit operator std::string() const { return to_string(); } }; -} \ No newline at end of file + // --- Failable (Expected) Template --- + + struct unexpected { + error err; + explicit unexpected(error e) : err(std::move(e)) {} + explicit unexpected(i32 code, std::string message) : err(code, message) {} + }; + + template + class failable { + private: + std::variant _storage; + + public: + // Success constructors + constexpr failable(const T& val) : _storage(val) {} + constexpr failable(T&& val) noexcept(std::is_nothrow_move_constructible_v) + : _storage(std::move(val)) {} + + // Failure constructors + constexpr failable(error err) : _storage(std::move(err)) {} + constexpr failable(unexpected unex) : _storage(std::move(unex.err)) {} + + // Status queries + [[nodiscard]] bool has_value() const noexcept { return std::holds_alternative(_storage); } + [[nodiscard]] explicit operator bool() const noexcept { return has_value(); } + + // Value Access + [[nodiscard]] T& value() & { return std::get(_storage); } + [[nodiscard]] const T& value() const& { return std::get(_storage); } + [[nodiscard]] T&& value() && { return std::get(std::move(_storage)); } + + [[nodiscard]] T value_or(T&& fallback) const& { + return has_value() ? std::get(_storage) : std::forward(fallback); + } + + // Error Access + [[nodiscard]] error& error_cause() & { return std::get(_storage); } + [[nodiscard]] const error& error_cause() const& { return std::get(_storage); } + + /// Attaches context to the contained error if in a failed state + failable& context(std::string_view msg, std::source_location loc = std::source_location::current()) { + if (!has_value()) { + std::get(_storage).context(msg, loc); + } + return *this; + } + }; + + // --- Void Specialization --- + + template<> + class failable { + private: + std::optional _err; + + public: + constexpr failable() noexcept = default; + constexpr failable(error err) : _err(std::move(err)) {} + failable(unexpected unex) : _err(std::move(unex.err)) {} + + [[nodiscard]] bool has_value() const noexcept { return !_err.has_value(); } + [[nodiscard]] explicit operator bool() const noexcept { return has_value(); } + + [[nodiscard]] const error& error_cause() const { return *_err; } + + failable& context(std::string_view msg, std::source_location loc = std::source_location::current()) { + if (_err) { + _err->context(msg, loc); + } + return *this; + } + }; + +} // namespace ckitty +