Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@

# Build directories
build/
build-*/
bin/
cmake-build-*/
out/

# Local scratch
tmp_*.cpp
4 changes: 2 additions & 2 deletions ASSUMPTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ This document records behavioral assumptions baked into smallfolk_cpp. If you re
- Numbers are stored internally as `double`. Integer values outside exact `double` range may lose precision on round-trip.
- Non-finite floats use Smallfolk's single-letter encodings (`I`, `i`, `N`, `Q`) rather than JSON-style `Infinity`/`NaN`. Set `LoadLimits::reject_non_finite_numbers` to reject these during `loads()`.
- String keys and values use `"` or `'` quoting; embedded quotes are doubled. There is no `\` escape syntax.
- Whitespace between tokens is limited to space and tab. Other whitespace (newlines, `\r`) is not skipped unless explicitly present in a string literal.
- Whitespace between tokens is limited to space and tab at value boundaries. Between a table key and `:` / `,` / `}`, only spaces are skipped (not tabs), to keep the hot path tight.
- Table keys that are positive integers with no fractional part serialize as array elements when consecutive from `1`. Gaps or non-integer numeric keys use explicit `key:value` form.
- **`loads()` assumes the input is trusted only to the extent configured by `LoadLimits`.** Default limits cap input size, nesting depth, value count, per-table entry count, and per-string length. Trailing garbage after a valid value is rejected by default.

Expand Down Expand Up @@ -49,7 +49,7 @@ This document records behavioral assumptions baked into smallfolk_cpp. If you re
## Locale and platform

- Number parsing uses the `"C"` locale via `std::strtod` to avoid locale-dependent decimal separators.
- `sprintf` / `snprintf` formatting for number output uses `% .17g` (Lua-minimum style precision for finite values).
- Number output uses `std::snprintf` with `%.17g` (Lua-minimum style precision for finite values).

## Security

Expand Down
17 changes: 17 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,21 @@ install(EXPORT smallfolk_cppTargets
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/smallfolk_cpp
)

include(CMakePackageConfigHelpers)
write_basic_package_version_file(
"${CMAKE_CURRENT_BINARY_DIR}/smallfolk_cppConfigVersion.cmake"
VERSION ${PROJECT_VERSION}
COMPATIBILITY SameMajorVersion
)
configure_package_config_file(
"${CMAKE_CURRENT_SOURCE_DIR}/cmake/smallfolk_cppConfig.cmake.in"
"${CMAKE_CURRENT_BINARY_DIR}/smallfolk_cppConfig.cmake"
INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/smallfolk_cpp
)
install(FILES
"${CMAKE_CURRENT_BINARY_DIR}/smallfolk_cppConfig.cmake"
"${CMAKE_CURRENT_BINARY_DIR}/smallfolk_cppConfigVersion.cmake"
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/smallfolk_cpp
)

smallfolk_add_static_analysis_targets()
27 changes: 16 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,12 @@ ctest --test-dir build --output-on-failure # if you enable CTest
cmake --install build --prefix /path/to/prefix
```

This installs `smallfolk.h`, the `smallfolk` library, and a CMake export file under `lib/cmake/smallfolk_cpp/`.
This installs `smallfolk.h`, `smallfolk_convert.h`, `smallfolk_schema.h`, the `smallfolk` library, and CMake package files under `lib/cmake/smallfolk_cpp/` so consumers can use `find_package`:

```cmake
find_package(smallfolk_cpp REQUIRED)
target_link_libraries(my_app PRIVATE smallfolk_cpp::smallfolk)
```

## Usage

Expand All @@ -55,7 +60,7 @@ This installs `smallfolk.h`, the `smallfolk` library, and a CMake export file un

// create a lua table and set some values to it
LuaVal table = LuaVal::table();
table[1] = "Hello"; // the values is automatically converted to LuaVal
table[1] = "Hello"; // the value is automatically converted to LuaVal
table["test"] = "world";
table[67.5] = -234.5;

