1 module xmlrpcc.decoder; 2 3 import std.stdio : writeln; 4 import std.xml : Element, Document; 5 import std.conv : to; 6 import std.exception : enforce; 7 import std.variant : Variant; 8 import std.typecons : Rebindable; 9 import std.string : format, strip; 10 import std.array : replace; 11 import std.datetime : DateTime; 12 import std.base64 : Base64; 13 14 import xmlrpcc.error; 15 import xmlrpcc.data; 16 17 @trusted: 18 19 class DecoderException : XmlRpcException { 20 private this(string msg, string file = __FILE__, size_t line = __LINE__) { 21 super(msg, file, line, next); 22 } 23 } 24 25 package: 26 27 MethodCallData decodeCall(string text) { 28 const call = new Document(text); 29 MethodCallData result; 30 result.name = find(call, "methodName").text(); 31 result.params = decodeParams(tryFind(call, "params")); 32 return result; 33 } 34 35 MethodResponseData decodeResponse(string text) { 36 MethodResponseData result; 37 const resp = new Document(text); 38 39 const fault = tryFind(resp, "fault"); 40 if (fault) { 41 result.fault = true; 42 result.params = decodeFault(fault); 43 } else { 44 result.fault = false; 45 result.params = decodeParams(tryFind(resp, "params")); 46 } 47 return result; 48 } 49 50 private: 51 52 auto decodeParams(in Element params) { 53 Variant[] result; 54 if (params is null) // Parameters are optional 55 return result; 56 57 foreach (param; params.elements) { 58 if (param.tag.name != "param") { 59 debug (xmlrpc) 60 writeln("Decoder: Invalid parameter tag: ", param.tag.name); 61 continue; 62 } 63 debug (xmlrpc) { 64 if (param.elements.length != 1) 65 writeln("Decoder: Parameter must contain exactly one sub-element, not ", param.elements.length); 66 } 67 result ~= decodeParam(param); 68 } 69 return result; 70 } 71 72 auto decodeFault(in Element fault) { 73 Variant[] result; 74 auto value = tryFind(fault, "value"); 75 if (value) 76 result ~= decodeValue(value); 77 return result; 78 } 79 80 auto decodeParam(in Element param) { 81 return decodeValue(find(param, "value")); 82 } 83 84 Variant decodeValue(in Element value) { 85 Rebindable!(const Element) storage; 86 string type; 87 if (value.elements.length == 0) // Default is string 88 { 89 storage = value; 90 type = "string"; 91 } else { 92 enforce(value.elements.length == 1, new DecoderException("Value tag must have at most one sub-element")); 93 storage = value.elements[0]; 94 type = storage.tag.name; 95 } 96 97 if (type == "struct") 98 return decodeStructValue(storage); 99 100 if (type == "array") 101 return decodeArrayValue(storage); 102 103 return decodePrimitiveValue(storage, type); 104 } 105 106 Variant decodeStructValue(in Element storage) { 107 Variant[string] result; 108 foreach (member; storage.elements) { 109 if (member.tag.name != "member") { 110 debug (xmlrpc) 111 writeln("Decoder: Invalid struct subelement: ", member.tag.name); 112 continue; 113 } 114 const nameNode = find(member, "name"); 115 const valueNode = find(member, "value"); 116 const key = nameNode.text(); 117 Variant value = decodeValue(valueNode); 118 result[key] = value; 119 } 120 return Variant(result); 121 } 122 123 Variant decodeArrayValue(in Element storage) { 124 Variant[] result; 125 const data = find(storage, "data"); 126 foreach (value; data.elements) { 127 if (value.tag.name != "value") { 128 debug (xmlrpc) 129 writeln("Decoder: Invalid array member tag: ", value.tag.name); 130 continue; 131 } 132 result ~= decodeValue(value); 133 } 134 return Variant(result); 135 } 136 137 Variant decodePrimitiveValue(in Element storage, string type) { 138 string data = storage.text(); 139 switch (type) { 140 case "int": 141 case "i4": 142 return Variant(to!int(data)); 143 144 case "i8": 145 return Variant(to!long(data)); 146 147 case "string": 148 return Variant(data); 149 150 case "double": 151 return Variant(to!double(data)); 152 153 case "boolean": 154 debug (xmlrpc) { 155 if (data != "0" && data != "1") 156 writeln("Decoder: Invalid literal for boolean: " ~ data); 157 } 158 return Variant(to!int(data) != 0); // Sloppy conversion 159 160 case "dateTime.iso8601": 161 data = replace(data, ":", ""); // Conversion to the basic format 162 data = replace(data, "-", ""); 163 return Variant(DateTime.fromISOString(data)); 164 165 case "base64": 166 return Variant(Base64.decode(data)); 167 168 case "nil": 169 return Variant(null); 170 171 default: 172 throw new DecoderException("Unknown XMLRPC type " ~ type); 173 } 174 } 175 176 const(Element) tryFind(in Element e, string tag) { 177 foreach (sub; e.elements) 178 if (sub.tag.name == tag) 179 return sub; 180 return null; 181 } 182 183 const(Element) find(in Element e, string tag) { 184 const res = tryFind(e, tag); 185 enforce(res !is null, new DecoderException(format("Element <%s> does not contain <%s>", e.tag.name, tag))); 186 return res; 187 } 188 189 version (xmlrpc_unittest) unittest { 190 /* 191 * Call 192 */ 193 auto s = `<methodCall> 194 <methodName>examples.getStateName</methodName> 195 <params> 196 <param> 197 <value><i4>40</i4></value> 198 </param> 199 <param> 200 <value>South Dakota</value> 201 </param> 202 </params> 203 </methodCall>`; 204 auto call = decodeCall(s); 205 assert(call.name == "examples.getStateName"); 206 assert(call.params[0] == 40); 207 assert(call.params[1] == "South Dakota"); 208 209 /* 210 * OK response 211 */ 212 s = `<methodResponse> 213 <params> 214 <param> 215 <value> 216 <array> 217 <data> 218 <value><i4>12</i4></value> 219 <value><string>Egypt</string></value> 220 <value>Egypt</value> 221 <value><boolean>0</boolean></value> 222 <value><i4>-31</i4></value> 223 </data> 224 </array> 225 </value> 226 </param> 227 </params> 228 </methodResponse>`; 229 auto resp = decodeResponse(s); 230 assert(!resp.fault); 231 assert(resp.params[0][0] == 12); 232 assert(resp.params[0][1] == "Egypt"); 233 assert(resp.params[0][2] == "Egypt"); 234 assert(resp.params[0][3] == false); 235 assert(resp.params[0][4] == -31); 236 237 /* 238 * OK response 239 */ 240 s = `<methodResponse> 241 <params> 242 <param> 243 <value>40</value> 244 </param> 245 <param> 246 <value><string>South Dakota</string></value> 247 </param> 248 </params> 249 </methodResponse>`; 250 resp = decodeResponse(s); 251 assert(!resp.fault); 252 assert(resp.params[0] == "40"); 253 assert(resp.params[1] == "South Dakota"); 254 255 /* 256 * Fault response 257 */ 258 s = `<methodResponse> 259 <fault> 260 <value> 261 <struct> 262 <member> 263 <name>faultCode</name> 264 <value><int>4</int></value> 265 </member> 266 <member> 267 <name>faultString</name> 268 <value><string>Negative, Cassie. Computer control.</string></value> 269 </member> 270 </struct> 271 </value> 272 </fault> 273 </methodResponse>`; 274 resp = decodeResponse(s); 275 assert(resp.fault); 276 assert(resp.params[0]["faultCode"] == 4); 277 assert(resp.params[0]["faultString"] == "Negative, Cassie. Computer control."); 278 279 /* 280 * Call 281 */ 282 s = `<methodCall> 283 <methodName>theMethod</methodName> 284 <params> 285 <param> 286 <value> 287 <struct> 288 <member><name>arrays</name> 289 <value> 290 <array> 291 <data> 292 <value><array><data><value><int>10</int></value></data></array></value> 293 <value><array><data><value><int>15</int></value></data></array></value> 294 </data> 295 </array> 296 </value> 297 </member> 298 <member><name>question</name><value>Kaneda, what do you see? Kaneda!</value></member> 299 </struct> 300 </value> 301 </param> 302 <param><value><dateTime.iso8601>19980717T14:08:55</dateTime.iso8601></value></param> 303 </params> 304 </methodCall>`; 305 call = decodeCall(s); 306 assert(call.name == "theMethod"); 307 Variant arrays = call.params[0]["arrays"]; 308 assert(arrays[0][0] == 10); 309 assert(arrays[1][0] == 15); 310 assert(call.params[0]["question"] == "Kaneda, what do you see? Kaneda!"); 311 assert(call.params[1] == DateTime(1998, 7, 17, 14, 8, 55)); 312 }