Loading...
Searching...
No Matches
score-plugin-avnd/AvndProcesses/ValueSerialization.hpp
1#pragma once
2#include <ossia/detail/json.hpp>
3#include <ossia/network/value/format_value.hpp>
4#include <ossia/network/value/value.hpp>
5
6#include <QCborStreamReader>
7#include <QCborStreamWriter>
8#include <QIODevice>
9
10#include <halp/callback.hpp>
11#include <halp/controls.hpp>
12#include <halp/layout.hpp>
13#include <halp/meta.hpp>
14#include <rapidjson/error/en.h>
15#include <rapidjson/memorystream.h>
16#include <rapidjson/prettywriter.h>
17#include <rapidjson/reader.h>
18
19#include <cmath>
20
21#include <algorithm>
22#include <array>
23#include <bit>
24#include <charconv>
25#include <climits>
26#include <cstdint>
27#include <iterator>
28#include <limits>
29#include <span>
30#include <stdexcept>
31#include <string>
32#include <string_view>
33#include <unordered_set>
34#include <utility>
35#include <vector>
36
37// std::from_chars' floating-point overloads cannot be called on every platform
38// score builds for: libc++ deletes them where its runtime has no support, and
39// Apple's availability annotations gate them behind macOS 26 while our
40// deployment target is 12. The integer overloads are available everywhere.
41// Definable from the build to exercise the fallback on a platform that does not
42// need it.
43#if !defined(SCORE_HAS_STD_FLOAT_FROM_CHARS)
44#if defined(__cpp_lib_to_chars) && !defined(_LIBCPP_VERSION)
45#define SCORE_HAS_STD_FLOAT_FROM_CHARS 1
46#else
47#define SCORE_HAS_STD_FLOAT_FROM_CHARS 0
48#endif
49#endif
50
51#if !SCORE_HAS_STD_FLOAT_FROM_CHARS
52#include <boost/lexical_cast/try_lexical_convert.hpp>
53#endif
54
55namespace avnd_tools::value_serialization
56{
57// Deliberately ordinary JSON/CBOR, not OSCQuery's typetag-dependent wire format.
58// null <-> impulse; vectors -> arrays -> lists; unset values are errors.
59// Integer tokens must fit int32. Floating tokens round to float32, with overflow,
60// non-finite values and nonzero-to-zero underflow rejected. This is NOT a lossless
61// int64/double archive. UTF-8 text and embedded NULs are length-aware throughout.
62// Objects/maps preserve insertion order and reject duplicate keys. CBOR accepts
63// the JSON-compatible subset only: text keys, no tags/byte strings/undefined.
64// Bounded event-time work: 8 MiB wire data, 65536 values+keys, 64 containers.
65// JSON is strict (including UTF-8), overriding libossia's permissive parse flags.
66inline constexpr std::size_t max_bytes = 8 * 1024 * 1024;
67inline constexpr std::size_t max_nodes = 65536;
68inline constexpr std::size_t max_depth = 64;
69
70struct codec_error : std::runtime_error
71{
72 using std::runtime_error::runtime_error;
73};
74
75// Parses exactly what std::from_chars accepts, so the two implementations below
76// cannot disagree about which tokens are valid. Boost differs in two ways that
77// are corrected here: it takes a leading '+', and it returns zero for a token
78// that underflows instead of reporting it, where from_chars fails outright.
79inline bool parse_float64(std::string_view text, double& out) noexcept
80{
81#if SCORE_HAS_STD_FLOAT_FROM_CHARS
82 const auto* const last = text.data() + text.size();
83 const auto parsed = std::from_chars(text.data(), last, out);
84 return parsed.ec == std::errc{} && parsed.ptr == last;
85#else
86 if(text.starts_with('+'))
87 return false;
88 if(!boost::conversion::try_lexical_convert(text, out))
89 return false;
90 if(out == 0.)
91 {
92 const auto mantissa = text.substr(0, text.find_first_of("eE"));
93 if(mantissa.find_first_of("123456789") != std::string_view::npos)
94 return false;
95 }
96 return true;
97#endif
98}
99
100struct limits
101{
102 std::size_t nodes{};
103 void node()
104 {
105 if(++nodes > max_nodes)
106 throw codec_error{"Value count limit exceeded"};
107 }
108 static void depth(std::size_t n)
109 {
110 if(n > max_depth)
111 throw codec_error{"Nesting depth limit exceeded"};
112 }
113};
114
115inline void utf8(std::string_view text)
116{
117 rapidjson::MemoryStream stream{text.data(), text.size()};
118 while(stream.Tell() < text.size())
119 {
120 unsigned codepoint{};
121 if(!rapidjson::UTF8<>::Decode(stream, &codepoint))
122 throw codec_error{"Invalid UTF-8 text"};
123 }
124}
125
126inline ossia::value floating(double number)
127{
128 if(!std::isfinite(number) || std::abs(number) > std::numeric_limits<float>::max())
129 throw codec_error{"Number outside finite float32 range"};
130 const float value = static_cast<float>(number);
131 if(number != 0. && value == 0.f)
132 throw codec_error{"Number underflows float32"};
133 return value;
134}
135
136inline ossia::value integer(std::int64_t number)
137{
138 if(number < std::numeric_limits<std::int32_t>::min()
139 || number > std::numeric_limits<std::int32_t>::max())
140 throw codec_error{"Integer outside int32 range"};
141 return static_cast<std::int32_t>(number);
142}
143
144inline ossia::value unsigned_integer(std::uint64_t number)
145{
146 if(number > static_cast<std::uint64_t>(std::numeric_limits<std::int32_t>::max()))
147 throw codec_error{"Integer outside int32 range"};
148 return static_cast<std::int32_t>(number);
149}
150
151// SAX builds the destination directly: no unbounded DOM or recursive parser.
152struct json_reader : rapidjson::BaseReaderHandler<rapidjson::UTF8<>, json_reader>
153{
154 struct frame
155 {
156 ossia::value value;
157 std::string key;
158 std::unordered_set<std::string> keys;
159 };
160 std::vector<frame> stack;
161 ossia::value result;
162 limits budget;
163
164 bool put(ossia::value value)
165 {
166 if(stack.empty())
167 result = std::move(value);
168 else if(auto* list = stack.back().value.target<std::vector<ossia::value>>())
169 list->push_back(std::move(value));
170 else
171 stack.back().value.get<ossia::value_map_type>().emplace_back(
172 std::move(stack.back().key), std::move(value));
173 return true;
174 }
175 bool scalar(ossia::value value)
176 {
177 budget.node();
178 return put(std::move(value));
179 }
180 bool Null() { return scalar(ossia::impulse{}); }
181 bool Bool(bool v) { return scalar(v); }
182 bool RawNumber(const char* s, rapidjson::SizeType n, bool)
183 {
184 const std::string_view token{s, n};
185 if(token.find_first_of(".eE") == std::string_view::npos)
186 {
187 std::int32_t number{};
188 const auto parsed = std::from_chars(s, s + n, number);
189 if(parsed.ec != std::errc{} || parsed.ptr != s + n)
190 throw codec_error{"Integer outside int32 range"};
191 return scalar(number);
192 }
193 double number{};
194 if(!parse_float64(token, number))
195 throw codec_error{"Number outside finite float32 range"};
196 return scalar(floating(number));
197 }
198 bool String(const char* s, rapidjson::SizeType n, bool)
199 {
200 return scalar(std::string{s, n});
201 }
202 bool Key(const char* s, rapidjson::SizeType n, bool)
203 {
204 budget.node();
205 auto& top = stack.back();
206 top.key.assign(s, n);
207 if(!top.keys.insert(top.key).second)
208 throw codec_error{"Duplicate map key"};
209 return true;
210 }
211 bool start(ossia::value value)
212 {
213 budget.node();
214 limits::depth(stack.size() + 1);
215 stack.push_back({std::move(value), {}, {}});
216 return true;
217 }
218 bool StartObject() { return start(ossia::value_map_type{}); }
219 bool StartArray() { return start(std::vector<ossia::value>{}); }
220 bool end()
221 {
222 auto value = std::move(stack.back().value);
223 stack.pop_back();
224 return put(std::move(value));
225 }
226 bool EndObject(rapidjson::SizeType) { return end(); }
227 bool EndArray(rapidjson::SizeType) { return end(); }
228};
229
230inline ossia::value from_json(std::string_view input)
231{
232 if(input.size() > max_bytes)
233 throw codec_error{"Input byte limit exceeded"};
234 // MemoryStream's EOF sentinel is NUL: forbid actual NUL bytes in JSON text,
235 // otherwise a valid root followed by NUL and junk could appear complete.
236 if(input.find('\0') != std::string_view::npos)
237 throw codec_error{"Unescaped NUL in JSON text"};
238 rapidjson::MemoryStream stream{input.data(), input.size()};
239 rapidjson::Reader reader;
240 json_reader handler;
241 constexpr unsigned flags = rapidjson::kParseValidateEncodingFlag
242 | rapidjson::kParseNumbersAsStringsFlag
243 | rapidjson::kParseIterativeFlag;
244 if(!reader.Parse<flags>(stream, handler))
245 throw codec_error{
246 std::string{rapidjson::GetParseError_En(reader.GetParseErrorCode())}
247 + " at byte " + std::to_string(reader.GetErrorOffset())};
248 return std::move(handler.result);
249}
250
251// This stream bounds allocation during writing, rather than checking after a
252// potentially enormous escaped JSON string has already been allocated.
254{
255 using Ch = char;
256 std::string data;
257 void Put(char c)
258 {
259 if(data.size() == max_bytes)
260 throw codec_error{"Output byte limit exceeded"};
261 data.push_back(c);
262 }
263 void Flush() { }
264};
265
266template <template <typename, typename, typename, typename, unsigned> class Writer>
268{
269 json_output output;
270 Writer<
271 json_output, rapidjson::UTF8<>, rapidjson::UTF8<>, rapidjson::CrtAllocator,
272 rapidjson::kWriteValidateEncodingFlag>
273 writer{output};
274 void null() { writer.Null(); }
275 void number(int v) { writer.Int(v); }
276 void number(float v) { writer.Double(v); }
277 void boolean(bool v) { writer.Bool(v); }
278 void text(std::string_view v)
279 {
280 if(!writer.String(v.data(), static_cast<rapidjson::SizeType>(v.size())))
281 throw codec_error{"Invalid UTF-8 text"};
282 }
283 void key(std::string_view v) { text(v); }
284 void array(std::size_t) { writer.StartArray(); }
285 void map(std::size_t) { writer.StartObject(); }
286 void end_array() { writer.EndArray(); }
287 void end_map() { writer.EndObject(); }
288};
289
292
293struct cbor_output : QIODevice
294{
295 std::string data;
296 bool overflow{};
297 cbor_output() { open(QIODevice::WriteOnly); }
298 qint64 readData(char*, qint64) override { return -1; }
299 qint64 writeData(const char* source, qint64 size) override
300 {
301 if(size < 0 || static_cast<std::uint64_t>(size) > max_bytes - data.size())
302 {
303 overflow = true;
304 return -1;
305 }
306 data.append(source, static_cast<std::size_t>(size));
307 return size;
308 }
309};
310
312{
313 cbor_output output;
314 QCborStreamWriter writer{&output};
315 void null() { writer.appendNull(); }
316 void number(int v) { writer.append(v); }
317 void number(float v) { writer.append(v); }
318 void boolean(bool v) { writer.append(v); }
319 void text(std::string_view v)
320 {
321 utf8(v);
322 writer.appendTextString(v.data(), static_cast<qsizetype>(v.size()));
323 }
324 void key(std::string_view v) { text(v); }
325 void array(std::size_t n) { writer.startArray(n); }
326 void map(std::size_t n) { writer.startMap(n); }
327 void end_array() { writer.endArray(); }
328 void end_map() { writer.endMap(); }
329};
330
331template <typename Writer>
333{
334 Writer& writer;
335 limits& budget;
336 std::size_t depth{};
337 void operator()() const { throw codec_error{"Cannot serialize an unset value"}; }
338 void operator()(ossia::impulse) const { writer.null(); }
339 void operator()(int v) const { writer.number(v); }
340 void operator()(float v) const
341 {
342 if(!std::isfinite(v))
343 throw codec_error{"Cannot serialize a non-finite number"};
344 writer.number(v);
345 }
346 void operator()(bool v) const { writer.boolean(v); }
347 void operator()(const std::string& v) const
348 {
349 if(v.size() > max_bytes)
350 throw codec_error{"String byte limit exceeded"};
351 writer.text(v);
352 }
353 void child(const ossia::value& v) const
354 {
355 budget.node();
356 v.apply(value_writer{writer, budget, depth + 1});
357 if constexpr(requires { writer.output.overflow; })
358 if(writer.output.overflow)
359 throw codec_error{"Output byte limit exceeded"};
360 }
361 template <std::size_t N>
362 void operator()(const std::array<float, N>& v) const
363 {
364 limits::depth(depth + 1);
365 writer.array(N);
366 for(float x : v)
367 {
368 budget.node();
369 (*this)(x);
370 }
371 writer.end_array();
372 }
373 void operator()(const std::vector<ossia::value>& v) const
374 {
375 limits::depth(depth + 1);
376 writer.array(v.size());
377 for(const auto& x : v)
378 child(x);
379 writer.end_array();
380 }
381 void operator()(const ossia::value_map_type& v) const
382 {
383 limits::depth(depth + 1);
384 writer.map(v.size());
385 std::unordered_set<std::string_view> keys;
386 for(const auto& [key, value] : v)
387 {
388 budget.node();
389 if(key.size() > max_bytes)
390 throw codec_error{"Key byte limit exceeded"};
391 if(!keys.insert(key).second)
392 throw codec_error{"Duplicate map key"};
393 writer.key(key);
394 child(value);
395 }
396 writer.end_map();
397 }
398};
399
400template <typename Writer>
401std::string encode(const ossia::value& value)
402{
403 Writer writer;
404 limits budget;
405 budget.node();
406 value.apply(value_writer<Writer>{writer, budget});
407 if constexpr(requires { writer.output.overflow; })
408 if(writer.output.overflow)
409 throw codec_error{"Output byte limit exceeded"};
410 return std::move(writer.output.data);
411}
412
413inline std::string cbor_text(QCborStreamReader& reader)
414{
415 std::string text;
416 for(;;)
417 {
418 // Bound advertised chunk sizes BEFORE allocating; consume directly into
419 // UTF-8 storage without a QString round trip.
420 const auto size = reader.currentStringChunkSize();
421 if(size < 0 || static_cast<std::uint64_t>(size) > max_bytes - text.size())
422 throw codec_error{"String byte limit exceeded"};
423 const auto offset = text.size();
424 text.resize(offset + static_cast<std::size_t>(size));
425 const auto chunk = reader.readStringChunk(text.data() + offset, size);
426 if(chunk.status == QCborStreamReader::Error)
427 throw codec_error{"Malformed CBOR text"};
428 text.resize(offset + static_cast<std::size_t>(chunk.data));
429 if(chunk.status == QCborStreamReader::EndOfString)
430 return text;
431 // CBOR requires each chunk (not merely their concatenation) to be UTF-8.
432 utf8(std::string_view{text}.substr(offset));
433 }
434}
435
436inline ossia::value
437cbor_value(QCborStreamReader& reader, limits& budget, std::size_t depth = 0)
438{
439 budget.node();
440 // After an error (e.g. input ending inside a map) the reader keeps reporting
441 // the type it had cached: isString() stays true and the string accessors then
442 // read an invalid tinycbor value (an assertion in a debug Qt).
443 if(reader.lastError() != QCborError::NoError)
444 throw codec_error{"Malformed CBOR input"};
445 ossia::value result;
446 if(reader.isContainer())
447 {
448 limits::depth(depth + 1);
449 const bool map = reader.isMap();
450 if(reader.isLengthKnown() && reader.length() > max_nodes)
451 throw codec_error{"Value count limit exceeded"};
452 if(!reader.enterContainer())
453 throw codec_error{"Malformed CBOR container"};
454 if(map)
455 {
456 ossia::value_map_type values;
457 std::unordered_set<std::string> keys;
458 while(reader.hasNext())
459 {
460 budget.node();
461 if(reader.lastError() != QCborError::NoError)
462 throw codec_error{"Malformed CBOR input"};
463 if(!reader.isString())
464 throw codec_error{"CBOR map keys must be text"};
465 auto key = cbor_text(reader);
466 if(!keys.insert(key).second)
467 throw codec_error{"Duplicate map key"};
468 values.emplace_back(std::move(key), cbor_value(reader, budget, depth + 1));
469 }
470 result = std::move(values);
471 }
472 else
473 {
474 std::vector<ossia::value> values;
475 while(reader.hasNext())
476 values.push_back(cbor_value(reader, budget, depth + 1));
477 result = std::move(values);
478 }
479 if(!reader.leaveContainer())
480 throw codec_error{"Malformed CBOR container"};
481 return result;
482 }
483 if(reader.isString())
484 return cbor_text(reader);
485 if(reader.isUnsignedInteger())
486 result = unsigned_integer(reader.toUnsignedInteger());
487 else if(reader.isNegativeInteger())
488 {
489 // QCborNegativeInteger stores the absolute magnitude, with 0 meaning 2^64.
490 const auto magnitude = static_cast<quint64>(reader.toNegativeInteger());
491 if(magnitude == 0 || magnitude > 2147483648ULL)
492 throw codec_error{"Integer outside int32 range"};
493 result = integer(-static_cast<std::int64_t>(magnitude));
494 }
495 else if(reader.isBool())
496 result = reader.toBool();
497 else if(reader.isNull())
498 result = ossia::impulse{};
499 else if(reader.isFloat16())
500 result = floating(static_cast<float>(reader.toFloat16()));
501 else if(reader.isFloat())
502 result = floating(reader.toFloat());
503 else if(reader.isDouble())
504 result = floating(reader.toDouble());
505 else
506 throw codec_error{
507 "Unsupported or malformed CBOR value (tags, bytes and undefined are not "
508 "values)"};
509 if(!reader.next())
510 throw codec_error{"Malformed CBOR value"};
511 return result;
512}
513
514inline ossia::value from_cbor(std::string_view input)
515{
516 if(input.size() > max_bytes)
517 throw codec_error{"Input byte limit exceeded"};
518 QCborStreamReader reader{input.data(), static_cast<qsizetype>(input.size())};
519 limits budget;
520 auto result = cbor_value(reader, budget);
521 if(reader.lastError() != QCborError::NoError)
522 throw codec_error{"Malformed CBOR input"};
523 if(reader.currentOffset() != static_cast<qint64>(input.size()))
524 throw codec_error{"Trailing CBOR input"};
525 return result;
526}
527
528enum class Format
529{
530 JSON,
531 CBOR,
532 Text,
533 Binary
534};
535enum class TextStyle
536{
537 Plain,
538 Pretty
539};
540enum class TextType
541{
542 Auto,
543 String,
544 Integer,
545 Float,
546 Boolean,
547 List
548};
549enum class LineEnding
550{
551 None,
552 LF,
553 CRLF
554};
555enum class ByteOrder
556{
557 Little,
558 Big
559};
560enum class BinaryMode
561{
562 FreeFlow,
563 Layout
564};
565enum class ScalarType
566{
567 u8,
568 i8,
569 u16,
570 i16,
571 u32,
572 i32,
573 f32,
574 f64
575};
576
577inline void append_bytes(std::string& output, std::string_view bytes)
578{
579 if(bytes.size() > max_bytes - output.size())
580 throw codec_error{"Output byte limit exceeded"};
581 output.append(bytes);
582}
583
584inline std::string_view line_ending(LineEnding ending)
585{
586 switch(ending)
587 {
588 case LineEnding::LF:
589 return "\n";
590 case LineEnding::CRLF:
591 return "\r\n";
592 default:
593 return {};
594 }
595}
596
597// Plain text infers shape: root strings are raw, flat lists/vectors delimited
598// JSON tokens, nested containers JSON. String and List decoding resolve the
599// unavoidable root-string and one-element-list ambiguities.
600inline ossia::value text_scalar(std::string_view text, TextType type)
601{
602 if(type == TextType::String)
603 return std::string{text};
604 if(type == TextType::Boolean || type == TextType::Auto)
605 {
606 if(text == "true")
607 return true;
608 if(text == "false")
609 return false;
610 if(type == TextType::Boolean)
611 throw codec_error{"Expected true or false"};
612 }
613 if(type == TextType::Integer
614 || (type == TextType::Auto && text.find_first_of(".eE") == text.npos))
615 {
616 std::int32_t number{};
617 auto parsed = std::from_chars(text.data(), text.data() + text.size(), number);
618 if(parsed.ec == std::errc{} && parsed.ptr == text.data() + text.size())
619 return number;
620 if(type == TextType::Integer || parsed.ec == std::errc::result_out_of_range)
621 throw codec_error{"Expected int32 text"};
622 }
623 double number{};
624 if(parse_float64(text, number))
625 return floating(number);
626 const bool numeric
627 = !text.empty()
628 && ((text.front() >= '0' && text.front() <= '9') || text.front() == '-'
629 || text.front() == '+' || text.front() == '.');
630 if(type == TextType::Float || numeric)
631 throw codec_error{"Invalid or out-of-range numeric text"};
632 return std::string{text};
633}
634
635inline bool structured_text(std::string_view text)
636{
637 const auto start = text.find_first_not_of(" \t\r\n");
638 if(start == text.npos)
639 return false;
640 text.remove_prefix(start);
641 const char first = text.front();
642 return first == '[' || first == '{' || first == '"' || first == '-'
643 || (first >= '0' && first <= '9') || text == "true" || text == "false"
644 || text == "null";
645}
646
647// Use the very same formatter as value_to_pretty_string, but with a bounded
648// output iterator: the public helper allocates an unbounded intermediate string.
650{
651 using iterator_category = std::output_iterator_tag;
652 using difference_type = std::ptrdiff_t;
653 using value_type = void;
654 using pointer = void;
655 using reference = void;
656 json_output* output;
657 pretty_output_iterator& operator*() { return *this; }
658 pretty_output_iterator& operator++() { return *this; }
659 pretty_output_iterator operator++(int) { return *this; }
660 pretty_output_iterator& operator=(char c)
661 {
662 output->Put(c);
663 return *this;
664 }
665};
666
668{
669 void null() { }
670 void number(int) { }
671 void number(float) { }
672 void boolean(bool) { }
673 void text(std::string_view text) { utf8(text); }
674 void key(std::string_view text) { utf8(text); }
675 void array(std::size_t) { }
676 void map(std::size_t) { }
677 void end_array() { }
678 void end_map() { }
679};
680
681inline void validate_value(const ossia::value& value)
682{
683 validation_writer writer;
684 limits budget;
685 budget.node();
686 value.apply(value_writer<validation_writer>{writer, budget});
687}
688
689inline std::string to_pretty(const ossia::value& value)
690{
691 validate_value(value);
692 json_output bounded;
693 fmt::format_to(pretty_output_iterator{&bounded}, "{}", value);
694 return std::move(bounded.data);
695}
696
697inline ossia::value from_pretty(std::string_view text)
698{
699 // Bound recursion and allocation BEFORE entering libossia's recursive grammar.
700 // Outside strings every value starts with a type name, and vector coordinates
701 // start with a number. Counting both conservatively also bounds list storage.
702 limits budget;
703 std::size_t depth{};
704 bool quoted{};
705 for(std::size_t i = 0; i < text.size();)
706 {
707 const char c = text[i++];
708 if(quoted)
709 {
710 if(c == '\\' && i < text.size() && text[i] == '"')
711 ++i;
712 else if(c == '"')
713 quoted = false;
714 continue;
715 }
716 if(c == '"')
717 quoted = true;
718 else if(c == '[')
719 limits::depth(++depth);
720 else if(c == ']')
721 {
722 if(depth == 0)
723 throw codec_error{"Unmatched pretty container"};
724 --depth;
725 }
726 else if((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'))
727 {
728 budget.node();
729 while(
730 i < text.size()
731 && ((text[i] >= 'a' && text[i] <= 'z') || (text[i] >= '0' && text[i] <= '9')))
732 ++i;
733 }
734 else if((c >= '0' && c <= '9') || c == '-' || c == '+' || c == '.')
735 {
736 budget.node();
737 const auto start = i - 1;
738 while(i < text.size()
739 && ((text[i] >= '0' && text[i] <= '9') || text[i] == '.' || text[i] == 'e'
740 || text[i] == 'E' || text[i] == '+' || text[i] == '-'))
741 ++i;
742 double number{};
743 const auto token = text.substr(start, i - start);
744 if(!parse_float64(token, number))
745 throw codec_error{"Invalid pretty number"};
746 floating(number);
747 }
748 }
749 if(quoted || depth)
750 throw codec_error{"Unterminated pretty container or string"};
751 // The public parser accepts trailing junk. Restrict decoding to canonical
752 // formatter output so this cannot silently accept an incomplete payload.
753 auto result = ossia::parse_pretty_value(text);
754 if(!result.valid() || to_pretty(result) != text)
755 throw codec_error{"Expected canonical ossia pretty text (maps are display only)"};
756 return result;
757}
758
759inline void text_delimiter(std::string_view delimiter)
760{
761 if(delimiter.empty() || delimiter.size() > max_bytes
762 || delimiter.find_first_of("\"\\[]{}") != delimiter.npos)
763 throw codec_error{
764 "Delimiter must be nonempty, bounded and exclude JSON quotes, escapes and "
765 "brackets"};
766 utf8(delimiter);
767}
768
769inline std::size_t text_separator(std::string_view text, std::string_view delimiter)
770{
771 bool quoted{};
772 for(std::size_t i = 0; i < text.size(); ++i)
773 {
774 if(quoted && text[i] == '\\')
775 ++i;
776 else if(text[i] == '"')
777 quoted = !quoted;
778 else if(!quoted && text.substr(i).starts_with(delimiter))
779 return i;
780 }
781 return text.npos;
782}
783
784template <typename Values>
785std::string delimited_text(const Values& values, std::string_view delimiter)
786{
787 text_delimiter(delimiter);
788 if(values.size() >= max_nodes)
789 throw codec_error{"Value count limit exceeded"};
790 std::string output;
791 std::size_t field_start{};
792 bool first = true;
793 for(const auto& value : values)
794 {
795 auto token = encode<json_writer>(value);
796 if(text_separator(token, delimiter) != token.npos)
797 throw codec_error{"Numeric text field contains the delimiter"};
798 if(!first)
799 {
800 const auto boundary = output.size();
801 append_bytes(output, delimiter);
802 if(text_separator(std::string_view{output}.substr(field_start), delimiter)
803 != boundary - field_start)
804 throw codec_error{"Text field overlaps the delimiter"};
805 field_start = output.size();
806 }
807 append_bytes(output, token);
808 first = false;
809 }
810 return output;
811}
812
813inline ossia::value from_text(
814 std::string_view text, TextStyle style, TextType type, std::string_view delimiter,
815 LineEnding ending)
816{
817 if(text.size() > max_bytes)
818 throw codec_error{"Input byte limit exceeded"};
819 utf8(text);
820 // String interpretation is a byte-for-byte UTF-8 escape hatch, even when
821 // the payload looks like JSON or ends in the selected line ending.
822 if(type == TextType::String)
823 return std::string{text};
824 const auto suffix = line_ending(ending);
825 if(!suffix.empty() && text.ends_with(suffix))
826 text.remove_suffix(suffix.size());
827 if(style == TextStyle::Pretty)
828 return from_pretty(text);
829 if(type != TextType::Auto && type != TextType::List)
830 return text_scalar(text, type);
831 const auto start = text.find_first_not_of(" \t\r\n");
832 if(start != text.npos && (text[start] == '[' || text[start] == '{'))
833 return from_json(text);
834 text_delimiter(delimiter);
835 auto separator = text_separator(text, delimiter);
836 if(type != TextType::List && separator == text.npos)
837 return structured_text(text) ? from_json(text) : text_scalar(text, TextType::Auto);
838 std::vector<ossia::value> values;
839 limits budget;
840 budget.node();
841 for(;;)
842 {
843 budget.node();
844 const auto token = text.substr(0, separator);
845 const auto token_start = token.find_first_not_of(" \t\r\n");
846 if(token_start != token.npos
847 && (token[token_start] == '[' || token[token_start] == '{'))
848 throw codec_error{"Nested text containers require a complete JSON representation"};
849 values.push_back(
850 structured_text(token) ? from_json(token) : text_scalar(token, TextType::Auto));
851 if(separator == text.npos)
852 break;
853 text.remove_prefix(separator + delimiter.size());
854 separator = text_separator(text, delimiter);
855 }
856 return values;
857}
858
859inline std::string to_text(
860 const ossia::value& value, TextStyle style, std::string_view delimiter,
861 LineEnding ending)
862{
863 if(const auto* list = value.target<std::vector<ossia::value>>();
864 list && list->size() >= max_nodes)
865 throw codec_error{"Value count limit exceeded"};
866 std::string output;
867 if(style == TextStyle::Pretty)
868 output = to_pretty(value);
869 else if(const auto* text = value.target<std::string>())
870 {
871 if(text->size() > max_bytes)
872 throw codec_error{"String byte limit exceeded"};
873 utf8(*text);
874 // Raw strings have no added framing, preserving embedded NUL and endings.
875 return *text;
876 }
877 else if(
878 const auto* list = value.target<std::vector<ossia::value>>();
879 list && !list->empty()
880 && std::all_of(list->begin(), list->end(), [](const ossia::value& item) {
881 return item.target<int>() || item.target<float>() || item.target<bool>()
882 || item.target<std::string>() || item.target<ossia::impulse>();
883 }))
884 output = delimited_text(*list, delimiter);
885 else if(const auto* vector = value.target<ossia::vec2f>())
886 output = delimited_text(*vector, delimiter);
887 else if(const auto* vector = value.target<ossia::vec3f>())
888 output = delimited_text(*vector, delimiter);
889 else if(const auto* vector = value.target<ossia::vec4f>())
890 output = delimited_text(*vector, delimiter);
891 else
892 output = encode<json_writer>(value);
893 append_bytes(output, line_ending(ending));
894 return output;
895}
896
897// Strict subset of Python struct. No prefix: selected endian, packed.
898// < little, > or ! big, = native endian packed, @ native endian/alignment.
899// [count]b B h H i I q Q f d ? s x, with whitespace only between fields.
900// Ns is ONE exact-size raw string (never silently padded or truncated).
901// Nx is explicit zero padding; zero repeats are numeric alignment directives.
902// Native ABI requires the standard widths below; no automatic trailing padding.
903// Decoding returns a flat list: integer results must fit int32 and floating
904// results round to finite float32 without overflow or nonzero-to-zero underflow.
905inline std::size_t binary_width(char code)
906{
907 switch(code)
908 {
909 case 'b':
910 case 'B':
911 case '?':
912 case 's':
913 case 'x':
914 return 1;
915 case 'h':
916 case 'H':
917 return 2;
918 case 'i':
919 case 'I':
920 case 'f':
921 return 4;
922 case 'q':
923 case 'Q':
924 case 'd':
925 return 8;
926 default:
927 throw codec_error{"Unknown binary format code"};
928 }
929}
930
931inline char scalar_code(ScalarType type)
932{
933 switch(type)
934 {
935 case ScalarType::u8:
936 return 'B';
937 case ScalarType::i8:
938 return 'b';
939 case ScalarType::u16:
940 return 'H';
941 case ScalarType::i16:
942 return 'h';
943 case ScalarType::u32:
944 return 'I';
945 case ScalarType::i32:
946 return 'i';
947 case ScalarType::f32:
948 return 'f';
949 case ScalarType::f64:
950 return 'd';
951 }
952 throw codec_error{"Unknown scalar type"};
953}
954
956{
957 char code;
958 std::size_t count;
959};
961{
962 ByteOrder order{};
963 std::vector<binary_field> fields;
964 std::size_t bytes{};
965 std::size_t values{};
966};
967
968static_assert(
969 CHAR_BIT == 8 && sizeof(short) == 2 && sizeof(int) == 4 && sizeof(long long) == 8
970 && sizeof(bool) == 1 && sizeof(float) == 4 && sizeof(double) == 8
971 && std::numeric_limits<float>::is_iec559 && std::numeric_limits<double>::is_iec559);
972static_assert(
973 std::endian::native == std::endian::little
974 || std::endian::native == std::endian::big);
975
976inline std::size_t native_alignment(char code)
977{
978 switch(code)
979 {
980 case 'h':
981 case 'H':
982 return alignof(short);
983 case 'i':
984 case 'I':
985 return alignof(int);
986 case 'q':
987 case 'Q':
988 return alignof(long long);
989 case 'f':
990 return alignof(float);
991 case 'd':
992 return alignof(double);
993 case '?':
994 return alignof(bool);
995 default:
996 return 1;
997 }
998}
999
1000inline binary_layout compile_layout(std::string_view format, ByteOrder order)
1001{
1002 if(format.size() > max_nodes)
1003 throw codec_error{"Format text limit exceeded"};
1004 binary_layout layout{order, {}, 0, 0};
1005 auto whitespace
1006 = [](char c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r'; };
1007 std::size_t pos{};
1008 while(pos < format.size() && whitespace(format[pos]))
1009 ++pos;
1010 bool native{};
1011 if(pos < format.size())
1012 {
1013 const char prefix = format[pos];
1014 if(prefix == '<' || prefix == '>' || prefix == '!' || prefix == '=' || prefix == '@')
1015 {
1016 ++pos;
1017 native = prefix == '@';
1018 layout.order = prefix == '<' ? ByteOrder::Little
1019 : prefix == '>' || prefix == '!' ? ByteOrder::Big
1020 : std::endian::native == std::endian::little ? ByteOrder::Little
1021 : ByteOrder::Big;
1022 }
1023 }
1024 while(pos < format.size())
1025 {
1026 if(whitespace(format[pos]))
1027 {
1028 ++pos;
1029 continue;
1030 }
1031 std::size_t count{};
1032 bool explicit_count{};
1033 while(pos < format.size() && format[pos] >= '0' && format[pos] <= '9')
1034 {
1035 explicit_count = true;
1036 count = count * 10 + (format[pos++] - '0');
1037 if(count > max_nodes)
1038 throw codec_error{"Binary count limit exceeded"};
1039 }
1040 if(!explicit_count)
1041 count = 1;
1042 if(pos == format.size())
1043 throw codec_error{"Invalid binary count"};
1044 const char code = format[pos++];
1045 const auto width = binary_width(code);
1046 if(count == 0 && (code == 's' || code == 'x'))
1047 throw codec_error{"Zero repeats require a numeric alignment code"};
1048 const auto alignment = native ? native_alignment(code) : 1;
1049 const auto padding = (alignment - layout.bytes % alignment) % alignment;
1050 if(padding > max_bytes - layout.bytes)
1051 throw codec_error{"Binary layout resource limit exceeded"};
1052 if(padding)
1053 {
1054 layout.fields.push_back({'x', padding});
1055 layout.bytes += padding;
1056 }
1057 const auto bytes = count * width;
1058 const auto values = code == 'x' ? 0 : code == 's' ? 1 : count;
1059 if(bytes > max_bytes - layout.bytes || values >= max_nodes - layout.values)
1060 throw codec_error{"Binary layout resource limit exceeded"};
1061 layout.bytes += bytes;
1062 layout.values += values;
1063 layout.fields.push_back({code, count});
1064 }
1065 if(layout.fields.empty())
1066 throw codec_error{"Empty binary layout"};
1067 return layout;
1068}
1069
1071{
1072 std::string source;
1073 ByteOrder order{};
1074 bool initialized{};
1075 binary_layout layout;
1076 std::string error;
1077 const binary_layout& get(std::string_view format, ByteOrder requested)
1078 {
1079 if(format.size() > max_nodes)
1080 throw codec_error{"Format text limit exceeded"};
1081 if(!initialized || source != format || order != requested)
1082 {
1083 source = format;
1084 order = requested;
1085 initialized = true;
1086 error.clear();
1087 try
1088 {
1089 layout = compile_layout(format, requested);
1090 }
1091 catch(const codec_error& e)
1092 {
1093 error = e.what();
1094 }
1095 }
1096 if(!error.empty())
1097 throw codec_error{error};
1098 return layout;
1099 }
1100};
1101
1102inline void
1103write_word(std::string& output, std::uint64_t word, std::size_t width, ByteOrder order)
1104{
1105 if(width > max_bytes - output.size())
1106 throw codec_error{"Output byte limit exceeded"};
1107 for(std::size_t i = 0; i < width; ++i)
1108 {
1109 const auto shift = 8 * (order == ByteOrder::Little ? i : width - 1 - i);
1110 output.push_back(static_cast<char>((word >> shift) & 255));
1111 }
1112}
1113
1114inline std::uint64_t
1115read_word(std::string_view input, std::size_t& pos, std::size_t width, ByteOrder order)
1116{
1117 if(width > input.size() - pos)
1118 throw codec_error{"Truncated binary input"};
1119 std::uint64_t word{};
1120 for(std::size_t i = 0; i < width; ++i)
1121 {
1122 const auto shift = 8 * (order == ByteOrder::Little ? i : width - 1 - i);
1123 word |= std::uint64_t(static_cast<unsigned char>(input[pos++])) << shift;
1124 }
1125 return word;
1126}
1127
1128inline ossia::value
1129read_scalar(std::string_view input, std::size_t& pos, char code, ByteOrder order)
1130{
1131 const auto width = binary_width(code);
1132 const auto word = read_word(input, pos, width, order);
1133 switch(code)
1134 {
1135 case 'f':
1136 return floating(std::bit_cast<float>(static_cast<std::uint32_t>(word)));
1137 case 'd':
1138 return floating(std::bit_cast<double>(word));
1139 case '?':
1140 if(word > 1)
1141 throw codec_error{"Boolean byte must be 0 or 1"};
1142 return bool(word);
1143 case 'b':
1144 case 'h':
1145 case 'i':
1146 case 'q': {
1147 const auto extended = width < 8 && (word & (std::uint64_t{1} << (width * 8 - 1)))
1148 ? word | (~std::uint64_t{0} << (width * 8))
1149 : word;
1150 return integer(std::bit_cast<std::int64_t>(extended));
1151 }
1152 default:
1153 return unsigned_integer(word);
1154 }
1155}
1156
1157inline void
1158write_scalar(std::string& output, const ossia::value& value, char code, ByteOrder order)
1159{
1160 const auto width = binary_width(code);
1161 if(code == 'f' || code == 'd')
1162 {
1163 double number;
1164 if(const auto* f = value.target<float>())
1165 number = *f;
1166 else if(const auto* i = value.target<int>())
1167 number = *i;
1168 else
1169 throw codec_error{"Floating field requires a number"};
1170 floating(number);
1171 const auto word
1172 = code == 'f'
1173 ? std::uint64_t(std::bit_cast<std::uint32_t>(static_cast<float>(number)))
1174 : std::bit_cast<std::uint64_t>(number);
1175 write_word(output, word, width, order);
1176 return;
1177 }
1178 std::int64_t number;
1179 if(const auto* i = value.target<int>())
1180 number = *i;
1181 else if(const auto* b = value.target<bool>())
1182 number = *b;
1183 else
1184 throw codec_error{"Integer field requires an integer or boolean"};
1185 const bool signed_type = code == 'b' || code == 'h' || code == 'i' || code == 'q';
1186 if(code == '?')
1187 {
1188 if(number < 0 || number > 1)
1189 throw codec_error{"Boolean field must be 0 or 1"};
1190 }
1191 else if(signed_type && width < 8)
1192 {
1193 const std::int64_t bound = std::int64_t{1} << (width * 8 - 1);
1194 if(number < -bound || number >= bound)
1195 throw codec_error{"Signed field overflow"};
1196 }
1197 else if(!signed_type)
1198 {
1199 if(number < 0
1200 || (width < 8 && std::uint64_t(number) >= (std::uint64_t{1} << (width * 8))))
1201 throw codec_error{"Unsigned field overflow"};
1202 }
1203 write_word(output, static_cast<std::uint64_t>(number), width, order);
1204}
1205
1207{
1208 std::string& output;
1209 ByteOrder order;
1210 limits& budget;
1211 std::size_t depth{};
1212 void operator()() const { throw codec_error{"Cannot serialize an unset value"}; }
1213 void operator()(ossia::impulse) const
1214 {
1215 throw codec_error{"Binary impulse has no width"};
1216 }
1217 void operator()(int v) const { write_scalar(output, v, 'i', order); }
1218 void operator()(float v) const { write_scalar(output, v, 'f', order); }
1219 void operator()(bool v) const { write_scalar(output, v, '?', order); }
1220 void operator()(const std::string& v) const { append_bytes(output, v); }
1221 void operator()(const ossia::value_map_type&) const
1222 {
1223 throw codec_error{"Binary maps have ambiguous field order"};
1224 }
1225 void operator()(const std::vector<ossia::value>& values) const
1226 {
1227 limits::depth(depth + 1);
1228 for(const auto& value : values)
1229 {
1230 budget.node();
1231 value.apply(free_binary_writer{output, order, budget, depth + 1});
1232 }
1233 }
1234 template <std::size_t N>
1235 void operator()(const std::array<float, N>& values) const
1236 {
1237 limits::depth(depth + 1);
1238 for(float value : values)
1239 {
1240 budget.node();
1241 (*this)(value);
1242 }
1243 }
1244};
1245
1246template <typename Values>
1247std::string to_binary_fields(const Values& values, const binary_layout& layout)
1248{
1249 std::string output;
1250 if(values.size() != layout.values)
1251 throw codec_error{"Binary layout input field count mismatch"};
1252 output.reserve(layout.bytes);
1253 std::size_t index{};
1254 for(const auto& field : layout.fields)
1255 {
1256 if(field.code == 'x')
1257 {
1258 output.append(field.count, '\0');
1259 continue;
1260 }
1261 const auto repeats = field.code == 's' ? 1 : field.count;
1262 for(std::size_t i = 0; i < repeats; ++i)
1263 {
1264 const ossia::value& item = values[index++];
1265 if(field.code == 's')
1266 {
1267 const auto* text = item.target<std::string>();
1268 if(!text || text->size() != field.count)
1269 throw codec_error{"Fixed string requires exactly its declared byte count"};
1270 append_bytes(output, *text);
1271 }
1272 else
1273 write_scalar(output, item, field.code, layout.order);
1274 }
1275 }
1276 return output;
1277}
1278
1279inline std::string
1280to_binary(const ossia::value& value, ByteOrder order, const binary_layout* layout)
1281{
1282 if(!layout)
1283 {
1284 std::string output;
1285 limits budget;
1286 budget.node();
1287 value.apply(free_binary_writer{output, order, budget});
1288 return output;
1289 }
1290 if(const auto* list = value.target<std::vector<ossia::value>>())
1291 return to_binary_fields(*list, *layout);
1292 if(const auto* vector = value.target<ossia::vec2f>())
1293 return to_binary_fields(*vector, *layout);
1294 if(const auto* vector = value.target<ossia::vec3f>())
1295 return to_binary_fields(*vector, *layout);
1296 if(const auto* vector = value.target<ossia::vec4f>())
1297 return to_binary_fields(*vector, *layout);
1298 return to_binary_fields(std::span{&value, 1}, *layout);
1299}
1300
1301inline ossia::value from_binary(
1302 std::string_view input, ByteOrder order, ScalarType type,
1303 const binary_layout* layout)
1304{
1305 if(input.size() > max_bytes)
1306 throw codec_error{"Input byte limit exceeded"};
1307 std::vector<ossia::value> values;
1308 std::size_t pos{};
1309 if(!layout)
1310 {
1311 const auto code = scalar_code(type);
1312 const auto width = binary_width(code);
1313 if(input.size() % width)
1314 throw codec_error{"Truncated binary scalar"};
1315 if(input.size() / width >= max_nodes)
1316 throw codec_error{"Value count limit exceeded"};
1317 values.reserve(input.size() / width);
1318 while(pos < input.size())
1319 values.push_back(read_scalar(input, pos, code, order));
1320 }
1321 else
1322 {
1323 if(input.size() != layout->bytes)
1324 throw codec_error{"Truncated or trailing binary input"};
1325 values.reserve(layout->values);
1326 for(const auto& field : layout->fields)
1327 {
1328 if(field.code == 'x')
1329 {
1330 pos += field.count;
1331 continue;
1332 }
1333 if(field.code == 's')
1334 {
1335 values.emplace_back(std::string{input.substr(pos, field.count)});
1336 pos += field.count;
1337 }
1338 else
1339 for(std::size_t i = 0; i < field.count; ++i)
1340 values.push_back(read_scalar(input, pos, field.code, layout->order));
1341 }
1342 }
1343 return values;
1344}
1345
1346// Callback outputs emit once per input update, including repeated equal events.
1347// Each failed update emits an explicit empty/unset result and Success=false,
1348// never the previous successful payload. Error is empty on successful updates.
1349// Conversion finishes before publishing; callback order is error, status, data.
1350template <typename Output, typename Function>
1351void publish(Output& output, Function&& function)
1352{
1353 using result_type = decltype(function());
1354 result_type result;
1355 std::string error;
1356 try
1357 {
1358 result = function();
1359 }
1360 catch(const std::exception& e)
1361 {
1362 error = e.what();
1363 }
1364 output.error(error);
1365 output.success(error.empty());
1366 output.result(std::move(result));
1367}
1368}
1369
1370namespace avnd_tools
1371{
1373{
1374 halp_meta(name, "Deserialize")
1375 halp_meta(c_name, "avnd_deserialize")
1376 halp_meta(author, "ossia team")
1377 halp_meta(category, "Control/Serialization")
1378 halp_meta(uuid, "5d265cd1-014f-48bb-9c51-d594c8b58b61")
1379 halp_meta(
1380 description,
1381 "Decode Bytes as strict JSON, JSON-compatible CBOR, UTF-8 text or binary. "
1382 "Integers must fit int32; floats round to finite float32 without underflow. "
1383 "Binary decoding returns an ordered list. Limits: 8 MiB, 65536 nodes, 64 "
1384 "containers. Errors clear Value; Error is empty on success.")
1385 struct ins
1386 {
1387 struct : halp::val_port<"Bytes", std::string>
1388 {
1389 void update(Deserialize& self) { self.process(); }
1390 } input;
1391 halp::enum_t<value_serialization::Format, "Format"> format;
1392 struct : halp::enum_t<value_serialization::TextStyle, "Text style">
1393 {
1394 halp_meta(
1395 description,
1396 "Plain reads delimited fields or nested JSON. Pretty reads canonical ossia "
1397 "type labels only; maps are display only, floats were rounded to two "
1398 "decimals, and only quotes are escaped.")
1399 } text_style;
1400 struct : halp::enum_t<value_serialization::TextType, "Interpretation">
1401 {
1402 halp_meta(
1403 description,
1404 "Auto recognizes separators, JSON and exact scalar tokens, otherwise raw "
1405 "strings. String preserves all UTF-8 bytes and line endings. List forces "
1406 "single delimited fields into a list. Numeric options require exact scalars. "
1407 "Pretty uses its own labels unless String is selected.")
1408 } text_type;
1409 struct : halp::lineedit<"Delimiter", ",">
1410 {
1411 halp_meta(
1412 description,
1413 "Separator for flat Plain lists and vectors, ignored inside JSON-quoted "
1414 "strings. Must exclude quotes, backslashes and brackets. Nested containers "
1415 "use JSON.")
1416 } delimiter;
1417 struct : halp::enum_t<value_serialization::LineEnding, "Line ending">
1418 {
1419 halp_meta(
1420 description,
1421 "Remove one matching suffix before parsing, except with String "
1422 "interpretation.")
1423 } line_ending;
1424 struct : halp::enum_t<value_serialization::ByteOrder, "Byte order">
1425 {
1426 halp_meta(
1427 description, "Raw scalar endian and the default for layouts without a prefix.")
1428 } byte_order;
1429 struct : halp::enum_t<value_serialization::BinaryMode, "Binary mode">
1430 {
1431 halp_meta(
1432 description,
1433 "FreeFlow reads repeated scalars; Layout reads explicitly typed fields. Both "
1434 "return lists.")
1435 } binary_mode;
1436 struct : halp::enum_t<value_serialization::ScalarType, "Scalar type">
1437 {
1438 halp_meta(
1439 description,
1440 "Repeated raw binary width. Integers must fit int32; floats round to finite "
1441 "float32 without underflow.")
1442 } scalar_type;
1443 struct : halp::lineedit<"Layout", "<B H I f 4s 2x">
1444 {
1445 halp_meta(
1446 description,
1447 "Python struct subset: < > ! = packed, @ native endian and alignment; "
1448 "no prefix uses Byte order and packed widths. [count]b B h H i I q Q f d ? s "
1449 "x. "
1450 "Counts <=65536; zero numeric counts align only. Ns is one exact-size raw "
1451 "string; Nx skips padding. No automatic end padding. Int32 and finite float32 "
1452 "results.")
1453 } layout;
1454 } inputs;
1455 struct
1456 {
1457 halp::callback<"Value", ossia::value> result;
1458 halp::callback<"Success", bool> success;
1459 halp::callback<"Error", std::string> error;
1460 } outputs;
1461
1462 struct ui
1463 {
1464 halp_meta(layout, halp::layouts::vbox)
1465 halp::item<&ins::format> format;
1467 {
1468 halp_meta(layout, halp::layouts::tabs)
1469 halp_flag(hide_tabs);
1470 static constexpr auto model = &ins::format;
1471 struct
1472 {
1473 halp_meta(name, "JSON")
1474 halp_meta(layout, halp::layouts::vbox)
1475 halp::label types{.text = "Strict UTF-8 JSON: int32, finite float32."};
1476 halp::label values{.text = "null = impulse; arrays = lists; ordered maps."};
1477 halp::label limits{.text = "8 MiB, 65536 values/keys, 64 containers."};
1478 } json;
1479 struct
1480 {
1481 halp_meta(name, "CBOR")
1482 halp_meta(layout, halp::layouts::vbox)
1483 halp::label types{.text = "JSON-compatible CBOR: int32, finite float32."};
1484 halp::label keys{.text = "UTF-8 text keys; no tags, byte strings or undefined."};
1485 halp::label values{.text = "null = impulse; arrays = lists; ordered maps."};
1486 halp::label limits{.text = "8 MiB, 65536 values/keys, 64 containers."};
1487 } cbor;
1488 struct
1489 {
1490 halp_meta(name, "Text")
1491 halp_meta(layout, halp::layouts::vbox)
1492 halp::item<&ins::text_style> style;
1493 halp::item<&ins::text_type> type;
1494 halp::item<&ins::delimiter> delimiter;
1495 halp::item<&ins::line_ending> ending;
1496 } text;
1497 struct
1498 {
1499 halp_meta(name, "Binary")
1500 halp_meta(layout, halp::layouts::vbox)
1501 halp::item<&ins::byte_order> order;
1502 halp::item<&ins::binary_mode> mode;
1503 halp::item<&ins::scalar_type> scalar;
1504 halp::item<&ins::layout> format_text;
1505 } binary;
1506 } settings;
1507 };
1508
1510 void process()
1511 {
1512 using namespace value_serialization;
1513 publish(outputs, [&]() -> ossia::value {
1514 const auto& bytes = inputs.input.value;
1515 switch(inputs.format.value)
1516 {
1517 case Format::JSON:
1518 return from_json(bytes);
1519 case Format::CBOR:
1520 return from_cbor(bytes);
1521 case Format::Text:
1522 return from_text(
1523 bytes, inputs.text_style.value, inputs.text_type.value,
1524 inputs.delimiter.value, inputs.line_ending.value);
1525 case Format::Binary:
1526 return from_binary(
1527 bytes, inputs.byte_order.value, inputs.scalar_type.value,
1528 inputs.binary_mode.value == BinaryMode::Layout
1529 ? &cache.get(inputs.layout.value, inputs.byte_order.value)
1530 : nullptr);
1531 }
1532 throw codec_error{"Unknown format"};
1533 });
1534 }
1535};
1536
1538{
1539 halp_meta(name, "Serialize")
1540 halp_meta(c_name, "avnd_serialize")
1541 halp_meta(author, "ossia team")
1542 halp_meta(category, "Control/Serialization")
1543 halp_meta(uuid, "5d265cd1-014f-48bb-9c51-d594c8b58b62")
1544 halp_meta(
1545 description,
1546 "Encode Value as strict JSON, JSON-compatible CBOR, UTF-8 text or binary. "
1547 "Plain Text infers shape: raw strings, delimited flat lists/vectors, nested JSON; "
1548 "vectors decode as lists. Pretty Text is ossia display, not a lossless archive. "
1549 "Free-flow binary flattens lists/vectors; maps and impulses fail. "
1550 "Limits: 8 MiB, 65536 nodes, 64 containers. Errors clear Bytes.")
1551 struct ins
1552 {
1553 struct : halp::val_port<"Value", ossia::value>
1554 {
1555 void update(Serialize& self) { self.process(); }
1556 } input;
1557 halp::enum_t<value_serialization::Format, "Format"> format;
1558 struct : halp::toggle<"Pretty print">
1559 {
1560 halp_meta(
1561 description, "Indent JSON without changing its values or numeric precision.")
1562 } pretty;
1563 struct : halp::enum_t<value_serialization::TextStyle, "Text style">
1564 {
1565 halp_meta(
1566 description,
1567 "Plain infers shape: root strings are raw UTF-8, scalars are plain tokens, "
1568 "flat lists/vectors are delimited JSON tokens, nested containers use JSON. "
1569 "Pretty uses ossia type labels and two-decimal floats: lossy display; maps "
1570 "and ambiguous escaped strings cannot be decoded.")
1571 } text_style;
1572 struct : halp::lineedit<"Delimiter", ",">
1573 {
1574 halp_meta(
1575 description,
1576 "Separator for nonempty flat Plain lists and vectors. Strings are "
1577 "JSON-quoted; numeric delimiter collisions fail. Must exclude quotes, "
1578 "backslashes and brackets. One-field lists require List interpretation when "
1579 "decoding.")
1580 } delimiter;
1581 struct : halp::enum_t<value_serialization::LineEnding, "Line ending">
1582 {
1583 halp_meta(
1584 description,
1585 "Append a line ending to Text, except raw Plain strings which are unchanged.")
1586 } line_ending;
1587 struct : halp::enum_t<value_serialization::ByteOrder, "Byte order">
1588 {
1589 halp_meta(
1590 description, "Free-flow endian and the default for layouts without a prefix.")
1591 } byte_order;
1592 struct : halp::enum_t<value_serialization::BinaryMode, "Binary mode">
1593 {
1594 halp_meta(
1595 description,
1596 "FreeFlow writes int32, float32, bool bytes and raw strings, flattening lists "
1597 "and vectors without tags. Layout writes explicitly typed fields.")
1598 } binary_mode;
1599 struct : halp::lineedit<"Layout", "<B H I f 4s 2x">
1600 {
1601 halp_meta(
1602 description,
1603 "Python struct subset: < > ! = packed, @ native endian and alignment; "
1604 "no prefix uses Byte order and packed widths. [count]b B h H i I q Q f d ? s "
1605 "x. "
1606 "Counts <=65536; zero numeric counts align only. Ns consumes one exact-size "
1607 "raw string, never padded or truncated; Nx writes zeros. No automatic end "
1608 "padding.")
1609 } layout;
1610 } inputs;
1611 struct
1612 {
1613 halp::callback<"Bytes", std::string> result;
1614 halp::callback<"Success", bool> success;
1615 halp::callback<"Error", std::string> error;
1616 } outputs;
1617
1618 struct ui
1619 {
1620 halp_meta(layout, halp::layouts::vbox)
1621 halp::item<&ins::format> format;
1623 {
1624 halp_meta(layout, halp::layouts::tabs)
1625 halp_flag(hide_tabs);
1626 static constexpr auto model = &ins::format;
1627 struct
1628 {
1629 halp_meta(name, "JSON")
1630 halp_meta(layout, halp::layouts::vbox)
1631 halp::item<&ins::pretty> pretty;
1632 } json;
1633 struct
1634 {
1635 halp_meta(name, "CBOR")
1636 halp_meta(layout, halp::layouts::vbox)
1637 halp::label types{.text = "JSON-compatible CBOR: int32, finite float32."};
1638 halp::label values{.text = "Impulse = null; vectors/lists = arrays."};
1639 halp::label keys{.text = "UTF-8 text and unique map keys required."};
1640 halp::label limits{.text = "8 MiB, 65536 values/keys, 64 containers."};
1641 } cbor;
1642 struct
1643 {
1644 halp_meta(name, "Text")
1645 halp_meta(layout, halp::layouts::vbox)
1646 halp::item<&ins::text_style> style;
1647 halp::item<&ins::delimiter> delimiter;
1648 halp::item<&ins::line_ending> ending;
1649 } text;
1650 struct
1651 {
1652 halp_meta(name, "Binary")
1653 halp_meta(layout, halp::layouts::vbox)
1654 halp::item<&ins::byte_order> order;
1655 halp::item<&ins::binary_mode> mode;
1656 halp::item<&ins::layout> format_text;
1657 } binary;
1658 } settings;
1659 };
1660
1662 void process()
1663 {
1664 using namespace value_serialization;
1665 publish(outputs, [&]() -> std::string {
1666 const auto& value = inputs.input.value;
1667 switch(inputs.format.value)
1668 {
1669 case Format::JSON:
1670 return inputs.pretty.value ? encode<pretty_json_writer>(value)
1671 : encode<json_writer>(value);
1672 case Format::CBOR:
1673 return encode<cbor_writer>(value);
1674 case Format::Text:
1675 return to_text(
1676 value, inputs.text_style.value, inputs.delimiter.value,
1677 inputs.line_ending.value);
1678 case Format::Binary:
1679 return to_binary(
1680 value, inputs.byte_order.value,
1681 inputs.binary_mode.value == BinaryMode::Layout
1682 ? &cache.get(inputs.layout.value, inputs.byte_order.value)
1683 : nullptr);
1684 }
1685 throw codec_error{"Unknown format"};
1686 });
1687 }
1688};
1689}
Definition BaseScenarioSerialization.cpp:17
STL namespace.
Definition score-plugin-avnd/AvndProcesses/ValueSerialization.hpp:1386
Definition score-plugin-avnd/AvndProcesses/ValueSerialization.hpp:1467
Definition score-plugin-avnd/AvndProcesses/ValueSerialization.hpp:1463
Definition score-plugin-avnd/AvndProcesses/ValueSerialization.hpp:1373
Definition score-plugin-avnd/AvndProcesses/ValueSerialization.hpp:1552
Definition score-plugin-avnd/AvndProcesses/ValueSerialization.hpp:1623
Definition score-plugin-avnd/AvndProcesses/ValueSerialization.hpp:1619
Definition score-plugin-avnd/AvndProcesses/ValueSerialization.hpp:1538
Definition score-plugin-avnd/AvndProcesses/ValueSerialization.hpp:268
Definition score-plugin-avnd/AvndProcesses/ValueSerialization.hpp:956
Definition score-plugin-avnd/AvndProcesses/ValueSerialization.hpp:961
Definition score-plugin-avnd/AvndProcesses/ValueSerialization.hpp:294
Definition score-plugin-avnd/AvndProcesses/ValueSerialization.hpp:312
Definition score-plugin-avnd/AvndProcesses/ValueSerialization.hpp:71
Definition score-plugin-avnd/AvndProcesses/ValueSerialization.hpp:1207
Definition score-plugin-avnd/AvndProcesses/ValueSerialization.hpp:254
Definition score-plugin-avnd/AvndProcesses/ValueSerialization.hpp:155
Definition score-plugin-avnd/AvndProcesses/ValueSerialization.hpp:153
Definition score-plugin-avnd/AvndProcesses/ValueSerialization.hpp:1071
Definition score-plugin-avnd/AvndProcesses/ValueSerialization.hpp:101
Definition score-plugin-avnd/AvndProcesses/ValueSerialization.hpp:650
Definition score-plugin-avnd/AvndProcesses/ValueSerialization.hpp:668
Definition score-plugin-avnd/AvndProcesses/ValueSerialization.hpp:333