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 ossia::value result;
441 if(reader.isContainer())
442 {
443 limits::depth(depth + 1);
444 const bool map = reader.isMap();
445 if(reader.isLengthKnown() && reader.length() > max_nodes)
446 throw codec_error{"Value count limit exceeded"};
447 if(!reader.enterContainer())
448 throw codec_error{"Malformed CBOR container"};
449 if(map)
450 {
451 ossia::value_map_type values;
452 std::unordered_set<std::string> keys;
453 while(reader.hasNext())
454 {
455 budget.node();
456 if(!reader.isString())
457 throw codec_error{"CBOR map keys must be text"};
458 auto key = cbor_text(reader);
459 if(!keys.insert(key).second)
460 throw codec_error{"Duplicate map key"};
461 values.emplace_back(std::move(key), cbor_value(reader, budget, depth + 1));
462 }
463 result = std::move(values);
464 }
465 else
466 {
467 std::vector<ossia::value> values;
468 while(reader.hasNext())
469 values.push_back(cbor_value(reader, budget, depth + 1));
470 result = std::move(values);
471 }
472 if(!reader.leaveContainer())
473 throw codec_error{"Malformed CBOR container"};
474 return result;
475 }
476 if(reader.isString())
477 return cbor_text(reader);
478 if(reader.isUnsignedInteger())
479 result = unsigned_integer(reader.toUnsignedInteger());
480 else if(reader.isNegativeInteger())
481 {
482 // QCborNegativeInteger stores the absolute magnitude, with 0 meaning 2^64.
483 const auto magnitude = static_cast<quint64>(reader.toNegativeInteger());
484 if(magnitude == 0 || magnitude > 2147483648ULL)
485 throw codec_error{"Integer outside int32 range"};
486 result = integer(-static_cast<std::int64_t>(magnitude));
487 }
488 else if(reader.isBool())
489 result = reader.toBool();
490 else if(reader.isNull())
491 result = ossia::impulse{};
492 else if(reader.isFloat16())
493 result = floating(static_cast<float>(reader.toFloat16()));
494 else if(reader.isFloat())
495 result = floating(reader.toFloat());
496 else if(reader.isDouble())
497 result = floating(reader.toDouble());
498 else
499 throw codec_error{
500 "Unsupported or malformed CBOR value (tags, bytes and undefined are not "
501 "values)"};
502 if(!reader.next())
503 throw codec_error{"Malformed CBOR value"};
504 return result;
505}
506
507inline ossia::value from_cbor(std::string_view input)
508{
509 if(input.size() > max_bytes)
510 throw codec_error{"Input byte limit exceeded"};
511 QCborStreamReader reader{input.data(), static_cast<qsizetype>(input.size())};
512 limits budget;
513 auto result = cbor_value(reader, budget);
514 if(reader.lastError() != QCborError::NoError)
515 throw codec_error{"Malformed CBOR input"};
516 if(reader.currentOffset() != static_cast<qint64>(input.size()))
517 throw codec_error{"Trailing CBOR input"};
518 return result;
519}
520
521enum class Format
522{
523 JSON,
524 CBOR,
525 Text,
526 Binary
527};
528enum class TextStyle
529{
530 Plain,
531 Pretty
532};
533enum class TextType
534{
535 Auto,
536 String,
537 Integer,
538 Float,
539 Boolean,
540 List
541};
542enum class LineEnding
543{
544 None,
545 LF,
546 CRLF
547};
548enum class ByteOrder
549{
550 Little,
551 Big
552};
553enum class BinaryMode
554{
555 FreeFlow,
556 Layout
557};
558enum class ScalarType
559{
560 u8,
561 i8,
562 u16,
563 i16,
564 u32,
565 i32,
566 f32,
567 f64
568};
569
570inline void append_bytes(std::string& output, std::string_view bytes)
571{
572 if(bytes.size() > max_bytes - output.size())
573 throw codec_error{"Output byte limit exceeded"};
574 output.append(bytes);
575}
576
577inline std::string_view line_ending(LineEnding ending)
578{
579 switch(ending)
580 {
581 case LineEnding::LF:
582 return "\n";
583 case LineEnding::CRLF:
584 return "\r\n";
585 default:
586 return {};
587 }
588}
589
590// Plain text infers shape: root strings are raw, flat lists/vectors delimited
591// JSON tokens, nested containers JSON. String and List decoding resolve the
592// unavoidable root-string and one-element-list ambiguities.
593inline ossia::value text_scalar(std::string_view text, TextType type)
594{
595 if(type == TextType::String)
596 return std::string{text};
597 if(type == TextType::Boolean || type == TextType::Auto)
598 {
599 if(text == "true")
600 return true;
601 if(text == "false")
602 return false;
603 if(type == TextType::Boolean)
604 throw codec_error{"Expected true or false"};
605 }
606 if(type == TextType::Integer
607 || (type == TextType::Auto && text.find_first_of(".eE") == text.npos))
608 {
609 std::int32_t number{};
610 auto parsed = std::from_chars(text.data(), text.data() + text.size(), number);
611 if(parsed.ec == std::errc{} && parsed.ptr == text.data() + text.size())
612 return number;
613 if(type == TextType::Integer || parsed.ec == std::errc::result_out_of_range)
614 throw codec_error{"Expected int32 text"};
615 }
616 double number{};
617 if(parse_float64(text, number))
618 return floating(number);
619 const bool numeric
620 = !text.empty()
621 && ((text.front() >= '0' && text.front() <= '9') || text.front() == '-'
622 || text.front() == '+' || text.front() == '.');
623 if(type == TextType::Float || numeric)
624 throw codec_error{"Invalid or out-of-range numeric text"};
625 return std::string{text};
626}
627
628inline bool structured_text(std::string_view text)
629{
630 const auto start = text.find_first_not_of(" \t\r\n");
631 if(start == text.npos)
632 return false;
633 text.remove_prefix(start);
634 const char first = text.front();
635 return first == '[' || first == '{' || first == '"' || first == '-'
636 || (first >= '0' && first <= '9') || text == "true" || text == "false"
637 || text == "null";
638}
639
640// Use the very same formatter as value_to_pretty_string, but with a bounded
641// output iterator: the public helper allocates an unbounded intermediate string.
643{
644 using iterator_category = std::output_iterator_tag;
645 using difference_type = std::ptrdiff_t;
646 using value_type = void;
647 using pointer = void;
648 using reference = void;
649 json_output* output;
650 pretty_output_iterator& operator*() { return *this; }
651 pretty_output_iterator& operator++() { return *this; }
652 pretty_output_iterator operator++(int) { return *this; }
653 pretty_output_iterator& operator=(char c)
654 {
655 output->Put(c);
656 return *this;
657 }
658};
659
661{
662 void null() { }
663 void number(int) { }
664 void number(float) { }
665 void boolean(bool) { }
666 void text(std::string_view text) { utf8(text); }
667 void key(std::string_view text) { utf8(text); }
668 void array(std::size_t) { }
669 void map(std::size_t) { }
670 void end_array() { }
671 void end_map() { }
672};
673
674inline void validate_value(const ossia::value& value)
675{
676 validation_writer writer;
677 limits budget;
678 budget.node();
679 value.apply(value_writer<validation_writer>{writer, budget});
680}
681
682inline std::string to_pretty(const ossia::value& value)
683{
684 validate_value(value);
685 json_output bounded;
686 fmt::format_to(pretty_output_iterator{&bounded}, "{}", value);
687 return std::move(bounded.data);
688}
689
690inline ossia::value from_pretty(std::string_view text)
691{
692 // Bound recursion and allocation BEFORE entering libossia's recursive grammar.
693 // Outside strings every value starts with a type name, and vector coordinates
694 // start with a number. Counting both conservatively also bounds list storage.
695 limits budget;
696 std::size_t depth{};
697 bool quoted{};
698 for(std::size_t i = 0; i < text.size();)
699 {
700 const char c = text[i++];
701 if(quoted)
702 {
703 if(c == '\\' && i < text.size() && text[i] == '"')
704 ++i;
705 else if(c == '"')
706 quoted = false;
707 continue;
708 }
709 if(c == '"')
710 quoted = true;
711 else if(c == '[')
712 limits::depth(++depth);
713 else if(c == ']')
714 {
715 if(depth == 0)
716 throw codec_error{"Unmatched pretty container"};
717 --depth;
718 }
719 else if((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'))
720 {
721 budget.node();
722 while(
723 i < text.size()
724 && ((text[i] >= 'a' && text[i] <= 'z') || (text[i] >= '0' && text[i] <= '9')))
725 ++i;
726 }
727 else if((c >= '0' && c <= '9') || c == '-' || c == '+' || c == '.')
728 {
729 budget.node();
730 const auto start = i - 1;
731 while(i < text.size()
732 && ((text[i] >= '0' && text[i] <= '9') || text[i] == '.' || text[i] == 'e'
733 || text[i] == 'E' || text[i] == '+' || text[i] == '-'))
734 ++i;
735 double number{};
736 const auto token = text.substr(start, i - start);
737 if(!parse_float64(token, number))
738 throw codec_error{"Invalid pretty number"};
739 floating(number);
740 }
741 }
742 if(quoted || depth)
743 throw codec_error{"Unterminated pretty container or string"};
744 // The public parser accepts trailing junk. Restrict decoding to canonical
745 // formatter output so this cannot silently accept an incomplete payload.
746 auto result = ossia::parse_pretty_value(text);
747 if(!result.valid() || to_pretty(result) != text)
748 throw codec_error{"Expected canonical ossia pretty text (maps are display only)"};
749 return result;
750}
751
752inline void text_delimiter(std::string_view delimiter)
753{
754 if(delimiter.empty() || delimiter.size() > max_bytes
755 || delimiter.find_first_of("\"\\[]{}") != delimiter.npos)
756 throw codec_error{
757 "Delimiter must be nonempty, bounded and exclude JSON quotes, escapes and "
758 "brackets"};
759 utf8(delimiter);
760}
761
762inline std::size_t text_separator(std::string_view text, std::string_view delimiter)
763{
764 bool quoted{};
765 for(std::size_t i = 0; i < text.size(); ++i)
766 {
767 if(quoted && text[i] == '\\')
768 ++i;
769 else if(text[i] == '"')
770 quoted = !quoted;
771 else if(!quoted && text.substr(i).starts_with(delimiter))
772 return i;
773 }
774 return text.npos;
775}
776
777template <typename Values>
778std::string delimited_text(const Values& values, std::string_view delimiter)
779{
780 text_delimiter(delimiter);
781 if(values.size() >= max_nodes)
782 throw codec_error{"Value count limit exceeded"};
783 std::string output;
784 std::size_t field_start{};
785 bool first = true;
786 for(const auto& value : values)
787 {
788 auto token = encode<json_writer>(value);
789 if(text_separator(token, delimiter) != token.npos)
790 throw codec_error{"Numeric text field contains the delimiter"};
791 if(!first)
792 {
793 const auto boundary = output.size();
794 append_bytes(output, delimiter);
795 if(text_separator(std::string_view{output}.substr(field_start), delimiter)
796 != boundary - field_start)
797 throw codec_error{"Text field overlaps the delimiter"};
798 field_start = output.size();
799 }
800 append_bytes(output, token);
801 first = false;
802 }
803 return output;
804}
805
806inline ossia::value from_text(
807 std::string_view text, TextStyle style, TextType type, std::string_view delimiter,
808 LineEnding ending)
809{
810 if(text.size() > max_bytes)
811 throw codec_error{"Input byte limit exceeded"};
812 utf8(text);
813 // String interpretation is a byte-for-byte UTF-8 escape hatch, even when
814 // the payload looks like JSON or ends in the selected line ending.
815 if(type == TextType::String)
816 return std::string{text};
817 const auto suffix = line_ending(ending);
818 if(!suffix.empty() && text.ends_with(suffix))
819 text.remove_suffix(suffix.size());
820 if(style == TextStyle::Pretty)
821 return from_pretty(text);
822 if(type != TextType::Auto && type != TextType::List)
823 return text_scalar(text, type);
824 const auto start = text.find_first_not_of(" \t\r\n");
825 if(start != text.npos && (text[start] == '[' || text[start] == '{'))
826 return from_json(text);
827 text_delimiter(delimiter);
828 auto separator = text_separator(text, delimiter);
829 if(type != TextType::List && separator == text.npos)
830 return structured_text(text) ? from_json(text) : text_scalar(text, TextType::Auto);
831 std::vector<ossia::value> values;
832 limits budget;
833 budget.node();
834 for(;;)
835 {
836 budget.node();
837 const auto token = text.substr(0, separator);
838 const auto token_start = token.find_first_not_of(" \t\r\n");
839 if(token_start != token.npos
840 && (token[token_start] == '[' || token[token_start] == '{'))
841 throw codec_error{"Nested text containers require a complete JSON representation"};
842 values.push_back(
843 structured_text(token) ? from_json(token) : text_scalar(token, TextType::Auto));
844 if(separator == text.npos)
845 break;
846 text.remove_prefix(separator + delimiter.size());
847 separator = text_separator(text, delimiter);
848 }
849 return values;
850}
851
852inline std::string to_text(
853 const ossia::value& value, TextStyle style, std::string_view delimiter,
854 LineEnding ending)
855{
856 if(const auto* list = value.target<std::vector<ossia::value>>();
857 list && list->size() >= max_nodes)
858 throw codec_error{"Value count limit exceeded"};
859 std::string output;
860 if(style == TextStyle::Pretty)
861 output = to_pretty(value);
862 else if(const auto* text = value.target<std::string>())
863 {
864 if(text->size() > max_bytes)
865 throw codec_error{"String byte limit exceeded"};
866 utf8(*text);
867 // Raw strings have no added framing, preserving embedded NUL and endings.
868 return *text;
869 }
870 else if(
871 const auto* list = value.target<std::vector<ossia::value>>();
872 list && !list->empty()
873 && std::all_of(list->begin(), list->end(), [](const ossia::value& item) {
874 return item.target<int>() || item.target<float>() || item.target<bool>()
875 || item.target<std::string>() || item.target<ossia::impulse>();
876 }))
877 output = delimited_text(*list, delimiter);
878 else if(const auto* vector = value.target<ossia::vec2f>())
879 output = delimited_text(*vector, delimiter);
880 else if(const auto* vector = value.target<ossia::vec3f>())
881 output = delimited_text(*vector, delimiter);
882 else if(const auto* vector = value.target<ossia::vec4f>())
883 output = delimited_text(*vector, delimiter);
884 else
885 output = encode<json_writer>(value);
886 append_bytes(output, line_ending(ending));
887 return output;
888}
889
890// Strict subset of Python struct. No prefix: selected endian, packed.
891// < little, > or ! big, = native endian packed, @ native endian/alignment.
892// [count]b B h H i I q Q f d ? s x, with whitespace only between fields.
893// Ns is ONE exact-size raw string (never silently padded or truncated).
894// Nx is explicit zero padding; zero repeats are numeric alignment directives.
895// Native ABI requires the standard widths below; no automatic trailing padding.
896// Decoding returns a flat list: integer results must fit int32 and floating
897// results round to finite float32 without overflow or nonzero-to-zero underflow.
898inline std::size_t binary_width(char code)
899{
900 switch(code)
901 {
902 case 'b':
903 case 'B':
904 case '?':
905 case 's':
906 case 'x':
907 return 1;
908 case 'h':
909 case 'H':
910 return 2;
911 case 'i':
912 case 'I':
913 case 'f':
914 return 4;
915 case 'q':
916 case 'Q':
917 case 'd':
918 return 8;
919 default:
920 throw codec_error{"Unknown binary format code"};
921 }
922}
923
924inline char scalar_code(ScalarType type)
925{
926 switch(type)
927 {
928 case ScalarType::u8:
929 return 'B';
930 case ScalarType::i8:
931 return 'b';
932 case ScalarType::u16:
933 return 'H';
934 case ScalarType::i16:
935 return 'h';
936 case ScalarType::u32:
937 return 'I';
938 case ScalarType::i32:
939 return 'i';
940 case ScalarType::f32:
941 return 'f';
942 case ScalarType::f64:
943 return 'd';
944 }
945 throw codec_error{"Unknown scalar type"};
946}
947
949{
950 char code;
951 std::size_t count;
952};
954{
955 ByteOrder order{};
956 std::vector<binary_field> fields;
957 std::size_t bytes{};
958 std::size_t values{};
959};
960
961static_assert(
962 CHAR_BIT == 8 && sizeof(short) == 2 && sizeof(int) == 4 && sizeof(long long) == 8
963 && sizeof(bool) == 1 && sizeof(float) == 4 && sizeof(double) == 8
964 && std::numeric_limits<float>::is_iec559 && std::numeric_limits<double>::is_iec559);
965static_assert(
966 std::endian::native == std::endian::little
967 || std::endian::native == std::endian::big);
968
969inline std::size_t native_alignment(char code)
970{
971 switch(code)
972 {
973 case 'h':
974 case 'H':
975 return alignof(short);
976 case 'i':
977 case 'I':
978 return alignof(int);
979 case 'q':
980 case 'Q':
981 return alignof(long long);
982 case 'f':
983 return alignof(float);
984 case 'd':
985 return alignof(double);
986 case '?':
987 return alignof(bool);
988 default:
989 return 1;
990 }
991}
992
993inline binary_layout compile_layout(std::string_view format, ByteOrder order)
994{
995 if(format.size() > max_nodes)
996 throw codec_error{"Format text limit exceeded"};
997 binary_layout layout{order, {}, 0, 0};
998 auto whitespace
999 = [](char c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r'; };
1000 std::size_t pos{};
1001 while(pos < format.size() && whitespace(format[pos]))
1002 ++pos;
1003 bool native{};
1004 if(pos < format.size())
1005 {
1006 const char prefix = format[pos];
1007 if(prefix == '<' || prefix == '>' || prefix == '!' || prefix == '=' || prefix == '@')
1008 {
1009 ++pos;
1010 native = prefix == '@';
1011 layout.order = prefix == '<' ? ByteOrder::Little
1012 : prefix == '>' || prefix == '!' ? ByteOrder::Big
1013 : std::endian::native == std::endian::little ? ByteOrder::Little
1014 : ByteOrder::Big;
1015 }
1016 }
1017 while(pos < format.size())
1018 {
1019 if(whitespace(format[pos]))
1020 {
1021 ++pos;
1022 continue;
1023 }
1024 std::size_t count{};
1025 bool explicit_count{};
1026 while(pos < format.size() && format[pos] >= '0' && format[pos] <= '9')
1027 {
1028 explicit_count = true;
1029 count = count * 10 + (format[pos++] - '0');
1030 if(count > max_nodes)
1031 throw codec_error{"Binary count limit exceeded"};
1032 }
1033 if(!explicit_count)
1034 count = 1;
1035 if(pos == format.size())
1036 throw codec_error{"Invalid binary count"};
1037 const char code = format[pos++];
1038 const auto width = binary_width(code);
1039 if(count == 0 && (code == 's' || code == 'x'))
1040 throw codec_error{"Zero repeats require a numeric alignment code"};
1041 const auto alignment = native ? native_alignment(code) : 1;
1042 const auto padding = (alignment - layout.bytes % alignment) % alignment;
1043 if(padding > max_bytes - layout.bytes)
1044 throw codec_error{"Binary layout resource limit exceeded"};
1045 if(padding)
1046 {
1047 layout.fields.push_back({'x', padding});
1048 layout.bytes += padding;
1049 }
1050 const auto bytes = count * width;
1051 const auto values = code == 'x' ? 0 : code == 's' ? 1 : count;
1052 if(bytes > max_bytes - layout.bytes || values >= max_nodes - layout.values)
1053 throw codec_error{"Binary layout resource limit exceeded"};
1054 layout.bytes += bytes;
1055 layout.values += values;
1056 layout.fields.push_back({code, count});
1057 }
1058 if(layout.fields.empty())
1059 throw codec_error{"Empty binary layout"};
1060 return layout;
1061}
1062
1064{
1065 std::string source;
1066 ByteOrder order{};
1067 bool initialized{};
1068 binary_layout layout;
1069 std::string error;
1070 const binary_layout& get(std::string_view format, ByteOrder requested)
1071 {
1072 if(format.size() > max_nodes)
1073 throw codec_error{"Format text limit exceeded"};
1074 if(!initialized || source != format || order != requested)
1075 {
1076 source = format;
1077 order = requested;
1078 initialized = true;
1079 error.clear();
1080 try
1081 {
1082 layout = compile_layout(format, requested);
1083 }
1084 catch(const codec_error& e)
1085 {
1086 error = e.what();
1087 }
1088 }
1089 if(!error.empty())
1090 throw codec_error{error};
1091 return layout;
1092 }
1093};
1094
1095inline void
1096write_word(std::string& output, std::uint64_t word, std::size_t width, ByteOrder order)
1097{
1098 if(width > max_bytes - output.size())
1099 throw codec_error{"Output byte limit exceeded"};
1100 for(std::size_t i = 0; i < width; ++i)
1101 {
1102 const auto shift = 8 * (order == ByteOrder::Little ? i : width - 1 - i);
1103 output.push_back(static_cast<char>((word >> shift) & 255));
1104 }
1105}
1106
1107inline std::uint64_t
1108read_word(std::string_view input, std::size_t& pos, std::size_t width, ByteOrder order)
1109{
1110 if(width > input.size() - pos)
1111 throw codec_error{"Truncated binary input"};
1112 std::uint64_t word{};
1113 for(std::size_t i = 0; i < width; ++i)
1114 {
1115 const auto shift = 8 * (order == ByteOrder::Little ? i : width - 1 - i);
1116 word |= std::uint64_t(static_cast<unsigned char>(input[pos++])) << shift;
1117 }
1118 return word;
1119}
1120
1121inline ossia::value
1122read_scalar(std::string_view input, std::size_t& pos, char code, ByteOrder order)
1123{
1124 const auto width = binary_width(code);
1125 const auto word = read_word(input, pos, width, order);
1126 switch(code)
1127 {
1128 case 'f':
1129 return floating(std::bit_cast<float>(static_cast<std::uint32_t>(word)));
1130 case 'd':
1131 return floating(std::bit_cast<double>(word));
1132 case '?':
1133 if(word > 1)
1134 throw codec_error{"Boolean byte must be 0 or 1"};
1135 return bool(word);
1136 case 'b':
1137 case 'h':
1138 case 'i':
1139 case 'q': {
1140 const auto extended = width < 8 && (word & (std::uint64_t{1} << (width * 8 - 1)))
1141 ? word | (~std::uint64_t{0} << (width * 8))
1142 : word;
1143 return integer(std::bit_cast<std::int64_t>(extended));
1144 }
1145 default:
1146 return unsigned_integer(word);
1147 }
1148}
1149
1150inline void
1151write_scalar(std::string& output, const ossia::value& value, char code, ByteOrder order)
1152{
1153 const auto width = binary_width(code);
1154 if(code == 'f' || code == 'd')
1155 {
1156 double number;
1157 if(const auto* f = value.target<float>())
1158 number = *f;
1159 else if(const auto* i = value.target<int>())
1160 number = *i;
1161 else
1162 throw codec_error{"Floating field requires a number"};
1163 floating(number);
1164 const auto word
1165 = code == 'f'
1166 ? std::uint64_t(std::bit_cast<std::uint32_t>(static_cast<float>(number)))
1167 : std::bit_cast<std::uint64_t>(number);
1168 write_word(output, word, width, order);
1169 return;
1170 }
1171 std::int64_t number;
1172 if(const auto* i = value.target<int>())
1173 number = *i;
1174 else if(const auto* b = value.target<bool>())
1175 number = *b;
1176 else
1177 throw codec_error{"Integer field requires an integer or boolean"};
1178 const bool signed_type = code == 'b' || code == 'h' || code == 'i' || code == 'q';
1179 if(code == '?')
1180 {
1181 if(number < 0 || number > 1)
1182 throw codec_error{"Boolean field must be 0 or 1"};
1183 }
1184 else if(signed_type && width < 8)
1185 {
1186 const std::int64_t bound = std::int64_t{1} << (width * 8 - 1);
1187 if(number < -bound || number >= bound)
1188 throw codec_error{"Signed field overflow"};
1189 }
1190 else if(!signed_type)
1191 {
1192 if(number < 0
1193 || (width < 8 && std::uint64_t(number) >= (std::uint64_t{1} << (width * 8))))
1194 throw codec_error{"Unsigned field overflow"};
1195 }
1196 write_word(output, static_cast<std::uint64_t>(number), width, order);
1197}
1198
1200{
1201 std::string& output;
1202 ByteOrder order;
1203 limits& budget;
1204 std::size_t depth{};
1205 void operator()() const { throw codec_error{"Cannot serialize an unset value"}; }
1206 void operator()(ossia::impulse) const
1207 {
1208 throw codec_error{"Binary impulse has no width"};
1209 }
1210 void operator()(int v) const { write_scalar(output, v, 'i', order); }
1211 void operator()(float v) const { write_scalar(output, v, 'f', order); }
1212 void operator()(bool v) const { write_scalar(output, v, '?', order); }
1213 void operator()(const std::string& v) const { append_bytes(output, v); }
1214 void operator()(const ossia::value_map_type&) const
1215 {
1216 throw codec_error{"Binary maps have ambiguous field order"};
1217 }
1218 void operator()(const std::vector<ossia::value>& values) const
1219 {
1220 limits::depth(depth + 1);
1221 for(const auto& value : values)
1222 {
1223 budget.node();
1224 value.apply(free_binary_writer{output, order, budget, depth + 1});
1225 }
1226 }
1227 template <std::size_t N>
1228 void operator()(const std::array<float, N>& values) const
1229 {
1230 limits::depth(depth + 1);
1231 for(float value : values)
1232 {
1233 budget.node();
1234 (*this)(value);
1235 }
1236 }
1237};
1238
1239template <typename Values>
1240std::string to_binary_fields(const Values& values, const binary_layout& layout)
1241{
1242 std::string output;
1243 if(values.size() != layout.values)
1244 throw codec_error{"Binary layout input field count mismatch"};
1245 output.reserve(layout.bytes);
1246 std::size_t index{};
1247 for(const auto& field : layout.fields)
1248 {
1249 if(field.code == 'x')
1250 {
1251 output.append(field.count, '\0');
1252 continue;
1253 }
1254 const auto repeats = field.code == 's' ? 1 : field.count;
1255 for(std::size_t i = 0; i < repeats; ++i)
1256 {
1257 const ossia::value& item = values[index++];
1258 if(field.code == 's')
1259 {
1260 const auto* text = item.target<std::string>();
1261 if(!text || text->size() != field.count)
1262 throw codec_error{"Fixed string requires exactly its declared byte count"};
1263 append_bytes(output, *text);
1264 }
1265 else
1266 write_scalar(output, item, field.code, layout.order);
1267 }
1268 }
1269 return output;
1270}
1271
1272inline std::string
1273to_binary(const ossia::value& value, ByteOrder order, const binary_layout* layout)
1274{
1275 if(!layout)
1276 {
1277 std::string output;
1278 limits budget;
1279 budget.node();
1280 value.apply(free_binary_writer{output, order, budget});
1281 return output;
1282 }
1283 if(const auto* list = value.target<std::vector<ossia::value>>())
1284 return to_binary_fields(*list, *layout);
1285 if(const auto* vector = value.target<ossia::vec2f>())
1286 return to_binary_fields(*vector, *layout);
1287 if(const auto* vector = value.target<ossia::vec3f>())
1288 return to_binary_fields(*vector, *layout);
1289 if(const auto* vector = value.target<ossia::vec4f>())
1290 return to_binary_fields(*vector, *layout);
1291 return to_binary_fields(std::span{&value, 1}, *layout);
1292}
1293
1294inline ossia::value from_binary(
1295 std::string_view input, ByteOrder order, ScalarType type,
1296 const binary_layout* layout)
1297{
1298 if(input.size() > max_bytes)
1299 throw codec_error{"Input byte limit exceeded"};
1300 std::vector<ossia::value> values;
1301 std::size_t pos{};
1302 if(!layout)
1303 {
1304 const auto code = scalar_code(type);
1305 const auto width = binary_width(code);
1306 if(input.size() % width)
1307 throw codec_error{"Truncated binary scalar"};
1308 if(input.size() / width >= max_nodes)
1309 throw codec_error{"Value count limit exceeded"};
1310 values.reserve(input.size() / width);
1311 while(pos < input.size())
1312 values.push_back(read_scalar(input, pos, code, order));
1313 }
1314 else
1315 {
1316 if(input.size() != layout->bytes)
1317 throw codec_error{"Truncated or trailing binary input"};
1318 values.reserve(layout->values);
1319 for(const auto& field : layout->fields)
1320 {
1321 if(field.code == 'x')
1322 {
1323 pos += field.count;
1324 continue;
1325 }
1326 if(field.code == 's')
1327 {
1328 values.emplace_back(std::string{input.substr(pos, field.count)});
1329 pos += field.count;
1330 }
1331 else
1332 for(std::size_t i = 0; i < field.count; ++i)
1333 values.push_back(read_scalar(input, pos, field.code, layout->order));
1334 }
1335 }
1336 return values;
1337}
1338
1339// Callback outputs emit once per input update, including repeated equal events.
1340// Each failed update emits an explicit empty/unset result and Success=false,
1341// never the previous successful payload. Error is empty on successful updates.
1342// Conversion finishes before publishing; callback order is error, status, data.
1343template <typename Output, typename Function>
1344void publish(Output& output, Function&& function)
1345{
1346 using result_type = decltype(function());
1347 result_type result;
1348 std::string error;
1349 try
1350 {
1351 result = function();
1352 }
1353 catch(const std::exception& e)
1354 {
1355 error = e.what();
1356 }
1357 output.error(error);
1358 output.success(error.empty());
1359 output.result(std::move(result));
1360}
1361}
1362
1363namespace avnd_tools
1364{
1366{
1367 halp_meta(name, "Deserialize")
1368 halp_meta(c_name, "avnd_deserialize")
1369 halp_meta(author, "ossia team")
1370 halp_meta(category, "Control/Serialization")
1371 halp_meta(uuid, "5d265cd1-014f-48bb-9c51-d594c8b58b61")
1372 halp_meta(
1373 description,
1374 "Decode Bytes as strict JSON, JSON-compatible CBOR, UTF-8 text or binary. "
1375 "Integers must fit int32; floats round to finite float32 without underflow. "
1376 "Binary decoding returns an ordered list. Limits: 8 MiB, 65536 nodes, 64 "
1377 "containers. Errors clear Value; Error is empty on success.")
1378 struct ins
1379 {
1380 struct : halp::val_port<"Bytes", std::string>
1381 {
1382 void update(Deserialize& self) { self.process(); }
1383 } input;
1384 halp::enum_t<value_serialization::Format, "Format"> format;
1385 struct : halp::enum_t<value_serialization::TextStyle, "Text style">
1386 {
1387 halp_meta(
1388 description,
1389 "Plain reads delimited fields or nested JSON. Pretty reads canonical ossia "
1390 "type labels only; maps are display only, floats were rounded to two "
1391 "decimals, and only quotes are escaped.")
1392 } text_style;
1393 struct : halp::enum_t<value_serialization::TextType, "Interpretation">
1394 {
1395 halp_meta(
1396 description,
1397 "Auto recognizes separators, JSON and exact scalar tokens, otherwise raw "
1398 "strings. String preserves all UTF-8 bytes and line endings. List forces "
1399 "single delimited fields into a list. Numeric options require exact scalars. "
1400 "Pretty uses its own labels unless String is selected.")
1401 } text_type;
1402 struct : halp::lineedit<"Delimiter", ",">
1403 {
1404 halp_meta(
1405 description,
1406 "Separator for flat Plain lists and vectors, ignored inside JSON-quoted "
1407 "strings. Must exclude quotes, backslashes and brackets. Nested containers "
1408 "use JSON.")
1409 } delimiter;
1410 struct : halp::enum_t<value_serialization::LineEnding, "Line ending">
1411 {
1412 halp_meta(
1413 description,
1414 "Remove one matching suffix before parsing, except with String "
1415 "interpretation.")
1416 } line_ending;
1417 struct : halp::enum_t<value_serialization::ByteOrder, "Byte order">
1418 {
1419 halp_meta(
1420 description, "Raw scalar endian and the default for layouts without a prefix.")
1421 } byte_order;
1422 struct : halp::enum_t<value_serialization::BinaryMode, "Binary mode">
1423 {
1424 halp_meta(
1425 description,
1426 "FreeFlow reads repeated scalars; Layout reads explicitly typed fields. Both "
1427 "return lists.")
1428 } binary_mode;
1429 struct : halp::enum_t<value_serialization::ScalarType, "Scalar type">
1430 {
1431 halp_meta(
1432 description,
1433 "Repeated raw binary width. Integers must fit int32; floats round to finite "
1434 "float32 without underflow.")
1435 } scalar_type;
1436 struct : halp::lineedit<"Layout", "<B H I f 4s 2x">
1437 {
1438 halp_meta(
1439 description,
1440 "Python struct subset: < > ! = packed, @ native endian and alignment; "
1441 "no prefix uses Byte order and packed widths. [count]b B h H i I q Q f d ? s "
1442 "x. "
1443 "Counts <=65536; zero numeric counts align only. Ns is one exact-size raw "
1444 "string; Nx skips padding. No automatic end padding. Int32 and finite float32 "
1445 "results.")
1446 } layout;
1447 } inputs;
1448 struct
1449 {
1450 halp::callback<"Value", ossia::value> result;
1451 halp::callback<"Success", bool> success;
1452 halp::callback<"Error", std::string> error;
1453 } outputs;
1454
1455 struct ui
1456 {
1457 halp_meta(layout, halp::layouts::vbox)
1458 halp::item<&ins::format> format;
1460 {
1461 halp_meta(layout, halp::layouts::tabs)
1462 halp_flag(hide_tabs);
1463 static constexpr auto model = &ins::format;
1464 struct
1465 {
1466 halp_meta(name, "JSON")
1467 halp_meta(layout, halp::layouts::vbox)
1468 halp::label types{.text = "Strict UTF-8 JSON: int32, finite float32."};
1469 halp::label values{.text = "null = impulse; arrays = lists; ordered maps."};
1470 halp::label limits{.text = "8 MiB, 65536 values/keys, 64 containers."};
1471 } json;
1472 struct
1473 {
1474 halp_meta(name, "CBOR")
1475 halp_meta(layout, halp::layouts::vbox)
1476 halp::label types{.text = "JSON-compatible CBOR: int32, finite float32."};
1477 halp::label keys{.text = "UTF-8 text keys; no tags, byte strings or undefined."};
1478 halp::label values{.text = "null = impulse; arrays = lists; ordered maps."};
1479 halp::label limits{.text = "8 MiB, 65536 values/keys, 64 containers."};
1480 } cbor;
1481 struct
1482 {
1483 halp_meta(name, "Text")
1484 halp_meta(layout, halp::layouts::vbox)
1485 halp::item<&ins::text_style> style;
1486 halp::item<&ins::text_type> type;
1487 halp::item<&ins::delimiter> delimiter;
1488 halp::item<&ins::line_ending> ending;
1489 } text;
1490 struct
1491 {
1492 halp_meta(name, "Binary")
1493 halp_meta(layout, halp::layouts::vbox)
1494 halp::item<&ins::byte_order> order;
1495 halp::item<&ins::binary_mode> mode;
1496 halp::item<&ins::scalar_type> scalar;
1497 halp::item<&ins::layout> format_text;
1498 } binary;
1499 } settings;
1500 };
1501
1503 void process()
1504 {
1505 using namespace value_serialization;
1506 publish(outputs, [&]() -> ossia::value {
1507 const auto& bytes = inputs.input.value;
1508 switch(inputs.format.value)
1509 {
1510 case Format::JSON:
1511 return from_json(bytes);
1512 case Format::CBOR:
1513 return from_cbor(bytes);
1514 case Format::Text:
1515 return from_text(
1516 bytes, inputs.text_style.value, inputs.text_type.value,
1517 inputs.delimiter.value, inputs.line_ending.value);
1518 case Format::Binary:
1519 return from_binary(
1520 bytes, inputs.byte_order.value, inputs.scalar_type.value,
1521 inputs.binary_mode.value == BinaryMode::Layout
1522 ? &cache.get(inputs.layout.value, inputs.byte_order.value)
1523 : nullptr);
1524 }
1525 throw codec_error{"Unknown format"};
1526 });
1527 }
1528};
1529
1531{
1532 halp_meta(name, "Serialize")
1533 halp_meta(c_name, "avnd_serialize")
1534 halp_meta(author, "ossia team")
1535 halp_meta(category, "Control/Serialization")
1536 halp_meta(uuid, "5d265cd1-014f-48bb-9c51-d594c8b58b62")
1537 halp_meta(
1538 description,
1539 "Encode Value as strict JSON, JSON-compatible CBOR, UTF-8 text or binary. "
1540 "Plain Text infers shape: raw strings, delimited flat lists/vectors, nested JSON; "
1541 "vectors decode as lists. Pretty Text is ossia display, not a lossless archive. "
1542 "Free-flow binary flattens lists/vectors; maps and impulses fail. "
1543 "Limits: 8 MiB, 65536 nodes, 64 containers. Errors clear Bytes.")
1544 struct ins
1545 {
1546 struct : halp::val_port<"Value", ossia::value>
1547 {
1548 void update(Serialize& self) { self.process(); }
1549 } input;
1550 halp::enum_t<value_serialization::Format, "Format"> format;
1551 struct : halp::toggle<"Pretty print">
1552 {
1553 halp_meta(
1554 description, "Indent JSON without changing its values or numeric precision.")
1555 } pretty;
1556 struct : halp::enum_t<value_serialization::TextStyle, "Text style">
1557 {
1558 halp_meta(
1559 description,
1560 "Plain infers shape: root strings are raw UTF-8, scalars are plain tokens, "
1561 "flat lists/vectors are delimited JSON tokens, nested containers use JSON. "
1562 "Pretty uses ossia type labels and two-decimal floats: lossy display; maps "
1563 "and ambiguous escaped strings cannot be decoded.")
1564 } text_style;
1565 struct : halp::lineedit<"Delimiter", ",">
1566 {
1567 halp_meta(
1568 description,
1569 "Separator for nonempty flat Plain lists and vectors. Strings are "
1570 "JSON-quoted; numeric delimiter collisions fail. Must exclude quotes, "
1571 "backslashes and brackets. One-field lists require List interpretation when "
1572 "decoding.")
1573 } delimiter;
1574 struct : halp::enum_t<value_serialization::LineEnding, "Line ending">
1575 {
1576 halp_meta(
1577 description,
1578 "Append a line ending to Text, except raw Plain strings which are unchanged.")
1579 } line_ending;
1580 struct : halp::enum_t<value_serialization::ByteOrder, "Byte order">
1581 {
1582 halp_meta(
1583 description, "Free-flow endian and the default for layouts without a prefix.")
1584 } byte_order;
1585 struct : halp::enum_t<value_serialization::BinaryMode, "Binary mode">
1586 {
1587 halp_meta(
1588 description,
1589 "FreeFlow writes int32, float32, bool bytes and raw strings, flattening lists "
1590 "and vectors without tags. Layout writes explicitly typed fields.")
1591 } binary_mode;
1592 struct : halp::lineedit<"Layout", "<B H I f 4s 2x">
1593 {
1594 halp_meta(
1595 description,
1596 "Python struct subset: < > ! = packed, @ native endian and alignment; "
1597 "no prefix uses Byte order and packed widths. [count]b B h H i I q Q f d ? s "
1598 "x. "
1599 "Counts <=65536; zero numeric counts align only. Ns consumes one exact-size "
1600 "raw string, never padded or truncated; Nx writes zeros. No automatic end "
1601 "padding.")
1602 } layout;
1603 } inputs;
1604 struct
1605 {
1606 halp::callback<"Bytes", std::string> result;
1607 halp::callback<"Success", bool> success;
1608 halp::callback<"Error", std::string> error;
1609 } outputs;
1610
1611 struct ui
1612 {
1613 halp_meta(layout, halp::layouts::vbox)
1614 halp::item<&ins::format> format;
1616 {
1617 halp_meta(layout, halp::layouts::tabs)
1618 halp_flag(hide_tabs);
1619 static constexpr auto model = &ins::format;
1620 struct
1621 {
1622 halp_meta(name, "JSON")
1623 halp_meta(layout, halp::layouts::vbox)
1624 halp::item<&ins::pretty> pretty;
1625 } json;
1626 struct
1627 {
1628 halp_meta(name, "CBOR")
1629 halp_meta(layout, halp::layouts::vbox)
1630 halp::label types{.text = "JSON-compatible CBOR: int32, finite float32."};
1631 halp::label values{.text = "Impulse = null; vectors/lists = arrays."};
1632 halp::label keys{.text = "UTF-8 text and unique map keys required."};
1633 halp::label limits{.text = "8 MiB, 65536 values/keys, 64 containers."};
1634 } cbor;
1635 struct
1636 {
1637 halp_meta(name, "Text")
1638 halp_meta(layout, halp::layouts::vbox)
1639 halp::item<&ins::text_style> style;
1640 halp::item<&ins::delimiter> delimiter;
1641 halp::item<&ins::line_ending> ending;
1642 } text;
1643 struct
1644 {
1645 halp_meta(name, "Binary")
1646 halp_meta(layout, halp::layouts::vbox)
1647 halp::item<&ins::byte_order> order;
1648 halp::item<&ins::binary_mode> mode;
1649 halp::item<&ins::layout> format_text;
1650 } binary;
1651 } settings;
1652 };
1653
1655 void process()
1656 {
1657 using namespace value_serialization;
1658 publish(outputs, [&]() -> std::string {
1659 const auto& value = inputs.input.value;
1660 switch(inputs.format.value)
1661 {
1662 case Format::JSON:
1663 return inputs.pretty.value ? encode<pretty_json_writer>(value)
1664 : encode<json_writer>(value);
1665 case Format::CBOR:
1666 return encode<cbor_writer>(value);
1667 case Format::Text:
1668 return to_text(
1669 value, inputs.text_style.value, inputs.delimiter.value,
1670 inputs.line_ending.value);
1671 case Format::Binary:
1672 return to_binary(
1673 value, inputs.byte_order.value,
1674 inputs.binary_mode.value == BinaryMode::Layout
1675 ? &cache.get(inputs.layout.value, inputs.byte_order.value)
1676 : nullptr);
1677 }
1678 throw codec_error{"Unknown format"};
1679 });
1680 }
1681};
1682}
Definition BaseScenarioSerialization.cpp:17
STL namespace.
Definition score-plugin-avnd/AvndProcesses/ValueSerialization.hpp:1379
Definition score-plugin-avnd/AvndProcesses/ValueSerialization.hpp:1460
Definition score-plugin-avnd/AvndProcesses/ValueSerialization.hpp:1456
Definition score-plugin-avnd/AvndProcesses/ValueSerialization.hpp:1366
Definition score-plugin-avnd/AvndProcesses/ValueSerialization.hpp:1545
Definition score-plugin-avnd/AvndProcesses/ValueSerialization.hpp:1616
Definition score-plugin-avnd/AvndProcesses/ValueSerialization.hpp:1612
Definition score-plugin-avnd/AvndProcesses/ValueSerialization.hpp:1531
Definition score-plugin-avnd/AvndProcesses/ValueSerialization.hpp:268
Definition score-plugin-avnd/AvndProcesses/ValueSerialization.hpp:949
Definition score-plugin-avnd/AvndProcesses/ValueSerialization.hpp:954
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:1200
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:1064
Definition score-plugin-avnd/AvndProcesses/ValueSerialization.hpp:101
Definition score-plugin-avnd/AvndProcesses/ValueSerialization.hpp:643
Definition score-plugin-avnd/AvndProcesses/ValueSerialization.hpp:661
Definition score-plugin-avnd/AvndProcesses/ValueSerialization.hpp:333