Skip to content

Commit 168368f

Browse files
committed
feat(c14n): add canonicalization functionality
1 parent 350536b commit 168368f

File tree

5 files changed

+172
-1
lines changed

5 files changed

+172
-1
lines changed

xmlua.rockspec

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ build = {
3737
["xmlua"] = "xmlua.lua",
3838
["xmlua.attribute"] = "xmlua/attribute.lua",
3939
["xmlua.attribute-declaration"] = "xmlua/attribute-declaration.lua",
40+
["xmlua.c14n"] = "xmlua/c14n.lua",
4041
["xmlua.cdata-section"] = "xmlua/cdata-section.lua",
4142
["xmlua.converter"] = "xmlua/converter.lua",
4243
["xmlua.comment"] = "xmlua/comment.lua",
@@ -51,6 +52,7 @@ build = {
5152
["xmlua.html"] = "xmlua/html.lua",
5253
["xmlua.html-sax-parser"] = "xmlua/html-sax-parser.lua",
5354
["xmlua.libxml2"] = "xmlua/libxml2.lua",
55+
["xmlua.libxml2.c14n"] = "xmlua/libxml2/c14n.lua",
5456
["xmlua.libxml2.dict"] = "xmlua/libxml2/dict.lua",
5557
["xmlua.libxml2.encoding"] = "xmlua/libxml2/encoding.lua",
5658
["xmlua.libxml2.entities"] = "xmlua/libxml2/entities.lua",

xmlua/c14n.lua

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
local C14n = {}
2+
3+
local libxml2 = require("xmlua.libxml2")
4+
local ffi = require("ffi")
5+
6+
7+
8+
local C14N_MODES = {
9+
C14N_1_0 = ffi.C.XML_C14N_1_0, -- Original C14N 1.0 spec
10+
C14N_EXCLUSIVE_1_0 = ffi.C.XML_C14N_EXCLUSIVE_1_0, -- Exclusive C14N 1.0 spec
11+
C14N_1_1 = ffi.C.XML_C14N_1_1, -- C14N 1.1 spec
12+
}
13+
14+
15+
16+
local C14N_MODES_LOOKUP = {} -- lookup by name or number, returns the number
17+
for name, number in pairs(C14N_MODES) do
18+
C14N_MODES_LOOKUP[name] = number
19+
C14N_MODES_LOOKUP[number] = number
20+
end
21+
22+
23+
24+
-- list can be a string (space separated), an array of strings, or nil
25+
local function getNamespacePrefixArray(list)
26+
local list = list or {}
27+
28+
if type(list) == "string" then
29+
-- list is a string, assume it is the space separated PrefixList attribute, split it
30+
local list_str = list
31+
list = {}
32+
list_str:gsub("([^%s]+)", function(cap) list[#list+1] = cap end)
33+
end
34+
35+
if #list == 0 then
36+
return nil
37+
end
38+
39+
local result = ffi.new('xmlChar*[?]', #list+1)
40+
local refs = {}
41+
for i, prefix in ipairs(list) do
42+
local c_ns = ffi.new("unsigned char[?]", #prefix+1, prefix)
43+
ffi.copy(c_ns, prefix)
44+
result[i-1] = c_ns
45+
refs[i] = c_ns -- hold on to refs to prevent GC while in use
46+
end
47+
result[#list] = nil
48+
49+
return ffi.gc(result, function(ptr)
50+
refs = nil -- release references, so they can be GC'ed
51+
end)
52+
end
53+
54+
55+
56+
local function getNodesList(nodes)
57+
if (not nodes) or #nodes == 0 then
58+
return nil
59+
end
60+
61+
local nodeTab = ffi.new("xmlNodePtr[?]", #nodes)
62+
for i = 1, #nodes do
63+
nodeTab[i - 1] = nodes[i] -- FFI side is 0 indexed
64+
end
65+
66+
local set = ffi.new("xmlNodeSet")
67+
set.nodeNr = #nodes
68+
set.nodeMax = #nodes
69+
set.nodeTab = nodeTab
70+
71+
return set
72+
end
73+
74+
75+
76+
--- Canonicalise an xmlDocument or set of elements.
77+
-- @param self xmlDoc from which to canonicalize elements
78+
-- @param nodes list of nodes to include when canonicalizing, if 'nil' entire doc will be canonicalized
79+
-- @param mode any of C14N_1_0, C14N_EXCLUSIVE_1_0 (default), C14N_1_1
80+
-- @param inclusive_ns_prefixes array, or space-separated string, of namespace prefixes to include
81+
-- @param with_comments if truthy, comments will be included (default: false)
82+
-- @return string containing canonicalized xml
83+
function C14n:c14n(nodes, mode, inclusive_ns_prefixes, with_comments)
84+
if mode == nil then -- default is exclusive 1.0
85+
mode = "C14N_EXCLUSIVE_1_0"
86+
end
87+
with_comments = with_comments and 1 or 0 -- default = not including comments
88+
89+
mode = assert(C14N_MODES_LOOKUP[mode], "mode must be a valid C14N mode constant")
90+
local prefixes = getNamespacePrefixArray(inclusive_ns_prefixes)
91+
local nodeSet = getNodesList(nodes)
92+
local buffer = libxml2.xmlBufferCreate()
93+
local output_buffer = libxml2.xmlOutputBufferCreate(buffer)
94+
95+
local success = libxml2.xmlC14NDocSaveTo(self.document, nodeSet, mode,
96+
prefixes, with_comments, output_buffer)
97+
98+
if success < 0 then
99+
return nil, "failed to generate C14N string"
100+
end
101+
return libxml2.xmlBufferGetContent(buffer)
102+
end
103+
104+
105+
return C14n

xmlua/document.lua

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ local to_string = converter.to_string
77

88
local Serializable = require("xmlua.serializable")
99
local Searchable = require("xmlua.searchable")
10+
local C14n = require("xmlua.c14n")
1011

1112

1213
local CDATASection
@@ -36,7 +37,8 @@ local metatable = {}
3637
function metatable.__index(document, key)
3738
return methods[key] or
3839
Serializable[key] or
39-
Searchable[key]
40+
Searchable[key] or
41+
C14n[key]
4042
end
4143

4244
function methods:root()

xmlua/libxml2.lua

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ require("xmlua.libxml2.html-tree")
1515
require("xmlua.libxml2.xmlsave")
1616
require("xmlua.libxml2.xpath")
1717
require("xmlua.libxml2.entities")
18+
require("xmlua.libxml2.c14n")
1819

1920
local ffi = require("ffi")
2021
local loaded, xml2 = pcall(ffi.load, "xml2")
@@ -68,6 +69,17 @@ else
6869
libxml2.xmlFree = xml2.xmlFree
6970
end
7071

72+
local function __xmlC14NDocSaveToIsAvailable()
73+
local success, err = pcall(function()
74+
local func = xml2.xmlC14NDocSaveTo
75+
end)
76+
return success, err
77+
end
78+
79+
if __xmlC14NDocSaveToIsAvailable() then
80+
libxml2.xmlC14NDocSaveTo = xml2.xmlC14NDocSaveTo
81+
end
82+
7183
libxml2.xmlInitParser = xml2.xmlInitParser
7284
libxml2.xmlCleanupParser = xml2.xmlCleanupParser
7385

@@ -581,6 +593,10 @@ function libxml2.xmlBufferCreate()
581593
return ffi.gc(xml2.xmlBufferCreate(), xml2.xmlBufferFree)
582594
end
583595

596+
function libxml2.xmlOutputBufferCreate(buffer)
597+
return ffi.gc(xml2.xmlOutputBufferCreateBuffer(buffer, nil), xml2.xmlOutputBufferClose)
598+
end
599+
584600
function libxml2.xmlBufferGetContent(buffer)
585601
return ffi.string(buffer.content, buffer.use)
586602
end

xmlua/libxml2/c14n.lua

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
local ffi = require("ffi")
2+
3+
ffi.cdef[[
4+
typedef enum {
5+
XML_C14N_1_0 = 0, /* Original C14N 1.0 spec */
6+
XML_C14N_EXCLUSIVE_1_0 = 1, /* Exclusive C14N 1.0 spec */
7+
XML_C14N_1_1 = 2, /* C14N 1.1 spec */
8+
} xmlC14NMode;
9+
10+
typedef unsigned char xmlChar;
11+
typedef struct _xmlBuffer xmlBuffer;
12+
typedef xmlBuffer *xmlBufferPtr;
13+
14+
typedef struct _xmlCharEncodingHandler xmlCharEncodingHandler;
15+
typedef xmlCharEncodingHandler *xmlCharEncodingHandlerPtr;
16+
17+
typedef int (*xmlOutputWriteCallback) (void * context, const char * buffer, int len);
18+
typedef int (*xmlOutputCloseCallback) (void * context);
19+
20+
struct _xmlOutputBuffer {
21+
void* context;
22+
xmlOutputWriteCallback writecallback;
23+
xmlOutputCloseCallback closecallback;
24+
25+
xmlCharEncodingHandlerPtr encoder; /* I18N conversions to UTF-8 */
26+
27+
xmlBufferPtr buffer; /* Local buffer encoded in UTF-8 or ISOLatin */
28+
xmlBufferPtr conv; /* if encoder != NULL buffer for output */
29+
int written; /* total number of byte written */
30+
int error;
31+
};
32+
33+
typedef struct _xmlOutputBuffer xmlOutputBuffer;
34+
typedef xmlOutputBuffer *xmlOutputBufferPtr;
35+
36+
xmlOutputBufferPtr xmlAllocOutputBuffer (xmlCharEncodingHandlerPtr encoder);
37+
xmlOutputBufferPtr xmlOutputBufferCreateBuffer (xmlBufferPtr buffer, xmlCharEncodingHandlerPtr encoder);
38+
int xmlOutputBufferClose (xmlOutputBufferPtr out);
39+
40+
int xmlC14NDocSaveTo (xmlDocPtr doc,
41+
xmlNodeSetPtr nodes,
42+
int mode, /* a xmlC14NMode */
43+
xmlChar **inclusive_ns_prefixes,
44+
int with_comments,
45+
xmlOutputBufferPtr buf);
46+
]]

0 commit comments

Comments
 (0)