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
@@ -44,7 +44,17 @@ extern "C" {
44
44
  # define LEPTRIS_API
45
45
  # endif
46
46
  # else
47
- # define LEPTRIS_API __attribute__((visibility("default"), used))
47
+ /* GCC LTO internalizes even visibility("default") symbols with
48
+ * no in-library caller (#1154/#1196/#1204 class: the CLI under
49
+ * -flto, ppc64le shared builds). `externally_visible` is the
50
+ * GCC-side partner of `used`; clang accepts and ignores it, so
51
+ * it is scoped to GCC to keep -Wall clean elsewhere. */
52
+ # if defined(__GNUC__) && !defined(__clang__)
53
+ # define LEPTRIS_API __attribute__((visibility("default"), used, \
54
+ externally_visible))
55
+ # else
56
+ # define LEPTRIS_API __attribute__((visibility("default"), used))
57
+ # endif
48
58
  # endif
49
59
  #endif
50
60
  #endif /* LEPTRIS_FOR_BINDGEN */
@@ -793,42 +803,7 @@ LEPTRIS_API LeptrisStatus leptris_document_set_root(LeptrisDocument doc,
793
803
  */
794
804
  LEPTRIS_API LeptrisDocument leptris_parse_string(const char* xml, size_t length, LeptrisStatus* status);
795
805
 
796
- /**
797
- * Parse an HTML string into a document (issue #659)
798
- *
799
- * Tolerant HTML4/5 parse into the standard DOM: implied end tags
800
- * (p/li/td/tr/th/dt/dd/option/...), void elements, raw-text
801
- * <script>/<style>, minimized + unquoted attribute values,
802
- * case-insensitive tag/attribute names, and the HTML named-entity
803
- * table. Behaviors follow libxml2's HTMLparser as exposed by
804
- * Nokogiri (fragment shape: no synthesized <html>/<head>/<body>,
805
- * no implied <tbody>). Malformed input never fails the parse — it
806
- * degrades to text; a document with no nodes at all is an error.
807
- * The result serializes and queries (XPath/XSLT) like any
808
- * libleptris document.
809
- *
810
- * @param html HTML input (must be valid UTF-8)
811
- * @param length Length of input in bytes
812
- * @param status Output status code (can be NULL)
813
- * @return Document handle or NULL on error
814
- *
815
- * Memory: Caller must call leptris_document_free() when done
816
- * Thread safety: Not thread-safe. One document per thread.
817
- */
818
- LEPTRIS_API LeptrisDocument leptris_parse_html_string(const char* html,
819
- size_t length,
820
- LeptrisStatus* status);
821
-
822
- /* HTML4/libxml2-compatibility mode (#659): identical tolerant
823
- * tokenizer and Nokogiri document shape, except leading
824
- * script/style content stays in <body> (libxml2's shape —
825
- * title/meta/link/base still lift into the implied head). Bindings
826
- * that must match Nokogiri byte-for-byte pin this entry;
827
- * leptris_parse_html_string is the WHATWG-conformant engine.
828
- * Memory contract identical to leptris_parse_html_string. */
829
- LEPTRIS_API LeptrisDocument leptris_parse_html4_string(const char* html,
830
- size_t length,
831
- LeptrisStatus* status);
806
+ #include "leptris/html.h"
832
807
 
833
808
  /* ---- RELAX NG (#878) ------------------------------------------------
834
809
  * Phase 1: parse a RELAX NG XML-syntax schema into the compiled
@@ -1092,6 +1067,18 @@ LEPTRIS_API LeptrisStatus leptris_document_set_encoding(
1092
1067
  LEPTRIS_API LeptrisStatus leptris_document_set_standalone(
1093
1068
  LeptrisDocument doc, int standalone);
1094
1069
 
1070
+ /**
1071
+ * Un-set the XML declaration (#1229): the document stops being
1072
+ * declaration-bearing and serialization emits none — exactly as if
1073
+ * the input had none. Clears the version/encoding strings and the
1074
+ * standalone marker. Idempotent.
1075
+ *
1076
+ * @param doc Document handle
1077
+ * @return LEPTRIS_OK, or LEPTRIS_ERROR_NULL_ARG
1078
+ */
1079
+ LEPTRIS_API LeptrisStatus leptris_document_clear_declaration(
1080
+ LeptrisDocument doc);
1081
+
1095
1082
  /**
1096
1083
  * Programmatic DOCTYPE (#1094): create and set the document's
1097
1084
  * DOCTYPE (root name + optional PUBLIC/SYSTEM external
@@ -1105,6 +1092,19 @@ LEPTRIS_API LeptrisDoctype leptris_document_set_doctype(
1105
1092
  LeptrisDocument doc, const char* name,
1106
1093
  const char* public_id, const char* system_id);
1107
1094
 
1095
+ /**
1096
+ * Un-set the document's DOCTYPE (#1229). The LeptrisDoctype node
1097
+ * stays pool-owned and readable until leptris_document_free; it
1098
+ * simply leaves the document children chain and the serializer's
1099
+ * view. leptris_document_internal_subset returns NULL afterwards.
1100
+ *
1101
+ * @param doc Document handle
1102
+ * @return LEPTRIS_OK; LEPTRIS_ERROR_NOT_FOUND when no DOCTYPE is
1103
+ * set; LEPTRIS_ERROR_NULL_ARG for a NULL document.
1104
+ */
1105
+ LEPTRIS_API LeptrisStatus leptris_document_remove_doctype(
1106
+ LeptrisDocument doc);
1107
+
1108
1108
  /**
1109
1109
  * Get the document's internal DTD subset — the DOCTYPE declaration
1110
1110
  * (TODO 148 Phase 2).
@@ -3537,6 +3537,28 @@ LEPTRIS_API leptris_allocation_function leptris_get_memory_allocation_function(v
3537
3537
  */
3538
3538
  LEPTRIS_API leptris_deallocation_function leptris_get_memory_deallocation_function(void);
3539
3539
 
3540
+ /**
3541
+ * Allocate a buffer from the engine's heap (#1219).
3542
+ *
3543
+ * FFI callers that must hand the engine a buffer it later frees —
3544
+ * notably DTD parameter-entity loaders (leptris_dtd_set_pe_loader)
3545
+ * — allocate through this entry so allocator/CRT mismatches cannot
3546
+ * corrupt the heap (the engine DLL may link a different CRT than
3547
+ * the host language runtime). Routes through the custom allocator
3548
+ * when one is installed (leptris_set_memory_management_functions),
3549
+ * malloc otherwise; the engine releases loader buffers through the
3550
+ * matching deallocation path.
3551
+ *
3552
+ * @param len Bytes to allocate
3553
+ * @return Buffer from the engine's heap (release with
3554
+ * `leptris_free_string`), or NULL when len is 0 or the
3555
+ * allocation fails
3556
+ *
3557
+ * Memory: Caller-owned unless handed to the engine (PE-loader
3558
+ * buffers are consumed and freed by the parser).
3559
+ */
3560
+ LEPTRIS_API char* leptris_alloc_buffer(size_t len);
3561
+
3540
3562
  /* ============================================================================
3541
3563
  * Per-Document Allocator Hooks (TODO 74)
3542
3564
  *
@@ -96,6 +96,14 @@ LEPTRIS_API leptris_deallocation_function leptris_get_memory_deallocation_functi
96
96
  return g_leptris_dealloc_function;
97
97
  }
98
98
 
99
+ /* #1219: engine-heap buffer for FFI callers whose buffers the
100
+ * engine later frees (DTD PE loaders). The hook pair keeps the
101
+ * custom-allocator configuration symmetric. */
102
+ LEPTRIS_API char* leptris_alloc_buffer(size_t len) {
103
+ if (len == 0) return NULL;
104
+ return (char*)leptris_alloc_hook(len);
105
+ }
106
+
99
107
  LEPTRIS_API LeptrisStatus leptris_document_set_allocators(
100
108
  LeptrisDocument doc,
101
109
  leptris_allocation_function alloc,
@@ -199,3 +207,23 @@ LEPTRIS_API LeptrisStatus leptris_document_set_standalone(
199
207
  doc->had_declaration = 1;
200
208
  return LEPTRIS_OK;
201
209
  }
210
+
211
+ /* #1229: the remove-half — back to as-if-the-input-had-none. The
212
+ * setters' malloc'd strings are released here (document_free checks
213
+ * for NULL), and the standalone marker returns to unset. */
214
+ LEPTRIS_API LeptrisStatus leptris_document_clear_declaration(
215
+ struct leptris_document* doc) {
216
+ if (!doc) return LEPTRIS_ERROR_NULL_ARG;
217
+ if (doc->xml_version) {
218
+ leptris_free(doc->xml_version);
219
+ doc->xml_version = NULL;
220
+ }
221
+ if (doc->encoding) {
222
+ leptris_free(doc->encoding);
223
+ doc->encoding = NULL;
224
+ }
225
+ doc->standalone = -1;
226
+ doc->decl_encoding_verbatim = 0;
227
+ doc->had_declaration = 0;
228
+ return LEPTRIS_OK;
229
+ }
@@ -17,20 +17,9 @@
17
17
  extern "C" {
18
18
  #endif
19
19
 
20
- typedef enum {
21
- LEPTRIS_DIAG_INVALID = 0,
22
- LEPTRIS_DIAG_NOT_ALLOWED_ANYWHERE, /* name never valid in model */
23
- LEPTRIS_DIAG_NOT_ALLOWED_HERE, /* valid name, wrong position */
24
- LEPTRIS_DIAG_NOT_ALLOWED_YET, /* later; missing required first */
25
- LEPTRIS_DIAG_INCOMPLETE, /* content ends before required */
26
- LEPTRIS_DIAG_MISSING_REQUIRED_ATTR,
27
- LEPTRIS_DIAG_ATTR_NOT_ALLOWED,
28
- LEPTRIS_DIAG_ATTR_VALUE_INVALID,
29
- LEPTRIS_DIAG_CHAR_CONTENT_INVALID, /* data/value/list mismatch */
30
- LEPTRIS_DIAG_COUNT_
31
- } LeptrisDiagKind;
20
+ /* LeptrisDiagKind lives in leptris/types.h (public, #1200). */
32
21
 
33
- typedef struct {
22
+ typedef struct LeptrisDiag {
34
23
  LeptrisDiagKind kind;
35
24
  char* offender_name; /* captured at emit time (strdup'd);
36
25
  * NULL when the diag has no offender.
@@ -181,7 +181,11 @@ static uint64_t digest_element(LeptrisElement e, LeptrisDigestFlags flags) {
181
181
  attrs[n].value_len = leptris_attr_value_sv(a).length;
182
182
  n++;
183
183
  }
184
- qsort(attrs, n, sizeof(DigestAttr), digest_attr_cmp);
184
+ /* ATTR_ORDER (#1200): document order is canonical — the
185
+ * walk above already collected in document order, so
186
+ * skip the sort and attribute order feeds the hash. */
187
+ if (!(flags & LEPTRIS_DIGEST_ATTR_ORDER))
188
+ qsort(attrs, n, sizeof(DigestAttr), digest_attr_cmp);
185
189
  }
186
190
  h = digest_mix_u64(h, n);
187
191
  for (size_t i = 0; i < n; i++) {
@@ -74,6 +74,33 @@ LEPTRIS_API LeptrisDoctype leptris_document_set_doctype(
74
74
  return (LeptrisDoctype)dt;
75
75
  }
76
76
 
77
+ /* #1229: the remove-half of set_doctype. The node is pool-owned, so
78
+ * it is unlinked (document children chain, when linked) — never
79
+ * freed; it stays readable until leptris_document_free. */
80
+ LEPTRIS_API LeptrisStatus leptris_document_remove_doctype(
81
+ LeptrisDocument doc) {
82
+ if (!doc) return LEPTRIS_ERROR_NULL_ARG;
83
+ LeptrisDoctypeNode* dt = (LeptrisDoctypeNode*)doc->doctype;
84
+ if (!dt) return LEPTRIS_ERROR_NOT_FOUND;
85
+ LeptrisNode* prev = NULL;
86
+ for (LeptrisNode* c = (LeptrisNode*)doc->doc_children_head; c; ) {
87
+ LeptrisNode* next = leptris_node_get_next_sibling(c);
88
+ if (c == (LeptrisNode*)dt) {
89
+ if (prev)
90
+ leptris_node_set_next_sibling(prev, next);
91
+ else
92
+ doc->doc_children_head = next;
93
+ if (doc->doc_children_tail == c)
94
+ doc->doc_children_tail = prev;
95
+ break;
96
+ }
97
+ prev = c;
98
+ c = next;
99
+ }
100
+ doc->doctype = NULL;
101
+ return LEPTRIS_OK;
102
+ }
103
+
77
104
  LEPTRIS_API const char* leptris_doctype_get_name(LeptrisDoctypeNode* doctype) {
78
105
  return doctype ? doctype->name : NULL;
79
106
  }
@@ -687,19 +687,50 @@ const char* leptris_element_lookup_namespace(LeptrisElement elem,
687
687
  }
688
688
 
689
689
  /* Children manipulation */
690
- void leptris_element_append_child_internal_doc(LeptrisElement elem, LeptrisNode* child,
691
- struct leptris_document* doc);
690
+ void leptris_element_append_child_hint_doc(LeptrisElement elem,
691
+ LeptrisNode* child,
692
+ struct leptris_document* doc,
693
+ LeptrisNode* hint);
692
694
  void leptris_element_append_child_internal(LeptrisElement elem, LeptrisNode* child) {
693
695
  if (!elem || !child) return;
694
- leptris_element_append_child_internal_doc(elem, child,
695
- leptris_element_get_document(elem));
696
+ leptris_element_append_child_hint_doc(elem, child,
697
+ leptris_element_get_document(elem),
698
+ NULL);
699
+ }
700
+
701
+ /* #1225: caller-supplied tail hint. The HTML builder tracks each
702
+ * open element's last child itself — the shared direct-mapped
703
+ * tail cache loses a persistent parent to an address collision
704
+ * every ~1KB of fresh pool allocations, and each eviction costs
705
+ * an O(children) walk (quadratic on table/text-heavy pages). The
706
+ * hint is validated (parent back-pointer + no next sibling) and
707
+ * silently ignored when stale — callers stay correct through the
708
+ * cache/walk fallback. */
709
+ void leptris_element_append_child_tail(LeptrisElement elem,
710
+ LeptrisNode* child,
711
+ struct leptris_document* doc,
712
+ LeptrisNode* hint) {
713
+ if (!elem || !child) return;
714
+ if (hint &&
715
+ (leptris_node_parent_inline(hint) != (LeptrisElement)elem ||
716
+ leptris_node_get_next_sibling(hint) != NULL))
717
+ hint = NULL;
718
+ leptris_element_append_child_hint_doc(elem, child, doc, hint);
696
719
  }
697
720
 
698
- /* doc is the caller-resolved document (NULL for detached trees) —
699
- * the public mutation path resolves it once instead of paying the
700
- * root walk + map lookup on every append (TODO 195c). */
701
721
  void leptris_element_append_child_internal_doc(LeptrisElement elem, LeptrisNode* child,
702
722
  struct leptris_document* doc) {
723
+ leptris_element_append_child_hint_doc(elem, child, doc, NULL);
724
+ }
725
+
726
+ /* doc is the caller-resolved document (NULL for detached trees) —
727
+ * the public mutation path resolves it once instead of paying the
728
+ * root walk + map lookup on every append (TODO 195c). hint is a
729
+ * pre-validated last-child hint (NULL = none). */
730
+ void leptris_element_append_child_hint_doc(LeptrisElement elem,
731
+ LeptrisNode* child,
732
+ struct leptris_document* doc,
733
+ LeptrisNode* hint) {
703
734
  if (!elem || !child) return;
704
735
 
705
736
  /* SAFETY: Verify node type field is valid before accessing it
@@ -746,6 +777,12 @@ void leptris_element_append_child_internal_doc(LeptrisElement elem, LeptrisNode*
746
777
  mut_tail = s->child;
747
778
  }
748
779
 
780
+ /* An empty child list must win over any cached tail: a NULL
781
+ * first_child with a non-NULL tail is a stale cache (or stale
782
+ * tail pointer), and appending through it splices the child
783
+ * onto a detached node while first_child stays NULL (#1220). */
784
+ int list_empty = (leptris_elem_first_child(elem) == NULL);
785
+
749
786
  /* For element children, set up linked list structure */
750
787
  if (child->type == LEPTRIS_NODE_TYPE_ELEMENT) {
751
788
  LeptrisElement child_elem = (LeptrisElement)child;
@@ -757,7 +794,11 @@ void leptris_element_append_child_internal_doc(LeptrisElement elem, LeptrisNode*
757
794
  /* Append to end of children list.
758
795
  * last_child may point to any node type (text/comment/etc.) so
759
796
  * we set its next_sibling via the type-dispatching setter. */
760
- LeptrisNode* last_node = mut_tail ? mut_tail : leptris_elem_last_child(elem);
797
+ LeptrisNode* last_node =
798
+ list_empty ? NULL
799
+ : (hint ? hint
800
+ : (mut_tail ? mut_tail
801
+ : leptris_elem_last_child(elem)));
761
802
  if (last_node) {
762
803
  /* Lane 18 P3: sequential appends' tail is an element —
763
804
  * the offset store directly; the type-dispatching node
@@ -776,7 +817,11 @@ void leptris_element_append_child_internal_doc(LeptrisElement elem, LeptrisNode*
776
817
  elem->child_count++;
777
818
  } else {
778
819
  /* For non-element children (text, cdata, comment, pi), append to linked list */
779
- LeptrisNode* last = mut_tail ? mut_tail : leptris_elem_last_child(elem);
820
+ LeptrisNode* last =
821
+ list_empty ? NULL
822
+ : (hint ? hint
823
+ : (mut_tail ? mut_tail
824
+ : leptris_elem_last_child(elem)));
780
825
  if (last) {
781
826
  leptris_node_set_next_sibling(last, (LeptrisNode*)child);
782
827
  leptris_elem_set_last_child(elem, (LeptrisNode*)child);
@@ -1062,6 +1062,11 @@ const char* leptris_element_lookup_namespace(LeptrisElement elem,
1062
1062
  void leptris_element_append_child_internal(LeptrisElement elem, LeptrisNode* child);
1063
1063
  void leptris_element_append_child_internal_doc(LeptrisElement elem, LeptrisNode* child,
1064
1064
  struct leptris_document* doc);
1065
+ /* #1225: validated last-child hint skips the tail-cache/walk. */
1066
+ void leptris_element_append_child_tail(LeptrisElement elem,
1067
+ LeptrisNode* child,
1068
+ struct leptris_document* doc,
1069
+ LeptrisNode* hint);
1065
1070
  void leptris_element_prepend_child_internal(LeptrisElement elem, LeptrisNode* child);
1066
1071
 
1067
1072
  /* Bulk allocation for subtree copy (10-15% faster for large subtrees) */
@@ -51,7 +51,14 @@ static char* mut_name_carve(struct leptris_document* doc,
51
51
  * The store writes the full word — reserve it so the block-bound
52
52
  * check accounts for the write, never just the logical size. */
53
53
  if (name_len <= 8) need = sizeof(struct leptris_document*) + 9;
54
- if (doc->mut_name_cursor + need > doc->mut_name_end) {
54
+ /* Keep every slot pointer-aligned: namebp reads the doc
55
+ * backpointer as ((struct leptris_document**)name)[-1]. The
56
+ * raw sizes above (17B for the S8 path) walk the cursor
57
+ * through every alignment mod 8 — misaligned pointer stores
58
+ * (UB, faults on alignment-strict targets). */
59
+ need = (need + sizeof(void*) - 1) & ~(sizeof(void*) - 1);
60
+ if (!doc->mut_name_cursor ||
61
+ doc->mut_name_cursor + need > doc->mut_name_end) {
55
62
  struct leptris_mut_name_block* blk =
56
63
  (struct leptris_mut_name_block*)malloc(
57
64
  sizeof(struct leptris_mut_name_block) + MUT_NAME_BLOCK_BYTES);
@@ -107,7 +114,12 @@ static char* mut_str_carve(struct leptris_document* doc, const char* s,
107
114
  size_t len) {
108
115
  if (len + 1 > MUT_NAME_BLOCK_BYTES / 4) return NULL;
109
116
  size_t need = 8 + len + 1;
110
- if (doc->mut_name_cursor + need > doc->mut_name_end) {
117
+ /* Round the stride like mut_name_carve: this carve shares the
118
+ * name block's cursor, and an unrounded advance here misaligns
119
+ * every later namebp slot (doc backpointer stores). */
120
+ need = (need + sizeof(void*) - 1) & ~(sizeof(void*) - 1);
121
+ if (!doc->mut_name_cursor ||
122
+ doc->mut_name_cursor + need > doc->mut_name_end) {
111
123
  struct leptris_mut_name_block* blk =
112
124
  (struct leptris_mut_name_block*)malloc(
113
125
  sizeof(struct leptris_mut_name_block) + MUT_NAME_BLOCK_BYTES);
@@ -536,11 +548,39 @@ LeptrisStatus leptris_element_remove_child(LeptrisElement parent, LeptrisElement
536
548
  LeptrisStatus leptris_element_remove_all_children(LeptrisElement elem) {
537
549
  if (!elem) return LEPTRIS_ERROR_NULL_ARG;
538
550
 
539
- /* Walk through all children and clear parent references */
540
- LeptrisElement child = leptris_element_get_first_child(elem);
551
+ /* Walk EVERY child. The element-filtered getters skip text and
552
+ * other non-element nodes, which left their parent backpointers
553
+ * dangling; a later append then validated a stale mutation-tail
554
+ * entry against the detached child and spliced the new subtree
555
+ * onto it instead of the emptied element (#1220: HTML set_text
556
+ * produced an empty <p/>). */
557
+ LeptrisNode* child =
558
+ leptris_node_first_child_internal((LeptrisNode*)elem);
541
559
  while (child) {
542
- LeptrisElement next = leptris_element_get_next_sibling(child);
543
- leptris_elem_set_parent(child, NULL);
560
+ LeptrisNode* next = leptris_node_get_next_sibling(child);
561
+ switch (child->type) {
562
+ case LEPTRIS_NODE_TYPE_ELEMENT:
563
+ leptris_elem_set_parent((LeptrisElement)child, NULL);
564
+ break;
565
+ case LEPTRIS_NODE_TYPE_TEXT:
566
+ leptris_textnode_set_parent((LeptrisTextNode*)child, NULL);
567
+ break;
568
+ case LEPTRIS_NODE_TYPE_COMMENT:
569
+ leptris_comment_set_parent((LeptrisCommentNode*)child, NULL);
570
+ break;
571
+ case LEPTRIS_NODE_TYPE_CDATA:
572
+ leptris_cdata_set_parent((LeptrisCDATANode*)child, NULL);
573
+ break;
574
+ case LEPTRIS_NODE_TYPE_PI:
575
+ leptris_pi_set_parent((LeptrisPINode*)child, NULL);
576
+ break;
577
+ case LEPTRIS_NODE_TYPE_ENTITY_REF:
578
+ leptris_entity_ref_set_parent(
579
+ (LeptrisEntityRefNode*)child, NULL);
580
+ break;
581
+ default:
582
+ break;
583
+ }
544
584
  child = next;
545
585
  }
546
586
 
@@ -130,6 +130,12 @@ struct LeptrisDTD {
130
130
  * from the document parser). ttdtd_free destroys the pool only
131
131
  * when this flag is set. */
132
132
  int owns_pool;
133
+ /* #1211: a declaration matched its keyword but the parser could
134
+ * not deliver the promised structure (malformed ELEMENT/ATTLIST/
135
+ * ENTITY/NOTATION). leptris_dtd_parse frees and returns NULL.
136
+ * Lenient paths — comments, PIs, unresolved external PEs,
137
+ * duplicates — never set this. */
138
+ int parse_failed;
133
139
 
134
140
  /* External parameter-entity loader (application-owned I/O).
135
141
  * NULL = external PEs are not resolvable and are skipped.
@@ -6,8 +6,12 @@
6
6
  * Uses stateless parsing with hash table storage for O(1) lookup.
7
7
  */
8
8
 
9
+ #include "../../include/leptris/dtd.h"
9
10
  #include "model.h"
10
11
  #include "../memory/pool.h"
12
+ /* Engine-heap free for PE-loader buffers (#1219); declared
13
+ * locally — the full internal header tangles include order. */
14
+ extern void leptris_free_hook(void* ptr);
11
15
  #include <stdlib.h>
12
16
  #include <string.h>
13
17
  #include <ctype.h>
@@ -187,11 +191,16 @@ static DTDEntityDecl* dtd_parse_entity(DTDParser* p) {
187
191
  entity->notation_name = dtd_parse_name(p);
188
192
  }
189
193
 
190
- /* Skip to '>' */
194
+ /* Skip to '>' - unterminated (input exhausted first) is a
195
+ * malformed declaration: the NULL-on-error contract (#1211). */
191
196
  while (!dtd_at_end(p) && dtd_peek(p) != '>') {
192
197
  dtd_advance(p);
193
198
  }
194
- if (dtd_peek(p) == '>') dtd_advance(p);
199
+ if (dtd_peek(p) == '>') {
200
+ dtd_advance(p);
201
+ } else {
202
+ return NULL;
203
+ }
195
204
 
196
205
  return entity;
197
206
  }
@@ -226,6 +235,7 @@ static DTDElementDecl* dtd_parse_element(DTDParser* p) {
226
235
  int depth = 0;
227
236
  int in_quote = 0;
228
237
  char quote_char = 0;
238
+ int closed = 0;
229
239
 
230
240
  while (!dtd_at_end(p)) {
231
241
  char c = dtd_peek(p);
@@ -240,12 +250,15 @@ static DTDElementDecl* dtd_parse_element(DTDParser* p) {
240
250
  } else if (c == ')') {
241
251
  depth--;
242
252
  dtd_advance(p);
243
- if (depth == 0) break;
253
+ if (depth == 0) { closed = 1; break; }
244
254
  dtd_advance(p);
245
255
  continue;
246
256
  }
247
257
  dtd_advance(p);
248
258
  }
259
+ /* #1211: running out of input before the model closes is a
260
+ * malformed declaration — the NULL-on-error contract. */
261
+ if (!closed) return NULL;
249
262
 
250
263
  size_t len = p->pos - start;
251
264
  elem->content_model = (char*)leptris_pool_alloc(p->pool, len + 1);
@@ -262,11 +275,16 @@ static DTDElementDecl* dtd_parse_element(DTDParser* p) {
262
275
  }
263
276
  }
264
277
 
265
- /* Skip to '>' */
278
+ /* Skip to '>' - unterminated (input exhausted first) is a
279
+ * malformed declaration: the NULL-on-error contract (#1211). */
266
280
  while (!dtd_at_end(p) && dtd_peek(p) != '>') {
267
281
  dtd_advance(p);
268
282
  }
269
- if (dtd_peek(p) == '>') dtd_advance(p);
283
+ if (dtd_peek(p) == '>') {
284
+ dtd_advance(p);
285
+ } else {
286
+ return NULL;
287
+ }
270
288
 
271
289
  return elem;
272
290
  }
@@ -305,11 +323,16 @@ static DTDNotationDecl* dtd_parse_notation(DTDParser* p) {
305
323
  }
306
324
  }
307
325
 
308
- /* Skip to '>' */
326
+ /* Skip to '>' - unterminated (input exhausted first) is a
327
+ * malformed declaration: the NULL-on-error contract (#1211). */
309
328
  while (!dtd_at_end(p) && dtd_peek(p) != '>') {
310
329
  dtd_advance(p);
311
330
  }
312
- if (dtd_peek(p) == '>') dtd_advance(p);
331
+ if (dtd_peek(p) == '>') {
332
+ dtd_advance(p);
333
+ } else {
334
+ return NULL;
335
+ }
313
336
 
314
337
  return notation;
315
338
  }
@@ -357,7 +380,7 @@ LeptrisDTD* leptris_dtd_parse_internal_subset(const char* dtd_content, size_t le
357
380
  * @param len Length in bytes
358
381
  * @return 1 on success, -1 on invalid arguments
359
382
  */
360
- void leptris_dtd_set_pe_loader(LeptrisDTD* dtd,
383
+ LEPTRIS_API void leptris_dtd_set_pe_loader(LeptrisDTD* dtd,
361
384
  char* (*loader)(void* user_data,
362
385
  const char* system_id,
363
386
  size_t* out_len),
@@ -367,7 +390,7 @@ void leptris_dtd_set_pe_loader(LeptrisDTD* dtd,
367
390
  dtd->pe_loader_user = user_data;
368
391
  }
369
392
 
370
- int leptris_dtd_parse_external_subset(LeptrisDTD* dtd, const char* content,
393
+ LEPTRIS_API int leptris_dtd_parse_external_subset(LeptrisDTD* dtd, const char* content,
371
394
  size_t len) {
372
395
  if (!dtd || !content || len == 0) return -1;
373
396
  dtd_parse_into(dtd, content, len, 0);
@@ -443,7 +466,7 @@ if (text[i] == '%' && i + 1 < len) {
443
466
  memcpy(grown + w + loaded, text + j + 1,
444
467
  len - j - 1);
445
468
  grown[w + loaded + (len - j - 1)] = '\0';
446
- free(got);
469
+ leptris_free_hook(got);
447
470
  /* Recurse once for nested refs in the
448
471
  * loaded content. */
449
472
  char* rec = dtd_substitute_decl_pes(
@@ -451,7 +474,7 @@ if (text[i] == '%' && i + 1 < len) {
451
474
  w + loaded + (len - j - 1), depth + 1);
452
475
  return rec ? rec : grown;
453
476
  }
454
- free(got);
477
+ leptris_free_hook(got);
455
478
  }
456
479
  }
457
480
  if (val) {
@@ -587,16 +610,22 @@ static void dtd_parse_into(LeptrisDTD* dtd, const char* dtd_content,
587
610
  DTDEntityDecl* entity = dtd_parse_entity(&parser);
588
611
  if (entity) {
589
612
  ttdtd_add_entity(dtd, entity);
613
+ } else {
614
+ dtd->parse_failed = 1; /* #1211 */
590
615
  }
591
616
  } else if (dtd_match(&parser, "<!ELEMENT")) {
592
617
  DTDElementDecl* elem = dtd_parse_element(&parser);
593
618
  if (elem) {
594
619
  ttdtd_add_element(dtd, elem);
620
+ } else {
621
+ dtd->parse_failed = 1; /* #1211 */
595
622
  }
596
623
  } else if (dtd_match(&parser, "<!NOTATION")) {
597
624
  DTDNotationDecl* notation = dtd_parse_notation(&parser);
598
625
  if (notation) {
599
626
  ttdtd_add_notation(dtd, notation);
627
+ } else {
628
+ dtd->parse_failed = 1; /* #1211 */
600
629
  }
601
630
  } else if (dtd_match(&parser, "<!ATTLIST")) {
602
631
  /* Parse one or more <!ATTLIST element-name attr-decl+>
@@ -609,6 +638,7 @@ static void dtd_parse_into(LeptrisDTD* dtd, const char* dtd_content,
609
638
  char* elem_name = dtd_parse_name(&parser);
610
639
  if (!elem_name) {
611
640
  /* Malformed; skip to '>' */
641
+ dtd->parse_failed = 1; /* #1211 */
612
642
  while (!dtd_at_end(&parser) && dtd_peek(&parser) != '>') {
613
643
  dtd_advance(&parser);
614
644
  }
@@ -811,7 +841,7 @@ void leptris_dtd_free(LeptrisDTD* dtd) {
811
841
  * leptris_dtd_parse_internal_subset() directly with the document's
812
842
  * pool instead.
813
843
  */
814
- LeptrisDTD* leptris_dtd_parse(const char* dtd_content, size_t len) {
844
+ LEPTRIS_API LeptrisDTD* leptris_dtd_parse(const char* dtd_content, size_t len) {
815
845
  LeptrisMemoryPool* pool = leptris_pool_create();
816
846
  if (!pool) return NULL;
817
847
  LeptrisDTD* dtd = leptris_dtd_parse_internal_subset(dtd_content, len, pool);
@@ -823,5 +853,12 @@ LeptrisDTD* leptris_dtd_parse(const char* dtd_content, size_t len) {
823
853
  * Document-pool DTDs (created via the internal parser path) leave
824
854
  * owns_pool=0; their pool is destroyed with the document. */
825
855
  dtd->owns_pool = 1;
856
+ /* #1211: the documented NULL-on-error contract — malformed
857
+ * declarations (keyword matched, structure undeliverable) free
858
+ * the whole pool; lenient skips never set parse_failed. */
859
+ if (dtd->parse_failed) {
860
+ leptris_pool_destroy(pool);
861
+ return NULL;
862
+ }
826
863
  return dtd;
827
864
  }
@@ -7,6 +7,7 @@
7
7
  #include "common/port.h" /* LEPTRIS_THREAD_LOCAL (TODO.concurrency/01) */
8
8
  #include <stdio.h>
9
9
  #include <string.h>
10
+ #include "dom/diag.h" /* LeptrisDiag layout (#1200) */
10
11
 
11
12
  /* Error state — THREAD-LOCAL since TODO.concurrency/01: concurrent
12
13
  * parses (one document per thread) no longer race the channel. The
@@ -131,4 +132,25 @@ LEPTRIS_API const char* leptris_status_string(LeptrisStatus status) {
131
132
  case LEPTRIS_ERROR_IO: return "I/O error";
132
133
  default: return "Unknown error";
133
134
  }
135
+ }
136
+
137
+ /* ---- #1200: recover-class parse diagnostics ------------------- */
138
+
139
+ LEPTRIS_API size_t leptris_document_parse_diag_count(LeptrisDocument doc) {
140
+ return doc ? (size_t)doc->parse_diag_count : 0;
141
+ }
142
+
143
+ LEPTRIS_API int leptris_document_parse_diag(LeptrisDocument doc,
144
+ size_t index,
145
+ LeptrisDiagKind* kind,
146
+ char* message,
147
+ size_t message_cap) {
148
+ if (!doc || index >= (size_t)doc->parse_diag_count) return 0;
149
+ struct LeptrisDiag* d = (struct LeptrisDiag*)doc->parse_diags + index;
150
+ if (kind) *kind = d->kind;
151
+ if (message && message_cap) {
152
+ strncpy(message, d->message, message_cap - 1);
153
+ message[message_cap - 1] = '\0';
154
+ }
155
+ return 1;
134
156
  }