Expand Down Expand Up @@ -102,7 +107,7 @@ The benchmark serializes this sample payload:

## Table cycles

**Note: This feature was disabled cause of difficult implementing in C++and possibly unwanted infinite cycles. All table assigning create copies now in the C++ code and no @ notation is recognised for serializing or deserializing. Any such references are set to nil when deserializing. Any @ references are otherwise deep copies in the C++ code**
**Note: This feature was disabled because of difficult implementing in C++ and possibly unwanted infinite cycles. All table assigning creates deep copies now in the C++ code and no `@` notation is recognized for serializing or deserializing. Input containing `@` references is rejected on load. Assigning a table into itself (or as a key) always deep-copies; there are no shared cycles.**

From original smallfolk

Expand Down Expand Up @@ -303,7 +308,7 @@ LuaVal t5 = {1,2, "test", vec};
// Resulting table: {1,2,"test",{{"a","b"},{"a","b"}}}
```

Creating sequences is easy, but creating complex tables that contain different types of values can be difficult or take a lot of space in code. To avoid quirks and for conveience you can deserialize strings to create values in a compact way. Here two equivalent values are created with normal style and deserialization:
Creating sequences is easy, but creating complex tables that contain different types of values can be difficult or take a lot of space in code. To avoid quirks and for convenience you can deserialize strings to create values in a compact way. Here two equivalent values are created with normal style and deserialization:

```c++
LuaVal val1 = { 1,2, LuaVal::mrg({3,4.5}, LuaVal::LuaTable({{"ke","test"}})) };
Expand All @@ -323,9 +328,9 @@ May throw if LuaVal is not valid for some reason (which should not be possible).

### typetag

There are definitions for typetags used to identify each value type. These can be used in the constructor of a LuaValue as well.
For example a table can be created with `LuaValue table(TTABLE)`. You can get the typetag of an object with the member function `LuaTypeTag LuaVal::typetag()`.
GetTypeTag does not throw.
There are definitions for typetags used to identify each value type. These can be used in the constructor of a `LuaVal` as well.
For example a table can be created with `LuaVal table(TTABLE)`. You can get the typetag of an object with the member function `LuaTypeTag LuaVal::typetag()`.
`typetag()` does not throw.

```C++
enum LuaTypeTag
Expand Down Expand Up @@ -429,7 +434,7 @@ Variadic segments (`try_get_path("a", "b", 1)`) and `std::initializer_list<LuaVa

#### `lua_val` factories

`#include "smallfolk_convert.h"` for helpers such as `lua_val::map(...)`, `lua_val::array(...)`, `lua_val::string(...)`, and `lua_val::nil()`.
Scalar helpers such as `lua_val::string(...)` and `lua_val::nil()` live in `smallfolk.h`. Include `smallfolk_convert.h` for STL-friendly factories such as `lua_val::map(...)` and `lua_val::array(...)` from `std::map` / `std::vector` / similar containers.

