src/tnetstring.nim
changeset 9 c109bf5f2aa4
parent 7 370805b49c92
child 12 959dc81335c9
equal deleted inserted replaced
8:67c1c0c716e8 9:c109bf5f2aa4
       
     1 #
       
     2 # Copyright (c) 2015-2018, Mahlon E. Smith <mahlon@martini.nu>
       
     3 # All rights reserved.
       
     4 # Redistribution and use in source and binary forms, with or without
       
     5 # modification, are permitted provided that the following conditions are met:
       
     6 #
       
     7 #     * Redistributions of source code must retain the above copyright
       
     8 #       notice, this list of conditions and the following disclaimer.
       
     9 #
       
    10 #     * Redistributions in binary form must reproduce the above copyright
       
    11 #       notice, this list of conditions and the following disclaimer in the
       
    12 #       documentation and/or other materials provided with the distribution.
       
    13 #
       
    14 #     * Neither the name of Mahlon E. Smith nor the names of his
       
    15 #       contributors may be used to endorse or promote products derived
       
    16 #       from this software without specific prior written permission.
       
    17 #
       
    18 # THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY
       
    19 # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
       
    20 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
       
    21 # DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY
       
    22 # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
       
    23 # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
       
    24 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
       
    25 # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
       
    26 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
       
    27 # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
       
    28 
       
    29 ## This module implements a simple TNetstring parser and serializer.
       
    30 ## TNetString stands for "tagged netstring" and is a modification of Dan
       
    31 ## Bernstein's netstrings specification.  TNetstrings allow for the same data
       
    32 ## structures as JSON but in a format that is resistant to buffer overflows
       
    33 ## and backward compatible with original netstrings.  They make no assumptions
       
    34 ## about string contents, allowing for easy transmission of binary data mixed
       
    35 ## with strongly typed values.
       
    36 
       
    37 ## See http://cr.yp.to/proto/netstrings.txt and http://tnetstrings.org/ for additional information.
       
    38 ##
       
    39 ## This module borrows heavily (in both usage and code) from the nim JSON stdlib
       
    40 ## (json.nim) -- (c) Copyright 2015 Andreas Rumpf, Dominik Picheta.
       
    41 ## 
       
    42 ## Usage example:
       
    43 ##
       
    44 ## .. code-block:: nim
       
    45 ##
       
    46 ##   let
       
    47 ##       tnetstr = "52:4:test,3:1.3^4:key2,4:true!6:things,12:1:1#1:2#1:3#]}"
       
    48 ##       tnetobj = parse_tnetstring( tnetstr )
       
    49 ##
       
    50 ##   # tnetobj is now equivalent to the structure:
       
    51 ##   # @[(key: test, val: 1.3), (key: key2, val: true), (key: things, val: @[1, 2, 3])]
       
    52 ##
       
    53 ##   assert( tnetobj.kind == TNetstringObject )
       
    54 ##   echo tnetobj[ "test" ]
       
    55 ##   echo tnetobj[ "key2" ]
       
    56 ##   for item in tnetobj[ "things" ]:
       
    57 ##       echo item
       
    58 ##   
       
    59 ## Results in:
       
    60 ##
       
    61 ## .. code-block:: nim
       
    62 ##
       
    63 ##   1.3
       
    64 ##   true
       
    65 ##   1
       
    66 ##   2
       
    67 ##   3
       
    68 ##
       
    69 ## This module can also be used to reasonably create a serialized
       
    70 ## TNetstring, suitable for network transmission:
       
    71 ##
       
    72 ## .. code-block:: nim
       
    73 ##    
       
    74 ##    let
       
    75 ##        number  = 1000
       
    76 ##        list    = @[ "thing1", "thing2" ]
       
    77 ##        tnettop = newTNetstringArray() # top-level array
       
    78 ##        tnetsub = newTNetstringArray() # sub array
       
    79 ##    
       
    80 ##    tnettop.add( newTNetstringInt(number) )
       
    81 ##    for item in list:
       
    82 ##        tnetsub.add( newTNetstringString(item) )
       
    83 ##    tnettop.add( tnetsub )
       
    84 ##    
       
    85 ##    # Equivalent to: @[1000, @[thing1, thing2]]
       
    86 ##    echo dump_tnetstring( tnettop )
       
    87 ##
       
    88 ## Results in:
       
    89 ##
       
    90 ## .. code-block:: nim
       
    91 ##    
       
    92 ##    29:4:1000#18:6:thing1,6:thing2,]]
       
    93 ##
       
    94 
       
    95 import
       
    96     hashes,
       
    97     parseutils,
       
    98     strutils
       
    99 
       
   100 const version = "0.1.2"
       
   101 
       
   102 type 
       
   103   TNetstringKind* = enum     ## enumeration of all valid types
       
   104     TNetstringString,        ## a string literal
       
   105     TNetstringInt,           ## an integer literal
       
   106     TNetstringFloat,         ## a float literal
       
   107     TNetstringBool,          ## a ``true`` or ``false`` value
       
   108     TNetstringNull,          ## the value ``null``
       
   109     TNetstringObject,        ## an object: the ``}`` token
       
   110     TNetstringArray          ## an array: the ``]`` token
       
   111 
       
   112   TNetstringNode* = ref TNetstringNodeObj
       
   113   TNetstringNodeObj* {.acyclic.} = object
       
   114       extra*: string
       
   115       case kind*: TNetstringKind
       
   116       of TNetstringString:
       
   117           str*: string
       
   118       of TNetstringInt:
       
   119           num*: BiggestInt
       
   120       of TNetstringFloat:
       
   121           fnum*: float
       
   122       of TNetstringBool:
       
   123           bval*: bool
       
   124       of TNetstringNull:
       
   125           nil
       
   126       of TNetstringObject:
       
   127           fields*: seq[ tuple[key: string, val: TNetstringNode] ]
       
   128       of TNetstringArray:
       
   129           elems*: seq[ TNetstringNode ]
       
   130 
       
   131   TNetstringParseError* = object of ValueError ## Raised for a TNetstring error
       
   132 
       
   133 
       
   134 proc raiseParseErr*( t: TNetstringNode, msg: string ) {.noinline, noreturn.} =
       
   135   ## Raises a `TNetstringParseError` exception.
       
   136   raise newException( TNetstringParseError, msg )
       
   137 
       
   138 
       
   139 proc newTNetstringString*( s: string ): TNetstringNode =
       
   140     ## Create a new String typed TNetstringNode.
       
   141     new( result )
       
   142     result.kind = TNetstringString
       
   143     result.str = s
       
   144 
       
   145 
       
   146 proc newTNetstringInt*( i: BiggestInt ): TNetstringNode =
       
   147     ## Create a new Integer typed TNetstringNode.
       
   148     new( result )
       
   149     result.kind = TNetstringInt
       
   150     result.num = i
       
   151 
       
   152 
       
   153 proc newTNetstringFloat*( f: float ): TNetstringNode =
       
   154     ## Create a new Float typed TNetstringNode.
       
   155     new( result )
       
   156     result.kind = TNetstringFloat
       
   157     result.fnum = f
       
   158 
       
   159 
       
   160 proc newTNetstringBool*( b: bool ): TNetstringNode =
       
   161     ## Create a new Boolean typed TNetstringNode.
       
   162     new( result )
       
   163     result.kind = TNetstringBool
       
   164     result.bval = b
       
   165 
       
   166 
       
   167 proc newTNetstringNull*(): TNetstringNode =
       
   168     ## Create a new nil typed TNetstringNode.
       
   169     new( result )
       
   170     result.kind = TNetstringNull
       
   171 
       
   172 
       
   173 proc newTNetstringObject*(): TNetstringNode =
       
   174     ## Create a new Object typed TNetstringNode.
       
   175     new( result )
       
   176     result.kind = TNetstringObject
       
   177     result.fields = @[]
       
   178 
       
   179 
       
   180 proc newTNetstringArray*(): TNetstringNode =
       
   181     ## Create a new Array typed TNetstringNode.
       
   182     new( result )
       
   183     result.kind = TNetstringArray
       
   184     result.elems = @[]
       
   185 
       
   186 
       
   187 proc getStr*( node: TNetstringNode, default: string = "" ): string =
       
   188     ## Retrieves the string value of a `TNetstringString TNetstringNodee`.
       
   189     ## Returns ``default`` if ``node`` is not a ``TNetstringString``.
       
   190     if node.kind != TNetstringString: return default
       
   191     return node.str
       
   192 
       
   193 
       
   194 proc getInt*( node: TNetstringNode, default: BiggestInt = 0 ): BiggestInt =
       
   195     ## Retrieves the int value of a `TNetstringInt TNetstringNode`.
       
   196     ## Returns ``default`` if ``node`` is not a ``TNetstringInt``.
       
   197     if node.kind != TNetstringInt: return default
       
   198     return node.num
       
   199 
       
   200 
       
   201 proc getFloat*( node: TNetstringNode, default: float = 0.0 ): float =
       
   202     ## Retrieves the float value of a `TNetstringFloat TNetstringNode`.
       
   203     ## Returns ``default`` if ``node`` is not a ``TNetstringFloat``.
       
   204     if node.kind != TNetstringFloat: return default
       
   205     return node.fnum
       
   206 
       
   207 
       
   208 proc getBool*( node: TNetstringNode, default: bool = false ): bool =
       
   209     ## Retrieves the bool value of a `TNetstringBool TNetstringNode`.
       
   210     ## Returns ``default`` if ``node`` is not a ``TNetstringBool``.
       
   211     if node.kind != TNetstringBool: return default
       
   212     return node.bval
       
   213 
       
   214 
       
   215 proc getFields*( node: TNetstringNode,
       
   216     default: seq[tuple[key: string, val: TNetstringNode]] = @[] ):
       
   217         seq[tuple[key: string, val: TNetstringNode]] =
       
   218     ## Retrieves the key, value pairs of a `TNetstringObject TNetstringNode`.
       
   219     ## Returns ``default`` if ``node`` is not a ``TNetstringObject``.
       
   220     if node.kind != TNetstringObject: return default
       
   221     return node.fields
       
   222 
       
   223 
       
   224 proc getElems*( node: TNetstringNode, default: seq[TNetstringNode] = @[] ): seq[TNetstringNode] =
       
   225     ## Retrieves the values of a `TNetstringArray TNetstringNode`.
       
   226     ## Returns ``default`` if ``node`` is not a ``TNetstringArray``.
       
   227     if node.kind != TNetstringArray: return default
       
   228     return node.elems
       
   229 
       
   230 
       
   231 proc parse_tnetstring*( data: string ): TNetstringNode =
       
   232     ## Given an encoded tnetstring, parse and return a TNetstringNode.
       
   233     var
       
   234         length:  int
       
   235         kind:    char
       
   236         payload: string
       
   237         extra:   string
       
   238 
       
   239     let sep_pos = data.skipUntil( ':' )
       
   240     if sep_pos == data.len: raiseParseErr( result, "Invalid data: No separator token found." )
       
   241 
       
   242     try:
       
   243         length       = data[ 0 .. sep_pos - 1 ].parseInt
       
   244         kind         = data[ sep_pos + length + 1 ]
       
   245         payload      = data[ sep_pos + 1 .. sep_pos + length ]
       
   246         extra        = data[ sep_pos + length + 2 .. ^1 ]
       
   247 
       
   248     except ValueError, IndexError:
       
   249         var msg = getCurrentExceptionMsg()
       
   250         raiseParseErr( result, msg )
       
   251 
       
   252     case kind:
       
   253         of ',':
       
   254             result = newTNetstringString( payload )
       
   255 
       
   256         of '#':
       
   257             try:
       
   258                 result = newTNetstringInt( payload.parseBiggestInt )
       
   259             except ValueError:
       
   260                 var msg = getCurrentExceptionMsg()
       
   261                 raiseParseErr( result, msg )
       
   262 
       
   263         of '^':
       
   264             try:
       
   265                 result = newTNetstringFloat( payload.parseFloat )
       
   266             except ValueError:
       
   267                 var msg = getCurrentExceptionMsg()
       
   268                 raiseParseErr( result, msg )
       
   269 
       
   270         of '!':
       
   271             result = newTNetstringBool( payload == "true" )
       
   272 
       
   273         of '~':
       
   274             if length != 0: raiseParseErr( result, "Invalid data: Payload must be 0 length for null." )
       
   275             result = newTNetstringNull()
       
   276             
       
   277         of ']':
       
   278             result = newTNetstringArray()
       
   279 
       
   280             var subnode = parse_tnetstring( payload )
       
   281             result.elems.add( subnode )
       
   282 
       
   283             while subnode.extra != "":
       
   284                 subnode = parse_tnetstring( subnode.extra )
       
   285                 result.elems.add( subnode )
       
   286 
       
   287         of '}':
       
   288             result = newTNetstringObject()
       
   289             var key = parse_tnetstring( payload )
       
   290 
       
   291             if ( key.extra == "" ): raiseParseErr( result, "Invalid data: Unbalanced tuple." )
       
   292             if ( key.kind != TNetstringString ): raiseParseErr( result, "Invalid data: Object keys must be strings." )
       
   293 
       
   294             var value = parse_tnetstring( key.extra )
       
   295             result.fields.add( (key: key.str, val: value) )
       
   296 
       
   297             while value.extra != "":
       
   298                 var subkey = parse_tnetstring( value.extra )
       
   299                 if ( subkey.extra == "" ): raiseParseErr( result, "Invalid data: Unbalanced tuple." )
       
   300                 if ( subkey.kind != TNetstringString ): raiseParseErr( result, "Invalid data: Object keys must be strings." )
       
   301 
       
   302                 value = parse_tnetstring( subkey.extra )
       
   303                 result.fields.add( (key: subkey.str, val: value) )
       
   304 
       
   305         else:
       
   306             raiseParseErr( result, "Invalid data: Unknown tnetstring type '$1'." % $kind )
       
   307 
       
   308     result.extra = extra
       
   309 
       
   310 
       
   311 iterator items*( node: TNetstringNode ): TNetstringNode =
       
   312     ## Iterator for the items of `node`. `node` has to be a TNetstringArray.
       
   313     assert node.kind == TNetstringArray
       
   314     for i in items( node.elems ):
       
   315         yield i
       
   316 
       
   317 
       
   318 iterator mitems*( node: var TNetstringNode ): var TNetstringNode =
       
   319     ## Iterator for the items of `node`. `node` has to be a TNetstringArray. Items can be
       
   320     ## modified.
       
   321     assert node.kind == TNetstringArray
       
   322     for i in mitems( node.elems ):
       
   323         yield i
       
   324 
       
   325 
       
   326 iterator pairs*( node: TNetstringNode ): tuple[ key: string, val: TNetstringNode ] =
       
   327     ## Iterator for the child elements of `node`. `node` has to be a TNetstringObject.
       
   328     assert node.kind == TNetstringObject
       
   329     for key, val in items( node.fields ):
       
   330         yield ( key, val )
       
   331 
       
   332 
       
   333 iterator mpairs*( node: var TNetstringNode ): var tuple[ key: string, val: TNetstringNode ] =
       
   334     ## Iterator for the child elements of `node`. `node` has to be a TNetstringObject.
       
   335     ## Items can be modified.
       
   336     assert node.kind == TNetstringObject
       
   337     for keyVal in mitems( node.fields ):
       
   338         yield keyVal
       
   339 
       
   340 
       
   341 proc `$`*( node: TNetstringNode ): string =
       
   342     ## Delegate stringification of `TNetstringNode` to its underlying object.
       
   343     return case node.kind:
       
   344     of TNetstringString:
       
   345         $node.str
       
   346     of TNetstringInt:
       
   347         $node.num
       
   348     of TNetstringFloat:
       
   349         $node.fnum
       
   350     of TNetstringBool:
       
   351         $node.bval
       
   352     of TNetstringNull:
       
   353         "(nil)"
       
   354     of TNetstringArray:
       
   355         $node.elems
       
   356     of TNetstringObject:
       
   357         $node.fields
       
   358 
       
   359 
       
   360 proc `==`* ( a, b: TNetstringNode ): bool =
       
   361     ## Check two TNetstring nodes for equality.
       
   362     if a.isNil:
       
   363         if b.isNil: return true
       
   364         return false
       
   365     elif b.isNil or a.kind != b.kind:
       
   366         return false
       
   367     else:
       
   368         return case a.kind
       
   369         of TNetstringString:
       
   370             a.str == b.str
       
   371         of TNetstringInt:
       
   372             a.num == b.num
       
   373         of TNetstringFloat:
       
   374             a.fnum == b.fnum
       
   375         of TNetstringBool:
       
   376             a.bval == b.bval
       
   377         of TNetstringNull:
       
   378             true
       
   379         of TNetstringArray:
       
   380             a.elems == b.elems
       
   381         of TNetstringObject:
       
   382             a.fields == b.fields
       
   383 
       
   384 
       
   385 proc copy*( node: TNetstringNode ): TNetstringNode =
       
   386     ## Perform a deep copy of TNetstringNode.
       
   387     new( result )
       
   388     result.kind  = node.kind
       
   389     result.extra = node.extra
       
   390 
       
   391     case node.kind
       
   392     of TNetstringString:
       
   393         result.str = node.str
       
   394     of TNetstringInt:
       
   395         result.num = node.num
       
   396     of TNetstringFloat:
       
   397         result.fnum = node.fnum
       
   398     of TNetstringBool:
       
   399         result.bval = node.bval
       
   400     of TNetstringNull:
       
   401         discard
       
   402     of TNetstringArray:
       
   403         result.elems = @[]
       
   404         for item in items( node ):
       
   405             result.elems.add( copy(item) )
       
   406     of TNetstringObject:
       
   407         result.fields = @[]
       
   408         for key, value in items( node.fields ):
       
   409             result.fields.add( (key, copy(value)) )
       
   410 
       
   411 
       
   412 proc delete*( node: TNetstringNode, key: string ) =
       
   413     ## Deletes ``node[key]`` preserving the order of the other (key, value)-pairs.
       
   414     assert( node.kind == TNetstringObject )
       
   415     for i in 0..node.fields.len - 1:
       
   416         if node.fields[i].key == key:
       
   417             node.fields.delete( i )
       
   418             return
       
   419     raise newException( IndexError, "key not in object" )
       
   420 
       
   421 
       
   422 proc hash*( node: TNetstringNode ): Hash =
       
   423     ## Compute the hash for a TNetstringString node
       
   424     return case node.kind
       
   425     of TNetstringString:
       
   426         hash( node.str )
       
   427     of TNetstringInt:
       
   428         hash( node.num )
       
   429     of TNetstringFloat:
       
   430         hash( node.fnum )
       
   431     of TNetstringBool:
       
   432         hash( node.bval.int )
       
   433     of TNetstringNull:
       
   434         hash( 0 )
       
   435     of TNetstringArray:
       
   436         hash( node.elems )
       
   437     of TNetstringObject:
       
   438         hash( node.fields )
       
   439 
       
   440 
       
   441 proc len*( node: TNetstringNode ): int =
       
   442     ## If `node` is a `TNetstringArray`, it returns the number of elements.
       
   443     ## If `node` is a `TNetstringObject`, it returns the number of pairs.
       
   444     ## If `node` is a `TNetstringString`, it returns strlen.
       
   445     ## Else it returns 0.
       
   446     return case node.kind
       
   447     of TNetstringString:
       
   448         node.str.len
       
   449     of TNetstringArray:
       
   450         node.elems.len
       
   451     of TNetstringObject:
       
   452         node.fields.len
       
   453     else:
       
   454         0
       
   455 
       
   456 
       
   457 proc `[]`*( node: TNetstringNode, name: string ): TNetstringNode =
       
   458     ## Gets a field from a `TNetstringNode`, which must not be nil.
       
   459     ## If the value at `name` does not exist, returns nil
       
   460     assert( not isNil(node) )
       
   461     assert( node.kind == TNetstringObject )
       
   462     for key, item in node:
       
   463         if key == name:
       
   464             return item
       
   465     return nil
       
   466 
       
   467 
       
   468 proc `[]`*( node: TNetstringNode, index: int ): TNetstringNode =
       
   469     ## Gets the node at `index` in an Array. Result is undefined if `index`
       
   470     ## is out of bounds.
       
   471     assert( not isNil(node) )
       
   472     assert( node.kind == TNetstringArray )
       
   473     return node.elems[ index ]
       
   474 
       
   475 
       
   476 proc hasKey*( node: TNetstringNode, key: string ): bool =
       
   477     ## Checks if `key` exists in `node`.
       
   478     assert( node.kind == TNetstringObject )
       
   479     for k, item in items( node.fields ):
       
   480         if k == key: return true
       
   481 
       
   482 
       
   483 proc add*( parent, child: TNetstringNode ) =
       
   484     ## Appends `child` to a TNetstringArray node `parent`.
       
   485     assert( parent.kind == TNetstringArray )
       
   486     parent.elems.add( child )
       
   487 
       
   488 
       
   489 proc add*( node: TNetstringNode, key: string, val: TNetstringNode ) =
       
   490     ## Adds ``(key, val)`` pair to the TNetstringObject `node`.
       
   491     ## For speed reasons no check for duplicate keys is performed.
       
   492     ## (Note, ``[]=`` performs the check.)
       
   493     assert( node.kind == TNetstringObject )
       
   494     node.fields.add( (key, val) )
       
   495 
       
   496 
       
   497 proc `[]=`*( node: TNetstringNode, index: int, val: TNetstringNode ) =
       
   498     ## Sets an index for a `TNetstringArray`.
       
   499     assert( node.kind == TNetstringArray )
       
   500     node.elems[ index ] = val
       
   501 
       
   502 
       
   503 proc `[]=`*( node: TNetstringNode, key: string, val: TNetstringNode ) =
       
   504     ## Sets a field from a `TNetstringObject`. Performs a check for duplicate keys.
       
   505     assert( node.kind == TNetstringObject )
       
   506     for i in 0 .. node.fields.len - 1:
       
   507         if node.fields[i].key == key:
       
   508             node.fields[i].val = val
       
   509             return
       
   510     node.fields.add( (key, val) )
       
   511 
       
   512 
       
   513 proc dump_tnetstring*( node: TNetstringNode ): string =
       
   514     ## Renders a TNetstring `node` as a regular string.
       
   515     case node.kind
       
   516     of TNetstringString:
       
   517         result = $( node.str.len ) & ':' & node.str & ','
       
   518     of TNetstringInt:
       
   519         let str = $( node.num )
       
   520         result = $( str.len ) & ':' & str & '#'
       
   521     of TNetstringFloat:
       
   522         let str = $( node.fnum )
       
   523         result = $( str.len ) & ':' & str & '^'
       
   524     of TNetstringBool:
       
   525         result = if node.bval: "4:true!" else: "5:false!"
       
   526     of TNetstringNull:
       
   527         result = "0:~"
       
   528     of TNetstringArray:
       
   529         result = ""
       
   530         for n in node.items:
       
   531             result = result & n.dump_tnetstring
       
   532         result = $( result.len ) & ':' & result & ']'
       
   533     of TNetstringObject:
       
   534         result = ""
       
   535         for key, val in node.pairs:
       
   536             result = result & $( key.len ) & ':' & key & ',' # key
       
   537             result = result & val.dump_tnetstring            # val
       
   538         result = $( result.len ) & ':' & result & '}'
       
   539 
       
   540 
       
   541 #
       
   542 # Tests!
       
   543 #
       
   544 when isMainModule:
       
   545 
       
   546     # Expected exceptions
       
   547     #
       
   548     try:
       
   549         discard parse_tnetstring( "totally invalid" )
       
   550     except TNetstringParseError:
       
   551         doAssert( true, "invalid tnetstring" )
       
   552     try:
       
   553         discard parse_tnetstring( "what:ever" )
       
   554     except TNetstringParseError:
       
   555         doAssert( true, "bad length" )
       
   556     try:
       
   557         discard parse_tnetstring( "3:yep~" )
       
   558     except TNetstringParseError:
       
   559         doAssert( true, "null w/ > 0 length" )
       
   560     try:
       
   561         discard parse_tnetstring( "8:1:1#1:1#}" )
       
   562     except TNetstringParseError:
       
   563         doAssert( true, "hash with non-string key" )
       
   564     try:
       
   565         discard parse_tnetstring( "7:4:test,}" )
       
   566     except TNetstringParseError:
       
   567         doAssert( true, "hash with odd number of elements" )
       
   568     try:
       
   569         discard parse_tnetstring( "2:25*" )
       
   570     except TNetstringParseError:
       
   571         doAssert( true, "unknown netstring tag" )
       
   572 
       
   573     # Equality
       
   574     #
       
   575     let tnet_int = parse_tnetstring( "1:1#" )
       
   576     doAssert( tnet_int == tnet_int )
       
   577     doAssert( tnet_int == parse_tnetstring( "1:1#" ) )
       
   578     doAssert( parse_tnetstring( "0:~" ) == parse_tnetstring( "0:~" ) )
       
   579 
       
   580     # Type detection
       
   581     #
       
   582     doAssert( tnet_int.kind == TNetstringInt )
       
   583     doAssert( parse_tnetstring( "1:a," ).kind         == TNetstringString )
       
   584     doAssert( parse_tnetstring( "3:1.0^" ).kind       == TNetstringFloat )
       
   585     doAssert( parse_tnetstring( "5:false!" ).kind     == TNetstringBool )
       
   586     doAssert( parse_tnetstring( "0:~" ).kind          == TNetstringNull )
       
   587     doAssert( parse_tnetstring( "9:2:hi,1:1#}" ).kind == TNetstringObject )
       
   588     doAssert( parse_tnetstring( "8:1:1#1:2#]" ).kind  == TNetstringArray )
       
   589 
       
   590     # Iteration (both array and tuple)
       
   591     #
       
   592     var
       
   593         keys: array[ 2, string ]
       
   594         vals: array[ 4, string ]
       
   595         k_idx = 0
       
   596         idx = 0
       
   597     for key, val in parse_tnetstring( "35:2:hi,8:1:a,1:b,]5:there,8:1:c,1:d,]}" ):
       
   598         keys[ idx ] = key
       
   599         idx = idx + 1
       
   600         for item in val:
       
   601             vals[ k_idx ] = item.str
       
   602             k_idx = k_idx + 1
       
   603     doAssert( keys == ["hi","there"] )
       
   604     doassert( vals == ["a","b","c","d"] )
       
   605 
       
   606     # Deep copies
       
   607     #
       
   608     var original = parse_tnetstring( "35:2:hi,8:1:a,1:b,]5:there,8:1:c,1:d,]}" )
       
   609     var copied   = original.copy
       
   610     doAssert( original == copied )
       
   611     doAssert( original.repr != copied.repr )
       
   612     doAssert( original.fields.pop.val.elems.pop.repr != copied.fields.pop.val.elems.pop.repr )
       
   613 
       
   614     # Key deletion
       
   615     #
       
   616     var tnet_obj = parse_tnetstring( "35:2:hi,8:1:a,1:b,]5:there,8:1:c,1:d,]}" )
       
   617     tnet_obj.delete( "hi" )
       
   618     doAssert( tnet_obj.fields.len == 1 )
       
   619 
       
   620     # Hashing
       
   621     #
       
   622     doAssert( tnet_int.hash == 1.hash )
       
   623     doAssert( parse_tnetstring( "4:true!" ).hash == hash( true.int ) )
       
   624 
       
   625     # Length checks.
       
   626     #
       
   627     tnet_obj = parse_tnetstring( "35:2:hi,8:1:a,1:b,]5:there,8:1:c,1:d,]}" )
       
   628     doAssert( parse_tnetstring( "0:~" ).len == 0 )
       
   629     doAssert( tnet_obj.len == 2 )
       
   630     doAssert( parse_tnetstring( "8:1:1#1:2#]" ).len == 2 )
       
   631     doAssert( parse_tnetstring( "5:hallo," ).len == 5 )
       
   632 
       
   633     # Index accessors
       
   634     #
       
   635     tnet_obj = parse_tnetstring( "20:1:1#1:2#1:3#1:4#1:5#]" )
       
   636     doAssert( tnet_obj[ 2 ].num == 3 )
       
   637 
       
   638     # Key accessors
       
   639     #
       
   640     tnet_obj = parse_tnetstring( "11:2:hi,3:yep,}" )
       
   641     doAssert( $tnet_obj["hi"] == "yep" )
       
   642     doAssert( tnet_obj.has_key( "hi" ) == true )
       
   643     doAssert( tnet_obj.has_key( "nope-not-here" ) == false )
       
   644 
       
   645     # Adding elements to an existing TNetstring array
       
   646     #
       
   647     var tnet_array = newTNetstringArray()
       
   648     for i in 1 .. 10:
       
   649         tnet_obj = newTNetstringInt( i )
       
   650         tnet_array.add( tnet_obj )
       
   651     tnet_array[ 6 ] = newTNetstringString( "yep" )
       
   652     doAssert( tnet_array.len == 10 )
       
   653     doAssert( tnet_array[ 4 ].num == 5 )
       
   654     doAssert( tnet_array[ 6 ].str == "yep" )
       
   655 
       
   656     # Adding pairs to an existing TNetstring aobject.
       
   657     #
       
   658     tnet_obj = newTNetstringObject()
       
   659     tnet_obj.add( "yo", newTNetstringInt(1) )
       
   660     tnet_obj.add( "yep", newTNetstringInt(2) )
       
   661     doAssert( tnet_obj["yo"].num == 1 )
       
   662     doAssert( tnet_obj["yep"].num == 2 )
       
   663     doAssert( tnet_obj.len == 2 )
       
   664     tnet_obj[ "more" ] = newTNetstringInt(1)
       
   665     tnet_obj[ "yo" ] = newTNetstringInt(1) # dup check
       
   666     doAssert( tnet_obj.len == 3 )
       
   667 
       
   668     # Serialization.
       
   669     #
       
   670     var tstr = "308:9:givenName,6:Mahlon,16:departmentNumber,22:Information Technology," &
       
   671         "5:title,19:Senior Technologist,13:accountConfig,48:7:vmemail,4:true!7:allpage," &
       
   672         "5:false!7:galhide,0:~}13:homeDirectory,14:/home/m/mahlon,3:uid,6:mahlon,9:yubi" &
       
   673         "KeyId,12:vvidhghkhehj,5:gecos,12:Mahlon Smith,2:sn,5:Smith,14:employeeNumber,5:12921#}"
       
   674     tnet_obj = parse_tnetstring( tstr )
       
   675     doAssert( tstr == tnet_obj.dump_tnetstring )
       
   676 
       
   677     # Value fetching methods
       
   678     #
       
   679     var tnet_null = newTNetstringNull()
       
   680     tnet_obj = newTNetstringString( "Hello." )
       
   681     doAssert( tnet_obj.getStr == "Hello." )
       
   682     doAssert( tnet_null.getStr("nope") == "nope" )
       
   683     doAssert( tnet_null.getStr == "" )
       
   684     tnet_obj = newTNetstringInt( 42 )
       
   685     doAssert( tnet_obj.getInt == 42 )
       
   686     doAssert( tnet_null.getInt == 0 )
       
   687     doAssert( tnet_null.getInt(1) == 1 )
       
   688     tnet_obj = newTNetstringFloat( 1.0 )
       
   689     doAssert( tnet_obj.getFloat == 1.0 )
       
   690     doAssert( tnet_null.getFloat == 0 )
       
   691     doAssert( tnet_null.getFloat(0.1) == 0.1 )
       
   692     tnet_obj = newTNetstringObject()
       
   693     tnet_obj[ "yay" ] = newTNetstringInt( 1 )
       
   694     doAssert( tnet_obj.getFields[0].val == newTNetstringInt(1) )
       
   695     doAssert( tnet_null.getFields.len == 0 )
       
   696     tnet_obj = newTNetstringArray()
       
   697     tnet_obj.add( newTNetstringInt(1) )
       
   698     doAssert( tnet_obj.getElems[0] == newTNetstringInt(1) )
       
   699     doAssert( tnet_null.getElems.len == 0 )
       
   700 
       
   701 
       
   702     echo "* Tests passed!"
       
   703 
       
   704     while true and defined( testing ):
       
   705         for line in readline( stdin ).split_lines:
       
   706             let input = line.strip
       
   707             try:
       
   708                 var tnetstring = parse_tnetstring( input )
       
   709                 echo "  parsed     --> ", tnetstring
       
   710                 echo "  serialized --> ", tnetstring.dump_tnetstring, "\n"
       
   711             except TNetstringParseError:
       
   712                 echo input, " --> ", getCurrentExceptionMsg()
       
   713