From a8b09001f77229acf7a75322a4e5f17a59ac5eb7 Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Tue, 10 Oct 2023 02:35:43 -0700 Subject: [PATCH] finish doxygen docs. This also includes a lot of code cleanups along the way. --- .clang-tidy | 33 +++++++++ .gitignore | 2 + Arena.cc | 49 ++++++++----- Arena.h | 172 ++++++++++++++++++++++++++++++++++------------ Buffer.cc | 25 +++---- Buffer.h | 24 ++----- CMakeDocs.txt | 3 + CMakeLists.txt | 77 ++++++++++++--------- CMakePack.txt | 1 - Dictionary.cc | 74 ++++++++++---------- Dictionary.h | 108 +++++++++++++++++++++++++---- TLV.cc | 28 +++++--- TLV.h | 120 ++++++++++++++++++++++++++------ Test.cc | 2 +- Test.h | 34 ++++++--- dictionaryTest.cc | 17 ++--- klib.h | 39 +++++++++++ klib.pc.in | 10 +++ testFixtures.h | 12 +++- 19 files changed, 596 insertions(+), 234 deletions(-) create mode 100644 .clang-tidy create mode 100644 klib.h create mode 100644 klib.pc.in diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..1b80d39 --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,33 @@ +Checks: >- + bugprone-*, + cppcoreguidelines-*, + google-*, + misc-*, + modernize-*, + performance-*, + readability-*, + -bugprone-lambda-function-name, + -bugprone-reserved-identifier, + -cppcoreguidelines-avoid-goto, + -cppcoreguidelines-avoid-magic-numbers, + -cppcoreguidelines-avoid-non-const-global-variables, + -cppcoreguidelines-pro-bounds-array-to-pointer-decay, + -cppcoreguidelines-pro-type-vararg, + -google-readability-braces-around-statements, + -google-readability-function-size, + -misc-no-recursion, + -modernize-return-braced-init-list, + -modernize-use-nodiscard, + -modernize-use-trailing-return-type, + -performance-unnecessary-value-param, + -readability-magic-numbers, + +CheckOptions: + - key: readability-function-cognitive-complexity.Threshold + value: 100 + - key: readability-function-cognitive-complexity.IgnoreMacros + value: true + # Set naming conventions for your style below (there are dozens of naming settings possible): + # See https://clang.llvm.org/extra/clang-tidy/checks/readability/identifier-naming.html + - key: readability-identifier-naming.ClassCase + value: CamelCase diff --git a/.gitignore b/.gitignore index b4281c2..b34b1e7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,12 @@ .idea +.trunk .vc .vscode *.a *.o *.bin +*.pc build core core.* diff --git a/Arena.cc b/Arena.cc index 4910b43..7a3aafa 100644 --- a/Arena.cc +++ b/Arena.cc @@ -26,7 +26,7 @@ namespace klib { Arena::Arena() - : store(nullptr), size(0), fd(0), arenaType(ARENA_UNINIT) + : store(nullptr), size(0), fd(0), arenaType(ArenaType::Uninit) {} @@ -39,20 +39,20 @@ Arena::~Arena() void Arena::Initialize() { - assert(this->arenaType != ARENA_UNINIT); + assert(this->arenaType != ArenaType::Uninit); this->store = nullptr; this->size = 0; - this->arenaType = ARENA_UNINIT; + this->arenaType = ArenaType::Uninit; this->fd = 0; } int -Arena::SetStatic(uint8_t *mem, size_t allocSize) +Arena::SetStatic(uint8_t *mem, size_t memSize) { this->store = mem; - this->size = allocSize; - this->arenaType = ARENA_STATIC; + this->size = memSize; + this->arenaType = ArenaType::Static; return 0; } @@ -64,7 +64,7 @@ Arena::SetAlloc(size_t allocSize) this->Destroy(); } - this->arenaType = ARENA_ALLOC; + this->arenaType = ArenaType::Alloc; this->size = allocSize; this->store = new uint8_t[allocSize]; if (this->store == nullptr) { @@ -85,7 +85,7 @@ Arena::MemoryMap(int memFileDes, size_t memSize) this->Destroy(); } - this->arenaType = ARENA_MMAP; + this->arenaType = ArenaType::MemoryMapped; this->size = memSize; this->store = (uint8_t *) mmap(NULL, memSize, PROT_RW, MAP_SHARED, memFileDes, 0); @@ -224,18 +224,18 @@ Arena::Clear() void Arena::Destroy() { - if (this->arenaType == ARENA_UNINIT) { + if (this->arenaType == ArenaType::Uninit) { return; } switch (this->arenaType) { - case ARENA_STATIC: + case ArenaType::Static: break; - case ARENA_ALLOC: + case ArenaType::Alloc: delete this->store; break; #if defined(__linux__) - case ARENA_MMAP: + case ArenaType::MemoryMapped: if (munmap(this->store, this->size) == -1) { abort(); return; @@ -257,7 +257,7 @@ Arena::Destroy() } - this->arenaType = ARENA_UNINIT; + this->arenaType = ArenaType::Uninit; this->size = 0; this->store = nullptr; return; @@ -266,24 +266,24 @@ Arena::Destroy() std::ostream & operator<<(std::ostream &os, Arena &arena) { - auto cursor = arena.Store(); + auto cursor = arena.NewCursor(); char cursorString[33] = {0}; snprintf(cursorString, 32, "%#016llx", (long long unsigned int)cursor); os << "Arena<"; switch (arena.Type()) { - case ARENA_UNINIT: + case ArenaType::Uninit: os << "uninitialized"; break; - case ARENA_STATIC: + case ArenaType::Static: os << "static"; break; - case ARENA_ALLOC: + case ArenaType::Alloc: os << "allocated"; break; #if defined(__linux__) - case ARENA_MMAP: + case ArenaType::MemoryMapped: os << "mmap/file"; break; #endif @@ -328,5 +328,18 @@ Arena::Write(const char *path) return retc; } +uint8_t & +Arena::operator[](size_t index) +{ + if (index > this->size) { +#if defined(DESKTOP_BUILD) and !defined(KLIB_NO_ASSERT) + throw std::range_error("index out of range"); +#else + abort(); +#endif + } + return this->store[index]; +} + } // namespace klib diff --git a/Arena.h b/Arena.h index eeabbd3..87e7c53 100644 --- a/Arena.h +++ b/Arena.h @@ -4,24 +4,13 @@ /// \date 2023-10-06 /// \brief Memory management using an arena. /// -/// \section COPYRIGHT -/// Copyright 2023 K. Isom -/// -/// Permission to use, copy, modify, and/or distribute this software for -/// any purpose with or without fee is hereby granted, provided that the -/// above copyright notice and this permission notice appear in all copies. -/// -/// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL -/// WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED -/// WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR -/// BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES -/// OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, -/// WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, -/// ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS -/// SOFTWARE. -/// -/// \section DESCRIPTION /// Arena defines a memory management backend for pre-allocating memory. +/// +/// \section PLATFORM SUPPORT +/// +/// Arena will build on the major platforms, but memory-mapped files are only +/// supported on Unix-like systems. File I/O on Windows, for example, reads the +/// file into an allocated arena. See Arena::Open for more details. #ifndef KIMODEM_ARENA_H @@ -37,71 +26,156 @@ namespace klib { -/// \enum ArenaType describes the type of #Arena. -enum ArenaType : uint8_t { - /// ARENA_UNINIT is an unintialized arena. - ARENA_UNINIT, - /// ARENA_STATIC is an arena backed by a static block of memory. - ARENA_STATIC, - /// ARENA_ALLOC is an arena backed by allocated memory. - ARENA_ALLOC, - /// ARENA_MMAP is an arena backed by a memory-mapped file. - ARENA_MMAP, +/// \enum ArenaType +/// +/// ArenaType describes the type of \class Arena. +enum class ArenaType : uint8_t { + /// Uninit is an unintialized arena. + Uninit, + /// Static is an arena backed by a static block of memory. + Static, + /// Alloc is an arena backed by allocated memory. + Alloc, + /// MemoryMapped is an arena backed by a memory-mapped file. + MemoryMapped, }; /// Arena is the class that implements a memory arena. +/// +/// The Arena uses the concept of a cursor to point to memory in the arena. The +/// #NewCursor and #End methods return pointers to the start and end of the +/// arena memory. +/// +/// The arena should be initalized with one of the Set methods (SetStatic, +/// SetAlloc) or one of the file-based options (Create, Open, MemoryMap). At +/// this point, no further memory management should be done until the end of the +/// arena's life, at which point Destroy should be called. class Arena { public: + /// An Arena is initialized with no backing memory. Arena(); ~Arena(); - /* - * InitializeArena is intended for use only with systems that - * do not initialize new variables to zero. It should be called - * exactly once, at the start of the program. Any other time the - * arena needs to be reset, it should be called with Clear or - * Destroy. - */ + /// Initialize is intended for use only with systems that do not + /// initialize new variables to zero. It should be called exactly once, + /// at the start of the program. Any other time the arena needs to be + /// reset, it should be called with #Clear or #Destroy. void Initialize(); - int SetStatic(uint8_t *, size_t); + /// SetStatic points the arena to a chunk of memory. That memory is + /// intended to be statically allocated, e.g. via a global `static + /// uint8_t memory[memSize];`. If the arena is already backed, then + /// #Destroy will be called first. + /// + /// \param mem A pointer to a section of memory, preferably statically + /// allocated. + /// \param memSize The size of the memory section. + /// \return Returns 0 on success and -1 on error. + int SetStatic(uint8_t *mem, size_t memSize); + /// SetAlloc allocates a chunk of memory for the arena; the arena takes + /// ownership. If the arena is already backed, then #Destroy will be + /// called first. + /// + /// \param allocSize The size of memory to allocate. + /// \return Returns 0 on success and -1 on error. int SetAlloc(size_t allocSize); #if defined(__linux__) + /// MemoryMap points the arena to a memory-mapped file. This is + /// currently only supported on Linux. If the arena is already backed, + /// then #Destroy will be called first. + /// + /// \param memFileDes File descriptor to map into memory. + /// \param memSize The size of memory to map. + /// \return Returns 0 on success and -1 on error. int MemoryMap(int memFileDes, size_t memSize); // Arena will own fd. + + /// Create creates a new file, truncating it if it already exists. On + /// Unix-based platforms, the arena will be backed by a memory via + /// #MemoryMap. On other platforms (e.g. Windows), the arena will read + /// the file into an allocated arena, calling #SetAlloc. + /// + /// \param path The path to the file that should be created. + /// \param fileSize The size of the file to create. + /// \param mode The permissions to load. + /// \return Returns 0 on success and -1 on error. int Create(const char *path, size_t fileSize, mode_t mode); + + /// Open reads a file into the arena; the file must already exist. On + /// Unix-based platforms, the arena will be backed by a memory via + /// #MemoryMap. On other platforms (e.g. Windows), the arena will read + /// the file into an allocated arena, calling #SetAlloc. On these + /// platforms, in order to persist changes, #Write must be called to + /// sync changes to disk. + /// + /// \param path The path to the file to be loaded. + /// \return Returns 0 on success and -1 on error. int Open(const char *path); #elif defined(__WIN64__) || defined(__WIN32__) int Open(const char *path); #endif + /// NewCursor returns a pointer to the start of the memory in the arena. + /// + /// \return A pointer to the start of the arena memory. uint8_t *NewCursor() const { return this->store; } + + /// End returns a pointer to the end of the arena memory. + /// + /// \return A pointer to the end of the arena memory. uint8_t *End() { return this->store + this->size; } + + /// CursorInArena checks whether the cursor is still in the arena. + /// + /// \param cursor A pointer that ostensibly points to the arena's + /// memory. + /// \return True if the cursor is still in the arena. bool CursorInArena(const uint8_t *cursor); + /// Returns the current size of the arena. + /// + /// \return The size of the arena. size_t Size() const { return this->size; } - uint8_t Type() const + /// Type returns an ArenaType describing the arena. + /// + /// \return An ArenaType describing the backing memory for the arena. + ArenaType Type() const { return this->arenaType; } - uintptr_t Store() { return (uintptr_t)this->store; } + /// Ready returns whether the arena is initialized. + bool Ready() const { return this->Type() != ArenaType::Uninit; }; - bool Ready() const { return this->Type() != ARENA_UNINIT; }; + /// Clear zeroizes the memory in the arena. void Clear(); - void Destroy(); /* dispose of any memory used by arena */ - /* - * DANGER: if arena is file backed (mmap or open), DO NOT WRITE TO THE - * BACKING FILE! - */ + /// Destroy removes any backing memory (e.g. from SetAlloc or + /// MemoryMap). This does not call Clear; if the arena was backed by a + /// file that should be persisted, it would wipe out the file. + void Destroy(); + + /// Write dumps the arena to a file suitable for loading by Open. + /// + /// \warning DANGER: if arena is memory-mapped, DO NOT WRITE TO THE + /// BACKING FILE! + /// + /// \param path + /// \return Returns 0 on success and -1 on error. int Write(const char *path); - uint8_t &operator[](size_t index) - { return this->store[index]; } + /// This operator allows the data in the arena to be accessed + /// as if it were an array. If the index is out of bounds, it + /// will throw a range_error. + /// + /// \throws std::range_error. + /// + /// \param index The index to retrieve. + /// \return + uint8_t &operator[](size_t index); private: uint8_t *store; @@ -111,6 +185,14 @@ private: }; +/// Write an Arena out to the output stream. +/// +/// The resulting output looks something like +/// Arena@0x7fff91dfad70,store<128B>@0x0055d6c5881ec0. +/// +/// \param os +/// \param arena +/// \return std::ostream &operator<<(std::ostream& os, Arena &arena); diff --git a/Buffer.cc b/Buffer.cc index 64fafe0..e73710a 100644 --- a/Buffer.cc +++ b/Buffer.cc @@ -3,22 +3,7 @@ /// \author K. Isom /// \date 2023-10-09 /// -/// \section COPYRIGHT -/// Copyright 2023 K. Isom -/// -/// Permission to use, copy, modify, and/or distribute this software for -/// any purpose with or without fee is hereby granted, provided that the -/// above copyright notice and this permission notice appear in all copies. -/// -/// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL -/// WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED -/// WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR -/// BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES -/// OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, -/// WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, -/// ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS -/// SOFTWARE. -/// + #include #include @@ -31,7 +16,11 @@ namespace klib { +/// The defaultCapacity for a new Buffer is a reasonably arbitrary starting +/// point. constexpr size_t defaultCapacity = 32; +/// maxReasonableLine is the longest a reasonable line could be. It assumes +/// something like a long, unprettified JSON strong or the like. constexpr size_t maxReasonableLine = 8192; @@ -323,7 +312,11 @@ uint8_t & Buffer::operator[](size_t index) { if (index > this->length) { +#if defined(DESKTOP_BUILD) and !defined(KLIB_NO_ASSERT) throw std::range_error("array index out of bounds"); +#else + abort(); +#endif } return this->contents[index]; } diff --git a/Buffer.h b/Buffer.h index 37ceb25..1827c83 100644 --- a/Buffer.h +++ b/Buffer.h @@ -1,26 +1,9 @@ /// -/// \file Buffer.hcc +/// \file Buffer.h /// \author K. Isom /// \date 2023-10-09 /// \brief Buffer implements basic line buffers. /// -/// \section COPYRIGHT -/// Copyright 2023 K. Isom -/// -/// Permission to use, copy, modify, and/or distribute this software for -/// any purpose with or without fee is hereby granted, provided that the -/// above copyright notice and this permission notice appear in all copies. -/// -/// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL -/// WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED -/// WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR -/// BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES -/// OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, -/// WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, -/// ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS -/// SOFTWARE. -/// -/// \section DESCRIPTION /// Buffer implements a basic uint8_t line buffer that is intended for use in text /// editing. It allocates memory in powers of two, and will grow or shrink /// as needed. @@ -221,8 +204,11 @@ private: bool autoTrim; }; - +/// The << operator is overloaded to write out the contents of the Buffer. std::ostream &operator<<(std::ostream &os, const Buffer &buf); + +/// Two Buffers are not equal if their lengths differ or if their contents +/// differ. inline bool operator!=(const Buffer &lhs, const Buffer &rhs) { return !(lhs == rhs); }; } // namespace klib diff --git a/CMakeDocs.txt b/CMakeDocs.txt index d8f0912..9a2a703 100644 --- a/CMakeDocs.txt +++ b/CMakeDocs.txt @@ -2,6 +2,9 @@ find_package(Doxygen) if (${DOXYGEN_FOUND}) +set(DOXYGEN_GENERATE_MAN YES) +set(DOXYGEN_GENERATE_LATEX YES) +#set(DOXYGEN_EXTRACT_ALL YES) doxygen_add_docs(klib_docs ${HEADER_FILES} ${SOURCE_FILES}) diff --git a/CMakeLists.txt b/CMakeLists.txt index 65a5821..1d9000a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,43 +1,42 @@ cmake_minimum_required(VERSION 3.25) -project(klib LANGUAGES CXX VERSION 0.0.1) +project(klib LANGUAGES CXX + VERSION 0.0.1 + DESCRIPTION "Kyle's C++ library") set(CMAKE_CXX_STANDARD 14) -if(MSVC) - add_compile_options("/W4" "$<$:/O2>") -else() - add_compile_options("-Wall" "-Wextra" "-Werror" "$<$:-O3>") - if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") - add_compile_options("-stdlib=libc++") - else() - # nothing special for gcc at the moment - endif() -endif() +if (MSVC) + add_compile_options("/W4" "$<$:/O2>") +else () + add_compile_options("-Wall" "-Wextra" "-Werror" "$<$:-O3>") + if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") + add_compile_options("-stdlib=libc++") + else () + # nothing special for gcc at the moment + endif () +endif () add_compile_options("-DDESKTOP_BUILD") set(HEADER_FILES - Arena.h - Buffer.h - Dictionary.h - Test.h - TLV.h) + klib.h + Arena.h + Buffer.h + Dictionary.h + Test.h + TLV.h) set(SOURCE_FILES - Arena.cc - Buffer.cc - Dictionary.cc - Test.cc - TLV.cc) + Arena.cc + Buffer.cc + Dictionary.cc + Test.cc + TLV.cc) add_library(klib STATIC - Arena.cc - Buffer.cc - Dictionary.cc - TLV.cc -) -install(TARGETS klib LIBRARY DESTINATION ${PREFIX}/lib) -install(FILES ${HEADER_FILES} DESTINATION include/klib) -install(FILES klibConfig.cmake DESTINATION share/klib/cmake) + Arena.cc + Buffer.cc + Dictionary.cc + TLV.cc) include(CTest) enable_testing() @@ -56,10 +55,22 @@ add_test(bufferTest buffer_test) include(CMakePackageConfigHelpers) write_basic_package_version_file( - klibConfig.cmake - VERSION ${PACKAGE_VERSION} - COMPATIBILITY AnyNewerVersion + klibConfig.cmake + VERSION ${PACKAGE_VERSION} + COMPATIBILITY AnyNewerVersion ) +add_custom_target(cloc + COMMAND cloc ${SOURCE_FILES} ${HEADER_FILES} + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}) + +configure_file(klib.pc.in klib.pc @ONLY) +install(TARGETS klib LIBRARY DESTINATION ${PREFIX}/lib) +install(FILES ${HEADER_FILES} DESTINATION include/{klib}) +install(FILES klibConfig.cmake DESTINATION share/klib/cmake) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/klib.pc DESTINATION lib/pkgconfig) +install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/html DESTINATION share/doc/klib) +install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/man DESTINATION share/man) + include(CMakePack.txt) -include(CMakeDocs.txt) \ No newline at end of file +include(CMakeDocs.txt) diff --git a/CMakePack.txt b/CMakePack.txt index b7fefb2..cd98b69 100644 --- a/CMakePack.txt +++ b/CMakePack.txt @@ -7,7 +7,6 @@ set(CPACK_PACKAGE_VERSION_MAJOR ${PROJECT_VERSION_MAJOR}) set(CPACK_PACKAGE_VERSION_MINOR ${PROJECT_VERSION_MINOR}) set(CPACK_PACKAGE_VERSION_PATCH ${PROJECT_VERSION_PATCH}) - # Debian settings set(CPACK_DEBIAN_PACKAGE_MAINTAINER "K. Isom") set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "The Shimmering Clarity C++ library") diff --git a/Dictionary.cc b/Dictionary.cc index cf8504d..2d2e5dc 100644 --- a/Dictionary.cc +++ b/Dictionary.cc @@ -6,13 +6,17 @@ #include #endif + +namespace klib { + + bool Dictionary::Lookup(const char *key, uint8_t klen, TLV::Record &res) { res.Tag = this->kTag; - uint8_t *cursor = TLV::FindTag(this->arena, NULL, res); + uint8_t *cursor = TLV::FindTag(this->arena, nullptr, res); - while (cursor != NULL) { + while (cursor != nullptr) { if ((klen == res.Len) && (memcmp(res.Val, key, klen) == 0)) { TLV::ReadFromMemory(res, cursor); @@ -33,11 +37,11 @@ int Dictionary::Set(const char *key, uint8_t klen, const char *val, uint8_t vlen) { TLV::Record rec; - uint8_t *cursor = NULL; + uint8_t *cursor = nullptr; SetRecord(rec, this->kTag, klen, key); cursor = this->seek(key, klen); - if (cursor != NULL) { + if (cursor != nullptr) { TLV::DeleteRecord(this->arena, cursor); TLV::DeleteRecord(this->arena, cursor); } @@ -46,13 +50,13 @@ Dictionary::Set(const char *key, uint8_t klen, const char *val, uint8_t vlen) return -1; } - cursor = TLV::WriteToMemory(this->arena, NULL, rec); - if (cursor == NULL) { + cursor = TLV::WriteToMemory(this->arena, nullptr, rec); + if (cursor == nullptr) { return -1; } SetRecord(rec, this->vTag, vlen, val); - if (TLV::WriteToMemory(this->arena, NULL, rec) == NULL) { + if (TLV::WriteToMemory(this->arena, nullptr, rec) == nullptr) { return -1; } @@ -67,9 +71,9 @@ Dictionary::seek(const char *key, uint8_t klen) TLV::Record rec; rec.Tag = this->kTag; - uint8_t *cursor = TLV::LocateTag(this->arena, NULL, rec); + uint8_t *cursor = TLV::LocateTag(this->arena, nullptr, rec); - while (cursor != NULL) { + while (cursor != nullptr) { if ((klen == rec.Len) && (this->kTag == rec.Tag)) { if (memcmp(rec.Val, key, klen) == 0) { return cursor; @@ -79,14 +83,14 @@ Dictionary::seek(const char *key, uint8_t klen) cursor = TLV::LocateTag(this->arena, cursor, rec); } - return NULL; + return nullptr; } bool -Dictionary::Has(const char *key, uint8_t klen) +Dictionary::Contains(const char *key, uint8_t klen) { - return this->seek(key, klen) != NULL; + return this->seek(key, klen) != nullptr; } @@ -95,10 +99,10 @@ Dictionary::spaceAvailable(uint8_t klen, uint8_t vlen) { size_t required = 0; uintptr_t remaining = 0; - uint8_t *cursor = NULL; + uint8_t *cursor = nullptr; - cursor = TLV::FindEmpty(this->arena, NULL); - if (cursor == NULL) { + cursor = TLV::FindEmpty(this->arena, nullptr); + if (cursor == nullptr) { return false; } @@ -111,41 +115,39 @@ Dictionary::spaceAvailable(uint8_t klen, uint8_t vlen) } -#if defined(DESKTOP_BUILD) -void -Dictionary::DumpKVPairs() +std::ostream & +operator<<(std::ostream &os, const Dictionary &dictionary) { - uint8_t *cursor = (this->arena).NewCursor(); +#if defined(DESKTOP_BUILD) + uint8_t *cursor = (dictionary.arena).NewCursor(); TLV::Record rec; TLV::ReadFromMemory(rec, cursor); - std::cout << "Dictionary KV pairs" << std::endl; - if (rec.Tag == TAG_EMPTY) { - std::cout << "\t(NONE)" << std::endl; - return; + os << "Dictionary KV pairs" << std::endl; + if (rec.Tag == TLV::TAG_EMPTY) { + os << "\t(NONE)" << std::endl; + return os; } - while ((cursor != NULL) && (rec.Tag != TAG_EMPTY)) { - std::cout << "\t" << rec.Val << "->"; + while ((cursor != nullptr) && (rec.Tag != TLV::TAG_EMPTY)) { + os << "\t" << rec.Val << "->"; cursor = TLV::SkipRecord(rec, cursor); TLV::ReadFromMemory(rec, cursor); - std::cout << rec.Val << std::endl; + os << rec.Val << std::endl; cursor = TLV::SkipRecord(rec, cursor); TLV::ReadFromMemory(rec, cursor); } - -} -#else -void -Dictionary::DumpKVPairs() -{ - -} #endif + return os; +} -void + +int Dictionary::DumpToFile(const char *path) { - this->arena.Write(path); + return this->arena.Write(path); } + + +} // namespace klib \ No newline at end of file diff --git a/Dictionary.h b/Dictionary.h index 5dec825..412f43c 100644 --- a/Dictionary.h +++ b/Dictionary.h @@ -1,3 +1,10 @@ +/// +/// \file klib.h +/// \author kyle +/// \date 2023-10-06 +/// + + #ifndef KLIB_DICTIONARY_H #define KLIB_DICTIONARY_H @@ -6,39 +13,110 @@ #include "TLV.h" -#define DICTIONARY_TAG_KEY 1 -#define DICTIONARY_TAG_VAL 2 +static constexpr uint8_t DICTIONARY_TAG_KEY = 1; +static constexpr uint8_t DICTIONARY_TAG_VAL = 2; + + +namespace klib { /* * A Dictionary is a collection of key-value pairs, similar to how * a dictionary is a mapping of names to definitions. */ +/// Dictionary implements a key-value store on top of Arena and TLV::Record. +/// +/// phonebook of SSIDs and WPA keys on a microcontroller. This phonebook had to +/// be stored in persistent NVRAM storage, preëmpting the use of std::map or +/// similar. The hardware in use was also not conducive to more expensive +/// options. It was originally named Phonebook until it was adapted to a more +/// general-purpose data structure. +/// +/// Keys and vales are stored as sequential pairs of TLV records; they are +/// expected to contain string values but this isn't necessarily the case. The +/// tag values default to a tag of DICTIONARY_TAG_KEY, and values to a tag of +/// DICTIONARY_TAG_VAL. class Dictionary { public: + /// A Dictionary can be initialized with just a backing Arena. + /// + /// \param arena The backing arena for the Dictionary. Dictionary(Arena &arena) : arena(arena), kTag(DICTIONARY_TAG_KEY), - vTag(DICTIONARY_TAG_VAL) {} ; + vTag(DICTIONARY_TAG_VAL) + {}; + + /// A Dictionary can also be configured with custom key and value types. + /// + /// \param arena The backing arena for the Dictionary. + /// \param kt The value to use for key tags. + /// \param vt The value to use for val tags. Dictionary(Arena &arena, uint8_t kt, uint8_t vt) : arena(arena), kTag(kt), - vTag(vt) {}; + vTag(vt) + {}; - bool Lookup(const char *key, uint8_t klen, TLV::Record &res); - int Set(const char *key, uint8_t klen, const char *val, - uint8_t vlen); - bool Has(const char *key, uint8_t klen); - void DumpKVPairs(); - void DumpToFile(const char *path); + /// Lookup checks to see if the Dictionary has a value under key. + /// + /// \param key The key to search for. + /// \param klen The length of the key. + /// \param res The TLV::Record to store the value in; + /// \return True if the key was found, false otherwise. + bool Lookup(const char *key, uint8_t klen, TLV::Record &res); + + /// Set adds a pairing for key → value in the Dictionary. + /// + /// If the key is already present in the dictionary, both the + /// key and value are deleted, and a new pair is insert. + /// + /// \warning If the key is present, but there isn't enough space to + /// store the new Val, the Dictionary will not contain either key + /// or value. + /// + /// + /// \param key The key to associate. + /// \param klen The length of the key. + /// \param val The value to associate. + /// \param vlen The length of the value. + /// \return Returns 0 on success and -1 on failure. + int Set(const char *key, uint8_t klen, const char *val, + uint8_t vlen); + + /// Contains checks the dictionary to see if it contains a given key. + /// + /// \param key The key to look up. + /// \param klen The length of the key. + /// \return True if the key is in the Dictionary, otherwise false. + bool Contains(const char *key, uint8_t klen); + + + /// DumpToFile is a wrapper aorund a call to Arena::Write on the + /// underlying Arena. + /// + /// \param path The path to the dumped file. + /// \return 0 on success, -1 on failure. + int DumpToFile(const char *path); + + /// operator<< writes the key pairs to the output stream. + /// + /// \param os The output stream to write to. + /// \param dictionary The dictionary to write out. + /// \return The output stream is returned. + friend std::ostream &operator<<(std::ostream &os, + const Dictionary &dictionary); private: - uint8_t *seek(const char *key, uint8_t klen); - bool spaceAvailable(uint8_t klen, uint8_t vlen); + uint8_t *seek(const char *key, uint8_t klen); - Arena &arena; - uint8_t kTag; - uint8_t vTag; + bool spaceAvailable(uint8_t klen, uint8_t vlen); + + Arena &arena; + uint8_t kTag; + uint8_t vTag; }; +} // namespace klib + #endif diff --git a/TLV.cc b/TLV.cc index 4a156c3..4802bb1 100644 --- a/TLV.cc +++ b/TLV.cc @@ -14,7 +14,7 @@ namespace TLV { static bool spaceAvailable(Arena &arena, uint8_t *cursor, uint8_t len) { - if (cursor == NULL) { + if (cursor == nullptr) { return false; } @@ -33,21 +33,25 @@ clearUnused(Record &rec) uint8_t * WriteToMemory(Arena &arena, uint8_t *cursor, Record &rec) { - // If cursor is NULL, the user needs us to select an empty + // If cursor is nullptr, the user needs us to select an empty // slot for the record. If we can't find one, that's an // error. // // If, however, the user gives us a cursor, we'll trust it // (though spaceAvailable will sanity check that cursor). - if (cursor == NULL) { + if (cursor == nullptr) { cursor = FindEmpty(arena, cursor); - if (cursor == NULL) { - return NULL; + if (cursor == nullptr) { + return nullptr; } } + if (!arena.CursorInArena(cursor)) { + return nullptr; + } + if (!spaceAvailable(arena, cursor, rec.Len)) { - return NULL; + return nullptr; } memcpy(cursor, &rec, REC_SIZE(rec)); @@ -98,21 +102,21 @@ LocateTag(Arena &arena, uint8_t *cursor, Record &rec) { uint8_t tag, len; - if (cursor == NULL) { + if (cursor == nullptr) { cursor = arena.NewCursor(); } while ((tag = cursor[0]) != rec.Tag) { len = cursor[1]; if (!spaceAvailable(arena, cursor, len)) { - return NULL; + return nullptr; } cursor += len; cursor += 2; } if (tag != rec.Tag) { - return NULL; + return nullptr; } if (tag != TAG_EMPTY) { @@ -142,7 +146,11 @@ SkipRecord(Record &rec, uint8_t *cursor) void DeleteRecord(Arena &arena, uint8_t *cursor) { - if (cursor == NULL) { + if (cursor == nullptr) { + return; + } + + if (!arena.CursorInArena(cursor)) { return; } diff --git a/TLV.h b/TLV.h index bbd112a..0ffa62d 100644 --- a/TLV.h +++ b/TLV.h @@ -1,35 +1,76 @@ +/// +/// \file TLV.h +/// \author K. Isom +/// \date 2023-10-06 +/// \brief TLV.h implements basic tag-length-value records. +/// +/// TLV implements tag-length-value (TLV) records. Each record can have +/// a maximum length of 253 bytes; each TLV record occupies a fixed 255 +/// bytes in memory. TLV records don't allocate memory. +/// +/// This system uses an Arena as a backing store. +/// + #ifndef KIMODEM_TLV_H #define KIMODEM_TLV_H #include +#include #include "Arena.h" -using namespace klib; - - -#ifndef TLV_MAX_LEN -#define TLV_MAX_LEN 253 -#endif - - -#define TAG_EMPTY 0 - namespace klib { namespace TLV { +#ifndef TLV_MAX_LEN +static constexpr size_t TLV_MAX_LEN = 253; +#endif +static constexpr uint8_t TAG_EMPTY = 0; + + +/// Record describes a tag-length-value record. +/// +/// TLV records occupy a fixed size in memory, which can be controlled with the +/// TLV_MAX_LEN define. If this isn't defined, it defaults to a size of 253. +/// When writen to an Arena, it occupies Len + 2 bytes. The strings +/// are not null-terminated in the arena. struct Record { - uint8_t Tag; - uint8_t Len; - char Val[TLV_MAX_LEN]; + /// A Tag is used to identify the type of this record. + uint8_t Tag; + /// Len describes the number of bytes stored in #Val. + uint8_t Len; + /// Val contains the data in the record. + uint8_t Val[TLV_MAX_LEN]; }; -uint8_t *WriteToMemory(Arena &, uint8_t *, Record &); -void ReadFromMemory(Record &, uint8_t *); -void SetRecord(Record &, uint8_t, uint8_t, const char *); -void DeleteRecord(Arena &, uint8_t *); +/// WriteToMemory writes the TLV record into the arena at the location pointed +/// to in the arena. +/// +/// \param arena The backing memory store. +/// \param cursor Pointer into the arena's memory. +/// \param rec A TLV record to be serialized. +/// \return A pointer the memory after the record. +uint8_t *WriteToMemory(Arena &arena, uint8_t *cursor, Record &rec); + +/// ReadFromMemory reads a record from the memory pointed to by the cursor. +/// +/// \param rec The TLV record to be filled in. +/// \param cursor A pointer into an arena's memory store. +void ReadFromMemory(Record &rec, uint8_t *cursor); + +/// SetRecord sets a record. +/// +/// \param rec The record to be set. +/// \param tag The record's tag. +/// \param length The record's length. +/// \param data The data to fill the record with. +void SetRecord(Record &rec, uint8_t tag, uint8_t length, const char *data); + +/// DeleteRecord removes the record from the arena. All records ahead of this +/// record are shifted backwards so that there are no gaps. +void DeleteRecord(Arena &arena, uint8_t *cursor); /* * returns a pointer to memory where the record was found, @@ -37,11 +78,48 @@ void DeleteRecord(Arena &, uint8_t *); * FindTag will call LocateTag and then SkipRecord if the * tag was found. */ -uint8_t *FindTag(Arena &, uint8_t *, Record &); -uint8_t *LocateTag(Arena &, uint8_t *, Record &); +/// FindTag finds the next occurrence of the record's tag. +/// +/// The record must have a tag set, which tells FindTag which tag to look for. +/// If found, it fills the record. \see LocateTag. +/// +/// \param arena The backing memory for the TLV store. +/// \param cursor A pointer to memory inside the arena; if it's NULL, the +/// search starts at the beginning of the arena. +/// \param rec The record to be filled. +/// \return If the tag is found, a cursor pointing to the next record is +/// returned; otherwise nullptr is returned. +uint8_t *FindTag(Arena &arena, uint8_t *cursor, Record &rec); -uint8_t *FindEmpty(Arena &, uint8_t *); -uint8_t *SkipRecord(Record &, uint8_t *); +/// LocateTag operates similarly to FindTag, but the cursor points to the +/// beginning of the found record. +/// +/// \param arena The backing memory for the TLV store. +/// \param cursor A pointer to memory inside the arena; if it's NULL, the +/// search starts at the beginning of the arena. +/// \param rec The record to be filled. +/// \return If the tag is found, a cursor pointing to the record is +/// returned; otherwise nullptr is returned. +uint8_t *LocateTag(Arena &arena, uint8_t *cursor, Record &rec); + +/// FindEmpty finds a pointer the next available empty space. +/// +/// \return A cursor to the start of empty space in the arena, or nullptr +/// if there is no more empty space available. +/// +/// \param arena The backing memory for the TLV store. +/// \param cursor A pointer to memory inside the arena; if it's NULL, the +/// search starts at the beginning of the arena. +/// \return If the arena has space available, a cursor pointing the start +/// of empty space; otherwise, nullptr is returned. +uint8_t *FindEmpty(Arena &arena, uint8_t *cursor); + +/// SkipRecord skips the cursor to the next record. +/// +/// \param rec The record that should be skipped. +/// \param cursor A pointer to the record in the arena. +/// \return The pointer to the next record in the arena. +uint8_t *SkipRecord(Record &rec, uint8_t *cursor); } // namespace TLV diff --git a/Test.cc b/Test.cc index 46a9d5f..cb114b6 100644 --- a/Test.cc +++ b/Test.cc @@ -15,7 +15,7 @@ namespace klib { void TestAssert(bool condition, std::string message = "Assertion failed.") { -#if defined(NDEBUG) +#if defined(NDEBUG) || defined(KLIB_NO_ASSERT) if (!condition) { throw AssertionFailed(message); } diff --git a/Test.h b/Test.h index 7ca9799..7a8ea31 100644 --- a/Test.h +++ b/Test.h @@ -1,27 +1,43 @@ -// -// Created by kyle on 2023-10-09. -// - -#include - +/// +/// \file Test.h +/// \author K. Isom +/// \date 2023-10-09 +/// \brief Test.h implements basic testing tools. +/// #ifndef KLIB_TEST_H #define KLIB_TEST_H +#include + namespace klib { -void -TestAssert(bool condition, std::string message); +/// TestAssert is a variant on the assert macro. +/// +/// If NDEBUG is set, TestAssert will throw an exception if condition is false. +/// Otherwise, it calls assert after printing the message. +/// +/// In addition to NDEBUG, KLIB_NO_ASSERT will suppress assertions. +/// +/// \throws AssertionFailed +/// +/// \param condition The condition to assert. +/// \param message The message that should be displayed if condition is false. +inline void TestAssert(bool condition, std::string message); +/// AssertionFailed indicates that some invariant didn't hold. class AssertionFailed : public std::exception { public: + /// AssertionFailed is constructed with a message describing what + /// failed. explicit AssertionFailed(std::string message); + /// what returns a message describing the exception. char *what(); -public: +private: std::string msg; }; diff --git a/dictionaryTest.cc b/dictionaryTest.cc index daba3df..6843317 100644 --- a/dictionaryTest.cc +++ b/dictionaryTest.cc @@ -4,6 +4,7 @@ #include "Arena.h" #include "Dictionary.h" #include "testFixtures.h" +using namespace klib; constexpr char TEST_KVSTR1[] = "foo"; @@ -56,18 +57,18 @@ main(int argc, const char *argv[]) TLV::SetRecord(expect, DICTIONARY_TAG_VAL, TEST_KVSTRLEN3, TEST_KVSTR3); Dictionary dict(arena); - assert(!dict.Has(TEST_KVSTR2, TEST_KVSTRLEN2)); + assert(!dict.Contains(TEST_KVSTR2, TEST_KVSTRLEN2)); assert(testSetKV(dict, TEST_KVSTR1, TEST_KVSTRLEN1, TEST_KVSTR3, TEST_KVSTRLEN3)); - dict.DumpKVPairs(); + std::cout << dict; assert(testSetKV(dict, TEST_KVSTR2, TEST_KVSTRLEN2, TEST_KVSTR3, TEST_KVSTRLEN3)); - dict.DumpKVPairs(); - assert(dict.Has(TEST_KVSTR2, TEST_KVSTRLEN2)); + std::cout << dict; + assert(dict.Contains(TEST_KVSTR2, TEST_KVSTRLEN2)); assert(testSetKV(dict, TEST_KVSTR4, TEST_KVSTRLEN4, TEST_KVSTR5, TEST_KVSTRLEN5)); - dict.DumpKVPairs(); + std::cout << dict; assert(dict.Lookup(TEST_KVSTR2, TEST_KVSTRLEN2, value)); assert(cmpRecord(value, expect)); @@ -75,7 +76,7 @@ main(int argc, const char *argv[]) std::cout << "test overwriting key" << std::endl; assert(testSetKV(dict, TEST_KVSTR2, TEST_KVSTRLEN2, TEST_KVSTR6, TEST_KVSTRLEN6)); - dict.DumpKVPairs(); + std::cout << dict; TLV::SetRecord(expect, DICTIONARY_TAG_VAL, TEST_KVSTRLEN6, TEST_KVSTR6); std::cout << "\tlookup" << std::endl; assert(dict.Lookup(TEST_KVSTR2, TEST_KVSTRLEN2, value)); @@ -85,7 +86,7 @@ main(int argc, const char *argv[]) std::cout << "\tadd new key to dictionary" << std::endl; assert(testSetKV(dict, TEST_KVSTR3, TEST_KVSTRLEN3, TEST_KVSTR5, TEST_KVSTRLEN5)); - dict.DumpKVPairs(); + std::cout << dict; TLV::SetRecord(expect, DICTIONARY_TAG_VAL, TEST_KVSTRLEN5, TEST_KVSTR5); assert(dict.Lookup(TEST_KVSTR4, TEST_KVSTRLEN4, value)); @@ -100,5 +101,5 @@ main(int argc, const char *argv[]) #endif arena.Clear(); - dict.DumpKVPairs(); + std::cout << dict; } diff --git a/klib.h b/klib.h new file mode 100644 index 0000000..e6fc694 --- /dev/null +++ b/klib.h @@ -0,0 +1,39 @@ +/// +/// \file klib.h +/// \author kyle +/// \created 2023-10-10 +/// \brief klib is my collection of C++ data structures and code. +/// +/// \section COPYRIGHT +/// Copyright 2023 K. Isom +/// +/// Permission to use, copy, modify, and/or distribute this software for +/// any purpose with or without fee is hereby granted, provided that the +/// above copyright notice and this permission notice appear in all copies. +/// +/// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL +/// WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +/// WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR +/// BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES +/// OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +/// WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +/// ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS +/// SOFTWARE. +/// +/// \mainpage klib documentation +/// Hello, world. +/// +/// \section Introduction +/// +/// This is a collection of data structures and subroutines that I find +/// useful in building things. + +#ifndef KLIB_KLIB_H +#define KLIB_KLIB_H + + +/// klib is the top-level namespace containing all the code in this library. +namespace klib {} + + +#endif // KLIB_KLIB_H diff --git a/klib.pc.in b/klib.pc.in new file mode 100644 index 0000000..3ff810d --- /dev/null +++ b/klib.pc.in @@ -0,0 +1,10 @@ +prefix="@CMAKE_INSTALL_PREFIX@" +exec_prefix="${prefix}" +libdir="${prefix}/lib" +includedir="${prefix}/include" + +Name: @PROJECT_NAME@ +Description: @CMAKE_PROJECT_DESCRIPTION@ +Version: @PROJECT_VERSION@ +Cflags: -I${includedir} +Libs: -L${libdir} -l@PROJECT_NAME@ \ No newline at end of file diff --git a/testFixtures.h b/testFixtures.h index 31b743b..1fe1176 100644 --- a/testFixtures.h +++ b/testFixtures.h @@ -3,6 +3,7 @@ #include +#include "TLV.h" #define ARENA_SIZE 128 @@ -20,10 +21,14 @@ #define TEST_STR4 "How is a raven like a writing desk?" #define TEST_STRLEN4 35 + +namespace klib { + + static bool cmpRecord(TLV::Record &a, TLV::Record &b) { - if (a.Tag != b .Tag) { + if (a.Tag != b.Tag) { return false; } @@ -39,4 +44,7 @@ cmpRecord(TLV::Record &a, TLV::Record &b) } -#endif +} // namespace klib + + +#endif // KLIB_TESTFIXTURES_H