LLUDP Dissector
From OpenSimulator
Revision as of 04:07, 31 July 2008 by Neopallium (Talk | contribs)
Contents |
LLUDP protocol dissector
On this page you will find the Lua code for a wireshark protocol dissector that can parse the message_template.msg file and use that information to decode all the message fields from the Linden UDP protocol.
Description of source files
- "lludp.lua" -- contains the code that decodes each packet header and decompresses zero-encoded packets. This file uses wireshark only functions for accessing packet bytes and building a tree of information from each packet.
- "llmessage.lua" -- contains the message_template.msg file parser the decodes the tokens from the lexer into an tree of tables containing all details about each message/block/variable from the template file. This file only has pure lua code.
- "lexer.lua" -- contains the template file lexer. This lexer knows how to tokenize the template file into the follow tokens: IDENTIFIER, NUMBER, COMMENT, EOL. The stream of tokens produced by this lexer is parsed by the "llmessage.lua" file.
File "lludp.lua"
--[[ BSD-Licensed: Copyright (c) 2008, Robert G. Jakabosky <bobby@sharedrealm.com> All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the SharedRealm nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ]] dofile("llmessage.lua") -- cache globals to local for speed. local str_format=string.format -- test if ByteArray only has printable ASCII character and ends with '\0' local allowed_special = { [0] = true, -- null [9] = true, -- tab [10] = true, -- new line [13] = true, -- carriage return } local function is_string(bytes) local c local max = bytes:len() - 1 for i=0,max do c = bytes:get_index(i) if c >= 127 then -- not ascii character return false elseif c < 32 then -- control character range if not allowed_special[c] then -- control characters between NULL and Space return false elseif c == 0 and i < max then -- null byte only allowed at end. return false end end end return true end -- lludp protocol example -- declare our protocol local lludp_proto = Proto("lludp","LLUDP","LindenLabs UDP Protocol") -- setup preferences lludp_proto.prefs["template_file"] = Pref.string("Message template file", "message_template.msg", "Message template file") lludp_proto.prefs["udp_port_start"] = Pref.string("UDP port range start", "13000", "First UDP port to decode as this protocol") lludp_proto.prefs["udp_port_end"] = Pref.string("UDP port range end", "13050", "Last UDP port to decode as this protocol") -- current preferences settings. local current_settings = { template_file = "", udp_port_start = -1, udp_port_end = -1, } -- current list of parsed messages. local message_details = nil -- setup protocol fields. lludp_proto.fields = {} local fds = lludp_proto.fields fds.flags = ProtoField.new("Flags", "lludp.flags", "FT_UINT8", nil, "BASE_HEX", "0xFF") fds.flags_zero = ProtoField.new("Zero", "lludp.flags.zero", "FT_UINT8", nil, "BASE_HEX", "0x80") fds.flags_reliable = ProtoField.new("Reliable", "lludp.flags.rel", "FT_UINT8", nil, "BASE_HEX", "0x40") fds.flags_resent = ProtoField.new("Resent", "lludp.flags.res", "FT_UINT8", nil, "BASE_HEX", "0x20") fds.flags_ack = ProtoField.new("Ack", "lludp.flags.ack", "FT_UINT8", nil, "BASE_HEX", "0x10") fds.sequence = ProtoField.new("Sequence", "lludp.sequence", "FT_UINT32", nil, "BASE_DEC") fds.extra_len = ProtoField.new("Extra length", "lludp.extra_len", "FT_UINT8", nil, "BASE_DEC") fds.extra_bytes = ProtoField.new("Extra header", "lludp.extra_bytes", "FT_BYTES", nil, "BASE_HEX") fds.msg_id = ProtoField.new("Message ID", "lludp.msg.id", "FT_UINT32", nil, "BASE_HEX") fds.msg_name = ProtoField.new("Message name", "lludp.msg.name", "FT_STRINGZ", nil) fds.msg = ProtoField.new("Message body", "lludp.msg", "FT_BYTES", nil, "BASE_HEX") fds.acks_count = ProtoField.new("Acks count", "lludp.acks_count", "FT_UINT8", nil, "BASE_DEC") fds.acks = ProtoField.new("Acks", "lludp.acks", "FT_UINT32", nil, "BASE_DEC") fds.block_count = ProtoField.new("Block count", "lludp.block_count", "FT_UINT8", nil, "BASE_DEC") fds.block = ProtoField.new("Block", "lludp.block", "FT_BYTES", nil, "BASE_HEX") fds.var_fixed = ProtoField.new("Fixed blob", "lludp.var.fixed", "FT_BYTES", nil, "BASE_HEX") fds.var_variable = ProtoField.new("Variable blob", "lludp.var.variable", "FT_BYTES", nil, "BASE_HEX") fds.var_string = ProtoField.new("String", "lludp.var.string", "FT_STRINGZ", nil) fds.var_u8 = ProtoField.new("U8", "lludp.var.u8", "FT_UINT8", nil, "BASE_DEC") fds.var_u16 = ProtoField.new("U16", "lludp.var.u16", "FT_UINT16", nil, "BASE_DEC") fds.var_u32 = ProtoField.new("U32", "lludp.var.u32", "FT_UINT32", nil, "BASE_DEC") fds.var_u64 = ProtoField.new("U64", "lludp.var.u64", "FT_UINT64", nil, "BASE_DEC") fds.var_s8 = ProtoField.new("S8", "lludp.var.s8", "FT_INT8", nil, "BASE_DEC") fds.var_s16 = ProtoField.new("S16", "lludp.var.s16", "FT_INT16", nil, "BASE_DEC") fds.var_s32 = ProtoField.new("S32", "lludp.var.s32", "FT_INT32", nil, "BASE_DEC") fds.var_s64 = ProtoField.new("S64", "lludp.var.s64", "FT_INT64", nil, "BASE_DEC") fds.var_f32 = ProtoField.new("F32", "lludp.var.f32", "FT_FLOAT", nil, "BASE_DEC") fds.var_f64 = ProtoField.new("F64", "lludp.var.f64", "FT_DOUBLE", nil, "BASE_DEC") fds.var_llvector3 = ProtoField.new("LLVector3", "lludp.var.llvector3", "FT_BYTES", nil, "BASE_HEX") fds.var_llvector3d = ProtoField.new("LLVector3d", "lludp.var.llvector3d", "FT_BYTES", nil, "BASE_HEX") fds.var_llvector4 = ProtoField.new("LLVector4", "lludp.var.llvector4", "FT_BYTES", nil, "BASE_HEX") fds.var_llquaternion = ProtoField.new("LLQuaternion", "lludp.var.llquaternion", "FT_BYTES", nil, "BASE_HEX") fds.var_lluid = ProtoField.new("LLUID", "lludp.var.lluid", "FT_BYTES", nil, "BASE_HEX") fds.var_bool = ProtoField.new("BOOL", "lludp.var.bool", "FT_UINT8", nil, "BASE_DEC") fds.var_ipaddr = ProtoField.new("IPADDR", "lludp.var.ipaddr", "FT_IPv4", nil, "BASE_DEC") fds.var_ipport = ProtoField.new("IPPORT", "lludp.var.ipport", "FT_UINT16", nil, "BASE_DEC") -- variable type handlers. local variable_handlers = { Fixed = function(block_tree, buffer, offset, len, var) local rang = buffer(offset, len) local ti = block_tree:add_le(fds.var_fixed, rang) if len <= 4 then ti:set_text(str_format("%s: 0x%08x",var.name, rang:uint())) else ti:set_text(str_format("%s: length=%d, Blob:%s", var.name, len, tostring(rang))) end end, Variable = function(block_tree, buffer, offset, len, var) local is_data = false -- try to guess if this field is text. if var.name:find("Data") then -- this is a data find. is_data = true end local str_rang = buffer(offset + var.count_length, len - var.count_length) local bytes = str_rang:bytes() if not is_data and is_string(bytes) then local str = str_rang:string() local ti = block_tree:add(fds.var_string, buffer(offset, len), str) ti:set_text(str_format("%s: %s", var.name, str)) else local rang = buffer(offset, len) local ti = block_tree:add_le(fds.var_variable, rang) if len <= 4 then ti:set_text(str_format("%s: 0x%08x",var.name, rang:uint())) else ti:set_text(str_format("%s: length=%d, Blob:%s", var.name, len, tostring(rang))) end end end, U8 = function(block_tree, buffer, offset, len, var) local rang = buffer(offset, len) local ti = block_tree:add_le(fds.var_u8, rang) ti:set_text(str_format("%s: %d", var.name, rang:le_uint())) end, U16 = function(block_tree, buffer, offset, len, var) local rang = buffer(offset, len) local ti = block_tree:add_le(fds.var_u16, rang) ti:set_text(str_format("%s: %d", var.name, rang:le_uint())) end, U32 = function(block_tree, buffer, offset, len, var) local rang = buffer(offset, len) local ti = block_tree:add_le(fds.var_u32, rang) ti:set_text(str_format("%s: %d", var.name, rang:le_uint())) end, U64 = function(block_tree, buffer, offset, len, var) local rang = buffer(offset, len) local ti = block_tree:add_le(fds.var_u64, rang) ti:set_text(str_format("%s: 0x%s",var.name, tostring(rang))) end, S8 = function(block_tree, buffer, offset, len, var) local rang = buffer(offset, len) local ti = block_tree:add_le(fds.var_s8, rang) local num = rang:le_uint() if num > 127 then num = num - 256 end ti:set_text(str_format("%s: %d",var.name, num)) end, S16 = function(block_tree, buffer, offset, len, var) local rang = buffer(offset, len) local ti = block_tree:add_le(fds.var_s16, rang) local num = rang:le_uint() if num > 32768 then num = num - 65536 end ti:set_text(str_format("%s: %d",var.name, num)) end, S32 = function(block_tree, buffer, offset, len, var) local rang = buffer(offset, len) local ti = block_tree:add_le(fds.var_s32, rang) local num = rang:le_uint() if num > 2147483648 then num = num - 4294967296 end ti:set_text(str_format("%s: %d",var.name, num)) end, S64 = function(block_tree, buffer, offset, len, var) local rang = buffer(offset, len) local ti = block_tree:add_le(fds.var_s64, rang) ti:set_text(str_format("%s: 0x%s",var.name, tostring(rang))) end, F32 = function(block_tree, buffer, offset, len, var) local rang = buffer(offset, len) local ti = block_tree:add_le(fds.var_f32, rang) ti:set_text(str_format("%s: %f", var.name, rang:le_float())) end, F64 = function(block_tree, buffer, offset, len, var) local rang = buffer(offset, len) local ti = block_tree:add_le(fds.var_f64, rang) ti:set_text(str_format("%s: %f", var.name, rang:le_float())) end, LLVector3 = function(block_tree, buffer, offset, len, var) local rang = buffer(offset, len) local ti = block_tree:add_le(fds.var_llvector3, rang) -- parse LLVector3 local x,y,z x = buffer(offset + 0,4):le_float() y = buffer(offset + 4,4):le_float() z = buffer(offset + 8,4):le_float() -- display ti:set_text(str_format("%s: <%f,%f,%f>", var.name, x, y, z)) end, LLVector3d = function(block_tree, buffer, offset, len, var) local rang = buffer(offset, len) local ti = block_tree:add_le(fds.var_llvector3d, rang) -- parse LLVector3d local x,y,z x = buffer(offset + 0,8):le_float() y = buffer(offset + 8,8):le_float() z = buffer(offset + 16,8):le_float() -- display ti:set_text(str_format("%s: <%f,%f,%f>", var.name, x, y, z)) end, LLVector4 = function(block_tree, buffer, offset, len, var) local rang = buffer(offset, len) local ti = block_tree:add_le(fds.var_llvector4, rang) -- parse LLVector4 local x,y,z,s x = buffer(offset + 0,4):le_float() y = buffer(offset + 4,4):le_float() z = buffer(offset + 8,4):le_float() s = buffer(offset + 12,4):le_float() -- display ti:set_text(str_format("%s: <%f,%f,%f,%f>", var.name, x, y, z, s)) end, LLQuaternion = function(block_tree, buffer, offset, len, var) local rang = buffer(offset, len) local ti = block_tree:add_le(fds.var_llquaternion, rang) -- parse LLQuaternion local x,y,z,w x = buffer(offset + 0,4):le_float() y = buffer(offset + 4,4):le_float() z = buffer(offset + 8,4):le_float() -- calculate W w = 1 - (x * x) - (y * y) - (z * z) if w > 0 then w = math.sqrt(w) else w = 0 end -- display ti:set_text(str_format("%s: <%f,%f,%f,%f>", var.name, x, y, z, w)) end, LLUUID = function(block_tree, buffer, offset, len, var) local rang = buffer(offset, len) local ti = block_tree:add_le(fds.var_lluid, rang) local str = tostring(rang) str = str:sub(1,8) .. '-' .. str:sub(9,12) .. '-' .. str:sub(13,16) .. '-' .. str:sub(17,20) .. '-' .. str:sub(21) ti:set_text(str_format("%s: %s", var.name, str)) end, BOOL = function(block_tree, buffer, offset, len, var) local rang = buffer(offset, len) local ti = block_tree:add_le(fds.var_bool, rang) local val = "false" if rang:le_uint() > 0 then val = "true" end ti:set_text(str_format("%s: %s", var.name, val)) end, IPADDR = function(block_tree, buffer, offset, len, var) local rang = buffer(offset, len) local ti = block_tree:add(fds.var_ipaddr, rang) ti:set_text(str_format("%s: %s", var.name, tostring(rang:ipv4()))) end, IPPORT = function(block_tree, buffer, offset, len, var) local rang = buffer(offset, len) local ti = block_tree:add(fds.var_ipport, rang) ti:set_text(str_format("%s: %d", var.name, rang:uint())) end, } -- un-register lludp to handle udp port range local function unregister_udp_port_range(start_port, end_port) if not start_port or start_port <= 0 or not end_port or end_port <= 0 then return end udp_port_table = DissectorTable.get("udp.port") for port = start_port,end_port do udp_port_table:remove(port,lludp_proto) end end -- register lludp to handle udp port range local function register_udp_port_range(start_port, end_port) if not start_port or start_port <= 0 or not end_port or end_port <= 0 then return end udp_port_table = DissectorTable.get("udp.port") for port = start_port,end_port do udp_port_table:add(port,lludp_proto) end end -- handle preferences changes. function lludp_proto.init(arg1, arg2) local old_start, old_end local new_start, new_end -- check if preferences have changed. for pref_name,old_v in pairs(current_settings) do local new_v = lludp_proto.prefs[pref_name] if new_v ~= old_v then if pref_name == "template_file" then -- load & parse message_template.msg file. local file = new_v if file and file:len() > 0 then local new_details = parse_template(file) if new_details then message_details = new_details end end elseif pref_name == "udp_port_start" then old_start = old_v new_start = new_v elseif pref_name == "udp_port_end" then old_end = old_v new_end = new_v end -- save new value. current_settings[pref_name] = new_v end end -- un-register old port range if old_start and old_end then unregister_udp_port_range(tonumber(old_start), tonumber(old_end)) end -- register new port range. if new_start and new_end then register_udp_port_range(tonumber(new_start), tonumber(new_end)) end end -- parse flag bits. local FLAG_ZER = 4 local FLAG_REL = 3 local FLAG_RES = 2 local FLAG_ACK = 1 local flag_names = {"ACK", "RES", "REL", "ZER"} local bits_lookup = { {}, {1}, {2}, {2,1}, {3}, {3,1}, {3,2}, {3,2,1}, {4}, {4,1}, {4,2}, {4,2,1}, {4,3}, {4,3,1}, {4,3,2}, {4,3,2,1}, } local function parse_flags(flags) flags = (flags / 16) + 1 local bit_list = bits_lookup[flags] local bits = {} local names = "" for _, bit in ipairs(bit_list) do bits[bit] = true if names:len() > 0 then names = names .. ", " end names = names .. flag_names[bit] end return bits, names end local function grow_buff(buff, size) local old_size = buff:len() if old_size > size then return end -- buffer needs to grow buff:set_size(size) -- fill new space with zeros for i = old_size,size-1 do buff:set_index(i, 0) end end local function zero_decode(zero_buf) local out_buf = ByteArray.new() local zero_off = 0 local zero_len = zero_buf:len() local out_size = 0 local out_off = 0 local b -- pre-allocate grow_buff(out_buf, zero_len) out_size = zero_len -- zero expand repeat b = zero_buf:get_index(zero_off) if b == 0 then -- get zero count local count = zero_buf:get_index(zero_off + 1) if count == 0 then count = 255 end out_off = out_off + count -- fill zeros if out_off > out_size then out_size = out_off + 128 grow_buff(out_buf, out_size) end zero_off = zero_off + 2 else if out_off >= out_size then out_size = out_off + (zero_len - zero_off) + 4 grow_buff(out_buf, out_size) end -- copy non-zero bytes. out_buf:set_index(out_off,b) zero_off = zero_off + 1 out_off = out_off + 1 end until zero_off == zero_len -- truncate to real size. out_buf:set_size(out_off) return out_buf:tvb("Decompressed Data") end local function parse_msg_id(buff) local b = buff:get_index(0) local msg_id = b local msg_id_len = 1 if b == 255 then b = buff:get_index(1) msg_id = msg_id * 256 + b msg_id_len = 2 if b == 255 then b = buff:get_index(2) msg_id = msg_id * 256 + b b = buff:get_index(3) msg_id = msg_id * 256 + b msg_id_len = 4 end end return msg_id, msg_id_len end -- get message name. local function get_msg_name(msg_id) -- check that we have message details if message_details == nil then return str_format("0x%08x", msg_id) end -- find message name from id. local msg = message_details.msgs[msg_id] -- Invalid message id if msg == nil then return str_format("0x%08x", msg_id) end return msg.name end -- calculate length a block. local function get_block_length(msg_buffer, start_offset, block) -- check if bock is fixed length. if block.fixed_length then return block.min_length end -- parse block's variables to calculate total block length. local offset = start_offset local rang for _,var in ipairs(block) do local len = 0 if var.has_count then -- variable with length bytes. len = var.count_length --print(var.name, offset, ", len:", len) rang = msg_buffer(offset, len) len = len + rang:le_uint() --print(var.name, var.count_length, ", total:", len) else -- fixed length variable len = var.length --print(var.name, ", total:", len) end offset = offset + len end return (offset - start_offset) end -- build block tree local function build_block_tree(msg_buffer, block_tree, start_offset, block) local offset = start_offset local rang -- parse block's variables for _,var in ipairs(block) do local len = 0 if var.has_count then -- variable with length bytes. len = var.count_length rang = msg_buffer(offset, len) len = len + rang:le_uint() else -- fixed length variable len = var.length end -- get variable's type field. local handler = variable_handlers[var.type] -- parse variable. if handler then handler(block_tree, msg_buffer, offset, len, var) end offset = offset + len end return (offset - start_offset) end -- buid message tree local function build_msg_tree(msg_buffer, msg_tree, msg_id) local offset = 0 local rang -- check that we have message details if message_details == nil then msg_tree:set_text(str_format("Message Id: 0x%08x", msg_id)) return nil end -- find message name from id. local msg = message_details.msgs[msg_id] -- Invalid message id if msg == nil then msg = str_format("Invalid message id: 0x%08x", msg_id) msg_tree:add_expert_info(PI_MALFORMED, PI_ERROR, msg) msg_tree:set_text(msg) return nil end -- skip message id bytes. offset = msg.id_length -- set message name. msg_tree:set_text(msg.name .. ":") -- proccess message blocks for _,block in ipairs(msg) do local count = block.count if count == nil then -- parse count byte. rang = msg_buffer(offset,1) count = rang:uint() msg_tree:add(fds.block_count,rang) offset = offset + 1 end -- print("block name: ", block.name, count) for n=1,count do local block_len = get_block_length(msg_buffer, offset, block) -- parse block rang = msg_buffer(offset, block_len) local block_tree = msg_tree:add(fds.block,rang) if count > 1 then block_tree:set_text(str_format("%s: %d of %d",block.name,n,count)) else block_tree:set_text(block.name) end -- parse block variables. build_block_tree(msg_buffer, block_tree, offset, block) offset = offset + block_len end end return msg.name end -- packet dissector function lludp_proto.dissector(buffer,pinfo,tree) local rang,offset pinfo.cols.protocol = "LLUDP" local lludp_tree = tree:add(lludp_proto,buffer(),"Linden UDP Protocol") -- Flags byte. offset = 0 rang = buffer(offset,1) local flags = rang:uint() local flags_bits, flags_list = parse_flags(flags) flags_tree = lludp_tree:add(fds.flags, rang) flags_tree:set_text("Flags: " .. str_format('0x%02X (%s)', flags, flags_list)) flags_tree:add(fds.flags_zero, rang) flags_tree:add(fds.flags_reliable, rang) flags_tree:add(fds.flags_resent, rang) flags_tree:add(fds.flags_ack, rang) offset = offset + 1 -- Sequence number 4 bytes. rang = buffer(offset,4) local sequence = rang:uint() lludp_tree:add(fds.sequence, rang) offset = offset + 4 -- Extra header length. rang = buffer(offset,1) local extra_length = rang:uint() lludp_tree:add(fds.extra_len,rang) offset = offset + 1 -- Extra header data. if extra_length > 0 then rang = buffer(offset, extra_length) lludp_tree:add(fds.extra_bytes, rang) offset = offset + extra_length end -- Appended Acks. count local acks_bytes = 0 local acks_count = 0 if flags_bits[FLAG_ACK] then rang = buffer(buffer:len() - 1, 1) acks_count = rang:uint() acks_bytes = (acks_count * 4) + 1 end -- Zero Decode local msg_len = (buffer:len() - acks_bytes) - offset if flags_bits[FLAG_ZER] then msg_buffer=zero_decode(buffer(offset,msg_len):bytes()) msg_len = msg_buffer:len() offset = 0 else msg_buffer = buffer(offset, msg_len):tvb() offset = 0 end -- Message ID local msg_id, msg_id_len = -1, 4 if msg_id_len > msg_len then msg_id_len = msg_len end msg_id, msg_id_len = parse_msg_id(msg_buffer(offset, msg_id_len):bytes()) rang = msg_buffer(offset, msg_id_len) lludp_tree:add(fds.msg_id, rang) local msg_name = get_msg_name(msg_id) if msg_name == nil then msg_name = str_format("0x%08x", msg_id) else lludp_tree:add(fds.msg_name, rang, msg_name) end -- Message body. rang = msg_buffer(offset, msg_len) local msg_tree = lludp_tree:add(fds.msg, rang) build_msg_tree(msg_buffer, msg_tree, msg_id) -- Appended Acks. list. if flags_bits[FLAG_ACK] then local acks_off = buffer:len() rang = buffer(acks_off - 1, 1) acks_off = acks_off - acks_bytes local acks_tree = lludp_tree:add(fds.acks_count, rang) for i = 1,acks_count do rang = buffer(acks_off,4) acks_tree:add(fds.acks, rang) acks_off = acks_off + 4 end end -- Info column pinfo.cols.info = str_format('[%s] Seq=%u Type=%s', flags_list, sequence, msg_name) end -- register lludp to handle udp ports 9000-9003 register_udp_port_range(9000,9003)
File "llmessage.lua"
--[[ BSD-Licensed: Copyright (c) 2008, Robert G. Jakabosky <bobby@sharedrealm.com> All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the SharedRealm nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ]] dofile("lexer.lua") local lexer local cur_token = nil local cur_token_str = nil local function get_token(skip_tokens) repeat token = lexer.get_token() if token ~= nil then cur_token = token[1] cur_token_str = token[2] end until token == nil or not skip_tokens[cur_token] return token end local function run_parser(parser) local state = parser.init() if parser.skip_tokens == nil then parser.skip_tokens = {} end while get_token(parser.skip_tokens) do -- check what the parser is expecting next. if state.expect then -- check expected type if state.expect ~= cur_token then error(string.format("state.expected token '%s' instead of '%s'", TokenNames[state.expect], TokenNames[cur_token])) end -- reset expect field state.expect = nil end if state.expect_str then -- check expected string if state.expect_str ~= cur_token_str then error(string.format("state.expected token '%s' instead of '%s'", state.expect_str, cur_token_str)) end -- reset expect_str field state.expect_str = nil end -- get handler function for current token. local f = parser[cur_token] if f ~= nil then local ret = f(state) if ret then -- praser finished. return ret end elseif parser.unhandled_error then error(string.format("unhandled token '%s' when paring '%s'\n", cur_token_str, parser.name)) end end return parser.eof(state) end -- Known variable types and there fixed length. -- length == -1, requires a number after the type that is the length of count field -- length == -2, requires a number after the type that is the fixed variable length. VariableTypes = { Null = 0, Fixed = -2, Variable = -1, U8 = 1, U16 = 2, U32 = 4, U64 = 8, S8 = 1, S16 = 2, S32 = 4, S64 = 8, F32 = 4, F64 = 8, LLVector3 = 12, LLVector3d = 24, LLVector4 = 16, LLQuaternion = 12, LLUUID = 16, BOOL = 1, IPADDR = 4, IPPORT = 2, } -- -- Variable parser -- local variable_parser = { name = "variable", unhandled_error = false, skip_tokens = {[Token.EOL] = true}, init = function() return { name = "<MISSING VARIABLE NAME>", type = "Null", has_count = false, length = 0, expect = Token.IDENTIFIER, expect_field = "name", required = 2 } end, [Token.IDENTIFIER] = function(state) if state.expect_field == "name" then state.name = cur_token_str state.expect = Token.IDENTIFIER state.expect_field = "type" state.required = state.required - 1 elseif state.expect_field == "type" then state.type = cur_token_str state.length = VariableTypes[state.type] if state.length == nil then error("Unknown variable type: " .. cur_token_str) elseif state.length == -1 or state.length == -2 then state.expect = Token.NUMBER else state.required = state.required - 1 end else error(string.format("unhandled variable identifier: %s\n",cur_token_str)) end return nil end, [Token.NUMBER] = function(state) if state.expect_field == "type" then if state.length == -1 then -- variable field length uses embedded count field state.has_count = true state.count_length = tonumber(cur_token_str) state.length = nil elseif state.length == -2 then -- fixed field length state.length = tonumber(cur_token_str) end state.required = state.required - 1 else error(string.format("unhandled variable number: %s\n",cur_token_str)) end return nil end, ["{"] = function(state) error("sub block not allowed in variable block") end, ["}"] = function(state) if state.required > 0 then error("missing " .. state.required .. " fields") end -- clean state. state.required = nil state.expect = nil state.expect_field = nil return state end, eof = function(state) error("missing '}' token at end of variable: " .. state.name) end, } -- Block Quantities local BlockQuantity = { Single = 1, Variable = -1, Multiple = -2, } -- -- Block parser -- local block_parser = { name = "block", unhandled_error = false, skip_tokens = {[Token.EOL] = true}, init = function() return { name = "<MISSING BLOCK NAME>", quantity = "Single", count = 0, min_length = 0, fixed_length = true, expect = Token.IDENTIFIER, expect_field = "name", required = 2 } end, [Token.IDENTIFIER] = function(state) if state.expect_field == "name" then state.name = cur_token_str state.expect = Token.IDENTIFIER state.expect_field = "quantity" state.required = state.required - 1 elseif state.expect_field == "quantity" then state.quantity = cur_token_str state.count = BlockQuantity[cur_token_str] if state.count == nil then error("Unknown block quantity: " .. cur_token_str) elseif state.count == -2 then state.expect_field = "count" state.expect = Token.NUMBER else if state.count == -1 then state.has_count = true state.count_length = 1 state.count = nil end state.required = state.required - 1 end else error(string.format("unhandled block identifier: %s\n",cur_token_str)) end return nil end, [Token.NUMBER] = function(state) if state.expect_field == "count" then state.count = tonumber(cur_token_str) state.required = state.required - 1 else error(string.format("unhandled block number: %s\n",cur_token_str)) end return nil end, ["{"] = function(state) local variable = run_parser(variable_parser) table.insert(state,variable) -- add length of fixed length variables to minimal length of block. if variable.has_count then state.min_length = state.min_length + variable.count_length state.fixed_length = false else state.min_length = state.min_length + variable.length end end, ["}"] = function(state) if state.required > 0 then error("missing " .. state.required .. " fields") end -- clean state. state.required = nil state.expect = nil state.expect_field = nil return state end, eof = function(state) error("missing '}' token at end of block: " .. state.name) end, } -- -- Message parser -- local message_parser = { name = "message", unhandled_error = false, skip_tokens = {[Token.EOL] = true}, init = function() -- create state return { name = "<MISSING MESSAGE NAME>", expect = Token.IDENTIFIER, expect_field = "name", fixed_length = true, min_length = 0, required = 5 } end, [Token.IDENTIFIER] = function(state) if state.expect_field == "name" then state.name = cur_token_str state.expect = Token.IDENTIFIER state.expect_field = "frequency" state.required = state.required - 1 elseif state.expect_field == "frequency" then state.frequency = cur_token_str state.expect = Token.NUMBER state.required = state.required - 1 elseif state.expect_field == "trust" then state.trust = cur_token_str state.expect = Token.IDENTIFIER state.expect_field = "compression" state.required = state.required - 1 elseif state.expect_field == "compression" then state.compression = cur_token_str state.required = state.required - 1 else error(string.format("unhandled message identifier: %s\n",cur_token_str)) end return nil end, [Token.NUMBER] = function(state) if state.expect_field == "frequency" then state.number = tonumber(cur_token_str) state.expect = Token.IDENTIFIER state.expect_field = "trust" state.required = state.required - 1 -- create true message id from frequency and message number local freq = state.frequency if freq == "High" then -- High is already correct. state.id = state.number state.id_length = 1 elseif freq == "Medium" then state.id = tonumber("0xFF" .. string.format("%02X", state.number)) state.id_length = 2 elseif freq == "Low" then state.id = tonumber("0xFFFF" .. string.format("%04X", state.number)) state.id_length = 4 else -- Fixed is already correct. state.id = state.number state.id_length = 4 end else error(string.format("unhandled message number: %s\n",cur_token_str)) end return nil end, ["{"] = function(state) local block = run_parser(block_parser) table.insert(state,block) -- add min length of block to minimal length of message local min_length = block.min_length if block.has_count then -- add one byte for the block count min_length = min_length + 1 state.fixed_length = false else -- if block is not fixed length then message can't be fixed length. if not block.fixed_length then state.fixed_length = false end min_length = min_length * block.count end state.min_length = state.min_length + min_length end, ["}"] = function(state) if state.required > 0 then error("missing " .. state.required .. " fields") end -- clean state. state.required = nil state.expect = nil state.expect_field = nil return state end, eof = function(state) error("missing '}' token at end of message: " .. state.name) end, } -- -- Template file parser -- local template_parser = { name = "message_template", unhandled_error = false, init = function() return { version = 0, msg_count = 0, msgs = {} } end, [Token.IDENTIFIER] = function(state) -- handle version if cur_token_str == "version" then state.expect = Token.NUMBER state.last_ident = cur_token_str else error(string.format("unknown template identifier: %s\n",cur_token_str)) end return nil end, [Token.NUMBER] = function(state) -- handle version number if state.last_ident == "version" then state.last_ident = nil state.version = tonumber(cur_token_str) -- check version number if state.version ~= 2 then error("invalid verion: " .. state.version) end else error(string.format("unhandled template number: %s\n",cur_token_str)) end return nil end, ["{"] = function(state) local message = run_parser(message_parser) state.msg_count = state.msg_count + 1 state.msgs[message.id] = message end, ["}"] = function(state) error(string.format("unhandled '%s' token",cur_token_str)) end, eof = function(state) return state end, } function parse_template(file) -- create lexer local status, ret = pcall(get_lexer,file) if not status then io.write(string.format("Failed parse file into tokens: %s\n", ret)) return nil end lexer = ret -- parse template file local status, ret = pcall(run_parser,template_parser) if not status then io.write(string.format("Failed parsing on line %d: %s\n", lexer.get_line_number(), ret)) return nil end io.write("finished parsing: " .. file .. "\n") -- return list of messages parsed from file. return ret end --parse_template("message_template.msg") --print_tokens("message_template.msg")
File "lexer.lua"
--[[ BSD-Licensed: Copyright (c) 2008, Robert G. Jakabosky <bobby@sharedrealm.com> All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the SharedRealm nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ]] -- Token types. Token = { NONE = -1, IDENTIFIER = -2, NUMBER = -3, COMMENT = -6, EOL = -7, ["{"] = "{", ["}"] = "}", } TokenNames = { [-1] = "NONE", [-2] = "IDENTIFIER", [-3] = "NUMBER", [-6] = "COMMENT", [-7] = "EOL", ["{"] = "{", ["}"] = "}", } -- parse line into array of tokens. local function default_parse_tokens(line) local tokens = {} local comment = nil -- check for a comment on this line. local idx = line:find("//") if idx ~= nil then comment = {Token.COMMENT, line:sub(idx)} -- remove comment from line line = line:sub(1,idx - 1) end -- split line into tokens using white-space as token delimitator for tok in line:gmatch("%s?([^%s]+)") do local tok_type = Token.NONE -- check for number if tonumber(tok) ~= nil then tok_type = Token.NUMBER elseif Token[tok] then -- token is same as type tok_type = Token[tok] else -- token is an identifier tok_type = Token.IDENTIFIER end table.insert(tokens,{tok_type,tok}) end -- insert comment token. if comment ~= nil then table.insert(tokens,comment) end -- add token to mark the end of this line table.insert(tokens,{Token.EOL, ""}) return tokens end function get_lexer(file, parse_tokens) -- use the default line tokenizer if one is not provided if parse_tokens == nil then parse_tokens = default_parse_tokens end -- next/current line code local line_num = 0 local line = nil local next_line = io.lines(file) -- parse line tokens code local get_next_token = nil local next_tokens = function () local f, tokens, idx repeat line_num = line_num + 1 line = next_line() if line == nil then return nil end tokens = parse_tokens(line) until tokens ~= nil -- create get_next_toekn function from table iterator f, tokens, idx = ipairs(tokens) get_next_token = function() idx, token = f(tokens, idx) return token end return tokens end -- get first group of tokens if next_tokens() == nil then -- error reading file or empty file return nil end -- build lexer table. local lexer = { get_token = function () local token repeat token = get_next_token() if token == nil then -- get next group of tokens if next_tokens() == nil then -- end of file. return nil end end until token ~= nil return token end, get_line_number = function() return line_num end } return lexer end function print_tokens(file) local lexer = get_lexer(file) local num = -1 while true do local tok = lexer.get_token() if tok == nil then break end if num ~= lexer.get_line_number() then num = lexer.get_line_number() io.write("\n") io.write(string.format("%d: ",num)) end io.write(string.format("%s ",tok[2])) end io.write("\n") end