leptris 1.9.201.4-arm-linux → 1.9.207.0-arm-linux

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +69 -0
  3. data/Rakefile +8 -2
  4. data/lib/leptris/version.rb +1 -1
  5. data/lib/leptris/xml/document.rb +25 -0
  6. data/lib/leptris/xml/dtd.rb +113 -0
  7. data/lib/leptris/xml/ffi.rb +75 -0
  8. data/lib/leptris/xml.rb +1 -0
  9. data/lib/libleptris.so +0 -0
  10. data/vendor-src/libleptris/CHANGELOG.md +79 -0
  11. data/vendor-src/libleptris/CLAUDE.md +1 -1
  12. data/vendor-src/libleptris/CMakeLists.txt +1 -1
  13. data/vendor-src/libleptris/README.adoc +26 -0
  14. data/vendor-src/libleptris/TODO.max-perf/2-3-oneshot-tokenizer-design.md +98 -0
  15. data/vendor-src/libleptris/cli/commands/validate.c +4 -2
  16. data/vendor-src/libleptris/docs/guide/parsing.md +30 -0
  17. data/vendor-src/libleptris/scripts/check_export_surface.py +20 -0
  18. data/vendor-src/libleptris/src/CMakeLists.txt +44 -1
  19. data/vendor-src/libleptris/src/include/leptris/descriptor.h +7 -1
  20. data/vendor-src/libleptris/src/include/leptris/dom/document.h +7 -1
  21. data/vendor-src/libleptris/src/include/leptris/dom/element.h +7 -1
  22. data/vendor-src/libleptris/src/include/leptris/dom/serialize.h +7 -1
  23. data/vendor-src/libleptris/src/include/leptris/dtd.h +46 -9
  24. data/vendor-src/libleptris/src/include/leptris/error.h +63 -1
  25. data/vendor-src/libleptris/src/include/leptris/html.h +79 -0
  26. data/vendor-src/libleptris/src/include/leptris/sax/sax.h +20 -1
  27. data/vendor-src/libleptris/src/include/leptris/types.h +29 -1
  28. data/vendor-src/libleptris/src/include/leptris/xpath/xpath.h +7 -1
  29. data/vendor-src/libleptris/src/include/leptris.h +59 -37
  30. data/vendor-src/libleptris/src/leptris/core.c +28 -0
  31. data/vendor-src/libleptris/src/leptris/dom/diag.h +2 -13
  32. data/vendor-src/libleptris/src/leptris/dom/digest.c +5 -1
  33. data/vendor-src/libleptris/src/leptris/dom/doctype.c +27 -0
  34. data/vendor-src/libleptris/src/leptris/dom/element.c +54 -9
  35. data/vendor-src/libleptris/src/leptris/dom/element.h +5 -0
  36. data/vendor-src/libleptris/src/leptris/dom/element_modify.c +46 -6
  37. data/vendor-src/libleptris/src/leptris/dtd/model.h +6 -0
  38. data/vendor-src/libleptris/src/leptris/dtd/parser.c +49 -12
  39. data/vendor-src/libleptris/src/leptris/error.c +22 -0
  40. data/vendor-src/libleptris/src/leptris/flat/direct_parse.c +61 -6
  41. data/vendor-src/libleptris/src/leptris/html/html_parse.c +156 -42
  42. data/vendor-src/libleptris/src/leptris/leptris.c +11 -2
  43. data/vendor-src/libleptris/src/leptris/leptris_internal.h +23 -1
  44. data/vendor-src/libleptris/src/leptris/sax/pull.c +62 -18
  45. data/vendor-src/libleptris/src/leptris/sax/sax_internal.h +7 -0
  46. data/vendor-src/libleptris/src/leptris/sax/streaming.c +155 -4
  47. data/vendor-src/libleptris/src/leptris/xpath/evaluator.c +19 -2
  48. data/vendor-src/libleptris/src/leptris/xpath/evaluator_operators.c +126 -0
  49. data/vendor-src/libleptris/src/leptris/xpath/functions_ext31.c +653 -151
  50. data/vendor-src/libleptris/src/leptris/xpath/parser.c +4 -2
  51. data/vendor-src/libleptris/src/leptris/xslt/xslt_functions.c +9 -5
  52. data/vendor-src/libleptris/test/dom/test_digest.cpp +36 -0
  53. data/vendor-src/libleptris/test/dom/test_dom.cpp +24 -0
  54. data/vendor-src/libleptris/test/dom/test_node_surface_parity.cpp +81 -0
  55. data/vendor-src/libleptris/test/dtd/test_dtd_validate.cpp +113 -0
  56. data/vendor-src/libleptris/test/html/test_html.cpp +51 -0
  57. data/vendor-src/libleptris/test/parser/test_parser.cpp +43 -0
  58. data/vendor-src/libleptris/test/sax/test_pull.cpp +32 -0
  59. data/vendor-src/libleptris/test/xquery/qt3/fn/replace.xml +865 -0
  60. data/vendor-src/libleptris/test/xquery/qt3/fn/string-join.xml +449 -0
  61. data/vendor-src/libleptris/test/xquery/qt3/fn/tokenize.xml +639 -0
  62. data/vendor-src/libleptris/test/xquery/test_qt3.cpp +74 -6
  63. data/vendor-src/libleptris/vcpkg.json +1 -1
  64. metadata +7 -1
