comparison mde/file/deserialize.d @ 81:d8fccaa45d5f

Moved file IO code from mde/mergetag to mde/file[/mergetag] and changed how some errors are caught.
author Diggory Hardy <diggory.hardy@gmail.com>
date Fri, 29 Aug 2008 11:59:43 +0100
parents mde/mergetag/deserialize.d@61ea26abe4dd
children ac1e3fd07275
comparison
equal deleted inserted replaced
80:ea58f277f487 81:d8fccaa45d5f
1 /* LICENSE BLOCK
2 Part of mde: a Modular D game-oriented Engine
3 Copyright © 2007-2008 Diggory Hardy
4
5 This program is free software: you can redistribute it and/or modify it under the terms
6 of the GNU General Public License as published by the Free Software Foundation, either
7 version 2 of the License, or (at your option) any later version.
8
9 This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
10 without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
11 See the GNU General Public License for more details.
12
13 You should have received a copy of the GNU General Public License
14 along with this program. If not, see <http://www.gnu.org/licenses/>. */
15
16 /**************************************************************************************************
17 * Generic deserialization templated function.
18 *
19 * Supports:
20 * Associative arrays, arrays (inc. strings), structs, char types, bool, int types, float types.
21 *
22 * There are also some public utility functions with their own documentation.
23 *
24 * Examples:
25 * ------------------------------------------------------------------------------------------------
26 * // Basic examples:
27 * ulong a = deserialize!(ulong) ("20350");
28 * float d = deserialize!(float) (" 1.2e-9 ");
29 * int[] b = deserialize!(int[]) ("[0,1,2,3]");
30 *
31 * // String and char[] syntax:
32 * char[] c = deserialize!(char[]) ("\"A string\"");
33 * char[] e = deserialize!(char[]) ("['a','n','o','t','h','e','r', ' ' ,'s','t','r','i','n','g']");
34 *
35 * // These be used interchangably; here's a more complex example of an associative array:
36 * bool[char[]] f = deserialize!(bool[char[]]) ("[ \"one\":true, ['t','w','o']:false, \"three\":1, \"four\":000 ]");
37 *
38 * // There is also a special notation for ubyte[] types:
39 * // The digits following 0x must be in pairs and each specify one ubyte.
40 * assert ( deserialize!(ubyte[]) (`0x01F2AC`) == deserialize!(ubyte[]) (`[01 ,0xF2, 0xAC]`) );
41 *
42 * // There's no limit to the complexity!
43 * char[char[][][][char]][bool] z = ...; // don't expect me to write this!
44 * ------------------------------------------------------------------------------------------------
45 *
46 * Throws:
47 * May throw a ParseException or a UnicodeException (which both extend TextException).
48 *
49 * TODO: Optimize memory allocation (if possible?). Test best sizes for initial allocations
50 * instead of merely guessing?
51 *************************************************************************************************/
52 //NOTE: in case of multiple formats, make this a dummy module importing both serialize modules,
53 // or put all the code here.
54 module mde.file.deserialize;
55
56 // tango imports
57 import tango.core.Exception : TextException, UnicodeException;
58 import cInt = tango.text.convert.Integer;
59 import cFloat = tango.text.convert.Float;
60 import Utf = tango.text.convert.Utf;
61 import Util = tango.text.Util;
62
63 /**
64 * Base class for deserialize exceptions.
65 */
66 class ParseException : TextException
67 {
68 this( char[] msg )
69 {
70 super( msg );
71 }
72 }
73
74 alias deserialize parseTo; // support the old name
75
76 //BEGIN deserialize templates
77
78 // Associative arrays
79
80 T[S] deserialize(T : T[S], S) (char[] src) {
81 src = Util.trim(src);
82 if (src.length < 2 || src[0] != '[' || src[$-1] != ']')
83 throw new ParseException ("Invalid associative array: not [ ... ]"); // bad braces.
84
85 T[S] ret;
86 foreach (char[] pair; split (src[1..$-1])) {
87 uint i = 0;
88 while (i < pair.length) { // advance to the ':'
89 char c = pair[i];
90 if (c == ':') break;
91 if (c == '\'' || c == '"') { // string or character
92 ++i;
93 while (i < pair.length && pair[i] != c) {
94 if (pair[i] == '\\')
95 ++i; // escape seq.
96 ++i;
97 }
98 // Could have an unterminated ' or " causing i >= pair.length, but:
99 // 1. Impossible: split would have thrown
100 // 2. In any case this would be caught below.
101 }
102 ++i;
103 }
104 if (i >= pair.length)
105 throw new ParseException ("Invalid associative array: encountered [ ... KEY] (missing :DATA)");
106 ret[deserialize!(S) (pair[0..i])] = deserialize!(T) (pair[i+1..$]);
107 }
108 return ret;
109 }
110
111
112 // Arrays
113
114 T[] deserialize(T : T[]) (char[] src) {
115 src = Util.trim(src);
116 if (src.length >= 2 && src[0] == '[' && src[$-1] == ']')
117 return toArray!(T[]) (src);
118 throw new ParseException ("Invalid array: not [ ... ]");
119 }
120
121 // String (array special case)
122 T deserialize(T : char[]) (char[] src) {
123 src = Util.trim(src);
124 if (src.length >= 2 && src[0] == '"' && src[$-1] == '"') {
125 src = src[1..$-1];
126 T ret;
127 ret.length = src.length; // maximum length; retract to actual length later
128 uint i = 0;
129 for (uint t = 0; t < src.length;) {
130 // process a block of non-escaped characters
131 uint s = t;
132 while (t < src.length && src[t] != '\\') ++t; // non-escaped characters
133 uint j = i + t - s;
134 ret[i..j] = src[s..t]; // copy a block
135 i = j;
136
137 // process a block of escaped characters
138 while (t < src.length && src[t] == '\\') {
139 t++;
140 if (t == src.length)
141 throw new ParseException ("Invalid string: ends \\\" !"); // next char is "
142 ret[i++] = unEscapeChar (src[t++]); // throws if it's invalid
143 }
144 }
145 return ret[0..i];
146 }
147 else if (src.length >= 2 && src[0] == '[' && src[$-1] == ']')
148 return toArray!(T) (src);
149 throw new ParseException ("Invalid string: not quoted (\"*\") or char array (['a',...,'c'])");
150 }
151 // Unicode conversions for strings:
152 T deserialize(T : wchar[]) (char[] src) {
153 // May throw a UnicodeException; don't bother catching and rethrowing:
154 return Utf.toString16 (deserialize!(char[]) (src));
155 }
156 T deserialize(T : dchar[]) (char[] src) {
157 // May throw a UnicodeException; don't bother catching and rethrowing:
158 return Utf.toString32 (deserialize!(char[]) (src));
159 }
160
161 // Binary (array special case)
162 T deserialize(T : ubyte[]) (char[] src) {
163 src = Util.trim(src);
164 // Standard case:
165 if (src.length >= 2 && src[0] == '[' && src[$-1] == ']') return toArray!(T) (src);
166 // Special case: sequence of hex digits, each pair of which is a ubyte
167 if (src.length >= 2 && src[0..2] == "0x") {
168 src = src[2..$]; // strip down to actual digits
169
170 // Must be in pairs:
171 if (src.length % 2 == 1)
172 throw new ParseException ("Invalid binary: odd number of chars");
173
174 T ret;
175 ret.length = src.length / 2; // exact
176
177 for (uint i, pos; pos + 1 < src.length; ++i) {
178 ubyte x = readHexChar(src, pos) << 4;
179 x |= readHexChar(src, pos);
180 ret[i] = x;
181 }
182 return ret;
183 }
184 else throw new ParseException ("Invalid ubyte[]: not an array and doesn't start 0x");
185 }
186
187
188 // Basic types
189
190 // Char
191 // Assumes value is <= 127 (for valid UTF-8), since input would be invalid UTF-8 if not anyway.
192 // (And we're not really interested in checking for valid unicode; char[] conversions don't either.)
193 T deserialize(T : char) (char[] src) {
194 src = Util.trim(src);
195 if (src.length < 3 || src[0] != '\'' || src[$-1] != '\'')
196 throw new ParseException ("Invalid char: not 'x' or '\\x'");
197 if (src[1] != '\\') {
198 if (src.length == 3)
199 return src[1]; // Either non escaped
200 throw new ParseException ("Invalid char: too long (or non-ASCII)");
201 } else if (src.length == 4)
202 return unEscapeChar (src[2]); // Or escaped
203
204 throw new ParseException ("Invalid char: '\\'");
205 }
206 // Basic unicode convertions for wide-chars.
207 // Assumes value is <= 127 as does deserialize!(char).
208 T deserialize(T : wchar) (char[] src) {
209 return cast(T) deserialize!(char) (src);
210 }
211 T deserialize(T : dchar) (char[] src) {
212 return cast(T) deserialize!(char) (src);
213 }
214
215 // Bool
216 T deserialize(T : bool) (char[] src) {
217 src = Util.trim(src);
218 if (src == "true")
219 return true;
220 if (src == "false")
221 return false;
222 uint pos;
223 while (src.length > pos && src[pos] == '0') ++pos; // skip leading zeros
224 if (src.length == pos && pos > 0)
225 return false;
226 if (src.length == pos + 1 && src[pos] == '1')
227 return true;
228 throw new ParseException ("Invalid bool: not true or false and doesn't evaluate to 0 or 1");
229 }
230
231 // Ints
232 T deserialize(T : byte) (char[] src) {
233 return toTInt!(T) (src);
234 }
235 T deserialize(T : short) (char[] src) {
236 return toTInt!(T) (src);
237 }
238 T deserialize(T : int) (char[] src) {
239 return toTInt!(T) (src);
240 }
241 T deserialize(T : long) (char[] src) {
242 return toTInt!(T) (src);
243 }
244 T deserialize(T : ubyte) (char[] src) {
245 return toTInt!(T) (src);
246 }
247 T deserialize(T : ushort) (char[] src) {
248 return toTInt!(T) (src);
249 }
250 T deserialize(T : uint) (char[] src) {
251 return toTInt!(T) (src);
252 }
253 T deserialize(T : ulong) (char[] src) {
254 return toTInt!(T) (src);
255 }
256 debug (UnitTest) unittest {
257 assert (deserialize!(byte) ("-5") == cast(byte) -5);
258 // annoyingly, octal syntax differs from D (blame tango):
259 assert (deserialize!(uint[]) ("[0b0100,0o724,0xFa59c,0xFFFFFFFF,0]") == [0b0100u,0724,0xFa59c,0xFFFFFFFF,0]);
260 }
261
262 // Floats
263 T deserialize(T : float) (char[] src) {
264 return toTFloat!(T) (src);
265 }
266 T deserialize(T : double) (char[] src) {
267 return toTFloat!(T) (src);
268 }
269 T deserialize(T : real) (char[] src) {
270 return toTFloat!(T) (src);
271 }
272
273
274 // Structs
275 T deserialize(T) (char[] src) {
276 static assert (is(T == struct), "Unsupported type: "~typeof(T));
277
278 src = Util.trim(src);
279 if (src.length < 2 || src[0] != '{' || src[$-1] != '}')
280 throw new ParseException ("Invalid struct: not { ... }");
281
282 // cannot access elements of T.tupleof with non-const key, so use a type which can be
283 // accessed with a non-const key to store slices:
284 char[][T.tupleof.length] temp;
285 foreach (char[] pair; split (src[1..$-1])) {
286 uint i = 0;
287 while (i < pair.length) { // advance to the ':'
288 char c = pair[i];
289 if (c == ':')
290 break;
291 // key must be an int so no need for string checks
292 ++i;
293 }
294 if (i >= pair.length)
295 throw new ParseException ("Invalid struct: encountered { ... KEY} (missing :DATA)");
296
297 size_t k = deserialize!(size_t) (pair[0..i]);
298 // Note: could check no entry was already stored in temp.
299 temp[k] = pair[i+1..$];
300 }
301 T ret;
302 setStruct (ret, temp);
303 return ret;
304 }
305 //END deserialize templates
306
307 //BEGIN Utility funcs
308 /** Splits a string into substrings separated by '$(B ,)' with support for characters and strings
309 * containing escape sequences and for embedded arrays ($(B [...])).
310 *
311 * Params:
312 * src A string to separate on commas. It shouldn't have enclosing brackets.
313 *
314 * Returns:
315 * An array of substrings within src, excluding commas. Whitespace is not stripped and
316 * empty strings may get returned.
317 *
318 * Remarks:
319 * This function is primarily intended for as a utility function for use by the templates
320 * parsing arrays and associative arrays, but it may be useful in other cases too. Hence the
321 * fact no brackets are stripped from src.
322 */
323 //FIXME foreach struct is more efficient
324 char[][] split (char[] src) {
325 src = Util.trim (src);
326 if (src == "")
327 return []; // empty array: no elements when no data
328
329 uint depth = 0; // surface depth (embedded arrays)
330 char[][] ret;
331 ret.length = src.length / 3; // unlikely to need a longer array
332 uint k = 0; // current split piece
333 uint i = 0, j = 0; // current read location, start of current piece
334
335 while (i < src.length) {
336 char c = src[i];
337 if (c == '\'' || c == '"') { // string or character
338 ++i;
339 while (i < src.length && src[i] != c) {
340 if (src[i] == '\\')
341 ++i; // escape seq.
342 ++i;
343 } // Doesn't throw if no terminal quote at end of src, but this should be caught later.
344 }
345 else if (c == '[') ++depth;
346 else if (c == ']') {
347 if (depth)
348 --depth;
349 else throw new ParseException ("Invalid array literal: closes before end of data item.");
350 }
351 else if (c == ',' && depth == 0) { // only if not an embedded array
352 if (ret.length <= k)
353 ret.length = ret.length * 2;
354 ret[k++] = src[j..i]; // add this piece and increment k
355 j = i + 1;
356 }
357 ++i;
358 }
359 if (i > src.length)
360 throw new ParseException ("Unterminated quote (\' or \")");
361
362 if (ret.length <= k)
363 ret.length = k + 1;
364 ret[k] = src[j..i]; // add final piece (i >= j)
365 return ret[0..k+1];
366 }
367
368 /* Templated read-int function to read (un)signed 1-4 byte integers.
369 *
370 * Actually a reimplementation of tango.text.convert.Integer toLong and parse functions.
371 */
372 private TInt toTInt(TInt) (char[] src) {
373 const char[] INT_OUT_OF_RANGE = "Integer out of range";
374 bool sign;
375 uint radix, ate, ate2;
376
377 // Trim off whitespace.
378 // NOTE: Cannot use tango.text.convert.Integer.trim to trim leading whitespace since it doesn't
379 // treat new-lines, etc. as whitespace which for our purposes is whitespace.
380 src = Util.trim (src);
381
382 ate = cInt.trim (src, sign, radix);
383 if (ate == src.length)
384 throw new ParseException ("Invalid integer: no digits");
385 ulong val = cInt.convert (src[ate..$], radix, &ate2);
386 ate += ate2;
387
388 if (ate < src.length)
389 throw new ParseException ("Invalid integer at marked character: \"" ~ src[0..ate] ~ "'" ~ src[ate] ~ "'" ~ src[ate+1..$] ~ "\"");
390
391 if (val > TInt.max)
392 throw new ParseException (INT_OUT_OF_RANGE);
393 if (sign) {
394 long sval = cast(long) -val;
395 if (sval > TInt.min)
396 return cast(TInt) sval;
397 else throw new ParseException (INT_OUT_OF_RANGE);
398 }
399 return cast(TInt) val;
400 }
401
402 /* Basically a reimplementation of tango.text.convert.Float.toFloat which checks for
403 * whitespace before throwing an exception for overlong input. */
404 private TFloat toTFloat(TFloat) (char[] src) {
405 // NOTE: As for toTInt(), this needs to strip leading as well as trailing whitespace.
406 src = Util.trim (src);
407 if (src == "")
408 throw new ParseException ("Invalid float: no digits");
409 uint ate;
410
411 TFloat x = cFloat.parse (src, &ate);
412 return x;
413 }
414
415 /* Throws an exception on invalid escape sequences. Supported escape sequences are the following
416 * subset of those supported by D: \" \' \\ \a \b \f \n \r \t \v
417 */
418 private char unEscapeChar (char c)
419 {
420 // This code was generated:
421 if (c <= 'b') {
422 if (c <= '\'') {
423 if (c == '\"') {
424 return '\"';
425 } else if (c == '\'') {
426 return '\'';
427 }
428 } else {
429 if (c == '\\') {
430 return '\\';
431 } else if (c == 'a') {
432 return '\a';
433 } else if (c == 'b') {
434 return '\b';
435 }
436 }
437 } else {
438 if (c <= 'n') {
439 if (c == 'f') {
440 return '\f';
441 } else if (c == 'n') {
442 return '\n';
443 }
444 } else {
445 if (c == 'r') {
446 return '\r';
447 } else if (c == 't') {
448 return '\t';
449 } else if (c == 'v') {
450 return '\v';
451 }
452 }
453 }
454
455 // if we haven't returned:
456 throw new ParseException ("Bad escape sequence: \\"~c);
457 }
458
459 // Reads one hex char: [0-9A-Fa-f]. Otherwise throws an exception. Doesn't check src.length.
460 private ubyte readHexChar (char[] src, inout uint pos) {
461 ubyte x;
462 if (src[pos] >= '0' && src[pos] <= '9') x = src[pos] - '0';
463 else if (src[pos] >= 'A' && src[pos] <= 'F') x = src[pos] - 'A' + 10;
464 else if (src[pos] >= 'a' && src[pos] <= 'f') x = src[pos] - 'a' + 10;
465 else throw new ParseException ("Invalid hex digit.");
466 ++pos;
467 return x;
468 }
469
470 // Generic array reader
471 // Assumes input is of form "[xxxxx]" (i.e. first and last chars are '[', ']' and length >= 2).
472 private T[] toArray(T : T[]) (char[] src) {
473 T[] ret = new T[16]; // avoid unnecessary allocations
474 uint i = 0;
475 foreach (char[] element; split(src[1..$-1])) {
476 if (i == ret.length) ret.length = ret.length * 2;
477 ret[i] = deserialize!(T) (element);
478 ++i;
479 }
480 return ret[0..i];
481 }
482
483 /** Set a struct's elements from an array.
484 *
485 * For a more generic version, see http://www.dsource.org/projects/tutorials/wiki/StructTupleof
486 */
487 // NOTE: Efficiency? Do recursive calls get inlined?
488 private void setStruct(S, size_t N, size_t i = 0) (ref S s, char[][N] src) {
489 static assert (is(S == struct), "Only to be used with structs.");
490 static assert (N == S.tupleof.length, "src.length != S.tupleof.length");
491 static if (i < N) {
492 if (src[i])
493 s.tupleof[i] = deserialize!(typeof(s.tupleof[i])) (src[i]);
494 setStruct!(S, N, i+1) (s, src);
495 }
496 }
497 //END Utility funcs
498
499 debug (UnitTest) {
500 import tango.util.log.Log : Log, Logger;
501
502 private Logger logger;
503 static this() {
504 logger = Log.getLogger ("text.deserialize");
505 }
506 unittest {
507 // Utility
508 bool throws (void delegate() dg) {
509 bool r = false;
510 try {
511 dg();
512 } catch (Exception e) {
513 r = true;
514 logger.info ("Exception caught: "~e.msg);
515 }
516 return r;
517 }
518 assert (!throws ({ int i = 5; }));
519 assert (throws ({ throw new Exception ("Test - this exception should be caught"); }));
520
521
522 // Associative arrays
523 char[][char] X = deserialize!(char[][char]) (`['a':"animal\n", 'b':['b','u','s','\n']]`);
524 char[][char] Y = ['a':cast(char[])"animal\n", 'b':['b','u','s','\n']];
525
526 //FIXME: when the compiler's fixed: http://d.puremagic.com/issues/show_bug.cgi?id=1671
527 // just assert (X == Y)
528 assert (X.length == Y.length);
529 assert (X.keys == Y.keys);
530 assert (X.values == Y.values);
531 //X.rehash; Y.rehash; // doesn't make a difference
532 //assert (X == Y); // fails (compiler bug)
533
534 assert (throws ({ deserialize!(int[int]) (`[1:1`); })); // bad brackets
535 assert (throws ({ deserialize!(int[char[]]) (`["ab\":1]`); })); // unterminated quote
536 assert (throws ({ deserialize!(int[char[]]) (`["abc,\a\b\c":1]`); })); // bad escape seq.
537 assert (throws ({ deserialize!(int[char[]]) (`["abc"]`); })); // no data
538
539
540 // Arrays
541 assert (deserialize!(double[]) (`[1.0,1.0e-10]`) == [1.0, 1.0e-10]);// generic array stuff
542 assert (deserialize!(double[]) (`[ ]`) == cast(double[]) []); // empty array
543 assert (deserialize!(int[][]) (`[[1],[2,3],[]]`) == [[1],[2,3],[]]);// sub-array
544 assert (throws ({ deserialize!(int[]) (`[1,2`); })); // bad brackets
545 assert (throws ({ deserialize!(int[][]) (`[[1]]]`); })); // bad brackets
546
547 // char[] and char conversions, with commas, escape sequences and multichar UTF8 characters:
548 assert (deserialize!(char[][]) (`[ ".\"", [',','\''] ,"!\b€" ]`) == [ ".\"".dup, [',','\''] ,"!\b€" ]);
549 assert (throws ({ deserialize!(char[]) ("\"\\\""); }));
550 assert (throws ({ deserialize!(char[]) (`['a'`); })); // bad brackets
551
552 // wchar[] and dchar[] conversions:
553 // The characters were pretty-much pulled at random from unicode tables.
554 // The last few cause some wierd (display only) effects in my editor.
555 assert (deserialize!(wchar[]) ("\"Test string: ¶α؟अกሀ搀\"") == "Test string: ¶α؟अกሀ搀"w);
556 assert (deserialize!(dchar[]) ("\"Test string: ¶α؟अกሀ搀\"") == "Test string: ¶α؟अกሀ搀"d);
557
558 assert (deserialize!(ubyte[]) (`0x01F2aC`) == cast(ubyte[]) [0x01, 0xF2, 0xAC]); // ubyte[] special notation
559 assert (deserialize!(ubyte[]) (`[01 ,0xF2, 0xAC]`) == cast(ubyte[]) [0x01, 0xF2, 0xAC]); // ubyte[] std notation
560 assert (throws ({ deserialize!(ubyte[]) (`0x123`); })); // digits not in pairs
561 assert (throws ({ deserialize!(ubyte[]) (`[2,5`); })); // not [...] or 0x..
562 assert (throws ({ deserialize!(ubyte[]) (`0x123j`); }));
563
564
565 // char types
566 assert (deserialize!(char) ("'\\\''") == '\'');
567 assert (deserialize!(wchar) ("'X'") == 'X');
568 assert (deserialize!(dchar) ("'X'") == 'X');
569 assert (throws ({ deserialize!(char) ("'\\'"); }));
570 assert (throws ({ deserialize!(char) ("'£'"); })); // non-ascii
571 assert (throws ({ deserialize!(char) ("''"); }));
572 assert (throws ({ deserialize!(char) ("'ab'"); }));
573 assert (throws ({ deserialize!(wchar) ("''"); }));
574
575
576 // bool
577 assert (deserialize!(bool[]) (`[true,false,01,00]`) == cast(bool[]) [1,0,1,0]);
578 assert (throws ({ deserialize!(bool) ("011"); }));
579
580
581 // ints
582 assert (deserialize!(byte) ("-5") == cast(byte) -5);
583 assert (deserialize!(int) ("-0x7FFFFFFF") == cast(int) -0x7FFF_FFFF);
584 // annoyingly, octal syntax differs from D (blame tango):
585 assert (deserialize!(uint[]) ("[0b0100,0o724,0xFa59c,0xFFFFFFFF,0]") == [0b0100u,0724,0xFa59c,0xFFFFFFFF,0]);
586 assert (throws ({ deserialize!(int) (""); }));
587 assert (throws ({ deserialize!(int) ("0x8FFFFFFF"); }));
588 assert (throws ({ deserialize!(uint) ("-1"); }));
589 assert (throws ({ deserialize!(uint) ("1a"); }));
590
591
592 // floats
593 assert (deserialize!(float) ("0.0") == 0.0f);
594 assert (deserialize!(double) ("-1e25") == -1e25);
595 assert (deserialize!(real) ("5.24e-269") == cast(real) 5.24e-269);
596 assert (throws ({ deserialize!(float) (""); }));
597
598
599 // structs
600 struct A { int x = 5; char y; }
601 struct B { A a; float b; }
602 A a; a.y = 'y';
603 assert (deserialize!(A) ("{ 1 : 'y' }") == a);
604 B b; b.a = a; b.b = 1.0f;
605 assert (deserialize!(B) (" {1:1.0,0: { 1 : 'y' } } ") == b);
606 assert (throws ({ deserialize!(A) (" 1:'x'}"); })); // bad braces
607 assert (throws ({ deserialize!(A) ("{ 1 }"); })); // no :DATA
608
609
610 // unEscapeChar
611 assert (deserialize!(char[]) ("\"\\a\\b\\t\\n\\v\\f\\r\\\"\\\'\\\\\"") == "\a\b\t\n\v\f\r\"\'\\");
612
613 logger.info ("Unittest complete.");
614 }
615 }