Module: Leptris::XML::FFI

Extended by:
FFI::Library
Defined in:
lib/leptris/xml/ffi.rb

Defined Under Namespace

Classes: ParseOptionsStruct, PullEventStruct, SAXHandler, SaxEventRecord, SerializeExtStruct, SerializeOptions

Constant Summary collapse

SAX_EVENT_START_DOCUMENT =
0
SAX_EVENT_END_DOCUMENT =
1
SAX_EVENT_START_ELEMENT =
2
SAX_EVENT_END_ELEMENT =
3
SAX_EVENT_CHARACTERS =
4
SAX_EVENT_COMMENT =
5
SAX_EVENT_CDATA =
6
SAX_EVENT_PI =
7
SAX_EVENT_START_PREFIX =
8
SAX_EVENT_END_PREFIX =
9
SAX_EVENT_ERROR =
10
ITERPARSE_TOP_LEVEL =
0
ITERPARSE_FULL_DOCUMENT =
1
LEPTRIS_OK =
0
LEPTRIS_ERROR_MEMORY =
-1
LEPTRIS_ERROR_PARSE =
-2
LEPTRIS_ERROR_XPATH =
-3
LEPTRIS_ERROR_NULL_ARG =
-4
LEPTRIS_ERROR_INVALID_ARG =
-5
LEPTRIS_ERROR_NOT_FOUND =
-6
LEPTRIS_ERROR_IO =
-7
LEPTRIS_ERROR_NOT_IMPLEMENTED =
-8
XPATH_NODESET =
0
XPATH_BOOLEAN =
1
XPATH_NUMBER =
2
XPATH_STRING =
3
XPATH_NODE_ELEMENT =
0
XPATH_NODE_ATTRIBUTE =
1
XPATH_NODE_TEXT =
2
XPATH_NODE_OTHER =
3
NODE_ELEMENT =
0
NODE_TEXT =
1
NODE_COMMENT =
2
NODE_CDATA =
3
NODE_PI =
4
NODE_DOCTYPE =
5
NODE_ATTRIBUTE =
6
C14N_1_0 =
0
C14N_1_1 =
1
C14N_MODE_CANONICAL =
0
C14N_MODE_EXCLUSIVE =
1
TRAVERSE_PRE_ORDER =
0
TRAVERSE_POST_ORDER =
1
PULL_START_ELEMENT =
0
PULL_END_ELEMENT =
1
PULL_TEXT =
2
PULL_COMMENT =
3
PULL_CDATA =
4
PULL_PI =
5
PULL_END_DOCUMENT =
6
PULL_ERROR =
7
LEPTRIS_PARSE_DEFAULT =
0
LEPTRIS_PARSE_DROP_WS_TEXT =
1
LEPTRIS_PARSE_DTDATTR =

