1 /*
  2  * Copyright (C) 2013 Glyptodon LLC
  3  *
  4  * Permission is hereby granted, free of charge, to any person obtaining a copy
  5  * of this software and associated documentation files (the "Software"), to deal
  6  * in the Software without restriction, including without limitation the rights
  7  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  8  * copies of the Software, and to permit persons to whom the Software is
  9  * furnished to do so, subject to the following conditions:
 10  *
 11  * The above copyright notice and this permission notice shall be included in
 12  * all copies or substantial portions of the Software.
 13  *
 14  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 15  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 16  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 17  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 18  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 19  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 20  * THE SOFTWARE.
 21  */
 22 
 23 var Guacamole = Guacamole || {};
 24 
 25 /**
 26  * Simple Guacamole protocol parser that invokes an oninstruction event when
 27  * full instructions are available from data received via receive().
 28  * 
 29  * @constructor
 30  */
 31 Guacamole.Parser = function() {
 32 
 33     /**
 34      * Reference to this parser.
 35      * @private
 36      */
 37     var parser = this;
 38 
 39     /**
 40      * Current buffer of received data. This buffer grows until a full
 41      * element is available. After a full element is available, that element
 42      * is flushed into the element buffer.
 43      * 
 44      * @private
 45      */
 46     var buffer = "";
 47 
 48     /**
 49      * Buffer of all received, complete elements. After an entire instruction
 50      * is read, this buffer is flushed, and a new instruction begins.
 51      * 
 52      * @private
 53      */
 54     var element_buffer = [];
 55 
 56     // The location of the last element's terminator
 57     var element_end = -1;
 58 
 59     // Where to start the next length search or the next element
 60     var start_index = 0;
 61 
 62     /**
 63      * Appends the given instruction data packet to the internal buffer of
 64      * this Guacamole.Parser, executing all completed instructions at
 65      * the beginning of this buffer, if any.
 66      *
 67      * @param {String} packet The instruction data to receive.
 68      */
 69     this.receive = function(packet) {
 70 
 71         // Truncate buffer as necessary
 72         if (start_index > 4096 && element_end >= start_index) {
 73 
 74             buffer = buffer.substring(start_index);
 75 
 76             // Reset parse relative to truncation
 77             element_end -= start_index;
 78             start_index = 0;
 79 
 80         }
 81 
 82         // Append data to buffer
 83         buffer += packet;
 84 
 85         // While search is within currently received data
 86         while (element_end < buffer.length) {
 87 
 88             // If we are waiting for element data
 89             if (element_end >= start_index) {
 90 
 91                 // We now have enough data for the element. Parse.
 92                 var element = buffer.substring(start_index, element_end);
 93                 var terminator = buffer.substring(element_end, element_end+1);
 94 
 95                 // Add element to array
 96                 element_buffer.push(element);
 97 
 98                 // If last element, handle instruction
 99                 if (terminator == ";") {
100 
101                     // Get opcode
102                     var opcode = element_buffer.shift();
103 
104                     // Call instruction handler.
105                     if (parser.oninstruction != null)
106                         parser.oninstruction(opcode, element_buffer);
107 
108                     // Clear elements
109                     element_buffer.length = 0;
110 
111                 }
112                 else if (terminator != ',')
113                     throw new Error("Illegal terminator.");
114 
115                 // Start searching for length at character after
116                 // element terminator
117                 start_index = element_end + 1;
118 
119             }
120 
121             // Search for end of length
122             var length_end = buffer.indexOf(".", start_index);
123             if (length_end != -1) {
124 
125                 // Parse length
126                 var length = parseInt(buffer.substring(element_end+1, length_end));
127                 if (length == NaN)
128                     throw new Error("Non-numeric character in element length.");
129 
130                 // Calculate start of element
131                 start_index = length_end + 1;
132 
133                 // Calculate location of element terminator
134                 element_end = start_index + length;
135 
136             }
137             
138             // If no period yet, continue search when more data
139             // is received
140             else {
141                 start_index = buffer.length;
142                 break;
143             }
144 
145         } // end parse loop
146 
147     };
148 
149     /**
150      * Fired once for every complete Guacamole instruction received, in order.
151      * 
152      * @event
153      * @param {String} opcode The Guacamole instruction opcode.
154      * @param {Array} parameters The parameters provided for the instruction,
155      *                           if any.
156      */
157     this.oninstruction = null;
158 
159 };
160