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
PULL_START_PREFIX =

Prefix events (types.h LeptrisPullEventType 8/9): the default namespace's prefix is legitimately "".

8
PULL_END_PREFIX =
9
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.



841
842
843
844
845
846
# File 'lib/leptris/xml/ffi.rb', line 841

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.



968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
# File 'lib/leptris/xml/ffi.rb', line 968

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.



931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
# File 'lib/leptris/xml/ffi.rb', line 931

def self.fetch_children(c_ptr)
  # One thread-local access serves both buffers (the helper
  # re-looked it up); the kinds buffer sizes from the POINTERS
  # buffer's capacity — it may have grown past FETCH_INITIAL on
  # earlier use, and C writes one kind per copied child.
  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
  kinds = scratch[:ints]
  if kinds.nil? || kinds.size / INT_BYTES < cap
    kinds&.free
    kinds = scratch[:ints] = ::FFI::MemoryPointer.new(:int, cap)
  end
  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.free
      kinds = scratch[:ints] =
        ::FFI::MemoryPointer.new(:int, 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.



1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
# File 'lib/leptris/xml/ffi.rb', line 1003

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



1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
# File 'lib/leptris/xml/ffi.rb', line 1036

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



871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
# File 'lib/leptris/xml/ffi.rb', line 871

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].



1049
1050
1051
1052
1053
1054
# File 'lib/leptris/xml/ffi.rb', line 1049

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.



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

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.



1062
1063
1064
# File 'lib/leptris/xml/ffi.rb', line 1062

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.



910
911
912
913
914
915
916
917
918
# File 'lib/leptris/xml/ffi.rb', line 910

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.



1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
# File 'lib/leptris/xml/ffi.rb', line 1103

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



1090
1091
1092
1093
1094
1095
1096
1097
1098
# File 'lib/leptris/xml/ffi.rb', line 1090

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



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

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.



897
898
899
900
901
902
903
# File 'lib/leptris/xml/ffi.rb', line 897

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



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

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).



994
995
996
# File 'lib/leptris/xml/ffi.rb', line 994

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

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

Yields:



867
868
869
# File 'lib/leptris/xml/ffi.rb', line 867

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