libleptris >= 1.9.8 (#606): apply DTD ATTLIST default (and #FIXED) attribute values at parse time — libxml2 XML_PARSE_DTDATTR opt-in parity.

2
NS_SET_CACHE =

Namespace-binding lifecycle, written once: flatten the prefix/URI hash into the alternating CStringArray wire format and build the C set. Sets are cached per distinct vocabulary: the XPath VM reads the set as a const map during evaluation (vm.c) and never mutates it, so one set serves the process across queries and threads — building the wire format plus a C set per query cost ~6.7 µs, 2.6× a no-namespace query on a small document. Cached sets are never freed (bounded by the process's distinct namespace vocabularies — the same trade the CSS translation cache makes). Failed builds raise before caching.

{}
XPATH_KIND_HINT =

XPath result-set batch (leptris_xpath_result_get_nodes_ex): the call fills out_kinds in the 4-value XPATH_NODE space (ELEMENT/ATTRIBUTE/TEXT/OTHER). ELEMENT is the only value that maps unambiguously — XPath's data model reports CDATA as TEXT, so a TEXT hint cannot distinguish Text from CDATA (different content getters) — everything else falls back to get_type in the wrapper. Returns [pointers, element_hints].

{ XPATH_NODE_ELEMENT => NODE_ELEMENT }.freeze

Class Method Summary collapse

Class Method Details

.check_status(status) ⇒ Object

Single status seam: turns a C status code into a Ruby error, appending the library's last-error detail when present. The detail is a library-global string, so it can only be treated as best-effort context.



823
824
825
826
827
828
# File 'lib/leptris/xml/ffi.rb', line 823

def self.check_status(status)
  unless status == LEPTRIS_OK
    raise Leptris::XML::Error, status_message(status)
  end
  status
end

.fetch_attributes_raw(el_ptr) ⇒ Object

Raw qname-ordered attribute pairs INCLUDING xmlns at their byte positions (leptris_element_attributes_raw, 1.9.18 #635) — the streaming SAX contract's mixed view.



943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
# File 'lib/leptris/xml/ffi.rb', line 943

def self.fetch_attributes_raw(el_ptr)
  total = leptris_element_attributes_raw(el_ptr, nil, nil, 0)
  return [] if total.zero?
  scratch = (Thread.current[:leptris_scratch] ||= {})
  names = scratch[:attr_names]
  if names.nil? || names.size / PTR_BYTES < total
    names&.free
    names = scratch[:attr_names] =
      ::FFI::MemoryPointer.new(:pointer, total)
  end
  values = scratch[:attr_values]
  if values.nil? || values.size / PTR_BYTES < total
    values&.free
    values = scratch[:attr_values] =
      ::FFI::MemoryPointer.new(:pointer, total)
  end
  copied = leptris_element_attributes_raw(
    el_ptr, names, values, total)
  Array.new(copied) do |i|
    [utf8(names.get_pointer(i * PTR_BYTES).read_string),
     utf8(values.get_pointer(i * PTR_BYTES).read_string)]
  end
end

.fetch_children(c_ptr) ⇒ Object

Returns [pointers, kinds] — the kinds ride the batch (leptris_node_children_ex, 1.9.18 #617) so wrappers skip the per-child get_type dispatch.



913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
# File 'lib/leptris/xml/ffi.rb', line 913

def self.fetch_children(c_ptr)
  scratch = (Thread.current[:leptris_scratch] ||= {})
  buffer = scratch[:pointers]
  if buffer.nil?
    buffer = scratch[:pointers] =
      ::FFI::MemoryPointer.new(:pointer, FETCH_INITIAL)
  end
  # size the kinds buffer from the POINTERS buffer's capacity —
  # it may have grown past FETCH_INITIAL on earlier use, and C
  # writes one kind per copied child
  cap = buffer.size / PTR_BYTES
  kinds = scratch_ints(cap)
  copied = leptris_node_children_ex(c_ptr, buffer, kinds, cap)
  if copied == cap
    total = leptris_node_children_ex(c_ptr, nil, nil, 0)
    if total > cap
      buffer.free
      buffer = scratch[:pointers] =
        ::FFI::MemoryPointer.new(:pointer, total)
      kinds = scratch_ints(total)
      copied = leptris_node_children_ex(c_ptr, buffer, kinds, total)
    end
  end
  [buffer.get_array_of_pointer(0, copied),
   kinds.get_array_of_int(0, copied)]
end

.fetch_element_children(el_ptr) ⇒ Object

Element-only child batch (the mirror of fetch_children over leptris_element_children): element receivers skip wrapping — and paying for — the text/comment children they will filter out anyway; every wrap carries the ELEMENT hint so the per-child get_type dispatch disappears too.



978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# File 'lib/leptris/xml/ffi.rb', line 978

def self.fetch_element_children(el_ptr)
  scratch = (Thread.current[:leptris_scratch] ||= {})
  buffer = scratch[:pointers]
  if buffer.nil?
    buffer = scratch[:pointers] =
      ::FFI::MemoryPointer.new(:pointer, FETCH_INITIAL)
  end
  cap = buffer.size / PTR_BYTES
  copied = leptris_element_children(el_ptr, buffer, cap)
  if copied == cap
    # leptris_element_children has NO count-only mode (NULL
    # returns 0, unlike leptris_node_children) — size the
    # regrow from the dedicated counter.
    total = leptris_element_child_count(el_ptr)
    if total > cap
      buffer.free
      buffer = scratch[:pointers] =
        ::FFI::MemoryPointer.new(:pointer, total)
      copied = leptris_element_children(el_ptr, buffer, total)
    end
  end
  buffer.get_array_of_pointer(0, copied)
end

.fetch_result_nodes(result_ptr, count) ⇒ Object



1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
# File 'lib/leptris/xml/ffi.rb', line 1011

def self.fetch_result_nodes(result_ptr, count)
  return [[], nil] if count.zero?
  buffer = scratch_pointers(count)
  kinds = scratch_ints(count)
  copied = leptris_xpath_result_get_nodes_ex(
    result_ptr, buffer, kinds, count)
  hints = kinds.get_array_of_int(0, copied)
    .map { |k| XPATH_KIND_HINT[k] }
  [buffer.get_array_of_pointer(0, copied), hints]
end

.ns_set_for(hash) ⇒ Object



853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
# File 'lib/leptris/xml/ffi.rb', line 853

def self.ns_set_for(hash)
  pairs = hash.map { |prefix, uri| [prefix.to_s, uri.to_s] }
  key = if pairs.size == 1
          "#{pairs[0][0]}\x00#{pairs[0][1]}"
        else
          pairs.sort.join("\x00")
        end
  NS_SET_CACHE.fetch(key) do
    flat = pairs.flatten
    buffer, _anchors = Leptris::XML::CStringArray.to_c(flat)
    set = leptris_xpath_ns_set_new_from_pairs(buffer, flat.length / 2)
    if set.null?
      raise Leptris::XML::Error,
        "leptris_xpath_ns_set_new_from_pairs failed"
    end
    NS_SET_CACHE[key] = set
  end
end

.parse_fragment_with_status(xml, document_ptr) ⇒ Object

Fragment parse with the status out-param read: returns [document-or-fragment pointer, status].



1024
1025
1026
1027
1028
1029
# File 'lib/leptris/xml/ffi.rb', line 1024

def self.parse_fragment_with_status(xml, document_ptr)
  status = ::FFI::MemoryPointer.new(:int)
  raw = leptris_parse_fragment(
    xml, xml.bytesize, document_ptr, status)
  [raw, status.read_int]
end

.read_owned_string(ptr) ⇒ Object

Reads an libleptris-owned char* result and frees it as one unit, so a call site can neither leak nor double-free.



813
814
815
816
817
# File 'lib/leptris/xml/ffi.rb', line 813

def self.read_owned_string(ptr)
  return "" if ptr.nil? || ptr.null?
  ptr.read_string.tap { leptris_free_string(ptr) }
    .force_encoding(Encoding::UTF_8)
end

.read_pi_data(data) ⇒ Object

libxml2 consumes the whole whitespace run following the PI target when parsing; the engine retains it as data (leptris-ruby#85). Every Ruby-facing PI-data read normalizes here — whitespace-only data becomes "" so "pi-without-data" PIs report empty consistently. Read-time only: PI#data= stores verbatim.



1037
1038
1039
# File 'lib/leptris/xml/ffi.rb', line 1037

def self.read_pi_data(data)
  data&.sub(/\A[ \t\r\n]+/, "")
end

.scratch_bytes(need) ⇒ Object

Byte scratch for serialize-into cycles: the size+fill pair allocated and freed a buffer per call — inner_html serializes each child, and the allocation was most of the per-child cost. Grows to the largest serialization seen; no Ruby reentrancy during a serialize call.



892
893
894
895
896
897
898
899
900
# File 'lib/leptris/xml/ffi.rb', line 892

def self.scratch_bytes(need)
  scratch = (Thread.current[:leptris_scratch] ||= {})
  ptr = scratch[:bytes]
  if ptr.nil? || ptr.size < need
    ptr&.free
    ptr = scratch[:bytes] = ::FFI::MemoryPointer.new(:char, need)
  end
  ptr
end

.scratch_events(count) ⇒ Object

Pull-event staging block for leptris_pull_next_batch (the #589 bulk transport): LeptrisPullEvent records, stride from the struct layout that mirrors the ABI.



1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
# File 'lib/leptris/xml/ffi.rb', line 1078

def self.scratch_events(count)
  need = count * PullEventStruct.size
  scratch = (Thread.current[:leptris_scratch] ||= {})
  ptr = scratch[:pull_events]
  if ptr.nil? || ptr.size < need
    ptr&.free
    ptr = scratch[:pull_events] = ::FFI::MemoryPointer.new(need)
  end
  ptr
end

.scratch_ints(count) ⇒ Object



1065
1066
1067
1068
1069
1070
1071
1072
1073
# File 'lib/leptris/xml/ffi.rb', line 1065

def self.scratch_ints(count)
  scratch = (Thread.current[:leptris_scratch] ||= {})
  ptr = scratch[:ints]
  if ptr.nil? || ptr.size / INT_BYTES < count
    ptr&.free
    ptr = scratch[:ints] = ::FFI::MemoryPointer.new(:int, count)
  end
  ptr
end

.scratch_pointers(count) ⇒ Object



1055
1056
1057
1058
1059
1060
1061
1062
1063
# File 'lib/leptris/xml/ffi.rb', line 1055

def self.scratch_pointers(count)
  scratch = (Thread.current[:leptris_scratch] ||= {})
  ptr = scratch[:pointers]
  if ptr.nil? || ptr.size / PTR_BYTES < count
    ptr&.free
    ptr = scratch[:pointers] = ::FFI::MemoryPointer.new(:pointer, count)
  end
  ptr
end

.serialize_into_string(ffi_function, c_ptr, options) ⇒ Object

Caller-buffer serialization cycle: size query (buf=NULL), allocate exactly, fill, read. Since libleptris 1.9.0 the pair reuses one serialization through the per-document cache. ffi_function is leptris_document_serialize_into or leptris_element_serialize_into. The buffer holds the serialization + NUL and XML output is NUL-free, so the bounded read is exact.



879
880
881
882
883
884
885
# File 'lib/leptris/xml/ffi.rb', line 879

def self.serialize_into_string(ffi_function, c_ptr, options)
  need = ffi_function.call(c_ptr, nil, 0, nil, options)
  return "" if need.zero?
  buffer = scratch_bytes(need)
  ffi_function.call(c_ptr, buffer, need, nil, options)
  buffer.read_string.force_encoding(Encoding::UTF_8)
end

.status_message(status) ⇒ Object



830
831
832
833
834
# File 'lib/leptris/xml/ffi.rb', line 830

def self.status_message(status)
  base = leptris_status_string(status).to_s
  detail = leptris_last_error.to_s
  detail.empty? ? base : "#{base} (#{detail})"
end

.utf8(str) ⇒ Object

Attribute strings cross the seam UTF-8 on both faces (ADR-0002).



969
970
971
# File 'lib/leptris/xml/ffi.rb', line 969

def self.utf8(str)
  str.nil? ? nil : str.force_encoding(Encoding::UTF_8)
end

.with_ns_set(hash) {|ns_set_for(hash)| ... } ⇒ Object

Yields:



849
850
851
# File 'lib/leptris/xml/ffi.rb', line 849

def self.with_ns_set(hash)
  yield ns_set_for(hash)
end