@@ -437,7 +437,47 @@ static inline int dp_add_attr_inline(DParser* p, LeptrisElement elem,
437
437
  * Careful: first_attribute_off == 0 means empty list. We can't decode
438
438
  * the pointer and check for NULL — at offset 0 the decoded pointer
439
439
  * is `elem` itself, which is non-NULL. Check the offset field. */
440
+ /* #1200: duplicate-attribute detection. Prefilter on (len,
441
+ * first byte) — a mismatching pair costs two loads; the memcmp
442
+ * only runs on genuine candidates. First definition wins
443
+ * (libxml2 recover semantics, and the digest's first-wins
444
+ * becomes official); the duplicate never enters the chain, it
445
+ * costs its already-carved slot. */
440
446
  if (elem->first_attribute_off != 0) {
447
+ int dup = 0;
448
+ for (struct leptris_attribute* a =
449
+ (struct leptris_attribute*)((char*)elem +
450
+ elem->first_attribute_off);
451
+ a; a = leptris_attr_next(a)) {
452
+ if (a->name_view.length == attr->name_view.length &&
453
+ a->name_view.data[0] == attr->name_view.data[0] &&
454
+ memcmp(a->name_view.data, attr->name_view.data,
455
+ attr->name_view.length) == 0) {
456
+ dup = 1;
457
+ break;
458
+ }
459
+ }
460
+ if (dup) {
461
+ if (p->doc) {
462
+ extern struct LeptrisDiag* leptris_diag_emit(
463
+ struct LeptrisDiag**, int*, int*,
464
+ LeptrisDiagKind, LeptrisNodeRef,
465
+ const char*, ...);
466
+ char qname[130];
467
+ size_t ql = attr->name_view.length < 129
468
+ ? attr->name_view.length : 129;
469
+ memcpy(qname, attr->name_view.data, ql);
470
+ qname[ql] = '\0';
471
+ leptris_diag_emit(&p->doc->parse_diags,
472
+ &p->doc->parse_diag_count,
473
+ &p->doc->parse_diag_cap,
474
+ LEPTRIS_DIAG_RECOVER, NULL,
475
+ "Attribute %s redefined", qname);
476
+ }
477
+ /* 1 = duplicate skipped (keep first): the caller skips
478
+ * the raw-attr registration so BOTH surfaces agree. */
479
+ return 1;
480
+ }
441
481
  /* Cache should always be valid mid-parse. Fall back to walk
442
482
  * only if cache is NULL (defensive — shouldn't happen). */
443
483
  struct leptris_attribute* tail = p->current_elem_last_attr;
@@ -845,13 +885,16 @@ static int dp_parse_attrs(DParser* p, LeptrisElement elem) {
845
885
  continue;
846
886
  }
847
887
 
848
- /* Regular attribute — zero-copy name/value, bulk-allocated struct. */
849
- if (dp_add_attr_inline(p, elem, name_start, name_len,
850
- val_start, val_len, has_amp, has_ws,
851
- attr_colon) != 0)
888
+ /* Regular attribute — zero-copy name/value, bulk-allocated
889
+ * struct. 1 = duplicate skipped: neither surface sees it. */
890
+ int arc = dp_add_attr_inline(p, elem, name_start, name_len,
891
+ val_start, val_len, has_amp,
892
+ has_ws, attr_colon);
893
+ if (arc < 0) return -1;
894
+ if (arc == 0 &&
895
+ dp_raw_attr(p, elem, name_start, name_len,
896
+ val_start, val_len) != 0)
852
897
  return -1;
853
- if (dp_raw_attr(p, elem, name_start, name_len,
854
- val_start, val_len) != 0) return -1;
855
898
  }
856
899
  /* for (;;): every exit is a return inside the loop (0 = tag
857
900
  * closed, 1 = self-closing, -1 = error) — there is no
@@ -1571,9 +1614,21 @@ static struct leptris_document* direct_parse_internal(char* buf, size_t len,
1571
1614
  if (close_local_len <= 8 &&
1572
1615
  (p.probe_slack ||
1573
1616
  close_local + 8 <= p.end + 1)) {
1617
+ /* The mask follows MEMORY order: memcpy fills the low
1618
+ * bytes on little-endian and the high bytes on
1619
+ * big-endian, so each dialect must mask the end its
1620
+ * load populated. The unconditional low-bytes mask
1621
+ * compared bytes past the name on big-endian and
1622
+ * failed every close tag (#1197). */
1623
+ #if defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__
1624
+ uint64_t mask = (close_local_len == 8)
1625
+ ? ~(uint64_t)0
1626
+ : (~(uint64_t)0 << (64 - close_local_len * 8));
1627
+ #else
1574
1628
  uint64_t mask = (close_local_len == 8)
1575
1629
  ? ~(uint64_t)0
1576
1630
  : ((1ull << (close_local_len * 8)) - 1);
1631
+ #endif
1577
1632
  uint64_t a, b;
1578
1633
  memcpy(&a, open_name, 8);
1579
1634
  memcpy(&b, close_local, 8);
@@ -2722,6 +2722,13 @@ typedef struct {
2722
2722
  /* #659 foreign content: per-slot namespace of the open stack
2723
2723
  * (0 HTML, 1 SVG, 2 MathML) — parallel to open[]. */
2724
2724
  uint8_t open_ns[256];
2725
+ /* #1225: last child of each open element — the builder's own
2726
+ * O(1) append tail. The doc-level direct-mapped tail cache
2727
+ * loses a persistent parent to an address collision roughly
2728
+ * every 1KB of fresh pool allocations; each miss walks the
2729
+ * whole child chain, which is quadratic on table/text-heavy
2730
+ * pages. Slots are refreshed on push (NULL) and by hb_put. */
2731
+ LeptrisNodeRef open_tail[256];
2725
2732
  size_t depth;
2726
2733
  LeptrisElement root; /* first top-level element */
2727
2734
  LeptrisNodeRef top_head; /* top-level chain: text, comments, root */
@@ -3054,30 +3061,130 @@ static int h_is_formatting(const char* n) {
3054
3061
 
3055
3062
  /* WHATWG "special" category (13.2.4.2) — the adoption agency's
3056
3063
  * furthest-block candidates. */
3064
+ /* Generated tag classifier (#1218 hot path): one first-char
3065
+ * bucket walk replaces the ~78-entry strcmp scans that the
3066
+ * sampler pinned at ~21% of WHATWG parse (h_is_special_ww +
3067
+ * h_reconstructs). Keep in sync with the WHATWG special and
3068
+ * no-formatting sets. */
3069
+ typedef struct {
3070
+ char name[12];
3071
+ uint8_t len;
3072
+ uint8_t special; /* WHATWG 13.2.4 special set */
3073
+ uint8_t no_fmt; /* never reconstructed (formatting) */
3074
+ } HTagInfo;
3075
+
3076
+ static const HTagInfo h_tag_infos[] = {
3077
+ {"address", 7, 1, 1},
3078
+ {"applet", 6, 1, 0},
3079
+ {"area", 4, 1, 0},
3080
+ {"article", 7, 1, 1},
3081
+ {"aside", 5, 1, 1},
3082
+ {"base", 4, 1, 1},
3083
+ {"basefont", 8, 1, 1},
3084
+ {"bgsound", 7, 1, 1},
3085
+ {"blockquote", 10, 1, 1},
3086
+ {"body", 4, 1, 1},
3087
+ {"br", 2, 1, 0},
3088
+ {"button", 6, 1, 0},
3089
+ {"caption", 7, 1, 1},
3090
+ {"center", 6, 1, 1},
3091
+ {"col", 3, 1, 1},
3092
+ {"colgroup", 8, 1, 1},
3093
+ {"dd", 2, 1, 1},
3094
+ {"details", 7, 1, 1},
3095
+ {"dialog", 6, 1, 1},
3096
+ {"dir", 3, 1, 1},
3097
+ {"div", 3, 1, 1},
3098
+ {"dl", 2, 1, 1},
3099
+ {"dt", 2, 1, 1},
3100
+ {"embed", 5, 1, 0},
3101
+ {"fieldset", 8, 1, 1},
3102
+ {"figcaption", 10, 1, 1},
3103
+ {"figure", 6, 1, 1},
3104
+ {"footer", 6, 1, 1},
3105
+ {"form", 4, 1, 1},
3106
+ {"frame", 5, 1, 1},
3107
+ {"frameset", 8, 1, 1},
3108
+ {"h1", 2, 1, 1},
3109
+ {"h2", 2, 1, 1},
3110
+ {"h3", 2, 1, 1},
3111
+ {"h4", 2, 1, 1},
3112
+ {"h5", 2, 1, 1},
3113
+ {"h6", 2, 1, 1},
3114
+ {"head", 4, 1, 1},
3115
+ {"header", 6, 1, 1},
3116
+ {"hgroup", 6, 1, 1},
3117
+ {"hr", 2, 1, 1},
3118
+ {"html", 4, 1, 1},
3119
+ {"iframe", 6, 1, 0},
3120
+ {"img", 3, 1, 0},
3121
+ {"input", 5, 1, 0},
3122
+ {"keygen", 6, 1, 0},
3123
+ {"li", 2, 1, 1},
3124
+ {"link", 4, 1, 1},
3125
+ {"listing", 7, 1, 1},
3126
+ {"main", 4, 1, 1},
3127
+ {"marquee", 7, 1, 0},
3128
+ {"menu", 4, 1, 1},
3129
+ {"meta", 4, 1, 1},
3130
+ {"nav", 3, 1, 1},
3131
+ {"noembed", 7, 1, 0},
3132
+ {"noframes", 8, 1, 1},
3133
+ {"noscript", 8, 1, 0},
3134
+ {"object", 6, 1, 0},
3135
+ {"ol", 2, 1, 1},
3136
+ {"p", 1, 1, 1},
3137
+ {"param", 5, 1, 0},
3138
+ {"plaintext", 9, 1, 1},
3139
+ {"pre", 3, 1, 1},
3140
+ {"script", 6, 1, 1},
3141
+ {"search", 6, 1, 1},
3142
+ {"section", 7, 1, 1},
3143
+ {"select", 6, 1, 0},
3144
+ {"source", 6, 1, 0},
3145
+ {"style", 5, 1, 1},
3146
+ {"summary", 7, 1, 1},
3147
+ {"table", 5, 1, 1},
3148
+ {"tbody", 5, 1, 1},
3149
+ {"td", 2, 1, 1},
3150
+ {"template", 8, 1, 1},
3151
+ {"textarea", 8, 1, 1},
3152
+ {"tfoot", 5, 1, 1},
3153
+ {"th", 2, 1, 1},
3154
+ {"thead", 5, 1, 1},
3155
+ {"title", 5, 1, 1},
3156
+ {"tr", 2, 1, 1},
3157
+ {"track", 5, 1, 0},
3158
+ {"ul", 2, 1, 1},
3159
+ {"wbr", 3, 1, 0},
3160
+ {"xmp", 3, 1, 0},
3161
+ };
3162
+
3163
+ /* bucket[first_char] = start index; bucket[first_char+1] = end */
3164
+ static const uint8_t h_tag_bucket[27] = {
3165
+ 0, 5, 12, 16, 23, 24, 31, 31, 42,
3166
+ 45, 45, 46, 49, 53, 57, 59, 63, 63,
3167
+ 63, 70, 81, 82, 82, 83, 84, 84, 84,
3168
+ };
3169
+
3170
+ /* Resolve a (lowercased) tag name's flags; NULL when unknown. */
3171
+ static const HTagInfo* h_tag_lookup(const char* n) {
3172
+ if (!n) return NULL;
3173
+ unsigned char c = (unsigned char)n[0];
3174
+ if (c < 'a' || c > 'z') return NULL;
3175
+ for (int i = h_tag_bucket[c - 'a'];
3176
+ i < h_tag_bucket[c - 'a' + 1]; i++) {
3177
+ const HTagInfo* t = &h_tag_infos[i];
3178
+ if ((uint8_t)n[t->len] == 0 && t->name[t->len - 1] == n[t->len - 1] &&
3179
+ memcmp(n, t->name, t->len) == 0)
3180
+ return t;
3181
+ }
3182
+ return NULL;
3183
+ }
3184
+
3057
3185
  static int h_is_special_ww(const char* n) {
3058
- static const char* const k[] = {
3059
- "address", "applet", "area", "article", "aside",
3060
- "base", "basefont", "bgsound", "blockquote", "body",
3061
- "br", "button", "caption", "center", "col",
3062
- "colgroup", "dd", "details", "dialog", "dir",
3063
- "div",
3064
- "dl", "dt", "embed", "fieldset", "figcaption",
3065
- "figure", "footer", "form", "frame", "frameset",
3066
- "h1", "h2", "h3", "h4", "h5",
3067
- "h6", "head", "header", "hgroup", "hr",
3068
- "html", "iframe", "img", "input", "keygen",
3069
- "li", "link", "listing", "main", "marquee",
3070
- "menu", "meta", "nav", "noembed", "noframes",
3071
- "noscript", "object", "ol", "p", "param",
3072
- "plaintext", "pre", "script", "search", "section",
3073
- "select", "source", "style", "summary", "table",
3074
- "tbody", "td", "template", "textarea", "tfoot",
3075
- "th", "thead", "title", "tr", "track",
3076
- "ul", "wbr", "xmp", NULL};
3077
- if (!n) return 0;
3078
- for (int i = 0; k[i]; i++)
3079
- if (strcmp(n, k[i]) == 0) return 1;
3080
- return 0;
3186
+ const HTagInfo* t = h_tag_lookup(n);
3187
+ return t && t->special;
3081
3188
  }
3082
3189
 
3083
3190
  #define H_NS_HTML 0
@@ -3249,22 +3356,8 @@ static int h_input_type_hidden(const char* q, const char* end) {
3249
3356
  * head set, the block-level set (they close p instead), the table
3250
3357
  * family, and raw-text elements. */
3251
3358
  static int h_reconstructs(const char* n) {
3252
- static const char* const no[] = {
3253
- "html", "head", "body", "frameset", "base", "basefont",
3254
- "bgsound", "link", "meta", "script", "style", "template",
3255
- "title", "noframes", "address", "article", "aside",
3256
- "blockquote", "center", "details", "dialog", "dir",
3257
- "div", "dl", "fieldset", "figcaption", "figure",
3258
- "footer", "header", "hgroup", "main", "menu", "nav",
3259
- "ol", "p", "search", "section", "summary", "ul",
3260
- "h1", "h2", "h3", "h4", "h5", "h6",
3261
- "pre", "listing", "form", "li", "dd", "dt",
3262
- "plaintext", "textarea", "table", "hr", "caption", "col",
3263
- "colgroup", "frame", "tbody", "tfoot", "thead", "td",
3264
- "th", "tr", NULL};
3265
- for (int i = 0; no[i]; i++)
3266
- if (strcmp(n, no[i]) == 0) return 0;
3267
- return 1;
3359
+ const HTagInfo* t = h_tag_lookup(n);
3360
+ return !(t && t->no_fmt);
3268
3361
  }
3269
3362
 
3270
3363
  /* ---- #659 foreign content (WHATWG 12.2.6.5) ---- */
@@ -3658,6 +3751,24 @@ static void h_insert_before(HBuilder* b, LeptrisElement parent,
3658
3751
  parent->child_count++;
3659
3752
  }
3660
3753
 
3754
+ /* O(1) append for open-stack parents (#1225): route through the
3755
+ * builder's per-slot tail. Non-open parents fall back to the
3756
+ * shared internal (whose tail cache + walk still apply). The hint
3757
+ * is validated inside leptris_element_append_child_tail, so any
3758
+ * stale slot is silently ignored — correctness never depends on
3759
+ * the bookkeeping. */
3760
+ static void hb_put(HBuilder* b, LeptrisElement parent, LeptrisNodeRef n) {
3761
+ for (size_t i = b->depth; i-- > 0;) {
3762
+ if (b->open[i] == parent) {
3763
+ leptris_element_append_child_tail(parent, n, b->doc,
3764
+ b->open_tail[i]);
3765
+ b->open_tail[i] = n;
3766
+ return;
3767
+ }
3768
+ }
3769
+ leptris_element_append_child_internal_doc(parent, n, b->doc);
3770
+ }
3771
+
3661
3772
  static void h_append(HBuilder* b, LeptrisNodeRef n) {
3662
3773
  /* "in frameset" (13.2.6.4.18): non-whitespace text is IGNORED
3663
3774
  * with a frameset open ANYWHERE on top; whitespace inserts
@@ -3932,7 +4043,7 @@ static void h_append(HBuilder* b, LeptrisNodeRef n) {
3932
4043
  return;
3933
4044
  }
3934
4045
  }
3935
- leptris_element_append_child_internal_doc(top, n, b->doc);
4046
+ hb_put(b, top, n);
3936
4047
  } else {
3937
4048
  h_top_append(b, n);
3938
4049
  }
@@ -3999,6 +4110,7 @@ static LeptrisElement h_open_foreign(HBuilder* b, const char* name,
3999
4110
  if (b->depth == 0 && !b->root) b->root = e;
4000
4111
  if (b->depth < 256) {
4001
4112
  b->open[b->depth] = e;
4113
+ b->open_tail[b->depth] = NULL;
4002
4114
  b->open_ns[b->depth] = (uint8_t)ns;
4003
4115
  b->depth++;
4004
4116
  }
@@ -4036,6 +4148,7 @@ static LeptrisElement h_open_element(HBuilder* b, const char* name) {
4036
4148
  if (b->depth == 0 && !b->root) b->root = e;
4037
4149
  if (b->depth < 256) {
4038
4150
  b->open[b->depth] = e;
4151
+ b->open_tail[b->depth] = NULL;
4039
4152
  b->open_ns[b->depth] = H_NS_HTML;
4040
4153
  b->depth++;
4041
4154
  }
@@ -4236,6 +4349,7 @@ static LeptrisElement h_afe_open_clone(HBuilder* b, LeptrisElement src) {
4236
4349
  if (b->depth == 0 && !b->root) b->root = c;
4237
4350
  if (b->depth < 256) {
4238
4351
  b->open[b->depth] = c;
4352
+ b->open_tail[b->depth] = NULL;
4239
4353
  b->open_ns[b->depth] = H_NS_HTML;
4240
4354
  b->depth++;
4241
4355
  }
@@ -4518,6 +4632,7 @@ static LeptrisElement h_open_named(HBuilder* b, const char* name,
4518
4632
  if (b->depth == 0 && !b->root) b->root = e;
4519
4633
  if (push && b->depth < 256) {
4520
4634
  b->open[b->depth] = e;
4635
+ b->open_tail[b->depth] = NULL;
4521
4636
  b->open_ns[b->depth] = H_NS_HTML;
4522
4637
  b->depth++;
4523
4638
  }
@@ -4532,8 +4647,7 @@ static LeptrisElement h_new_child(HBuilder* b, LeptrisElement parent,
4532
4647
  LeptrisElement e = leptris_element_create_with_view(nv, b->pool);
4533
4648
  if (!e) return NULL;
4534
4649
  leptris_root_doc_register(e, b->doc);
4535
- leptris_element_append_child_internal_doc(parent, (LeptrisNodeRef)e,
4536
- b->doc);
4650
+ hb_put(b, parent, (LeptrisNodeRef)e);
4537
4651
  return e;
4538
4652
  }
4539
4653
 
@@ -836,6 +836,13 @@ LEPTRIS_API void leptris_document_free(struct leptris_document* doc) {
836
836
  /* The pool will be destroyed below, freeing all elements */
837
837
  }
838
838
 
839
+ /* Parse diagnostics (#1200): the recover-channel list. */
840
+ {
841
+ extern void leptris_diag_free(struct LeptrisDiag**, int*, int*);
842
+ leptris_diag_free(&doc->parse_diags, &doc->parse_diag_count,
843
+ &doc->parse_diag_cap);
844
+ }
845
+
839
846
  /* Free document fields */
840
847
  if (doc->encoding) {
841
848
  LEPTRIS_FREE(doc->encoding);
@@ -921,9 +928,11 @@ LEPTRIS_API void leptris_document_free(struct leptris_document* doc) {
921
928
  leptris_root_doc_unregister((LeptrisElement)doc->new_dom_root);
922
929
  }
923
930
  /* #1038: fallback-path registrations (detached elements) die
924
- * with the doc too — see leptris_root_doc_unregister_doc. */
931
+ * with the doc too — see leptris_root_doc_unregister_doc. The
932
+ * declaration must match root_doc_map.c's size_t return or the
933
+ * amalgamated single-TU build fails (#1217). */
925
934
  {
926
- extern void leptris_root_doc_unregister_doc(
935
+ extern size_t leptris_root_doc_unregister_doc(
927
936
  struct leptris_document*);
928
937
  leptris_root_doc_unregister_doc(doc);
929
938
  }
@@ -9,6 +9,19 @@
9
9
  #ifndef LEPTRIS_INTERNAL_H
10
10
  #define LEPTRIS_INTERNAL_H
11
11
 
12
+ /* Internal entry points must not leak into the shared-library ABI.
13
+ * Windows already hides them (no __declspec(dllexport)) and macOS
14
+ * build flags hide non-exported globals, but plain ELF keeps
15
+ * default visibility — leptris_set_error escaped into the Linux
16
+ * dynamic table while being absent from the PE exports, which no
17
+ * cross-platform FFI mirror can attach against (#1197 release
18
+ * cascade). */
19
+ #if defined(_WIN32) || !(defined(__GNUC__) || defined(__clang__))
20
+ #define LEPTRIS_LOCAL
21
+ #else
22
+ #define LEPTRIS_LOCAL __attribute__((visibility("hidden")))
23
+ #endif
24
+
12
25
  #include <stddef.h>
13
26
  #include <stdint.h>
14
27
 
@@ -104,6 +117,8 @@ struct leptris_mut_attr_block {
104
117
  char bytes[];
105
118
  };
106
119
 
120
+ struct LeptrisDiag;
121
+
107
122
  struct leptris_document {
108
123
  struct leptris_element* root; /* Root element (legacy API) */
109
124
  /* Snapshot of this document's last failing parse message
@@ -111,6 +126,13 @@ struct leptris_document {
111
126
  * thread-local channel). */
112
127
  char last_error_message[256];
113
128
 
129
+ /* Recover-class parse diagnostics (#1200): the DOM parse's
130
+ * parity surface with the SAX lane's recover errors. Pool-free
131
+ * list (leptris_diag_* owns it); released in document_free. */
132
+ struct LeptrisDiag* parse_diags;
133
+ int parse_diag_count;
134
+ int parse_diag_cap;
135
+
114
136
  /* EXSLT-style extension pack enabled via leptris_exslt_enable
115
137
  * (TODO.concurrency/06). */
116
138
  int exslt_enabled;
@@ -883,7 +905,7 @@ static inline int leptris_strcmp(const char* s1, const char* s2) {
883
905
  * ============================================================================ */
884
906
 
885
907
  /* Set error with basic message */
886
- void leptris_set_error(leptris_error_code code, const char* message);
908
+ LEPTRIS_LOCAL void leptris_set_error(leptris_error_code code, const char* message);
887
909
 
888
910
  /* Set error with line/column position */
889
911
  void leptris_set_parse_error_position(int line, int column);
@@ -8,6 +8,7 @@
8
8
  * by the slice, not the document. All event strings are owned by the
9
9
  * puller and remain valid until the NEXT leptris_pull_next call. */
10
10
  #include "../../include/leptris/sax/sax.h"
11
+ #include "sax_internal.h"
11
12
  #include <stdio.h>
12
13
  #include <stdlib.h>
13
14
  #include <string.h>
@@ -22,6 +23,11 @@ typedef struct pull_event {
22
23
  /* START_ELEMENT attributes: owned flat copies [n1,v1,n2,v2...] */
23
24
  char** attrs;
24
25
  size_t attr_count;
26
+ /* One-shot buffer pulls (TODO.max-perf/2-3 slice 1): strings
27
+ * borrow the sax parser's stable storage (input buffer + scratch,
28
+ * alive until parser free) — queue_reset_event frees only the
29
+ * attrs array itself. */
30
+ int borrowed;
25
31
  } pull_event;
26
32
 
27
33
  struct leptris_pull_parser {
@@ -57,9 +63,14 @@ struct leptris_pull_parser {
57
63
  };
58
64
 
59
65
  static void queue_reset_event(pull_event* e) {
60
- free(e->name); free(e->text);
66
+ if (!e->borrowed) {
67
+ free(e->name);
68
+ free(e->text);
69
+ }
61
70
  if (e->attrs) {
62
- for (size_t i = 0; i < e->attr_count * 2; i++) free(e->attrs[i]);
71
+ if (!e->borrowed)
72
+ for (size_t i = 0; i < e->attr_count * 2; i++)
73
+ free(e->attrs[i]);
63
74
  free(e->attrs);
64
75
  }
65
76
  memset(e, 0, sizeof(*e));
@@ -73,6 +84,18 @@ static char* pull_strdup_n(const char* s, size_t n) {
73
84
  return c;
74
85
  }
75
86
 
87
+ /* Event strings: borrow in one-shot mode (stable until parser free),
88
+ * strdup otherwise (streaming file mode — the source chunk dies at
89
+ * the next feed). */
90
+ static char* ev_string(pull_event* e, const struct leptris_pull_parser* p,
91
+ const char* s, size_t n) {
92
+ if (p->sax->one_shot) {
93
+ e->borrowed = 1;
94
+ return (char*)(s ? s : "");
95
+ }
96
+ return pull_strdup_n(s ? s : "", n);
97
+ }
98
+
76
99
  static pull_event* queue_push(struct leptris_pull_parser* p) {
77
100
  size_t used = (p->tail + p->cap - p->head) % p->cap;
78
101
  if (used + 1 == p->cap) { /* full: grow */
@@ -104,16 +127,25 @@ static void cb_start_element(void* ud, const char* name, const char** attrs) {
104
127
  pull_event* e = queue_push(p);
105
128
  if (!e) return;
106
129
  e->type = LEPTRIS_PULL_START_ELEMENT;
107
- e->name = pull_strdup_n(name, strlen(name));
130
+ e->name = ev_string(e, p, name, strlen(name));
108
131
  if (attrs) {
109
132
  size_t n = 0;
110
133
  while (attrs[n]) n += 2; /* flat [name, value, ..., NULL] */
111
134
  e->attr_count = n / 2;
112
135
  e->attrs = (char**)calloc(n + 1, sizeof(char*));
113
136
  if (e->attrs) {
114
- for (size_t i = 0; i < n; i++)
115
- e->attrs[i] = pull_strdup_n(attrs[i] ? attrs[i] : "",
116
- attrs[i] ? strlen(attrs[i]) : 0);
137
+ if (p->sax->one_shot) {
138
+ /* Names/values are frame/scratch-owned (parser
139
+ * lifetime); the array itself stays queue-owned. */
140
+ e->borrowed = 1;
141
+ for (size_t i = 0; i < n; i++)
142
+ e->attrs[i] = (char*)(attrs[i] ? attrs[i] : "");
143
+ } else {
144
+ for (size_t i = 0; i < n; i++)
145
+ e->attrs[i] = pull_strdup_n(
146
+ attrs[i] ? attrs[i] : "",
147
+ attrs[i] ? strlen(attrs[i]) : 0);
148
+ }
117
149
  }
118
150
  }
119
151
  }
@@ -123,7 +155,7 @@ static void cb_end_element(void* ud, const char* name) {
123
155
  pull_event* e = queue_push(p);
124
156
  if (!e) return;
125
157
  e->type = LEPTRIS_PULL_END_ELEMENT;
126
- e->name = pull_strdup_n(name, strlen(name));
158
+ e->name = ev_string(e, p, name, strlen(name));
127
159
  }
128
160
 
129
161
  static void cb_characters(void* ud, const char* text, size_t len) {
@@ -131,7 +163,10 @@ static void cb_characters(void* ud, const char* text, size_t len) {
131
163
  pull_event* e = queue_push(p);
132
164
  if (!e) return;
133
165
  e->type = LEPTRIS_PULL_TEXT;
134
- e->text = pull_strdup_n(text, len);
166
+ /* Text spans are raw input-buffer slices — no NUL terminator in
167
+ * stable storage, so they always copy even in one-shot mode
168
+ * (the public event contract is NUL-terminated strings). */
169
+ e->text = pull_strdup_n(text ? text : "", len);
135
170
  e->text_len = len;
136
171
  }
137
172
 
@@ -140,7 +175,7 @@ static void cb_comment(void* ud, const char* comment) {
140
175
  pull_event* e = queue_push(p);
141
176
  if (!e) return;
142
177
  e->type = LEPTRIS_PULL_COMMENT;
143
- e->text = pull_strdup_n(comment, strlen(comment));
178
+ e->text = ev_string(e, p, comment, strlen(comment));
144
179
  e->text_len = strlen(comment);
145
180
  }
146
181
 
@@ -149,7 +184,7 @@ static void cb_cdata(void* ud, const char* cdata) {
149
184
  pull_event* e = queue_push(p);
150
185
  if (!e) return;
151
186
  e->type = LEPTRIS_PULL_CDATA;
152
- e->text = pull_strdup_n(cdata, strlen(cdata));
187
+ e->text = ev_string(e, p, cdata, strlen(cdata));
153
188
  e->text_len = strlen(cdata);
154
189
  }
155
190
 
@@ -158,9 +193,9 @@ static void cb_pi(void* ud, const char* target, const char* data) {
158
193
  pull_event* e = queue_push(p);
159
194
  if (!e) return;
160
195
  e->type = LEPTRIS_PULL_PI;
161
- e->name = pull_strdup_n(target, strlen(target));
196
+ e->name = ev_string(e, p, target, strlen(target));
162
197
  if (data) {
163
- e->text = pull_strdup_n(data, strlen(data));
198
+ e->text = ev_string(e, p, data, strlen(data));
164
199
  e->text_len = strlen(data);
165
200
  }
166
201
  }
@@ -177,9 +212,9 @@ static void cb_start_prefix(void* ud, const char* prefix, const char* uri) {
177
212
  pull_event* e = queue_push(p);
178
213
  if (!e) return;
179
214
  e->type = LEPTRIS_PULL_START_PREFIX;
180
- e->name = pull_strdup_n(prefix ? prefix : "",
181
- prefix ? strlen(prefix) : 0);
182
- e->text = pull_strdup_n(uri ? uri : "", uri ? strlen(uri) : 0);
215
+ e->name = ev_string(e, p, prefix ? prefix : "",
216
+ prefix ? strlen(prefix) : 0);
217
+ e->text = ev_string(e, p, uri ? uri : "", uri ? strlen(uri) : 0);
183
218
  e->text_len = uri ? strlen(uri) : 0;
184
219
  }
185
220
 
@@ -188,8 +223,8 @@ static void cb_end_prefix(void* ud, const char* prefix) {
188
223
  pull_event* e = queue_push(p);
189
224
  if (!e) return;
190
225
  e->type = LEPTRIS_PULL_END_PREFIX;
191
- e->name = pull_strdup_n(prefix ? prefix : "",
192
- prefix ? strlen(prefix) : 0);
226
+ e->name = ev_string(e, p, prefix ? prefix : "",
227
+ prefix ? strlen(prefix) : 0);
193
228
  }
194
229
 
195
230
  static void cb_error(void* ud, const char* message, int line, int column) {
@@ -240,6 +275,10 @@ LEPTRIS_API LeptrisPullParser leptris_pull_new(const char* xml, size_t len) {
240
275
  if (!p) return NULL;
241
276
  p->input = xml;
242
277
  p->len = len;
278
+ /* Memory source: single-feed parse — enable the one-shot fast
279
+ * paths (TODO.max-perf/2-3 slice 1). File mode keeps the
280
+ * chunked-streaming paths. */
281
+ leptris_sax_parser_set_one_shot(p->sax, 1);
243
282
  return p;
244
283
  }
245
284
 
@@ -279,8 +318,13 @@ LEPTRIS_API const LeptrisPullEvent* leptris_pull_next(LeptrisPullParser pull) {
279
318
  chunk_ptr = pull->file_buf;
280
319
  is_final = (chunk < PULL_SLICE);
281
320
  } else {
321
+ /* In-memory source: the whole buffer is caller-owned and
322
+ * resident, so slicing buys nothing — ONE feed makes the
323
+ * parse one-shot (no token ever straddles a chunk
324
+ * boundary), the precondition for zero-copy slice
325
+ * emission (TODO.max-perf/2-3, slice 1). File mode keeps
326
+ * the bounded-memory slicing. */
282
327
  chunk = pull->len - pull->pos;
283
- if (chunk > PULL_SLICE) chunk = PULL_SLICE;
284
328
  chunk_ptr = pull->input + pull->pos;
285
329
  pull->pos += chunk;
286
330
  is_final = pull->pos == pull->len;
@@ -154,6 +154,13 @@ struct LeptrisSAXParser {
154
154
  * state machine. */
155
155
  int streaming;
156
156
 
157
+ /* One-shot parse (TODO.max-perf/2-3 slice 1): the whole input
158
+ * arrives in a single feed and stays alive for the parser's
159
+ * lifetime (pull buffer mode). Tokens can never straddle a
160
+ * chunk boundary, so name scans run in place with one direct
161
+ * copy into the frame instead of carry+scratch+frame. */
162
+ int one_shot;
163
+
157
164
  /* Emitted start_document once we've committed to parsing --
158
165
  * used to avoid emitting twice across feed() calls. */
159
166
  int start_emitted;