finish doxygen docs.

This also includes a lot of code cleanups along the way.
This commit is contained in:
Kyle Isom 2023-10-10 02:35:43 -07:00
parent 386869df44
commit a8b09001f7
19 changed files with 596 additions and 234 deletions

33
.clang-tidy Normal file
View File

@ -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

2
.gitignore vendored
View File

@ -1,10 +1,12 @@
.idea
.trunk
.vc
.vscode
*.a
*.o
*.bin
*.pc
build
core
core.*

View File

@ -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

172
Arena.h
View File

@ -4,24 +4,13 @@
/// \date 2023-10-06
/// \brief Memory management using an arena.
///
/// \section COPYRIGHT
/// Copyright 2023 K. Isom <kyle@imap.cc>
///
/// 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<allocated>@0x7fff91dfad70,store<128B>@0x0055d6c5881ec0.
///
/// \param os
/// \param arena
/// \return
std::ostream &operator<<(std::ostream& os, Arena &arena);

View File

@ -3,22 +3,7 @@
/// \author K. Isom <kyle@imap.cc>
/// \date 2023-10-09
///
/// \section COPYRIGHT
/// Copyright 2023 K. Isom <kyle@imap.cc>
///
/// 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 <cassert>
#include <cstring>
@ -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];
}

View File

@ -1,26 +1,9 @@
///
/// \file Buffer.hcc
/// \file Buffer.h
/// \author K. Isom <kyle@imap.cc>
/// \date 2023-10-09
/// \brief Buffer implements basic line buffers.
///
/// \section COPYRIGHT
/// Copyright 2023 K. Isom <kyle@imap.cc>
///
/// 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

View File

@ -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})

View File

@ -1,21 +1,24 @@
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)
if (MSVC)
add_compile_options("/W4" "$<$<CONFIG:RELEASE>:/O2>")
else()
else ()
add_compile_options("-Wall" "-Wextra" "-Werror" "$<$<CONFIG:RELEASE>:-O3>")
if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")
if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")
add_compile_options("-stdlib=libc++")
else()
else ()
# nothing special for gcc at the moment
endif()
endif()
endif ()
endif ()
add_compile_options("-DDESKTOP_BUILD")
set(HEADER_FILES
klib.h
Arena.h
Buffer.h
Dictionary.h
@ -33,11 +36,7 @@ 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)
TLV.cc)
include(CTest)
enable_testing()
@ -61,5 +60,17 @@ write_basic_package_version_file(
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)

View File

@ -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")

View File

@ -6,13 +6,17 @@
#include <iostream>
#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

View File

@ -1,3 +1,10 @@
///
/// \file klib.h
/// \author kyle
/// \date 2023-10-06
///
#ifndef KLIB_DICTIONARY_H
#define KLIB_DICTIONARY_H
@ -6,33 +13,102 @@
#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)
{};
/// 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);
bool Has(const char *key, uint8_t klen);
void DumpKVPairs();
void DumpToFile(const char *path);
/// 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);
Arena &arena;
@ -41,4 +117,6 @@ private:
};
} // namespace klib
#endif

28
TLV.cc
View File

@ -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;
}

116
TLV.h
View File

@ -1,35 +1,76 @@
///
/// \file TLV.h
/// \author K. Isom <kyle@imap.cc>
/// \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 <cstdint>
#include <array>
#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 {
/// 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;
char Val[TLV_MAX_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

View File

@ -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);
}

34
Test.h
View File

@ -1,27 +1,43 @@
//
// Created by kyle on 2023-10-09.
//
#include <string>
///
/// \file Test.h
/// \author K. Isom <kyle@imap.cc>
/// \date 2023-10-09
/// \brief Test.h implements basic testing tools.
///
#ifndef KLIB_TEST_H
#define KLIB_TEST_H
#include <string>
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;
};

View File

@ -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;
}

39
klib.h Normal file
View File

@ -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 <kyle@imap.cc>
///
/// 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

10
klib.pc.in Normal file
View File

@ -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@

View File

@ -3,6 +3,7 @@
#include <string.h>
#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