A method for erasing data with a key is `luaval.rem(key)` which also returns the accessed table.
This function do not throw unless you use it on non table objects or with nil keys.
Expand All @@ -444,12 +449,12 @@ table.set(table, "table as key?"); // table will work as a key, but it will be a
std::cout << table.get("self copy").get(3).num() << std::endl; // get a value from a nested table
table["number"] = 234; // Use table access operator to assign a value
LuaVal & value = table["number"]; // Use table access operator to get a value
table.set(e, LuaVal::nil).rem("number"); // remove some values through set and rem functions
table.set("self copy", LuaVal::nil).rem("number"); // remove some values through set and rem functions
if (table.has(100) and table[100].isstring())
std::cout << table[100].str() << std::end;
std::cout << table[100].str() << std::endl;
```

For conveniency tables also have the methods `luaval.insert(value[, pos])`, `luaval.remove([pos])` and `luaval.len()`.
For convenience tables also have the methods `luaval.insert(value[, pos])`, `luaval.remove([pos])` and `luaval.len()`.
The len function returns the number of consecutive integer key elements in the table starting at index 1. It is similar to the # operator in lua.
Insert and remove shift the values on the right side of the given position and insert or remove a value to or at the given position. If position is omitted, the value is inserted to the end of the list or the last element is removed.
Insert and remove both return the accessed table.
Expand Down
5 changes: 5 additions & 0 deletions cmake/smallfolk_cppConfig.cmake.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@PACKAGE_INIT@

include("${CMAKE_CURRENT_LIST_DIR}/smallfolk_cppTargets.cmake")

check_required_components(smallfolk_cpp)
53 changes: 17 additions & 36 deletions smallfolk.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
#include <stdarg.h> // va_start
#include <functional> // std::hash
#include <mutex>
#include <vector>

namespace
{
Expand Down Expand Up @@ -139,8 +138,6 @@ namespace

namespace Serializer
{
typedef std::vector<LuaVal> TABLES;
typedef std::unordered_map<LuaVal, unsigned int, LuaVal::LuaValHasher> MEMO;
typedef std::stringstream ACC;

inline bool is_nan_value(double value)
Expand Down Expand Up @@ -190,15 +187,15 @@ namespace Serializer
return arr;
}

unsigned int dump_type_table(LuaVal const & object, unsigned int nmemo, MEMO& memo, ACC& acc);
unsigned int dump_object(LuaVal const & object, unsigned int nmemo, MEMO& memo, ACC& acc);
void dump_type_table(LuaVal const & object, ACC& acc);
void dump_object(LuaVal const & object, ACC& acc);
std::string escape_quotes(const std::string &before, char quote);
std::string unescape_quotes(const std::string &before, char quote);
bool nonzero_digit(char c);
bool is_digit(char c);
char strat(std::string const & string, std::string::size_type i);
LuaVal expect_number(std::string const & string, size_t& start, ParseContext & ctx);
LuaVal expect_object(std::string const & string, size_t& i, TABLES& tables, ParseContext & ctx);
LuaVal expect_object(std::string const & string, size_t& i, ParseContext & ctx);
}

LoadLimits const & LuaVal::default_load_limits()
Expand Down Expand Up @@ -871,9 +868,7 @@ std::string LuaVal::dumps(std::string * errmsg) const
{
Serializer::ACC acc;
acc << std::setprecision(17); // min lua precision
unsigned int nmemo = 0;
Serializer::MEMO memo;
Serializer::dump_object(*this, nmemo, memo, acc);
Serializer::dump_object(*this, acc);
return acc.str();
}
catch (smallfolk_exception const & e)
Expand Down Expand Up @@ -907,10 +902,9 @@ LuaVal LuaVal::loads(std::string const & string, LoadLimits const & limits, std:
"load limit exceeded: max input size %zu",
limits.max_input_size);

Serializer::TABLES tables;
ParseContext ctx{ limits, 0, 0 };
size_t i = 0;
LuaVal result = Serializer::expect_object(string, i, tables, ctx);
LuaVal result = Serializer::expect_object(string, i, ctx);
skip_whitespace(string, i);
if (limits.require_consumed_input && i != string.length())
throw smallfolk_exception("unexpected trailing input at position %zu", i);
Expand Down Expand Up @@ -976,22 +970,11 @@ LuaVal& LuaVal::operator=(LuaVal const& val)
return *this;
}

unsigned int Serializer::dump_type_table(LuaVal const & object, unsigned int nmemo, MEMO & memo, ACC & acc)
void Serializer::dump_type_table(LuaVal const & object, ACC & acc)
{
if (!object.istable())
throw smallfolk_exception("using dump_type_table on non table object");

/*
// @ circular table references are disabled; deep copy on assign avoids shared refs.
auto it = memo.find(object);
if (it != memo.end())
{
acc << '@' << it->second;
return nmemo;
}
memo[object] = ++nmemo;
*/

acc << '{';
bool first = true;
std::map<unsigned int, const LuaVal*> arr;
Expand All @@ -1011,27 +994,26 @@ unsigned int Serializer::dump_type_table(LuaVal const & object, unsigned int nme
first = false;
if (v.first != i)
{
nmemo = dump_object(v.first, nmemo, memo, acc);
dump_object(v.first, acc);
acc << ':';
}
else
++i;
nmemo = dump_object(*v.second, nmemo, memo, acc);
dump_object(*v.second, acc);
}
for (auto&& v : hash)
{
if (!first)
acc << ',';
first = false;
nmemo = dump_object(*v.first, nmemo, memo, acc);
dump_object(*v.first, acc);
acc << ':';
nmemo = dump_object(*v.second, nmemo, memo, acc);
dump_object(*v.second, acc);
}
acc << '}';
return nmemo;
}

unsigned int Serializer::dump_object(LuaVal const & object, unsigned int nmemo, MEMO & memo, ACC & acc)
void Serializer::dump_object(LuaVal const & object, ACC & acc)
{
switch (object.typetag())
{
Expand All @@ -1050,11 +1032,11 @@ unsigned int Serializer::dump_object(LuaVal const & object, unsigned int nmemo,
append_number_token(acc, object.num());
break;
case TTABLE:
return dump_type_table(object, nmemo, memo, acc);
dump_type_table(object, acc);
break;
default:
throw smallfolk_exception("dump_object invalid or unhandled tag %i", object.typetag());
}
return nmemo;
}

std::string Serializer::escape_quotes(const std::string & before, char quote)
Expand Down Expand Up @@ -1194,15 +1176,15 @@ LuaVal Serializer::expect_number(std::string const & string, size_t & start, Par
return value;
}

LuaVal Serializer::expect_object(std::string const & string, size_t & i, Serializer::TABLES & tables, ParseContext & ctx)
LuaVal Serializer::expect_object(std::string const & string, size_t & i, ParseContext & ctx)
{
char cc = strat(string, i++);
switch (cc)
{
case ' ':
case '\t':
// skip whitespace
return expect_object(string, i, tables, ctx);
return expect_object(string, i, ctx);
case 't':
ctx.on_value_created();
return true;
Expand Down Expand Up @@ -1285,7 +1267,6 @@ LuaVal Serializer::expect_object(std::string const & string, size_t & i, Seriali
LuaVal nt(TTABLE);
ctx.on_value_created();
unsigned int j = 1;
tables.push_back(nt);
if (strat(string, i) == '}')
{
++i;
Expand All @@ -1301,13 +1282,13 @@ LuaVal Serializer::expect_object(std::string const & string, size_t & i, Seriali
ctx.limits.max_table_entries);
}

LuaVal k = expect_object(string, i, tables, ctx);
LuaVal k = expect_object(string, i, ctx);
char at = strat(string, i);
while (at == ' ')
at = strat(string, ++i);
if (at == ':')
{
nt.set(k, expect_object(string, ++i, tables, ctx));
nt.set(k, expect_object(string, ++i, ctx));
}
else
{
Expand Down
3 changes: 1 addition & 2 deletions smallfolk.h
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,7 @@ class LuaVal
};

typedef std::unordered_map<LuaVal, LuaVal> LuaTable;
// Circular reference memleak if insert self to self (deep copy on assign avoids sharing).
typedef std::unique_ptr<LuaTable> TblPtr;
typedef std::unique_ptr<LuaTable> TblPtr; // Table assign deep-copies; @ circular refs unsupported.

LuaVal(const LuaTypeTag tag) : tag(tag), tbl_ptr(tag == TTABLE ? new LuaTable() : nullptr), d(0), b(false) {}
LuaVal() : tag(TTABLE), tbl_ptr(new LuaTable()), d(0), b(false) {}
Expand Down