├── .github └── workflows │ └── test.yml ├── .gitignore ├── .travis.yml ├── CMakeLists.txt ├── LICENSE ├── README.md ├── README.zh.md ├── luaunit.lua ├── pb.c ├── pb.h ├── protoc.lua ├── rockspecs ├── lua-protobuf-0.5.3-1.rockspec └── lua-protobuf-scm-1.rockspec ├── serpent.lua └── test.lua /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: CI 4 | 5 | # Controls when the action will run. 6 | on: 7 | # Triggers the workflow on push or pull request events but only for the master branch 8 | push: 9 | branches: [ master ] 10 | pull_request: 11 | branches: [ master ] 12 | 13 | # Allows you to run this workflow manually from the Actions tab 14 | workflow_dispatch: 15 | 16 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 17 | jobs: 18 | test_ubuntu: 19 | if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')" 20 | runs-on: ${{ matrix.os }} 21 | env: 22 | ROCKSPEC: rockspecs/lua-protobuf-scm-1.rockspec 23 | strategy: 24 | matrix: 25 | luaVersion: ["5.1", "5.2", "5.3", "5.4", "luajit-openresty"] 26 | os: ["ubuntu-latest"] 27 | steps: 28 | - name: Install LCov 29 | run: | 30 | sudo apt install libperlio-gzip-perl libjson-perl libcapture-tiny-perl libdatetime-perl 31 | wget "https://github.com/linux-test-project/lcov/releases/download/v1.16/lcov-1.16.tar.gz" 32 | tar zxf "lcov-1.16.tar.gz" 33 | cd "lcov-1.16" 34 | sudo make install 35 | - uses: actions/checkout@master 36 | - name: Install Lua ${{ matrix.luaVersion }} 37 | uses: leafo/gh-actions-lua@master 38 | with: 39 | luaVersion: ${{ matrix.luaVersion }} 40 | - name: Install Luarocks 41 | uses: leafo/gh-actions-luarocks@master 42 | - name: Build 43 | run: | 44 | luarocks make $ROCKSPEC CFLAGS="-O3 -fPIC -Wall -Wextra --coverage" LIBFLAG="-shared --coverage" 45 | - name: Run Tests 46 | run: | 47 | lua test.lua 48 | - name: Run LCov 49 | run: | 50 | mkdir -p coverage 51 | lcov -c -d . -o coverage/lcov.info.all 52 | lcov -r coverage/lcov.info.all '/usr/*' -o coverage/lcov.info 53 | - name: Coveralls 54 | uses: coverallsapp/github-action@master 55 | with: 56 | github-token: ${{ secrets.GITHUB_TOKEN }} 57 | flag-name: run-${{ matrix.luaVersion }} 58 | parallel: true 59 | finish: 60 | needs: test_ubuntu 61 | runs-on: ubuntu-latest 62 | steps: 63 | - name: Coveralls Finished 64 | uses: coverallsapp/github-action@master 65 | with: 66 | github-token: ${{ secrets.github_token }} 67 | parallel-finished: true 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.dSYM 2 | *.dll 3 | *.dll.* 4 | *.exp 5 | *.gcda 6 | *.gcno 7 | *.gcov 8 | *.iobj 9 | *.ipdb 10 | *.lib 11 | *.natvis 12 | *.obj 13 | *.out 14 | *.pb 15 | *.pdb 16 | *.rock 17 | *.so 18 | *.o 19 | *.gc* 20 | google 21 | lua*/ 22 | out* 23 | test* 24 | test*.lua 25 | build/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: c 2 | os: linux 3 | dist: xenial 4 | 5 | env: 6 | global: 7 | - ROCKSPEC=rockspecs/lua-protobuf-scm-1.rockspec 8 | jobs: 9 | - LUA="lua 5.1" 10 | - LUA="lua 5.2" 11 | - LUA="lua 5.3" 12 | - LUA="lua 5.4" 13 | - LUA="luajit 2.0" 14 | - LUA="luajit 2.1" 15 | 16 | branches: 17 | only: 18 | - master 19 | - lua54 20 | 21 | before_install: 22 | - pip install --user urllib3[secure] cpp-coveralls 23 | - curl -O https://raw.githubusercontent.com/luarocks/hererocks/master/hererocks.py 24 | - python hererocks.py env --$LUA -rlatest 25 | - source env/bin/activate 26 | 27 | install: 28 | # - sudo luarocks make $ROCKSPEC CFLAGS="-O2 -fPIC -ftest-coverage -fprofile-arcs" LIBFLAG="-shared --coverage" 29 | - luarocks make $ROCKSPEC CFLAGS="-O3 -fPIC -Wall -Wextra --coverage" LIBFLAG="-shared --coverage" 30 | 31 | script: 32 | - lua test.lua 33 | # - lunit.sh test.lua 34 | 35 | after_success: 36 | - coveralls 37 | # - coveralls -b .. -r .. --dump c.report.json 38 | # - luacov-coveralls -j c.report.json -v 39 | 40 | notifications: 41 | email: 42 | on_success: change 43 | on_failure: always 44 | 45 | # vim: ft=yaml nu et sw=2 fdc=2 fdm=syntax 46 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project(lua-protobuf) 2 | cmake_minimum_required(VERSION 3.17) 3 | add_library(pb SHARED pb.c) 4 | 5 | # set(LUA_LIBRARIES ../lua) 6 | # set(LUA_INCLUDE_DIR ../lua) 7 | # include_directories(${LUA_INCLUDE_DIR}) 8 | 9 | set_target_properties(pb PROPERTIES PREFIX "") 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Xavier Wang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Google protobuf support for Lua 2 | 3 | [![Build Status](https://img.shields.io/github/actions/workflow/status/starwing/lua-protobuf/test.yml?branch=master)](https://github.com/starwing/lua-protobuf/actions?query=branch%3Amaster)[![Coverage Status](https://img.shields.io/coveralls/github/starwing/lua-protobuf)](https://coveralls.io/github/starwing/lua-protobuf?branch=master) 4 | 5 | English | [中文](https://github.com/starwing/lua-protobuf/blob/master/README.zh.md) 6 | 7 | --- 8 | 9 | This project offers a C module for Lua (5.1, 5.2, 5.3, 5.4 and LuaJIT) manipulating Google's protobuf protocol, both for version 2 and 3 syntax and semantics. It splits to the lower-level and the high-level parts for different goals. 10 | 11 | For converting between binary protobuf data with Lua tables, using `pb.load()` loads the compiled protobuf schema content (`*.pb` file) generated by Google protobuf's compiler named `protoc` and call `pb.encode()`/`pb.decode()`. 12 | 13 | Or use these modules to manipulate the raw wire format in lower-level way: 14 | 15 | - `pb.slice`: a wire format decoding module. 16 | - `pb.buffer`: a buffer implement that use to encode basic types into protobuf's wire format. It can be used to support streaming decode protobuf data. 17 | - `pb.conv`: a module converting integers in the protobuf wire format. 18 | - `pb.io`: a module access `stdin/stdout` or other files in binary mode. 19 | 20 | If you don't want to depend Google's protobuf compiler, `protoc.lua` is a pure Lua module translating text-based protobuf schema content into the `*.pb` binary format. 21 | 22 | ## Install 23 | 24 | To install, you could just use `luarocks`: 25 | 26 | ```shell 27 | luarocks install lua-protobuf 28 | ``` 29 | 30 | If you want to build it from source, just clone the repo and use luarocks: 31 | 32 | ```shell 33 | git clone https://github.com/starwing/lua-protobuf 34 | luarocks make rockspecs/lua-protobuf-scm-1.rockspec 35 | ``` 36 | 37 | If you don't have luarocks, use `hererocks` to install Lua and luarocks: 38 | 39 | ```shell 40 | pip install hererocks 41 | git clone https://github.com/starwing/lua-protobuf 42 | hererocks -j 2.0 -rlatest . 43 | bin/luarocks make lua-protobuf/rockspecs/lua-protobuf-scm-1.rockspec CFLAGS="-fPIC -Wall -Wextra" LIBFLAGS="-shared" 44 | cp protoc.lua pb.so .. 45 | ``` 46 | 47 | Or you can build it by hand, it only has a pure Lua module `protoc.lua` and a pair of C source: `pb.h` and `pb.c`. *Notice* that in order to build the `pb` C module, you need Lua header file and/or libary file installed. replace `$LUA_HEADERS` and `$LUA_LIBS` below to real install locations. 48 | 49 | To build it on macOS, use your favor compiler: 50 | 51 | ```shell 52 | gcc -O2 -shared -undefined dynamic_lookup -I "$LUA_HEADERS" pb.c -o pb.so 53 | ``` 54 | 55 | On Linux, use the nearly same command: 56 | 57 | ```shell 58 | gcc -O2 -shared -fPIC -I "$LUA_HEADERS" pb.c -o pb.so 59 | ``` 60 | 61 | On Windows, you could use MinGW or MSVC, create a `*.sln` project or build it on the command line (notice the `Lua_BUILD_AS_DLL` flag): 62 | 63 | ```shell 64 | cl /O2 /LD /Fepb.dll /I "$LUA_HEADERS" /DLUA_BUILD_AS_DLL pb.c "$LUA_LIBS" 65 | ``` 66 | 67 | ## Example 68 | 69 | ```lua 70 | local pb = require "pb" 71 | local protoc = require "protoc" 72 | 73 | -- load schema from text (just for demo, use protoc.new() in real world) 74 | assert(protoc:load [[ 75 | message Phone { 76 | optional string name = 1; 77 | optional int64 phonenumber = 2; 78 | } 79 | message Person { 80 | optional string name = 1; 81 | optional int32 age = 2; 82 | optional string address = 3; 83 | repeated Phone contacts = 4; 84 | } ]]) 85 | 86 | -- lua table data 87 | local data = { 88 | name = "ilse", 89 | age = 18, 90 | contacts = { 91 | { name = "alice", phonenumber = 12312341234 }, 92 | { name = "bob", phonenumber = 45645674567 } 93 | } 94 | } 95 | 96 | -- encode lua table data into binary format in lua string and return 97 | local bytes = assert(pb.encode("Person", data)) 98 | print(pb.tohex(bytes)) 99 | 100 | -- and decode the binary data back into lua table 101 | local data2 = assert(pb.decode("Person", bytes)) 102 | print(require "serpent".block(data2)) 103 | 104 | ``` 105 | 106 | ## Use case 107 | 108 | [![零境交错](https://img.tapimg.com/market/images/e59627dc9039ff22ba7d000b5c9fe7f6.jpg?imageView2/2/h/560/q/40/format/jpg/interlace/1/ignore-error/1)](http://djwk.qq.com) 109 | 110 | 111 | 112 | ## Usage 113 | 114 | ### `protoc` Module 115 | 116 | | Function | Returns | Descriptions | 117 | | ------------------- | ------------- | ---------------------------------------------------- | 118 | | `protoc.new()` | Proroc object | create a new compiler instance | 119 | | `protoc.reload()` | true | reload all google standard messages into `pb` module | 120 | | `p:parse(string)` | table | transform schema to `DescriptorProto` table | 121 | | `p:compile(string)` | string | transform schema to binary *.pb format data | 122 | | `p:load(string)` | true | load schema into `pb` module | 123 | | `p.loaded` | table | contains all parsed `DescriptorProto` table | 124 | | `p.unknown_import` | see below | handle schema import error | 125 | | `p.unknown_type` | see below | handle unknown type in schema | 126 | | `p.include_imports` | bool | auto load imported proto | 127 | 128 | To parse a text schema content, create a compiler instance first: 129 | 130 | ```lua 131 | local p = protoc.new() 132 | ``` 133 | 134 | Then, set some options to the compiler, e.g. the unknown handlers: 135 | 136 | ```lua 137 | -- set some hooks 138 | p.unknown_import = function(self, module_name) ... end 139 | p.unknown_type = function(self, type_name) ... end 140 | -- ... and options 141 | p.include_imports = true 142 | ``` 143 | 144 | The `unknown_import` and `unknown_type` handle could be `true`, string or a function. Seting it to `true` means all *non-exist* modules and types are given a default value without triggering an error; A string means a Lua pattern that indicates whether an unknown module or type should raise an error, e.g. 145 | 146 | ```lua 147 | p.unknown_type = "Foo.*" 148 | ``` 149 | 150 | means all types prefixed by `Foo` will be treat as existing type and do not trigger errors. 151 | 152 | If these are functions, the unknown type and module name will be passed to functions. For module handler, it should return a `DescriptorProto` Table produced by `p:load()` functions, for type handler, it should return a type name and type, such as `message` or `enum`, e.g. 153 | 154 | ```lua 155 | function p:unknown_import(name) 156 | -- if can not find "foo.proto", load "my_foo.proto" instead 157 | return p:parsefile("my_"..name) 158 | end 159 | 160 | function p:unknown_type(name) 161 | -- if cannot find "Type", treat it as ".MyType" and is a message type return ".My"..name, "message" 162 | end 163 | ``` 164 | 165 | After setting options, use `load()` or `compile()` or `parse()` function to get result. 166 | 167 | ### `pb` Module 168 | 169 | `pb` module has high-level routines to manipulate protobuf messages. 170 | 171 | In below table of functions, we have several types that have special means: 172 | 173 | - `type`: a string that indicates the protobuf message type, `".Foo"` means the type in a proto definition that has not `package` statement declared. `"foo.Foo"` means the type in a proto definition that declared `package foo;` 174 | 175 | - `data`: could be string, `pb.Slice` value or `pb.Buffer` value. 176 | 177 | - `iterator`: a function that can use in Lua `for in` statement, e.g. 178 | 179 | ```lua 180 | for name in pb.types() do 181 | print(name) 182 | end 183 | ``` 184 | 185 | **NOTICE**: Only `pb.load()` returns error on failure, *do check* the result it returns. Other routines raise a error when failure for convenience. 186 | 187 | | Function | Returns | Description | 188 | | ------------------------------ | --------------- | ------------------------------------------------------- | 189 | | `pb.clear()` | None | clear all types | 190 | | `pb.clear(type)` | None | delete specific type | 191 | | `pb.load(data)` | boolean,integer | load a binary schema data into `pb` module | 192 | | `pb.encode(type, table)` | string | encode a message table into binary form | 193 | | `pb.encode(type, table, b)` | buffer | encode a message table into binary form to buffer | 194 | | `pb.decode(type, data)` | table | decode a binary message into Lua table | 195 | | `pb.decode(type, data, table)` | table | decode a binary message into a given Lua table | 196 | | `pb.pack(type, ...)` | string | encode a message with flatten fields (ordered by field number) | 197 | | `pb.unpack(data, type, ...)` | values... | decode a message with flatten fields (just like above) | 198 | | `pb.types()` | iterator | iterate all types in `pb` module | 199 | | `pb.type(type)` | see below | return informations for specific type | 200 | | `pb.fields(type)` | iterator | iterate all fields in a message | 201 | | `pb.field(type, string)` | see below | return informations for specific field of type | 202 | | `pb.typefmt(type)` | string | transform type name of field into pack/unpack formatter | 203 | | `pb.enum(type, string)` | number | get the value of a enum by name | 204 | | `pb.enum(type, number)` | string | get the name of a enum by value | 205 | | `pb.defaults(type[, table/nil])` | table | get the default table of type | 206 | | `pb.hook(type[, function])` | function | get or set hook functions | 207 | | `pb.encode_hook(type[, function])` | function | get or set encode hook functions | 208 | | `pb.option(string)` | string | set options to decoder/encoder | 209 | | `pb.state()` | `pb.State` | retrieve current pb state | 210 | | `pb.state(newstate \| nil)` | `pb.State` | set new pb state and retrieve the old one | 211 | | `pb.tohex(string)` | string | encode string as hexadigits, for debug purposes | 212 | 213 | #### Schema loading 214 | 215 | `pb.load()` accepts the schema binary data and returns a boolean indicates the result of loading, success or failure, and a offset reading in schema so far that is useful to figure out the reason of failure. 216 | 217 | #### Type mapping 218 | 219 | | Protobuf Types | Lua Types | 220 | | -------------------------------------------------- | ------------------------------------------------------------ | 221 | | `double`, `float` | `number` | 222 | | `int32`, `uint32`, `fixed32`, `sfixed32`, `sint32` | `number` or `integer` in Lua 5.3+ | 223 | | `int64`, `uint64`, `fixed64`, `sfixed64`, `sint64` | `number` or `"#"` prefixed `string` or `integer` in Lua 5.3+ | 224 | | `bool` | `boolean` | 225 | | `string`, `bytes` | `string` | 226 | | `message` | `table` | 227 | | `enum` | `string` or `number` | 228 | 229 | #### Type Information 230 | 231 | Using `pb.(type|field)[s]()` functions retrieve type information for loaded messages. 232 | 233 | `pb.type()` returns multiple informations for specified type: 234 | 235 | - name : the full qualifier name of type, e.g. ".package.TypeName" 236 | - basename: the type name without package prefix, e.g. "TypeName" 237 | - `"map"` | `"enum"` | `"message"`: whether the type is a map_entry type, enum type or message type. 238 | 239 | `pb.types()` returns a iterators, behavior like call `pb.type()` on every types of all messages. 240 | 241 | ```lua 242 | print(pb.type "MyType") 243 | 244 | -- list all types that loaded into pb 245 | for name, basename, type in pb.types() do 246 | print(name, basename, type) 247 | end 248 | ``` 249 | 250 | `pb.field()` returns information of the specified field for one type: 251 | 252 | - name: the name of the field 253 | - number: number of field in the schema 254 | - type: field type 255 | - default value: if no default value, nil 256 | - `"packed"`|`"repeated"`| `"optional"`: label of the field, optional or repeated, required is not supported 257 | - [oneof_name, oneof_index]: if this is a `oneof` field, this is the `oneof` name and index 258 | 259 | And `pb.fields()` iterates all fields in a message: 260 | 261 | ```lua 262 | print(pb.field("MyType", "the_first_field")) 263 | 264 | -- notice that you needn't receive all return values from iterator 265 | for name, number, type in pb.fields "MyType" do 266 | print(name, number, type) 267 | end 268 | ``` 269 | 270 | `pb.enum()` maps from enum name and value: 271 | 272 | ```lua 273 | protoc:load [[ 274 | enum Color { Red = 1; Green = 2; Blue = 3 } 275 | ]] 276 | print(pb.enum("Color", "Red")) --> 1 277 | print(pb.enum("Color", 2)) --> "Green" 278 | ``` 279 | 280 | #### Default Values 281 | 282 | Using `pb.defaults()` to get or set a table with all default values from a message. this table will be used as the metatable of the corresponding decoded message table when setting `use_default_metatable` option. 283 | 284 | You could also call `pb.defaults` with `"*map"` or `"*array"` to get the default metatable for map and array when decoding a message. These settings will bypass `use_default_metatable` option. 285 | 286 | To clear a default metatable, just pass `nil` as second argument to `pb.defaults()`. 287 | 288 | ```lua 289 | check_load [[ 290 | message TestDefault { 291 | optional int32 defaulted_int = 10 [ default = 777 ]; 292 | optional bool defaulted_bool = 11 [ default = true ]; 293 | optional string defaulted_str = 12 [ default = "foo" ]; 294 | optional float defaulted_num = 13 [ default = 0.125 ]; 295 | } ]] 296 | print(require "serpent".block(pb.defaults "TestDefault")) 297 | -- output: 298 | -- { 299 | -- defaulted_bool = true, 300 | -- defaulted_int = 777, 301 | -- defaulted_num = 0.125, 302 | -- defaulted_str = "foo" 303 | -- } --[[table: 0x7f8c1e52b050]] 304 | 305 | ``` 306 | 307 | #### Hooks 308 | 309 | If set `pb.option "enable_hooks"`, the hook function will be enabled. you could use `pb.hook()` and `pb.encode_hook` to set or get a decode or encode hook function, respectively: call it with type name directly get current setted hook; call it with two arguments to set a hook; and call it with `nil` as the second argument to remove the hook. in all case, the original one will be returned. 310 | 311 | After the hook function setted and hook enabled, the decode function will be called *after* a message get decoded and encode functions will be called *before* the message is encoded. So you could get all values in the table passed to hook function. That's the only argument of hook. 312 | 313 | If you need type name in hook functions, use this helper: 314 | 315 | ```lua 316 | local function make_hook(name, func) 317 | return pb.hook(name, function(t) 318 | return func(name, t) 319 | end) 320 | end 321 | ``` 322 | 323 | You could enable “encode hooking” function by call `pb.option “enable_enchooks”`. encode hooks will be called **before** any message or enum value been encoded. The hook caller only accepts one value, the comming encoding value, and can returns a new value that instead to be encoded. The hook could returns `nil` or just not return anything to makes `pb.encode` just encode the original value. 324 | 325 | You could setup encode hooks by `pb.encode_hook()` routine, it’s just as same as `pb.hook()`, but for getting/setting the encode hooks. 326 | 327 | #### Options 328 | 329 | Setting options to change the behavior of other routines. 330 | These options are supported currently: 331 | 332 | | Option | Description | 333 | | ----------------------- | ------------------------------------------------------------ | 334 | | `enum_as_name` | set value to enum name when decode a enum **(default)** | 335 | | `enum_as_value` | set value to enum value when decode a enum | 336 | | `int64_as_number` | set value to integer when it fit into uint32, otherwise return a number **(default)** | 337 | | `int64_as_string` | same as above, but return a string instead | 338 | | `int64_as_hexstring` | same as above, but return a hexadigit string instead | 339 | | `auto_default_values` | act as `use_default_values` for proto3 and act as `no_default_values` for the others **(default)** | 340 | | `no_default_values` | do not default values for decoded message table | 341 | | `use_default_values` | set default values by copy values from default table before decode | 342 | | `use_default_metatable` | set default values by set table from `pb.default()` as the metatable | 343 | | `enable_hooks` | `pb.decode` will call `pb.hooks()` hook functions | 344 | | `disable_hooks` | `pb.decode` do not call hooks **(default)** | 345 | | `enable_enchooks` | `pb.encode` will call `pb.encode_hook()` hook functions | 346 | | `disable_enchooks` | do not call hooks when encoding messages **(default)** | 347 | | `encode_default_values` | `pb.encode` encode the default value into the wire format | 348 | | `no_encode_default_values` | do not encode default values **(default)** | 349 | | `decode_default_array` | work with `no_default_values`,decode null to empty table for array | 350 | | `no_decode_default_array` | work with `no_default_values`,decode null to nil for array **(default)** | 351 | | `encode_order` | `pb.encode` encode the messages with field number order | 352 | | `no_encode_order` | do not have guarantees about encode orders **(default)** | 353 | | `decode_default_message` | `pb.decode` decode the empty messages as a empty table | 354 | | `no_decode_default_message` | `pb.decode` decode the empty messages as `nil` **(default)** | 355 | 356 | *Note*: The string returned by `int64_as_string` or `int64_as_hexstring` will prefix a `'#'` character. Because Lua may convert between string with number, prefix a `'#'` makes Lua return the string as-is. 357 | 358 | all routines in all module accepts `'#'` prefix `string`/`hex string` as arguments regardless of the option setting. 359 | 360 | #### Multiple State 361 | 362 | `pb` module support multiple states. A state is a database that contains all type information of registered messages. You can retrieve current state by `pb.state()`, or set new state by `pb.state(newstate)`. 363 | 364 | Use `pb.state(nil)` to discard current state, but not to set a new one (the following routines call that use the state will create a new default state automatedly). Use `pb.state()` to retrieve current state without setting a new one. e.g. 365 | 366 | ```lua 367 | local old = pb.state(nil) 368 | -- if you use protoc.lua, call protoc.reload() here. 369 | assert(pb.load(...)) 370 | -- do someting ... 371 | pb.state(old) 372 | ``` 373 | 374 | Notice that if you use `protoc.lua` module, it will register some message to the state, so you should call `proto.reload()` after setting a new state. 375 | 376 | 377 | 378 | ### `pb.io` Module 379 | 380 | `pb.io` module reads binary data from a file or `stdin`/`stdout`, `pb.io.read()` reads binary data from a file, or `stdin` if no file name given as the first parameter. 381 | 382 | `pb.io.write()` and `pb.io.dump()` are same as Lua's `io.write()` except they write binary data. the former writes data to `stdout`, and the latter writes data to a file specified by the first parameter as the file name. 383 | 384 | All these functions return a true value when success, and return `nil, errmsg` when an error occurs. 385 | 386 | | Function | Returns | Description | 387 | | ---------------------- | ------- | ----------------------------------- | 388 | | `io.read()` | string | read all binary data from `stdin` | 389 | | `io.read(string)` | string | read all binary data from file name | 390 | | `io.write(...)` | true | write binary data to `stdout` | 391 | | `io.dump(string, ...)` | string | write binary data to file name | 392 | 393 | 394 | 395 | ### `pb.conv` Module 396 | 397 | `pb.conv` provide functions to convert between numbers. 398 | 399 | | Encode Function | Decode Function | 400 | | ---------------------- | ---------------------- | 401 | | `conv.encode_int32()` | `conv.decode_int32()` | 402 | | `conv.encode_uint32()` | `conv.decode_uint32()` | 403 | | `conv.encode_sint32()` | `conv.decode_sint32()` | 404 | | `conv.encode_sint64()` | `conv.decode_sint64()` | 405 | | `conv.encode_float()` | `conv.decode_float()` | 406 | | `conv.encode_double()` | `conv.decode_double()` | 407 | 408 | 409 | 410 | ### `pb.slice` Module 411 | 412 | Slice object parse binary protobuf data in a low-level way. Use `slice.new()` to create a slice object, with the optional offset `i` and `j` to access a subpart of the original data (named a *view*). 413 | 414 | As protobuf usually nest sub message with in a range of slice, a slice object has a stack itself to support this. Calling `s:enter(i, j)` saves current position and enters next level with the optional offset `i` and `j` just as `slice.new()`. calling `s:leave()` restore the prior view. `s:level()` returns the current level, and `s:level(n)` returns the current position, the start and the end position information of the `n`th level. calling `s:enter()` without parameter will read a length delimited type value from the slice and enter the view in reading value. Using `#a` to get the count of bytes remains in current view. 415 | 416 | ```lua 417 | local s = slice.new("") 418 | local tag = s:unpack "v" 419 | if tag%8 == 2 then -- tag has a type of string/bytes? maybe it's a sub-message. 420 | s:enter() -- read following bytes value, and enter the view of bytes value. 421 | -- do something with bytes value, e.g. reads a lot of fixed32 integers from bytes. 422 | local t = {} 423 | while #s > 0 do 424 | t[#t+1] = s:unpack "d" 425 | end 426 | s:leave() -- after done, leave bytes value and ready to read next value. 427 | end 428 | ``` 429 | 430 | To read values from slice, use `slice.unpack()`, it use a format string to control how to read into a slice as below table (same format character are also used in `buffer.pack()`). Notice that you can use `pb.typefmt()` to convert between format and protobuf type names (returned from `pb.field()`). 431 | 432 | | Format | Description | 433 | | ------ | ------------------------------------------------------------ | 434 | | v | variable Int value | 435 | | d | 4 bytes fixed32 value | 436 | | q | 8 bytes fixed64 value | 437 | | s | length delimited value, usually a `string`, `bytes` or `message` in protobuf. | 438 | | c | receive a extra number parameter `count` after the format, and reads `count` bytes in slice. | 439 | | b | variable int value as a Lua `boolean` value. | 440 | | f | 4 bytes `fixed32` value as floating point `number` value. | 441 | | F | 8 bytes `fixed64` value as floating point `number` value. | 442 | | i | variable int value as signed int value, i.e. `int32` | 443 | | j | variable int value as zig-zad encoded signed int value, i.e.`sint32` | 444 | | u | variable int value as unsigned int value, i.e. `uint32` | 445 | | x | 4 bytes fixed32 value as unsigned fixed32 value, i.e.`fixed32` | 446 | | y | 4 bytes fixed32 value as signed fixed32 value, i.e. `sfixed32` | 447 | | I | variable int value as signed int value, i.e.`int64` | 448 | | J | variable int value as zig-zad encoded signed int value, i.e. `sint64` | 449 | | U | variable int value and treat it as `uint64` | 450 | | X | 8 bytes fixed64 value as unsigned fixed64 value, i.e. `fixed64` | 451 | | Y | 8 bytes fixed64 value as signed fixed64 value, i.e. `sfixed64` | 452 | 453 | And extra format can be used to control the read cursor in one `slice.unpack()` process: 454 | 455 | | Format | Description | 456 | | ------ | ------------------------------------------------------------ | 457 | | @ | returns current cursor position in the slice, related with the beginning of the current view. | 458 | | * | set the current cursor position to the extra parameter after format string. | 459 | | + | set the relate cursor position, i.e. add the extra parameter to the current position. | 460 | 461 | e.g. If you want to read a `varint` value twice, you can write it as: 462 | 463 | ```lua 464 | local v1, v2 = s:unpack("v*v", 1) 465 | -- v: reads a `varint` value 466 | -- *: receive the second parameter 1 and set it to the current cursor position, i.e. restore the cursor to the head of the view 467 | -- v: reads the first `varint` value again 468 | ``` 469 | 470 | All routines in `pb.slice` module: 471 | 472 | | Function | Returns | Description | 473 | | ------------------------- | ------------ | ------------------------------------------------------------ | 474 | | `slice.new(data[,i[,j]])` | Slice object | create a new slice object | 475 | | `s:delete()` | none | same as `s:reset()`, free it's content | 476 | | `tostring(s)` | string | return the string repr of the object | 477 | | `#s` | number | returns the count of bytes can read in current view | 478 | | `s:result([i[, j]])` | String | return the remaining bytes in current view | 479 | | `s:reset([...])` | self | reset object to another data | 480 | | `s:level()` | number | returns the count of stored state | 481 | | `s:level(number)` | p, i, j | returns the informations of the `n`th stored state | 482 | | `s:enter()` | self | reads a bytes value, and enter it's view | 483 | | `s:enter(i[, j])` | self | enter a view start at `i` and ends at `j`, includes | 484 | | `s:leave([number])` | self, n | leave the number count of level (default 1) and return current level | 485 | | `s:unpack(fmt, ...)` | values... | reads values of current view from slice | 486 | 487 | 488 | 489 | ### `pb.buffer` Module 490 | 491 | Buffer module used to construct a protobuf data format stream in a low-level way. It's just a bytes data buffer. using `buffer.pack()` to append values to the buffer, and `buffer.result()` to get the encoded raw data, or `buffer.tohex()` to get the human-readable hex digit value of data. 492 | 493 | `buffer.pack()` use the same format syntax with `slice.unpack()`, and support `'()'` format means the inner value will be encoded as a length delimited value, i.e. a message value encoded format. 494 | 495 | parenthesis can be nested. 496 | 497 | e.g. 498 | 499 | ```lua 500 | b:pack("(vvv)", 1, 2, 3) -- get a bytes value that contains three varint value. 501 | ``` 502 | 503 | 504 | 505 | `buffer.pack()` also support '#' format, it means prepends a length into the buffer. 506 | 507 | e.g. 508 | 509 | ```lua 510 | b:pack("#", 5) -- prepends a varint length #b-5+1 at offset 5 511 | ``` 512 | 513 | All routines in `pb.buffer` module: 514 | 515 | | Function | Returns | Description | 516 | | ------------------- | ------------- | ------------------------------------------------------------ | 517 | | `buffer.new([...])` | Buffer object | create a new buffer object, extra args will passed to `b:reset()` | 518 | | `b:delete()` | none | same as `b:reset()`, free it's content | 519 | | `tostring(b)` | string | returns the string repr of the object | 520 | | `#b` | number | returns the encoded count of bytes in buffer | 521 | | `b:reset()` | self | reset to a empty buffer | 522 | | `b:reset([...])` | self | resets the buffer and set its content as the concat of it's args | 523 | | `b:tohex([i[, j]])` | string | return the string of hexadigit represent of the data, `i` and `j` are ranges in encoded data, includes. Omit it means the whole range | 524 | | `b:result([i[,j]])` | string | return the raw data, `i` and `j` are ranges in encoded data, includes,. Omit it means the whole range | 525 | | `b:pack(fmt, ...)` | self | encode the values passed to `b:pack()`, use `fmt` to indicate how to encode value | 526 | 527 | 528 | 529 | -------------------------------------------------------------------------------- /README.zh.md: -------------------------------------------------------------------------------- 1 | # 在Lua中操作Google protobuf格式数据 2 | 3 | [![Build Status](https://img.shields.io/github/actions/workflow/status/starwing/lua-protobuf/test.yml?branch=master)](https://github.com/starwing/lua-protobuf/actions?query=branch%3Amaster)[![Coverage Status](https://img.shields.io/coveralls/github/starwing/lua-protobuf)](https://coveralls.io/github/starwing/lua-protobuf?branch=master) 4 | 5 | [English](https://github.com/starwing/lua-protobuf/blob/master/README.md) | 中文 6 | 7 | --- 8 | 9 | Urho3d集成说明:https://note.youdao.com/ynoteshare1/index.html?id=20d06649fab669371140256abd7a362b&type=note 10 | 11 | Unreal SLua集成:https://github.com/zengdelang/slua-unreal-pb 12 | 13 | Unreal UnLua集成:https://github.com/hxhb/unlua-pb 14 | 15 | ToLua集成说明:[链接](http://changxianjie.gitee.io/unitypartner/2019/10/01/tolua中使用protobuf3—集成lua-protobuf/) 16 | 17 | xlua集成:[链接](https://github.com/91Act/build_xlua_with_libs) 18 | 19 | QQ群:485016061 [![lua-protobuf1交流群](https://pub.idqqimg.com/wpa/images/group.png)](https://shang.qq.com/wpa/qunwpa?idkey=d7e2973604a723c4f77d0a837df39be26e15be2c2ec29d5ebfdb64f94e74e6ae) 20 | 21 | 本项目提供在 Lua 全版本(5.1+、LuaJIT)下的protobuf 2/3 版本支持。提供了高级的消息编码/解码接口以及底层的protobuf wireformat 二进制数据操作接口。 22 | 23 | 使用高级接口,你需要通过 `pb.load()` 接口导入 protobuf 的消息描述文件(schema文件,.proto后缀名)的二进制版本(本质上是schema文件通过官方的 `FileDescriptorSet` 编码得到的二进制pb消息),导入的信息被存储在称为“state”的内存数据库中供消息编码/解码使用。你也可以通过`pb`模块提供的一系列接口来读取这个数据库里的内容。 24 | 25 | 要使用底层接口,你需要使用下面几个库提供的功能: 26 | 27 | - `pb.slice`:读取二进制的wireformat信息。 28 | - `pb.buffer`:写入二进制wireformat信息。 29 | - `pb.conv`:在Lua的数字类型和protobuf提供的一众数字数据类型之间转换。 30 | - `pb.io`:用于将`pb`模块用于工具当中使用:通过标准输入输出流读取写入二进制消息。 31 | 32 | 另外,为了得到schema文件的二进制版本(一般后缀名是.pb的文件),你需要官方protobuf项目提供的schema编译器二进制`protoc.exe`工具,如果在你的平台下获得这个工具太麻烦,或者你希望能有一个小体积的protobuf编译工具,你可以使用项目自带的另一个独立的纯 Lua库:`protoc.lua`文件。该文件提供了纯Lua实现的schema文件编译器,并集成了通过调用`pb.load()`载入编译结果的方便接口。但是要注意因为这个库是纯Lua实现的,它编译的速度会非常慢,如果你的schema文件太大或者编译的时候遇到了性能瓶颈。还是推荐你通过`protoc.exe`或者在开发时利用`pb`库自己写脚本将schema编译成.pb文件供生产环境使用。 33 | 34 | ## 安装 35 | 36 | **注意**:`lua-prootbuf`毕竟是个纯C的Lua库,而Lua库的编译安装是**有门槛**的。如果遇到了问题,建议询问**有Lua的C模块使用经验**的人,或者参阅《Lua程序设计》里的相关内容,预先学习相关知识。 37 | 38 | 另外,Lua的C模块是通用的,任何使用Lua的环境下都可以使用,然而XLua等C#环境通常有自己的一套规则,需要一些额外的集成操作。请确认**自己对这些环境集成C模块足够了解**,或者**咨询足够了解的人**获得帮助。这里只提供通用C模块的安装方法。 39 | 40 | 最简单的安装方法是使用Lua生态的包管理器`luarocks`进行安装(注意,这样**依然需要你的电脑上装有C编译器**,如果安装失败,你应该首先检查你的`luarocks`能否正常工作,即,能否正常安装其他Lua C模块): 41 | 42 | ```shell 43 | luarocks install lua-protobuf 44 | ``` 45 | 46 | 你也可以使用`luarocks`从源码安装(这样装的版本会更新一些): 47 | 48 | ```shell 49 | git clone https://github.com/starwing/lua-protobuf 50 | luarocks make rockspecs/lua-protobuf-scm-1.rockspec 51 | ``` 52 | 53 | 如果你没有/不会配置`luarocks`,有一个Python写的方便脚本可以在你的电脑上安装`luarocks`——还是需要你有能正常运行的C编译器——当然,也需要你有Python。 54 | 55 | ```shell 56 | pip install hererocks 57 | git clone https://github.com/starwing/lua-protobuf 58 | hererocks -j 2.0 -rlatest . 59 | bin/luarocks make lua-protobuf/rockspecs/lua-protobuf-scm-1.rockspec CFLAGS="-fPIC -Wall -Wextra" LIBFLAGS="-shared" 60 | cp protoc.lua pb.so .. 61 | ``` 62 | 63 | 如果你想尝试Hard模式的话,自己手动编译也是可行的。 64 | 65 | 这是macOS的编译命令行: 66 | 67 | ```shell 68 | gcc -O2 -shared -undefined dynamic_lookup pb.c -o pb.so 69 | ``` 70 | 71 | Linux的: 72 | 73 | ```shell 74 | gcc -O2 -shared -fPIC pb.c -o pb.so 75 | ``` 76 | 77 | Windows的(注意那个Lua_BUILD_AS_DLL!在Windows上必须带这个预处理宏编译): 78 | 79 | ```shell 80 | cl /O2 /LD /Fepb.dll /I Lua53\include /DLUA_BUILD_AS_DLL pb.c Lua53\lib\lua53.lib 81 | ``` 82 | 83 | ## 样例 84 | 85 | ```lua 86 | local pb = require "pb" 87 | local protoc = require "protoc" 88 | 89 | -- 直接载入schema (这么写只是方便, 生产环境推荐使用 protoc.new() 接口) 90 | assert(protoc:load [[ 91 | message Phone { 92 | optional string name = 1; 93 | optional int64 phonenumber = 2; 94 | } 95 | message Person { 96 | optional string name = 1; 97 | optional int32 age = 2; 98 | optional string address = 3; 99 | repeated Phone contacts = 4; 100 | } ]]) 101 | 102 | -- lua 表数据 103 | local data = { 104 | name = "ilse", 105 | age = 18, 106 | contacts = { 107 | { name = "alice", phonenumber = 12312341234 }, 108 | { name = "bob", phonenumber = 45645674567 } 109 | } 110 | } 111 | 112 | -- 将Lua表编码为二进制数据 113 | local bytes = assert(pb.encode("Person", data)) 114 | print(pb.tohex(bytes)) 115 | 116 | -- 再解码回Lua表 117 | local data2 = assert(pb.decode("Person", bytes)) 118 | print(require "serpent".block(data2)) 119 | 120 | ``` 121 | 122 | ## 使用案例 123 | 124 | [![零境交错](https://img.tapimg.com/market/images/e59627dc9039ff22ba7d000b5c9fe7f6.jpg?imageView2/2/h/560/q/40/format/jpg/interlace/1/ignore-error/1)](http://djwk.qq.com) 125 | 126 | 127 | 128 | ## 接口文档 129 | 130 | 请注意接口文档有些是`.`有些是`:`!`.`代表这是静态函数,直接调用即可,`:`代表这是一个方法,需要在一个对象上调用。 131 | 132 | ### `protoc` 模块 133 | 134 | | 接口 | 返回 | 描述 | 135 | | --------------- | ------- | --------------------------------------------- | 136 | | `protoc.new()` | 编译器对象 | 创建一个新的编译器对象 | 137 | | `protoc.reload()` | `true` | 重新载入谷歌标准的schema信息(编译需要用到) | 138 | | `p:parse(string[, filename])` | table | 将文本schema信息转换成 `DescriptorProto` 消息的Lua表 | 139 | | `p:compile(string[, filename])` | string | 将文本schema信息转换成二进制.pb文件数据 | 140 | | `p:load(string[, filename])` | `true` | 将文本schema信息转换后,调用`pb.load()`载入内存数据库 | 141 | | `p.loaded` | table | 一个包含了所有已载入的 `DescriptorProto` 表的缓存表 | 142 | | `p.unknown_import` | 详情见下 | 处理schema里`import`语句找不到文件的回调 | 143 | | `p.unknown_type` | 详情见下 | 处理schema里未知类型的回调 | 144 | | `p.include_imports` | bool | 编译结果中包含所有`import`的文件 | 145 | 146 | 要编译一个文本的schema信息,首先,生成一个编译器实例。一个编译器实例会记住你用它编译的每一个scehma文件,从而能够正确处理schema之间的import关系: 147 | 148 | ```lua 149 | local p = protoc.new() 150 | ``` 151 | 152 | 生成了编译器实例之后,可以给编译器实例设置一些选项或者回调: 153 | 154 | ```lua 155 | -- 设置回调…… 156 | p.unknown_import = function(self, module_name) ... end 157 | p.unknown_type = function(self, type_name) ... end 158 | -- ……和选项 159 | p.include_imports = true 160 | ``` 161 | 162 | `unknown_import`和`unknown_type`可以被设置成多种类型的值:如果被设置成`true`,则所有不存在的模块或者消息类型会自动生成一个默认值(空模块/空消息)而不会报错。`pb.load()`会自动处理空消息的合并,因此这样载入信息也不会出错。如果设置一个字符串值,那么这个字符串是一个Lua的正则表达式,满足正则表达式的模块或者消息类型会被设置成默认值,而不满足的则会报错: 163 | 164 | ```lua 165 | p.unknown_type = "Foo.*" 166 | ``` 167 | 168 | 上面的选项意味着所有`Foo`包里的消息会被当作好像已经载入了,即使没找到也不会报错。 169 | 170 | 如果这些回调被设置成一个函数,这个函数会在找不到模块/消息的时候被调用。调用的时候会传入找不到的模块/消息的名字,你需要返回一个`DescriptorProto`数据表(对模块而言),或者一个类型的名字和这个类型的实际分类,比如说`message`或者`enum`,如下所示: 171 | 172 | ```lua 173 | function p:unknown_import(name) 174 | -- 如果找不到 "foo.proto" 文件而调用了这个函数,那就自己手动载入 "my_foo.proto" 文件并返回信息 175 | return p:parsefile("my_"..name) 176 | end 177 | 178 | function p:unknown_type(name) 179 | -- 如果找不到 "Type", 那就把它当 "MyType" 消息编译好了,注意前面那个“.”,那是包名。 180 | return ".My"..name, "message" 181 | end 182 | ``` 183 | 184 | 设置好这些选项或者回调以后,使用`load()`或`compile()`或`parse()`来按照你的需求得到想要的结果。这些函数都需要直接传入scehma的文本内容作为参数,可以可选地多传入当前schema对应的文件名,用于方便schema之间的`import`指令找到对应的文件,但是除非设置了`include_imports`,否则`import`指令即使找到了对应的文件也不会编译/加载对应文件,只是会检查类型引入是否正确。即使没有设置`include_imports`,也可以手动按照拓扑顺序依次load对应文件从而加载所有schema。 185 | 186 | ### `pb` 模块 187 | 188 | `pb`模块提供了编码/解码信息的高级接口、内存schema数据库的载入和读取接口,以及其他的一些方便的工具函数。 189 | 190 | 下面的表格里给出了`pb`模块里所有的函数,注意表格中的中“返回”一栏中的一些特殊返回值,这些返回值有一些约定俗成的含义: 191 | 192 | - `type`:这个返回值代表返回的是一个代表类型的字符串。`".Foo"`代表没有包名的proto文件里的Foo消息,而`"foo.Foo"`代表写了`package foo;`包名的proto文件里的Foo消息。 193 | 194 | - `data`:一个字符串,或者`pb.Slice`对象或者`pb.Buffer`对象,反正是个能表示二进制数据的东西 195 | 196 | - `iterator`:返回一个能在for in语句里使用的迭代器对象,比如说: 197 | 198 | ```lua 199 | for name in pb.types() do 200 | print(name) 201 | end 202 | ``` 203 | 204 | **注意**:只有`pb.load()`通过返回值返回是否出错,你要用`assert(pb.load(...))`去调用这个函数!其他的函数会直接扔一个错误异常,不需要你对返回值调用`assert()`函数。 205 | 206 | | 接口 | 返回 | 描述 | 207 | | --------------- | ------- | --------------------------------------------- | 208 | | `pb.clear()` | None | 清除所有类型 | 209 | | `pb.clear(type)` | None | 清除特定类型 | 210 | | `pb.load(data)` | boolean,integer | 将一个二进制schema信息载入内存数据库 | 211 | | `pb.encode(type, table)` | string | 将table按照type消息类型进行编码 | 212 | | `pb.encode(type, table, b)` | buffer | 同上,但是编码进额外提供的buffer对象里并返回 | 213 | | `pb.decode(type, data)` | table | 将二进制data按照type消息类型解码为一个表 | 214 | | `pb.decode(type, data, table)` | table | 同上,但是解码到你提供的表里 | 215 | | `pb.pack(type, ...)` | string | 编码展开后的消息(后续参数按number顺序提供) | 216 | | `pb.unpack(data, fmt, ...)` | values... | 解码展开后的消息(同上) | 217 | | `pb.types()` | iterator | 遍历内存数据库里所有的消息类型,返回具体信息 | 218 | | `pb.type(type)` | 详情见下 | 返回内存数据库特定消息类型的具体信息 | 219 | | `pb.fields(type)` | iterator | 遍历特定消息里所有的域,返回具体信息 | 220 | | `pb.field(type, string)` | 详情见下 | 返回特定消息里特定域的具体信息 | 221 | | `pb.field(type, number)` | 详情见下 | 返回特定消息里特定域的具体信息 | 222 | | `pb.typefmt(type)` | string | 得到 protobuf 数据类型名对应的 pack/unpack 的格式字符串 | 223 | | `pb.enum(type, string)` | number | 提供特定枚举里的名字,返回枚举数字 | 224 | | `pb.enum(type, number)` | string | 提供特定枚举里的数字,返回枚举名字 | 225 | | `pb.defaults(type[, table|nil])` | table | 获得或设置特定消息类型的默认表 | 226 | | `pb.hook(type[, function])` | function | 获得或设置特定消息类型的解码钩子 | 227 | | `pb.encode_hook(type[, function])` | function | 获得或设置特定消息类型的编码钩子 | 228 | | `pb.option(string)` | string | 设置编码或解码的具体选项 | 229 | | `pb.state()` | `pb.State` | 返回当前的内存数据库 | 230 | | `pb.state(newstate \| nil)` | `pb.State` | 设置或删除当前的内存数据库,返回旧的内存数据库 | 231 | | `pb.tohex(string)` | string | 将string(通常是二进制数据)编码成16进制串用于显示,通常用于调试的目的。 | 232 | 233 | #### 内存数据库载入 Schema 信息 234 | 235 | `pb.load()` 接受一个二进制的schema数据,并将其载入到内存数据库中。如果载入成功则返回`true`,否则返回`false`,无论成功与否,都会返回读取的二进制数据的字节数。如果载入失败,你可以检查在这个字节位置周围是否有数据错误的情况,比如被`NUL`字符截断等等的问题。 236 | 237 | 二进制流中是什么样的schema,就会载入什么样的schema。通常只能载入一个文件。如果需要同时载入多个文件(比如包括import后的文件,或者多个不相干文件),可以通过在使用`protoc.exe`或者`protoc.lua`编译二进制schema的时候编译多个文件,或者使用`include_imports`在二进制数据中包含多个文件的内容实现。注意根据protobuf的特性,直接将多个schema二进制数据连接在一起载入也是可行的。 238 | 239 | 240 | #### 类型映射 241 | 242 | | Protobuf 类型 | Lua 类型 | 243 | | ------------------------------------------------- | ----------------------------------------------------------- | 244 | | `double`, `float` | `number` | 245 | | `int32`, `uint32`, `fixed32`, `sfixed32`, `sint32` | `number` 或 `integer` (Lua 5.3+) | 246 | | `int64`, `uint64`, `fixed64`, `sfixed64`, `sint64` | `number` 或 `"#"` 打头的 `string` 或 `integer` (Lua 5.3+) | 247 | | `bool` | `boolean` | 248 | | `string`, `bytes` | `string` | 249 | | `message` | `table` | 250 | | `enum` | `string` 或 `number` | 251 | 252 | #### 内存数据库信息获取 253 | 254 | 可以使用`pb.type()`、`pb.types()`、`pb.field()`、`pb.fields()`系列函数获取内存数据库内的消息类型信息。 255 | 256 | 内存数据库存储了可以编码/解码的所有的消息类型信息,如果内存数据库中无法查询到对应信息,则编码/解码可能失败。使用*限定后的消息类型*名字就可以获取对应的类型信息。比如`foo` 包里的`Foo`消息的*限定消息类型名字*是`".foo.Foo"`,如果没有包名,则直接在消息名前面加`"."`,比如`".Foo"`就是没有包名的`Foo`消息的限定名称。 257 | 258 | 通过调用`pb.type()`,你可以获得下面的信息: 259 | 260 | - name:即限定的消息类型名称(如`".package.TypeName"`) 261 | - basename:即去除了包名的消息类型名称(如`"TypeName"`) 262 | - type:`"map"` | `"enum"` | `"message"`,消息的实际类型——`MapEntry`类型,或者枚举,或者消息。 263 | 264 | `pb.types()`返回了一个迭代器,就好像对内存数据库里存储的每个类型调用 `pb.type()`一样: 265 | 266 | ```lua 267 | -- 打印出 MyType 消息的详细信息 268 | print(pb.type "MyType") 269 | 270 | -- 列出内存数据库里存储的所有消息类型的信息 271 | for name, basename, type in pb.types() do 272 | print(name, basename, type) 273 | end 274 | ``` 275 | 276 | `pb.field()` 返回了一个特定的消息里的一个域的详细信息: 277 | 278 | - name: 域名 279 | - number: schema中该域的对应数字(序号) 280 | - type: 域类型 281 | - default value: 域的默认值,没有的话是`nil` 282 | - `"packed"`|`"repeated"`| `"optional"`:域的标签,注意并不支持`required`,会被当作`optional` 283 | - oneof_name:域所属的oneof块的名字,可选 284 | - , oneof_index:域所属的oneof块的索引,可选 285 | 286 | `pb.fields()` 会返回一个好像对消息类型里的每个域调用`pb.field()`一样的迭代器对象: 287 | 288 | ```lua 289 | -- 打印 MyType 消息类型里 the_first_field 域的详细信息 290 | print(pb.field("MyType", "the_first_field")) 291 | 292 | -- 遍历 MyType 消息类型里所有的域,注意你并不需要写全所有的详细信息(后面的信息会被忽略掉) 293 | for name, number, type in pb.fields "MyType" do 294 | print(name, number, type) -- 只需要打印这三个 295 | end 296 | ``` 297 | 298 | `pb.enum()` 转换枚举的名字和值: 299 | 300 | ```lua 301 | protoc:load [[ 302 | enum Color { Red = 1; Green = 2; Blue = 3 } 303 | ]] 304 | print(pb.enum("Color", "Red")) --> 1 305 | print(pb.enum("Color", 2)) --> "Green" 306 | ``` 307 | 308 | 其实枚举本身就是一种特殊的“消息类型”,在内存数据库里,枚举和消息其实没什么区别,因此使用`pb.field()`也能做到枚举的名字和值之间的转换,`pb.enum()`这个名字更多的只是语义上的区别,另外因为只返回一个值,可能会相对快一些。 309 | 310 | #### 默认值 311 | 312 | 你可以调用`pb.defaults()` 函数得到对应一个消息类型的一张Lua表,这张表存储了该消息类型所有域的默认值。 313 | 314 | `pb.defaults()`函数的第一个参数是指定的消息类型名称,如果有可选的第二个参数,那么该参数会被指定为该类型的默认值。 315 | 316 | 其实通过`pb.decode("Type")`本身就能得到一张填充了默认值的Lua表。而`pb.defaults`不同的是,他提供的表会被内存数据库记住,如果你设置了`use_default_metatable`这个选项,那么这个默认值表就会成为对应类型被解码时被自动设置的元表—也就是说,可以支持解码一个空表,但是你能通过元表取得所有域的默认值。 317 | 318 | 另外,你可以传递`"*array"`或者`"*map"`作为特殊的类型名给`pb.defaults()`函数,效果是给解码出来的map/repeated设置元表。这个特性和`use_default_metatable`无关,如果不想设置元表了,只要删掉对应元表就可以禁用这个特性了。 319 | 320 | 示例如下: 321 | 322 | 323 | ```lua 324 | check_load [[ 325 | message TestDefault { 326 | optional int32 defaulted_int = 10 [ default = 777 ]; 327 | optional bool defaulted_bool = 11 [ default = true ]; 328 | optional string defaulted_str = 12 [ default = "foo" ]; 329 | optional float defaulted_num = 13 [ default = 0.125 ]; 330 | } ]] 331 | print(require "serpent".block(pb.defaults "TestDefault")) 332 | -- output: 333 | -- { 334 | -- defaulted_bool = true, 335 | -- defaulted_int = 777, 336 | -- defaulted_num = 0.125, 337 | -- defaulted_str = "foo" 338 | -- } --[[table: 0x7f8c1e52b050]] 339 | 340 | ``` 341 | 342 | #### 钩子 343 | 344 | 如果通过`pb.option "enable_hooks"`启用了钩子功能,那么你可以通过`pb.hook()`函数为指定的消息类型设置一个解码钩子。一个钩子是一个会在该消息类型所有的域都被读取完毕之后调用的一个函数。你可以在这个时候对这个已经读取完毕的消息表做任何事。比如设置上一节提到的元表。 345 | 346 | `pb.hook()`的第一个参数是指定的消息类型名称,第二个参数就是钩子函数了。如果第二个参数是`nil`,则这个类型的钩子函数会被清除;任何情况下,`pb.hook()`函数都会返回之前设置过的钩子函数(或者`nil`)。 347 | 348 | 钩子函数只有一个参数,即当前已经处理完的消息表。如果你同时还需要消息类型名称,那么可以使用以下工具函数: 349 | 350 | ```lua 351 | local function make_hook(name, func) 352 | return pb.hook(name, function(t) 353 | return func(name, t) 354 | end) 355 | end 356 | ``` 357 | 358 | 你可以通过 `pb.option “enable_enchooks”` 启用编码钩子功能。编码钩子会在编码消息中任何 `message` 或者 `enum` 类型的成员**之前**调用。只有一个参数即该成员本体。如果你有返回任何值,那么这个值就会被作为这个 `message` 或者 `enum` 被编码。返回 `nil` (或者不返回值)则待编码的值不变。 359 | 360 | 编码钩子通过 `pb.encode_hook()` 函数设置,该函数和 `pb.hook()` 类似,但是用来设置编码钩子。 361 | 362 | #### 选项 363 | 364 | 你可以通过调用`pb.option()`函数设置选项来改变编码/解码时的行为。 365 | 366 | 目前支持的选项如下: 367 | 368 | | 选项 | 描述 | 369 | | --------------------- | ----------------------------------------------------- | 370 | | `enum_as_name` | 解码枚举的时候,设置值为枚举名 **(默认)** | 371 | | `enum_as_value` | 解码枚举的时候,设置值为枚举值数字 | 372 | | `int64_as_number` | 如果值的大小小于uint32允许的最大值,则存储整数,否则存储Lua浮点数类型($\le$ Lua 5.2,可能会导致不精确)或者64位整数类型($\ge$ Lua 5.3,这个版本开始才支持64位整数类型) **(默认)** | 373 | | `int64_as_string` | 同上,但返回一个前缀`"#"`的字符串以避免精度损失 | 374 | | `int64_as_hexstring` | 同上,但返回一个16进制的字符串 | 375 | | `auto_default_values` | 对于 proto3,采取 `use_default_values` 的设置;对于其他 protobuf 格式,则采取 `no_default_values` 的设置 **(默认)** | 376 | | `no_default_values` | 忽略默认值设置 | 377 | | `use_default_values` | 将默认值表复制到解码目标表中来 | 378 | | `use_default_metatable` | 将默认值表作为解码目标表的元表使用 | 379 | | `enable_hooks` | `pb.decode` 启用钩子功能 | 380 | | `disable_hooks` | `pb.decode` 禁用钩子功能 **(默认)** | 381 | | `enable_enchooks` | `pb.encode`启用钩子功能 | 382 | | `disable_enchooks` | `pb.encode` 禁用钩子功能 **(默认)** | 383 | | `encode_default_values` | 默认值也参与编码 | 384 | | `no_encode_default_values` | 默认值不参与编码 **(默认)** | 385 | | `decode_default_array` | 配合`no_default_values`选项,对于数组,将空值解码为空表 | 386 | | `no_decode_default_array` | 配合`no_default_values`选项,对于数组,将空值解码为nil **(默认)** | 387 | | `encode_order` | 保证对相同的schema和data,`pb.encode`编码出的结果一致(按照field number的顺序进行编码)。如果message中空field特别多,可能会导致效率下降。 | 388 | | `no_encode_order` | 不保证对相同输入,`pb.encode`编码出的结果一致。**(默认)** | 389 | | `decode_default_message` | 将空子消息解析成默认值表 | 390 | | `no_decode_default_message` | 将空子消息解析成 `nil` **(default)** | 391 | 392 | 393 | *注意*: `int64_as_string` 或 `int64_as_hexstring` 返回的字符串会带一个 `'#'` 字符前缀,因为Lua会自动把数字表示的字符串当作数字使用,从而导致精度损失。带一个前缀会让Lua认为这个字符串并不是数字,从而避免了Lua的自动转换。 394 | 395 | 本模块中所有接受数字参数的函数都支持使用带`'#'`前缀的字符串用于表示数字,无论是否开启了相关的选项都是如此。如果需要表格中提供的数字,也同样支持使用前缀字符串指定。 396 | 397 | #### 多内存数据库 398 | 399 | `pb` 模块支持同时存在多个内存数据库,但是你每次只能使用其中的一个。内存数据库仅仅存储所有的类型。默认值表、选项等等不受影响。你可以通过`pb.state()`函数来获得/设置内存数据库。 400 | 401 | 如果要新建一个内存数据库,调用 `pb.state(nil)` 函数清除当前内存数据库(会在返回值中返回),如果在调用`pb.load()`函数载入消息的类型的时候发现当前没有内存数据库,那么载入器会自动创建一个新的内存数据库。从而支持多个内存数据库同时存在。你可以同样使用`pb.state()` 函数来切换这多个内存数据库。 402 | 403 | 下面的示例能让你在独立的内存数据库中完成某些操作: 404 | 405 | ```lua 406 | local old = pb.state(nil) -- 清空当前内存数据库 407 | -- 如果要使用 protoc.lua, 这里还需要调用 protoc.reload() 函数 408 | assert(pb.load(...)) -- 载入新的消息类型信息 409 | -- 开始编码/解码 ... 410 | pb.state(old) -- 恢复旧的内存数据库(并丢弃刚才新建的那个) 411 | ``` 412 | 413 | 需要注意的是 `protoc.Lua` 模块会注册一些Google标准消息类型到内存数据库中,因此一定要记得在创建新的内存数据库之后,调用 `proto.reload()` 函数恢复这些信息。 414 | 415 | ### `pb.io` 模块 416 | 417 | `pb.io` 模块从文件或者 `stdin`/`stdout`中读取或者写入二进制数据。提供这个模块的目的是在Windows下,Lua没有二进制读写标准输入输出的能力。然而要实现一个官方的`protoc`插件则必须能够读写二进制的标准输入输出流。因为官方的`protoc`找到插件以后会用插件启动新进程,然后把读取编译好的proto文件的内容用二进制的`FileDescriptorSet`消息的格式发给新进程的`stdin`。所以提供了这个插件,才可以用纯Lua写官方的插件。 418 | 419 | `pb.io.read(filename)` 负责从提供的文件名指定的文件里读取二进制数据,如果不提供文件名,那么就直接从 `stdin` 读取。 420 | 421 | `pb.io.write()` 和 `pb.io.dump()` 和Lua标准库里的 `io.write()` 是一样的,只是会写二进制数据。前者写`stdout`,而后者写到第一个参数提供的文件名所指定的文件中。 422 | 423 | 这些函数执行成功的时候都会返回`true`,执行失败的时候会返回 `nil, errmsg`,所以调用的时候记得用`assert()`包住以捕获错误。 424 | 425 | | 接口 | 返回 | 描述 | 426 | | --------------- | ------- | --------------------------------------------- | 427 | | `io.read()` | string | 从 `stdin`读取所有二进制数据 | 428 | | `io.read(string)` | string | 从文件中读取所有二进制数据 | 429 | | `io.write(...)` | true | 将二进制数据写入 `stdout` | 430 | | `io.dump(string, ...)` | string | write binary data to file name | 431 | 432 | ### `pb.conv` 模块 433 | 434 | `pb.conv` 能够在Lua和protobuf提供的各种数字类型之间进行转换。如果你要使用底层接口,那么这个模块就会很有用。 435 | 436 | | Encode Function | Decode Function | 437 | | ---------------------- | ---------------------- | 438 | | `conv.encode_int32()` | `conv.decode_int32()` | 439 | | `conv.encode_uint32()` | `conv.decode_uint32()` | 440 | | `conv.encode_sint32()` | `conv.decode_sint32()` | 441 | | `conv.encode_sint64()` | `conv.decode_sint64()` | 442 | | `conv.encode_float()` | `conv.decode_float()` | 443 | | `conv.encode_double()` | `conv.decode_double()` | 444 | 445 | ### `pb.slice` 模块 446 | 447 | “Slice”是一种类似于“视图”的对象,它代表某个二进制数据的一部分。使用`slice.new()`可以创建一个slice视图,它会自动关联你传给new函数的那个对象,并且在它之上获取一个指针用于读取二进制的底层wireformat信息。 448 | 449 | slice对象最重要的方法是`slice:unpack()`,它的第一个参数是一个格式字符串,每个格式字符代表需要解码的一个类型。具体的格式字符下面会用表格的形式给出,这些格式字符也可以使用`pb.typefmt()`函数从protobuf的基础类型的名字转换而来。请注意,`pb.buffer`模块的重要方法`buffer:pack()`使用的是同一套格式字符: 450 | 451 | | 格式字符 | 描述 | 452 | | ------ | ------------------------------------------------------------ | 453 | | v | 基础类型,变长的整数类型,1到10个字节(`varint`) | 454 | | d | 基础类型,4 字节数字类型 | 455 | | q | 基础类型,8 字节数字类型 | 456 | | s | 基础类型,带长度数据通常是 `string`, `bytes` 或者 `message` 类型的数据 | 457 | | c | fmt之后额外接受一个数字参数 `count`,直接读取接下来的 `count` 个字节 | 458 | | b | 布尔类型:`bool` | 459 | | f | 4 字节浮点数类型:`float` | 460 | | F | 8 字节浮点数类型:`double` | 461 | | i | `varint`表示的32位有符号整数:`int32` | 462 | | j | `varint`表示的zig-zad 编码的有符号32位整数:`sint32` | 463 | | u | `varint`表示的32位无符号整数:`uint32` | 464 | | x | 4 字节的无符号32位整数:`fixed32` | 465 | | y | 4 字节的有符号32位整数:`sfixed32` | 466 | | I | `varint`表示的64位有符号整数:`int64` | 467 | | J | `varint`表示的zig-zad 编码的有符号64位整数:`sint64` | 468 | | U | `varint`表示的64位无符号整数:`uint64` | 469 | | X | 4 字节的无符号32位整数:`fixed32` | 470 | | Y | 4 字节的有符号32位整数:`sfixed32` | 471 | 472 | slice对象内部会维护“当前读到哪儿”的位置信息。每当使用unpack读取的时候,会自动指向下一个待读取的偏移,可以使用`#slice` 方法得知“还剩下多少字节的数据没有读”。下面的表给出了操控“当前位置”的“格式字符”——注意,这些格式字符只能用在`unpack`函数里,`pack`是不给用的: 473 | 474 | | 格式字符 | 描述 | 475 | | ------ | ------------------------------------------------------------ | 476 | | @ | 返回1开始的当前读取位置偏移,以当前视图开始为1 | 477 | | * | fmt参数后提供一个额外参数,直接设置偏移为这个参数的值 | 478 | | + | fmt参数后提供一个额外参数,设置偏移为加上这个参数以后的值 | 479 | 480 | 下面是一个“将一个 `varint` 类型的值读取两次”的例子: 481 | ```lua 482 | local v1, v2 = s:unpack("v*v", 1) 483 | -- v: 读取一个 varint 484 | -- *: 接受下一个参数(这里是1),将其设置为当前位置:现在读取位置回到一开始了 485 | -- v: 现在,把上一个 varint 再读一遍 486 | ``` 487 | 488 | 除了读取位置以外,slice还支持“进入”和“退出”视图,也就是视图栈的功能。比如说,你的协议里是一个消息A,套一个消息B,再套一个消息C,你用`slice:new()`可以得到整个消息A的视图,那么在发现消息B出现的时候,你可以通过`s:enter()`先读取一个带长度数据(这个数据读取后被跳过),然后将视图缩小到这个带长度数据内部:也就是消息B的内容。同理可以处理消息C。当处理完之后,调用`s:leave()`可以回到原视图了,注意在读取带长度数据的时候这个数据就被跳过了,这时回到原视图读取位置正好是带长度数据之后,就可以继续处理后续的消息了。 489 | 490 | `s:enter()`还支持接受两个参数i和j直接给出偏移量位置。注意这种情况下,`s:leave()`就不会修改读取位置了——因为并没有读取操作。 491 | 492 | 下面是一个使用底层接口读取一个消息的示例: 493 | 494 | ```lua 495 | local s = slice.new("") 496 | local tag = s:unpack "v" 497 | if tag%8 == 2 then -- tag 是 string/bytes 类型?这可能就是个子消息 498 | s:enter() -- 读取这个带长度数据,进入数据本身 499 | -- 现在可以对这个带长度数据做任何事儿了:比如说,读取一堆的fixed32数据 500 | local t = {} 501 | while #s > 0 do 502 | t[#t+1] = s:unpack "d" 503 | end 504 | s:leave() -- 搞定了?回到上级视图继续读取接下来的数据 505 | end 506 | ``` 507 | 508 | 以下是`pb.slice`模块里的所有接口: 509 | 510 | | 接口 | 返回 | 描述 | 511 | | --------------- | ------- | --------------------------------------------- | 512 | | `slice.new(data[,i[,j]])` | Slice object | 创建一个新的 slice 对象 | 513 | | `s:delete()` | none | 和 `s:reset()`相同,重置并释放slice对象引用的内存 | 514 | | `tostring(s)` | string | 返回slice的字符串表示信息 | 515 | | `#s` | number | 得到当前视图还未读取的字节数 | 516 | | `s:result([i[, j]])` | String | 得到当前视图的二进制数据 | 517 | | `s:reset([data[,i[,j]]])` | self | 将slice对象重置绑定另一个数据源 | 518 | | `s:level()` | number | 返回当前视图栈的深度 | 519 | | `s:level(number)` | p, i, j | 返回第n层视图栈的信息(读取位置、视图偏移) | 520 | | `s:enter()` | self | 读取一个带长度数据,并将其视图推入视图栈 | 521 | | `s:enter(i[, j])` | self | 将[i,j]字节范围的数据推入视图栈 | 522 | | `s:leave([number])` | self, n | 离开n层的视图栈(默认离开一层),返回当前视图栈深度 | 523 | | `s:unpack(fmt, ...)` | values... | 利用fmt和额外参数,读取当前视图内的信息 | 524 | 525 | ### `pb.buffer` 模块 526 | 527 | Buffer模块本质上其实就是一个内存缓存,类似Java的“StringBuilder”的东西。他是一个构建wireformat数据的底层接口。使用`buffer:pack()`可以向缓存里新增数据,使用`buffer:result()`可以得到编码后的二进制数据结构。或者使用`buffer:tohex()`获取人类可读的16进制编码字符串。 528 | 529 | `buffer.pack()` 使用和 `slice.unpack()`相同的格式字符,请参见`pb.slice`的文档获取详细的格式字符的表格。除此以外,pack还支持 `'()'`格式字符,用于支持编码嵌套的带长度数据——也就是嵌套消息的结构。括号格式字符是可以嵌套的。下面是一个例子: 530 | 531 | ```lua 532 | b:pack("(vvv)", 1, 2, 3) -- 获得一个编码了3个varint的带长度数据 533 | ``` 534 | 535 | `buffer.pack()` 也支持 '#' 格式字符:该格式字符会额外读取一个长度参数`oldlen`,并将`buffer`从`oldlen`开始到结束的所有字节数据重新编码为一个带长度数据: 536 | 537 | ```lua 538 | b:pack("#", 5) -- 将b里从第五个字节开始的所有数据编码为一个带长度数据类型数据 539 | ``` 540 | 541 | 这个功能可以用来更方便地编码长度不清楚的嵌套子消息:先读取一个当前长度,然后直接将子消息编码进buffer,一旦编码结束,就编码一个`"#"`格式,这样之前编码的所有信息就自动成为了一个带长度数据——即合法的子消息类型数据。 542 | 543 | 下面是 `pb.buffer` 模块里所有的接口: 544 | 545 | | 接口 | 返回 | 描述 | 546 | | --------------- | ------- | --------------------------------------------- | 547 | | `buffer.new([...])` | Buffer object | 创建一个新的buffer对象,额外参数会传递给`b:reset(...)`函数 | 548 | | `b:delete()` | none | 即`b:reset()`,释放buffer使用的内存 | 549 | | `tostring(b)` | string | 返回buffer的字符串表示信息 | 550 | | `#b` | number | 返回buffer中已经完成编码的字节数 | 551 | | `b:reset()` | self | 清空buffer中的所有数据 | 552 | | `b:reset([...])` | self | 清空buffer,并将其数据设置为所有的参数,如同`io.write()`一样处理其参数 | 553 | | `b:tohex([i[, j]])` | string | 返回可选范围(默认是全部)的数据的16进制表示 | 554 | | `b:result([i[,j]])` | string | 返回编码后二进制数据。允许只返回一部分。默认返回全部 | 555 | | `b:pack(fmt, ...)` | self | 利用fmt和额外参数,将参数里提供的数据编码到buffer中 | 556 | 557 | --- 558 | 559 | -------------------------------------------------------------------------------- /protoc.lua: -------------------------------------------------------------------------------- 1 | local string = string 2 | local tonumber = tonumber 3 | local setmetatable = setmetatable 4 | local error = error 5 | local ipairs = ipairs 6 | local io = io 7 | local table = table 8 | local math = math 9 | local assert = assert 10 | local tostring = tostring 11 | local type = type 12 | local insert_tab = table.insert 13 | local str_gmatch = string.gmatch 14 | 15 | local function meta(name, t) 16 | t = t or {} 17 | t.__name = name 18 | t.__index = t 19 | return t 20 | end 21 | 22 | local function default(t, k, def) 23 | local v = t[k] 24 | if not v then 25 | v = def or {} 26 | t[k] = v 27 | end 28 | return v 29 | end 30 | 31 | local Lexer = meta "Lexer" do 32 | 33 | local escape = { 34 | a = "\a", b = "\b", f = "\f", n = "\n", 35 | r = "\r", t = "\t", v = "\v" 36 | } 37 | 38 | local function tohex(x) return string.byte(tonumber(x, 16)) end 39 | local function todec(x) return string.byte(tonumber(x, 10)) end 40 | local function toesc(x) return escape[x] or x end 41 | 42 | function Lexer.new(name, src) 43 | local self = { 44 | name = name, 45 | src = src, 46 | pos = 1 47 | } 48 | return setmetatable(self, Lexer) 49 | end 50 | 51 | function Lexer:__call(patt, pos) 52 | return self.src:match(patt, pos or self.pos) 53 | end 54 | 55 | function Lexer:test(patt) 56 | self:whitespace() 57 | local pos = self('^'..patt..'%s*()') 58 | if not pos then return false end 59 | self.pos = pos 60 | return true 61 | end 62 | 63 | function Lexer:expected(patt, name) 64 | if not self:test(patt) then 65 | return self:error((name or ("'"..patt.."'")).." expected") 66 | end 67 | return self 68 | end 69 | 70 | function Lexer:pos2loc(pos) 71 | local linenr = 1 72 | pos = pos or self.pos 73 | for start, stop in self.src:gmatch "()[^\n]*()\n?" do 74 | if start <= pos and pos <= stop then 75 | return linenr, pos - start + 1 76 | end 77 | linenr = linenr + 1 78 | end 79 | end 80 | 81 | function Lexer:error(fmt, ...) 82 | local ln, co = self:pos2loc() 83 | return error(("%s:%d:%d: "..fmt):format(self.name, ln, co, ...)) 84 | end 85 | 86 | function Lexer:opterror(opt, msg) 87 | if not opt then return self:error(msg) end 88 | return nil 89 | end 90 | 91 | function Lexer:whitespace() 92 | local pos, c = self "^%s*()(%/?)" 93 | self.pos = pos 94 | if c == '' then return self end 95 | return self:comment() 96 | end 97 | 98 | function Lexer:comment() 99 | local pos = self "^%/%/[^\n]*\n?()" 100 | if not pos then 101 | if self "^%/%*" then 102 | pos = self "^%/%*.-%*%/()" 103 | if not pos then 104 | self:error "unfinished comment" 105 | end 106 | end 107 | end 108 | if not pos then return self end 109 | self.pos = pos 110 | return self:whitespace() 111 | end 112 | 113 | function Lexer:line_end(opt) 114 | self:whitespace() 115 | local pos = self '^[%s;]*%s*()' 116 | if not pos then 117 | return self:opterror(opt, "';' expected") 118 | end 119 | self.pos = pos 120 | return pos 121 | end 122 | 123 | function Lexer:eof() 124 | self:whitespace() 125 | return self.pos > #self.src 126 | end 127 | 128 | function Lexer:keyword(kw, opt) 129 | self:whitespace() 130 | local ident, pos = self "^([%a_][%w_]*)%s*()" 131 | if not ident or ident ~= kw then 132 | return self:opterror(opt, "''"..kw..'" expected') 133 | end 134 | self.pos = pos 135 | return kw 136 | end 137 | 138 | function Lexer:ident(name, opt) 139 | self:whitespace() 140 | local b, ident, pos = self "^()([%a_][%w_]*)%s*()" 141 | if not ident then 142 | return self:opterror(opt, (name or 'name')..' expected') 143 | end 144 | self.pos = pos 145 | return ident, b 146 | end 147 | 148 | function Lexer:full_ident(name, opt) 149 | self:whitespace() 150 | local b, ident, pos = self "^()([%a_.][%w_.]*)%s*()" 151 | if not ident or ident:match "%.%.+" then 152 | return self:opterror(opt, (name or 'name')..' expected') 153 | end 154 | self.pos = pos 155 | return ident, b 156 | end 157 | 158 | function Lexer:integer(opt) 159 | self:whitespace() 160 | local ns, oct, hex, s, pos = 161 | self "^([+-]?)(0?)([xX]?)([0-9a-fA-F]+)%s*()" 162 | local n 163 | if oct == '0' and hex == '' then 164 | n = tonumber(s, 8) 165 | elseif oct == '' and hex == '' then 166 | n = tonumber(s, 10) 167 | elseif oct == '0' and hex ~= '' then 168 | n = tonumber(s, 16) 169 | end 170 | if not n then 171 | return self:opterror(opt, 'integer expected') 172 | end 173 | self.pos = pos 174 | return ns == '-' and -n or n 175 | end 176 | 177 | function Lexer:number(opt) 178 | self:whitespace() 179 | if self:test "nan%f[%A]" then 180 | return 0.0/0.0 181 | elseif self:test "inf%f[%A]" then 182 | return 1.0/0.0 183 | end 184 | local ns, d1, s, d2, s2, pos = self "^([+-]?)(%.?)([0-9]+)(%.?)([0-9]*)()" 185 | if not ns then 186 | return self:opterror(opt, 'floating-point number expected') 187 | end 188 | local es, pos2 = self("(^[eE][+-]?[0-9]+)%s*()", pos) 189 | if d1 == "." and d2 == "." then 190 | return self:error "malformed floating-point number" 191 | end 192 | self.pos = pos2 or pos 193 | local n = tonumber(d1..s..d2..s2..(es or "")) 194 | return ns == '-' and -n or n 195 | end 196 | 197 | function Lexer:quote(opt) 198 | self:whitespace() 199 | local q, start = self '^(["\'])()' 200 | if not start then 201 | return self:opterror(opt, 'string expected') 202 | end 203 | self.pos = start 204 | local patt = '()(\\?'..q..')%s*()' 205 | while true do 206 | local stop, s, pos = self(patt) 207 | if not stop then 208 | self.pos = start-1 209 | return self:error "unfinished string" 210 | end 211 | self.pos = pos 212 | if s == q then 213 | return self.src:sub(start, stop-1) 214 | :gsub("\\x(%x+)", tohex) 215 | :gsub("\\(%d+)", todec) 216 | :gsub("\\(.)", toesc) 217 | end 218 | end 219 | end 220 | 221 | function Lexer:structure(opt) 222 | self:whitespace() 223 | if not self:test "{" then 224 | return self:opterror(opt, 'opening curly brace expected') 225 | end 226 | local t = {} 227 | while not self:test "}" do 228 | local pos, name, npos = self "^%s*()(%b[])()" 229 | if not pos then 230 | name = self:full_ident "field name" 231 | else 232 | self.pos = npos 233 | end 234 | self:test ":" 235 | local value = self:constant() 236 | self:test "," 237 | self:line_end "opt" 238 | t[name] = value 239 | end 240 | return t 241 | end 242 | 243 | function Lexer:array(opt) 244 | self:whitespace() 245 | if not self:test "%[" then 246 | return self:opterror(opt, 'opening square bracket expected') 247 | end 248 | local t = {} 249 | while not self:test "]" do 250 | local value = self:constant() 251 | self:test "," 252 | t[#t + 1] = value 253 | end 254 | return t 255 | end 256 | 257 | function Lexer:constant(opt) 258 | local c = self:full_ident('constant', 'opt') 259 | if c == "true" then return true end 260 | if c == "false" then return false end 261 | if c == "none" then return nil end 262 | if c then return c end 263 | c = self:number('opt') or 264 | self:quote('opt') or 265 | self:structure('opt') or 266 | self:array('opt') 267 | if c == nil and not opt then 268 | return self:error "constant expected" 269 | end 270 | return c 271 | end 272 | 273 | function Lexer:option_name() 274 | local ident 275 | if self:test "%(" then 276 | ident = self:full_ident "option name" 277 | self:expected "%)" 278 | else 279 | ident = self:ident "option name" 280 | end 281 | while self:test "%." do 282 | ident = ident .. "." .. self:ident() 283 | end 284 | return ident 285 | end 286 | 287 | function Lexer:type_name() 288 | if self:test "%." then 289 | local id, pos = self:full_ident "type name" 290 | return "."..id, pos 291 | else 292 | return self:full_ident "type name" 293 | end 294 | end 295 | 296 | end 297 | 298 | local Parser = meta "Parser" do 299 | Parser.typemap = {} 300 | Parser.loaded = {} 301 | Parser.paths = { "", "." } 302 | 303 | function Parser.new() 304 | local self = {} 305 | self.typemap = {} 306 | self.loaded = {} 307 | self.paths = { "", "." } 308 | return setmetatable(self, Parser) 309 | end 310 | 311 | function Parser:reset() 312 | self.typemap = {} 313 | self.loaded = {} 314 | return self 315 | end 316 | 317 | function Parser:error(msg) 318 | return self.lex:error(msg) 319 | end 320 | 321 | function Parser:addpath(path) 322 | insert_tab(self.paths, path) 323 | end 324 | 325 | function Parser:parsefile(name) 326 | local info = self.loaded[name] 327 | if info then return info end 328 | local errors = {} 329 | for _, path in ipairs(self.paths) do 330 | local fn = path ~= "" and path.."/"..name or name 331 | local fh, err = io.open(fn) 332 | if fh then 333 | local content = fh:read "*a" 334 | info = self:parse(content, name) 335 | fh:close() 336 | return info 337 | end 338 | insert_tab(errors, err or fn..": ".."unknown error") 339 | end 340 | local import_fallback = self.unknown_import 341 | if import_fallback == true then 342 | info = import_fallback 343 | elseif import_fallback then 344 | info = import_fallback(self, name) 345 | end 346 | if not info then 347 | error("module load error: "..name.."\n\t"..table.concat(errors, "\n\t")) 348 | end 349 | return info 350 | end 351 | 352 | -- parser 353 | 354 | local labels = { optional = 1; required = 2; repeated = 3 } 355 | 356 | local key_types = { 357 | int32 = 5; int64 = 3; uint32 = 13; 358 | uint64 = 4; sint32 = 17; sint64 = 18; 359 | fixed32 = 7; fixed64 = 6; sfixed32 = 15; 360 | sfixed64 = 16; bool = 8; string = 9; 361 | } 362 | 363 | local com_types = { 364 | group = 10; message = 11; enum = 14; 365 | } 366 | 367 | local types = { 368 | double = 1; float = 2; int32 = 5; 369 | int64 = 3; uint32 = 13; uint64 = 4; 370 | sint32 = 17; sint64 = 18; fixed32 = 7; 371 | fixed64 = 6; sfixed32 = 15; sfixed64 = 16; 372 | bool = 8; string = 9; bytes = 12; 373 | group = 10; message = 11; enum = 14; 374 | } 375 | 376 | local function register_type(self, lex, tname, typ) 377 | if not tname:match "%."then 378 | tname = self.prefix..tname 379 | end 380 | if self.typemap[tname] then 381 | return lex:error("type %s already defined", tname) 382 | end 383 | self.typemap[tname] = typ 384 | end 385 | 386 | local function type_info(lex, tname) 387 | local tenum = types[tname] 388 | if com_types[tname] then 389 | return lex:error("invalid type name: "..tname) 390 | elseif tenum then 391 | tname = nil 392 | end 393 | return tenum, tname 394 | end 395 | 396 | local function map_info(lex) 397 | local keyt = lex:ident "key type" 398 | if not key_types[keyt] then 399 | return lex:error("invalid key type: "..keyt) 400 | end 401 | local valt = lex:expected "," :type_name() 402 | local name = lex:expected ">" :ident() 403 | local ident = name:gsub("^%a", string.upper) 404 | :gsub("_(%a)", string.upper).."Entry" 405 | local kt, ktn = type_info(lex, keyt) 406 | local vt, vtn = type_info(lex, valt) 407 | return name, types.message, ident, { 408 | name = ident, 409 | field = { 410 | { 411 | name = "key", 412 | number = 1; 413 | label = labels.optional, 414 | type = kt, 415 | type_name = ktn 416 | }, 417 | { 418 | name = "value", 419 | number = 2; 420 | label = labels.optional, 421 | type = vt, 422 | type_name = vtn 423 | }, 424 | }, 425 | options = { map_entry = true } 426 | } 427 | end 428 | 429 | local function inline_option(lex, info) 430 | if lex:test "%[" then 431 | info = info or {} 432 | while true do 433 | local name = lex:option_name() 434 | local value = lex:expected '=' :constant() 435 | info[name] = value 436 | if lex:test "%]" then 437 | return info 438 | end 439 | lex:expected ',' 440 | end 441 | end 442 | end 443 | 444 | local function field(self, lex, ident) 445 | local name, typ, type_name, map_entry 446 | if ident == "map" and lex:test "%<" then 447 | name, typ, type_name, map_entry = map_info(lex) 448 | self.locmap[map_entry.field[1]] = lex.pos 449 | self.locmap[map_entry.field[2]] = lex.pos 450 | register_type(self, lex, type_name, types.message) 451 | else 452 | typ, type_name = type_info(lex, ident) 453 | name = lex:ident() 454 | end 455 | local info = { 456 | name = name, 457 | number = lex:expected "=":integer(), 458 | label = ident == "map" and labels.repeated or labels.optional, 459 | type = typ, 460 | type_name = type_name 461 | } 462 | local options = inline_option(lex) 463 | if options then 464 | info.default_value, options.default = tostring(options.default), nil 465 | info.json_name, options.json_name = options.json_name, nil 466 | info.options = options 467 | end 468 | if info.number <= 0 then 469 | lex:error("invalid tag number: "..info.number) 470 | end 471 | return info, map_entry 472 | end 473 | 474 | local function label_field(self, lex, ident, parent) 475 | local label = labels[ident] 476 | local info, map_entry 477 | if not label then 478 | if self.syntax == "proto2" and ident ~= "map" then 479 | return lex:error("proto2 disallow missing label") 480 | end 481 | return field(self, lex, ident) 482 | end 483 | local proto3_optional = label == labels.optional and self.syntax == "proto3" 484 | if proto3_optional and not (self.proto3_optional and parent) then 485 | return lex:error("proto3 disallow 'optional' label") 486 | end 487 | info, map_entry = field(self, lex, lex:type_name()) 488 | if proto3_optional then 489 | local ot = default(parent, "oneof_decl") 490 | info.oneof_index = #ot 491 | ot[#ot+1] = { name = "_" .. info.name } 492 | else 493 | info.label = label 494 | end 495 | return info, map_entry 496 | end 497 | 498 | local toplevel = {} do 499 | 500 | function toplevel:package(lex, info) 501 | local package = lex:full_ident 'package name' 502 | lex:line_end() 503 | info.package = package 504 | self.prefix = "."..package.."." 505 | return self 506 | end 507 | 508 | function toplevel:import(lex, info) 509 | local mode = lex:ident('"weak" or "public"', 'opt') or "public" 510 | if mode ~= 'weak' and mode ~= 'public' then 511 | return lex:error '"weak or "public" expected' 512 | end 513 | local name = lex:quote() 514 | lex:line_end() 515 | local result = self:parsefile(name) 516 | if self.on_import then 517 | self.on_import(result) 518 | end 519 | local dep = default(info, 'dependency') 520 | local index = #dep 521 | dep[index+1] = name 522 | if mode == "public" then 523 | local it = default(info, 'public_dependency') 524 | insert_tab(it, index) 525 | else 526 | local it = default(info, 'weak_dependency') 527 | insert_tab(it, index) 528 | end 529 | end 530 | 531 | local msgbody = {} do 532 | 533 | function msgbody:message(lex, info) 534 | local nested_type = default(info, 'nested_type') 535 | insert_tab(nested_type, toplevel.message(self, lex)) 536 | return self 537 | end 538 | 539 | function msgbody:enum(lex, info) 540 | local nested_type = default(info, 'enum_type') 541 | insert_tab(nested_type, toplevel.enum(self, lex)) 542 | return self 543 | end 544 | 545 | function msgbody:extend(lex, info) 546 | local extension = default(info, 'extension') 547 | local nested_type = default(info, 'nested_type') 548 | local ft, mt = toplevel.extend(self, lex, {}) 549 | for _, v in ipairs(ft) do 550 | insert_tab(extension, v) 551 | end 552 | for _, v in ipairs(mt) do 553 | insert_tab(nested_type, v) 554 | end 555 | return self 556 | end 557 | 558 | function msgbody:extensions(lex, info) 559 | local rt = default(info, 'extension_range') 560 | local idx = #rt 561 | repeat 562 | local start = lex:integer "field number range" 563 | local stop = math.floor(2^29) 564 | if lex:keyword('to', 'opt') then 565 | if not lex:keyword('max', 'opt') then 566 | stop = lex:integer "field number range end or 'max'" 567 | end 568 | insert_tab(rt, { start = start, ['end'] = stop }) 569 | else 570 | insert_tab(rt, { start = start, ['end'] = start }) 571 | end 572 | until not lex:test ',' 573 | rt[idx+1].options = inline_option(lex) 574 | lex:line_end() 575 | return self 576 | end 577 | 578 | function msgbody:reserved(lex, info) 579 | lex:whitespace() 580 | if not lex '^%d' then 581 | local rt = default(info, 'reserved_name') 582 | repeat 583 | insert_tab(rt, (lex:quote())) 584 | until not lex:test ',' 585 | else 586 | local rt = default(info, 'reserved_range') 587 | local first = true 588 | repeat 589 | local start = lex:integer(first and 'field name or number range' 590 | or 'field number range') 591 | if lex:keyword('to', 'opt') then 592 | if lex:keyword('max', 'opt') then 593 | insert_tab(rt, { start = start, ['end'] = 2^29-1 }) 594 | else 595 | local stop = lex:integer 'field number range end' 596 | insert_tab(rt, { start = start, ['end'] = stop }) 597 | end 598 | else 599 | insert_tab(rt, { start = start, ['end'] = start }) 600 | end 601 | first = false 602 | until not lex:test ',' 603 | end 604 | lex:line_end() 605 | return self 606 | end 607 | 608 | function msgbody:oneof(lex, info) 609 | local fs = default(info, "field") 610 | local ts = default(info, "nested_type") 611 | local ot = default(info, "oneof_decl") 612 | local index = #ot + 1 613 | local oneof = { name = lex:ident() } 614 | lex:expected "{" 615 | while not lex:test "}" do 616 | local ident = lex:type_name() 617 | if ident == "option" then 618 | toplevel.option(self, lex, oneof) 619 | else 620 | local f, t = field(self, lex, ident) 621 | self.locmap[f] = lex.pos 622 | if t then insert_tab(ts, t) end 623 | f.oneof_index = index - 1 624 | insert_tab(fs, f) 625 | end 626 | lex:line_end 'opt' 627 | end 628 | ot[index] = oneof 629 | end 630 | 631 | function msgbody:option(lex, info) 632 | toplevel.option(self, lex, info) 633 | end 634 | 635 | end 636 | 637 | function toplevel:message(lex, info) 638 | local name = lex:ident 'message name' 639 | local typ = { name = name } 640 | register_type(self, lex, name, types.message) 641 | local prefix = self.prefix 642 | self.prefix = prefix..name.."." 643 | lex:expected "{" 644 | while not lex:test "}" do 645 | local ident, pos = lex:type_name() 646 | local body_parser = msgbody[ident] 647 | if body_parser then 648 | body_parser(self, lex, typ) 649 | else 650 | local fs = default(typ, 'field') 651 | local f, t = label_field(self, lex, ident, typ) 652 | self.locmap[f] = pos 653 | insert_tab(fs, f) 654 | if t then 655 | local ts = default(typ, 'nested_type') 656 | insert_tab(ts, t) 657 | end 658 | end 659 | lex:line_end 'opt' 660 | end 661 | lex:line_end 'opt' 662 | if info then 663 | info = default(info, 'message_type') 664 | insert_tab(info, typ) 665 | end 666 | self.prefix = prefix 667 | return typ 668 | end 669 | 670 | function toplevel:enum(lex, info) 671 | local name, pos = lex:ident 'enum name' 672 | local enum = { name = name } 673 | self.locmap[enum] = pos 674 | register_type(self, lex, name, types.enum) 675 | lex:expected "{" 676 | while not lex:test "}" do 677 | local ident, pos = lex:ident 'enum constant name' 678 | if ident == 'option' then 679 | toplevel.option(self, lex, enum) 680 | elseif ident == 'reserved' then 681 | msgbody.reserved(self, lex, enum) 682 | else 683 | local values = default(enum, 'value') 684 | local number = lex:expected '=' :integer() 685 | local value = { 686 | name = ident, 687 | number = number, 688 | options = inline_option(lex) 689 | } 690 | self.locmap[value] = pos 691 | insert_tab(values, value) 692 | end 693 | lex:line_end 'opt' 694 | end 695 | lex:line_end 'opt' 696 | if info then 697 | info = default(info, 'enum_type') 698 | insert_tab(info, enum) 699 | end 700 | return enum 701 | end 702 | 703 | function toplevel:option(lex, info) 704 | local ident = lex:option_name() 705 | lex:expected "=" 706 | local value = lex:constant() 707 | lex:line_end() 708 | local options = info and default(info, 'options') or {} 709 | options[ident] = value 710 | return options, self 711 | end 712 | 713 | function toplevel:extend(lex, info) 714 | local name = lex:type_name() 715 | local ft = info and default(info, 'extension') or {} 716 | local mt = info and default(info, 'message_type') or {} 717 | lex:expected "{" 718 | while not lex:test "}" do 719 | local ident, pos = lex:type_name() 720 | local f, t = label_field(self, lex, ident) 721 | self.locmap[f] = pos 722 | f.extendee = name 723 | insert_tab(ft, f) 724 | insert_tab(mt, t) 725 | lex:line_end 'opt' 726 | end 727 | return ft, mt 728 | end 729 | 730 | local svr_body = {} do 731 | 732 | function svr_body:rpc(lex, info) 733 | local name, pos = lex:ident "rpc name" 734 | local rpc = { name = name } 735 | self.locmap[rpc] = pos 736 | local _, tn 737 | lex:expected "%(" 738 | rpc.client_streaming = lex:keyword("stream", "opt") 739 | _, tn = type_info(lex, lex:type_name()) 740 | if not tn then return lex:error "rpc input type must by message" end 741 | rpc.input_type = tn 742 | lex:expected "%)" :expected "returns" :expected "%(" 743 | rpc.server_streaming = lex:keyword("stream", "opt") 744 | _, tn = type_info(lex, lex:type_name()) 745 | if not tn then return lex:error "rpc output type must by message" end 746 | rpc.output_type = tn 747 | lex:expected "%)" 748 | if lex:test "{" then 749 | while not lex:test "}" do 750 | lex:line_end "opt" 751 | lex:keyword "option" 752 | toplevel.option(self, lex, rpc) 753 | end 754 | end 755 | lex:line_end "opt" 756 | local t = default(info, "method") 757 | insert_tab(t, rpc) 758 | end 759 | 760 | function svr_body:option(lex, info) 761 | return toplevel.option(self, lex, info) 762 | end 763 | 764 | function svr_body.stream(_, lex) 765 | lex:error "stream not implement yet" 766 | end 767 | 768 | end 769 | 770 | function toplevel:service(lex, info) 771 | local name, pos = lex:ident 'service name' 772 | local svr = { name = name } 773 | self.locmap[svr] = pos 774 | lex:expected "{" 775 | while not lex:test "}" do 776 | local ident = lex:type_name() 777 | local body_parser = svr_body[ident] 778 | if body_parser then 779 | body_parser(self, lex, svr) 780 | else 781 | return lex:error "expected 'rpc' or 'option' in service body" 782 | end 783 | lex:line_end 'opt' 784 | end 785 | lex:line_end 'opt' 786 | if info then 787 | info = default(info, 'service') 788 | insert_tab(info, svr) 789 | end 790 | return svr 791 | end 792 | 793 | end 794 | 795 | local function make_context(self, lex) 796 | local ctx = { 797 | syntax = "proto2"; 798 | locmap = {}; 799 | prefix = "."; 800 | lex = lex; 801 | } 802 | ctx.loaded = self.loaded 803 | ctx.typemap = self.typemap 804 | ctx.paths = self.paths 805 | ctx.proto3_optional = 806 | self.proto3_optional or self.experimental_allow_proto3_optional 807 | ctx.unknown_type = self.unknown_type 808 | ctx.unknown_import = self.unknown_import 809 | ctx.on_import = self.on_import 810 | 811 | return setmetatable(ctx, Parser) 812 | end 813 | 814 | function Parser:parse(src, name) 815 | local loaded = self.loaded[name] 816 | if loaded then 817 | if loaded == true then 818 | error("loop loaded: "..name) 819 | end 820 | return loaded 821 | end 822 | 823 | name = name or "" 824 | self.loaded[name] = true 825 | local lex = Lexer.new(name, src) 826 | local ctx = make_context(self, lex) 827 | local info = { name = lex.name, syntax = ctx.syntax } 828 | 829 | local syntax = lex:keyword('syntax', 'opt') 830 | if syntax then 831 | info.syntax = lex:expected '=' :quote() 832 | ctx.syntax = info.syntax 833 | lex:line_end() 834 | end 835 | 836 | while not lex:eof() do 837 | local ident = lex:ident() 838 | local top_parser = toplevel[ident] 839 | if top_parser then 840 | top_parser(ctx, lex, info) 841 | else 842 | lex:error("unknown keyword '"..ident.."'") 843 | end 844 | lex:line_end "opt" 845 | end 846 | self.loaded[name] = name ~= "" and info or nil 847 | return ctx:resolve(lex, info) 848 | end 849 | 850 | -- resolver 851 | 852 | local function empty() end 853 | 854 | local function iter(t, k) 855 | local v = t[k] 856 | if v then return ipairs(v) end 857 | return empty 858 | end 859 | 860 | local function check_dup(self, lex, typ, map, k, v) 861 | local old = map[v[k]] 862 | if old then 863 | local ln, co = lex:pos2loc(self.locmap[old]) 864 | lex:error("%s '%s' exists, previous at %d:%d", 865 | typ, v[k], ln, co) 866 | end 867 | map[v[k]] = v 868 | end 869 | 870 | local function check_type(self, lex, tname) 871 | if tname:match "^%." then 872 | local t = self.typemap[tname] 873 | if not t then 874 | return lex:error("unknown type '%s'", tname) 875 | end 876 | return t, tname 877 | end 878 | local prefix = self.prefix 879 | for i = #prefix+1, 1, -1 do 880 | local op = prefix[i] 881 | prefix[i] = tname 882 | local tn = table.concat(prefix, ".", 1, i) 883 | prefix[i] = op 884 | local t = self.typemap[tn] 885 | if t then return t, tn end 886 | end 887 | local tn, t 888 | local type_fallback = self.unknown_type 889 | if type_fallback then 890 | if type_fallback == true then 891 | tn = true 892 | elseif type(type_fallback) == 'string' then 893 | tn = tname:match(type_fallback) and true 894 | else 895 | tn = type_fallback(self, tname) 896 | end 897 | end 898 | if tn then 899 | t = types[t or "message"] 900 | if tn == true then tn = "."..tname end 901 | return t, tn 902 | end 903 | return lex:error("unknown type '%s'", tname) 904 | end 905 | 906 | local function check_field(self, lex, info) 907 | if info.extendee then 908 | local t, tn = check_type(self, lex, info.extendee) 909 | if t ~= types.message then 910 | lex:error("message type expected in extension") 911 | end 912 | info.extendee = tn 913 | end 914 | if info.type_name then 915 | local t, tn = check_type(self, lex, info.type_name) 916 | info.type = t 917 | info.type_name = tn 918 | end 919 | end 920 | 921 | local function check_enum(self, lex, info) 922 | local names, numbers = {}, {} 923 | for _, v in iter(info, 'value') do 924 | lex.pos = assert(self.locmap[v]) 925 | check_dup(self, lex, 'enum name', names, 'name', v) 926 | if not (info.options and info.options.allow_alias) then 927 | check_dup(self, lex, 'enum number', numbers, 'number', v) 928 | end 929 | end 930 | end 931 | 932 | local function check_message(self, lex, info) 933 | insert_tab(self.prefix, info.name) 934 | local names, numbers = {}, {} 935 | for _, v in iter(info, 'field') do 936 | lex.pos = assert(self.locmap[v]) 937 | check_dup(self, lex, 'field name', names, 'name', v) 938 | check_dup(self, lex, 'field number', numbers, 'number', v) 939 | check_field(self, lex, v) 940 | end 941 | for _, v in iter(info, 'nested_type') do 942 | check_message(self, lex, v) 943 | end 944 | for _, v in iter(info, 'extension') do 945 | lex.pos = assert(self.locmap[v]) 946 | check_field(self, lex, v) 947 | end 948 | self.prefix[#self.prefix] = nil 949 | end 950 | 951 | local function check_service(self, lex, info) 952 | local names = {} 953 | for _, v in iter(info, 'method') do 954 | lex.pos = self.locmap[v] 955 | check_dup(self, lex, 'rpc name', names, 'name', v) 956 | local t, tn = check_type(self, lex, v.input_type) 957 | v.input_type = tn 958 | if t ~= types.message then 959 | lex:error "message type expected in parameter" 960 | end 961 | t, tn = check_type(self, lex, v.output_type) 962 | v.output_type = tn 963 | if t ~= types.message then 964 | lex:error "message type expected in return" 965 | end 966 | end 967 | end 968 | 969 | function Parser:resolve(lex, info) 970 | self.prefix = { "" } 971 | for token in str_gmatch(info.package or "", "[^.]+") do 972 | insert_tab(self.prefix, token) 973 | end 974 | for _, v in iter(info, 'message_type') do 975 | check_message(self, lex, v) 976 | end 977 | for _, v in iter(info, 'enum_type') do 978 | check_enum(self, lex, v) 979 | end 980 | for _, v in iter(info, 'service') do 981 | check_service(self, lex, v) 982 | end 983 | for _, v in iter(info, 'extension') do 984 | lex.pos = assert(self.locmap[v]) 985 | check_field(self, lex, v) 986 | end 987 | self.prefix = nil 988 | return info 989 | end 990 | 991 | end 992 | 993 | local has_pb, pb = pcall(require, "pb") do 994 | if has_pb then 995 | local descriptor_pb = 996 | "\10\179;\10\16descriptor.proto\18\15google.protobuf\"M\10\17FileDescrip".. 997 | "torSet\0188\10\4file\24\1 \3(\0112$.google.protobuf.FileDescriptorProto".. 998 | "R\4file\"\228\4\10\19FileDescriptorProto\18\18\10\4name\24\1 \1(\9R\4na".. 999 | "me\18\24\10\7package\24\2 \1(\9R\7package\18\30\10\10dependency\24\3 \3".. 1000 | "(\9R\10dependency\18+\10\17public_dependency\24\10 \3(\5R\16publicDepen".. 1001 | "dency\18'\10\15weak_dependency\24\11 \3(\5R\14weakDependency\18C\10\12m".. 1002 | "essage_type\24\4 \3(\0112 .google.protobuf.DescriptorProtoR\11messageTy".. 1003 | "pe\18A\10\9enum_type\24\5 \3(\0112$.google.protobuf.EnumDescriptorProto".. 1004 | "R\8enumType\18A\10\7service\24\6 \3(\0112'.google.protobuf.ServiceDescr".. 1005 | "iptorProtoR\7service\18C\10\9extension\24\7 \3(\0112%.google.protobuf.F".. 1006 | "ieldDescriptorProtoR\9extension\0186\10\7options\24\8 \1(\0112\28.googl".. 1007 | "e.protobuf.FileOptionsR\7options\18I\10\16source_code_info\24\9 \1(\011".. 1008 | "2\31.google.protobuf.SourceCodeInfoR\14sourceCodeInfo\18\22\10\6syntax".. 1009 | "\24\12 \1(\9R\6syntax\"\185\6\10\15DescriptorProto\18\18\10\4name\24\1 ".. 1010 | "\1(\9R\4name\18;\10\5field\24\2 \3(\0112%.google.protobuf.FieldDescript".. 1011 | "orProtoR\5field\18C\10\9extension\24\6 \3(\0112%.google.protobuf.FieldD".. 1012 | "escriptorProtoR\9extension\18A\10\11nested_type\24\3 \3(\0112 .google.p".. 1013 | "rotobuf.DescriptorProtoR\10nestedType\18A\10\9enum_type\24\4 \3(\0112$.".. 1014 | "google.protobuf.EnumDescriptorProtoR\8enumType\18X\10\15extension_range".. 1015 | "\24\5 \3(\0112/.google.protobuf.DescriptorProto.ExtensionRangeR\14exten".. 1016 | "sionRange\18D\10\10oneof_decl\24\8 \3(\0112%.google.protobuf.OneofDescr".. 1017 | "iptorProtoR\9oneofDecl\0189\10\7options\24\7 \1(\0112\31.google.protobu".. 1018 | "f.MessageOptionsR\7options\18U\10\14reserved_range\24\9 \3(\0112..googl".. 1019 | "e.protobuf.DescriptorProto.ReservedRangeR\13reservedRange\18#\10\13rese".. 1020 | "rved_name\24\10 \3(\9R\12reservedName\26z\10\14ExtensionRange\18\20\10".. 1021 | "\5start\24\1 \1(\5R\5start\18\16\10\3end\24\2 \1(\5R\3end\18@\10\7optio".. 1022 | "ns\24\3 \1(\0112&.google.protobuf.ExtensionRangeOptionsR\7options\0267".. 1023 | "\10\13ReservedRange\18\20\10\5start\24\1 \1(\5R\5start\18\16\10\3end\24".. 1024 | "\2 \1(\5R\3end\"|\10\21ExtensionRangeOptions\18X\10\20uninterpreted_opt".. 1025 | "ion\24\231\7 \3(\0112$.google.protobuf.UninterpretedOptionR\19uninterpr".. 1026 | "etedOption*\9\8\232\7\16\128\128\128\128\2\"\193\6\10\20FieldDescriptor".. 1027 | "Proto\18\18\10\4name\24\1 \1(\9R\4name\18\22\10\6number\24\3 \1(\5R\6nu".. 1028 | "mber\18A\10\5label\24\4 \1(\0142+.google.protobuf.FieldDescriptorProto.".. 1029 | "LabelR\5label\18>\10\4type\24\5 \1(\0142*.google.protobuf.FieldDescript".. 1030 | "orProto.TypeR\4type\18\27\10\9type_name\24\6 \1(\9R\8typeName\18\26\10".. 1031 | "\8extendee\24\2 \1(\9R\8extendee\18#\10\13default_value\24\7 \1(\9R\12d".. 1032 | "efaultValue\18\31\10\11oneof_index\24\9 \1(\5R\10oneofIndex\18\27\10\9j".. 1033 | "son_name\24\10 \1(\9R\8jsonName\0187\10\7options\24\8 \1(\0112\29.googl".. 1034 | "e.protobuf.FieldOptionsR\7options\18'\10\15proto3_optional\24\17 \1(\8R".. 1035 | "\14proto3Optional\"\182\2\10\4Type\18\15\10\11TYPE_DOUBLE\16\1\18\14\10".. 1036 | "\10TYPE_FLOAT\16\2\18\14\10\10TYPE_INT64\16\3\18\15\10\11TYPE_UINT64\16".. 1037 | "\4\18\14\10\10TYPE_INT32\16\5\18\16\10\12TYPE_FIXED64\16\6\18\16\10\12T".. 1038 | "YPE_FIXED32\16\7\18\13\10\9TYPE_BOOL\16\8\18\15\10\11TYPE_STRING\16\9".. 1039 | "\18\14\10\10TYPE_GROUP\16\10\18\16\10\12TYPE_MESSAGE\16\11\18\14\10\10T".. 1040 | "YPE_BYTES\16\12\18\15\10\11TYPE_UINT32\16\13\18\13\10\9TYPE_ENUM\16\14".. 1041 | "\18\17\10\13TYPE_SFIXED32\16\15\18\17\10\13TYPE_SFIXED64\16\16\18\15\10".. 1042 | "\11TYPE_SINT32\16\17\18\15\10\11TYPE_SINT64\16\18\"C\10\5Label\18\18\10".. 1043 | "\14LABEL_OPTIONAL\16\1\18\18\10\14LABEL_REQUIRED\16\2\18\18\10\14LABEL_".. 1044 | "REPEATED\16\3\"c\10\20OneofDescriptorProto\18\18\10\4name\24\1 \1(\9R\4".. 1045 | "name\0187\10\7options\24\2 \1(\0112\29.google.protobuf.OneofOptionsR\7o".. 1046 | "ptions\"\227\2\10\19EnumDescriptorProto\18\18\10\4name\24\1 \1(\9R\4nam".. 1047 | "e\18?\10\5value\24\2 \3(\0112).google.protobuf.EnumValueDescriptorProto".. 1048 | "R\5value\0186\10\7options\24\3 \1(\0112\28.google.protobuf.EnumOptionsR".. 1049 | "\7options\18]\10\14reserved_range\24\4 \3(\01126.google.protobuf.EnumDe".. 1050 | "scriptorProto.EnumReservedRangeR\13reservedRange\18#\10\13reserved_name".. 1051 | "\24\5 \3(\9R\12reservedName\26;\10\17EnumReservedRange\18\20\10\5start".. 1052 | "\24\1 \1(\5R\5start\18\16\10\3end\24\2 \1(\5R\3end\"\131\1\10\24EnumVal".. 1053 | "ueDescriptorProto\18\18\10\4name\24\1 \1(\9R\4name\18\22\10\6number\24".. 1054 | "\2 \1(\5R\6number\18;\10\7options\24\3 \1(\0112!.google.protobuf.EnumVa".. 1055 | "lueOptionsR\7options\"\167\1\10\22ServiceDescriptorProto\18\18\10\4name".. 1056 | "\24\1 \1(\9R\4name\18>\10\6method\24\2 \3(\0112&.google.protobuf.Method".. 1057 | "DescriptorProtoR\6method\0189\10\7options\24\3 \1(\0112\31.google.proto".. 1058 | "buf.ServiceOptionsR\7options\"\137\2\10\21MethodDescriptorProto\18\18".. 1059 | "\10\4name\24\1 \1(\9R\4name\18\29\10\10input_type\24\2 \1(\9R\9inputTyp".. 1060 | "e\18\31\10\11output_type\24\3 \1(\9R\10outputType\0188\10\7options\24\4".. 1061 | " \1(\0112\30.google.protobuf.MethodOptionsR\7options\0180\10\16client_s".. 1062 | "treaming\24\5 \1(\8:\5falseR\15clientStreaming\0180\10\16server_streami".. 1063 | "ng\24\6 \1(\8:\5falseR\15serverStreaming\"\145\9\10\11FileOptions\18!".. 1064 | "\10\12java_package\24\1 \1(\9R\11javaPackage\0180\10\20java_outer_class".. 1065 | "name\24\8 \1(\9R\18javaOuterClassname\0185\10\19java_multiple_files\24".. 1066 | "\10 \1(\8:\5falseR\17javaMultipleFiles\18D\10\29java_generate_equals_an".. 1067 | "d_hash\24\20 \1(\8B\2\24\1R\25javaGenerateEqualsAndHash\18:\10\22java_s".. 1068 | "tring_check_utf8\24\27 \1(\8:\5falseR\19javaStringCheckUtf8\18S\10\12op".. 1069 | "timize_for\24\9 \1(\0142).google.protobuf.FileOptions.OptimizeMode:\5SP".. 1070 | "EEDR\11optimizeFor\18\29\10\10go_package\24\11 \1(\9R\9goPackage\0185".. 1071 | "\10\19cc_generic_services\24\16 \1(\8:\5falseR\17ccGenericServices\0189".. 1072 | "\10\21java_generic_services\24\17 \1(\8:\5falseR\19javaGenericServices".. 1073 | "\0185\10\19py_generic_services\24\18 \1(\8:\5falseR\17pyGenericServices".. 1074 | "\0187\10\20php_generic_services\24* \1(\8:\5falseR\18phpGenericServices".. 1075 | "\18%\10\10deprecated\24\23 \1(\8:\5falseR\10deprecated\18.\10\16cc_enab".. 1076 | "le_arenas\24\31 \1(\8:\4trueR\14ccEnableArenas\18*\10\17objc_class_pref".. 1077 | "ix\24$ \1(\9R\15objcClassPrefix\18)\10\16csharp_namespace\24% \1(\9R\15".. 1078 | "csharpNamespace\18!\10\12swift_prefix\24' \1(\9R\11swiftPrefix\18(\10".. 1079 | "\16php_class_prefix\24( \1(\9R\14phpClassPrefix\18#\10\13php_namespace".. 1080 | "\24) \1(\9R\12phpNamespace\0184\10\22php_metadata_namespace\24, \1(\9R".. 1081 | "\20phpMetadataNamespace\18!\10\12ruby_package\24- \1(\9R\11rubyPackage".. 1082 | "\18X\10\20uninterpreted_option\24\231\7 \3(\0112$.google.protobuf.Unint".. 1083 | "erpretedOptionR\19uninterpretedOption\":\10\12OptimizeMode\18\9\10\5SPE".. 1084 | "ED\16\1\18\13\10\9CODE_SIZE\16\2\18\16\10\12LITE_RUNTIME\16\3*\9\8\232".. 1085 | "\7\16\128\128\128\128\2J\4\8&\16'\"\227\2\10\14MessageOptions\18<\10\23".. 1086 | "message_set_wire_format\24\1 \1(\8:\5falseR\20messageSetWireFormat\18L".. 1087 | "\10\31no_standard_descriptor_accessor\24\2 \1(\8:\5falseR\28noStandardD".. 1088 | "escriptorAccessor\18%\10\10deprecated\24\3 \1(\8:\5falseR\10deprecated".. 1089 | "\18\27\10\9map_entry\24\7 \1(\8R\8mapEntry\18X\10\20uninterpreted_optio".. 1090 | "n\24\231\7 \3(\0112$.google.protobuf.UninterpretedOptionR\19uninterpret".. 1091 | "edOption*\9\8\232\7\16\128\128\128\128\2J\4\8\4\16\5J\4\8\5\16\6J\4\8\6".. 1092 | "\16\7J\4\8\8\16\9J\4\8\9\16\10\"\226\3\10\12FieldOptions\18A\10\5ctype".. 1093 | "\24\1 \1(\0142#.google.protobuf.FieldOptions.CType:\6STRINGR\5ctype\18".. 1094 | "\22\10\6packed\24\2 \1(\8R\6packed\18G\10\6jstype\24\6 \1(\0142$.google".. 1095 | ".protobuf.FieldOptions.JSType:\9JS_NORMALR\6jstype\18\25\10\4lazy\24\5 ".. 1096 | "\1(\8:\5falseR\4lazy\18%\10\10deprecated\24\3 \1(\8:\5falseR\10deprecat".. 1097 | "ed\18\25\10\4weak\24\10 \1(\8:\5falseR\4weak\18X\10\20uninterpreted_opt".. 1098 | "ion\24\231\7 \3(\0112$.google.protobuf.UninterpretedOptionR\19uninterpr".. 1099 | "etedOption\"/\10\5CType\18\10\10\6STRING\16\0\18\8\10\4CORD\16\1\18\16".. 1100 | "\10\12STRING_PIECE\16\2\"5\10\6JSType\18\13\10\9JS_NORMAL\16\0\18\13\10".. 1101 | "\9JS_STRING\16\1\18\13\10\9JS_NUMBER\16\2*\9\8\232\7\16\128\128\128\128".. 1102 | "\2J\4\8\4\16\5\"s\10\12OneofOptions\18X\10\20uninterpreted_option\24".. 1103 | "\231\7 \3(\0112$.google.protobuf.UninterpretedOptionR\19uninterpretedOp".. 1104 | "tion*\9\8\232\7\16\128\128\128\128\2\"\192\1\10\11EnumOptions\18\31\10".. 1105 | "\11allow_alias\24\2 \1(\8R\10allowAlias\18%\10\10deprecated\24\3 \1(\8:".. 1106 | "\5falseR\10deprecated\18X\10\20uninterpreted_option\24\231\7 \3(\0112$.".. 1107 | "google.protobuf.UninterpretedOptionR\19uninterpretedOption*\9\8\232\7".. 1108 | "\16\128\128\128\128\2J\4\8\5\16\6\"\158\1\10\16EnumValueOptions\18%\10".. 1109 | "\10deprecated\24\1 \1(\8:\5falseR\10deprecated\18X\10\20uninterpreted_o".. 1110 | "ption\24\231\7 \3(\0112$.google.protobuf.UninterpretedOptionR\19uninter".. 1111 | "pretedOption*\9\8\232\7\16\128\128\128\128\2\"\156\1\10\14ServiceOption".. 1112 | "s\18%\10\10deprecated\24! \1(\8:\5falseR\10deprecated\18X\10\20uninterp".. 1113 | "reted_option\24\231\7 \3(\0112$.google.protobuf.UninterpretedOptionR\19".. 1114 | "uninterpretedOption*\9\8\232\7\16\128\128\128\128\2\"\224\2\10\13Method".. 1115 | "Options\18%\10\10deprecated\24! \1(\8:\5falseR\10deprecated\18q\10\17id".. 1116 | "empotency_level\24\" \1(\0142/.google.protobuf.MethodOptions.Idempotenc".. 1117 | "yLevel:\19IDEMPOTENCY_UNKNOWNR\16idempotencyLevel\18X\10\20uninterprete".. 1118 | "d_option\24\231\7 \3(\0112$.google.protobuf.UninterpretedOptionR\19unin".. 1119 | "terpretedOption\"P\10\16IdempotencyLevel\18\23\10\19IDEMPOTENCY_UNKNOWN".. 1120 | "\16\0\18\19\10\15NO_SIDE_EFFECTS\16\1\18\14\10\10IDEMPOTENT\16\2*\9\8".. 1121 | "\232\7\16\128\128\128\128\2\"\154\3\10\19UninterpretedOption\18A\10\4na".. 1122 | "me\24\2 \3(\0112-.google.protobuf.UninterpretedOption.NamePartR\4name".. 1123 | "\18)\10\16identifier_value\24\3 \1(\9R\15identifierValue\18,\10\18posit".. 1124 | "ive_int_value\24\4 \1(\4R\16positiveIntValue\18,\10\18negative_int_valu".. 1125 | "e\24\5 \1(\3R\16negativeIntValue\18!\10\12double_value\24\6 \1(\1R\11do".. 1126 | "ubleValue\18!\10\12string_value\24\7 \1(\12R\11stringValue\18'\10\15agg".. 1127 | "regate_value\24\8 \1(\9R\14aggregateValue\26J\10\8NamePart\18\27\10\9na".. 1128 | "me_part\24\1 \2(\9R\8namePart\18!\10\12is_extension\24\2 \2(\8R\11isExt".. 1129 | "ension\"\167\2\10\14SourceCodeInfo\18D\10\8location\24\1 \3(\0112(.goog".. 1130 | "le.protobuf.SourceCodeInfo.LocationR\8location\26\206\1\10\8Location\18".. 1131 | "\22\10\4path\24\1 \3(\5B\2\16\1R\4path\18\22\10\4span\24\2 \3(\5B\2\16".. 1132 | "\1R\4span\18)\10\16leading_comments\24\3 \1(\9R\15leadingComments\18+".. 1133 | "\10\17trailing_comments\24\4 \1(\9R\16trailingComments\18:\10\25leading".. 1134 | "_detached_comments\24\6 \3(\9R\23leadingDetachedComments\"\209\1\10\17G".. 1135 | "eneratedCodeInfo\18M\10\10annotation\24\1 \3(\0112-.google.protobuf.Gen".. 1136 | "eratedCodeInfo.AnnotationR\10annotation\26m\10\10Annotation\18\22\10\4p".. 1137 | "ath\24\1 \3(\5B\2\16\1R\4path\18\31\10\11source_file\24\2 \1(\9R\10sour".. 1138 | "ceFile\18\20\10\5begin\24\3 \1(\5R\5begin\18\16\10\3end\24\4 \1(\5R\3en".. 1139 | "dB~\10\19com.google.protobufB\16DescriptorProtosH\1Z-google.golang.org/".. 1140 | "protobuf/types/descriptorpb\248\1\1\162\2\3GPB\170\2\26Google.Protobuf.".. 1141 | "Reflection" 1142 | 1143 | function Parser.reload() 1144 | assert(pb.load(descriptor_pb), "load descriptor msg failed") 1145 | end 1146 | 1147 | local function do_compile(self, f, ...) 1148 | if self.include_imports then 1149 | local old = self.on_import 1150 | local infos = {} 1151 | function self.on_import(info) 1152 | insert_tab(infos, info) 1153 | end 1154 | local r = f(...) 1155 | insert_tab(infos, r) 1156 | self.on_import = old 1157 | return { file = infos } 1158 | end 1159 | return { file = { f(...) } } 1160 | end 1161 | 1162 | function Parser:compile(s, name) 1163 | if self == Parser then self = Parser.new() end 1164 | local set = do_compile(self, self.parse, self, s, name) 1165 | return pb.encode('.google.protobuf.FileDescriptorSet', set) 1166 | end 1167 | 1168 | function Parser:compilefile(fn) 1169 | if self == Parser then self = Parser.new() end 1170 | local set = do_compile(self, self.parsefile, self, fn) 1171 | return pb.encode('.google.protobuf.FileDescriptorSet', set) 1172 | end 1173 | 1174 | function Parser:load(s, name) 1175 | if self == Parser then self = Parser.new() end 1176 | local ret, pos = pb.load(self:compile(s, name)) 1177 | if ret then return ret, pos end 1178 | error("load failed at offset "..pos) 1179 | end 1180 | 1181 | function Parser:loadfile(fn) 1182 | if self == Parser then self = Parser.new() end 1183 | local ret, pos = pb.load(self:compilefile(fn)) 1184 | if ret then return ret, pos end 1185 | error("load failed at offset "..pos) 1186 | end 1187 | 1188 | Parser.reload() 1189 | 1190 | end 1191 | end 1192 | 1193 | return Parser 1194 | -------------------------------------------------------------------------------- /rockspecs/lua-protobuf-0.5.3-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "lua-protobuf" 2 | version = "0.5.3-1" 3 | source = { 4 | url = "git+https://github.com/starwing/lua-protobuf.git", 5 | tag = "0.5.3" 6 | } 7 | description = { 8 | summary = "protobuf data support for Lua", 9 | detailed = [[ 10 | This project offers a simple C library for basic protobuf wire format encode/decode. 11 | ]], 12 | homepage = "https://github.com/starwing/lua-protobuf", 13 | license = "MIT" 14 | } 15 | dependencies = { 16 | "lua >= 5.1" 17 | } 18 | build = { 19 | type = "builtin", 20 | modules = { 21 | pb = "pb.c", 22 | protoc = "protoc.lua" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /rockspecs/lua-protobuf-scm-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "lua-protobuf" 2 | version = "scm-1" 3 | 4 | source = { 5 | url = "git+https://github.com/starwing/lua-protobuf.git", 6 | } 7 | 8 | description = { 9 | summary = "protobuf data support for Lua", 10 | detailed = [[ 11 | This project offers a simple C library for basic protobuf wire format encode/decode. 12 | ]], 13 | homepage = "https://github.com/starwing/lua-protobuf", 14 | license = "MIT", 15 | } 16 | 17 | dependencies = { 18 | "lua >= 5.1" 19 | } 20 | 21 | build = { 22 | type = "builtin", 23 | modules = { 24 | pb = "pb.c"; 25 | protoc = "protoc.lua"; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /serpent.lua: -------------------------------------------------------------------------------- 1 | local n, v = "serpent", "0.30" -- (C) 2012-17 Paul Kulchenko; MIT License 2 | local c, d = "Paul Kulchenko", "Lua serializer and pretty printer" 3 | local snum = {[tostring(1/0)]='1/0 --[[math.huge]]',[tostring(-1/0)]='-1/0 --[[-math.huge]]',[tostring(0/0)]='0/0'} 4 | local badtype = {thread = true, userdata = true, cdata = true} 5 | local getmetatable = debug and debug.getmetatable or getmetatable 6 | local pairs = function(t) return next, t end -- avoid using __pairs in Lua 5.2+ 7 | local keyword, globals, G = {}, {}, (_G or _ENV) 8 | for _,k in ipairs({'and', 'break', 'do', 'else', 'elseif', 'end', 'false', 9 | 'for', 'function', 'goto', 'if', 'in', 'local', 'nil', 'not', 'or', 'repeat', 10 | 'return', 'then', 'true', 'until', 'while'}) do keyword[k] = true end 11 | for k,v in pairs(G) do globals[v] = k end -- build func to name mapping 12 | for _,g in ipairs({'coroutine', 'debug', 'io', 'math', 'string', 'table', 'os'}) do 13 | for k,v in pairs(type(G[g]) == 'table' and G[g] or {}) do globals[v] = g..'.'..k end end 14 | 15 | local function s(t, opts) 16 | local name, indent, fatal, maxnum = opts.name, opts.indent, opts.fatal, opts.maxnum 17 | local sparse, custom, huge = opts.sparse, opts.custom, not opts.nohuge 18 | local space, maxl = (opts.compact and '' or ' '), (opts.maxlevel or math.huge) 19 | local maxlen, metatostring = tonumber(opts.maxlength), opts.metatostring 20 | local iname, comm = '_'..(name or ''), opts.comment and (tonumber(opts.comment) or math.huge) 21 | local numformat = opts.numformat or "%.17g" 22 | local seen, sref, syms, symn = {}, {'local '..iname..'={}'}, {}, 0 23 | local function gensym(val) return '_'..(tostring(tostring(val)):gsub("[^%w]",""):gsub("(%d%w+)", 24 | -- tostring(val) is needed because __tostring may return a non-string value 25 | function(s) if not syms[s] then symn = symn+1; syms[s] = symn end return tostring(syms[s]) end)) end 26 | local function safestr(s) return type(s) == "number" and tostring(huge and snum[tostring(s)] or numformat:format(s)) 27 | or type(s) ~= "string" and tostring(s) -- escape NEWLINE/010 and EOF/026 28 | or ("%q"):format(s):gsub("\010","n"):gsub("\026","\\026") end 29 | local function comment(s,l) return comm and (l or 0) < comm and ' --[['..select(2, pcall(tostring, s))..']]' or '' end 30 | local function globerr(s,l) return globals[s] and globals[s]..comment(s,l) or not fatal 31 | and safestr(select(2, pcall(tostring, s))) or error("Can't serialize "..tostring(s)) end 32 | local function safename(path, name) -- generates foo.bar, foo[3], or foo['b a r'] 33 | local n = name == nil and '' or name 34 | local plain = type(n) == "string" and n:match("^[%l%u_][%w_]*$") and not keyword[n] 35 | local safe = plain and n or '['..safestr(n)..']' 36 | return (path or '')..(plain and path and '.' or '')..safe, safe end 37 | local alphanumsort = type(opts.sortkeys) == 'function' and opts.sortkeys or function(k, o, n) -- k=keys, o=originaltable, n=padding 38 | local maxn, to = tonumber(n) or 12, {number = 'a', string = 'b'} 39 | local function padnum(d) return ("%0"..tostring(maxn).."d"):format(tonumber(d)) end 40 | table.sort(k, function(a,b) 41 | -- sort numeric keys first: k[key] is not nil for numerical keys 42 | return (k[a] ~= nil and 0 or to[type(a)] or 'z')..(tostring(a):gsub("%d+",padnum)) 43 | < (k[b] ~= nil and 0 or to[type(b)] or 'z')..(tostring(b):gsub("%d+",padnum)) end) end 44 | local function val2str(t, name, indent, insref, path, plainindex, level) 45 | local ttype, level, mt = type(t), (level or 0), getmetatable(t) 46 | local spath, sname = safename(path, name) 47 | local tag = plainindex and 48 | ((type(name) == "number") and '' or name..space..'='..space) or 49 | (name ~= nil and sname..space..'='..space or '') 50 | if seen[t] then -- already seen this element 51 | sref[#sref+1] = spath..space..'='..space..seen[t] 52 | return tag..'nil'..comment('ref', level) end 53 | -- protect from those cases where __tostring may fail 54 | if type(mt) == 'table' then 55 | local to, tr = pcall(function() return mt.__tostring(t) end) 56 | local so, sr = pcall(function() return mt.__serialize(t) end) 57 | if (opts.metatostring ~= false and to or so) then -- knows how to serialize itself 58 | seen[t] = insref or spath 59 | t = so and sr or tr 60 | ttype = type(t) 61 | end -- new value falls through to be serialized 62 | end 63 | if ttype == "table" then 64 | if level >= maxl then return tag..'{}'..comment('maxlvl', level) end 65 | seen[t] = insref or spath 66 | if next(t) == nil then return tag..'{}'..comment(t, level) end -- table empty 67 | if maxlen and maxlen < 0 then return tag..'{}'..comment('maxlen', level) end 68 | local maxn, o, out = math.min(#t, maxnum or #t), {}, {} 69 | for key = 1, maxn do o[key] = key end 70 | if not maxnum or #o < maxnum then 71 | local n = #o -- n = n + 1; o[n] is much faster than o[#o+1] on large tables 72 | for key in pairs(t) do if o[key] ~= key then n = n + 1; o[n] = key end end end 73 | if maxnum and #o > maxnum then o[maxnum+1] = nil end 74 | if opts.sortkeys and #o > maxn then alphanumsort(o, t, opts.sortkeys) end 75 | local sparse = sparse and #o > maxn -- disable sparsness if only numeric keys (shorter output) 76 | for n, key in ipairs(o) do 77 | local value, ktype, plainindex = t[key], type(key), n <= maxn and not sparse 78 | if opts.valignore and opts.valignore[value] -- skip ignored values; do nothing 79 | or opts.keyallow and not opts.keyallow[key] 80 | or opts.keyignore and opts.keyignore[key] 81 | or opts.valtypeignore and opts.valtypeignore[type(value)] -- skipping ignored value types 82 | or sparse and value == nil then -- skipping nils; do nothing 83 | elseif ktype == 'table' or ktype == 'function' or badtype[ktype] then 84 | if not seen[key] and not globals[key] then 85 | sref[#sref+1] = 'placeholder' 86 | local sname = safename(iname, gensym(key)) -- iname is table for local variables 87 | sref[#sref] = val2str(key,sname,indent,sname,iname,true) end 88 | sref[#sref+1] = 'placeholder' 89 | local path = seen[t]..'['..tostring(seen[key] or globals[key] or gensym(key))..']' 90 | sref[#sref] = path..space..'='..space..tostring(seen[value] or val2str(value,nil,indent,path)) 91 | else 92 | out[#out+1] = val2str(value,key,indent,insref,seen[t],plainindex,level+1) 93 | if maxlen then 94 | maxlen = maxlen - #out[#out] 95 | if maxlen < 0 then break end 96 | end 97 | end 98 | end 99 | local prefix = string.rep(indent or '', level) 100 | local head = indent and '{\n'..prefix..indent or '{' 101 | local body = table.concat(out, ','..(indent and '\n'..prefix..indent or space)) 102 | local tail = indent and "\n"..prefix..'}' or '}' 103 | return (custom and custom(tag,head,body,tail,level) or tag..head..body..tail)..comment(t, level) 104 | elseif badtype[ttype] then 105 | seen[t] = insref or spath 106 | return tag..globerr(t, level) 107 | elseif ttype == 'function' then 108 | seen[t] = insref or spath 109 | if opts.nocode then return tag.."function() --[[..skipped..]] end"..comment(t, level) end 110 | local ok, res = pcall(string.dump, t) 111 | local func = ok and "((loadstring or load)("..safestr(res)..",'@serialized'))"..comment(t, level) 112 | return tag..(func or globerr(t, level)) 113 | else return tag..safestr(t) end -- handle all other types 114 | end 115 | local sepr = indent and "\n" or ";"..space 116 | local body = val2str(t, name, indent) -- this call also populates sref 117 | local tail = #sref>1 and table.concat(sref, sepr)..sepr or '' 118 | local warn = opts.comment and #sref>1 and space.."--[[incomplete output with shared/self-references skipped]]" or '' 119 | return not name and body..warn or "do local "..body..sepr..tail.."return "..name..sepr.."end" 120 | end 121 | 122 | local function deserialize(data, opts) 123 | local env = (opts and opts.safe == false) and G 124 | or setmetatable({}, { 125 | __index = function(t,k) return t end, 126 | __call = function(t,...) error("cannot call functions") end 127 | }) 128 | local f, res = (loadstring or load)('return '..data, nil, nil, env) 129 | if not f then f, res = (loadstring or load)(data, nil, nil, env) end 130 | if not f then return f, res end 131 | if setfenv then setfenv(f, env) end 132 | return pcall(f) 133 | end 134 | 135 | local function merge(a, b) if b then for k,v in pairs(b) do a[k] = v end end; return a; end 136 | return { _NAME = n, _COPYRIGHT = c, _DESCRIPTION = d, _VERSION = v, serialize = s, 137 | load = deserialize, 138 | dump = function(a, opts) return s(a, merge({name = '_', compact = true, sparse = true}, opts)) end, 139 | line = function(a, opts) return s(a, merge({sortkeys = true, comment = true}, opts)) end, 140 | block = function(a, opts) return s(a, merge({indent = ' ', sortkeys = true, comment = true}, opts)) end } 141 | -------------------------------------------------------------------------------- /test.lua: -------------------------------------------------------------------------------- 1 | local lu = require "luaunit" 2 | 3 | local pb = require "pb" 4 | local pbio = require "pb.io" 5 | local buffer = require "pb.buffer" 6 | local slice = require "pb.slice" 7 | local conv = require "pb.conv" 8 | local protoc = require "protoc" 9 | 10 | -- local assert_not = lu.assertEvalToFalse 11 | local eq = lu.assertEquals 12 | local table_eq = lu.assertItemsEquals 13 | local fail = lu.assertErrorMsgContains 14 | local is_true = lu.assertIsTrue 15 | 16 | local types = 0 17 | for _ in pb.types() do 18 | types = types + 1 19 | end 20 | pbio.write("pb predefined types: ", tostring(types), "\n") 21 | 22 | local function check_load(chunk, name) 23 | local pbdata = protoc.new():compile(chunk, name) 24 | local ret, offset = pb.load(pbdata) 25 | if not ret then 26 | error("load error at "..offset.. 27 | "\nproto: "..chunk.. 28 | "\ndata: "..buffer(pbdata):tohex()) 29 | end 30 | end 31 | 32 | local function check_msg(name, data, r) 33 | local chunk2, _ = pb.encode(name, data) 34 | local data2 = pb.decode(name, chunk2) 35 | --print("msg: ", name, "<"..chunk2:gsub(".", function(s) 36 | --return ("%02X "):format(s:byte()) 37 | --end)..">") 38 | eq(data2, r or data) 39 | end 40 | 41 | local function withstate(f) 42 | local old = pb.state(nil) 43 | local ok, res = pcall(f, old) 44 | pb.state(old) 45 | assert(ok, res) 46 | end 47 | 48 | _G.test_io = {} do 49 | 50 | function _G.test_io.setup() 51 | pbio.dump("address.proto", [[ 52 | message Phone { 53 | optional string name = 1; 54 | optional int64 phonenumber = 2; 55 | } 56 | message Person { 57 | optional string name = 1; 58 | optional int32 age = 2; 59 | optional string address = 3; 60 | repeated Phone contacts = 4; 61 | } ]]) 62 | end 63 | 64 | function _G.test_io.teardown() 65 | os.remove "address.proto" 66 | os.remove "address.pb" 67 | end 68 | 69 | function _G.test_io.test() 70 | local code = "assert(io.write(require 'pb.io'.read()))" 71 | assert(pbio.dump("t.lua", code)) 72 | local fh = assert(io.popen(arg[-1].." t.lua < t.lua", "r")) 73 | eq(fh:read "*a", code) 74 | fh:close() 75 | assert(os.remove "t.lua") 76 | fail("-not-exists-", function() assert(pbio.read "-not-exists-") end) 77 | 78 | local chunk = protoc.new():compile(pbio.read "address.proto", 79 | "address.proto") 80 | assert(pbio.dump("address.pb", chunk)) 81 | assert(pb.loadfile "address.pb") 82 | assert(pb.type "Person") 83 | eq(pb.type "-not-exists-", nil) 84 | local ft = {} 85 | for name in pb.fields "Person" do 86 | ft[name] = true 87 | end 88 | table_eq(ft, { name=true, age=true,address=true,contacts=true }) 89 | 90 | eq(pb.decode("Person", "\240\255\255\255\255\255\255\255\255\1\1"), {}) 91 | eq(pb.decode("Person", "\240\255\255\255\255\255\255\255\255\255"), {}) 92 | eq(pb.decode("Person", "\240\255\255\255\255"), {}) 93 | eq(pb.decode("Person", "\242\255\255\255\255\1\255"), {}) 94 | eq(pb.decode("Person", "\x71"), {}) 95 | eq(pb.decode("Person", "\x33\1\2\3\4\x34"), {}) 96 | eq(pb.decode("Person", "\x33\1\2\3\4\x44"), {}) 97 | eq(pb.decode("Person", "\x33\1\2\3\4"), {}) 98 | eq(pb.decode("Person", "\x75"), {}) 99 | eq(pb.decode("Person", "\x75\1\1\1\1"), {}) 100 | 101 | fail("type 'Foo' does not exists", function() pb.encode("Foo", {}) end) 102 | fail("type 'Foo' does not exists", function() pb.decode("Foo", "") end) 103 | 104 | fail("string expected for field 'name', got boolean", 105 | function() pb.encode("Person", { name = true }) end) 106 | 107 | fail("type mismatch for field 'name' at offset 2, bytes expected for type string, got varint", 108 | function() pb.decode("Person", "\8\1") end) 109 | 110 | fail("invalid varint value at offset 2", 111 | function() pb.decode("Person", "\16\255") end) 112 | 113 | fail("invalid bytes length: 0 (at offset 2)", 114 | function() pb.decode("Person", "\10\255") end) 115 | 116 | fail("unfinished bytes (len 10 at offset 3)", 117 | function() pb.decode("Person", "\10\10") end) 118 | 119 | local data = { 120 | name = "ilse", 121 | age = 18, 122 | contacts = { 123 | { name = "alice", phonenumber = 12312341234 }, 124 | { name = "bob", phonenumber = 45645674567 } 125 | } 126 | } 127 | check_msg(".Person", data) 128 | 129 | pb.clear() 130 | protoc.reload() 131 | 132 | fail("-not-exists-", function() assert(pb.loadfile "-not-exists-") end) 133 | assert(pb.type ".google.protobuf.FileDescriptorSet") 134 | end 135 | 136 | end 137 | 138 | _G.test_depend = {} do 139 | 140 | function _G.test_depend.setup() 141 | pbio.dump("depend1.proto", [[ 142 | syntax = "proto2"; 143 | 144 | message Depend1Msg { 145 | optional int32 id = 1; 146 | optional string name = 2; 147 | } ]]) 148 | end 149 | 150 | function _G.test_depend.teardown() 151 | os.remove "depend1.proto" 152 | end 153 | 154 | function _G.test_depend.test() 155 | local function load_depend(p) 156 | p:load [[ 157 | syntax = "proto2"; 158 | 159 | import "depend1.proto"; 160 | 161 | message Depend2Msg { 162 | optional Depend1Msg dep1 = 1; 163 | optional int32 other = 2; 164 | } ]] 165 | end 166 | 167 | load_depend(protoc.new()) 168 | local t = { dep1 = { id = 1, name = "foo" }, other = 2 } 169 | check_msg("Depend2Msg", t, { dep1 = {}, other = 2 }) 170 | 171 | eq(protoc.new():loadfile "depend1.proto", true) 172 | local chunk = pb.encode("Depend2Msg", t) 173 | check_msg("Depend2Msg", t) 174 | 175 | pb.clear "Depend1Msg" 176 | check_msg("Depend2Msg", t, { other = 2 }) 177 | table_eq(pb.decode("Depend2Msg", chunk), { other = 2 }) 178 | 179 | eq(protoc.new():loadfile "depend1.proto", true) 180 | check_msg("Depend2Msg", t) 181 | 182 | pb.clear "Depend1Msg" 183 | pb.clear "Depend2Msg" 184 | 185 | local p = protoc.new() 186 | p.include_imports = true 187 | load_depend(p) 188 | check_msg("Depend2Msg", t) 189 | assert(pb.type ".google.protobuf.FileDescriptorSet") 190 | end 191 | 192 | end 193 | 194 | function _G.test_extend() 195 | local P = protoc.new() 196 | 197 | P.unknown_import = true 198 | P.unknown_type = true 199 | assert(P:load([[ 200 | syntax = "proto2"; 201 | import "descriptor.proto"; 202 | 203 | extend google.protobuf.EnumValueOptions { 204 | optional string name = 51000; 205 | } 206 | 207 | message Extendable { 208 | extend NestExtend { 209 | optional string ext_name = 100; 210 | } 211 | optional int32 id = 1; 212 | extensions 100 to max; 213 | } ]], "extend1.proto")) 214 | eq(pb.type "Extendable", ".Extendable") 215 | 216 | P.unknown_import = nil 217 | P.unknown_type = nil 218 | local chunk = assert(P:compile [[ 219 | syntax = "proto2"; 220 | import "extend1.proto" 221 | 222 | enum MyEnum { 223 | First = 1 [(name) = "first"]; 224 | Second = 2 [(name) = "second"]; 225 | Third = 3 [(name) = "third"]; 226 | } 227 | 228 | message NestExtend { 229 | optional uint32 id = 1; 230 | extensions 100 to max; 231 | } 232 | 233 | extend Extendable { 234 | optional string ext_name = 100; 235 | } ]]) 236 | assert(pb.load(chunk)) 237 | assert(pb.type "MyEnum") 238 | 239 | local t = { ext_name = "foo", id = 10 } 240 | check_msg("Extendable", t) 241 | 242 | local t2 = { ext_name = "foo", id = 10 } 243 | check_msg("NestExtend", t2) 244 | 245 | local data = pb.decode("google.protobuf.FileDescriptorSet", chunk) 246 | eq(data.file[1].enum_type[1].value[1].options.name, "first") 247 | eq(data.file[1].enum_type[1].value[2].options.name, "second") 248 | eq(data.file[1].enum_type[1].value[3].options.name, "third") 249 | assert(pb.type ".google.protobuf.FileDescriptorSet") 250 | end 251 | 252 | function _G.test_type() 253 | pb.clear "not-exists" 254 | 255 | fail("type '' does not exists", function() 256 | pb.decode("", "") 257 | end) 258 | 259 | check_load [[ 260 | message TestTypes { 261 | optional double dv = 1; 262 | optional float fv = 2; 263 | optional int64 i64v = 3; 264 | optional uint64 u64v = 4; 265 | optional int32 i32v = 5; 266 | optional uint32 u32v = 13; 267 | optional fixed64 f64v = 6; 268 | optional fixed32 f32v = 7; 269 | optional bool bv = 8; 270 | optional string sv = 9; 271 | optional bytes btv = 12; 272 | optional sfixed32 sf32v = 15; 273 | optional sfixed64 sf64v = 16; 274 | optional sint32 s32v = 17; 275 | optional sint64 s64v = 18; 276 | } ]] 277 | 278 | local data = { 279 | dv = 0.125; 280 | fv = 0.5; 281 | i64v = -12345678901234567; 282 | u64v = 12345678901234567; 283 | i32v = -2101112222; 284 | u32v = 2101112222; 285 | f64v = 12345678901234567; 286 | f32v = 1231231234; 287 | bv = true; 288 | sv = "foo"; 289 | btv = "bar"; 290 | sf32v = -123; 291 | sf64v = -456; 292 | s32v = -1234; 293 | s64v = -4321; 294 | } 295 | 296 | check_msg(".TestTypes", data) 297 | pb.clear "TestTypes" 298 | 299 | check_load [[ 300 | message test_type { 301 | optional uint32 r = 1; 302 | repeated uint64 r64 = 2; 303 | } 304 | message test2 { 305 | optional test_type test_type = 1; 306 | } ]] 307 | 308 | pb.option "int64_as_string" 309 | local data = { 310 | r64 = { 311 | 1231234123, 312 | "#45645674567", 313 | "#18446744073709551615" 314 | } 315 | } 316 | check_msg(".test_type", data) 317 | pb.option "int64_as_number" 318 | 319 | check_msg("test_type", {r = 1}) 320 | pb.clear "test_type" 321 | pb.clear "test2" 322 | assert(pb.type ".google.protobuf.FileDescriptorSet") 323 | end 324 | 325 | function _G.test_default() 326 | withstate(function() 327 | protoc.reload() 328 | check_load [[ 329 | enum TestDefaultColor { 330 | RED = 0; 331 | GREEN = 1; 332 | BLUE = 2; 333 | } 334 | message TestDefault { 335 | // some fields here 336 | optional int32 foo = 1; 337 | 338 | optional uint32 defaulted_uint = 18 [ default = 666 ]; 339 | optional int32 defaulted_int = 10 [ default = 777 ]; 340 | optional bool defaulted_bool = 11 [ default = true ]; 341 | optional string defaulted_str = 12 [ default = "foo" ]; 342 | optional float defaulted_num = 13 [ default = 0.125 ]; 343 | 344 | optional TestDefaultColor color = 14 [default = RED]; 345 | optional bool bool1 = 15 [default=false]; 346 | optional bool bool2 = 16 [default=foo]; 347 | repeated int32 array = 17; 348 | } ]] 349 | check_msg("TestDefault", { foo = 1 }) 350 | local function copy_no_meta(t) 351 | local r = {} 352 | for k, v in pairs(t) do 353 | if k ~= "__index" then 354 | r[k] = v 355 | end 356 | end 357 | return r 358 | end 359 | pb.option "enum_as_value" 360 | table_eq(copy_no_meta(pb.defaults "TestDefault"), { 361 | defaulted_uint = 666, 362 | defaulted_int = 777, 363 | defaulted_bool = true, 364 | defaulted_str = "foo", 365 | defaulted_num = 0.125, 366 | color = 0, 367 | bool1 = false, 368 | bool2 = nil 369 | }) 370 | pb.option "enum_as_name" 371 | pb.defaults("TestDefault", nil) 372 | table_eq(copy_no_meta(pb.defaults "TestDefault"), { 373 | defaulted_uint = 666, 374 | defaulted_int = 777, 375 | defaulted_bool = true, 376 | defaulted_str = "foo", 377 | defaulted_num = 0.125, 378 | color = "RED", 379 | bool1 = false, 380 | bool2 = nil 381 | }) 382 | pb.clear "TestDefault" 383 | pb.clear "TestDefaultColor" 384 | fail("type not found", 385 | function() pb.defaults "-invalid-type-" end) 386 | 387 | check_load [[ 388 | syntax = "proto3"; 389 | enum TestDefaultColor { 390 | RED = 0; 391 | GREEN = 1; 392 | BLUE = 2; 393 | } 394 | message TestNest{} 395 | message TestNest1 { 396 | TestNest nest = 1; 397 | } 398 | message TestNest2 { 399 | TestNest1 nest = 1; 400 | } 401 | message TestNest3 { 402 | TestNest2 nest = 1; 403 | } 404 | message TestDefault { 405 | // some fields here 406 | int32 foo = 1; 407 | 408 | int32 defaulted_int = 10; 409 | bool defaulted_bool = 11; 410 | string defaulted_str = 12; 411 | float defaulted_num = 13; 412 | 413 | TestDefaultColor color = 14; 414 | bool bool1 = 15; 415 | bool bool2 = 16; 416 | TestNest nest = 17; 417 | repeated int32 array = 18; 418 | } ]] 419 | 420 | pb.option "decode_default_message" 421 | local dt = pb.decode("TestNest3", "") 422 | table_eq(dt, {nest={nest={nest={}}}}) 423 | pb.option "no_decode_default_message" 424 | 425 | local _, _, _, _, rep = pb.field("TestDefault", "foo") 426 | eq(rep, "optional") 427 | table_eq(copy_no_meta(pb.defaults "TestDefault"), { 428 | defaulted_int = 0, 429 | defaulted_bool = false, 430 | defaulted_str = "", 431 | defaulted_num = 0.0, 432 | color = "RED", 433 | bool1 = false, 434 | bool2 = false, 435 | }) 436 | pb.option "enum_as_value" 437 | pb.defaults("TestDefault", nil) 438 | table_eq(copy_no_meta(pb.defaults "TestDefault"), { 439 | defaulted_int = 0, 440 | defaulted_bool = false, 441 | defaulted_str = "", 442 | defaulted_num = 0.0, 443 | color = 0, 444 | bool1 = false, 445 | bool2 = false, 446 | }) 447 | 448 | pb.option "use_default_metatable" 449 | local dt = pb.decode("TestDefault", "") 450 | eq(getmetatable(dt), pb.defaults "TestDefault") 451 | eq(dt.defaulted_int, 0) 452 | eq(dt.defaulted_bool, false) 453 | eq(dt.defaulted_str, "") 454 | eq(dt.defaulted_num, 0.0) 455 | eq(dt.color, 0) 456 | eq(dt.bool1, false) 457 | eq(dt.bool2, false) 458 | table_eq(dt.array, {}) 459 | table_eq(pb.decode "TestDefault", pb.decode("TestDefault", "")) 460 | 461 | pb.option "use_default_values" 462 | dt = pb.decode("TestDefault", "") 463 | eq(getmetatable(dt), nil) 464 | table_eq(dt, { 465 | defaulted_int = 0, 466 | defaulted_bool = false, 467 | defaulted_str = "", 468 | defaulted_num = 0.0, 469 | color = 0, 470 | bool1 = false, 471 | bool2 = false, 472 | array = {} 473 | }) 474 | eq(dt.defaulted_int, 0) 475 | eq(dt.defaulted_bool, false) 476 | eq(dt.defaulted_str, "") 477 | eq(dt.defaulted_num, 0.0) 478 | eq(dt.color, 0) 479 | eq(dt.bool1, false) 480 | eq(dt.bool2, false) 481 | table_eq(dt.array, {}) 482 | 483 | pb.option "no_default_values" 484 | pb.option "encode_default_values" 485 | pb.option "decode_default_array" 486 | local dt = pb.decode("TestDefault", "") 487 | eq(getmetatable(dt), nil) 488 | table_eq(dt,{ array = {} }) 489 | local chunk2, _ = pb.encode("TestDefault", {defaulted_int = 0,defaulted_bool = true}) 490 | local dt = pb.decode("TestDefault", chunk2) 491 | eq(dt.defaulted_int, 0) 492 | eq(dt.defaulted_bool, true) 493 | eq(dt.defaulted_str, nil) 494 | eq(dt.defaulted_num, nil) 495 | eq(dt.color, nil) 496 | eq(dt.bool1, nil) 497 | eq(dt.bool2, nil) 498 | table_eq(dt.array, {}) 499 | 500 | pb.option "no_encode_default_values" 501 | pb.option "no_decode_default_array" 502 | pb.option "no_default_values" 503 | 504 | pb.option "enum_as_name" 505 | pb.clear "TestDefault" 506 | pb.clear "TestNest" 507 | pb.option "auto_default_values" 508 | end) 509 | end 510 | 511 | function _G.test_enum() 512 | check_load [[ 513 | enum Color { 514 | Red = 0; 515 | Green = 1; 516 | Blue = 2; 517 | } 518 | message TestEnum { 519 | optional Color color = 1; 520 | } ]] 521 | eq(pb.enum("Color", 0), "Red") 522 | eq(pb.enum("Color", "Red"), 0) 523 | eq(pb.enum("Color", 1), "Green") 524 | eq(pb.enum("Color", "Green"), 1) 525 | local t = {} 526 | for name, number in pb.fields "Color" do 527 | t[name] = number 528 | end 529 | table_eq(t, {Red=0, Green=1, Blue=2}) 530 | eq({pb.field("TestEnum", "color")}, {"color", 1, ".Color", nil, "optional"}) 531 | 532 | local data = { color = "Red" } 533 | check_msg("TestEnum", data) 534 | 535 | local data2 = { color = 123 } 536 | check_msg("TestEnum", data2) 537 | 538 | pb.option "enum_as_value" 539 | check_msg("TestEnum", data, { color = 0 }) 540 | 541 | pb.option "int64_as_string" 542 | check_msg("TestEnum", { color = "#18446744073709551615" }) 543 | pb.option "int64_as_number" 544 | 545 | pb.option "enum_as_name" 546 | check_msg("TestEnum", data, { color = "Red" }) 547 | 548 | fail("invalid varint value at offset 2", 549 | function() pb.decode("TestEnum", "\8\255") end) 550 | fail("number/string expected at enum field 'color', got boolean", 551 | function() pb.encode("TestEnum", { color = true }) end) 552 | fail("can not encode unknown enum 'foo' at field 'color'", 553 | function() pb.encode("TestEnum", { color = "foo" }) end) 554 | 555 | check_load [[ 556 | message TestAlias { 557 | enum AliasedEnum { 558 | option allow_alias = true; 559 | ZERO = 0; 560 | ONE = 1; 561 | TWO = 2; 562 | FIRST = 1; 563 | } 564 | repeated AliasedEnum aliased_enumf = 2; 565 | } ]] 566 | check_msg("TestAlias", 567 | { aliased_enumf = { "ZERO", "FIRST", "TWO", 23, "ONE" } }, 568 | { aliased_enumf = { "ZERO", "FIRST", "TWO", 23, "FIRST" } }) 569 | assert(pb.type ".google.protobuf.FileDescriptorSet") 570 | end 571 | 572 | function _G.test_packed() 573 | check_load [[ 574 | message Empty {} 575 | message TestPacked { 576 | repeated int64 packs = 1 [packed=true]; 577 | } ]] 578 | 579 | local data = { 580 | packs = { 1,2,3,4,-1,-2,3 } 581 | } 582 | check_msg(".TestPacked", data) 583 | fail("table expected at field 'packs', got boolean", 584 | function() pb.encode("TestPacked", { packs = true }) end) 585 | 586 | local data = {packs = {}} 587 | check_msg(".TestPacked", data, {packs = {}}) 588 | 589 | local hasEmpty 590 | for _, name in pb.types() do 591 | if name == "Empty" then 592 | hasEmpty = true 593 | break 594 | end 595 | end 596 | assert(hasEmpty) 597 | 598 | pb.clear "TestPacked" 599 | pb.clear "Empty" 600 | eq(pb.type("TestPacked"), nil) 601 | for _, name in pb.types() do 602 | assert(name ~= "TestPacked", name) 603 | assert(name ~= "Empty", name) 604 | end 605 | eq(pb.types()(nil, "not-exists"), nil) 606 | 607 | check_load [[ 608 | syntax="proto3"; 609 | 610 | message MyMessage 611 | { 612 | repeated int32 intList = 1; 613 | repeated int32 nopacks = 2 [packed=false]; 614 | } ]] 615 | check_msg(".MyMessage", {intList={}}, {intList={}, nopacks={}}) 616 | local b = pb.encode("MyMessage", { intList = { 1,2,3 } }) 617 | eq(pb.tohex(b), "0A 03 01 02 03") 618 | local b = pb.encode("MyMessage", { nopacks = { 1,2,3 } }) 619 | eq(pb.tohex(b), "10 01 10 02 10 03") 620 | 621 | check_load [[ 622 | syntax="proto3"; 623 | 624 | message MessageA 625 | { 626 | int32 intValue = 1; 627 | } 628 | message MessageB 629 | { 630 | repeated MessageA messageValue = 1; 631 | } ]] 632 | pb.option "use_default_values" 633 | check_msg("MessageB", { messageValue = { { intValue = 0 } } }) 634 | pb.option "no_default_values" 635 | check_msg("MessageB", { messageValue = { {} } }) 636 | check_msg("MessageB", { messageValue = { { intValue = 1 } } }) 637 | eq(pb.tohex(pb.encode( 638 | "MessageB", { messageValue = { {} } })), "0A 00") 639 | eq(pb.tohex(pb.encode( 640 | "MessageB", { messageValue = { { intValue = 1 } } })), "0A 02 08 01") 641 | pb.clear "MessageA" 642 | pb.clear "MessageB" 643 | 644 | check_load [[ 645 | syntax="proto3"; 646 | message Message3 647 | { 648 | repeated int32 v1 = 1; 649 | } ]] 650 | check_load [[ 651 | message Message2 652 | { 653 | repeated int32 v1 = 1; 654 | } ]] 655 | local t = { v1 = {1,2,3,4,5} } 656 | local bytes = pb.encode("Message2", t) 657 | eq(pb.decode("Message3", bytes), t) 658 | bytes = pb.encode("Message3", t) 659 | eq(pb.decode("Message2", bytes), t) 660 | pb.clear "Message2" 661 | pb.clear "Message3" 662 | pb.option "auto_default_values" 663 | assert(pb.type ".google.protobuf.FileDescriptorSet") 664 | end 665 | 666 | function _G.test_map() 667 | check_load [[ 668 | syntax = "proto3"; 669 | message TestEmpty {} 670 | message TestNum { 671 | int32 f = 1; 672 | } 673 | message TestMap { 674 | map map = 1; 675 | map packed_map = 2 [packed=true]; 676 | map msg_map = 3; 677 | } ]] 678 | 679 | check_msg("TestMap", { map = {}, packed_map = {}, msg_map = {} }) 680 | check_msg("TestMap", { map = {}, packed_map = {}, msg_map = { [""] = {} } }) 681 | 682 | check_msg(".TestMap", { 683 | map = { one = 1, two = 2, three = 3 }; 684 | packed_map = { one = 1, two = 2, three = 3 } 685 | }, { 686 | map = { one = 1, two = 2, three = 3 }; 687 | packed_map = { one = 1, two = 2, three = 3 }; 688 | msg_map = {} 689 | }) 690 | 691 | local data2 = { map = { one = 1, [1]=1 } } 692 | fail("string expected for field 'key', got number", function() 693 | local chunk = pb.encode("TestMap", data2) 694 | table_eq(pb.decode("TestMap", chunk), { 695 | map = {one = 1}, 696 | packed_map = {}, 697 | msg_map = {}, 698 | }) 699 | end) 700 | fail("type mismatch for repeated field 'map' at offset 2,".. 701 | " bytes expected for type message, got varint", function() 702 | local chunk = pb.encode("TestNum", {f = 123}) 703 | pb.decode("TestMap", chunk) 704 | end) 705 | eq(pb.decode("TestMap", "\10\4\3\10\1\1"), { 706 | map = {["\1"] = 0}, 707 | packed_map = {}, 708 | msg_map = {}, 709 | }) 710 | eq(pb.decode("TestMap", "\10\0"), { 711 | map = { [""] = 0 }, 712 | packed_map = {}, 713 | msg_map = {} 714 | }) 715 | eq(pb.decode("TestMap", "\26\0"), { 716 | map = {}, 717 | packed_map = {}, 718 | msg_map = {[""] = {}} 719 | }) 720 | 721 | check_load [[ 722 | syntax = "proto2"; 723 | message TestMap2 { 724 | map map = 1; 725 | repeated uint32 arr = 2; 726 | } ]] 727 | check_msg("TestMap2", { map = { one = 1, two = 2, three = 3 } }) 728 | local map_meta = assert(pb.defaults "*map") 729 | local arr_meta = assert(pb.defaults "*arr") 730 | pb.option "decode_default_array" 731 | local data = pb.decode("TestMap2", "") 732 | eq(getmetatable(data.map), map_meta) 733 | eq(getmetatable(data.arr), arr_meta) 734 | pb.defaults("*map", nil) 735 | pb.defaults("*arr", nil) 736 | data = pb.decode("TestMap2", "") 737 | eq(getmetatable(data.map), nil) 738 | eq(getmetatable(data.arr), nil) 739 | pb.option "no_decode_default_array" 740 | eq(pb.defaults("*map", map_meta), nil) 741 | eq(pb.defaults("*arr", arr_meta), nil) 742 | data = pb.decode("TestMap2", "\10\0\18\0") 743 | eq(getmetatable(data.map), map_meta) 744 | eq(getmetatable(data.arr), arr_meta) 745 | local own_arr_meta, own_map_meta = {}, {} 746 | local t = { 747 | arr=setmetatable({},own_arr_meta), 748 | map=setmetatable({},own_map_meta) 749 | } 750 | data = pb.decode("TestMap2", "\10\0\18\0", t) 751 | eq(getmetatable(data.map), own_map_meta) 752 | eq(getmetatable(data.arr), own_arr_meta) 753 | pb.defaults("*map", nil) 754 | pb.defaults("*arr", nil) 755 | data = pb.decode("TestMap2", "\10\0") 756 | eq(getmetatable(data.map), nil) 757 | eq(getmetatable(data.arr), nil) 758 | end 759 | 760 | function _G.test_oneof() 761 | check_load [[ 762 | syntax = "proto3"; 763 | message TO_M1 { 764 | } 765 | message TO_M2 { 766 | } 767 | message TO_M3 { 768 | int32 value = 1; 769 | } 770 | message TestOneof { 771 | oneof body_oneof { 772 | TO_M1 m1 = 100; 773 | TO_M2 m2 = 200; 774 | TO_M3 m3 = 300; 775 | } 776 | } ]] 777 | check_msg("TestOneof", {}) 778 | check_msg("TestOneof", { m1 = {}, body_oneof = "m1" }) 779 | check_msg("TestOneof", { m2 = {}, body_oneof = "m2" }) 780 | check_msg("TestOneof", { m3 = { value = 0 }, body_oneof = "m3" }) 781 | check_msg("TestOneof", { m3 = { value = 10 }, body_oneof = "m3" }) 782 | pb.clear "TestOneof" 783 | 784 | check_load [[ 785 | syntax = "proto3"; 786 | message TestOneof { 787 | oneof body { 788 | uint32 foo = 1; 789 | string bar = 2; 790 | } 791 | } 792 | message Outter { 793 | TestOneof msg = 1; 794 | } 795 | ]] 796 | 797 | check_msg("TestOneof", { foo = 0, body = "foo" }) 798 | check_msg("TestOneof", { bar = "", body = "bar" }) 799 | local chunk = pb.encode("TestOneof", { foo = 0, bar = "" }) 800 | local data = pb.decode("TestOneof", chunk) 801 | is_true(data.body == "foo" or data.body == "bar") 802 | check_msg("Outter", { msg = { foo = 0, body = "foo" }}) 803 | check_msg("Outter", { msg = { bar = "", body = "bar" }}) 804 | local chunk = pb.encode("Outter", {msg = { foo = 0, bar = "" }}) 805 | local data = pb.decode("Outter", chunk) 806 | is_true(data.msg.body == "foo" or data.msg.body == "bar") 807 | pb.clear "TestOneof" 808 | pb.clear "Outter" 809 | 810 | check_load [[ 811 | syntax = "proto3"; 812 | message TestOneof { 813 | oneof test_oneof { 814 | string name = 4; 815 | int32 value = 5; 816 | } 817 | } ]] 818 | 819 | check_msg("TestOneof", { name = "foo", test_oneof = "name" }) 820 | check_msg("TestOneof", { value = 0, test_oneof = "value" }) 821 | local chunk = pb.encode("TestOneof", { name = "foo", value = 0 }) 822 | local data = pb.decode("TestOneof", chunk) 823 | is_true(data.test_oneof == "name" or data.test_oneof == "value") 824 | 825 | eq(pb.field("TestOneof", "name"), "name") 826 | pb.clear("TestOneof", "name") 827 | eq(pb.field("TestOneof", "name"), nil) 828 | eq(pb.type "TestOneof", ".TestOneof") 829 | pb.clear "TestOneof" 830 | eq(pb.type "TestOneof", nil) 831 | pb.option "auto_default_values" 832 | assert(pb.type ".google.protobuf.FileDescriptorSet") 833 | end 834 | 835 | function _G.test_conv() 836 | eq(conv.encode_uint32(-1), 0xFFFFFFFF) 837 | eq(conv.decode_uint32(0xFFFFFFFF), 0xFFFFFFFF) 838 | eq(conv.decode_uint32(conv.encode_uint32(-1)), 0xFFFFFFFF) 839 | 840 | pb.option "int64_as_string" 841 | eq(conv.encode_int32(-1), "#18446744073709551615") 842 | pb.option "int64_as_number" 843 | 844 | eq(conv.encode_int32(0x12300000123), 0x123) 845 | eq(conv.encode_int32(0xFFFFFFFF), -1) 846 | eq(conv.encode_int32(0x123FFFFFFFF), -1) 847 | eq(conv.encode_int32(0x123FFFFFFFE), -2) 848 | eq(conv.decode_int32(0x12300000123), 0x123) 849 | eq(conv.decode_int32(0xFFFFFFFF), -1) 850 | eq(conv.decode_int32(0x123FFFFFFFF), -1) 851 | eq(conv.decode_int32(0x123FFFFFFFE), -2) 852 | eq(conv.decode_int32(conv.encode_int32(-1)), -1) 853 | 854 | eq(conv.encode_sint32(0), 0) 855 | eq(conv.encode_sint32(-1), 1) 856 | eq(conv.encode_sint32(1), 2) 857 | eq(conv.encode_sint32(-2), 3) 858 | eq(conv.encode_sint32(2), 4) 859 | eq(conv.encode_sint32(-3), 5) 860 | eq(conv.encode_sint32(-123), 245) 861 | eq(conv.encode_sint32(123), 246) 862 | eq(conv.encode_sint64(-123), 245) 863 | eq(conv.encode_sint64(123), 246) 864 | 865 | eq(conv.decode_sint32(0), 0) 866 | eq(conv.decode_sint32(1), -1) 867 | eq(conv.decode_sint32(2), 1) 868 | eq(conv.decode_sint32(3), -2) 869 | eq(conv.decode_sint32(4), 2) 870 | eq(conv.decode_sint32(5), -3) 871 | eq(conv.decode_sint32(245), -123) 872 | eq(conv.decode_sint32(246), 123) 873 | eq(conv.decode_sint32(0xFFFFFFFF), -0x80000000) 874 | eq(conv.decode_sint32(0xFFFFFFFE), 0x7FFFFFFF) 875 | eq(conv.decode_sint64(0xFFFFFFFF), -0x80000000) 876 | eq(conv.decode_sint64(0xFFFFFFFE), 0x7FFFFFFF) 877 | 878 | eq(conv.decode_float(conv.encode_float(123.125)), 123.125) 879 | eq(conv.decode_double(conv.encode_double(123.125)), 123.125) 880 | 881 | pb.option "int64_as_string" 882 | eq(conv.decode_sint64(conv.encode_sint64("#1311768467294899695")), "#1311768467294899695") 883 | pb.option "int64_as_hexstring" 884 | eq(conv.decode_sint64(conv.encode_sint64("#0x1234567890ABCDEF")), "#0x1234567890ABCDEF") 885 | pb.option "int64_as_number" 886 | if _VERSION == "Lua 5.3" then 887 | eq(conv.decode_sint64(conv.encode_sint64("#0x1234567890ABCDEF")), 0x1234567890ABCDEF) 888 | else 889 | assert(conv.decode_sint64(conv.encode_sint64("#0x1234567890ABCDEF"))) 890 | end 891 | 892 | eq(conv.encode_sint32('---1'), 1) 893 | fail("number/string expected, got boolean", function() conv.encode_sint64(true) end) 894 | fail("integer format error: '@xyz'", function() conv.encode_sint64('@xyz') end) 895 | assert(pb.type ".google.protobuf.FileDescriptorSet") 896 | end 897 | 898 | function _G.test_buffer() 899 | eq(buffer.pack("vvv", 1,2,3), "\1\2\3") 900 | eq(buffer.tohex(buffer.pack("d", 4294967295)), "FF FF FF FF") 901 | if _VERSION == "Lua 5.3" then 902 | eq(buffer.tohex(buffer.pack("q", 9223372036854775807)), "FF FF FF FF FF FF FF 7F") 903 | else 904 | eq(buffer.tohex(buffer.pack("q", "#9223372036854775807")), "FF FF FF FF FF FF FF 7F") 905 | end 906 | eq(buffer.pack("s", "foo"), "\3foo") 907 | eq(buffer.pack("cc", "foo", "bar"), "foobar") 908 | eq(buffer():pack("vvv", 1,2,3):result(), "\1\2\3") 909 | 910 | eq(buffer("foo", "bar"):result(), "foobar") 911 | eq(buffer.new("foo", "bar"):result(), "foobar") 912 | 913 | eq(pb.fromhex"01 23 456789ABCDEF", "\1\35\69\103\137\171\205\239") 914 | 915 | local b = buffer.new() 916 | b:pack("b", true); eq(b:tohex(-1), "01") 917 | b:pack("f", 0.125); eq(b:tohex(-4), "00 00 00 3E") 918 | b:pack("F", 0.125); eq(b:tohex(-8), "00 00 00 00 00 00 C0 3F") 919 | b:pack("i", 4294967295); eq(b:tohex(-10), "FF FF FF FF FF FF FF FF FF 01") 920 | b:pack("j", 4294967295); eq(b:tohex(-1), "01") 921 | b:pack("u", 4294967295); eq(b:tohex(-5), "FF FF FF FF 0F") 922 | b:pack("x", 4294967295); eq(b:tohex(-4), "FF FF FF FF") 923 | b:pack("y", 4294967295); eq(b:tohex(-4), "FF FF FF FF") 924 | if _VERSION == "Lua 5.3" then 925 | b:pack("I", 9223372036854775807); eq(b:tohex(-9), "FF FF FF FF FF FF FF FF 7F") 926 | b:pack("J", 9223372036854775807); eq(b:tohex(-10), "FE FF FF FF FF FF FF FF FF 01") 927 | b:pack("U", 9223372036854775807); eq(b:tohex(-9), "FF FF FF FF FF FF FF FF 7F") 928 | b:pack("X", 9223372036854775807); eq(b:tohex(-8), "FF FF FF FF FF FF FF 7F") 929 | b:pack("Y", 9223372036854775807); eq(b:tohex(-8), "FF FF FF FF FF FF FF 7F") 930 | else 931 | b:pack("I", "#9223372036854775807"); eq(b:tohex(-9), "FF FF FF FF FF FF FF FF 7F") 932 | b:pack("J", "#9223372036854775807"); eq(b:tohex(-10), "FE FF FF FF FF FF FF FF FF 01") 933 | b:pack("U", "#9223372036854775807"); eq(b:tohex(-9), "FF FF FF FF FF FF FF FF 7F") 934 | b:pack("X", "#9223372036854775807"); eq(b:tohex(-8), "FF FF FF FF FF FF FF 7F") 935 | b:pack("Y", "#9223372036854775807"); eq(b:tohex(-8), "FF FF FF FF FF FF FF 7F") 936 | end 937 | assert(#b ~= 0) 938 | assert(#b:reset() == 0) 939 | assert(tostring(b):match 'pb.Buffer') 940 | 941 | b = buffer.new "foo" 942 | assert(#b == 3) 943 | b:delete() 944 | assert(#b == 0) 945 | b:pack("vvv", 1,2,3) 946 | assert(#b == 3) 947 | 948 | b = buffer.new() 949 | eq(b:pack("(vvv)", 1,2,3):tohex(-4), "03 01 02 03") 950 | eq(b:pack("((vvv))", 1,2,3):tohex(-5), "04 03 01 02 03") 951 | fail("unmatch '(' in format", function() buffer.pack "(" end) 952 | fail("unexpected ')' in format", function() buffer.pack ")" end) 953 | fail("number/'#number' expected for type 'int32', got string", function() buffer.pack("i", "foo") end) 954 | fail("number/'#number' expected for type 'int32', got boolean", function() buffer.pack("i", true) end) 955 | fail("invalid formater: '!'", function() buffer.pack '!' end) 956 | 957 | b = buffer.new() 958 | eq(b:pack("c", ("a"):rep(1025)):result(), ("a"):rep(1025)) 959 | eq(b:pack("c", ("b"):rep(1025)):result(), ("a"):rep(1025)..("b"):rep(1025)) 960 | eq(#b, 2050) 961 | b:reset("foo", "bar") 962 | eq(#b, 6) 963 | 964 | fail("integer format error: 'foo'", function() buffer.pack("v", "foo") end) 965 | if _VERSION == "Lua 5.3" or _VERSION == "Lua 5.4" then 966 | fail("integer format error", function() buffer.pack("v", 1e308) end) 967 | else 968 | fail("number has no integer representation", function() buffer.pack("v", 1e308) end) 969 | end 970 | 971 | b = buffer.new() 972 | fail("encode bytes failed", function() b:pack("#", 10) end) 973 | check_load [[ 974 | message Test { optional int32 value = 1 } 975 | ]] 976 | local len = #b 977 | eq(#b, 0) 978 | eq(pb.encode("Test", { value = 1 }, b), b) 979 | eq(#b, 2) 980 | b:pack("#", len) 981 | eq(b:tohex(), "02 08 01") 982 | 983 | b = buffer.new() 984 | eq(b:pack("i", -1):tohex(), "FF FF FF FF FF FF FF FF FF 01") 985 | assert(pb.type ".google.protobuf.FileDescriptorSet") 986 | end 987 | 988 | function _G.test_slice() 989 | local s = slice.new "\3\1\2\3" 990 | eq(#s, 4) 991 | eq(s:level(), 1) 992 | eq(s:level(1), 1) 993 | eq(s:level(2), nil) 994 | eq({s:level(-1)}, {1,1,4}) 995 | eq(s:enter(), s) 996 | eq(s:level(), 2) 997 | eq({s:level(-1)}, {2,2,4}) 998 | eq({s:level(1)}, {5,1,4}) 999 | eq({s:unpack "vvv"}, {1,2,3}) 1000 | eq(s:unpack "v", nil) 1001 | eq(s:leave(), s) 1002 | eq(s:unpack("+s", -4), "\1\2\3") 1003 | 1004 | s = slice "\3\1\2\3" 1005 | for i = 2, 20 do 1006 | s:enter(1, 4); eq(s:level(), i) 1007 | end 1008 | for i = 19, 1 do 1009 | s:leave(); eq(s:level(), i) 1010 | end 1011 | eq(s:tohex(), "03 01 02 03") 1012 | eq(s:result(-3), "\1\2\3") 1013 | eq(#s, 4) 1014 | eq(#s:reset(), 0) 1015 | eq(#s:reset"foo", 3) 1016 | 1017 | eq({slice.unpack("\255\1", "v@")}, { 255, 3 }) 1018 | eq({slice.unpack("\1", "v*v", 1)}, { 1, 1 }) 1019 | fail("invalid formater: '!'", function() slice.unpack("\1", '!') end) 1020 | 1021 | table_eq({slice.unpack("\1\2\3", "vvv")}, {1,2,3}) 1022 | eq(slice.unpack("\255\255\255\255", "d"), 4294967295) 1023 | if _VERSION == "Lua 5.3" then 1024 | eq(slice.unpack("\255\255\255\255\255\255\255\127", "q"), 9223372036854775807) 1025 | else 1026 | pb.option 'int64_as_string' 1027 | eq(slice.unpack("\255\255\255\255\255\255\255\127", "q"), '#9223372036854775807') 1028 | pb.option 'int64_as_number' 1029 | end 1030 | eq(slice.unpack("\3foo", "s"), "foo") 1031 | eq({slice.unpack("foobar", "cc", 3, 3)}, {"foo", "bar"}) 1032 | 1033 | eq(slice.unpack("\255\255\255\127\255", "v"), 0xFFFFFFF) 1034 | fail("invalid varint value at offset 1", function() 1035 | slice.unpack(("\255"):rep(10), "v") end) 1036 | fail("invalid varint value at offset 1", function() slice.unpack("\255\255\255", "v") end) 1037 | fail("invalid varint value at offset 1", function() slice.unpack("\255\255\255", "v") end) 1038 | fail("invalid bytes value at offset 1", function() slice.unpack("\3\1\2", "s") end) 1039 | fail("invalid fixed32 value at offset 1", function() slice.unpack("\1\2\3", "d") end) 1040 | fail("invalid fixed64 value at offset 1", function() slice.unpack("\1\2\3", "q") end) 1041 | fail("invalid sub string at offset 1", function() slice.unpack("\3\1\2", "c", 5) end) 1042 | fail("invalid varint value at offset 1", function() slice.unpack("\255\255\255", "i") end) 1043 | fail("invalid fixed32 value at offset 1", function() slice.unpack("\255\255\255", "x") end) 1044 | fail("invalid fixed64 value at offset 1", function() slice.unpack("\255\255\255", "X") end) 1045 | fail("string/buffer/slice expected, got boolean", function() slice.unpack(true, "v") end) 1046 | fail("bytes wireformat expected at offset 1", function() slice"\1":enter() end) 1047 | 1048 | fail("level (3) exceed max level 2", function() 1049 | local s1 = slice.new "\1\2\3" 1050 | s1:enter() 1051 | s1:leave(3) 1052 | end) 1053 | 1054 | s:reset "\1\2\3" 1055 | eq({s:leave()}, {s, 1}) 1056 | eq(s:level(), 1) 1057 | eq({s:level(-1)}, {1,1,3}) 1058 | 1059 | 1060 | assert(tostring(s):match 'pb.Slice') 1061 | assert(pb.type ".google.protobuf.FileDescriptorSet") 1062 | end 1063 | 1064 | function _G.test_typefmt() 1065 | -- load schema from text 1066 | assert(protoc:load [[ 1067 | message Phone { 1068 | optional string name = 1; 1069 | optional int64 phonenumber = 2; 1070 | } 1071 | message Person { 1072 | optional string name = 1; 1073 | optional int32 age = 2; 1074 | optional string address = 3; 1075 | repeated Phone contacts = 4; 1076 | } ]]) 1077 | 1078 | -- lua table data 1079 | local data = { 1080 | name = "ilse", 1081 | age = 18, 1082 | contacts = { 1083 | { name = "alice", phonenumber = 12312341234 }, 1084 | { name = "bob", phonenumber = 45645674567 } 1085 | } 1086 | } 1087 | 1088 | local bytes = assert(pb.encode("Person", data)) 1089 | local s = require "pb.slice".new(bytes) 1090 | local function decode(type, str, d) 1091 | while #str > 0 do 1092 | local _, tag = str:unpack"@v" 1093 | local name, _, pbtype = pb.field(type, math.floor(tag / 8)) 1094 | local fmt = pb.typefmt(pbtype) 1095 | if fmt == "message" then 1096 | str:enter() 1097 | if d[name][1] then 1098 | decode(pbtype, str, d[name][1]) 1099 | table.remove(d[name], 1) 1100 | end 1101 | str:leave() 1102 | else 1103 | assert(d[name] == str:unpack(fmt)) 1104 | end 1105 | end 1106 | end 1107 | decode("Person", s, data) 1108 | 1109 | assert(pb.typefmt'F' == "double" ) 1110 | assert(pb.typefmt'f' == "float" ) 1111 | assert(pb.typefmt'I' == "int64" ) 1112 | assert(pb.typefmt'U' == "uint64" ) 1113 | assert(pb.typefmt'i' == "int32" ) 1114 | assert(pb.typefmt'X' == "fixed64" ) 1115 | assert(pb.typefmt'x' == "fixed32" ) 1116 | assert(pb.typefmt'b' == "bool" ) 1117 | assert(pb.typefmt't' == "string" ) 1118 | assert(pb.typefmt'g' == "group" ) 1119 | assert(pb.typefmt'S' == "message" ) 1120 | assert(pb.typefmt's' == "bytes" ) 1121 | assert(pb.typefmt'u' == "uint32" ) 1122 | assert(pb.typefmt'v' == "enum" ) 1123 | assert(pb.typefmt'y' == "sfixed32") 1124 | assert(pb.typefmt'Y' == "sfixed64") 1125 | assert(pb.typefmt'j' == "sint32" ) 1126 | assert(pb.typefmt'J' == "sint64" ) 1127 | 1128 | assert(pb.typefmt"varint" == 'v') 1129 | assert(pb.typefmt"64bit" == 'q') 1130 | assert(pb.typefmt"bytes" == 's') 1131 | assert(pb.typefmt"gstart" == '!') 1132 | assert(pb.typefmt"gend" == '!') 1133 | assert(pb.typefmt"32bit" == 'd') 1134 | assert(pb.typefmt"double" == 'F') 1135 | assert(pb.typefmt"float" == 'f') 1136 | assert(pb.typefmt"int64" == 'I') 1137 | assert(pb.typefmt"uint64" == 'U') 1138 | assert(pb.typefmt"int32" == 'i') 1139 | assert(pb.typefmt"fixed64" == 'X') 1140 | assert(pb.typefmt"fixed32" == 'x') 1141 | assert(pb.typefmt"bool" == 'b') 1142 | assert(pb.typefmt"string" == 't') 1143 | assert(pb.typefmt"group" == 'g') 1144 | assert(pb.typefmt"bytes" == 's') 1145 | assert(pb.typefmt"uint32" == 'u') 1146 | assert(pb.typefmt"enum" == 'v') 1147 | assert(pb.typefmt"sfixed32" == 'y') 1148 | assert(pb.typefmt"sfixed64" == 'Y') 1149 | assert(pb.typefmt"sint32" == 'j') 1150 | assert(pb.typefmt"sint64" == 'J') 1151 | 1152 | assert(pb.typefmt "whatever" == '!') 1153 | end 1154 | 1155 | function _G.test_load() 1156 | withstate(function() 1157 | protoc.reload() 1158 | assert(protoc:load [[ message Test_Load1 { optional int32 t = 1; } ]]) 1159 | assert(pb.type "Test_Load1") 1160 | assert(protoc:load [[ message Test_Load2 { optional int32 t = 2; } ]]) 1161 | assert(pb.type "Test_Load2") 1162 | protoc.reload() 1163 | local p = protoc.new() 1164 | assert(p:load [[ message Test_Load1 { optional int32 t = 1; } ]]) 1165 | assert(pb.type "Test_Load1") 1166 | assert(p:load [[ message Test_Load2 { optional int32 t = 2; } ]]) 1167 | assert(pb.type "Test_Load2") 1168 | end) 1169 | 1170 | withstate(function(old) 1171 | assert(old.setdefault) 1172 | eq(pb.type ".google.protobuf.FileDescriptorSet", nil) 1173 | eq({pb.load "\16\255\255\1\10\2\18\3"}, {false, 8}) 1174 | pb.state(nil) -- discard previous one 1175 | eq(pb.type ".google.protobuf.FileDescriptorSet", nil) 1176 | 1177 | local buf = buffer.new() 1178 | local function v(n) return n*8 + 0 end 1179 | local function s(n) return n*8 + 2 end 1180 | buf:pack("v(v(vsv(vsvvvvv(vvvv)vv)vvv(vv)v(vv)))vv", 1181 | s(1), -- FileDescriptorSet.file 1182 | s(4), -- FileDescriptorProto.message_type 1183 | s(1), -- DescriptorProto.name 1184 | "load_test", 1185 | s(2), -- DescriptorProto.field 1186 | s(1), -- FieldDescriptorProto.name 1187 | "test_unknown", 1188 | v(3), -- FieldDescriptorProto.number 1189 | 1, 1190 | v(4), -- FieldDescriptorProto.label 1191 | 1, 1192 | s(8), -- FieldDescriptorProto.options 1193 | v(2), -- FieldOptions.packed 1194 | 1, 1195 | v(100), 0, -- unknown field options 1196 | v(100), 0, -- unknown field entry 1197 | v(100), 0, -- unknown type entry 1198 | s(8), -- DescriptorProto.oneof_decl 1199 | v(100), 0, -- unknown oneof entry 1200 | s(7), -- DescriptorProto.options 1201 | v(100), 0, -- unknown type options 1202 | v(100), 0 -- unknown file options 1203 | ) 1204 | eq(pb.load(buf:result()), true) 1205 | fail("unknown type ", function() pb.encode("load_test", { test_unknown = 1 }) end) 1206 | fail(" expected for type , got varint", function() pb.decode("load_test", "\8\1") end) 1207 | fail("unknown type (0)", function() pb.decode("load_test", "\14\1") end) 1208 | 1209 | buf:reset() 1210 | buf:pack("v(v(vsv(vsvvvv)))", 1211 | s(1), s(4), s(1), "load_test", 1212 | s(2), s(1), "test_unknown", v(3), 1, v(4), 1) 1213 | eq(pb.load(buf:result()), true) 1214 | fail("type mismatch for field 'test_unknown' at offset 2, expected for type , got varint", 1215 | function() pb.decode("load_test", "\8\1") end) 1216 | fail("type mismatch for field 'test_unknown' at offset 2, expected for type , got 64bit", 1217 | function() pb.decode("load_test", "\9\1") end) 1218 | fail("type mismatch for field 'test_unknown' at offset 2, expected for type , got bytes", 1219 | function() pb.decode("load_test", "\10\1") end) 1220 | fail("type mismatch for field 'test_unknown' at offset 2, expected for type , got gstart", 1221 | function() pb.decode("load_test", "\11\1") end) 1222 | fail("type mismatch for field 'test_unknown' at offset 2, expected for type , got gend", 1223 | function() pb.decode("load_test", "\12\1") end) 1224 | fail("type mismatch for field 'test_unknown' at offset 2, expected for type , got 32bit", 1225 | function() pb.decode("load_test", "\13\1") end) 1226 | 1227 | buf:reset() 1228 | buf:pack("v(v(vsv(vsvvvv)))", 1229 | s(1), s(4), s(1), "load_test", 1230 | s(2), s(1), "test_unknown", v(3), 2, v(4), 1) 1231 | eq(pb.load(buf:result()), true) 1232 | 1233 | buf:reset() 1234 | buf:pack("v(v(vsv(vsvvvv)))", 1235 | s(1), s(4), s(1), "load_test", 1236 | s(2), s(1), "test_unknown", v(3), 1, v(4), 1) 1237 | eq(pb.load(buf:result()), true) 1238 | 1239 | buf:reset() 1240 | buf:pack("v(v(vsv(vsvvvv)))", 1241 | s(1), s(4), s(1), "load_test", 1242 | s(2), s(1), "test_unknown2", v(3), 1, v(4), 1) 1243 | eq(pb.load(buf:result()), true) 1244 | 1245 | buf:reset() 1246 | buf:pack("v(v(vsv(vsvvvv)))", 1247 | s(1), s(4), s(1), "load_test", 1248 | s(2), s(1), "test_unknown", v(3), 1, v(4), 1) 1249 | eq(pb.load(buf:result()), true) 1250 | 1251 | buf:reset() 1252 | buf:pack("v(v(vsv(vvvv)))", 1253 | s(1), s(4), s(1), "load_test", 1254 | s(2), v(3), 1, v(4), 1) 1255 | eq(pb.load(buf:result()), false) 1256 | 1257 | buf:reset() 1258 | buf:pack("v(v(vsv(vvvvvv)))", 1259 | s(1), s(4), s(1), "load_test", 1260 | s(2), v(3), 1, v(4), 1, v(5), 11) 1261 | eq(pb.load(buf:result()), false) 1262 | 1263 | buf:reset() 1264 | buf:pack("v(v(vsv(vvvv)))", 1265 | s(1), s(4), s(1), "load_test", 1266 | s(6), v(3), 1, v(4), 1) 1267 | eq(pb.load(buf:result()), false) 1268 | 1269 | buf:reset() 1270 | buf:pack("v(v(v(vx)))", s(1), s(4), s(6), v(3), -1) 1271 | eq({pb.load(buf:result())}, { false, 8 }) 1272 | end) 1273 | assert(pb.type ".google.protobuf.FileDescriptorSet") 1274 | end 1275 | 1276 | function _G.test_hook() 1277 | withstate(function() 1278 | protoc.reload() 1279 | check_load [[ 1280 | enum Type { 1281 | HOME = 1; 1282 | WORK = 2; 1283 | } 1284 | message Phone { 1285 | optional string name = 1; 1286 | optional int64 phonenumber = 2; 1287 | optional Type type = 3; 1288 | } 1289 | message Person { 1290 | optional string name = 1; 1291 | optional int32 age = 2; 1292 | optional string address = 3; 1293 | repeated Phone contacts = 4; 1294 | } ]] 1295 | pb.option "enable_hooks" 1296 | assert(pb.hook "Phone" == nil) 1297 | fail("function expected, got boolean", 1298 | function() pb.hook("Phone", true) end) 1299 | fail("type not found", 1300 | function() pb.hook "-invalid-type-" end) 1301 | local function make_hook(name, func) 1302 | local fetch = pb.hook(name) 1303 | local function helper(t) 1304 | return func(name, t) 1305 | end 1306 | local oldh = pb.hook(name, helper) 1307 | assert(fetch == oldh) 1308 | assert(pb.hook(name) == helper) 1309 | end 1310 | local s = {} 1311 | make_hook("Person", function(name, t) 1312 | s[#s+1] = ("(%s|%s)"):format(name, t.name) 1313 | t.hooked = true 1314 | end) 1315 | make_hook("Phone", function(name, t) 1316 | s[#s+1] = ("(%s|%s|%s)"):format(name, t.name, t.phonenumber) 1317 | t.hooked = true 1318 | return t 1319 | end) 1320 | make_hook("Type", function(name, t) 1321 | s[#s+1] = ("(%s|%s)"):format(name, t) 1322 | return { type = name, value = t } 1323 | end) 1324 | local data = { 1325 | name = "ilse", 1326 | age = 18, 1327 | contacts = { 1328 | { name = "alice", type = "HOME", phonenumber = 12312341234 }, 1329 | { name = "bob", type = "WORK", phonenumber = 45645674567 } 1330 | } 1331 | } 1332 | local res = pb.decode("Person", pb.encode("Person", data)) 1333 | s = table.concat(s) 1334 | assert(s == "(Type|HOME)(Phone|alice|12312341234)".. 1335 | "(Type|WORK)(Phone|bob|45645674567)".. 1336 | "(Person|ilse)") 1337 | assert(res.hooked) 1338 | assert(res.contacts[1].hooked) 1339 | assert(res.contacts[2].hooked) 1340 | assert(type(res.contacts[1].type) == "table") 1341 | assert(type(res.contacts[2].type) == "table") 1342 | end) 1343 | end 1344 | 1345 | function _G.test_encode_hook() 1346 | withstate(function() 1347 | protoc.reload() 1348 | check_load [[ 1349 | enum Type { 1350 | HOME = 1; 1351 | WORK = 2; 1352 | } 1353 | message Phone { 1354 | optional string name = 1; 1355 | optional int64 phonenumber = 2; 1356 | optional Type type = 3; 1357 | } 1358 | message Person { 1359 | optional string name = 1; 1360 | optional int32 age = 2; 1361 | optional string address = 3; 1362 | repeated Phone contacts = 4; 1363 | } ]] 1364 | pb.option "enable_hooks" 1365 | pb.option "enable_enchooks" 1366 | assert(pb.encode_hook "Phone" == nil) 1367 | fail("function expected, got boolean", 1368 | function() pb.encode_hook("Phone", true) end) 1369 | fail("type not found", 1370 | function() pb.encode_hook "-invalid-type-" end) 1371 | local function make_encode_hook(name, func) 1372 | local fetch = pb.encode_hook(name) 1373 | local function helper(t) 1374 | return func(name, t) 1375 | end 1376 | local oldh = pb.encode_hook(name, helper) 1377 | assert(fetch == oldh) 1378 | assert(pb.encode_hook(name) == helper) 1379 | end 1380 | local s = {} 1381 | make_encode_hook("Person", function(name, t) 1382 | s[#s+1] = ("(%s|%s)"):format(name, t.name) 1383 | return t 1384 | end) 1385 | make_encode_hook("Phone", function(name, ph) 1386 | ph_name, ty, num = ph:match("(%w+)|(%w+)|(%d+)") 1387 | t = { 1388 | name = ph_name, 1389 | type = ty, 1390 | phonenumber = tonumber(num), 1391 | } 1392 | s[#s+1] = ("(%s|%s|%s)"):format(name, t.name, t.phonenumber) 1393 | return t 1394 | end) 1395 | make_encode_hook("Type", function(name, v) 1396 | local t = v:lower() == v and "HOME" or "WORK" 1397 | s[#s+1] = ("(%s|(%s)%s)"):format(name, v, t) 1398 | return t 1399 | end) 1400 | local data = { 1401 | name = "ilse", 1402 | age = 18, 1403 | contacts = { 1404 | "alice|zzz|12312341234", 1405 | "bob|Grr|45645674567", 1406 | } 1407 | } 1408 | local res = pb.decode("Person", pb.encode("Person", data)) 1409 | s = table.concat(s) 1410 | assert(s == "(Person|ilse)(Phone|alice|12312341234)".. 1411 | "(Type|(zzz)HOME)(Phone|bob|45645674567)".. 1412 | "(Type|(Grr)WORK)") 1413 | end) 1414 | end 1415 | 1416 | function _G.test_unsafe() 1417 | local unsafe = require "pb.unsafe" 1418 | assert(type(unsafe.decode) == "function") 1419 | assert(type(unsafe.use) == "function") 1420 | fail("userdata expected, got boolean", 1421 | function() unsafe.load(true, 1) 1422 | end) 1423 | fail("userdata expected, got boolean", 1424 | function() unsafe.decode("", true, 1) 1425 | end) 1426 | fail("userdata expected, got boolean", 1427 | function() unsafe.slice(true, 1) 1428 | end) 1429 | local s, len = unsafe.touserdata(io.stdin, 0) 1430 | -- s is a null pointer! 1431 | fail("userdata expected, got userdata", 1432 | function() unsafe.slice(s, len) 1433 | end) 1434 | check_load [[ 1435 | message TestType { 1436 | } 1437 | ]] 1438 | s, len = unsafe.touserdata("", 0) 1439 | eq(type(s), "userdata") 1440 | eq(len, 0) 1441 | table_eq(unsafe.decode("TestType", s, len), {}) 1442 | table_eq(pb.decode("TestType", unsafe.slice(s, len)), {}) 1443 | table_eq({unsafe.load(s, len)}, {true , 1}) 1444 | pb.clear "TestType" 1445 | eq((unsafe.use "global"), true) 1446 | eq((unsafe.use "local"), true) 1447 | end 1448 | 1449 | function _G.test_order() 1450 | withstate(function() 1451 | protoc.reload() 1452 | check_load [[ 1453 | enum Type { 1454 | HOME = 1; 1455 | WORK = 2; 1456 | } 1457 | message Phone { 1458 | optional string name = 1; 1459 | optional int64 phonenumber = 2; 1460 | optional Type type = 3; 1461 | } 1462 | message Person { 1463 | optional string name = 1; 1464 | optional int32 age = 2; 1465 | optional string address = 3; 1466 | repeated Phone contacts = 4; 1467 | } ]] 1468 | pb.option "encode_order" 1469 | local data = { 1470 | name = "ilse", 1471 | age = 18, 1472 | contacts = { 1473 | { name = "alice", phonenumber = 12312341234 }, 1474 | { name = "bob", phonenumber = 45645674567 } 1475 | } 1476 | } 1477 | local b1 = pb.encode("Person", data) 1478 | local b2 = pb.encode("Person", data) 1479 | eq(b1, b2) 1480 | end) 1481 | end 1482 | 1483 | function _G.test_pack_unpack() 1484 | withstate(function() 1485 | protoc.reload() 1486 | check_load [[ 1487 | syntax = "proto3"; 1488 | enum Type { 1489 | HOME = 1; 1490 | WORK = 2; 1491 | } 1492 | message Phone { 1493 | string name = 1; 1494 | int64 phonenumber = 2; 1495 | Type type = 3; 1496 | } 1497 | message Friend { 1498 | string name = 1; 1499 | repeated Friend friends = 2; 1500 | } 1501 | message Person { 1502 | // will be sorted by field number 1503 | 1504 | repeated Friend friends = 200; 1505 | map map = 100; 1506 | 1507 | string name = 1; 1508 | int32 age = 2; 1509 | string address = 3; 1510 | repeated Phone contacts = 4; 1511 | } ]] 1512 | 1513 | local function __copy(src) 1514 | if "table" ~= type(src) then return src end 1515 | 1516 | local dst = {} 1517 | for k, v in pairs(src) do 1518 | dst[k] = __copy(v) 1519 | end 1520 | 1521 | return dst 1522 | end 1523 | 1524 | local name = "ilse" 1525 | local age = 18 1526 | local address = "earth" 1527 | local contacts = { 1528 | { name = "alice", phonenumber = 12312341234 }, 1529 | { name = "bob", phonenumber = 45645674567 } 1530 | } 1531 | local map = { 1532 | ["m111111111"] = contacts[1], 1533 | ["m222222222"] = contacts[2] 1534 | } 1535 | local friends = { 1536 | { name = "f10", friends = { 1537 | { name = "f11"}, { name = "f12" } 1538 | }}, 1539 | { name = "f20", friends = { 1540 | { name = "f21"}, { name = "f22" } 1541 | }} 1542 | } 1543 | 1544 | local person = { 1545 | name = name, 1546 | age = age, 1547 | address = address, 1548 | contacts = __copy(contacts), 1549 | map = __copy(map), 1550 | friends = __copy(friends) 1551 | } 1552 | -- fill default value for eq 1553 | for _, m in pairs(person.map) do 1554 | m.type = 0 1555 | end 1556 | for _, m in pairs(person.contacts) do 1557 | m.type = 0 1558 | end 1559 | for _, f in pairs(person.friends) do 1560 | for _, f2 in pairs(f.friends) do 1561 | f2.friends = {} 1562 | end 1563 | end 1564 | local default_contacts = {name = "", phonenumber=0, type=0} 1565 | local default_map = {key = ""} 1566 | local default_friend = {friends = {}, name = ""} 1567 | 1568 | local b1 = pb.pack("Person", name, age, address, contacts, map, friends) 1569 | 1570 | local p = pb.decode("Person", b1) 1571 | eq(person, p) 1572 | 1573 | local n1, a1, e1, c1, m1, f1 = pb.unpack("Person", b1) 1574 | eq(n1, person.name) 1575 | eq(a1, person.age) 1576 | eq(e1, person.address) 1577 | eq(c1, person.contacts) 1578 | eq(m1, person.map) 1579 | eq(f1, person.friends) 1580 | 1581 | local b2 = pb.pack("Person") 1582 | local n2, a2, e2, c2, m2, f2 = pb.unpack("Person", b2) 1583 | eq(n2, "") 1584 | eq(a2, 0) 1585 | eq(e2, "") 1586 | eq(c2, default_contacts) 1587 | eq(m2, default_map) 1588 | eq(f2, default_friend) 1589 | 1590 | local b3 = pb.pack("Person", nil, age, nil, contacts, nil) 1591 | local n3, a3, e3, c3, m3, f3 = pb.unpack("Person", b3) 1592 | eq(n3, "") 1593 | eq(a3, person.age) 1594 | eq(e3, "") 1595 | eq(c3, person.contacts) 1596 | eq(m3, default_map) 1597 | eq(f3, default_friend) 1598 | 1599 | fail("number/'#number' expected for field 'age', got string", 1600 | function() pb.pack("Person", nil, "abc") end) 1601 | fail("bad argument #2 to 'pack' (string expected for field 'name', got number)", 1602 | function() pb.pack("Person", 100, "abc") end) 1603 | fail("type mismatch for field 'name' at offset 2, bytes expected for type string, got 32bit", 1604 | function() pb.unpack("Person", "\13\1") end) 1605 | 1606 | local ub = buffer.new() 1607 | pb.pack("Person", ub, nil, age, nil, contacts, nil) 1608 | 1609 | local us = slice.new(ub:result()) 1610 | local n5, a5, e5, c5, m5, f5 = pb.unpack("Person", us) 1611 | eq(n5, "") 1612 | eq(a5, person.age) 1613 | eq(e5, "") 1614 | eq(c5, person.contacts) 1615 | eq(m5, default_map) 1616 | eq(f5, default_friend) 1617 | 1618 | pb.option "no_default_values" 1619 | local n4, a4, e4, c4, m4, f4 = pb.unpack("Person", b2) 1620 | eq(n4, nil) 1621 | eq(a4, nil) 1622 | eq(e4, nil) 1623 | eq(c4, nil) 1624 | eq(m4, nil) 1625 | eq(f4, nil) 1626 | 1627 | pb.option "enable_hooks" 1628 | pb.option "enable_enchooks" 1629 | 1630 | local hook_contacts = { 1631 | { name = "alice", phonenumber = 123456789 }, 1632 | } 1633 | local hook_count = 0 1634 | pb.encode_hook("Person", function(v) 1635 | hook_count = hook_count + 1 1636 | eq(true, false) -- won't be called 1637 | end) 1638 | pb.encode_hook("Phone", function(v) 1639 | hook_count = hook_count + 1 1640 | eq(v, hook_contacts[1]) 1641 | end) 1642 | pb.hook("Person", function(v) 1643 | hook_count = hook_count + 1 1644 | eq(true, false) -- won't be called 1645 | end) 1646 | pb.hook("Phone", function(v) 1647 | hook_count = hook_count + 1 1648 | eq(v, hook_contacts[1]) 1649 | end) 1650 | local b5 = pb.pack("Person", nil, age, nil, hook_contacts) 1651 | local n5, a5 = pb.unpack("Person", b5) 1652 | 1653 | eq(hook_count, 2) 1654 | pb.option "disable_hooks" 1655 | pb.option "disable_enchooks" 1656 | 1657 | pb.option "auto_default_values" 1658 | pb.clear() 1659 | protoc.reload() 1660 | check_load [[ 1661 | syntax = "proto3"; 1662 | enum Type { 1663 | HOME = 1; 1664 | WORK = 2; 1665 | } 1666 | message Phone { 1667 | string name = 1; 1668 | int64 phonenumber = 2; 1669 | Type type = 3; 1670 | } 1671 | message Friend { 1672 | string name = 1; 1673 | repeated Friend friends = 2; 1674 | } 1675 | message Person { 1676 | // will be sorted by field number 1677 | 1678 | map map = 100; 1679 | 1680 | string name = 1; 1681 | int32 age = 2; 1682 | repeated Phone contacts = 4; 1683 | } ]] 1684 | 1685 | local n1, a1, c1, m1, f1 = pb.unpack("Person", b1) 1686 | eq(n1, person.name) 1687 | eq(a1, person.age) 1688 | -- eq(e1, person.address) -- no address field anymore 1689 | eq(c1, person.contacts) 1690 | eq(m1, person.map) 1691 | -- eq(f1, person.friends) -- no friend field anymore 1692 | end) 1693 | end 1694 | 1695 | function _G.test_extend_pack() 1696 | local P = protoc.new() 1697 | 1698 | assert(P:load([[ 1699 | syntax = "proto3"; 1700 | message ExtendPackTest { 1701 | int32 id = 200; 1702 | extensions 100 to 199; 1703 | } 1704 | ]], "extend_pack_test.proto")) 1705 | 1706 | local i = 123456789 1707 | local s = "abcdefghijklmn" 1708 | local b = pb.pack("ExtendPackTest", i) 1709 | 1710 | assert(P:load([[ 1711 | syntax = "proto3"; 1712 | import "extend_pack_test.proto" 1713 | 1714 | extend ExtendPackTest { 1715 | string ext_name = 100; 1716 | } 1717 | ]])) 1718 | local s1, i1 = pb.unpack("ExtendPackTest", b) 1719 | eq(s1, "") 1720 | eq(i1, i) 1721 | 1722 | local b2 = pb.pack("ExtendPackTest", s, i) 1723 | pb.clear("ExtendPackTest", "ext_name") 1724 | local v1, v2 = pb.unpack("ExtendPackTest", b2) 1725 | eq(v1, i) 1726 | eq(v2, nil) 1727 | end 1728 | 1729 | if _VERSION == "Lua 5.1" and not _G.jit then 1730 | lu.LuaUnit.run() 1731 | else 1732 | os.exit(lu.LuaUnit.run(), true) 1733 | end 1734 | 1735 | -- unixcc: run='rm -f *.gcda; lua test.lua; gcov pb.so-pb.c' 1736 | -- win32cc: run='del *.gcda & lua test.lua & gcov pb.c' 1737 | --------------------------------------------------------------------------------