diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 000000000..0c70156df --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,37 @@ +version: 2 +defaults: &defaults + working_directory: ~/repo + docker: + - image: circleci/openjdk:8-jdk + environment: + JVM_OPTS: -Xmx3200m + TERM: dumb +jobs: + build: + <<: *defaults + steps: + - checkout + - restore_cache: + keys: + - v1-dependencies-{{ checksum "build.sbt" }} + # fallback to using the latest cache if no exact match is found + - v1-dependencies- + - run: cat /dev/null | sbt compilerJVM/universal:packageBin + - store_artifacts: + path: jvm/target/universal/kaitai-struct-compiler-*.zip + - save_cache: + paths: + - ~/.m2 + key: v1-dependencies--{{ checksum "build.sbt" }} + test: + <<: *defaults + steps: + - run: cat /dev/null | sbt compilerJVM/test +workflows: + version: 2 + build_test_deploy: + jobs: + - build + - test: + requires: + - build diff --git a/.gitignore b/.gitignore index 9f85e31c2..e41b1404d 100644 --- a/.gitignore +++ b/.gitignore @@ -54,3 +54,4 @@ project/boot/ # Unpacking complete binaries for testing purposes /runnable +js/npm diff --git a/.idea/compiler.xml b/.idea/compiler.xml deleted file mode 100644 index 96cc43efa..000000000 --- a/.idea/compiler.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/hydra.xml b/.idea/hydra.xml new file mode 100644 index 000000000..66eeb9a09 --- /dev/null +++ b/.idea/hydra.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 3d7f6d271..d5d79e0ca 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,37 +1,6 @@ - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml index 0774832f9..61851a77b 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -7,8 +7,6 @@ - - diff --git a/.idea/modules/compiler-sources.iml b/.idea/modules/compiler-sources.iml index af9c5e700..a5bd03e61 100644 --- a/.idea/modules/compiler-sources.iml +++ b/.idea/modules/compiler-sources.iml @@ -4,24 +4,28 @@ - + - + - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules/compilerJS-build.iml b/.idea/modules/compilerJS-build.iml index 95ce319e3..c1b1cf752 100644 --- a/.idea/modules/compilerJS-build.iml +++ b/.idea/modules/compilerJS-build.iml @@ -1,6 +1,6 @@ - - + + @@ -11,157 +11,193 @@ + - + - + + + + + + + + + + + + + + + + + + + - - - + + + + + + + + - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/.idea/modules/compilerJS.iml b/.idea/modules/compilerJS.iml index ba43a620c..07c1da5ef 100644 --- a/.idea/modules/compilerJS.iml +++ b/.idea/modules/compilerJS.iml @@ -1,34 +1,37 @@ - - - + + + - - + - - + + + - + - + + - - - - - - + + + + + + + + \ No newline at end of file diff --git a/.idea/modules/compilerJVM-build.iml b/.idea/modules/compilerJVM-build.iml index 169767c13..e8d9ec882 100644 --- a/.idea/modules/compilerJVM-build.iml +++ b/.idea/modules/compilerJVM-build.iml @@ -1,6 +1,6 @@ - - + + @@ -11,157 +11,193 @@ + - + - + + + + + + + + + + + + + + + + + + + - - - + + + + + + + + - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/.idea/modules/compilerJVM.iml b/.idea/modules/compilerJVM.iml index fc02511df..356ba34ac 100644 --- a/.idea/modules/compilerJVM.iml +++ b/.idea/modules/compilerJVM.iml @@ -1,26 +1,26 @@ - - - + + + - - + - - + + + - + - - - - + + + + @@ -28,14 +28,16 @@ - - - - - - - - - + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules/root-build.iml b/.idea/modules/root-build.iml index ad68dd6c8..aba4b3e2a 100644 --- a/.idea/modules/root-build.iml +++ b/.idea/modules/root-build.iml @@ -1,6 +1,6 @@ - - + + @@ -12,156 +12,191 @@ - + - + + + + + + + + + + + + + + + + + + + - - - + + + + + + + + - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/.idea/modules/root.iml b/.idea/modules/root.iml index f8234bc36..abf4d75ce 100644 --- a/.idea/modules/root.iml +++ b/.idea/modules/root.iml @@ -1,33 +1,26 @@ - - - + + + - - + - - + + + - + - + - - - - - - - - + \ No newline at end of file diff --git a/.idea/sbt.xml b/.idea/sbt.xml index e6a1c1e56..68783b141 100644 --- a/.idea/sbt.xml +++ b/.idea/sbt.xml @@ -3,7 +3,6 @@ diff --git a/.idea/scala_compiler.xml b/.idea/scala_compiler.xml index f6bb54cd5..d0f8d1eea 100644 --- a/.idea/scala_compiler.xml +++ b/.idea/scala_compiler.xml @@ -2,10 +2,10 @@ diff --git a/README.md b/README.md index a59dad9b4..188a05fa6 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,21 @@ +### Submitting bugs / issues / ideas + +> We're phasing out "Issues" tab in this repository. Please submit all issues to the [main project's tracker](https://github.com/kaitai-io/kaitai_struct/issues). + +--- + # Kaitai Struct: compiler This project is an official reference compiler for [Kaitai Struct](https://github.com/kaitai-io/kaitai_struct) project. -Kaitai Struct is a declarative language used for describe various +Kaitai Struct is a declarative language used to describe various binary data structures, laid out in files or in memory: i.e. binary file formats, network stream packet formats, etc. The main idea is that a particular format is described in Kaitai Struct language (`.ksy` files) only once and then can be compiled with this compiler into source files in one of the supported programming -languages. These modules will include a generated code for a parser +languages. These modules will include the generated code for a parser that can read described data structure from a file / stream and give access to it in a nice, easy-to-comprehend API. @@ -65,8 +71,8 @@ source code in repository: git clone https://github.com/kaitai-io/kaitai_struct_compiler -See [DEVELOPERS.md](DEVELOPERS.md) for general pointers on how to proceed -with the source code then. +See the [developer documentation](http://doc.kaitai.io/developers.html) for +general pointers on how to proceed with the source code then. ## Usage @@ -78,8 +84,8 @@ just as full name. Common options: * `...` — source files (.ksy) -* `-t | --target ` — target languages (`cpp_stl`, - `csharp`, `java`, `javascript`, `perl`, `php`, `python`, `ruby`, `all`) +* `-t | --target ` — target languages (`graphviz`, `csharp`, + `all`, `perl`, `java`, `go`, `cpp_stl`, `php`, `lua`, `python`, `ruby`, `javascript` * `all` is a special case: it compiles all possible target languages, creating language-specific directories (as per language identifiers) inside output directory, and then creating output @@ -119,7 +125,9 @@ and describes format with ID `foo`: ## Licensing -Kaitai Struct compiler itself is copyright (C) 2015-2017 Kaitai +### Main code + +Kaitai Struct compiler itself is copyright (C) 2015-2018 Kaitai Project. This program is free software: you can redistribute it and/or modify @@ -135,7 +143,76 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . -Note that it applies only to compiler itself, not `.ksy` input files -that one supplies in normal process of compilation, nor to compiler's -output files — that consitutes normal usage process and you obviously -keep copyright to both. +### FastParse + +Portions of Kaitai Struct compiler are loosely based on +[pythonparse](https://github.com/lihaoyi/fastparse/tree/master/pythonparse/shared/src/main/scala/pythonparse) +from [FastParse](http://www.lihaoyi.com/fastparse/) and are copyright +(c) 2014 Li Haoyi (haoyi.sg@gmail.com). + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +### XMLUtils code + +Portions of Kaitai Struct compiler are based on `scala/xml/Utility.scala` from [Scala XML](https://github.com/scala/scala-xml). + +Copyright (c) 2002-2017 EPFL +Copyright (c) 2011-2017 Lightbend, Inc. + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +* Neither the name of the EPFL nor the names of its contributors may be + used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +THE POSSIBILITY OF SUCH DAMAGE. + +### Libraries used + +Kaitai Struct compiler depends on the following libraries: + +* [scopt](https://github.com/scopt/scopt) — MIT license +* [fastparse](http://www.lihaoyi.com/fastparse/) — MIT license +* [snakeyaml](https://bitbucket.org/asomov/snakeyaml) — Apache 2.0 license + +--- + +Note that these clauses only apply only to compiler itself, not `.ksy` +input files that one supplies in normal process of compilation, nor to +compiler's output files — that consitutes normal usage process and you +obviously keep copyright to both. diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index b96e26291..3943451bb 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,3 +1,83 @@ +# 0.8 (TBD) + +* New target languages: + * Lua (96% tests pass score) + * initial support for Go (15% tests pass score) +* New ksy features: + * Switchable default endianness: `meta/endian` can now contain a + switch-like structure (with `switch-on` and `cases`), akin to + switchable types + ([docs](http://doc.kaitai.io/user_guide.html#calc-endian)). + * Parametric user-defined types: one can use `type: my_type(arg1, + arg2, arg3)` to pass arguments into user type + ([docs](http://doc.kaitai.io/user_guide.html#param-types)). + * Custom processing types: one can use `process: + my_process_name(arg1, arg2, arg3)` to invoke custom processing + routine, implemented in imperative language + ([docs](http://doc.kaitai.io/user_guide.html#custom-process)). + * In repetitions, index of current repetition can be accessed using + `_index` in expressions + ([docs](http://doc.kaitai.io/user_guide.html#repeat-index)). + * Verbose enums: now one can specify documentation and other useful + information relevant to enums using verbose enum declaration + format + ([docs](http://doc.kaitai.io/user_guide.html#verbose-enums)). + * `meta/xref` key can be used for adding cross-references of a + format specifications (like relevant RFC entries, Wikidata + entries, ISO / IEEE / JIS / DIN / GOST standard numbers, PRONOM + identifiers, etc). +* General compilation improvements: + * Imports/includes for all languages are now managed properly, no + duplicate / unnecessary imports should be added + * Python: basic docstring support + * More strict ksy precompile checks (less likely to accept ksy that + will result in non-compilable code), better error messages +* CLI options: + * Python target now allows to specify package with `--python-package` + * Java target now allows custom KaitaiStream implementations and + thus allows to specify default implementation for `fromFile(...)` + using `--java-from-file-class`. +* Expression language: + * New methods: + * floats: `to_i` + * arrays: `min`, `max` + * Added byte array comparison +* Packaging / infrastructure improvements: + * ksc is now available as + [npm package](https://www.npmjs.com/package/kaitai-struct-compiler/), + which now a build dependency of a + [web IDE](https://ide.kaitai.io/) +* Runtime API changes: + * C++: now requires `KS_STR_ENCODING_ICONV` or + `KS_STR_ENCODING_NONE` to be defined to how to handle string + encodings + * Java: `KaitaiStream` is now an interface, and there are two + distinct classes which implement it: + * `ByteBufferKaitaiStream` provides KaitaiStream backed + `ByteBuffer` (and thus using memory-mapped files) + * `RandomAccessFileKaitaiStream` provides KaitaiStream backed by + `RandomAccessFile` (and thus uses normal OS read calls, as it + was done in older KaitaiStruct circa v0.5) + * JavaScript: Error classes are now subclasses of `KaitaiStream` and + were renamed in the following way: `KaitaiUnexpectedDataError` -> + `KaitaiStream`.`UnexpectedDataError` +* Major bugfixes: + * C++: adjusted to made compatible with OS X and Windows MSVC builds + * Fixed broken generation of byte array literals with high 8-bit set + in some targets + * Fixed float literals parsing, fixed larger integer keys YAML parsing + * Fixed inconsistency of debug mode vs non-debug mode behavior for + `repeat-*` + * Fixed chain of relative imports bug: now all relative imports work + always relative to the file being processed, not to current + compiler's dir + * Many problems with switching: invalid common type inferring, + invalid code being generated, added failsafe `if`-based + implementations for languages which do not support switching over + all possible types. + * Fixed most memory leaks in C++ (only exception-related leaks are + left now) + # 0.7 (2017-03-22) * New ksy features: diff --git a/build.sbt b/build.sbt index 67f0708aa..bf0d21f88 100644 --- a/build.sbt +++ b/build.sbt @@ -5,8 +5,8 @@ import sbt.Keys._ resolvers += Resolver.sonatypeRepo("public") -val VERSION = "0.7" -val TARGET_LANGS = "C++/STL, C#, Java, JavaScript, Perl, PHP, Python, Ruby" +val VERSION = "0.8" +val TARGET_LANGS = "C++/STL, C#, Java, JavaScript, Lua, Perl, PHP, Python, Ruby" lazy val root = project.in(file(".")). aggregate(compilerJS, compilerJVM). @@ -21,21 +21,21 @@ lazy val compiler = crossProject.in(file(".")). settings( organization := "io.kaitai", name := "kaitai-struct-compiler", - version := VERSION, + version := sys.env.getOrElse("KAITAI_STRUCT_VERSION", VERSION), licenses := Seq(("GPL-3.0", url("https://opensource.org/licenses/GPL-3.0"))), - scalaVersion := "2.11.7", + scalaVersion := "2.12.4", buildInfoKeys := Seq[BuildInfoKey](name, version, scalaVersion, sbtVersion), buildInfoPackage := "io.kaitai.struct", buildInfoOptions += BuildInfoOption.BuildTime, // Repo publish options - publishTo <<= version { (v: String) => + publishTo := version { (v: String) => val nexus = "https://oss.sonatype.org/" if (v.trim.endsWith("SNAPSHOT")) Some("snapshots" at nexus + "content/repositories/snapshots") else Some("releases" at nexus + "service/local/staging/deploy/maven2") - }, + }.value, pomExtra := http://kaitai.io @@ -54,15 +54,15 @@ lazy val compiler = crossProject.in(file(".")). , libraryDependencies ++= Seq( - "com.lihaoyi" %%% "fastparse" % "0.4.1", + "com.github.scopt" %%% "scopt" % "3.6.0", + "com.lihaoyi" %%% "fastparse" % "1.0.0", "org.yaml" % "snakeyaml" % "1.16" ) ). jvmSettings( mainClass in Compile := Some("io.kaitai.struct.JavaMain"), libraryDependencies ++= Seq( - "org.scalatest" %% "scalatest" % "2.2.6" % "test", - "com.github.scopt" %% "scopt" % "3.4.0" + "org.scalatest" %% "scalatest" % "3.0.1" % "test" ), testOptions in Test += Tests.Argument(TestFrameworks.ScalaTest, "-u", "target/test_out"), @@ -111,18 +111,23 @@ lazy val compiler = crossProject.in(file(".")). } }, - // Hack: we need /usr/share/kaitai-struct (the format directory) to be - // created as empty dir and packaged in compiler package, to be filled in - // with actual repository contents by "kaitai-struct-formats" package. - // "jvm/src/main/resources" is guaranteed to be an empty directory. - linuxPackageMappings += LinuxPackageMapping(Map( - new File("jvm/src/main/resources") -> "/usr/share/kaitai-struct" - )), + // We need /usr/share/kaitai-struct (the format directory) to be created as + // empty dir and packaged in compiler package, to be filled in with actual + // repository contents by "kaitai-struct-formats" package. + linuxPackageMappings += packageTemplateMapping("/usr/share/kaitai-struct")(), // Remove all "maintainer scripts", such as prerm/postrm/preinst/postinst: default // implementations create per-package virtual user that we won't use anyway maintainerScripts in Debian := Map(), + // Work around new Debian defaults and sbt-native-packager defaults, which + // build .deb packages that appear to be incompatible with older Debian/Ubuntu's + // dpkg and are not accepted by BinTray. + // + // For more information, see + // https://github.com/sbt/sbt-native-packager/issues/1067 + debianNativeBuildOptions in Debian := Seq("-Zgzip", "-z3"), + packageSummary in Linux := s"compiler to generate binary data parsers in $TARGET_LANGS", packageSummary in Windows := "Kaitai Struct compiler", packageDescription in Linux := diff --git a/js/README.md b/js/README.md new file mode 100644 index 000000000..5ad3c52ad --- /dev/null +++ b/js/README.md @@ -0,0 +1,196 @@ +## Kaitai Struct compiler in JavaScript + +This project is a official reference Kaitai Struct compiler, compiled +for JavaScript environments. + +Kaitai Struct is a declarative language used for describe various +binary data structures, laid out in files or in memory: i.e. binary +file formats, network stream packet formats, etc. + +The main idea is that a particular format is described in Kaitai +Struct language only once and then can be compiled with into +source files in one of the supported programming languages. These +modules will include a generated code for a parser that can read +described data structure from a file / stream and give access to it in +a nice, easy-to-comprehend API. + +For more info on Kaitai Struct, please refer to http://kaitai.io/ + +Note that reference Kaitai Struct compiler is written Scala, and thus +can be compiled for a variety of platforms, such as JVM, JavaScript +and native binaries. This package is compiled to be run JavaScript +environments. + +Currently, this JavaScript build offers only programmatic API, so it +is generally suited for developers of tools that use Kaitai Struct +(i.e. interactive compilers, visualizers, loaders, IDEs that integrate +the compiler). If you: + +* just look for a way to try out Kaitai Struct => try + [Kaitai Struct Web IDE](https://ide.kaitai.io/), which uses this + compiler internally +* want to load .ksy file transparently in your JavaScript code + (compiling it on the fly) => use + [Kaitai Struct loader for JavaScript](https://github.com/kaitai-io/kaitai-struct-loader). +* want to integrate compiler into your build flow => use a JVM-based + desktop compiler, which can be called as a command-line utility + +## Installation + +We publish two versions of the compiler to npm: + - A stable one, this includes the latest stable, released compiler. This is the default ("latest") version. + ``` + npm install kaitai-struct-compiler + ``` + - The other is the latest snapshot version which follows our master branch. This version is tagged as `@next`. + ``` + npm install kaitai-struct-compiler@next + ``` + +### Example project + +Our [examples repository](https://github.com/kaitai-io/kaitai_struct_examples) contains a few examples how to use the compiler. + +## Plugging in + +We publish the compiler as an [UMD module](https://github.com/umdjs/umd), so it works from various environments, including server-side (eg. node) and client-side (eg. web browser) ones. + +Note: currently we don't publish the compiler as standard ES module. This will probably change in the future. If you need ES module please comment on [this issue](https://github.com/kaitai-io/kaitai_struct/issues/180). + +### node + +```javascript +var KaitaiStructCompiler = require("kaitai-struct-compiler"); +var compiler = new KaitaiStructCompiler(); +``` + +### browser using script tags + +```html + + +``` + +### browser using AMD loader (eg. require.js) + +```html + + +``` + +## Usage + +### Basic usage of compile method + +```javascript +var ksyYaml = fs.readFileSync("zip.ksy"); /* string */ +var ksy = YAML.parse(ksyYaml); /* JS object */ +compiler.compile("javascript", ksy, null, false /* debugMode */).then(function(files) { + console.log("Compiled filenames: " + Object.keys(files).join(", ")); + console.log("Content of Zip.js file: " + files["Zip.js"]); +}); +``` + +### Getting compiler information + +```javascript +console.log("Version: " + compiler.version); +console.log("Build date: " + compiler.buildDate); +console.log("Supported languages: " + compiler.languages.join(", ")); +``` + +### Handle imports + +```javascript +var yamlImporter = { + importYaml: function(name, mode) { + console.log(" -> Import yaml called with name '" + name + "' and mode '" + mode + "'."); + var importKsyYaml = fs.readFileSync(name + ".ksy"); /* string */ + var importKsy = YAML.parse(importKsyYaml); /* JS object */ + return Promise.resolve(importKsy); + } +}; + +yamlImporter.importYaml("import_outer.ksy").then(function(ksy) { + compiler.compile("javascript", ksy, yamlImporter, false /* debugMode */).then(function(files) { + console.log("Compiled filenames: " + Object.keys(files).join(", ")); + }); +}); +``` + +### Debug mode + +You can compile in debug mode which adds `_debug` property to every object and this `_debug` object contains the start and end offsets of the parsed fields so you can find bytes of the field in the original binary. + +## Copyrights and licensing + +### Main code + +Kaitai Struct compiler itself is copyright (C) 2015-2017 Kaitai +Project. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or (at +your option) any later version. + +This program is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +### FastParse + +Portions of Kaitai Struct compiler are loosely based on +[pythonparse](https://github.com/lihaoyi/fastparse/tree/master/pythonparse/shared/src/main/scala/pythonparse) +from [FastParse](http://www.lihaoyi.com/fastparse/) and are copyright +(c) 2014 Li Haoyi (haoyi.sg@gmail.com). + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +### Libraries used + +Kaitai Struct compiler depends on the following libraries: + +* [scopt](https://github.com/scopt/scopt) — MIT license +* [fastparse](http://www.lihaoyi.com/fastparse/) — MIT license +* [snakeyaml](https://bitbucket.org/asomov/snakeyaml) — Apache 2.0 license + +--- + +Note that these clauses only apply only to compiler itself, not `.ksy` +input files that one supplies in normal process of compilation, nor to +compiler's output files — that consitutes normal usage process and you +obviously keep copyright to both. diff --git a/js/package.json b/js/package.json new file mode 100644 index 000000000..c2c2ae68d --- /dev/null +++ b/js/package.json @@ -0,0 +1,39 @@ +{ + "author": { + "name": "Kaitai team", + "url": "https://github.com/orgs/kaitai-io/people" + }, + "bugs": { + "url": "https://github.com/kaitai-io/kaitai_struct/issues" + }, + "bundleDependencies": false, + "deprecated": false, + "description": "Kaitai Struct Compiler", + "homepage": "https://github.com/kaitai-io/kaitai_struct_compiler#readme", + "keywords": [ + "kaitai", + "struct", + "compiler", + "binary", + "parsing", + "stream", + "runtime", + "file", + "format", + "structure", + "forenics", + "reversing", + "reverse-engineering" + ], + "license": "GPL-3.0", + "main": "kaitai-struct-compiler.js", + "name": "kaitai-struct-compiler", + "repository": { + "type": "git", + "url": "git+https://github.com/kaitai-io/kaitai_struct_compiler.git" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "version": "0.8.0-SNAPSHOT.3" +} diff --git a/js/src/main/scala/io/kaitai/struct/MainJs.scala b/js/src/main/scala/io/kaitai/struct/MainJs.scala index 3dffe7d04..2f16766e3 100644 --- a/js/src/main/scala/io/kaitai/struct/MainJs.scala +++ b/js/src/main/scala/io/kaitai/struct/MainJs.scala @@ -24,7 +24,7 @@ object MainJs { val specs = new JavaScriptClassSpecs(importer, firstSpec) Main.importAndPrecompile(specs, config).map { (_) => specs.flatMap({ case (_, spec) => - val files = Main.compile(spec, lang, config).files + val files = Main.compile(specs, spec, lang, config).files files.map((x) => x.fileName -> x.contents).toMap }).toJSDictionary }.toJSPromise diff --git a/js/update_npm_package.py b/js/update_npm_package.py new file mode 100755 index 000000000..2fae1dd65 --- /dev/null +++ b/js/update_npm_package.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python2 + +import os +import errno +import shutil +import re +import subprocess +import datetime + + +def mkdir_p(path): + try: + os.makedirs(path) + except OSError as exc: + if exc.errno == errno.EEXIST and os.path.isdir(path): + pass + else: + raise + +moduleTemplate = ''' +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + define([], factory); + } else if (typeof module === 'object' && module.exports) { + module.exports = factory(); + } else { + root.KaitaiStructCompiler = factory(); + } +}(this, function () { + +var exports = {}; +var __ScalaJSEnv = { exportsNamespace: exports }; + +{{compilerCode}} + +return exports.io.kaitai.struct.MainJs; + +})); +'''.lstrip() + +with open('target/scala-2.12/kaitai-struct-compiler-fastopt.js','rt') as f: compilerCode = f.read() + +moduleCode = moduleTemplate.replace('{{compilerCode}}', compilerCode) + +mkdir_p('npm') + +with open('npm/kaitai-struct-compiler.js','wt') as f: f.write(moduleCode) +for fn in ['../LICENSE', 'README.md']: + shutil.copy(fn, 'npm/') + +gitInfo = subprocess.check_output(['git log -1 --format=%H,%ct'], shell=True).strip().split(',') +commitId = gitInfo[0] +commitTs = int(gitInfo[1]) +commitDate = datetime.datetime.fromtimestamp(commitTs).strftime('%Y%m%d.%H%M%S') + +with open('package.json','rb') as f: packageJson = f.read() +packageJson = re.sub(r'("version": "\d+\.\d+\.\d+-SNAPSHOT.)[^"]*"', r'\g<1>%s"' % commitDate, packageJson) +with open('npm/package.json','wb') as f: f.write(packageJson) diff --git a/jvm/src/main/scala/io/kaitai/struct/JavaMain.scala b/jvm/src/main/scala/io/kaitai/struct/JavaMain.scala index e66307dae..712b3a549 100644 --- a/jvm/src/main/scala/io/kaitai/struct/JavaMain.scala +++ b/jvm/src/main/scala/io/kaitai/struct/JavaMain.scala @@ -8,6 +8,7 @@ import io.kaitai.struct.JavaMain.CLIConfig import io.kaitai.struct.format.{ClassSpec, ClassSpecs, KSVersion, YAMLParseException} import io.kaitai.struct.formats.JavaKSYParser import io.kaitai.struct.languages.components.LanguageCompilerStatic +import io.kaitai.struct.precompile.ErrorInInput object JavaMain { KSVersion.current = BuildInfo.version @@ -50,7 +51,7 @@ object JavaMain { if (VALID_LANGS.contains(x)) { success } else { - failure(s"'${x}' is not a valid target language; valid ones are: ${VALID_LANGS.mkString(", ")}") + failure(s"'$x' is not a valid target language; valid ones are: ${VALID_LANGS.mkString(", ")}") } } @@ -63,10 +64,18 @@ object JavaMain { c.copy(importPaths = c.importPaths ++ x.split(File.pathSeparatorChar)) } text(".ksy library search path(s) for imports (see also KSPATH env variable)") + opt[String]("go-package") valueName("") action { (x, c) => + c.copy(runtime = c.runtime.copy(goPackage = x)) + } text("Go package (Go only, default: none)") + opt[String]("java-package") valueName("") action { (x, c) => c.copy(runtime = c.runtime.copy(javaPackage = x)) } text("Java package (Java only, default: root package)") + opt[String]("java-from-file-class") valueName("") action { (x, c) => + c.copy(runtime = c.runtime.copy(javaFromFileClass = x)) + } text(s"Java class to be invoked in fromFile() helper (default: ${RuntimeConfig().javaFromFileClass})") + opt[String]("dotnet-namespace") valueName("") action { (x, c) => c.copy(runtime = c.runtime.copy(dotNetNamespace = x)) } text(".NET Namespace (.NET only, default: Kaitai)") @@ -75,6 +84,10 @@ object JavaMain { c.copy(runtime = c.runtime.copy(phpNamespace = x)) } text("PHP Namespace (PHP only, default: root package)") + opt[String]("python-package") valueName("") action { (x, c) => + c.copy(runtime = c.runtime.copy(pythonPackage = x)) + } text("Python package (Python only, default: root package)") + opt[Boolean]("opaque-types") action { (x, c) => c.copy(runtime = c.runtime.copy(opaqueTypes = x)) } text("opaque types allowed, default: false") @@ -207,10 +220,18 @@ class JavaMain(config: CLIConfig) { } srcFile.toString -> log }.toMap - if (config.jsonOutput) + + if (config.jsonOutput) { Console.println(JSON.mapToJson(logs)) + } else { + if (logsHaveErrors(logs)) + System.exit(2) + } } + private def logsHaveErrors(logs: Map[String, InputEntry]): Boolean = + logs.values.map(_.hasErrors).max + private def compileOneInput(srcFile: String) = { Log.fileOps.info(() => s"parsing $srcFile...") val specs = JavaKSYParser.localFileToSpecs(srcFile, config) @@ -241,9 +262,11 @@ class JavaMain(config: CLIConfig) { val lang = LanguageCompilerStatic.byString(langStr) specs.map { case (_, classSpec) => val res = try { - compileSpecAndWriteToFile(classSpec, lang, outDir) + compileSpecAndWriteToFile(specs, classSpec, lang, outDir) } catch { case ex: Throwable => + if (config.throwExceptions) + ex.printStackTrace() SpecFailure(List(exceptionToCompileError(ex, classSpec.nameAsStr))) } classSpec.nameAsStr -> res @@ -251,11 +274,12 @@ class JavaMain(config: CLIConfig) { } def compileSpecAndWriteToFile( + specs: ClassSpecs, spec: ClassSpec, lang: LanguageCompilerStatic, outDir: String ): SpecSuccess = { - val res = Main.compile(spec, lang, config.runtime) + val res = Main.compile(specs, spec, lang, config.runtime) res.files.foreach { (file) => Log.fileOps.info(() => s".... writing ${file.fileName}") @@ -278,6 +302,13 @@ class JavaMain(config: CLIConfig) { ex match { case ype: YAMLParseException => CompileError("(main)", ype.path, ype.msg) + case e: ErrorInInput => + val file = e.file.getOrElse(srcFile) + val msg = Option(e.getCause) match { + case Some(cause) => cause.getMessage + case None => e.getMessage + } + CompileError(file, e.path, msg) case _ => CompileError(srcFile, List(), ex.getMessage) } diff --git a/jvm/src/main/scala/io/kaitai/struct/formats/JavaKSYParser.scala b/jvm/src/main/scala/io/kaitai/struct/formats/JavaKSYParser.scala index 4a4c33f0b..57e371564 100644 --- a/jvm/src/main/scala/io/kaitai/struct/formats/JavaKSYParser.scala +++ b/jvm/src/main/scala/io/kaitai/struct/formats/JavaKSYParser.scala @@ -55,6 +55,10 @@ object JavaKSYParser { src case javaInt: java.lang.Integer => javaInt.intValue + case javaLong: java.lang.Long => + javaLong.longValue + case _: java.math.BigInteger => + src.toString case null => // may be not the very best idea, but these nulls // should be handled by real parsing code, i.e. where diff --git a/jvm/src/test/scala/io/kaitai/struct/exprlang/ExpressionsSpec.scala b/jvm/src/test/scala/io/kaitai/struct/exprlang/ExpressionsSpec.scala index 2c07db612..5e0fe54e3 100644 --- a/jvm/src/test/scala/io/kaitai/struct/exprlang/ExpressionsSpec.scala +++ b/jvm/src/test/scala/io/kaitai/struct/exprlang/ExpressionsSpec.scala @@ -48,6 +48,34 @@ class ExpressionsSpec extends FunSpec { Expressions.parse("0b1010_1_010") should be (IntNum(0xaa)) } + it("parses simple float") { + Expressions.parse("1.2345") should be (FloatNum(1.2345)) + } + + it("parses float with positive exponent") { + Expressions.parse("123e4") should be (FloatNum(123e4)) + } + + it("parses float with positive exponent with plus sign") { + Expressions.parse("123e+4") should be (FloatNum(123e4)) + } + + it("parses float with negative exponent") { + Expressions.parse("123e-7") should be (FloatNum(123e-7)) + } + + it("parses float + non-integral part with positive exponent") { + Expressions.parse("1.2345e7") should be (FloatNum(1.2345e7)) + } + + it("parses float + non-integral part with positive exponent with plus sign") { + Expressions.parse("123.45e+7") should be (FloatNum(123.45e7)) + } + + it("parses float + non-integral part with negative exponent") { + Expressions.parse("123.45e-7") should be (FloatNum(123.45e-7)) + } + it("parses 1 + 2") { Expressions.parse("1 + 2") should be (BinOp(IntNum(1), Add, IntNum(2))) } diff --git a/jvm/src/test/scala/io/kaitai/struct/translators/TranslatorSpec.scala b/jvm/src/test/scala/io/kaitai/struct/translators/TranslatorSpec.scala index 31765aa87..70a6efa1b 100644 --- a/jvm/src/test/scala/io/kaitai/struct/translators/TranslatorSpec.scala +++ b/jvm/src/test/scala/io/kaitai/struct/translators/TranslatorSpec.scala @@ -1,413 +1,454 @@ package io.kaitai.struct.translators -import io.kaitai.struct.{GraphvizClassCompiler, RuntimeConfig} import io.kaitai.struct.datatype.DataType import io.kaitai.struct.datatype.DataType._ import io.kaitai.struct.exprlang.{Ast, Expressions} import io.kaitai.struct.format.ClassSpec import io.kaitai.struct.languages._ import io.kaitai.struct.languages.components.LanguageCompilerStatic +import io.kaitai.struct.{ImportList, RuntimeConfig} import org.scalatest.FunSuite import org.scalatest.Matchers._ -import org.scalatest.prop.TableDrivenPropertyChecks - -class TranslatorSpec extends FunSuite with TableDrivenPropertyChecks { - val tests = Table( - ("src", "srcType", "expType", "expOut"), - - // Integer literals + unary minus - everybody("123", "123", Int1Type(true)), - everybody("223", "223", Int1Type(false)), - everybody("1234", "1234"), - everybody("-456", "-456"), - everybody("0x1234", "4660"), - // less and more than 32 Bit signed int - everybody("1000000000", "1000000000"), - everybodyExcept("100000000000", "100000000000", Map[LanguageCompilerStatic, String]( - JavaCompiler -> "100000000000L" - )), - - // Float literals - everybody("1.0", "1.0", CalcFloatType), - everybody("123.456", "123.456", CalcFloatType), - everybody("-123.456", "-123.456", CalcFloatType), - - // Simple integer operations - everybody("1 + 2", "(1 + 2)"), - - everybodyExcept("3 / 2", "(3 / 2)", Map( - JavaScriptCompiler -> "Math.floor(3 / 2)", - PerlCompiler -> "int(3 / 2)", - PHPCompiler -> "intval(3 / 2)", - PythonCompiler -> "3 // 2" - )), - - everybody("1 + 2 + 5", "((1 + 2) + 5)"), - - everybodyExcept("(1 + 2) / (7 * 8)", "((1 + 2) / (7 * 8))", Map( - JavaScriptCompiler -> "Math.floor((1 + 2) / (7 * 8))", - PerlCompiler -> "int((1 + 2) / (7 * 8))", - PHPCompiler -> "intval((1 + 2) / (7 * 8))", - PythonCompiler -> "(1 + 2) // (7 * 8)" - )), - - everybody("1 < 2", "1 < 2", CalcBooleanType), - - everybody("1 == 2", "1 == 2", CalcBooleanType), - - full("2 < 3 ? \"foo\" : \"bar\"", CalcIntType, CalcStrType, Map[LanguageCompilerStatic, String]( - CppCompiler -> "(2 < 3) ? (std::string(\"foo\")) : (std::string(\"bar\"))", - CSharpCompiler -> "2 < 3 ? \"foo\" : \"bar\"", - JavaCompiler -> "2 < 3 ? \"foo\" : \"bar\"", - JavaScriptCompiler -> "2 < 3 ? \"foo\" : \"bar\"", - PerlCompiler -> "2 < 3 ? \"foo\" : \"bar\"", - PHPCompiler -> "2 < 3 ? \"foo\" : \"bar\"", - PythonCompiler -> "u\"foo\" if 2 < 3 else u\"bar\"", - RubyCompiler -> "2 < 3 ? \"foo\" : \"bar\"" - )), - - everybody("~777", "~777"), - everybody("~(7+3)", "~(7 + 3)"), - - // Simple float operations - everybody("1.2 + 3.4", "(1.2 + 3.4)", CalcFloatType), - everybody("1.2 + 3", "(1.2 + 3)", CalcFloatType), - everybody("1 + 3.4", "(1 + 3.4)", CalcFloatType), - - everybody("1.0 < 2", "1.0 < 2", CalcBooleanType), - - everybody("3 / 2.0", "(3 / 2.0)", CalcFloatType), - - everybody("(1 + 2) / (7 * 8.1)", "((1 + 2) / (7 * 8.1))", CalcFloatType), - - // Boolean literals - full("true", CalcBooleanType, CalcBooleanType, Map[LanguageCompilerStatic, String]( - CppCompiler -> "true", - CSharpCompiler -> "true", - JavaCompiler -> "true", - JavaScriptCompiler -> "true", - PerlCompiler -> "1", - PHPCompiler -> "true", - PythonCompiler -> "True", - RubyCompiler -> "true" - )), - - full("false", CalcBooleanType, CalcBooleanType, Map[LanguageCompilerStatic, String]( - CppCompiler -> "false", - CSharpCompiler -> "false", - JavaCompiler -> "false", - JavaScriptCompiler -> "false", - PerlCompiler -> "0", - PHPCompiler -> "false", - PythonCompiler -> "False", - RubyCompiler -> "false" - )), - - full("some_bool.to_i", CalcBooleanType, CalcIntType, Map[LanguageCompilerStatic, String]( - CppCompiler -> "some_bool()", - CSharpCompiler -> "(SomeBool ? 1 : 0)", - JavaCompiler -> "(someBool() ? 1 : 0)", - JavaScriptCompiler -> "(this.someBool | 0)", - PerlCompiler -> "$self->some_bool()", - PHPCompiler -> "intval($this->someBool())", - PythonCompiler -> "int(self.some_bool)", - RubyCompiler -> "(some_bool ? 1 : 0)" - )), - - // Member access - full("foo_str", CalcStrType, CalcStrType, Map[LanguageCompilerStatic, String]( - CppCompiler -> "foo_str()", - CSharpCompiler -> "FooStr", - JavaCompiler -> "fooStr()", - JavaScriptCompiler -> "this.fooStr", - PerlCompiler -> "$self->foo_str()", - PHPCompiler -> "$this->fooStr()", - PythonCompiler -> "self.foo_str", - RubyCompiler -> "foo_str" - )), - - full("foo_block", userType("block"), userType("block"), Map[LanguageCompilerStatic, String]( - CppCompiler -> "foo_block()", - CSharpCompiler -> "FooBlock", - JavaCompiler -> "fooBlock()", - JavaScriptCompiler -> "this.fooBlock", - PerlCompiler -> "$self->foo_block()", - PHPCompiler -> "$this->fooBlock()", - PythonCompiler -> "self.foo_block", - RubyCompiler -> "foo_block" - )), - - full("foo.bar", FooBarProvider, CalcStrType, Map[LanguageCompilerStatic, String]( - CppCompiler -> "foo()->bar()", - CSharpCompiler -> "Foo.Bar", - JavaCompiler -> "foo().bar()", - JavaScriptCompiler -> "this.foo.bar", - PerlCompiler -> "$self->foo()->bar()", - PHPCompiler -> "$this->foo()->bar()", - PythonCompiler -> "self.foo.bar", - RubyCompiler -> "foo.bar" - )), - - full("foo.inner.baz", FooBarProvider, CalcIntType, Map[LanguageCompilerStatic, String]( - CppCompiler -> "foo()->inner()->baz()", - CSharpCompiler -> "Foo.Inner.Baz", - JavaCompiler -> "foo().inner().baz()", - JavaScriptCompiler -> "this.foo.inner.baz", - PerlCompiler -> "$self->foo()->inner()->baz()", - PHPCompiler -> "$this->foo()->inner()->baz()", - PythonCompiler -> "self.foo.inner.baz", - RubyCompiler -> "foo.inner.baz" - )), - - full("_root.foo", userType("block"), userType("block"), Map[LanguageCompilerStatic, String]( - CppCompiler -> "_root()->foo()", - CSharpCompiler -> "M_Root.Foo", - JavaCompiler -> "_root.foo()", - JavaScriptCompiler -> "this._root.foo", - PerlCompiler -> "$self->_root()->foo()", - PHPCompiler -> "$this->_root()->foo()", - PythonCompiler -> "self._root.foo", - RubyCompiler -> "_root.foo" - )), - - full("a != 2 and a != 5", CalcIntType, CalcBooleanType, Map[LanguageCompilerStatic, String]( - CppCompiler -> "a() != 2 && a() != 5", - CSharpCompiler -> "A != 2 && A != 5", - JavaCompiler -> "a() != 2 && a() != 5", - JavaScriptCompiler -> "this.a != 2 && this.a != 5", - PerlCompiler -> "$self->a() != 2 && $self->a() != 5", - PHPCompiler -> "$this->a() != 2 && $this->a() != 5", - PythonCompiler -> "self.a != 2 and self.a != 5", - RubyCompiler -> "a != 2 && a != 5" - )), - - // Arrays - full("[0, 1, 100500]", CalcIntType, ArrayType(CalcIntType), Map[LanguageCompilerStatic, String]( - CSharpCompiler -> "new List { 0, 1, 100500 }", - JavaCompiler -> "new ArrayList(Arrays.asList(0L, 1L, 100500L))", - JavaScriptCompiler -> "[0, 1, 100500]", - PerlCompiler -> "(0, 1, 100500)", - PHPCompiler -> "[0, 1, 100500]", - PythonCompiler -> "[0, 1, 100500]", - RubyCompiler -> "[0, 1, 100500]" - )), - - full("[34, 0, 10, 64, 65, 66, 92]", CalcIntType, CalcBytesType, Map[LanguageCompilerStatic, String]( - CppCompiler -> "std::string(\"\\x22\\x00\\x0A\\x40\\x41\\x42\\x5C\", 7)", - CSharpCompiler -> "new byte[] { 34, 0, 10, 64, 65, 66, 92 }", - JavaCompiler -> "new byte[] { 34, 0, 10, 64, 65, 66, 92 }", - JavaScriptCompiler -> "[34, 0, 10, 64, 65, 66, 92]", - PerlCompiler -> "pack('C*', (34, 0, 10, 64, 65, 66, 92))", - PHPCompiler -> "\"\\x22\\x00\\x0A\\x40\\x41\\x42\\x5C\"", - PythonCompiler -> "struct.pack('7b', 34, 0, 10, 64, 65, 66, 92)", - RubyCompiler -> "[34, 0, 10, 64, 65, 66, 92].pack('C*')" - )), - - full("[255, 0, 255]", CalcIntType, CalcBytesType, Map[LanguageCompilerStatic, String]( - CppCompiler -> "std::string(\"\\xFF\\x00\\xFF\", 3)", - CSharpCompiler -> "new byte[] { 255, 0, 255 }", - JavaCompiler -> "new byte[] { -1, 0, -1 }", - JavaScriptCompiler -> "[255, 0, 255]", - PerlCompiler -> "pack('C*', (255, 0, 255))", - PHPCompiler -> "\"\\xFF\\x00\\xFF\"", - PythonCompiler -> "struct.pack('3b', -1, 0, -1)", - RubyCompiler -> "[255, 0, 255].pack('C*')" - )), - - full("a[42]", ArrayType(CalcStrType), CalcStrType, Map[LanguageCompilerStatic, String]( - CppCompiler -> "a()->at(42)", - CSharpCompiler -> "A[42]", - JavaCompiler -> "a().get(42)", - JavaScriptCompiler -> "this.a[42]", - PythonCompiler -> "self.a[42]", - RubyCompiler -> "a[42]" - )), - - full("a[42 - 2]", ArrayType(CalcStrType), CalcStrType, Map[LanguageCompilerStatic, String]( - CppCompiler -> "a()->at((42 - 2))", - CSharpCompiler -> "A[(42 - 2)]", - JavaCompiler -> "a().get((42 - 2))", - JavaScriptCompiler -> "this.a[(42 - 2)]", - PythonCompiler -> "self.a[(42 - 2)]", - RubyCompiler -> "a[(42 - 2)]" - )), - - full("a.first", ArrayType(CalcIntType), CalcIntType, Map[LanguageCompilerStatic, String]( - CppCompiler -> "a()->front()", - CSharpCompiler -> "A[0]", - JavaCompiler -> "a().get(0)", - JavaScriptCompiler -> "this.a[0]", - PythonCompiler -> "self.a[0]", - RubyCompiler -> "a.first" - )), - - full("a.last", ArrayType(CalcIntType), CalcIntType, Map[LanguageCompilerStatic, String]( - CppCompiler -> "a()->back()", - CSharpCompiler -> "A[A.Length - 1]", - JavaCompiler -> "a().get(a().size() - 1)", - JavaScriptCompiler -> "this.a[this.a.length - 1]", - PythonCompiler -> "self.a[-1]", - RubyCompiler -> "a.last" - )), - - full("a.size", ArrayType(CalcIntType), CalcIntType, Map[LanguageCompilerStatic, String]( - CppCompiler -> "a()->size()", - CSharpCompiler -> "A.Count", - JavaCompiler -> "a().size()", - JavaScriptCompiler -> "this.a.length", - PHPCompiler -> "count(a)", - PerlCompiler -> "scalar($self->a())", - PythonCompiler -> "len(self.a)", - RubyCompiler -> "a.length" - )), - - // Strings - full("\"str\"", CalcIntType, CalcStrType, Map[LanguageCompilerStatic, String]( - CppCompiler -> "std::string(\"str\")", - CSharpCompiler -> "\"str\"", - JavaCompiler -> "\"str\"", - JavaScriptCompiler -> "\"str\"", - PerlCompiler -> "\"str\"", - PHPCompiler -> "\"str\"", - PythonCompiler -> "u\"str\"", - RubyCompiler -> "\"str\"" - )), - - full("\"str\\nnext\"", CalcIntType, CalcStrType, Map[LanguageCompilerStatic, String]( - CppCompiler -> "std::string(\"str\\nnext\")", - CSharpCompiler -> "\"str\\nnext\"", - JavaCompiler -> "\"str\\nnext\"", - JavaScriptCompiler -> "\"str\\nnext\"", - PerlCompiler -> "\"str\\nnext\"", - PHPCompiler -> "\"str\\nnext\"", - PythonCompiler -> "u\"str\\nnext\"", - RubyCompiler -> "\"str\\nnext\"" - )), - - full("\"str\\u000anext\"", CalcIntType, CalcStrType, Map[LanguageCompilerStatic, String]( - CppCompiler -> "std::string(\"str\\nnext\")", - CSharpCompiler -> "\"str\\nnext\"", - JavaCompiler -> "\"str\\nnext\"", - JavaScriptCompiler -> "\"str\\nnext\"", - PerlCompiler -> "\"str\\nnext\"", - PHPCompiler -> "\"str\\nnext\"", - PythonCompiler -> "u\"str\\nnext\"", - RubyCompiler -> "\"str\\nnext\"" - )), - - full("\"str\\0next\"", CalcIntType, CalcStrType, Map[LanguageCompilerStatic, String]( - CppCompiler -> "std::string(\"str\\000next\", 8)", - CSharpCompiler -> "\"str\\0next\"", - JavaCompiler -> "\"str\\000next\"", - JavaScriptCompiler -> "\"str\\000next\"", - PerlCompiler -> "\"str\\000next\"", - PHPCompiler -> "\"str\\000next\"", - PythonCompiler -> "u\"str\\000next\"", - RubyCompiler -> "\"str\\000next\"" - )), - - everybodyExcept("\"str1\" + \"str2\"", "\"str1\" + \"str2\"", Map[LanguageCompilerStatic, String]( - CppCompiler -> "std::string(\"str1\") + std::string(\"str2\")", - PerlCompiler -> "\"str1\" . \"str2\"", - PHPCompiler -> "\"str1\" . \"str2\"", - PythonCompiler -> "u\"str1\" + u\"str2\"" - ), CalcStrType), - - everybodyExcept("\"str1\" == \"str2\"", "\"str1\" == \"str2\"", Map[LanguageCompilerStatic, String]( - CppCompiler -> "std::string(\"str1\") == (std::string(\"str2\"))", - JavaCompiler -> "\"str1\".equals(\"str2\")", - PerlCompiler -> "\"str1\" eq \"str2\"", - PythonCompiler -> "u\"str1\" == u\"str2\"" - ), CalcBooleanType), - - everybodyExcept("\"str1\" != \"str2\"", "\"str1\" != \"str2\"", Map[LanguageCompilerStatic, String]( - CppCompiler -> "std::string(\"str1\") != std::string(\"str2\")", - JavaCompiler -> "!(\"str1\").equals(\"str2\")", - PerlCompiler -> "\"str1\" ne \"str2\"", - PythonCompiler -> "u\"str1\" != u\"str2\"" - ), CalcBooleanType), - - everybodyExcept("\"str1\" < \"str2\"", "\"str1\" < \"str2\"", Map[LanguageCompilerStatic, String]( - CppCompiler -> "(std::string(\"str1\").compare(std::string(\"str2\")) < 0)", - CSharpCompiler -> "(\"str1\".CompareTo(\"str2\") < 0)", - JavaCompiler -> "(\"str1\".compareTo(\"str2\") < 0)", - PerlCompiler -> "\"str1\" lt \"str2\"", - PythonCompiler -> "u\"str1\" < u\"str2\"" - ), CalcBooleanType), - - full("\"str\".length", CalcIntType, CalcIntType, Map[LanguageCompilerStatic, String]( - CppCompiler -> "std::string(\"str\").length()", - CSharpCompiler -> "\"str\".Length", - JavaCompiler -> "\"str\".length()", - JavaScriptCompiler -> "\"str\".length", - PerlCompiler -> "length(\"str\")", - PHPCompiler -> "strlen(\"str\")", - PythonCompiler -> "len(u\"str\")", - RubyCompiler -> "\"str\".size" - )), - - full("\"str\".reverse", CalcIntType, CalcStrType, Map[LanguageCompilerStatic, String]( - CppCompiler -> "kaitai::kstream::reverse(std::string(\"str\"))", - CSharpCompiler -> "new string(Array.Reverse(\"str\".ToCharArray()))", - JavaCompiler -> "new StringBuilder(\"str\").reverse().toString()", - JavaScriptCompiler -> "Array.from(\"str\").reverse().join('')", - PerlCompiler -> "scalar(reverse(\"str\"))", - PHPCompiler -> "strrev(\"str\")", - PythonCompiler -> "u\"str\"[::-1]", - RubyCompiler -> "\"str\".reverse" - )), - - full("\"12345\".to_i", CalcIntType, CalcIntType, Map[LanguageCompilerStatic, String]( - CppCompiler -> "std::stoi(std::string(\"12345\"))", - CSharpCompiler -> "Convert.ToInt64(\"12345\", 10)", - JavaCompiler -> "Long.parseLong(\"12345\", 10)", - JavaScriptCompiler -> "Number.parseInt(\"12345\", 10)", - PerlCompiler -> "\"12345\"", - PHPCompiler -> "intval(\"12345\", 10)", - PythonCompiler -> "int(u\"12345\")", - RubyCompiler -> "\"12345\".to_i" - )), - - full("\"1234fe\".to_i(16)", CalcIntType, CalcIntType, Map[LanguageCompilerStatic, String]( - CppCompiler -> "std::stoi(std::string(\"1234fe\"), 0, 16)", - CSharpCompiler -> "Convert.ToInt64(\"1234fe\", 16)", - JavaCompiler -> "Long.parseLong(\"1234fe\", 16)", - JavaScriptCompiler -> "Number.parseInt(\"1234fe\", 16)", - PerlCompiler -> "hex(\"1234fe\")", - PHPCompiler -> "intval(\"1234fe\", 16)", - PythonCompiler -> "int(u\"1234fe\", 16)", - RubyCompiler -> "\"1234fe\".to_i(16)" - )), - - // casts - full("other.as.bar", FooBarProvider, CalcStrType, Map[LanguageCompilerStatic, String]( - CppCompiler -> "static_cast(other())->bar()", - CSharpCompiler -> "((Block) (Other)).Bar", - JavaCompiler -> "((Block) (other())).bar()", - JavaScriptCompiler -> "this.other.bar", - PerlCompiler -> "$self->other()->bar()", - PHPCompiler -> "$this->other()->bar()", - PythonCompiler -> "self.other.bar", - RubyCompiler -> "other.bar" - )), - - // very simple workaround for Scala not having optional trailing commas - everybody("999", "999") - ) - - for ((src, tp, expType, expOut) <- tests) { + +class TranslatorSpec extends FunSuite { + + // Integer literals + unary minus + everybody("123", "123", Int1Type(true)) + everybody("223", "223", Int1Type(false)) + everybody("1234", "1234") + everybody("-456", "-456") + everybody("0x1234", "4660") + // less and more than 32 Bit signed int + everybody("1000000000", "1000000000") + everybodyExcept("100000000000", "100000000000", Map[LanguageCompilerStatic, String]( + JavaCompiler -> "100000000000L" + )) + + // Float literals + everybody("1.0", "1.0", CalcFloatType) + everybody("123.456", "123.456", CalcFloatType) + everybody("-123.456", "-123.456", CalcFloatType) + + // Simple integer operations + everybody("1 + 2", "(1 + 2)") + + everybodyExcept("3 / 2", "(3 / 2)", Map( + JavaScriptCompiler -> "Math.floor(3 / 2)", + LuaCompiler -> "3 / 2", + PerlCompiler -> "int(3 / 2)", + PHPCompiler -> "intval(3 / 2)", + PythonCompiler -> "3 // 2" + )) + + everybody("1 + 2 + 5", "((1 + 2) + 5)") + + everybodyExcept("(1 + 2) / (7 * 8)", "((1 + 2) / (7 * 8))", Map( + JavaScriptCompiler -> "Math.floor((1 + 2) / (7 * 8))", + LuaCompiler -> "(1 + 2) / (7 * 8)", + PerlCompiler -> "int((1 + 2) / (7 * 8))", + PHPCompiler -> "intval((1 + 2) / (7 * 8))", + PythonCompiler -> "(1 + 2) // (7 * 8)" + )) + + everybody("1 < 2", "1 < 2", CalcBooleanType) + + everybody("1 == 2", "1 == 2", CalcBooleanType) + + full("2 < 3 ? \"foo\" : \"bar\"", CalcIntType, CalcStrType, Map[LanguageCompilerStatic, String]( + CppCompiler -> "(2 < 3) ? (std::string(\"foo\")) : (std::string(\"bar\"))", + CSharpCompiler -> "2 < 3 ? \"foo\" : \"bar\"", + JavaCompiler -> "2 < 3 ? \"foo\" : \"bar\"", + JavaScriptCompiler -> "2 < 3 ? \"foo\" : \"bar\"", + LuaCompiler -> "2 < 3 and \"foo\" or \"bar\"", + PerlCompiler -> "2 < 3 ? \"foo\" : \"bar\"", + PHPCompiler -> "2 < 3 ? \"foo\" : \"bar\"", + PythonCompiler -> "u\"foo\" if 2 < 3 else u\"bar\"", + RubyCompiler -> "2 < 3 ? \"foo\" : \"bar\"" + )) + + everybody("~777", "~777") + everybody("~(7+3)", "~((7 + 3))") + + // Simple float operations + everybody("1.2 + 3.4", "(1.2 + 3.4)", CalcFloatType) + everybody("1.2 + 3", "(1.2 + 3)", CalcFloatType) + everybody("1 + 3.4", "(1 + 3.4)", CalcFloatType) + + everybody("1.0 < 2", "1.0 < 2", CalcBooleanType) + + everybody("3 / 2.0", "(3 / 2.0)", CalcFloatType) + + everybody("(1 + 2) / (7 * 8.1)", "((1 + 2) / (7 * 8.1))", CalcFloatType) + + // Boolean literals + full("true", CalcBooleanType, CalcBooleanType, Map[LanguageCompilerStatic, String]( + CppCompiler -> "true", + CSharpCompiler -> "true", + JavaCompiler -> "true", + JavaScriptCompiler -> "true", + LuaCompiler -> "true", + PerlCompiler -> "1", + PHPCompiler -> "true", + PythonCompiler -> "True", + RubyCompiler -> "true" + )) + + full("false", CalcBooleanType, CalcBooleanType, Map[LanguageCompilerStatic, String]( + CppCompiler -> "false", + CSharpCompiler -> "false", + JavaCompiler -> "false", + JavaScriptCompiler -> "false", + LuaCompiler -> "false", + PerlCompiler -> "0", + PHPCompiler -> "false", + PythonCompiler -> "False", + RubyCompiler -> "false" + )) + + full("some_bool.to_i", CalcBooleanType, CalcIntType, Map[LanguageCompilerStatic, String]( + CppCompiler -> "some_bool()", + CSharpCompiler -> "(SomeBool ? 1 : 0)", + JavaCompiler -> "(someBool() ? 1 : 0)", + JavaScriptCompiler -> "(this.someBool | 0)", + LuaCompiler -> "self.some_bool and 1 or 0", + PerlCompiler -> "$self->some_bool()", + PHPCompiler -> "intval($this->someBool())", + PythonCompiler -> "int(self.some_bool)", + RubyCompiler -> "(some_bool ? 1 : 0)" + )) + + // Member access + full("foo_str", CalcStrType, CalcStrType, Map[LanguageCompilerStatic, String]( + CppCompiler -> "foo_str()", + CSharpCompiler -> "FooStr", + JavaCompiler -> "fooStr()", + JavaScriptCompiler -> "this.fooStr", + LuaCompiler -> "self.foo_str", + PerlCompiler -> "$self->foo_str()", + PHPCompiler -> "$this->fooStr()", + PythonCompiler -> "self.foo_str", + RubyCompiler -> "foo_str" + )) + + full("foo_block", userType("block"), userType("block"), Map[LanguageCompilerStatic, String]( + CppCompiler -> "foo_block()", + CSharpCompiler -> "FooBlock", + JavaCompiler -> "fooBlock()", + JavaScriptCompiler -> "this.fooBlock", + LuaCompiler -> "self.foo_block", + PerlCompiler -> "$self->foo_block()", + PHPCompiler -> "$this->fooBlock()", + PythonCompiler -> "self.foo_block", + RubyCompiler -> "foo_block" + )) + + full("foo.bar", FooBarProvider, CalcStrType, Map[LanguageCompilerStatic, String]( + CppCompiler -> "foo()->bar()", + CSharpCompiler -> "Foo.Bar", + JavaCompiler -> "foo().bar()", + JavaScriptCompiler -> "this.foo.bar", + LuaCompiler -> "self.foo.bar", + PerlCompiler -> "$self->foo()->bar()", + PHPCompiler -> "$this->foo()->bar()", + PythonCompiler -> "self.foo.bar", + RubyCompiler -> "foo.bar" + )) + + full("foo.inner.baz", FooBarProvider, CalcIntType, Map[LanguageCompilerStatic, String]( + CppCompiler -> "foo()->inner()->baz()", + CSharpCompiler -> "Foo.Inner.Baz", + JavaCompiler -> "foo().inner().baz()", + JavaScriptCompiler -> "this.foo.inner.baz", + LuaCompiler -> "self.foo.inner.baz", + PerlCompiler -> "$self->foo()->inner()->baz()", + PHPCompiler -> "$this->foo()->inner()->baz()", + PythonCompiler -> "self.foo.inner.baz", + RubyCompiler -> "foo.inner.baz" + )) + + full("_root.foo", userType("block"), userType("block"), Map[LanguageCompilerStatic, String]( + CppCompiler -> "_root()->foo()", + CSharpCompiler -> "M_Root.Foo", + JavaCompiler -> "_root.foo()", + JavaScriptCompiler -> "this._root.foo", + LuaCompiler -> "self._root.foo", + PerlCompiler -> "$self->_root()->foo()", + PHPCompiler -> "$this->_root()->foo()", + PythonCompiler -> "self._root.foo", + RubyCompiler -> "_root.foo" + )) + + full("a != 2 and a != 5", CalcIntType, CalcBooleanType, Map[LanguageCompilerStatic, String]( + CppCompiler -> "a() != 2 && a() != 5", + CSharpCompiler -> "A != 2 && A != 5", + JavaCompiler -> "a() != 2 && a() != 5", + JavaScriptCompiler -> "this.a != 2 && this.a != 5", + LuaCompiler -> "self.a ~= 2 and self.a ~= 5", + PerlCompiler -> "$self->a() != 2 && $self->a() != 5", + PHPCompiler -> "$this->a() != 2 && $this->a() != 5", + PythonCompiler -> "self.a != 2 and self.a != 5", + RubyCompiler -> "a != 2 && a != 5" + )) + + // Arrays + full("[0, 1, 100500]", CalcIntType, ArrayType(CalcIntType), Map[LanguageCompilerStatic, String]( + CSharpCompiler -> "new List { 0, 1, 100500 }", + JavaCompiler -> "new ArrayList(Arrays.asList(0L, 1L, 100500L))", + JavaScriptCompiler -> "[0, 1, 100500]", + LuaCompiler -> "{0, 1, 100500}", + PerlCompiler -> "(0, 1, 100500)", + PHPCompiler -> "[0, 1, 100500]", + PythonCompiler -> "[0, 1, 100500]", + RubyCompiler -> "[0, 1, 100500]" + )) + + full("[34, 0, 10, 64, 65, 66, 92]", CalcIntType, CalcBytesType, Map[LanguageCompilerStatic, String]( + CppCompiler -> "std::string(\"\\x22\\x00\\x0A\\x40\\x41\\x42\\x5C\", 7)", + CSharpCompiler -> "new byte[] { 34, 0, 10, 64, 65, 66, 92 }", + JavaCompiler -> "new byte[] { 34, 0, 10, 64, 65, 66, 92 }", + JavaScriptCompiler -> "[34, 0, 10, 64, 65, 66, 92]", + LuaCompiler -> "\"\\034\\000\\010\\064\\065\\066\\092\"", + PerlCompiler -> "pack('C*', (34, 0, 10, 64, 65, 66, 92))", + PHPCompiler -> "\"\\x22\\x00\\x0A\\x40\\x41\\x42\\x5C\"", + PythonCompiler -> "struct.pack('7b', 34, 0, 10, 64, 65, 66, 92)", + RubyCompiler -> "[34, 0, 10, 64, 65, 66, 92].pack('C*')" + )) + + full("[255, 0, 255]", CalcIntType, CalcBytesType, Map[LanguageCompilerStatic, String]( + CppCompiler -> "std::string(\"\\xFF\\x00\\xFF\", 3)", + CSharpCompiler -> "new byte[] { 255, 0, 255 }", + JavaCompiler -> "new byte[] { -1, 0, -1 }", + JavaScriptCompiler -> "[255, 0, 255]", + LuaCompiler -> "\"\\255\\000\\255\"", + PerlCompiler -> "pack('C*', (255, 0, 255))", + PHPCompiler -> "\"\\xFF\\x00\\xFF\"", + PythonCompiler -> "struct.pack('3b', -1, 0, -1)", + RubyCompiler -> "[255, 0, 255].pack('C*')" + )) + + full("a[42]", ArrayType(CalcStrType), CalcStrType, Map[LanguageCompilerStatic, String]( + CppCompiler -> "a()->at(42)", + CSharpCompiler -> "A[42]", + JavaCompiler -> "a().get(42)", + JavaScriptCompiler -> "this.a[42]", + LuaCompiler -> "self.a[43]", + PHPCompiler -> "$this->a()[42]", + PythonCompiler -> "self.a[42]", + RubyCompiler -> "a[42]" + )) + + full("a[42 - 2]", ArrayType(CalcStrType), CalcStrType, Map[LanguageCompilerStatic, String]( + CppCompiler -> "a()->at((42 - 2))", + CSharpCompiler -> "A[(42 - 2)]", + JavaCompiler -> "a().get((42 - 2))", + JavaScriptCompiler -> "this.a[(42 - 2)]", + LuaCompiler -> "self.a[(43 - 2)]", + PHPCompiler -> "$this->a()[(42 - 2)]", + PythonCompiler -> "self.a[(42 - 2)]", + RubyCompiler -> "a[(42 - 2)]" + )) + + full("a.first", ArrayType(CalcIntType), CalcIntType, Map[LanguageCompilerStatic, String]( + CppCompiler -> "a()->front()", + CSharpCompiler -> "A[0]", + JavaCompiler -> "a().get(0)", + JavaScriptCompiler -> "this.a[0]", + LuaCompiler -> "self.a[1]", + PHPCompiler -> "$this->a()[0]", + PythonCompiler -> "self.a[0]", + RubyCompiler -> "a.first" + )) + + full("a.last", ArrayType(CalcIntType), CalcIntType, Map[LanguageCompilerStatic, String]( + CppCompiler -> "a()->back()", + CSharpCompiler -> "A[A.Length - 1]", + JavaCompiler -> "a().get(a().size() - 1)", + JavaScriptCompiler -> "this.a[this.a.length - 1]", + LuaCompiler -> "self.a[#self.a]", + PHPCompiler -> "$this->a()[count($this->a()) - 1]", + PythonCompiler -> "self.a[-1]", + RubyCompiler -> "a.last" + )) + + full("a.size", ArrayType(CalcIntType), CalcIntType, Map[LanguageCompilerStatic, String]( + CppCompiler -> "a()->size()", + CSharpCompiler -> "A.Count", + JavaCompiler -> "a().size()", + JavaScriptCompiler -> "this.a.length", + LuaCompiler -> "#self.a", + PHPCompiler -> "count($this->a())", + PerlCompiler -> "scalar($self->a())", + PythonCompiler -> "len(self.a)", + RubyCompiler -> "a.length" + )) + + // Strings + full("\"str\"", CalcIntType, CalcStrType, Map[LanguageCompilerStatic, String]( + CppCompiler -> "std::string(\"str\")", + CSharpCompiler -> "\"str\"", + JavaCompiler -> "\"str\"", + JavaScriptCompiler -> "\"str\"", + LuaCompiler -> "\"str\"", + PerlCompiler -> "\"str\"", + PHPCompiler -> "\"str\"", + PythonCompiler -> "u\"str\"", + RubyCompiler -> "\"str\"" + )) + + full("\"str\\nnext\"", CalcIntType, CalcStrType, Map[LanguageCompilerStatic, String]( + CppCompiler -> "std::string(\"str\\nnext\")", + CSharpCompiler -> "\"str\\nnext\"", + JavaCompiler -> "\"str\\nnext\"", + JavaScriptCompiler -> "\"str\\nnext\"", + LuaCompiler -> "\"str\\nnext\"", + PerlCompiler -> "\"str\\nnext\"", + PHPCompiler -> "\"str\\nnext\"", + PythonCompiler -> "u\"str\\nnext\"", + RubyCompiler -> "\"str\\nnext\"" + )) + + full("\"str\\u000anext\"", CalcIntType, CalcStrType, Map[LanguageCompilerStatic, String]( + CppCompiler -> "std::string(\"str\\nnext\")", + CSharpCompiler -> "\"str\\nnext\"", + JavaCompiler -> "\"str\\nnext\"", + JavaScriptCompiler -> "\"str\\nnext\"", + LuaCompiler -> "\"str\\nnext\"", + PerlCompiler -> "\"str\\nnext\"", + PHPCompiler -> "\"str\\nnext\"", + PythonCompiler -> "u\"str\\nnext\"", + RubyCompiler -> "\"str\\nnext\"" + )) + + full("\"str\\0next\"", CalcIntType, CalcStrType, Map[LanguageCompilerStatic, String]( + CppCompiler -> "std::string(\"str\\000next\", 8)", + CSharpCompiler -> "\"str\\0next\"", + JavaCompiler -> "\"str\\000next\"", + JavaScriptCompiler -> "\"str\\000next\"", + LuaCompiler -> "\"str\\000next\"", + PerlCompiler -> "\"str\\000next\"", + PHPCompiler -> "\"str\\000next\"", + PythonCompiler -> "u\"str\\000next\"", + RubyCompiler -> "\"str\\000next\"" + )) + + everybodyExcept("\"str1\" + \"str2\"", "\"str1\" + \"str2\"", Map[LanguageCompilerStatic, String]( + CppCompiler -> "std::string(\"str1\") + std::string(\"str2\")", + LuaCompiler -> "\"str1\" .. \"str2\"", + PerlCompiler -> "\"str1\" . \"str2\"", + PHPCompiler -> "\"str1\" . \"str2\"", + PythonCompiler -> "u\"str1\" + u\"str2\"" + ), CalcStrType) + + everybodyExcept("\"str1\" == \"str2\"", "\"str1\" == \"str2\"", Map[LanguageCompilerStatic, String]( + CppCompiler -> "std::string(\"str1\") == (std::string(\"str2\"))", + JavaCompiler -> "\"str1\".equals(\"str2\")", + LuaCompiler -> "\"str1\" == \"str2\"", + PerlCompiler -> "\"str1\" eq \"str2\"", + PythonCompiler -> "u\"str1\" == u\"str2\"" + ), CalcBooleanType) + + everybodyExcept("\"str1\" != \"str2\"", "\"str1\" != \"str2\"", Map[LanguageCompilerStatic, String]( + CppCompiler -> "std::string(\"str1\") != std::string(\"str2\")", + JavaCompiler -> "!(\"str1\").equals(\"str2\")", + LuaCompiler -> "\"str1\" ~= \"str2\"", + PerlCompiler -> "\"str1\" ne \"str2\"", + PythonCompiler -> "u\"str1\" != u\"str2\"" + ), CalcBooleanType) + + everybodyExcept("\"str1\" < \"str2\"", "\"str1\" < \"str2\"", Map[LanguageCompilerStatic, String]( + CppCompiler -> "(std::string(\"str1\").compare(std::string(\"str2\")) < 0)", + CSharpCompiler -> "(\"str1\".CompareTo(\"str2\") < 0)", + JavaCompiler -> "(\"str1\".compareTo(\"str2\") < 0)", + LuaCompiler -> "\"str1\" < \"str2\"", + PerlCompiler -> "\"str1\" lt \"str2\"", + PythonCompiler -> "u\"str1\" < u\"str2\"" + ), CalcBooleanType) + + full("\"str\".length", CalcIntType, CalcIntType, Map[LanguageCompilerStatic, String]( + CppCompiler -> "std::string(\"str\").length()", + CSharpCompiler -> "\"str\".Length", + JavaCompiler -> "\"str\".length()", + JavaScriptCompiler -> "#\"str\"", + LuaCompiler -> "string.len(\"str\")", + PerlCompiler -> "length(\"str\")", + PHPCompiler -> "strlen(\"str\")", + PythonCompiler -> "len(u\"str\")", + RubyCompiler -> "\"str\".size" + )) + + full("\"str\".reverse", CalcIntType, CalcStrType, Map[LanguageCompilerStatic, String]( + CppCompiler -> "kaitai::kstream::reverse(std::string(\"str\"))", + CSharpCompiler -> "new string(Array.Reverse(\"str\".ToCharArray()))", + JavaCompiler -> "new StringBuilder(\"str\").reverse().toString()", + JavaScriptCompiler -> "Array.from(\"str\").reverse().join('')", + LuaCompiler -> "string.reverse(\"str\")", + PerlCompiler -> "scalar(reverse(\"str\"))", + PHPCompiler -> "strrev(\"str\")", + PythonCompiler -> "u\"str\"[::-1]", + RubyCompiler -> "\"str\".reverse" + )) + + full("\"12345\".to_i", CalcIntType, CalcIntType, Map[LanguageCompilerStatic, String]( + CppCompiler -> "std::stoi(std::string(\"12345\"))", + CSharpCompiler -> "Convert.ToInt64(\"12345\", 10)", + JavaCompiler -> "Long.parseLong(\"12345\", 10)", + JavaScriptCompiler -> "Number.parseInt(\"12345\", 10)", + LuaCompiler -> "tonumber(\"12345\")", + PerlCompiler -> "\"12345\"", + PHPCompiler -> "intval(\"12345\", 10)", + PythonCompiler -> "int(u\"12345\")", + RubyCompiler -> "\"12345\".to_i" + )) + + full("\"1234fe\".to_i(16)", CalcIntType, CalcIntType, Map[LanguageCompilerStatic, String]( + CppCompiler -> "std::stoi(std::string(\"1234fe\"), 0, 16)", + CSharpCompiler -> "Convert.ToInt64(\"1234fe\", 16)", + JavaCompiler -> "Long.parseLong(\"1234fe\", 16)", + JavaScriptCompiler -> "Number.parseInt(\"1234fe\", 16)", + LuaCompiler -> "tonumber(\"1234fe\", 16)", + PerlCompiler -> "hex(\"1234fe\")", + PHPCompiler -> "intval(\"1234fe\", 16)", + PythonCompiler -> "int(u\"1234fe\", 16)", + RubyCompiler -> "\"1234fe\".to_i(16)" + )) + + // casts + full("other.as.bar", FooBarProvider, CalcStrType, Map[LanguageCompilerStatic, String]( + CppCompiler -> "static_cast(other())->bar()", + CSharpCompiler -> "((Block) (Other)).Bar", + JavaCompiler -> "((Block) (other())).bar()", + JavaScriptCompiler -> "this.other.bar", + LuaCompiler -> "self.other.bar", + PerlCompiler -> "$self->other()->bar()", + PHPCompiler -> "$this->other()->bar()", + PythonCompiler -> "self.other.bar", + RubyCompiler -> "other.bar" + )) + + def runTest(src: String, tp: TypeProvider, expType: DataType, expOut: ResultMap) { var eo: Option[Ast.expr] = None test(s"_expr:$src") { eo = Some(Expressions.parse(src)) } - LanguageCompilerStatic.NAME_TO_CLASS. - filter { case (_, langObj) => langObj != GraphvizClassCompiler }. - foreach { case (langName, langObj) => + val langs = Map[LanguageCompilerStatic, BaseTranslator]( + CppCompiler -> new CppTranslator(tp, new ImportList()), + CSharpCompiler -> new CSharpTranslator(tp, new ImportList()), + JavaCompiler -> new JavaTranslator(tp, new ImportList()), + JavaScriptCompiler -> new JavaScriptTranslator(tp), + LuaCompiler -> new LuaTranslator(tp, new ImportList()), + PerlCompiler -> new PerlTranslator(tp, new ImportList()), + PHPCompiler -> new PHPTranslator(tp, RuntimeConfig()), + PythonCompiler -> new PythonTranslator(tp, new ImportList()), + RubyCompiler -> new RubyTranslator(tp) + ) + + langs.foreach { case (langObj, tr) => + val langName = LanguageCompilerStatic.CLASS_TO_NAME(langObj) test(s"$langName:$src") { eo match { case Some(e) => - val tr: BaseTranslator = langObj.getTranslator(tp, RuntimeConfig()) + val tr: BaseTranslator = langs(langObj) expOut.get(langObj) match { case Some(expResult) => tr.detectType(e) should be(expType) @@ -433,6 +474,10 @@ class TranslatorSpec extends FunSuite with TableDrivenPropertyChecks { override def resolveType(typeName: String): DataType = throw new NotImplementedError + + override def isLazy(attrName: String): Boolean = false + + override def isLazy(inClass: ClassSpec, attrName: String): Boolean = false } case class Always(t: DataType) extends FakeTypeProvider { @@ -472,19 +517,17 @@ class TranslatorSpec extends FunSuite with TableDrivenPropertyChecks { lazy val ALL_LANGS = LanguageCompilerStatic.NAME_TO_CLASS.values - def full(src: String, srcType: DataType, expType: DataType, expOut: ResultMap): TestSpec = - (src, Always(srcType), expType, expOut) + def full(src: String, srcType: DataType, expType: DataType, expOut: ResultMap) = + runTest(src, Always(srcType), expType, expOut) - def full(src: String, tp: TypeProvider, expType: DataType, expOut: ResultMap): TestSpec = - (src, tp, expType, expOut) + def full(src: String, tp: TypeProvider, expType: DataType, expOut: ResultMap) = + runTest(src, tp, expType, expOut) - def everybody(src: String, expOut: String, expType: DataType = CalcIntType): TestSpec = { - (src, Always(CalcIntType), expType, ALL_LANGS.map((langObj) => langObj -> expOut).toMap) - } + def everybody(src: String, expOut: String, expType: DataType = CalcIntType) = + runTest(src, Always(CalcIntType), expType, ALL_LANGS.map((langObj) => langObj -> expOut).toMap) - def everybodyExcept(src: String, commonExpOut: String, rm: ResultMap, expType: DataType = CalcIntType): TestSpec = { - (src, Always(CalcIntType), expType, ALL_LANGS.map((langObj) => + def everybodyExcept(src: String, commonExpOut: String, rm: ResultMap, expType: DataType = CalcIntType) = + runTest(src, Always(CalcIntType), expType, ALL_LANGS.map((langObj) => langObj -> rm.getOrElse(langObj, commonExpOut) ).toMap) - } } diff --git a/project/build.properties b/project/build.properties index d638b4f34..e98ac44be 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version = 0.13.8 \ No newline at end of file +sbt.version = 1.1.0-RC4 diff --git a/project/plugins.sbt b/project/plugins.sbt index 2b65a24e6..322ef8094 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,5 +1,5 @@ logLevel := Level.Warn -addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.2.0-M8") -addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.6.1") -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.14") +addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.3.2") +addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.7.0") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.21") diff --git a/shared/src/main/scala/io/kaitai/struct/ClassCompiler.scala b/shared/src/main/scala/io/kaitai/struct/ClassCompiler.scala index b15b468fc..660ba8522 100644 --- a/shared/src/main/scala/io/kaitai/struct/ClassCompiler.scala +++ b/shared/src/main/scala/io/kaitai/struct/ClassCompiler.scala @@ -1,19 +1,21 @@ package io.kaitai.struct import io.kaitai.struct.CompileLog.FileSuccess -import io.kaitai.struct.datatype.DataType import io.kaitai.struct.datatype.DataType._ +import io.kaitai.struct.datatype._ +import io.kaitai.struct.exprlang.Ast import io.kaitai.struct.format._ import io.kaitai.struct.languages.components.{LanguageCompiler, LanguageCompilerStatic} import scala.collection.mutable.ListBuffer class ClassCompiler( + classSpecs: ClassSpecs, val topClass: ClassSpec, config: RuntimeConfig, langObj: LanguageCompilerStatic ) extends AbstractCompiler { - val provider = new ClassTypeProvider(topClass) + val provider = new ClassTypeProvider(classSpecs, topClass) val topClassName = topClass.name val lang: LanguageCompiler = langObj.getCompiler(provider, config) @@ -36,16 +38,22 @@ class ClassCompiler( ) } + /** + * Generates code for one full class using a given [[ClassSpec]]. + * @param curClass current class to generate code for + */ def compileClass(curClass: ClassSpec): Unit = { provider.nowClass = curClass - if (!curClass.doc.isEmpty) - lang.classDoc(curClass.name, curClass.doc) + if (!lang.innerDocstrings) + compileClassDoc(curClass) lang.classHeader(curClass.name) + if (lang.innerDocstrings) + compileClassDoc(curClass) val extraAttrs = ListBuffer[AttrSpec]() extraAttrs += AttrSpec(List(), RootIdentifier, UserTypeInstream(topClassName, None)) - extraAttrs += AttrSpec(List(), ParentIdentifier, UserTypeInstream(curClass.parentTypeName, None)) + extraAttrs += AttrSpec(List(), ParentIdentifier, curClass.parentType) // Forward declarations for recursive types curClass.types.foreach { case (typeName, _) => lang.classForwardDeclaration(List(typeName)) } @@ -56,12 +64,69 @@ class ClassCompiler( if (lang.debug) lang.debugClassSequence(curClass.seq) - lang.classConstructorHeader(curClass.name, curClass.parentTypeName, topClassName) + // Constructor + compileConstructor(curClass) + + // Read method(s) + compileEagerRead(curClass.seq, extraAttrs, curClass.meta.endian) + + // Destructor + compileDestructor(curClass) + + // Recursive types + if (lang.innerClasses) { + compileSubclasses(curClass) + + provider.nowClass = curClass + } + + compileInstances(curClass, extraAttrs) + + // Attributes declarations and readers + val allAttrs: List[MemberSpec] = curClass.seq ++ curClass.params ++ extraAttrs + compileAttrDeclarations(allAttrs) + compileAttrReaders(allAttrs) + + lang.classFooter(curClass.name) + + if (!lang.innerClasses) + compileSubclasses(curClass) + + if (!lang.innerEnums) + compileEnums(curClass) + } + + /** + * Compiles constructor for a given class. Generally, it should: + * + * * store passed parameters, io/root/parent/endianness if needed + * * initialize everything + * * invoke _read() method, if applicable + * + * @param curClass current class to generate code for + */ + def compileConstructor(curClass: ClassSpec) = { + lang.classConstructorHeader( + curClass.name, + curClass.parentType, + topClassName, + curClass.meta.endian.contains(InheritedEndian), + curClass.params + ) curClass.instances.foreach { case (instName, _) => lang.instanceClear(instName) } - compileSeq(curClass.seq, extraAttrs) + if (!lang.debug) + lang.runRead() lang.classConstructorFooter + } - lang.classDestructorHeader(curClass.name, curClass.parentTypeName, topClassName) + /** + * Compiles destructor for a given class. It should clean up everything + * (i.e. every applicable allocated seq / instance attribute variables, and + * any extra attribute variables, if they were used). + * @param curClass current class to generate code for + */ + def compileDestructor(curClass: ClassSpec) = { + lang.classDestructorHeader(curClass.name, curClass.parentType, topClassName) curClass.seq.foreach((attr) => lang.attrDestructor(attr, attr.id)) curClass.instances.foreach { case (id, instSpec) => instSpec match { @@ -70,40 +135,95 @@ class ClassCompiler( } } lang.classDestructorFooter + } - // Recursive types - if (lang.innerClasses) { - compileSubclasses(curClass) + def compileAttrDeclarations(attrs: List[MemberSpec]): Unit = { + attrs.foreach { (attr) => + val isNullable = if (lang.switchBytesOnlyAsRaw) { + attr.isNullableSwitchRaw + } else { + attr.isNullable + } + lang.attributeDeclaration(attr.id, attr.dataTypeComposite, isNullable) + } + } - provider.nowClass = curClass + /** + * Iterates over a given list of attributes and generates attribute + * readers (AKA getters) for each of them. + * @param attrs attribute list to traverse + */ + def compileAttrReaders(attrs: List[MemberSpec]): Unit = + attrs.foreach { (attr) => + // FIXME: Python should have some form of attribute docs too + if (!attr.doc.isEmpty && !lang.innerDocstrings) + lang.attributeDoc(attr.id, attr.doc) + val isNullable = if (lang.switchBytesOnlyAsRaw) { + attr.isNullableSwitchRaw + } else { + attr.isNullable + } + lang.attributeReader(attr.id, attr.dataTypeComposite, isNullable) } - curClass.instances.foreach { case (instName, instSpec) => compileInstance(curClass.name, instName, instSpec, extraAttrs) } + def compileEagerRead(seq: List[AttrSpec], extraAttrs: ListBuffer[AttrSpec], endian: Option[Endianness]): Unit = { + endian match { + case None | Some(_: FixedEndian) => + compileSeqProc(seq, extraAttrs, None) + case Some(ce: CalcEndian) => + lang.readHeader(None, false) + compileCalcEndian(ce) + lang.runReadCalc() + lang.readFooter() - // Attributes declarations and readers - (curClass.seq ++ extraAttrs).foreach((attr) => lang.attributeDeclaration(attr.id, attr.dataTypeComposite, attr.cond)) - (curClass.seq ++ extraAttrs).foreach { (attr) => - if (!attr.doc.isEmpty) - lang.attributeDoc(attr.id, attr.doc) - lang.attributeReader(attr.id, attr.dataTypeComposite, attr.cond) + compileSeqProc(seq, extraAttrs, Some(LittleEndian)) + compileSeqProc(seq, extraAttrs, Some(BigEndian)) + case Some(InheritedEndian) => + lang.readHeader(None, false) + lang.runReadCalc() + lang.readFooter() + + compileSeqProc(seq, extraAttrs, Some(LittleEndian)) + compileSeqProc(seq, extraAttrs, Some(BigEndian)) } + } - lang.classFooter(curClass.name) + val IS_LE_ID = SpecialIdentifier("_is_le") - if (!lang.innerClasses) - compileSubclasses(curClass) + def compileCalcEndian(ce: CalcEndian): Unit = { + def renderProc(result: FixedEndian): Unit = { + val v = Ast.expr.Bool(result == LittleEndian) + lang.instanceCalculate(IS_LE_ID, CalcBooleanType, v) + } - if (!lang.innerEnums) - compileEnums(curClass) + lang.switchCases[FixedEndian](IS_LE_ID, ce.on, ce.cases, renderProc, renderProc) + } + + /** + * Compiles seq reading method (complete with header and footer). + * @param seq sequence of attributes + * @param extraAttrs extra attributes to be allocated + * @param defEndian default endianness + */ + def compileSeqProc(seq: List[AttrSpec], extraAttrs: ListBuffer[AttrSpec], defEndian: Option[FixedEndian]) = { + lang.readHeader(defEndian, seq.isEmpty) + compileSeq(seq, extraAttrs, defEndian) + lang.readFooter() } - def compileSeq(seq: List[AttrSpec], extraAttrs: ListBuffer[AttrSpec]) = { + /** + * Compiles seq reading method body (only reading statements). + * @param seq sequence of attributes + * @param extraAttrs extra attributes to be allocated + * @param defEndian default endianness + */ + def compileSeq(seq: List[AttrSpec], extraAttrs: ListBuffer[AttrSpec], defEndian: Option[FixedEndian]) = { var wasUnaligned = false seq.foreach { (attr) => val nowUnaligned = isUnalignedBits(attr.dataType) if (wasUnaligned && !nowUnaligned) lang.alignToByte(lang.normalIO) - lang.attrParse(attr, attr.id, extraAttrs) + lang.attrParse(attr, attr.id, extraAttrs, defEndian) wasUnaligned = nowUnaligned } } @@ -111,23 +231,30 @@ class ClassCompiler( def compileEnums(curClass: ClassSpec): Unit = curClass.enums.foreach { case(_, enumColl) => compileEnum(curClass, enumColl) } + /** + * Compile subclasses for a given class. + * @param curClass current class to generate code for + */ def compileSubclasses(curClass: ClassSpec): Unit = curClass.types.foreach { case (_, intClass) => compileClass(intClass) } - def compileInstance(className: List[String], instName: InstanceIdentifier, instSpec: InstanceSpec, extraAttrs: ListBuffer[AttrSpec]): Unit = { + def compileInstances(curClass: ClassSpec, extraAttrs: ListBuffer[AttrSpec]) = { + curClass.instances.foreach { case (instName, instSpec) => + compileInstance(curClass.name, instName, instSpec, extraAttrs, curClass.meta.endian) + } + } + + def compileInstance(className: List[String], instName: InstanceIdentifier, instSpec: InstanceSpec, extraAttrs: ListBuffer[AttrSpec], endian: Option[Endianness]): Unit = { // Determine datatype val dataType = instSpec.dataTypeComposite - // Declare caching variable - val condSpec = instSpec match { - case vis: ValueInstanceSpec => ConditionalSpec(vis.ifExpr, NoRepeat) - case pis: ParseInstanceSpec => pis.cond - } - lang.instanceDeclaration(instName, dataType, condSpec) + compileInstanceDeclaration(instName, instSpec) - if (!instSpec.doc.isEmpty) - lang.attributeDoc(instName, instSpec.doc) - lang.instanceHeader(className, instName, dataType) + if (!lang.innerDocstrings) + compileInstanceDoc(instName, instSpec) + lang.instanceHeader(className, instName, dataType, instSpec.isNullable) + if (lang.innerDocstrings) + compileInstanceDoc(instName, instSpec) lang.instanceCheckCacheAndReturn(instName) instSpec match { @@ -136,7 +263,7 @@ class ClassCompiler( lang.instanceCalculate(instName, dataType, vi.value) lang.attrParseIfFooter(vi.ifExpr) case i: ParseInstanceSpec => - lang.attrParse(i, instName, extraAttrs) + lang.attrParse(i, instName, extraAttrs, endian) } lang.instanceSetCalculated(instName) @@ -144,9 +271,11 @@ class ClassCompiler( lang.instanceFooter } - def compileEnum(curClass: ClassSpec, enumColl: EnumSpec): Unit = { + def compileInstanceDeclaration(instName: InstanceIdentifier, instSpec: InstanceSpec): Unit = + lang.instanceDeclaration(instName, instSpec.dataTypeComposite, instSpec.isNullable) + + def compileEnum(curClass: ClassSpec, enumColl: EnumSpec): Unit = lang.enumDeclaration(curClass.name, enumColl.name.last, enumColl.sortedSeq) - } def isUnalignedBits(dt: DataType): Boolean = dt match { @@ -154,4 +283,14 @@ class ClassCompiler( case et: EnumType => isUnalignedBits(et.basedOn) case _ => false } + + def compileClassDoc(curClass: ClassSpec) = { + if (!curClass.doc.isEmpty) + lang.classDoc(curClass.name, curClass.doc) + } + + def compileInstanceDoc(instName: Identifier, instSpec: InstanceSpec) { + if (!instSpec.doc.isEmpty) + lang.attributeDoc(instName, instSpec.doc) + } } diff --git a/shared/src/main/scala/io/kaitai/struct/ClassTypeProvider.scala b/shared/src/main/scala/io/kaitai/struct/ClassTypeProvider.scala index be6172e2f..11eda9e07 100644 --- a/shared/src/main/scala/io/kaitai/struct/ClassTypeProvider.scala +++ b/shared/src/main/scala/io/kaitai/struct/ClassTypeProvider.scala @@ -6,7 +6,7 @@ import io.kaitai.struct.format._ import io.kaitai.struct.precompile.{EnumNotFoundError, FieldNotFoundError, TypeNotFoundError, TypeUndecidedError} import io.kaitai.struct.translators.TypeProvider -class ClassTypeProvider(topClass: ClassSpec) extends TypeProvider { +class ClassTypeProvider(classSpecs: ClassSpecs, var topClass: ClassSpec) extends TypeProvider { var nowClass = topClass var _currentIteratorType: Option[DataType] = None @@ -20,30 +20,36 @@ class ClassTypeProvider(topClass: ClassSpec) extends TypeProvider { override def determineType(inClass: ClassSpec, attrName: String): DataType = { attrName match { - case "_root" => + case Identifier.ROOT => makeUserType(topClass) - case "_parent" => + case Identifier.PARENT => if (inClass.parentClass == UnknownClassSpec) - throw new RuntimeException(s"Unable to derive _parent type in ${inClass.name.mkString("::")}") + throw new RuntimeException(s"Unable to derive ${Identifier.PARENT} type in ${inClass.name.mkString("::")}") makeUserType(inClass.parentClass) - case "_io" => + case Identifier.IO => KaitaiStreamType - case "_" => + case Identifier.ITERATOR => currentIteratorType - case "_on" => + case Identifier.SWITCH_ON => currentSwitchType + case Identifier.INDEX => + CalcIntType case _ => inClass.seq.foreach { el => if (el.id == NamedIdentifier(attrName)) return el.dataTypeComposite } + inClass.params.foreach { el => + if (el.id == NamedIdentifier(attrName)) + return el.dataType + } inClass.instances.get(InstanceIdentifier(attrName)) match { case Some(i: ValueInstanceSpec) => - i.dataType match { + val dt = i.dataType match { case Some(t) => t case None => throw new TypeUndecidedError(attrName) } - return i.dataType.get + return dt case Some(i: ParseInstanceSpec) => return i.dataTypeComposite case None => // do nothing } @@ -81,18 +87,43 @@ class ClassTypeProvider(topClass: ClassSpec) extends TypeProvider { override def resolveType(typeName: String): DataType = resolveType(nowClass, typeName) def resolveType(inClass: ClassSpec, typeName: String): DataType = { + if (inClass.name.last == typeName) + return makeUserType(inClass) + inClass.types.get(typeName) match { case Some(spec) => - val ut = UserTypeInstream(spec.name, None) - ut.classSpec = Some(spec) - ut + makeUserType(spec) case None => // let's try upper levels of hierarchy inClass.upClass match { case Some(upClass) => resolveType(upClass, typeName) case None => - throw new TypeNotFoundError(typeName, nowClass) + classSpecs.get(typeName) match { + case Some(spec) => makeUserType(spec) + case None => + throw new TypeNotFoundError(typeName, nowClass) + } } } } + + override def isLazy(attrName: String): Boolean = isLazy(nowClass, attrName) + + def isLazy(inClass: ClassSpec, attrName: String): Boolean = { + inClass.seq.foreach { el => + if (el.id == NamedIdentifier(attrName)) + return false + } + inClass.params.foreach { el => + if (el.id == NamedIdentifier(attrName)) + return false + } + inClass.instances.get(InstanceIdentifier(attrName)) match { + case Some(i) => + return true + case None => + // do nothing + } + throw new FieldNotFoundError(attrName, inClass) + } } diff --git a/shared/src/main/scala/io/kaitai/struct/CompileLog.scala b/shared/src/main/scala/io/kaitai/struct/CompileLog.scala index b5d8d03f3..134494b1e 100644 --- a/shared/src/main/scala/io/kaitai/struct/CompileLog.scala +++ b/shared/src/main/scala/io/kaitai/struct/CompileLog.scala @@ -1,10 +1,18 @@ package io.kaitai.struct +/** + * Namespace for all the objects related to compilation results. + */ object CompileLog { - sealed trait InputEntry extends Jsonable + trait CanHasErrors { + def hasErrors: Boolean + } + + sealed trait InputEntry extends Jsonable with CanHasErrors case class InputFailure(errors: List[CompileError]) extends InputEntry { override def toJson: String = JSON.mapToJson(Map("errors" -> errors)) + override def hasErrors = true } case class InputSuccess( @@ -15,13 +23,17 @@ object CompileLog { "firstSpecName" -> firstSpecName, "output" -> output )) + + override def hasErrors: Boolean = + output.values.map(_.values.map(_.hasErrors).max).max } /** Compilation result of a single [[io.kaitai.struct.format.ClassSpec]] into a single target language. */ - sealed trait SpecEntry extends Jsonable + sealed trait SpecEntry extends Jsonable with CanHasErrors case class SpecFailure(errors: List[CompileError]) extends SpecEntry { override def toJson: String = JSON.mapToJson(Map("errors" -> errors)) + override def hasErrors: Boolean = true } case class SpecSuccess( @@ -32,6 +44,7 @@ object CompileLog { "topLevelName" -> topLevelName, "files" -> files )) + override def hasErrors: Boolean = false } case class FileSuccess( diff --git a/shared/src/main/scala/io/kaitai/struct/GoClassCompiler.scala b/shared/src/main/scala/io/kaitai/struct/GoClassCompiler.scala new file mode 100644 index 000000000..0e57e55bb --- /dev/null +++ b/shared/src/main/scala/io/kaitai/struct/GoClassCompiler.scala @@ -0,0 +1,98 @@ +package io.kaitai.struct + +import io.kaitai.struct.datatype.DataType.{KaitaiStreamType, UserTypeInstream} +import io.kaitai.struct.datatype.{Endianness, FixedEndian, InheritedEndian} +import io.kaitai.struct.format._ +import io.kaitai.struct.languages.GoCompiler +import io.kaitai.struct.languages.components.ExtraAttrs + +import scala.collection.mutable.ListBuffer + +class GoClassCompiler( + classSpecs: ClassSpecs, + override val topClass: ClassSpec, + config: RuntimeConfig +) extends ClassCompiler(classSpecs, topClass, config, GoCompiler) { + + override def compileClass(curClass: ClassSpec): Unit = { + provider.nowClass = curClass + + val extraAttrs = ListBuffer[AttrSpec]() + extraAttrs += AttrSpec(List(), IoIdentifier, KaitaiStreamType) + extraAttrs += AttrSpec(List(), RootIdentifier, UserTypeInstream(topClassName, None)) + extraAttrs += AttrSpec(List(), ParentIdentifier, curClass.parentType) + + extraAttrs ++= getExtraAttrs(curClass) + + if (!curClass.doc.isEmpty) + lang.classDoc(curClass.name, curClass.doc) + + // Basic struct declaration + lang.classHeader(curClass.name) + compileAttrDeclarations(curClass.seq ++ extraAttrs) + curClass.instances.foreach { case (instName, instSpec) => + compileInstanceDeclaration(instName, instSpec) + } + lang.classFooter(curClass.name) + + // Constructor = Read() function + compileReadFunction(curClass, extraAttrs) + + compileInstances(curClass, extraAttrs) + + compileAttrReaders(curClass.seq ++ extraAttrs) + + compileEnums(curClass) + + // Recursive types + compileSubclasses(curClass) + } + + def compileReadFunction(curClass: ClassSpec, extraAttrs: ListBuffer[AttrSpec]) = { + lang.classConstructorHeader( + curClass.name, + curClass.parentType, + topClassName, + curClass.meta.endian.contains(InheritedEndian), + curClass.params + ) + // FIXME + val defEndian = curClass.meta.endian match { + case Some(fe: FixedEndian) => Some(fe) + case _ => None + } + compileSeq(curClass.seq, extraAttrs, defEndian) + lang.classConstructorFooter + } + + override def compileInstance(className: List[String], instName: InstanceIdentifier, instSpec: InstanceSpec, extraAttrs: ListBuffer[AttrSpec], endian: Option[Endianness]): Unit = { + // FIXME: support calculated endianness + + // Determine datatype + val dataType = instSpec.dataTypeComposite + + if (!instSpec.doc.isEmpty) + lang.attributeDoc(instName, instSpec.doc) + lang.instanceHeader(className, instName, dataType, instSpec.isNullable) + lang.instanceCheckCacheAndReturn(instName) + + instSpec match { + case vi: ValueInstanceSpec => + lang.attrParseIfHeader(instName, vi.ifExpr) + lang.instanceCalculate(instName, dataType, vi.value) + lang.attrParseIfFooter(vi.ifExpr) + case i: ParseInstanceSpec => + lang.attrParse(i, instName, extraAttrs, None) // FIXME + } + + lang.instanceSetCalculated(instName) + lang.instanceReturn(instName) + lang.instanceFooter + } + + def getExtraAttrs(curClass: ClassSpec): List[AttrSpec] = { + curClass.seq.foldLeft(List[AttrSpec]())( + (attrs, attr) => attrs ++ ExtraAttrs.forAttr(attr) + ) + } +} diff --git a/shared/src/main/scala/io/kaitai/struct/GraphvizClassCompiler.scala b/shared/src/main/scala/io/kaitai/struct/GraphvizClassCompiler.scala index 686f0e902..03d8ef1ce 100644 --- a/shared/src/main/scala/io/kaitai/struct/GraphvizClassCompiler.scala +++ b/shared/src/main/scala/io/kaitai/struct/GraphvizClassCompiler.scala @@ -1,22 +1,23 @@ package io.kaitai.struct -import io.kaitai.struct.exprlang.Ast -import io.kaitai.struct.exprlang.Ast.expr import io.kaitai.struct.datatype.DataType import io.kaitai.struct.datatype.DataType._ +import io.kaitai.struct.exprlang.Ast +import io.kaitai.struct.exprlang.Ast.expr import io.kaitai.struct.format._ import io.kaitai.struct.languages.components.{LanguageCompiler, LanguageCompilerStatic} -import io.kaitai.struct.translators.{BaseTranslator, RubyTranslator, TypeProvider} +import io.kaitai.struct.precompile.CalculateSeqSizes +import io.kaitai.struct.translators.RubyTranslator import scala.collection.mutable.ListBuffer -class GraphvizClassCompiler(topClass: ClassSpec) extends AbstractCompiler { +class GraphvizClassCompiler(classSpecs: ClassSpecs, topClass: ClassSpec) extends AbstractCompiler { import GraphvizClassCompiler._ val out = new StringLanguageOutputWriter(indent) - val provider = new ClassTypeProvider(topClass) - val translator = getTranslator(provider, RuntimeConfig()) + val provider = new ClassTypeProvider(classSpecs, topClass) + val translator = new RubyTranslator(provider) val links = ListBuffer[(String, String, String)]() val extraClusterLines = new StringLanguageOutputWriter(indent) @@ -86,19 +87,16 @@ class GraphvizClassCompiler(topClass: ClassSpec) extends AbstractCompiler { def compileSeq(className: List[String], curClass: ClassSpec): Unit = { tableStart(className, "seq") - var seqPos: Option[Int] = Some(0) - curClass.seq.foreach { (attr) => + + CalculateSeqSizes.forEachSeqAttr(curClass, (attr, seqPos, _, _) => { attr.id match { case NamedIdentifier(name) => tableRow(className, seqPosToStr(seqPos), attr, name) - - val size = dataTypeBitsSize(attr.dataType) - seqPos = (seqPos, size) match { - case (Some(pos), Some(siz)) => Some(pos + siz) - case _ => None - } + case NumberedIdentifier(n) => + tableRow(className, seqPosToStr(seqPos), attr, s"_${NumberedIdentifier.TEMPLATE}$n") } - } + }) + tableEnd } @@ -254,20 +252,23 @@ class GraphvizClassCompiler(topClass: ClassSpec) extends AbstractCompiler { */ def dataTypeSizeAsString(dataType: DataType, attrName: String): String = { dataType match { - case _: Int1Type => "1" - case IntMultiType(_, width, _) => width.width.toString - case FloatMultiType(width, _) => width.width.toString - case FixedBytesType(contents, _) => contents.length.toString case _: BytesEosType => END_OF_STREAM case blt: BytesLimitType => expressionSize(blt.size, attrName) - case _: BytesTerminatedType => UNKNOWN case StrFromBytesType(basedOn, _) => dataTypeSizeAsString(basedOn, attrName) case utb: UserTypeFromBytes => dataTypeSizeAsString(utb.bytes, attrName) - case UserTypeInstream(_, _) => UNKNOWN case EnumType(_, basedOn) => dataTypeSizeAsString(basedOn, attrName) - case _: SwitchType => UNKNOWN - case BitsType1 => "1b" - case BitsType(width) => s"${width}b" + case _ => + CalculateSeqSizes.dataTypeBitsSize(dataType) match { + case FixedSized(n) => + if (n % 8 == 0) { + s"${n / 8}" + } else { + s"${n}b" + } + case DynamicSized => UNKNOWN + case NotCalculatedSized | StartedCalculationSized => + throw new RuntimeException("Should never happen: problems with CalculateSeqSizes") + } } } @@ -367,14 +368,29 @@ class GraphvizClassCompiler(topClass: ClassSpec) extends AbstractCompiler { s"${getGraphvizNode(className, cs, s)}:${s}_type" def getGraphvizNode(className: List[String], cs: ClassSpec, s: String): String = { - cs.seq.foreach((attr) => - attr.id match { + cs.seq.foreach { (attr) => + val name = attr.id match { case NamedIdentifier(attrName) => - if (attrName == s) { - return s"${type2class(className)}__seq" - } + attrName + case NumberedIdentifier(n) => + s"_${NumberedIdentifier.TEMPLATE}$n" } - ) + if (name == s) { + return s"${type2class(className)}__seq" + } + } + + cs.params.foreach { (attr) => + val name = attr.id match { + case NamedIdentifier(attrName) => + attrName + case NumberedIdentifier(n) => + s"_${NumberedIdentifier.TEMPLATE}$n" + } + if (name == s) { + return s"${type2class(className)}__params" + } + } cs.instances.get(InstanceIdentifier(s)).foreach((inst) => return s"${type2class(className)}__inst__$s" @@ -388,8 +404,6 @@ class GraphvizClassCompiler(topClass: ClassSpec) extends AbstractCompiler { } object GraphvizClassCompiler extends LanguageCompilerStatic { - override def getTranslator(tp: TypeProvider, config: RuntimeConfig): BaseTranslator = new RubyTranslator(tp) - // FIXME: Unused, should be probably separated from LanguageCompilerStatic override def getCompiler( tp: ClassTypeProvider, @@ -399,46 +413,9 @@ object GraphvizClassCompiler extends LanguageCompilerStatic { def type2class(name: List[String]) = name.last def type2display(name: List[String]) = name.map(Utils.upperCamelCase).mkString("::") - /** - * Determines how many bits occupies given data type. - * - * @param dataType data type to analyze - * @return number of bits or None, if it's impossible to determine a priori - */ - def dataTypeBitsSize(dataType: DataType): Option[Int] = { - dataType match { - case BitsType1 => Some(1) - case BitsType(width) => Some(width) - case EnumType(_, basedOn) => dataTypeBitsSize(basedOn) - case _ => dataTypeByteSize(dataType).map((byteSize) => byteSize * 8) - } - } - - /** - * Determines how many bytes occupies a given data type. - * - * @param dataType data type to analyze - * @return number of bytes or None, if it's impossible to determine a priori - */ - def dataTypeByteSize(dataType: DataType): Option[Int] = { - dataType match { - case _: Int1Type => Some(1) - case IntMultiType(_, width, _) => Some(width.width) - case FixedBytesType(contents, _) => Some(contents.length) - case FloatMultiType(width, _) => Some(width.width) - case _: BytesEosType => None - case blt: BytesLimitType => evaluateIntLiteral(blt.size) - case _: BytesTerminatedType => None - case StrFromBytesType(basedOn, _) => dataTypeByteSize(basedOn) - case utb: UserTypeFromBytes => dataTypeByteSize(utb.bytes) - case UserTypeInstream(_, _) => None - case _: SwitchType => None - } - } - def dataTypeName(dataType: DataType): String = { dataType match { - case rt: ReadableType => rt.apiCall + case rt: ReadableType => rt.apiCall(None) // FIXME case ut: UserType => type2display(ut.name) case FixedBytesType(contents, _) => contents.map(_.formatted("%02X")).mkString(" ") case BytesTerminatedType(terminator, include, consume, eosError, _) => @@ -464,20 +441,6 @@ object GraphvizClassCompiler extends LanguageCompilerStatic { } } - /** - * Evaluates the expression, if possible to get the result without introduction - * of any variables or anything. - * - * @param expr expression to evaluate - * @return integer result or None - */ - def evaluateIntLiteral(expr: Ast.expr): Option[Int] = { - expr match { - case Ast.expr.IntNum(x) => Some(x.toInt) - case _ => None - } - } - def htmlEscape(s: String): String = { s.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """) } diff --git a/shared/src/main/scala/io/kaitai/struct/ImportList.scala b/shared/src/main/scala/io/kaitai/struct/ImportList.scala new file mode 100644 index 000000000..b619100d1 --- /dev/null +++ b/shared/src/main/scala/io/kaitai/struct/ImportList.scala @@ -0,0 +1,13 @@ +package io.kaitai.struct + +import scala.collection.mutable.ListBuffer + +/** + * Manages imports/includes/requires/etc lists used for particular compilation + * unit, makes sure they are unique. + */ +class ImportList { + private val list = ListBuffer[String]() + def add(s: String) = Utils.addUniqueAttr(list, s) + def toList: List[String] = list.toList +} diff --git a/shared/src/main/scala/io/kaitai/struct/JSON.scala b/shared/src/main/scala/io/kaitai/struct/JSON.scala index 4e2be6070..adb3fc04c 100644 --- a/shared/src/main/scala/io/kaitai/struct/JSON.scala +++ b/shared/src/main/scala/io/kaitai/struct/JSON.scala @@ -1,13 +1,17 @@ package io.kaitai.struct +import io.kaitai.struct.translators.CommonLiterals + +/** Common trait for all objects that can be serialized as JSON. */ trait Jsonable { + /** Serialize current state of the object into JSON string. */ def toJson: String } /** * Ultra-minimalistic JSON strings generator from arbitrary Scala objects. */ -object JSON { +object JSON extends CommonLiterals { /** * Converts an arbitrary Scala object to JSON string representation. * @param obj object to convert @@ -23,9 +27,8 @@ object JSON { } } - // FIXME: do proper string handling def stringToJson(str: String): String = - "\"%s\"".format(str) + doStringLiteral(str) def listToJson(obj: List[_]): String = "[" + obj.map((x) => stringify(x)).mkString(",") + "]" diff --git a/shared/src/main/scala/io/kaitai/struct/Log.scala b/shared/src/main/scala/io/kaitai/struct/Log.scala index 13ad6689b..1aeb161f6 100644 --- a/shared/src/main/scala/io/kaitai/struct/Log.scala +++ b/shared/src/main/scala/io/kaitai/struct/Log.scala @@ -25,6 +25,8 @@ object Log { "value", "parent", "type_resolve", + "type_valid", + "seq_sizes", "import" ) @@ -32,6 +34,8 @@ object Log { var typeProcValue: Logger = NullLogger var typeProcParent: Logger = NullLogger var typeResolve: Logger = NullLogger + var typeValid: Logger = NullLogger + var seqSizes: Logger = NullLogger var importOps: Logger = NullLogger def initFromVerboseFlag(subsystems: Seq[String]): Unit = { @@ -43,6 +47,8 @@ object Log { case "value" => typeProcValue = ConsoleLogger case "parent" => typeProcParent = ConsoleLogger case "type_resolve" => typeResolve = ConsoleLogger + case "type_valid" => typeValid = ConsoleLogger + case "seq_sizes" => seqSizes = ConsoleLogger case "import" => importOps = ConsoleLogger } } diff --git a/shared/src/main/scala/io/kaitai/struct/Main.scala b/shared/src/main/scala/io/kaitai/struct/Main.scala index c43a0e14c..420775f38 100644 --- a/shared/src/main/scala/io/kaitai/struct/Main.scala +++ b/shared/src/main/scala/io/kaitai/struct/Main.scala @@ -1,6 +1,7 @@ package io.kaitai.struct import io.kaitai.struct.format.{ClassSpec, ClassSpecs, GenericStructClassSpec} +import io.kaitai.struct.languages.GoCompiler import io.kaitai.struct.languages.components.LanguageCompilerStatic import io.kaitai.struct.precompile._ @@ -20,7 +21,7 @@ object Main { * into it and modifying classes itself by precompilation step */ def importAndPrecompile(specs: ClassSpecs, config: RuntimeConfig): Future[Unit] = { - new LoadImports(specs).processClass(specs.firstSpec).map { (allSpecs) => + new LoadImports(specs).processClass(specs.firstSpec, LoadImports.BasePath).map { (allSpecs) => Log.importOps.info(() => s"imports done, got: ${specs.keys} (async=$allSpecs)") specs.foreach { case (_, classSpec) => @@ -31,11 +32,12 @@ object Main { def precompile(classSpecs: ClassSpecs, topClass: ClassSpec, config: RuntimeConfig): Unit = { classSpecs.foreach { case (_, curClass) => MarkupClassNames.markupClassNames(curClass) } - val opaqueTypes = topClass.meta.get.opaqueTypes.getOrElse(config.opaqueTypes) + val opaqueTypes = topClass.meta.opaqueTypes.getOrElse(config.opaqueTypes) new ResolveTypes(classSpecs, opaqueTypes).run() - classSpecs.foreach { case (_, curClass) => ParentTypes.markup(curClass) } + new ParentTypes(classSpecs).run() new SpecsValueTypeDerive(classSpecs).run() - new TypeValidator(topClass).run() + new TypeValidator(classSpecs, topClass).run() + new CalculateSeqSizes(classSpecs).run() topClass.parentClass = GenericStructClassSpec } @@ -43,19 +45,22 @@ object Main { /** * Compiles a single [[ClassSpec]] into a single target language using * provided configuration. + * @param specs bundle of class specifications (used to search to references there) * @param spec class specification to compile * @param lang specifies which language compiler will be used * @param conf runtime compiler configuration * @return a container that contains all compiled files and results */ - def compile(spec: ClassSpec, lang: LanguageCompilerStatic, conf: RuntimeConfig): CompileLog.SpecSuccess = { + def compile(specs: ClassSpecs, spec: ClassSpec, lang: LanguageCompilerStatic, conf: RuntimeConfig): CompileLog.SpecSuccess = { val config = updateConfig(conf, spec) val cc = lang match { case GraphvizClassCompiler => - new GraphvizClassCompiler(spec) + new GraphvizClassCompiler(specs, spec) + case GoCompiler => + new GoClassCompiler(specs, spec, config) case _ => - new ClassCompiler(spec, config, lang) + new ClassCompiler(specs, spec, config, lang) } cc.compile } @@ -68,7 +73,7 @@ object Main { * @return updated runtime configuration with applied enforcements */ private def updateConfig(config: RuntimeConfig, topClass: ClassSpec): RuntimeConfig = { - if (topClass.meta.get.forceDebug) { + if (topClass.meta.forceDebug) { config.copy(debug = true) } else { config diff --git a/shared/src/main/scala/io/kaitai/struct/RuntimeConfig.scala b/shared/src/main/scala/io/kaitai/struct/RuntimeConfig.scala index 5a782c02d..aa7a56abf 100644 --- a/shared/src/main/scala/io/kaitai/struct/RuntimeConfig.scala +++ b/shared/src/main/scala/io/kaitai/struct/RuntimeConfig.scala @@ -3,7 +3,10 @@ package io.kaitai.struct case class RuntimeConfig( debug: Boolean = false, opaqueTypes: Boolean = false, + goPackage: String = "", javaPackage: String = "", + javaFromFileClass: String = "io.kaitai.struct.ByteBufferKaitaiStream", dotNetNamespace: String = "Kaitai", - phpNamespace: String = "" + phpNamespace: String = "", + pythonPackage: String = "" ) diff --git a/shared/src/main/scala/io/kaitai/struct/Utils.scala b/shared/src/main/scala/io/kaitai/struct/Utils.scala index eab0ca8f5..a14816f37 100644 --- a/shared/src/main/scala/io/kaitai/struct/Utils.scala +++ b/shared/src/main/scala/io/kaitai/struct/Utils.scala @@ -64,6 +64,18 @@ object Utils { } } + /** + * Joins collection together to make a single string. Makes extra exception for empty + * collections (not like [[TraversableOnce]] `mkString`). + * @param start the starting string. + * @param sep the separator string. + * @param end the ending string. + * @return If the collection is empty, returns empty string, otherwise returns `start`, + * then elements of the collection, every pair separated with `sep`, then `end`. + */ + def join[T](coll: TraversableOnce[T], start: String, sep: String, end: String): String = + if (coll.isEmpty) "" else coll.mkString(start, sep, end) + /** * Converts byte array (Seq[Byte]) into hex-escaped C-style literal characters * (i.e. like \xFF). diff --git a/shared/src/main/scala/io/kaitai/struct/datatype/DataType.scala b/shared/src/main/scala/io/kaitai/struct/datatype/DataType.scala index 85b2dd85d..78e111043 100644 --- a/shared/src/main/scala/io/kaitai/struct/datatype/DataType.scala +++ b/shared/src/main/scala/io/kaitai/struct/datatype/DataType.scala @@ -1,7 +1,8 @@ package io.kaitai.struct.datatype -import io.kaitai.struct.exprlang.Ast +import io.kaitai.struct.exprlang.{Ast, Expressions} import io.kaitai.struct.format._ +import io.kaitai.struct.translators.TypeDetector sealed trait DataType @@ -21,7 +22,7 @@ object DataType { * parameterless KaitaiStream API call. */ trait ReadableType extends DataType { - def apiCall: String + def apiCall(defEndian: Option[FixedEndian]): String } abstract class NumericType extends DataType @@ -30,12 +31,13 @@ object DataType { abstract class IntType extends NumericType case object CalcIntType extends IntType case class Int1Type(signed: Boolean) extends IntType with ReadableType { - override def apiCall: String = if (signed) "s1" else "u1" + override def apiCall(defEndian: Option[FixedEndian]): String = if (signed) "s1" else "u1" } - case class IntMultiType(signed: Boolean, width: IntWidth, endian: Endianness) extends IntType with ReadableType { - override def apiCall: String = { + case class IntMultiType(signed: Boolean, width: IntWidth, endian: Option[FixedEndian]) extends IntType with ReadableType { + override def apiCall(defEndian: Option[FixedEndian]): String = { val ch1 = if (signed) 's' else 'u' - s"$ch1${width.width}${endian.toString}" + val finalEnd = endian.orElse(defEndian) + s"$ch1${width.width}${finalEnd.map(_.toSuffix).getOrElse("")}" } } case object BitsType1 extends BooleanType @@ -43,9 +45,10 @@ object DataType { abstract class FloatType extends NumericType case object CalcFloatType extends FloatType - case class FloatMultiType(width: IntWidth, endian: Endianness) extends FloatType with ReadableType { - override def apiCall: String = { - s"f${width.width}${endian.toString}" + case class FloatMultiType(width: IntWidth, endian: Option[FixedEndian]) extends FloatType with ReadableType { + override def apiCall(defEndian: Option[FixedEndian]): String = { + val finalEnd = endian.orElse(defEndian) + s"f${width.width}${finalEnd.map(_.toSuffix).getOrElse("")}" } } @@ -86,23 +89,29 @@ object DataType { case object CalcBooleanType extends BooleanType case class ArrayType(elType: DataType) extends DataType - abstract class UserType(val name: List[String], val forcedParent: Option[Ast.expr]) extends DataType { + abstract class UserType( + val name: List[String], + val forcedParent: Option[Ast.expr], + var args: Seq[Ast.expr] + ) extends DataType { var classSpec: Option[ClassSpec] = None def isOpaque = { val cs = classSpec.get - cs.isTopLevel || (cs.meta match { - case None => false - case Some(meta) => meta.isOpaque - }) + cs.isTopLevel || cs.meta.isOpaque } } - case class UserTypeInstream(_name: List[String], _forcedParent: Option[Ast.expr]) extends UserType(_name, _forcedParent) + case class UserTypeInstream( + _name: List[String], + _forcedParent: Option[Ast.expr], + _args: Seq[Ast.expr] = Seq() + ) extends UserType(_name, _forcedParent, _args) case class UserTypeFromBytes( _name: List[String], _forcedParent: Option[Ast.expr], + _args: Seq[Ast.expr] = Seq(), bytes: BytesType, override val process: Option[ProcessExpr] - ) extends UserType(_name, _forcedParent) with Processing + ) extends UserType(_name, _forcedParent, _args) with Processing val USER_TYPE_NO_PARENT = Ast.expr.Bool(false) @@ -114,23 +123,114 @@ object DataType { var enumSpec: Option[EnumSpec] = None } - case class SwitchType(on: Ast.expr, cases: Map[Ast.expr, DataType]) extends DataType + case class SwitchType(on: Ast.expr, cases: Map[Ast.expr, DataType]) extends DataType { + def combinedType: DataType = TypeDetector.combineTypes(cases.values) + + /** + * @return True if this switch type includes an "else" case + */ + def hasElseCase: Boolean = cases.contains(SwitchType.ELSE_CONST) + + /** + * If a switch type has no else statement, it will turn out to be null + * every case would fail, so it's nullable. + * @return True if this switch type is nullable for regular languages. + */ + def isNullable: Boolean = !hasElseCase + + /** + * @return True if this switch type is nullable in a raw switch bytes languages (C++). + */ + def isNullableSwitchRaw: Boolean = { + val elseCase = cases.get(SwitchType.ELSE_CONST) + elseCase match { + case Some(_: BytesType) => + // else case with bytes type, nullable for C++-like languages + true + case Some(x) => + // else case with any user type, non-nullable + false + case None => + // no else case, even raw bytes, definitely nullable + true + } + } + + def hasSize: Boolean = + cases.values.exists((t) => + t.isInstanceOf[UserTypeFromBytes] || t.isInstanceOf[BytesType] + ) + } object SwitchType { /** * Constant that would be used for "else" case in SwitchType case class "cases" map. */ val ELSE_CONST = Ast.expr.Name(Ast.identifier("_")) + + val LEGAL_KEYS_SWITCH = Set( + "switch-on", + "cases" + ) + + def fromYaml1(switchSpec: Map[String, Any], path: List[String]): (String, Map[String, String]) = { + val _on = ParseUtils.getValueStr(switchSpec, "switch-on", path) + val _cases: Map[String, String] = switchSpec.get("cases") match { + case None => Map() + case Some(x) => ParseUtils.asMapStrStr(x, path ++ List("cases")) + } + + ParseUtils.ensureLegalKeys(switchSpec, LEGAL_KEYS_SWITCH, path) + (_on, _cases) + } + + def fromYaml( + switchSpec: Map[String, Any], + path: List[String], + metaDef: MetaSpec, + arg: YamlAttrArgs + ): SwitchType = { + val (_on, _cases) = fromYaml1(switchSpec, path) + + val on = Expressions.parse(_on) + val cases: Map[Ast.expr, DataType] = _cases.map { case (condition, typeName) => + Expressions.parse(condition) -> DataType.fromYaml( + Some(typeName), path ++ List("cases"), metaDef, + arg + ) + } + + // If we have size defined, and we don't have any "else" case already, add + // an implicit "else" case that will at least catch everything else as + // "untyped" byte array of given size + val addCases: Map[Ast.expr, DataType] = if (cases.contains(ELSE_CONST)) { + Map() + } else { + (arg.size, arg.sizeEos) match { + case (Some(sizeValue), false) => + Map(SwitchType.ELSE_CONST -> BytesLimitType(sizeValue, None, false, None, arg.process)) + case (None, true) => + Map(SwitchType.ELSE_CONST -> BytesEosType(None, false, None, arg.process)) + case (None, false) => + Map() + case (Some(_), true) => + throw new YAMLParseException("can't have both `size` and `size-eos` defined", path) + } + } + + SwitchType(on, cases ++ addCases) + } } private val ReIntType = """([us])(2|4|8)(le|be)?""".r private val ReFloatType = """f(4|8)(le|be)?""".r - private val ReBitType= """b(\d+)""".r + private val ReBitType = """b(\d+)""".r + private val ReUserTypeWithArgs = """(.+)\((.*)\)""".r def fromYaml( dto: Option[String], path: List[String], - metaDef: MetaDefaults, + metaDef: MetaSpec, arg: YamlAttrArgs ): DataType = { val r = dto match { @@ -185,14 +285,18 @@ object DataType { val bat = arg2.getByteArrayType(path) StrFromBytesType(bat, enc) case _ => - val dtl = classNameToList(dt) + val (arglessType, args) = dt match { + case ReUserTypeWithArgs(typeStr, argsStr) => (typeStr, Expressions.parseList(argsStr)) + case _ => (dt, List()) + } + val dtl = classNameToList(arglessType) if (arg.size.isEmpty && !arg.sizeEos && arg.terminator.isEmpty) { if (arg.process.isDefined) throw new YAMLParseException(s"user type '$dt': need 'size' / 'size-eos' / 'terminator' if 'process' is used", path) - UserTypeInstream(dtl, arg.parent) + UserTypeInstream(dtl, arg.parent, args) } else { val bat = arg.getByteArrayType(path) - UserTypeFromBytes(dtl, arg.parent, bat, arg.process) + UserTypeFromBytes(dtl, arg.parent, args, bat, arg.process) } } } @@ -209,7 +313,46 @@ object DataType { } } - def getEncoding(curEncoding: Option[String], metaDef: MetaDefaults, path: List[String]): String = { + private val RePureIntType = """([us])(2|4|8)""".r + private val RePureFloatType = """f(4|8)""".r + + def pureFromString(dto: Option[String]): DataType = { + dto match { + case None => CalcBytesType + case Some(dt) => dt match { + case "u1" => Int1Type(false) + case "s1" => Int1Type(true) + case RePureIntType(signStr, widthStr) => + IntMultiType( + signStr match { + case "s" => true + case "u" => false + }, + widthStr match { + case "2" => Width2 + case "4" => Width4 + case "8" => Width8 + }, + None + ) + case RePureFloatType(widthStr) => + FloatMultiType( + widthStr match { + case "4" => Width4 + case "8" => Width8 + }, + None + ) + case "str" => CalcStrType + case "bool" => CalcBooleanType + case "struct" => KaitaiStructType + case "io" => KaitaiStreamType + case "any" => AnyType + } + } + } + + def getEncoding(curEncoding: Option[String], metaDef: MetaSpec, path: List[String]): String = { curEncoding.orElse(metaDef.encoding) match { case Some(enc) => enc case None => @@ -224,4 +367,4 @@ object DataType { * @return class name notation as list of components */ def classNameToList(s: String): List[String] = s.split("::", -1).toList -} \ No newline at end of file +} diff --git a/shared/src/main/scala/io/kaitai/struct/datatype/Endianness.scala b/shared/src/main/scala/io/kaitai/struct/datatype/Endianness.scala index b75150e1f..9db938c3b 100644 --- a/shared/src/main/scala/io/kaitai/struct/datatype/Endianness.scala +++ b/shared/src/main/scala/io/kaitai/struct/datatype/Endianness.scala @@ -1,32 +1,67 @@ package io.kaitai.struct.datatype -import io.kaitai.struct.format.YAMLParseException +import io.kaitai.struct.datatype.DataType.SwitchType +import io.kaitai.struct.exprlang.{Ast, Expressions} +import io.kaitai.struct.format.{ParseUtils, YAMLParseException} sealed trait Endianness -case object LittleEndian extends Endianness { - override def toString = "le" + +abstract class FixedEndian extends Endianness { + def toSuffix: String +} +case object LittleEndian extends FixedEndian { + override def toSuffix = "le" } -case object BigEndian extends Endianness { - override def toString = "be" +case object BigEndian extends FixedEndian { + override def toSuffix = "be" } +case class CalcEndian(on: Ast.expr, cases: Map[Ast.expr, FixedEndian]) extends Endianness + +case object InheritedEndian extends Endianness + object Endianness { - def defaultFromString(s: Option[String], path: List[String]) = s match { - case None => None - case Some("be") => Some(BigEndian) - case Some("le") => Some(LittleEndian) - case Some(unknown) => throw YAMLParseException.badDictValue( - Set("be", "le"), unknown, path ++ List("endian") - ) + def fromYaml(src: Option[Any], path: List[String]): Option[Endianness] = { + src match { + case None => None + case Some("be") => Some(BigEndian) + case Some("le") => Some(LittleEndian) + case Some(srcMap: Map[Any, Any]) => + val endianMap = ParseUtils.asMapStr(srcMap, path) + Some(fromMap(endianMap, path)) + case _ => + throw new YAMLParseException( + s"unable to parse endianness: `le`, `be` or calculated endianness map is expected", + path ++ List("endian") + ) + } } - def fromString(s: Option[String], defaultEndian: Option[Endianness], dt: String, path: List[String]) = s match { - case Some("le") => LittleEndian - case Some("be") => BigEndian + def fromMap(srcMap: Map[String, Any], path: List[String]): CalcEndian = { + val (_on, _cases) = SwitchType.fromYaml1(srcMap, path) + + val on = Expressions.parse(_on) + val cases: Map[Ast.expr, FixedEndian] = _cases.map { case (condition, endStr) => + Expressions.parse(condition) -> (endStr match { + case "be" => BigEndian + case "le" => LittleEndian + case _ => + throw YAMLParseException.badDictValue(Set("be", "le"), endStr, path ++ List("cases", condition)) + }) + } + + CalcEndian(on, cases) + } + + def fromString(s: Option[String], defaultEndian: Option[Endianness], dt: String, path: List[String]): Option[FixedEndian] = s match { + case Some("le") => Some(LittleEndian) + case Some("be") => Some(BigEndian) case None => defaultEndian match { - case Some(e) => e - case None => throw new YAMLParseException(s"unable to use type '$dt' without default endianness", path ++ List("type")) + case Some(e: FixedEndian) => Some(e) + case Some(_: CalcEndian) | Some(InheritedEndian) => None // to be overridden during compile + case None => + throw new YAMLParseException(s"unable to use type '$dt' without default endianness", path ++ List("type")) } } } diff --git a/shared/src/main/scala/io/kaitai/struct/exprlang/Expressions.scala b/shared/src/main/scala/io/kaitai/struct/exprlang/Expressions.scala index f0533ad29..1e0d6f1ea 100644 --- a/shared/src/main/scala/io/kaitai/struct/exprlang/Expressions.scala +++ b/shared/src/main/scala/io/kaitai/struct/exprlang/Expressions.scala @@ -162,13 +162,18 @@ object Expressions { val topExpr: P[Ast.expr] = P( test ~ End ) + val topExprList: P[Seq[Ast.expr]] = P(testlist1 ~ End) + class ParseException(val src: String, val failure: Parsed.Failure) extends RuntimeException(failure.msg) - def parse(src: String): Ast.expr = { - val r = Expressions.topExpr.parse(src) + def parse(src: String): Ast.expr = realParse(src, topExpr) + def parseList(src: String): Seq[Ast.expr] = realParse(src, topExprList) + + private def realParse[T](src: String, parser: P[T]): T = { + val r = parser.parse(src) r match { - case Parsed.Success(value, index) => value + case Parsed.Success(value, _) => value case f: Parsed.Failure => throw new ParseException(src, f) } diff --git a/shared/src/main/scala/io/kaitai/struct/exprlang/Lexical.scala b/shared/src/main/scala/io/kaitai/struct/exprlang/Lexical.scala index b85d17a18..b04b07e25 100644 --- a/shared/src/main/scala/io/kaitai/struct/exprlang/Lexical.scala +++ b/shared/src/main/scala/io/kaitai/struct/exprlang/Lexical.scala @@ -80,9 +80,9 @@ object Lexical { val bindigit: P0 = P( "0" | "1" | "_" ) val hexdigit: P0 = P( digit | CharIn('a' to 'f', 'A' to 'F') | "_" ) - val floatnumber: P[BigDecimal] = P( pointfloat | exponentfloat ) + val floatnumber: P[BigDecimal] = P( exponentfloat | pointfloat ) val pointfloat: P[BigDecimal] = P( intpart.? ~ fraction | intpart ~ "." ).!.map(BigDecimal(_)) - val exponentfloat: P[BigDecimal] = P( (intpart | pointfloat) ~ exponent ).!.map(BigDecimal(_)) + val exponentfloat: P[BigDecimal] = P( (pointfloat | intpart) ~ exponent ).!.map(BigDecimal(_)) val intpart: P[BigDecimal] = P( digit.rep(1) ).!.map(BigDecimal(_)) val fraction: P0 = P( "." ~ digit.rep(1) ) val exponent: P0 = P( ("e" | "E") ~ ("+" | "-").? ~ digit.rep(1) ) diff --git a/shared/src/main/scala/io/kaitai/struct/format/AttrSpec.scala b/shared/src/main/scala/io/kaitai/struct/format/AttrSpec.scala index 2f1558327..444d81578 100644 --- a/shared/src/main/scala/io/kaitai/struct/format/AttrSpec.scala +++ b/shared/src/main/scala/io/kaitai/struct/format/AttrSpec.scala @@ -18,20 +18,53 @@ case object NoRepeat extends RepeatSpec case class ConditionalSpec(ifExpr: Option[Ast.expr], repeat: RepeatSpec) -trait AttrLikeSpec extends YAMLPath { +trait AttrLikeSpec extends MemberSpec { def dataType: DataType def cond: ConditionalSpec def doc: DocSpec def isArray: Boolean = cond.repeat != NoRepeat - def dataTypeComposite: DataType = { + override def dataTypeComposite: DataType = { if (isArray) { ArrayType(dataType) } else { dataType } } + + override def isNullable: Boolean = { + if (cond.ifExpr.isDefined) { + true + } else { + dataType match { + case st: SwitchType => + st.isNullable + case _ => + false + } + } + } + + def isNullableSwitchRaw: Boolean = { + if (cond.ifExpr.isDefined) { + true + } else { + dataType match { + case st: SwitchType => + st.isNullableSwitchRaw + case _ => + false + } + } + } + + /** + * Determines if this attribute is to be parsed lazily (i.e. on first use), + * or eagerly (during object construction, usually in a `_read` method) + * @return True if this attribute is lazy, false if it's eager + */ + def isLazy: Boolean } case class AttrSpec( @@ -40,7 +73,9 @@ case class AttrSpec( dataType: DataType, cond: ConditionalSpec = ConditionalSpec(None, NoRepeat), doc: DocSpec = DocSpec.EMPTY -) extends AttrLikeSpec +) extends AttrLikeSpec with MemberSpec { + override def isLazy = false +} case class YamlAttrArgs( size: Option[Ast.expr], @@ -93,6 +128,7 @@ object AttrSpec { "contents", "size", "size-eos", + "pad-right", "parent", "process" ) @@ -108,7 +144,7 @@ object AttrSpec { "enum" ) - def fromYaml(src: Any, path: List[String], metaDef: MetaDefaults, idx: Int): AttrSpec = { + def fromYaml(src: Any, path: List[String], metaDef: MetaSpec, idx: Int): AttrSpec = { val srcMap = ParseUtils.asMapStr(src, path) val id = ParseUtils.getOptValueStr(srcMap, "id", path) match { case Some(idStr) => @@ -123,7 +159,7 @@ object AttrSpec { fromYaml(srcMap, path, metaDef, id) } - def fromYaml(srcMap: Map[String, Any], path: List[String], metaDef: MetaDefaults, id: Identifier): AttrSpec = { + def fromYaml(srcMap: Map[String, Any], path: List[String], metaDef: MetaSpec, id: Identifier): AttrSpec = { try { fromYaml2(srcMap, path, metaDef, id) } catch { @@ -132,9 +168,9 @@ object AttrSpec { } } - def fromYaml2(srcMap: Map[String, Any], path: List[String], metaDef: MetaDefaults, id: Identifier): AttrSpec = { + def fromYaml2(srcMap: Map[String, Any], path: List[String], metaDef: MetaSpec, id: Identifier): AttrSpec = { val doc = DocSpec.fromYaml(srcMap, path) - val process = ProcessExpr.fromStr(ParseUtils.getOptValueStr(srcMap, "process", path)) + val process = ProcessExpr.fromStr(ParseUtils.getOptValueStr(srcMap, "process", path), path) // TODO: add proper path propagation val contents = srcMap.get("contents").map(parseContentSpec(_, path ++ List("contents"))) val size = ParseUtils.getOptValueStr(srcMap, "size", path).map(Expressions.parse) @@ -226,23 +262,33 @@ object AttrSpec { private def parseSwitch( switchSpec: Map[String, Any], path: List[String], - metaDef: MetaDefaults, + metaDef: MetaSpec, arg: YamlAttrArgs ): DataType = { val _on = ParseUtils.getValueStr(switchSpec, "switch-on", path) - val _cases: Map[String, String] = switchSpec.get("cases") match { - case None => Map() - case Some(x) => ParseUtils.asMapStrStr(x, path ++ List("cases")) - } + val _cases = ParseUtils.getValueMapStrStr(switchSpec, "cases", path) ParseUtils.ensureLegalKeys(switchSpec, LEGAL_KEYS_SWITCH, path) - val on = Expressions.parse(_on) + val on = try { + Expressions.parse(_on) + } catch { + case epe: Expressions.ParseException => + throw YAMLParseException.expression(epe, path ++ List("switch-on")) + } + val cases = _cases.map { case (condition, typeName) => - Expressions.parse(condition) -> DataType.fromYaml( - Some(typeName), path ++ List("cases"), metaDef, + val casePath = path ++ List("cases", condition) + val condType = DataType.fromYaml( + Some(typeName), casePath, metaDef, arg ) + try { + Expressions.parse(condition) -> condType + } catch { + case epe: Expressions.ParseException => + throw YAMLParseException.expression(epe, casePath) + } } // If we have size defined, and we don't have any "else" case already, add diff --git a/shared/src/main/scala/io/kaitai/struct/format/ClassSpec.scala b/shared/src/main/scala/io/kaitai/struct/format/ClassSpec.scala index 68ab182aa..2a6a34dab 100644 --- a/shared/src/main/scala/io/kaitai/struct/format/ClassSpec.scala +++ b/shared/src/main/scala/io/kaitai/struct/format/ClassSpec.scala @@ -1,5 +1,9 @@ package io.kaitai.struct.format +import io.kaitai.struct.datatype.DataType +import io.kaitai.struct.datatype.DataType.{KaitaiStructType, UserTypeInstream} +import scala.collection.mutable + /** * Type that we use when we want to refer to a class specification or something * close, but not (yet) that well defined. @@ -8,11 +12,18 @@ sealed trait ClassSpecLike case object UnknownClassSpec extends ClassSpecLike case object GenericStructClassSpec extends ClassSpecLike +sealed trait Sized +case object DynamicSized extends Sized +case object NotCalculatedSized extends Sized +case object StartedCalculationSized extends Sized +case class FixedSized(n: Int) extends Sized + case class ClassSpec( path: List[String], isTopLevel: Boolean, - meta: Option[MetaSpec], + meta: MetaSpec, doc: DocSpec, + params: List[ParamDefSpec], seq: List[AttrSpec], types: Map[String, ClassSpec], instances: Map[InstanceIdentifier, InstanceSpec], @@ -39,9 +50,22 @@ case class ClassSpec( */ var upClass: Option[ClassSpec] = None - def parentTypeName: List[String] = parentClass match { - case UnknownClassSpec | GenericStructClassSpec => List("kaitai_struct") - case t: ClassSpec => t.name + var seqSize: Sized = NotCalculatedSized + + def parentType: DataType = parentClass match { + case UnknownClassSpec | GenericStructClassSpec => KaitaiStructType + case t: ClassSpec => UserTypeInstream(t.name, None) + } + + /** + * Recursively traverses tree of types starting from this type, calling + * certain function for every type, starting from this one. + */ + def forEachRec(proc: (ClassSpec) => Unit): Unit = { + proc.apply(this) + types.foreach { case (_, typeSpec) => + typeSpec.forEachRec(proc) + } } } @@ -50,31 +74,37 @@ object ClassSpec { "meta", "doc", "doc-ref", + "params", "seq", "types", "instances", "enums" ) - def fromYaml(src: Any, path: List[String], metaDef: MetaDefaults): ClassSpec = { + def fromYaml(src: Any, path: List[String], metaDef: MetaSpec): ClassSpec = { val srcMap = ParseUtils.asMapStr(src, path) ParseUtils.ensureLegalKeys(srcMap, LEGAL_KEYS, path) - val meta = srcMap.get("meta").map(MetaSpec.fromYaml(_, path ++ List("meta"))) - val curMetaDef = metaDef.updateWith(meta) + val metaPath = path ++ List("meta") + val explicitMeta = srcMap.get("meta").map(MetaSpec.fromYaml(_, metaPath)).getOrElse(MetaSpec.emptyWithPath(metaPath)) + val meta = explicitMeta.fillInDefaults(metaDef) val doc = DocSpec.fromYaml(srcMap, path) + val params: List[ParamDefSpec] = srcMap.get("params") match { + case Some(value) => paramDefFromYaml(value, path ++ List("params")) + case None => List() + } val seq: List[AttrSpec] = srcMap.get("seq") match { - case Some(value) => seqFromYaml(value, path ++ List("seq"), curMetaDef) + case Some(value) => seqFromYaml(value, path ++ List("seq"), meta) case None => List() } val types: Map[String, ClassSpec] = srcMap.get("types") match { - case Some(value) => typesFromYaml(value, path ++ List("types"), curMetaDef) + case Some(value) => typesFromYaml(value, path ++ List("types"), meta) case None => Map() } val instances: Map[InstanceIdentifier, InstanceSpec] = srcMap.get("instances") match { - case Some(value) => instancesFromYaml(value, path ++ List("instances"), curMetaDef) + case Some(value) => instancesFromYaml(value, path ++ List("instances"), meta) case None => Map() } val enums: Map[String, EnumSpec] = srcMap.get("enums") match { @@ -82,13 +112,17 @@ object ClassSpec { case None => Map() } - val cs = ClassSpec(path, path.isEmpty, meta, doc, seq, types, instances, enums) + checkDupSeqInstIds(seq, instances) + + val cs = ClassSpec( + path, path.isEmpty, + meta, doc, + params, seq, types, instances, enums + ) // If that's a top-level class, set its name from meta/id if (path.isEmpty) { - if (meta.isEmpty) - throw new YAMLParseException("no `meta` encountered in top-level class spec", path) - meta.get.id match { + explicitMeta.id match { case None => throw new YAMLParseException("no `meta/id` encountered in top-level class spec", path ++ List("meta", "id")) case Some(id) => @@ -99,18 +133,68 @@ object ClassSpec { cs } - def seqFromYaml(src: Any, path: List[String], metaDef: MetaDefaults): List[AttrSpec] = { + def paramDefFromYaml(src: Any, path: List[String]): List[ParamDefSpec] = { src match { case srcList: List[Any] => - srcList.zipWithIndex.map { case (attrSrc, idx) => + val params = srcList.zipWithIndex.map { case (attrSrc, idx) => + ParamDefSpec.fromYaml(attrSrc, path ++ List(idx.toString), idx) + } + // FIXME: checkDupSeqIds(params) + params + case unknown => + throw new YAMLParseException(s"expected array, found $unknown", path) + } + } + + def seqFromYaml(src: Any, path: List[String], metaDef: MetaSpec): List[AttrSpec] = { + src match { + case srcList: List[Any] => + val seq = srcList.zipWithIndex.map { case (attrSrc, idx) => AttrSpec.fromYaml(attrSrc, path ++ List(idx.toString), metaDef, idx) } + checkDupSeqIds(seq) + seq case unknown => throw new YAMLParseException(s"expected array, found $unknown", path) } } - def typesFromYaml(src: Any, path: List[String], metaDef: MetaDefaults): Map[String, ClassSpec] = { + def checkDupSeqIds(seq: List[AttrSpec]): Unit = { + val attrIds = mutable.Map[String, AttrSpec]() + seq.foreach { (attr) => + attr.id match { + case NamedIdentifier(id) => + checkDupId(attrIds.get(id), id, attr) + attrIds.put(id, attr) + case _ => // do nothing with non-named IDs + } + } + } + + def checkDupSeqInstIds(seq: List[AttrSpec], instances: Map[InstanceIdentifier, InstanceSpec]): Unit = { + val attrIds: Map[String, AttrSpec] = seq.flatMap((attr) => attr.id match { + case NamedIdentifier(id) => Some(id -> attr) + case _ => None + }).toMap + + instances.foreach { case (id, instSpec) => + checkDupId(attrIds.get(id.name), id.name, instSpec) + } + } + + private def checkDupId(prevAttrOpt: Option[AttrSpec], id: String, nowAttr: YAMLPath) { + prevAttrOpt match { + case Some(prevAttr) => + throw new YAMLParseException( + s"duplicate attribute ID '$id', previously defined at /${prevAttr.pathStr}", + nowAttr.path + ) + case None => + // no dups, ok + } + } + + def typesFromYaml(src: Any, path: List[String], metaDef: MetaSpec): Map[String, ClassSpec] = { val srcMap = ParseUtils.asMapStr(src, path) srcMap.map { case (typeName, body) => Identifier.checkIdentifierSource(typeName, "type", path ++ List(typeName)) @@ -118,7 +202,7 @@ object ClassSpec { } } - def instancesFromYaml(src: Any, path: List[String], metaDef: MetaDefaults): Map[InstanceIdentifier, InstanceSpec] = { + def instancesFromYaml(src: Any, path: List[String], metaDef: MetaSpec): Map[InstanceIdentifier, InstanceSpec] = { val srcMap = ParseUtils.asMap(src, path) srcMap.map { case (key, body) => val instName = ParseUtils.asStr(key, path) @@ -137,14 +221,15 @@ object ClassSpec { } } - def fromYaml(src: Any): ClassSpec = fromYaml(src, List(), MetaDefaults(None, None)) + def fromYaml(src: Any): ClassSpec = fromYaml(src, List(), MetaSpec.OPAQUE) def opaquePlaceholder(typeName: List[String]): ClassSpec = { val placeholder = ClassSpec( List(), true, - meta = Some(MetaSpec.OPAQUE), + meta = MetaSpec.OPAQUE, doc = DocSpec.EMPTY, + params = List(), seq = List(), types = Map(), instances = Map(), diff --git a/shared/src/main/scala/io/kaitai/struct/format/ClassSpecs.scala b/shared/src/main/scala/io/kaitai/struct/format/ClassSpecs.scala index 24837174d..664ce5297 100644 --- a/shared/src/main/scala/io/kaitai/struct/format/ClassSpecs.scala +++ b/shared/src/main/scala/io/kaitai/struct/format/ClassSpecs.scala @@ -1,5 +1,7 @@ package io.kaitai.struct.format +import io.kaitai.struct.precompile.ErrorInInput + import scala.collection.mutable import scala.concurrent.Future @@ -12,6 +14,29 @@ import scala.concurrent.Future abstract class ClassSpecs(val firstSpec: ClassSpec) extends mutable.HashMap[String, ClassSpec] { this(firstSpec.name.head) = firstSpec + /** + * Calls certain function on all [[ClassSpec]] elements stored in this ClassSpecs, + * and all subtypes stored in these elements, recursively. + */ + def forEachRec(proc: (ClassSpec) => Unit): Unit = + forEachTopLevel((_, typeSpec) => typeSpec.forEachRec(proc)) + + /** + * Calls certain function on all top-level [[ClassSpec]] elements stored in this + * ClassSpecs. + */ + def forEachTopLevel(proc: (String, ClassSpec) => Unit): Unit = { + foreach { case (specName, typeSpec) => + try { + proc(specName, typeSpec) + } catch { + case ErrorInInput(err, path, None) => + // Try to emit more specific error, with a reference to current file + throw ErrorInInput(err, path, Some(specName)) + } + } + } + def importRelative(name: String, path: List[String], inFile: Option[String]): Future[Option[ClassSpec]] def importAbsolute(name: String, path: List[String], inFile: Option[String]): Future[Option[ClassSpec]] } diff --git a/shared/src/main/scala/io/kaitai/struct/format/EnumSpec.scala b/shared/src/main/scala/io/kaitai/struct/format/EnumSpec.scala index f7db4f515..8f10e19f2 100644 --- a/shared/src/main/scala/io/kaitai/struct/format/EnumSpec.scala +++ b/shared/src/main/scala/io/kaitai/struct/format/EnumSpec.scala @@ -1,25 +1,23 @@ package io.kaitai.struct.format -case class EnumSpec(map: Map[Long, String]) { +case class EnumSpec(map: Map[Long, EnumValueSpec]) { var name = List[String]() /** * Stabilize order of generated enums by sorting it by integer ID - it * both looks nicer and doesn't screw diffs in generated code. */ - lazy val sortedSeq: Seq[(Long, String)] = map.toSeq.sortBy(_._1) + lazy val sortedSeq: Seq[(Long, EnumValueSpec)] = map.toSeq.sortBy(_._1) } object EnumSpec { def fromYaml(src: Any, path: List[String]): EnumSpec = { val srcMap = ParseUtils.asMap(src, path) - EnumSpec(srcMap.map { case (id, name) => + EnumSpec(srcMap.map { case (id, desc) => val idLong = ParseUtils.asLong(id, path) - val symbName = ParseUtils.asStr(name, path ++ List(idLong.toString)) + val value = EnumValueSpec.fromYaml(desc, path ++ List(idLong.toString)) - Identifier.checkIdentifierSource(symbName, "enum member", path ++ List(idLong.toString)) - - idLong -> symbName + idLong -> value }) } } diff --git a/shared/src/main/scala/io/kaitai/struct/format/EnumValueSpec.scala b/shared/src/main/scala/io/kaitai/struct/format/EnumValueSpec.scala new file mode 100644 index 000000000..905e968a6 --- /dev/null +++ b/shared/src/main/scala/io/kaitai/struct/format/EnumValueSpec.scala @@ -0,0 +1,40 @@ +package io.kaitai.struct.format + +case class EnumValueSpec(name: String, doc: DocSpec) + +object EnumValueSpec { + def fromYaml(src: Any, path: List[String]): EnumValueSpec = { + src match { + case name: String => + fromSimpleName(name, path) + case x: Boolean => + fromSimpleName(x.toString, path) + case srcMap: Map[Any, Any] => + fromMap(ParseUtils.anyMapToStrMap(srcMap, path), path) + case _ => + throw YAMLParseException.badType("string or map", src, path) + } + } + + def fromSimpleName(name: String, path: List[String]): EnumValueSpec = { + Identifier.checkIdentifierSource(name, "enum member", path) + EnumValueSpec(name, DocSpec.EMPTY) + } + + val LEGAL_KEYS = Set( + "id", + "doc", + "doc-ref" + ) + + def fromMap(srcMap: Map[String, Any], path: List[String]): EnumValueSpec = { + ParseUtils.ensureLegalKeys(srcMap, LEGAL_KEYS, path, Some("enum value spec")) + + val name = ParseUtils.getValueStr(srcMap, "id", path) + Identifier.checkIdentifierSource(name, "enum value spec id", path) + + val doc = DocSpec.fromYaml(srcMap, path) + + EnumValueSpec(name, doc) + } +} diff --git a/shared/src/main/scala/io/kaitai/struct/format/Identifier.scala b/shared/src/main/scala/io/kaitai/struct/format/Identifier.scala index 01d6fbaa6..e3ae3c429 100644 --- a/shared/src/main/scala/io/kaitai/struct/format/Identifier.scala +++ b/shared/src/main/scala/io/kaitai/struct/format/Identifier.scala @@ -3,13 +3,22 @@ package io.kaitai.struct.format /** * Common abstract container for all identifiers that Kaitai Struct deals with. */ -abstract class Identifier +abstract class Identifier { + /** + * @return Human-readable name of identifier, to be used exclusively for ksc + * error messaging purposes. + */ + def humanReadable: String +} /** * Identifier generated automatically for seq attributes which lack true string "id" field. * @param idx unique number to identify attribute with */ -case class NumberedIdentifier(idx: Int) extends Identifier +case class NumberedIdentifier(idx: Int) extends Identifier { + import NumberedIdentifier._ + override def humanReadable: String = s"${TEMPLATE}_$idx" +} object NumberedIdentifier { val TEMPLATE = "unnamed" @@ -21,9 +30,13 @@ object NumberedIdentifier { */ case class NamedIdentifier(name: String) extends Identifier { Identifier.checkIdentifier(name) + + override def humanReadable: String = name } -case class InvalidIdentifier(id: String) extends RuntimeException +case class InvalidIdentifier(id: String) extends RuntimeException( + s"invalid ID: '$id', expected /${Identifier.ReIdentifier.toString}/" +) object Identifier { val ReIdentifier = "^[a-z][a-z0-9_]*$".r @@ -60,18 +73,30 @@ object Identifier { val IO = "_io" val ITERATOR = "_" val ITERATOR2 = "_buf" + val INDEX = "_index" + val SWITCH_ON = "_on" + val IS_LE = "_is_le" } -case class RawIdentifier(innerId: Identifier) extends Identifier +case class RawIdentifier(innerId: Identifier) extends Identifier { + override def humanReadable: String = s"raw(${innerId.humanReadable})" +} -case class IoStorageIdentifier(innerId: Identifier) extends Identifier +case class IoStorageIdentifier(innerId: Identifier) extends Identifier { + override def humanReadable: String = s"io(${innerId.humanReadable})" +} case class InstanceIdentifier(name: String) extends Identifier { Identifier.checkIdentifier(name) + + override def humanReadable: String = name } -case class SpecialIdentifier(name: String) extends Identifier +case class SpecialIdentifier(name: String) extends Identifier { + override def humanReadable: String = name +} object RootIdentifier extends SpecialIdentifier(Identifier.ROOT) object ParentIdentifier extends SpecialIdentifier(Identifier.PARENT) object IoIdentifier extends SpecialIdentifier(Identifier.IO) +object EndianIdentifier extends SpecialIdentifier(Identifier.IS_LE) diff --git a/shared/src/main/scala/io/kaitai/struct/format/InstanceSpec.scala b/shared/src/main/scala/io/kaitai/struct/format/InstanceSpec.scala index 024313427..6d6ca2821 100644 --- a/shared/src/main/scala/io/kaitai/struct/format/InstanceSpec.scala +++ b/shared/src/main/scala/io/kaitai/struct/format/InstanceSpec.scala @@ -3,8 +3,9 @@ package io.kaitai.struct.format import io.kaitai.struct.datatype.DataType import io.kaitai.struct.exprlang.{Ast, Expressions} -sealed abstract class InstanceSpec(val doc: DocSpec) { +sealed abstract class InstanceSpec(val doc: DocSpec) extends YAMLPath { def dataTypeComposite: DataType + def isNullable: Boolean } case class ValueInstanceSpec( path: List[String], @@ -12,17 +13,21 @@ case class ValueInstanceSpec( value: Ast.expr, ifExpr: Option[Ast.expr], var dataType: Option[DataType] -) extends InstanceSpec(_doc) with YAMLPath { +) extends InstanceSpec(_doc) { override def dataTypeComposite = dataType.get + override def isNullable: Boolean = ifExpr.isDefined } case class ParseInstanceSpec( + id: Identifier, path: List[String], private val _doc: DocSpec, dataType: DataType, cond: ConditionalSpec, pos: Option[Ast.expr], io: Option[Ast.expr] -) extends InstanceSpec(_doc) with AttrLikeSpec with YAMLPath +) extends InstanceSpec(_doc) with AttrLikeSpec { + override def isLazy = true +} object InstanceSpec { val LEGAL_KEYS_VALUE_INST = Set( @@ -33,7 +38,7 @@ object InstanceSpec { "if" ) - def fromYaml(src: Any, path: List[String], metaDef: MetaDefaults, id: InstanceIdentifier): InstanceSpec = { + def fromYaml(src: Any, path: List[String], metaDef: MetaSpec, id: InstanceIdentifier): InstanceSpec = { val srcMap = ParseUtils.asMapStr(src, path) ParseUtils.getOptValueStr(srcMap, "value", path).map(Expressions.parse) match { @@ -65,7 +70,7 @@ object InstanceSpec { val fakeAttrMap = srcMap.filterKeys((key) => key != "pos" && key != "io") val a = AttrSpec.fromYaml(fakeAttrMap, path, metaDef, id) - ParseInstanceSpec(path, a.doc, a.dataType, a.cond, pos, io) + ParseInstanceSpec(id, path, a.doc, a.dataType, a.cond, pos, io) } } } diff --git a/shared/src/main/scala/io/kaitai/struct/format/KSVersion.scala b/shared/src/main/scala/io/kaitai/struct/format/KSVersion.scala index b41836555..160ad48eb 100644 --- a/shared/src/main/scala/io/kaitai/struct/format/KSVersion.scala +++ b/shared/src/main/scala/io/kaitai/struct/format/KSVersion.scala @@ -65,7 +65,7 @@ object KSVersion { def current: KSVersion = _current.get def fromStr(str: String): KSVersion = - KSVersion(str.replace("-SNAPSHOT", "").split('.').map(_.toInt).toList) + KSVersion(str.replaceAll("-SNAPSHOT.*$", "").split('.').map(_.toInt).toList) /** * Hardcoded minimal version of runtime API that this particular diff --git a/shared/src/main/scala/io/kaitai/struct/format/MemberSpec.scala b/shared/src/main/scala/io/kaitai/struct/format/MemberSpec.scala new file mode 100644 index 000000000..c8ed8ce3e --- /dev/null +++ b/shared/src/main/scala/io/kaitai/struct/format/MemberSpec.scala @@ -0,0 +1,31 @@ +package io.kaitai.struct.format + +import io.kaitai.struct.datatype.DataType + +/** + * Base trait for everything that would be compiled to be members of the class, + * i.e. sequence attributes, parse instances, value instances, parameters. + */ +trait MemberSpec extends YAMLPath { + def id: Identifier + def dataType: DataType + def doc: DocSpec + + def dataTypeComposite = dataType + + /** + * Determines if this attribute can be "null" in some circumstances or not. + * In some target languages, it would affect data types used, init and + * cleanup procedures. + * @return True if this attribute can be "null", false if it's never "null" + */ + def isNullable: Boolean + + /** + * Determines if this attribute can be "null" in some circumstances or not. + * This version is for languages like C++, which make a special exception + * for raw byte arrays placement for switch statements. + * @return True if this attribute can be "null", false if it's never "null" + */ + def isNullableSwitchRaw: Boolean +} diff --git a/shared/src/main/scala/io/kaitai/struct/format/MetaDefaults.scala b/shared/src/main/scala/io/kaitai/struct/format/MetaDefaults.scala deleted file mode 100644 index 928f578cd..000000000 --- a/shared/src/main/scala/io/kaitai/struct/format/MetaDefaults.scala +++ /dev/null @@ -1,19 +0,0 @@ -package io.kaitai.struct.format - -import io.kaitai.struct.datatype.Endianness - -case class MetaDefaults( - endian: Option[Endianness], - encoding: Option[String] -) { - def updateWith(metaOpt: Option[MetaSpec]): MetaDefaults = { - metaOpt match { - case None => this - case Some(meta) => - MetaDefaults( - meta.endian.orElse(this.endian), - meta.encoding.orElse(this.encoding) - ) - } - } -} diff --git a/shared/src/main/scala/io/kaitai/struct/format/MetaSpec.scala b/shared/src/main/scala/io/kaitai/struct/format/MetaSpec.scala index abb8a6690..000b55245 100644 --- a/shared/src/main/scala/io/kaitai/struct/format/MetaSpec.scala +++ b/shared/src/main/scala/io/kaitai/struct/format/MetaSpec.scala @@ -1,6 +1,6 @@ package io.kaitai.struct.format -import io.kaitai.struct.datatype.Endianness +import io.kaitai.struct.datatype.{CalcEndian, Endianness, InheritedEndian} case class MetaSpec( path: List[String], @@ -11,9 +11,37 @@ case class MetaSpec( forceDebug: Boolean, opaqueTypes: Option[Boolean], imports: List[String] -) extends YAMLPath +) extends YAMLPath { + def fillInDefaults(defSpec: MetaSpec): MetaSpec = { + fillInEncoding(defSpec.encoding). + fillInEndian(defSpec.endian) + } + + private + def fillInEncoding(defEncoding: Option[String]): MetaSpec = { + (defEncoding, encoding) match { + case (None, _) => this + case (_, Some(_)) => this + case (Some(_), None) => + this.copy(encoding = defEncoding) + } + } + + def fillInEndian(defEndian: Option[Endianness]): MetaSpec = { + (defEndian, endian) match { + case (None, _) => this + case (_, Some(_)) => this + case (Some(_: CalcEndian), None) => + this.copy(endian = Some(InheritedEndian)) + case (Some(_), None) => + this.copy(endian = defEndian) + } + } +} object MetaSpec { + def emptyWithPath(path: List[String]) = OPAQUE.copy(isOpaque = false, path = path) + val OPAQUE = MetaSpec( path = List(), isOpaque = true, @@ -36,6 +64,7 @@ object MetaSpec { "ks-opaque-types", "license", "file-extension", + "xref", "application" ) @@ -48,6 +77,8 @@ object MetaSpec { throw YAMLParseException.incompatibleVersion(ver, KSVersion.current, path) } + val endian: Option[Endianness] = Endianness.fromYaml(srcMap.get("endian"), path) + ParseUtils.ensureLegalKeys(srcMap, LEGAL_KEYS, path) val id = ParseUtils.getOptValueStr(srcMap, "id", path) @@ -55,10 +86,6 @@ object MetaSpec { Identifier.checkIdentifierSource(idStr, "meta", path ++ List("id")) ) - val endian: Option[Endianness] = Endianness.defaultFromString( - ParseUtils.getOptValueStr(srcMap, "endian", path), - path - ) val encoding = ParseUtils.getOptValueStr(srcMap, "encoding", path) val forceDebug = ParseUtils.getOptValueBool(srcMap, "ks-debug", path).getOrElse(false) diff --git a/shared/src/main/scala/io/kaitai/struct/format/ParamDefSpec.scala b/shared/src/main/scala/io/kaitai/struct/format/ParamDefSpec.scala new file mode 100644 index 000000000..b5cef37be --- /dev/null +++ b/shared/src/main/scala/io/kaitai/struct/format/ParamDefSpec.scala @@ -0,0 +1,36 @@ +package io.kaitai.struct.format + +import io.kaitai.struct.datatype.DataType + +case class ParamDefSpec( + path: List[String], + id: Identifier, + dataType: DataType, + doc: DocSpec = DocSpec.EMPTY +) extends MemberSpec { + override def isNullable: Boolean = false + override def isNullableSwitchRaw: Boolean = false +} + +object ParamDefSpec { + def fromYaml(src: Any, path: List[String], idx: Int): ParamDefSpec = { + val srcMap = ParseUtils.asMapStr(src, path) + val id = ParseUtils.getValueIdentifier(srcMap, idx, "parameter", path) + fromYaml(srcMap, path, id) + } + + val LEGAL_KEYS = Set( + "id", + "type", + "doc", + "doc-ref" + ) + + def fromYaml(srcMap: Map[String, Any], path: List[String], id: Identifier): ParamDefSpec = { + val doc = DocSpec.fromYaml(srcMap, path) + val typeStr = ParseUtils.getOptValueStr(srcMap, "type", path) + val dataType = DataType.pureFromString(typeStr) + + ParamDefSpec(path, id, dataType, doc) + } +} diff --git a/shared/src/main/scala/io/kaitai/struct/format/ParseUtils.scala b/shared/src/main/scala/io/kaitai/struct/format/ParseUtils.scala index 09e589827..614f6954d 100644 --- a/shared/src/main/scala/io/kaitai/struct/format/ParseUtils.scala +++ b/shared/src/main/scala/io/kaitai/struct/format/ParseUtils.scala @@ -21,10 +21,19 @@ object ParseUtils { def getValueStr(src: Map[String, Any], field: String, path: List[String]): String = { src.get(field) match { - case Some(value: String) => - value - case unknown => - throw YAMLParseException.badType("string", unknown, path ++ List(field)) + case Some(value) => + asStr(value, path ++ List(field)) + case None => + throw YAMLParseException.noKey(path ++ List(field)) + } + } + + def getValueMapStrStr(src: Map[String, Any], field: String, path: List[String]): Map[String, String] = { + src.get(field) match { + case Some(value) => + asMapStrStr(value, path ++ List(field)) + case None => + throw YAMLParseException.noKey(path ++ List(field)) } } @@ -61,6 +70,19 @@ object ParseUtils { } } + def getValueIdentifier(src: Map[String, Any], idx: Int, entityName: String, path: List[String]): Identifier = { + getOptValueStr(src, "id", path) match { + case Some(idStr) => + try { + NamedIdentifier(idStr) + } catch { + case _: InvalidIdentifier => + throw YAMLParseException.invalidId(idStr, entityName, path ++ List("id")) + } + case None => NumberedIdentifier(idx) + } + } + /** * Gets a list of T-typed values from a given YAML map's key "field", * reporting errors accurately and ensuring type safety. @@ -113,6 +135,8 @@ object ParseUtils { str case n: Int => n.toString + case n: Long => + n.toString case n: Double => n.toString case n: Boolean => diff --git a/shared/src/main/scala/io/kaitai/struct/format/ProcessExpr.scala b/shared/src/main/scala/io/kaitai/struct/format/ProcessExpr.scala index 3f56f2b50..7fe8a6cf9 100644 --- a/shared/src/main/scala/io/kaitai/struct/format/ProcessExpr.scala +++ b/shared/src/main/scala/io/kaitai/struct/format/ProcessExpr.scala @@ -1,29 +1,21 @@ package io.kaitai.struct.format -import io.kaitai.struct.exprlang.{Expressions, Ast} +import io.kaitai.struct.exprlang.{Ast, Expressions} -trait ProcessExpr { - def outputType: String -} +sealed trait ProcessExpr -case object ProcessZlib extends ProcessExpr { - override def outputType: String = null -} -case object ProcessHexStrToInt extends ProcessExpr { - override def outputType: String = "u4" -} -case class ProcessXor(key: Ast.expr) extends ProcessExpr { - override def outputType: String = null -} -case class ProcessRotate(left: Boolean, key: Ast.expr) extends ProcessExpr { - override def outputType: String = null -} +case object ProcessZlib extends ProcessExpr +case class ProcessXor(key: Ast.expr) extends ProcessExpr +case class ProcessRotate(left: Boolean, key: Ast.expr) extends ProcessExpr +case class ProcessCustom(name: List[String], args: Seq[Ast.expr]) extends ProcessExpr object ProcessExpr { private val ReXor = "^xor\\(\\s*(.*?)\\s*\\)$".r private val ReRotate = "^ro(l|r)\\(\\s*(.*?)\\s*\\)$".r + private val ReCustom = "^([a-z][a-z0-9_.]*)\\(\\s*(.*?)\\s*\\)$".r + private val ReCustomNoArg = "^([a-z][a-z0-9_.]*)$".r - def fromStr(s: Option[String]): Option[ProcessExpr] = { + def fromStr(s: Option[String], path: List[String]): Option[ProcessExpr] = { s match { case None => None @@ -31,14 +23,16 @@ object ProcessExpr { Some(op match { case "zlib" => ProcessZlib - case "hexstr_to_int" => - ProcessHexStrToInt case ReXor(arg) => ProcessXor(Expressions.parse(arg)) case ReRotate(dir, arg) => ProcessRotate(dir == "l", Expressions.parse(arg)) + case ReCustom(name, args) => + ProcessCustom(name.split('.').toList, Expressions.parseList(args)) + case ReCustomNoArg(name) => + ProcessCustom(name.split('.').toList, Seq()) case _ => - throw new RuntimeException(s"Invalid process: '$s'") + throw YAMLParseException.badProcess(op, path) }) } } diff --git a/shared/src/main/scala/io/kaitai/struct/format/YAMLParseException.scala b/shared/src/main/scala/io/kaitai/struct/format/YAMLParseException.scala index 747f044fa..bedf3bb70 100644 --- a/shared/src/main/scala/io/kaitai/struct/format/YAMLParseException.scala +++ b/shared/src/main/scala/io/kaitai/struct/format/YAMLParseException.scala @@ -8,6 +8,9 @@ class YAMLParseException(val msg: String, val path: List[String]) extends RuntimeException(s"/${path.mkString("/")}: $msg", null) object YAMLParseException { + def noKey(path: List[String]): YAMLParseException = + new YAMLParseException(s"missing mandatory argument `${path.last}`", path) + def badType(expected: String, got: Any, path: List[String]): YAMLParseException = { val gotStr = got match { case null => "null" @@ -35,11 +38,23 @@ object YAMLParseException { val f = epe.failure val pos = StringReprOps.prettyIndex(f.extra.input, f.index) new YAMLParseException( - s"parsing expression '${epe.src}' failed on $pos, expected ${f.extra.traced.expected}", + s"parsing expression '${epe.src}' failed on $pos, expected ${f.extra.traced.expected.replaceAll("\n", "\\n")}", path ) } def exprType(expected: String, got: DataType, path: List[String]): YAMLParseException = new YAMLParseException(s"invalid type: expected $expected, got $got", path) + + def badProcess(got: String, path: List[String]): YAMLParseException = + new YAMLParseException(s"incorrect process expression `$got`", path) + + def invalidParamCount(paramSize: Int, argSize: Int, path: List[String]): YAMLParseException = + new YAMLParseException(s"parameter count mismatch: $paramSize declared, but $argSize used", path) + + def paramMismatch(idx: Int, argType: DataType, paramName: String, paramType: DataType, path: List[String]): YAMLParseException = + new YAMLParseException( + s"can't pass argument #$idx of type $argType into parameter `$paramName` of type $paramType", + path + ) } diff --git a/shared/src/main/scala/io/kaitai/struct/format/YAMLPath.scala b/shared/src/main/scala/io/kaitai/struct/format/YAMLPath.scala index 7c1dd465c..9282a6e66 100644 --- a/shared/src/main/scala/io/kaitai/struct/format/YAMLPath.scala +++ b/shared/src/main/scala/io/kaitai/struct/format/YAMLPath.scala @@ -1,5 +1,11 @@ package io.kaitai.struct.format +/** + * Common trait for all format parts that stores YAML path that corresponds + * to particular format part. Used to throw path-localized exceptions, i.e. + * [[YAMLParseException]] and [[io.kaitai.struct.precompile.ErrorInInput]], + * and implement better error messaging. + */ trait YAMLPath { def path: List[String] diff --git a/shared/src/main/scala/io/kaitai/struct/languages/CSharpCompiler.scala b/shared/src/main/scala/io/kaitai/struct/languages/CSharpCompiler.scala index a6048dae8..55fcd971a 100644 --- a/shared/src/main/scala/io/kaitai/struct/languages/CSharpCompiler.scala +++ b/shared/src/main/scala/io/kaitai/struct/languages/CSharpCompiler.scala @@ -1,13 +1,13 @@ package io.kaitai.struct.languages import io.kaitai.struct._ -import io.kaitai.struct.datatype.DataType import io.kaitai.struct.datatype.DataType._ +import io.kaitai.struct.datatype.{CalcEndian, DataType, FixedEndian, InheritedEndian} import io.kaitai.struct.exprlang.Ast import io.kaitai.struct.exprlang.Ast.expr import io.kaitai.struct.format.{RepeatUntil, _} import io.kaitai.struct.languages.components._ -import io.kaitai.struct.translators.{CSharpTranslator, TypeDetector, TypeProvider} +import io.kaitai.struct.translators.{CSharpTranslator, TypeDetector} class CSharpCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) extends LanguageCompiler(typeProvider, config) @@ -21,24 +21,26 @@ class CSharpCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) with NoNeedForFullClassPath { import CSharpCompiler._ - override def getStatic = CSharpCompiler + val translator = new CSharpTranslator(typeProvider, importList) override def indent: String = " " override def outFileName(topClassName: String): String = s"${type2class(topClassName)}.cs" + override def outImports(topClass: ClassSpec) = + importList.toList.map((x) => s"using $x;").mkString("", "\n", "\n") + override def fileHeader(topClassName: String): Unit = { - out.puts(s"// $headerComment") + outHeader.puts(s"// $headerComment") + outHeader.puts var ns = "Kaitai" if (!config.dotNetNamespace.isEmpty) ns = config.dotNetNamespace - out.puts - out.puts("using System;") - out.puts("using System.Collections.Generic;") - if (ns != "Kaitai") out.puts("using Kaitai;") - out.puts + if (ns != "Kaitai") + importList.add("Kaitai") + out.puts out.puts(s"namespace $ns") out.puts(s"{") out.inc @@ -55,46 +57,106 @@ class CSharpCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) out.puts(s"{") out.inc - out.puts(s"public static ${type2class(name)} FromFile(string fileName)") - out.puts(s"{") - out.inc - out.puts(s"return new ${type2class(name)}(new $kstreamName(fileName));") - out.dec - out.puts("}") + // `FromFile` is generated only for parameterless types + if (typeProvider.nowClass.params.isEmpty) { + out.puts(s"public static ${type2class(name)} FromFile(string fileName)") + out.puts(s"{") + out.inc + out.puts(s"return new ${type2class(name)}(new $kstreamName(fileName));") + out.dec + out.puts("}") + out.puts + } } override def classFooter(name: String): Unit = fileFooter(name) - override def classConstructorHeader(name: String, parentClassName: String, rootClassName: String): Unit = { - out.puts - out.puts(s"public ${type2class(name)}($kstreamName io, ${type2class(parentClassName)} parent = null, ${type2class(rootClassName)} root = null) : base(io)") + override def classConstructorHeader(name: String, parentType: DataType, rootClassName: String, isHybrid: Boolean, params: List[ParamDefSpec]): Unit = { + typeProvider.nowClass.meta.endian match { + case Some(_: CalcEndian) | Some(InheritedEndian) => + out.puts(s"private bool? ${privateMemberName(EndianIdentifier)};") + case _ => + // no _is_le variable + } + + val addEndian = if (isHybrid) ", bool? isLe = null" else "" + + val pIo = paramName(IoIdentifier) + val pParent = paramName(ParentIdentifier) + val pRoot = paramName(RootIdentifier) + + val paramsArg = Utils.join(params.map((p) => + s"${kaitaiType2NativeType(p.dataType)} ${paramName(p.id)}" + ), "", ", ", ", ") + + out.puts( + s"public ${type2class(name)}($paramsArg" + + s"$kstreamName $pIo, " + + s"${kaitaiType2NativeType(parentType)} $pParent = null, " + + s"${type2class(rootClassName)} $pRoot = null$addEndian) : base($pIo)" + ) out.puts(s"{") out.inc - out.puts(s"${privateMemberName(ParentIdentifier)} = parent;") + handleAssignmentSimple(ParentIdentifier, pParent) + + handleAssignmentSimple( + RootIdentifier, + if (name == rootClassName) s"$pRoot ?? this" else pRoot + ) - if (name == rootClassName) - out.puts(s"${privateMemberName(RootIdentifier)} = root ?? this;") - else - out.puts(s"${privateMemberName(RootIdentifier)} = root;") + if (isHybrid) + handleAssignmentSimple(EndianIdentifier, "isLe") - out.puts("_parse();") + // Store parameters passed to us + params.foreach((p) => handleAssignmentSimple(p.id, paramName(p.id))) + } + + override def classConstructorFooter: Unit = fileFooter(null) + + override def runRead(): Unit = + out.puts("_read();") + + override def runReadCalc(): Unit = { + out.puts + out.puts(s"if (${privateMemberName(EndianIdentifier)} == null) {") + out.inc + out.puts("throw new Exception(\"Unable to decide on endianness\");") + importList.add("System") + out.dec + out.puts(s"} else if (${privateMemberName(EndianIdentifier)} == true) {") + out.inc + out.puts("_readLE();") + out.dec + out.puts("} else {") + out.inc + out.puts("_readBE();") out.dec out.puts("}") - out.puts + } - out.puts("private void _parse()") + override def readHeader(endian: Option[FixedEndian], isEmpty: Boolean) = { + val readAccessAndType = if (debug) { + "public" + } else { + "private" + } + val suffix = endian match { + case Some(e) => s"${e.toSuffix.toUpperCase}" + case None => "" + } + out.puts(s"$readAccessAndType void _read$suffix()") out.puts("{") out.inc } - override def classConstructorFooter: Unit = fileFooter(null) + override def readFooter(): Unit = fileFooter("") - override def attributeDeclaration(attrName: Identifier, attrType: DataType, condSpec: ConditionalSpec): Unit = { - out.puts(s"private ${kaitaiType2NativeType(attrType)} ${privateMemberName(attrName)};") + override def attributeDeclaration(attrName: Identifier, attrType: DataType, isNullable: Boolean): Unit = { + out.puts(s"private ${kaitaiType2NativeTypeNullable(attrType, isNullable)} ${privateMemberName(attrName)};") } - override def attributeReader(attrName: Identifier, attrType: DataType, condSpec: ConditionalSpec): Unit = { - out.puts(s"public ${kaitaiType2NativeType(attrType)} ${publicMemberName(attrName)} { get { return ${privateMemberName(attrName)}; } }") + override def attributeReader(attrName: Identifier, attrType: DataType, isNullable: Boolean): Unit = { + out.puts(s"public ${kaitaiType2NativeTypeNullable(attrType, isNullable)} ${publicMemberName(attrName)} { get { return ${privateMemberName(attrName)}; } }") } override def universalDoc(doc: DocSpec): Unit = { @@ -118,6 +180,18 @@ class CSharpCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) } } + override def attrParseHybrid(leProc: () => Unit, beProc: () => Unit): Unit = { + out.puts(s"if (${privateMemberName(EndianIdentifier)} == true) {") + out.inc + leProc() + out.dec + out.puts("} else {") + out.inc + beProc() + out.dec + out.puts("}") + } + override def attrFixedContentsParse(attrName: Identifier, contents: String): Unit = out.puts(s"${privateMemberName(attrName)} = $normalIO.EnsureFixedContents($contents);") @@ -137,6 +211,11 @@ class CSharpCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) s"8 - (${expression(rotValue)})" } out.puts(s"$destName = $normalIO.ProcessRotateLeft($srcName, $expr, 1);") + case ProcessCustom(name, args) => + val procClass = types2class(name) + val procName = s"_process_${idToStr(varSrc)}" + out.puts(s"$procClass $procName = new $procClass(${args.map(expression).mkString(", ")});") + out.puts(s"$destName = $procName.Decode($srcName);") } } @@ -188,9 +267,14 @@ class CSharpCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) override def condIfFooter(expr: expr): Unit = fileFooter(null) override def condRepeatEosHeader(id: Identifier, io: String, dataType: DataType, needRaw: Boolean): Unit = { + importList.add("System.Collections.Generic") + if (needRaw) out.puts(s"${privateMemberName(RawIdentifier(id))} = new List();") out.puts(s"${privateMemberName(id)} = new ${kaitaiType2NativeType(ArrayType(dataType))}();") + out.puts("{") + out.inc + out.puts("var i = 0;") out.puts(s"while (!$io.IsEof) {") out.inc } @@ -199,13 +283,22 @@ class CSharpCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) out.puts(s"${privateMemberName(id)}.Add($expr);") } - override def condRepeatEosFooter: Unit = fileFooter(null) + override def condRepeatEosFooter: Unit = { + out.puts("i++;") + out.dec + out.puts("}") + out.dec + out.puts("}") + } override def condRepeatExprHeader(id: Identifier, io: String, dataType: DataType, needRaw: Boolean, repeatExpr: expr): Unit = { + importList.add("System.Collections.Generic") + if (needRaw) out.puts(s"${privateMemberName(RawIdentifier(id))} = new List((int) (${expression(repeatExpr)}));") out.puts(s"${privateMemberName(id)} = new ${kaitaiType2NativeType(ArrayType(dataType))}((int) (${expression(repeatExpr)}));") - out.puts(s"for (var i = 0; i < ${expression(repeatExpr)}; i++) {") + out.puts(s"for (var i = 0; i < ${expression(repeatExpr)}; i++)") + out.puts("{") out.inc } @@ -216,11 +309,14 @@ class CSharpCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) override def condRepeatExprFooter: Unit = fileFooter(null) override def condRepeatUntilHeader(id: Identifier, io: String, dataType: DataType, needRaw: Boolean, untilExpr: expr): Unit = { + importList.add("System.Collections.Generic") + if (needRaw) out.puts(s"${privateMemberName(RawIdentifier(id))} = new List();") out.puts(s"${privateMemberName(id)} = new ${kaitaiType2NativeType(ArrayType(dataType))}();") out.puts("{") out.inc + out.puts("var i = 0;") out.puts(s"${kaitaiType2NativeType(dataType)} ${translator.doName("_")};") out.puts("do {") out.inc @@ -238,6 +334,7 @@ class CSharpCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) override def condRepeatUntilFooter(id: Identifier, io: String, dataType: DataType, needRaw: Boolean, untilExpr: expr): Unit = { typeProvider._currentIteratorType = Some(dataType) + out.puts("i++;") out.dec out.puts(s"} while (!(${expression(untilExpr)}));") out.dec @@ -248,10 +345,10 @@ class CSharpCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) out.puts(s"${privateMemberName(id)} = $expr;") } - override def parseExpr(dataType: DataType, io: String): String = { + override def parseExpr(dataType: DataType, assignType: DataType, io: String, defEndian: Option[FixedEndian]): String = { dataType match { case t: ReadableType => - s"$io.Read${Utils.capitalize(t.apiCall)}()" + s"$io.Read${Utils.capitalize(t.apiCall(defEndian))}()" case blt: BytesLimitType => s"$io.ReadBytes(${expression(blt.size)})" case _: BytesEosType => @@ -263,6 +360,7 @@ class CSharpCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) case BitsType(width: Int) => s"$io.ReadBitsInt($width)" case t: UserType => + val addParams = Utils.join(t.args.map((a) => translator.translate(a)), "", ", ", ", ") val addArgs = if (t.isOpaque) { "" } else { @@ -271,9 +369,13 @@ class CSharpCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) case Some(fp) => translator.translate(fp) case None => "this" } - s", $parent, ${privateMemberName(RootIdentifier)}" + val addEndian = t.classSpec.get.meta.endian match { + case Some(InheritedEndian) => s", ${privateMemberName(EndianIdentifier)}" + case _ => "" + } + s", $parent, ${privateMemberName(RootIdentifier)}$addEndian" } - s"new ${types2class(t.name)}($io$addArgs)" + s"new ${types2class(t.name)}($addParams$io$addArgs)" } } @@ -289,35 +391,98 @@ class CSharpCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) expr2 } - override def switchStart(id: Identifier, on: Ast.expr): Unit = - out.puts(s"switch (${expression(on)}) {") + /** + * Designates switch mode. If false, we're doing real switch-case for this + * attribute. If true, we're doing if-based emulation. + */ + var switchIfs = false + + val NAME_SWITCH_ON = Ast.expr.Name(Ast.identifier(Identifier.SWITCH_ON)) + + override def switchStart(id: Identifier, on: Ast.expr): Unit = { + val onType = translator.detectType(on) + typeProvider._currentSwitchType = Some(onType) + + // Determine switching mode for this construct based on type + switchIfs = onType match { + case _: IntType | _: EnumType | _: StrType => false + case _ => true + } + + if (switchIfs) { + out.puts("{") + out.inc + out.puts(s"${kaitaiType2NativeType(onType)} ${expression(NAME_SWITCH_ON)} = ${expression(on)};") + } else { + out.puts(s"switch (${expression(on)}) {") + } + } + + def switchCmpExpr(condition: Ast.expr): String = + expression( + Ast.expr.Compare( + NAME_SWITCH_ON, + Ast.cmpop.Eq, + condition + ) + ) + + override def switchCaseFirstStart(condition: Ast.expr): Unit = { + if (switchIfs) { + out.puts(s"if (${switchCmpExpr(condition)})") + out.puts("{") + out.inc + } else { + switchCaseStart(condition) + } + } override def switchCaseStart(condition: Ast.expr): Unit = { - out.puts(s"case ${expression(condition)}: {") - out.inc + if (switchIfs) { + out.puts(s"else if (${switchCmpExpr(condition)})") + out.puts("{") + out.inc + } else { + out.puts(s"case ${expression(condition)}: {") + out.inc + } } override def switchCaseEnd(): Unit = { - out.puts("break;") - out.dec - out.puts("}") + if (switchIfs) { + out.dec + out.puts("}") + } else { + out.puts("break;") + out.dec + out.puts("}") + } } override def switchElseStart(): Unit = { - out.puts("default: {") - out.inc + if (switchIfs) { + out.puts("else") + out.puts("{") + out.inc + } else { + out.puts("default: {") + out.inc + } } - override def switchEnd(): Unit = + override def switchEnd(): Unit = { + if (switchIfs) + out.dec out.puts("}") + } - override def instanceDeclaration(attrName: InstanceIdentifier, attrType: DataType, condSpec: ConditionalSpec): Unit = { + override def instanceDeclaration(attrName: InstanceIdentifier, attrType: DataType, isNullable: Boolean): Unit = { out.puts(s"private bool ${flagForInstName(attrName)};") - out.puts(s"private ${kaitaiType2NativeType(attrType)} ${privateMemberName(attrName)};") + out.puts(s"private ${kaitaiType2NativeTypeNullable(attrType, isNullable)} ${privateMemberName(attrName)};") } - override def instanceHeader(className: String, instName: InstanceIdentifier, dataType: DataType): Unit = { - out.puts(s"public ${kaitaiType2NativeType(dataType)} ${publicMemberName(instName)}") + override def instanceHeader(className: String, instName: InstanceIdentifier, dataType: DataType, isNullable: Boolean): Unit = { + out.puts(s"public ${kaitaiType2NativeTypeNullable(dataType, isNullable)} ${publicMemberName(instName)}") out.puts("{") out.inc out.puts("get") @@ -343,7 +508,7 @@ class CSharpCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) out.puts(s"return ${privateMemberName(instName)};") } - override def instanceCalculate(instName: InstanceIdentifier, dataType: DataType, value: expr): Unit = + override def instanceCalculate(instName: Identifier, dataType: DataType, value: expr): Unit = // Perform explicit cast as unsigned integers can't be directly assigned to the default int type handleAssignmentSimple(instName, s"(${kaitaiType2NativeType(dataType)}) (${expression(value)})") @@ -391,13 +556,15 @@ class CSharpCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) case _ => s"_${idToStr(id)}" } } + + override def localTemporaryName(id: Identifier): String = s"_t_${idToStr(id)}" + + override def paramName(id: Identifier): String = s"p_${idToStr(id)}" } object CSharpCompiler extends LanguageCompilerStatic with StreamStructNames with UpperCamelCaseClasses { - override def getTranslator(tp: TypeProvider, config: RuntimeConfig) = new CSharpTranslator(tp) - override def getCompiler( tp: ClassTypeProvider, config: RuntimeConfig @@ -446,6 +613,18 @@ object CSharpCompiler extends LanguageCompilerStatic } } + def kaitaiType2NativeTypeNullable(t: DataType, isNullable: Boolean): String = { + val r = kaitaiType2NativeType(t) + if (isNullable) { + t match { + case _: NumericType | _: BooleanType => s"$r?" + case _ => r + } + } else { + r + } + } + def types2class(names: List[String]) = names.map(x => type2class(x)).mkString(".") override def kstructName = "KaitaiStruct" diff --git a/shared/src/main/scala/io/kaitai/struct/languages/CppCompiler.scala b/shared/src/main/scala/io/kaitai/struct/languages/CppCompiler.scala index 1d0add50f..3ebf25e96 100644 --- a/shared/src/main/scala/io/kaitai/struct/languages/CppCompiler.scala +++ b/shared/src/main/scala/io/kaitai/struct/languages/CppCompiler.scala @@ -1,13 +1,15 @@ package io.kaitai.struct.languages import io.kaitai.struct._ -import io.kaitai.struct.datatype.DataType import io.kaitai.struct.datatype.DataType._ +import io.kaitai.struct.datatype.{CalcEndian, DataType, FixedEndian, InheritedEndian} import io.kaitai.struct.exprlang.Ast import io.kaitai.struct.exprlang.Ast.expr import io.kaitai.struct.format._ import io.kaitai.struct.languages.components._ -import io.kaitai.struct.translators.{CppTranslator, TypeDetector, TypeProvider} +import io.kaitai.struct.translators.{CppTranslator, TypeDetector} + +import scala.collection.mutable.ListBuffer class CppCompiler( typeProvider: ClassTypeProvider, @@ -20,18 +22,25 @@ class CppCompiler( with EveryReadIsExpression { import CppCompiler._ + val importListSrc = new ImportList + val importListHdr = new ImportList + + override val translator = new CppTranslator(typeProvider, importListSrc) + val outSrcHeader = new StringLanguageOutputWriter(indent) + val outHdrHeader = new StringLanguageOutputWriter(indent) val outSrc = new StringLanguageOutputWriter(indent) val outHdr = new StringLanguageOutputWriter(indent) override def results(topClass: ClassSpec): Map[String, String] = { val fn = topClass.nameAsStr Map( - s"$fn.cpp" -> outSrc.result, - s"$fn.h" -> outHdr.result + s"$fn.cpp" -> (outSrcHeader.result + importListToStr(importListSrc) + outSrc.result), + s"$fn.h" -> (outHdrHeader.result + importListToStr(importListHdr) + outHdr.result) ) } - override def getStatic = CppCompiler + private def importListToStr(importList: ImportList): String = + importList.toList.map((x) => s"#include <$x>").mkString("", "\n", "\n") sealed trait AccessMode case object PrivateAccess extends AccessMode @@ -43,24 +52,20 @@ class CppCompiler( override def outFileName(topClassName: String): String = topClassName override def fileHeader(topClassName: String): Unit = { - outSrc.puts(s"// $headerComment") - outSrc.puts - outSrc.puts("#include \"" + outFileName(topClassName) + ".h\"") - outSrc.puts - outSrc.puts("#include ") - outSrc.puts("#include ") + outSrcHeader.puts(s"// $headerComment") + outSrcHeader.puts + outSrcHeader.puts("#include \"" + outFileName(topClassName) + ".h\"") + outSrcHeader.puts - outHdr.puts(s"#ifndef ${defineName(topClassName)}") - outHdr.puts(s"#define ${defineName(topClassName)}") - outHdr.puts - outHdr.puts(s"// $headerComment") - outHdr.puts - outHdr.puts("#include ") - outHdr.puts("#include ") - outHdr.puts - outHdr.puts("#include ") - outHdr.puts("#include ") // TODO: add only if required - outHdr.puts("#include ") // TODO: add only if required + outHdrHeader.puts(s"#ifndef ${defineName(topClassName)}") + outHdrHeader.puts(s"#define ${defineName(topClassName)}") + outHdrHeader.puts + outHdrHeader.puts(s"// $headerComment") + outHdrHeader.puts + outHdrHeader.puts("#include \"kaitai/kaitaistruct.h\"") + outHdrHeader.puts + + importListHdr.add("stdint.h") // API compatibility check val minVer = KSVersion.minimalRuntime.toInt @@ -113,27 +118,60 @@ class CppCompiler( outHdr.puts(s"class ${types2class(name)};") } - override def classConstructorHeader(name: List[String], parentClassName: List[String], rootClassName: List[String]): Unit = { + override def classConstructorHeader(name: List[String], parentType: DataType, rootClassName: List[String], isHybrid: Boolean, params: List[ParamDefSpec]): Unit = { + val (endianSuffixHdr, endianSuffixSrc) = if (isHybrid) { + (", int p_is_le = -1", ", int p_is_le") + } else { + ("", "") + } + + val paramsArg = Utils.join(params.map((p) => + s"${kaitaiType2NativeType(p.dataType)} ${paramName(p.id)}" + ), "", ", ", ", ") + + // Parameter names + val pIo = paramName(IoIdentifier) + val pParent = paramName(ParentIdentifier) + val pRoot = paramName(RootIdentifier) + + // Types + val tIo = s"$kstreamName*" + val tParent = kaitaiType2NativeType(parentType) + val tRoot = s"${types2class(rootClassName)}*" + outHdr.puts - outHdr.puts(s"${types2class(List(name.last))}(" + - s"$kstreamName* p_io, " + - s"${types2class(parentClassName)}* p_parent = 0, " + - s"${types2class(rootClassName)}* p_root = 0);" + outHdr.puts(s"${types2class(List(name.last))}($paramsArg" + + s"$tIo $pIo, " + + s"$tParent $pParent = 0, " + + s"$tRoot $pRoot = 0$endianSuffixHdr);" ) outSrc.puts - outSrc.puts(s"${types2class(name)}::${types2class(List(name.last))}(" + - s"$kstreamName *p_io, " + - s"${types2class(parentClassName)} *p_parent, " + - s"${types2class(rootClassName)} *p_root) : $kstructName(p_io) {" + outSrc.puts(s"${types2class(name)}::${types2class(List(name.last))}($paramsArg" + + s"$tIo $pIo, " + + s"$tParent $pParent, " + + s"$tRoot $pRoot$endianSuffixSrc) : $kstructName($pIo) {" ) outSrc.inc - handleAssignmentSimple(ParentIdentifier, "p_parent") + handleAssignmentSimple(ParentIdentifier, pParent) handleAssignmentSimple(RootIdentifier, if (name == rootClassName) { "this" } else { - "p_root" + pRoot }) + + typeProvider.nowClass.meta.endian match { + case Some(_: CalcEndian) | Some(InheritedEndian) => + ensureMode(PrivateAccess) + outHdr.puts("int m__is_le;") + handleAssignmentSimple(EndianIdentifier, if (isHybrid) "p_is_le" else "-1") + ensureMode(PublicAccess) + case _ => + // no _is_le variable + } + + // Store parameters passed to us + params.foreach((p) => handleAssignmentSimple(p.id, paramName(p.id))) } override def classConstructorFooter: Unit = { @@ -141,7 +179,7 @@ class CppCompiler( outSrc.puts("}") } - override def classDestructorHeader(name: List[String], parentTypeName: List[String], topClassName: List[String]): Unit = { + override def classDestructorHeader(name: List[String], parentType: DataType, topClassName: List[String]): Unit = { outHdr.puts(s"~${types2class(List(name.last))}();") outSrc.puts @@ -151,10 +189,51 @@ class CppCompiler( override def classDestructorFooter = classConstructorFooter - override def attributeDeclaration(attrName: Identifier, attrType: DataType, condSpec: ConditionalSpec): Unit = { + override def runRead(): Unit = { + outSrc.puts("_read();") + } + + override def runReadCalc(): Unit = { + outSrc.puts + outSrc.puts("if (m__is_le == -1) {") + outSrc.inc + importListSrc.add("stdexcept") + outSrc.puts("throw std::runtime_error(\"unable to decide on endianness\");") + outSrc.dec + outSrc.puts("} else if (m__is_le == 1) {") + outSrc.inc + outSrc.puts("_read_le();") + outSrc.dec + outSrc.puts("} else {") + outSrc.inc + outSrc.puts("_read_be();") + outSrc.dec + outSrc.puts("}") + } + + override def readHeader(endian: Option[FixedEndian], isEmpty: Boolean): Unit = { + val suffix = endian match { + case Some(e) => s"_${e.toSuffix}" + case None => "" + } + ensureMode(PrivateAccess) + outHdr.puts(s"void _read$suffix();") + outSrc.puts + outSrc.puts(s"void ${types2class(typeProvider.nowClass.name)}::_read$suffix() {") + outSrc.inc + } + + override def readFooter(): Unit = { + outSrc.dec + outSrc.puts("}") + + ensureMode(PublicAccess) + } + + override def attributeDeclaration(attrName: Identifier, attrType: DataType, isNullable: Boolean): Unit = { ensureMode(PrivateAccess) outHdr.puts(s"${kaitaiType2NativeType(attrType)} ${privateMemberName(attrName)};") - declareNullFlag(attrName, condSpec) + declareNullFlag(attrName, isNullable) } def ensureMode(newMode: AccessMode): Unit = { @@ -170,7 +249,7 @@ class CppCompiler( } } - override def attributeReader(attrName: Identifier, attrType: DataType, condSpec: ConditionalSpec): Unit = { + override def attributeReader(attrName: Identifier, attrType: DataType, isNullable: Boolean): Unit = { ensureMode(PublicAccess) outHdr.puts(s"${kaitaiType2NativeType(attrType)} ${publicMemberName(attrName)}() const { return ${privateMemberName(attrName)}; }") } @@ -197,79 +276,112 @@ class CppCompiler( } override def attrDestructor(attr: AttrLikeSpec, id: Identifier): Unit = { - val t = attr.dataTypeComposite - - val checkFlags = attr match { - case is: InstanceSpec => - val dataType = is.dataTypeComposite - dataType match { - case ut: UserType => - val checkLazy = calculatedFlagForName(id.asInstanceOf[InstanceIdentifier]) - val checkNull = if (is.cond.ifExpr.isDefined) { - s" && !${nullFlagForName(id)}" - } else { - "" - } - outSrc.puts(s"if ($checkLazy$checkNull) {") - outSrc.inc - true - case _ => - false - } - case as: AttrSpec => - as.dataType match { - case ut: UserType => - if (as.cond.ifExpr.isDefined) { - outSrc.puts(s"if (!${nullFlagForName(id)}) {") - outSrc.inc - true - } else { - false - } - case _ => - false - } + val checkLazy = if (attr.isLazy) { + Some(calculatedFlagForName(id)) + } else { + None } - t match { - case ArrayType(_: UserTypeFromBytes) => - outSrc.puts(s"delete ${privateMemberName(RawIdentifier(id))};") - case _ => - // no cleanup needed + val checkNull = if (attr.isNullableSwitchRaw) { + Some(s"!${nullFlagForName(id)}") + } else { + None } - t match { - case ArrayType(el: UserType) => - val arrVar = privateMemberName(id) - outSrc.puts(s"for (std::vector<${kaitaiType2NativeType(el)}>::iterator it = $arrVar->begin(); it != $arrVar->end(); ++it) {") - outSrc.inc - outSrc.puts("delete *it;") - outSrc.dec - outSrc.puts("}") - case _ => - // no cleanup needed - } + val checks: List[String] = List(checkLazy, checkNull).flatten - t match { - case _: UserTypeFromBytes => - outSrc.puts(s"delete ${privateMemberName(IoStorageIdentifier(RawIdentifier(id)))};") - case _ => - // no cleanup needed + if (checks.nonEmpty) { + outSrc.puts(s"if (${checks.mkString(" && ")}) {") + outSrc.inc } - t match { - case _: UserType | _: ArrayType => - outSrc.puts(s"delete ${privateMemberName(id)};") - case _ => - // no cleanup needed + val (innerType, hasRaw) = attr.dataType match { + case ut: UserTypeFromBytes => (ut, true) + case st: SwitchType => (st.combinedType, st.hasSize) + case t => (t, false) } - if (checkFlags) { + destructMember(id, innerType, attr.isArray, hasRaw, hasRaw) + + if (checks.nonEmpty) { outSrc.dec outSrc.puts("}") } } + def destructMember(id: Identifier, innerType: DataType, isArray: Boolean, hasRaw: Boolean, hasIO: Boolean): Unit = { + if (isArray) { + // raw is std::vector*, no need to delete its contents, but we + // need to clean up the vector pointer itself + if (hasRaw) + outSrc.puts(s"delete ${privateMemberName(RawIdentifier(id))};") + + // IO is std::vector*, needs destruction of both members + // and the vector pointer itself + if (hasIO) { + val ioVar = privateMemberName(IoStorageIdentifier(RawIdentifier(id))) + destructVector(s"$kstreamName*", ioVar) + outSrc.puts(s"delete $ioVar;") + } + + // main member contents + if (needsDestruction(innerType)) { + val arrVar = privateMemberName(id) + + // C++ specific substitution: AnyType results from generic struct + raw bytes + // so we would assume that only generic struct needs to be cleaned up + val realType = innerType match { + case AnyType => KaitaiStructType + case _ => innerType + } + + destructVector(kaitaiType2NativeType(realType), arrVar) + } + + // main member is a std::vector of something, always needs destruction + outSrc.puts(s"delete ${privateMemberName(id)};") + } else { + // raw is just a string, no need to cleanup => we ignore `hasRaw` + + // but hasIO is important + if (hasIO) + outSrc.puts(s"delete ${privateMemberName(IoStorageIdentifier(RawIdentifier(id)))};") + + if (needsDestruction(innerType)) + outSrc.puts(s"delete ${privateMemberName(id)};") + } + } + + def needsDestruction(t: DataType): Boolean = t match { + case _: UserType | _: ArrayType | KaitaiStructType | AnyType => true + case _ => false + } + + /** + * Generates std::vector contents destruction loop. + * @param elType element type, i.e. XXX in `std::vector<XXX>` + * @param arrVar variable name that holds pointer to std::vector + */ + def destructVector(elType: String, arrVar: String): Unit = { + outSrc.puts(s"for (std::vector<$elType>::iterator it = $arrVar->begin(); it != $arrVar->end(); ++it) {") + outSrc.inc + outSrc.puts("delete *it;") + outSrc.dec + outSrc.puts("}") + } + + override def attrParseHybrid(leProc: () => Unit, beProc: () => Unit): Unit = { + outSrc.puts("if (m__is_le == 1) {") + outSrc.inc + leProc() + outSrc.dec + outSrc.puts("} else {") + outSrc.inc + beProc() + outSrc.dec + outSrc.puts("}") + } + override def attrFixedContentsParse(attrName: Identifier, contents: String): Unit = outSrc.puts(s"${privateMemberName(attrName)} = $normalIO->ensure_fixed_contents($contents);") @@ -293,13 +405,20 @@ class CppCompiler( s"8 - (${expression(rotValue)})" } outSrc.puts(s"$destName = $kstreamName::process_rotate_left($srcName, $expr);") + case ProcessCustom(name, args) => + val procClass = name.map((x) => type2class(x)).mkString("::") + val procName = s"_process_${idToStr(varSrc)}" + + importListSrc.add(name.last + ".h") + + outSrc.puts(s"$procClass $procName(${args.map(expression).mkString(", ")});") + outSrc.puts(s"$destName = $procName.decode($srcName);") } } - override def allocateIO(varName: Identifier, rep: RepeatSpec): IoStorageIdentifier = { - val memberName = privateMemberName(varName) - - val ioName = IoStorageIdentifier(varName) + override def allocateIO(id: Identifier, rep: RepeatSpec, extraAttrs: ListBuffer[AttrSpec]): String = { + val memberName = privateMemberName(id) + val ioId = IoStorageIdentifier(id) val args = rep match { case RepeatEos | RepeatExpr(_) => s"$memberName->at($memberName->size() - 1)" @@ -307,7 +426,20 @@ class CppCompiler( case NoRepeat => memberName } - outSrc.puts(s"${privateMemberName(ioName)} = new $kstreamName($args);") + val newStream = s"new $kstreamName($args)" + + val (ioType, ioName) = rep match { + case NoRepeat => + outSrc.puts(s"${privateMemberName(ioId)} = $newStream;") + (KaitaiStreamType, privateMemberName(ioId)) + case _ => + val localIO = s"io_${idToStr(id)}" + outSrc.puts(s"$kstreamName* $localIO = $newStream;") + outSrc.puts(s"${privateMemberName(ioId)}->push_back($localIO);") + (ArrayType(KaitaiStreamType), localIO) + } + + Utils.addUniqueAttr(extraAttrs, AttrSpec(List(), ioId, ioType)) ioName } @@ -351,9 +483,16 @@ class CppCompiler( } override def condRepeatEosHeader(id: Identifier, io: String, dataType: DataType, needRaw: Boolean): Unit = { - if (needRaw) + importListHdr.add("vector") + + if (needRaw) { outSrc.puts(s"${privateMemberName(RawIdentifier(id))} = new std::vector();") + outSrc.puts(s"${privateMemberName(IoStorageIdentifier(RawIdentifier(id)))} = new std::vector<$kstreamName*>();") + } outSrc.puts(s"${privateMemberName(id)} = new std::vector<${kaitaiType2NativeType(dataType)}>();") + outSrc.puts("{") + outSrc.inc + outSrc.puts("int i = 0;") outSrc.puts(s"while (!$io->is_eof()) {") outSrc.inc } @@ -363,16 +502,25 @@ class CppCompiler( } override def condRepeatEosFooter: Unit = { + outSrc.puts("i++;") + outSrc.dec + outSrc.puts("}") outSrc.dec outSrc.puts("}") } override def condRepeatExprHeader(id: Identifier, io: String, dataType: DataType, needRaw: Boolean, repeatExpr: Ast.expr): Unit = { + importListHdr.add("vector") + val lenVar = s"l_${idToStr(id)}" outSrc.puts(s"int $lenVar = ${expression(repeatExpr)};") if (needRaw) { - outSrc.puts(s"${privateMemberName(RawIdentifier(id))} = new std::vector();") - outSrc.puts(s"${privateMemberName(RawIdentifier(id))}->reserve($lenVar);") + val rawId = privateMemberName(RawIdentifier(id)) + outSrc.puts(s"$rawId = new std::vector();") + outSrc.puts(s"$rawId->reserve($lenVar);") + val ioId = privateMemberName(IoStorageIdentifier(RawIdentifier(id))) + outSrc.puts(s"$ioId = new std::vector<$kstreamName*>();") + outSrc.puts(s"$ioId->reserve($lenVar);") } outSrc.puts(s"${privateMemberName(id)} = new std::vector<${kaitaiType2NativeType(dataType)}>();") outSrc.puts(s"${privateMemberName(id)}->reserve($lenVar);") @@ -390,11 +538,16 @@ class CppCompiler( } override def condRepeatUntilHeader(id: Identifier, io: String, dataType: DataType, needRaw: Boolean, untilExpr: expr): Unit = { - if (needRaw) + importListHdr.add("vector") + + if (needRaw) { outSrc.puts(s"${privateMemberName(RawIdentifier(id))} = new std::vector();") + outSrc.puts(s"${privateMemberName(IoStorageIdentifier(RawIdentifier(id)))} = new std::vector<$kstreamName*>();") + } outSrc.puts(s"${privateMemberName(id)} = new std::vector<${kaitaiType2NativeType(dataType)}>();") outSrc.puts("{") outSrc.inc + outSrc.puts("int i = 0;") outSrc.puts(s"${kaitaiType2NativeType(dataType)} ${translator.doName("_")};") outSrc.puts("do {") outSrc.inc @@ -412,6 +565,7 @@ class CppCompiler( override def condRepeatUntilFooter(id: Identifier, io: String, dataType: DataType, needRaw: Boolean, untilExpr: expr): Unit = { typeProvider._currentIteratorType = Some(dataType) + outSrc.puts("i++;") outSrc.dec outSrc.puts(s"} while (!(${expression(untilExpr)}));") outSrc.dec @@ -422,10 +576,10 @@ class CppCompiler( outSrc.puts(s"${privateMemberName(id)} = $expr;") } - override def parseExpr(dataType: DataType, io: String): String = { + override def parseExpr(dataType: DataType, assignType: DataType, io: String, defEndian: Option[FixedEndian]): String = { dataType match { case t: ReadableType => - s"$io->read_${t.apiCall}()" + s"$io->read_${t.apiCall(defEndian)}()" case blt: BytesLimitType => s"$io->read_bytes(${expression(blt.size)})" case _: BytesEosType => @@ -437,6 +591,7 @@ class CppCompiler( case BitsType(width: Int) => s"$io->read_bits_int($width)" case t: UserType => + val addParams = Utils.join(t.args.map((a) => translator.translate(a)), "", ", ", ", ") val addArgs = if (t.isOpaque) { "" } else { @@ -445,9 +600,13 @@ class CppCompiler( case Some(fp) => translator.translate(fp) case None => "this" } - s", $parent, ${privateMemberName(RootIdentifier)}" + val addEndian = t.classSpec.get.meta.endian match { + case Some(InheritedEndian) => ", m__is_le" + case _ => "" + } + s", $parent, ${privateMemberName(RootIdentifier)}$addEndian" } - s"new ${types2class(t.name)}($io$addArgs)" + s"new ${types2class(t.name)}($addParams$io$addArgs)" } } @@ -492,7 +651,7 @@ class CppCompiler( outSrc.puts(s"if (on == ${expression(condition)}) {") outSrc.inc } else { - outSrc.puts(s"case ${expression(condition)}:") + outSrc.puts(s"case ${expression(condition)}: {") outSrc.inc } } @@ -502,7 +661,7 @@ class CppCompiler( outSrc.puts(s"else if (on == ${expression(condition)}) {") outSrc.inc } else { - outSrc.puts(s"case ${expression(condition)}:") + outSrc.puts(s"case ${expression(condition)}: {") outSrc.inc } } @@ -514,6 +673,7 @@ class CppCompiler( } else { outSrc.puts("break;") outSrc.dec + outSrc.puts("}") } } @@ -522,7 +682,7 @@ class CppCompiler( outSrc.puts("else {") outSrc.inc } else { - outSrc.puts("default:") + outSrc.puts("default: {") outSrc.inc } } @@ -537,14 +697,14 @@ class CppCompiler( override def switchBytesOnlyAsRaw = true - override def instanceDeclaration(attrName: InstanceIdentifier, attrType: DataType, condSpec: ConditionalSpec): Unit = { + override def instanceDeclaration(attrName: InstanceIdentifier, attrType: DataType, isNullable: Boolean): Unit = { ensureMode(PrivateAccess) outHdr.puts(s"bool ${calculatedFlagForName(attrName)};") outHdr.puts(s"${kaitaiType2NativeType(attrType)} ${privateMemberName(attrName)};") - declareNullFlag(attrName, condSpec) + declareNullFlag(attrName, isNullable) } - override def instanceHeader(className: List[String], instName: InstanceIdentifier, dataType: DataType): Unit = { + override def instanceHeader(className: List[String], instName: InstanceIdentifier, dataType: DataType, isNullable: Boolean): Unit = { ensureMode(PublicAccess) outHdr.puts(s"${kaitaiType2NativeType(dataType)} ${publicMemberName(instName)}();") @@ -569,7 +729,7 @@ class CppCompiler( outSrc.puts(s"return ${privateMemberName(instName)};") } - override def enumDeclaration(curClass: List[String], enumName: String, enumColl: Seq[(Long, String)]): Unit = { + override def enumDeclaration(curClass: List[String], enumName: String, enumColl: Seq[(Long, EnumValueSpec)]): Unit = { val enumClass = types2class(List(enumName)) outHdr.puts @@ -578,12 +738,12 @@ class CppCompiler( if (enumColl.size > 1) { enumColl.dropRight(1).foreach { case (id, label) => - outHdr.puts(s"${value2Const(enumName, label)} = $id,") + outHdr.puts(s"${value2Const(enumName, label.name)} = $id,") } } enumColl.last match { case (id, label) => - outHdr.puts(s"${value2Const(enumName, label)} = $id") + outHdr.puts(s"${value2Const(enumName, label.name)} = $id") } outHdr.dec @@ -649,15 +809,25 @@ class CppCompiler( def defineName(className: String) = className.toUpperCase + "_H_" - def calculatedFlagForName(ksName: InstanceIdentifier) = - s"f_${ksName.name}" + /** + * Returns name of a member that stores "calculated flag" for a given lazy + * attribute. That is, if it's true, then calculation have already taken + * place and we need to return already calculated member in a getter, or, + * if it's false, we need to calculate / parse it first. + * @param ksName attribute ID + * @return calculated flag member name associated with it + */ + def calculatedFlagForName(ksName: Identifier) = + s"f_${idToStr(ksName)}" - def nullFlagForName(ksName: Identifier) = { - ksName match { - case NamedIdentifier(name) => s"n_$name" - case InstanceIdentifier(name) => s"n_$name" - } - } + /** + * Returns name of a member that stores "null flag" for a given attribute, + * that is, if it's true, then associated attribute is null. + * @param ksName attribute ID + * @return null flag member name associated with it + */ + def nullFlagForName(ksName: Identifier) = + s"n_${idToStr(ksName)}" override def idToStr(id: Identifier): String = { id match { @@ -674,8 +844,12 @@ class CppCompiler( override def publicMemberName(id: Identifier): String = idToStr(id) - def declareNullFlag(attrName: Identifier, condSpec: ConditionalSpec) = { - if (condSpec.ifExpr.nonEmpty) { + override def localTemporaryName(id: Identifier): String = s"_t_${idToStr(id)}" + + override def paramName(id: Identifier): String = s"p_${idToStr(id)}" + + def declareNullFlag(attrName: Identifier, isNullable: Boolean) = { + if (isNullable) { outHdr.puts(s"bool ${nullFlagForName(attrName)};") ensureMode(PublicAccess) outHdr.puts(s"bool _is_null_${idToStr(attrName)}() { ${publicMemberName(attrName)}(); return ${nullFlagForName(attrName)}; };") @@ -687,7 +861,6 @@ class CppCompiler( } object CppCompiler extends LanguageCompilerStatic with StreamStructNames { - override def getTranslator(tp: TypeProvider, config: RuntimeConfig) = new CppTranslator(tp) override def getCompiler( tp: ClassTypeProvider, config: RuntimeConfig diff --git a/shared/src/main/scala/io/kaitai/struct/languages/GoCompiler.scala b/shared/src/main/scala/io/kaitai/struct/languages/GoCompiler.scala new file mode 100644 index 000000000..430ff940b --- /dev/null +++ b/shared/src/main/scala/io/kaitai/struct/languages/GoCompiler.scala @@ -0,0 +1,494 @@ +package io.kaitai.struct.languages + +import io.kaitai.struct.datatype.{DataType, FixedEndian} +import io.kaitai.struct.datatype.DataType._ +import io.kaitai.struct.exprlang.Ast +import io.kaitai.struct.format._ +import io.kaitai.struct.languages.components._ +import io.kaitai.struct.translators.{GoTranslator, TranslatorResult, TypeDetector} +import io.kaitai.struct.{ClassTypeProvider, RuntimeConfig, Utils} + +class GoCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) + extends LanguageCompiler(typeProvider, config) + with SingleOutputFile + with UpperCamelCaseClasses + with ObjectOrientedLanguage + with UniversalFooter + with UniversalDoc + with AllocateIOLocalVar + with GoReads + with FixedContentsUsingArrayByteLiteral { + import GoCompiler._ + + override val translator = new GoTranslator(out, typeProvider, importList) + + override def innerClasses = false + + override def universalFooter: Unit = { + out.dec + out.puts("}") + } + + override def indent: String = "\t" + override def outFileName(topClassName: String): String = + s"src/${config.goPackage}/$topClassName.go" + + override def outImports(topClass: ClassSpec) = { + val imp = importList.toList + imp.size match { + case 0 => "" + case 1 => "import \"" + imp.head + "\"\n" + case _ => + "import (\n" + + imp.map((x) => indent + "\"" + x + "\"").mkString("", "\n", "\n") + + ")\n" + } + } + + override def fileHeader(topClassName: String): Unit = { + outHeader.puts(s"// $headerComment") + if (!config.goPackage.isEmpty) { + outHeader.puts + outHeader.puts(s"package ${config.goPackage}") + } + outHeader.puts + + importList.add("github.com/kaitai-io/kaitai_struct_go_runtime/kaitai") + + out.puts + } + + override def classHeader(name: List[String]): Unit = { + out.puts(s"type ${types2class(name)} struct {") + out.inc + } + + override def classFooter(name: List[String]): Unit = universalFooter + + override def classConstructorHeader(name: List[String], parentType: DataType, rootClassName: List[String], isHybrid: Boolean, params: List[ParamDefSpec]): Unit = { + out.puts + out.puts( + s"func (this *${types2class(name)}) Read(" + + s"io *$kstreamName, " + + s"parent ${kaitaiType2NativeType(parentType)}, " + + s"root *${types2class(rootClassName)}) (err error) {" + ) + out.inc + out.puts(s"${privateMemberName(IoIdentifier)} = io") + out.puts(s"${privateMemberName(ParentIdentifier)} = parent") + out.puts(s"${privateMemberName(RootIdentifier)} = root") + out.puts + } + + override def classConstructorFooter: Unit = { + out.puts("return err") + universalFooter + } + + override def runRead(): Unit = {} + override def runReadCalc(): Unit = ??? + override def readHeader(endian: Option[FixedEndian], isEmpty: Boolean): Unit = {} + override def readFooter(): Unit = {} + + override def attributeDeclaration(attrName: Identifier, attrType: DataType, isNullable: Boolean): Unit = { + out.puts(s"${idToStr(attrName)} ${kaitaiType2NativeType(attrType)}") + translator.returnRes = None + } + + override def attributeReader(attrName: Identifier, attrType: DataType, isNullable: Boolean): Unit = {} + + override def universalDoc(doc: DocSpec): Unit = { + out.puts + out.puts( "/**") + + doc.summary.foreach((summary) => out.putsLines(" * ", summary)) + + doc.ref match { + case TextRef(text) => + out.putsLines(" * ", "@see \"" + text + "\"") + case ref: UrlRef => + out.putsLines(" * ", s"@see ${ref.toAhref}") + case NoRef => + // no reference => output nothing + } + + out.puts( " */") + } + + override def attrParseHybrid(leProc: () => Unit, beProc: () => Unit): Unit = ??? + + override def attrFixedContentsParse(attrName: Identifier, contents: String): Unit = { + out.puts(s"${privateMemberName(attrName)} = $normalIO.ensureFixedContents($contents);") + } + + override def attrProcess(proc: ProcessExpr, varSrc: Identifier, varDest: Identifier): Unit = { + val srcName = privateMemberName(varSrc) + val destName = privateMemberName(varDest) + + proc match { + case ProcessXor(xorValue) => + out.puts(s"$destName = $kstreamName.processXor($srcName, ${expression(xorValue)});") + case ProcessZlib => + out.puts(s"$destName = $kstreamName.processZlib($srcName);") + case ProcessRotate(isLeft, rotValue) => + val expr = if (isLeft) { + expression(rotValue) + } else { + s"8 - (${expression(rotValue)})" + } + out.puts(s"$destName = $kstreamName.processRotateLeft($srcName, $expr, 1);") + } + } + + override def allocateIO(varName: Identifier, rep: RepeatSpec): String = { + val javaName = privateMemberName(varName) + + val ioName = idToStr(IoStorageIdentifier(varName)) + + val args = rep match { + case RepeatEos | RepeatExpr(_) => s"$javaName.get($javaName.size() - 1)" + case RepeatUntil(_) => translator.specialName(Identifier.ITERATOR2) + case NoRepeat => javaName + } + + importList.add("bytes") + + out.puts(s"$ioName := kaitai.NewStream(bytes.NewReader($args))") + ioName + } + + override def useIO(ioEx: Ast.expr): String = { + out.puts(s"$kstreamName io = ${expression(ioEx)};") + "io" + } + + override def pushPos(io: String): Unit = { + out.puts(s"_pos, err := $io.Pos()") + translator.outAddErrCheck() + } + + override def seek(io: String, pos: Ast.expr): Unit = { + importList.add("io") + + out.puts(s"_, err = $io.Seek(int64(${expression(pos)}), io.SeekStart)") + translator.outAddErrCheck() + } + + override def popPos(io: String): Unit = { + importList.add("io") + + out.puts(s"_, err = $io.Seek(_pos, io.SeekStart)") + translator.outAddErrCheck() + } + + override def alignToByte(io: String): Unit = + out.puts(s"$io.AlignToByte()") + + override def condIfHeader(expr: Ast.expr): Unit = { + out.puts(s"if (${expression(expr)}) {") + out.inc + } + + override def condRepeatEosHeader(id: Identifier, io: String, dataType: DataType, needRaw: Boolean): Unit = { + if (needRaw) + out.puts(s"${privateMemberName(RawIdentifier(id))} = new ArrayList();") + //out.puts(s"${privateMemberName(id)} = make(${kaitaiType2NativeType(ArrayType(dataType))})") + out.puts(s"for !$io.EOF() {") + out.inc + } + + override def handleAssignmentRepeatEos(id: Identifier, r: TranslatorResult): Unit = { + val name = privateMemberName(id) + val expr = translator.resToStr(r) + out.puts(s"$name = append($name, $expr)") + } + + override def condRepeatExprHeader(id: Identifier, io: String, dataType: DataType, needRaw: Boolean, repeatExpr: Ast.expr): Unit = { + if (needRaw) + out.puts(s"${privateMemberName(RawIdentifier(id))} = new ArrayList((int) (${expression(repeatExpr)}));") + out.puts(s"${privateMemberName(id)} = make(${kaitaiType2NativeType(ArrayType(dataType))}, ${expression(repeatExpr)})") + out.puts(s"for i := range ${privateMemberName(id)} {") + out.inc + } + + override def handleAssignmentRepeatExpr(id: Identifier, r: TranslatorResult): Unit = { + val name = privateMemberName(id) + val expr = translator.resToStr(r) + out.puts(s"$name[i] = $expr") + } + + override def condRepeatUntilHeader(id: Identifier, io: String, dataType: DataType, needRaw: Boolean, untilExpr: Ast.expr): Unit = { + if (needRaw) + out.puts(s"${privateMemberName(RawIdentifier(id))} = new ArrayList();") + out.puts(s"${privateMemberName(id)} = new ${kaitaiType2NativeType(ArrayType(dataType))}();") + out.puts("{") + out.inc + out.puts(s"${kaitaiType2NativeType(dataType)} ${translator.specialName(Identifier.ITERATOR)};") + out.puts("do {") + out.inc + } + + override def handleAssignmentRepeatUntil(id: Identifier, r: TranslatorResult, isRaw: Boolean): Unit = { + val expr = translator.resToStr(r) + val (typeDecl, tempVar) = if (isRaw) { + ("byte[] ", translator.specialName(Identifier.ITERATOR2)) + } else { + ("", translator.specialName(Identifier.ITERATOR)) + } + out.puts(s"$typeDecl$tempVar = $expr;") + out.puts(s"${privateMemberName(id)}.add($tempVar);") + } + + override def condRepeatUntilFooter(id: Identifier, io: String, dataType: DataType, needRaw: Boolean, untilExpr: Ast.expr): Unit = { + typeProvider._currentIteratorType = Some(dataType) + out.dec + out.puts(s"} while (!(${expression(untilExpr)}));") + out.dec + out.puts("}") + } + + override def handleAssignmentSimple(id: Identifier, r: TranslatorResult): Unit = { + val expr = translator.resToStr(r) + out.puts(s"${privateMemberName(id)} = $expr") + } + + override def parseExpr(dataType: DataType, io: String, defEndian: Option[FixedEndian]): String = { + dataType match { + case t: ReadableType => + s"$io.Read${Utils.capitalize(t.apiCall(defEndian))}()" + case blt: BytesLimitType => + s"$io.ReadBytes(int(${expression(blt.size)}))" + case _: BytesEosType => + s"$io.ReadBytesFull()" + case BytesTerminatedType(terminator, include, consume, eosError, _) => + s"$io.ReadBytesTerm($terminator, $include, $consume, $eosError)" + case BitsType1 => + s"$io.ReadBitsInt(1) != 0" + case BitsType(width: Int) => + s"$io.ReadBitsInt($width)" + case t: UserType => + val addArgs = if (t.isOpaque) { + "" + } else { + val parent = t.forcedParent match { + case Some(USER_TYPE_NO_PARENT) => "null" + case Some(fp) => translator.translate(fp) + case None => "this" + } + s", $parent, _root" + } + s"${types2class(t.name)}($io$addArgs)" + } + } + +// override def bytesPadTermExpr(expr0: String, padRight: Option[Int], terminator: Option[Int], include: Boolean) = { +// val expr1 = padRight match { +// case Some(padByte) => s"$kstreamName.bytesStripRight($expr0, (byte) $padByte)" +// case None => expr0 +// } +// val expr2 = terminator match { +// case Some(term) => s"$kstreamName.bytesTerminate($expr1, (byte) $term, $include)" +// case None => expr1 +// } +// expr2 +// } + + override def switchStart(id: Identifier, on: Ast.expr): Unit = + out.puts(s"switch (${expression(on)}) {") + + override def switchCaseStart(condition: Ast.expr): Unit = { + // Java is very specific about what can be used as "condition" in "case + // condition:". + val condStr = condition match { + case Ast.expr.EnumByLabel(enumName, enumVal) => + // If switch is over a enum, only literal enum values are supported, + // and they must be written as "MEMBER", not "SomeEnum.MEMBER". + value2Const(enumVal.name) + case _ => + expression(condition) + } + + out.puts(s"case $condStr: {") + out.inc + } + + override def switchCaseEnd(): Unit = { + out.puts("break;") + out.dec + out.puts("}") + } + + override def switchElseStart(): Unit = { + out.puts("default: {") + out.inc + } + + override def switchEnd(): Unit = + out.puts("}") + + override def instanceDeclaration(attrName: InstanceIdentifier, attrType: DataType, isNullable: Boolean): Unit = { + out.puts(s"${calculatedFlagForName(attrName)} bool") + out.puts(s"${idToStr(attrName)} ${kaitaiType2NativeType(attrType)}") + } + + override def instanceHeader(className: List[String], instName: InstanceIdentifier, dataType: DataType, isNullable: Boolean): Unit = { + out.puts(s"func (this *${types2class(className)}) ${publicMemberName(instName)}() (v ${kaitaiType2NativeType(dataType)}, err error) {") + out.inc + translator.returnRes = Some(dataType match { + case _: IntType => "0" + case _: BooleanType => "false" + case _: StrType => "\"\"" + case _ => "nil" + }) + } + + override def instanceCalculate(instName: Identifier, dataType: DataType, value: Ast.expr): Unit = { + val r = translator.translate(value) + val converted = dataType match { + case _: UserType => r + case _ => s"${kaitaiType2NativeType(dataType)}($r)" + } + out.puts(s"${privateMemberName(instName)} = $converted") + } + + override def instanceCheckCacheAndReturn(instName: InstanceIdentifier): Unit = { + out.puts(s"if (this.${calculatedFlagForName(instName)}) {") + out.inc + instanceReturn(instName) + universalFooter + } + + override def instanceReturn(instName: InstanceIdentifier): Unit = { + out.puts(s"return ${privateMemberName(instName)}, nil") + } + + override def instanceSetCalculated(instName: InstanceIdentifier): Unit = + out.puts(s"this.${calculatedFlagForName(instName)} = true") + + override def enumDeclaration(curClass: List[String], enumName: String, enumColl: Seq[(Long, EnumValueSpec)]): Unit = { + val enumClass = type2class(enumName) + + out.puts + out.puts(s"public enum $enumClass {") + out.inc + + if (enumColl.size > 1) { + enumColl.dropRight(1).foreach { case (id, label) => + out.puts(s"${value2Const(label.name)}($id),") + } + } + enumColl.last match { + case (id, label) => + out.puts(s"${value2Const(label.name)}($id);") + } + + out.puts + out.puts("private final long id;") + out.puts(s"$enumClass(long id) { this.id = id; }") + out.puts("public long id() { return id; }") + out.puts(s"private static final Map byId = new HashMap(${enumColl.size});") + out.puts("static {") + out.inc + out.puts(s"for ($enumClass e : $enumClass.values())") + out.inc + out.puts(s"byId.put(e.id(), e);") + out.dec + out.dec + out.puts("}") + out.puts(s"public static $enumClass byId(long id) { return byId.get(id); }") + out.dec + out.puts("}") + } + + def value2Const(s: String) = s.toUpperCase + + def idToStr(id: Identifier): String = { + id match { + case SpecialIdentifier(name) => name + case NamedIdentifier(name) => Utils.upperCamelCase(name) + case NumberedIdentifier(idx) => s"_${NumberedIdentifier.TEMPLATE}$idx" + case InstanceIdentifier(name) => Utils.lowerCamelCase(name) + case RawIdentifier(innerId) => "_raw_" + idToStr(innerId) + case IoStorageIdentifier(innerId) => "_io_" + idToStr(innerId) + } + } + + override def privateMemberName(id: Identifier): String = s"this.${idToStr(id)}" + + override def publicMemberName(id: Identifier): String = { + id match { + case IoIdentifier => "_IO" + case RootIdentifier => "_Root" + case ParentIdentifier => "_Parent" + case NamedIdentifier(name) => Utils.upperCamelCase(name) + case NumberedIdentifier(idx) => s"_${NumberedIdentifier.TEMPLATE}$idx" + case InstanceIdentifier(name) => Utils.upperCamelCase(name) + case RawIdentifier(innerId) => "_raw_" + idToStr(innerId) + } + } + + override def localTemporaryName(id: Identifier): String = s"_t_${idToStr(id)}" + + def calculatedFlagForName(id: Identifier) = s"_f_${idToStr(id)}" +} + +object GoCompiler extends LanguageCompilerStatic + with UpperCamelCaseClasses + with StreamStructNames { + + override def getCompiler( + tp: ClassTypeProvider, + config: RuntimeConfig + ): LanguageCompiler = new GoCompiler(tp, config) + + /** + * Determine Go data type corresponding to a KS data type. + * + * @param attrType KS data type + * @return Go data type + */ + def kaitaiType2NativeType(attrType: DataType): String = { + attrType match { + case Int1Type(false) => "uint8" + case IntMultiType(false, Width2, _) => "uint16" + case IntMultiType(false, Width4, _) => "uint32" + case IntMultiType(false, Width8, _) => "uint64" + + case Int1Type(true) => "int8" + case IntMultiType(true, Width2, _) => "int16" + case IntMultiType(true, Width4, _) => "int32" + case IntMultiType(true, Width8, _) => "int64" + + case FloatMultiType(Width4, _) => "float32" + case FloatMultiType(Width8, _) => "float64" + + case BitsType(_) => "uint64" + + case _: BooleanType => "bool" + case CalcIntType => "int" + case CalcFloatType => "float64" + + case _: StrType => "string" + case _: BytesType => "[]byte" + + case AnyType => "interface{}" + case KaitaiStreamType => "*" + kstreamName + case KaitaiStructType => kstructName + + case t: UserType => "*" + types2class(t.classSpec match { + case Some(cs) => cs.name + case None => t.name + }) + case EnumType(name, _) => types2class(name) + + case ArrayType(inType) => s"[]${kaitaiType2NativeType(inType)}" + + case SwitchType(_, cases) => kaitaiType2NativeType(TypeDetector.combineTypes(cases.values)) + } + } + + def types2class(names: List[String]) = names.map(x => type2class(x)).mkString("_") + + override def kstreamName: String = "kaitai.Stream" + override def kstructName: String = "interface{}" +} diff --git a/shared/src/main/scala/io/kaitai/struct/languages/JavaCompiler.scala b/shared/src/main/scala/io/kaitai/struct/languages/JavaCompiler.scala index 22570285f..b973b15f8 100644 --- a/shared/src/main/scala/io/kaitai/struct/languages/JavaCompiler.scala +++ b/shared/src/main/scala/io/kaitai/struct/languages/JavaCompiler.scala @@ -1,13 +1,13 @@ package io.kaitai.struct.languages -import io.kaitai.struct.datatype.DataType +import io.kaitai.struct._ import io.kaitai.struct.datatype.DataType._ +import io.kaitai.struct.datatype.{CalcEndian, DataType, FixedEndian, InheritedEndian} import io.kaitai.struct.exprlang.Ast import io.kaitai.struct.exprlang.Ast.expr import io.kaitai.struct.format._ import io.kaitai.struct.languages.components._ -import io.kaitai.struct.translators.{JavaTranslator, TypeDetector, TypeProvider} -import io.kaitai.struct._ +import io.kaitai.struct.translators.{JavaTranslator, TypeDetector} class JavaCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) extends LanguageCompiler(typeProvider, config) @@ -22,7 +22,18 @@ class JavaCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) with NoNeedForFullClassPath { import JavaCompiler._ - override def getStatic = JavaCompiler + val translator = new JavaTranslator(typeProvider, importList) + + // Preprocess fromFileClass and make import + val fromFileClass = { + val pos = config.javaFromFileClass.lastIndexOf('.') + if (pos < 0) { + config.javaFromFileClass + } else { + importList.add(config.javaFromFileClass) + config.javaFromFileClass.substring(pos + 1) + } + } override def universalFooter: Unit = { out.dec @@ -33,22 +44,20 @@ class JavaCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) override def outFileName(topClassName: String): String = s"src/${config.javaPackage.replace('.', '/')}/${type2class(topClassName)}.java" + override def outImports(topClass: ClassSpec) = + "\n" + importList.toList.map((x) => s"import $x;").mkString("\n") + "\n" + override def fileHeader(topClassName: String): Unit = { - out.puts(s"// $headerComment") + outHeader.puts(s"// $headerComment") if (!config.javaPackage.isEmpty) { - out.puts - out.puts(s"package ${config.javaPackage};") + outHeader.puts + outHeader.puts(s"package ${config.javaPackage};") } - out.puts - out.puts(s"import io.kaitai.struct.$kstructName;") - out.puts(s"import io.kaitai.struct.$kstreamName;") - out.puts - out.puts("import java.io.IOException;") - out.puts("import java.util.Arrays;") - out.puts("import java.util.ArrayList;") - out.puts("import java.util.HashMap;") - out.puts("import java.util.Map;") - out.puts("import java.nio.charset.Charset;") + + // Used in every class + importList.add(s"io.kaitai.struct.$kstructName") + importList.add(s"io.kaitai.struct.$kstreamName") + importList.add("java.io.IOException") out.puts } @@ -69,69 +78,128 @@ class JavaCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) out.puts("public Map> _arrStart = new HashMap>();") out.puts("public Map> _arrEnd = new HashMap>();") out.puts + + importList.add("java.util.ArrayList") + importList.add("java.util.HashMap") + importList.add("java.util.Map") } - out.puts(s"public static ${type2class(name)} fromFile(String fileName) throws IOException {") - out.inc - out.puts(s"return new ${type2class(name)}(new $kstreamName(fileName));") - out.dec - out.puts("}") + val isInheritedEndian = typeProvider.nowClass.meta.endian match { + case Some(InheritedEndian) => true + case _ => false + } + + // fromFile helper makes no sense for inherited endianness structures: + // they require endianness to be parsed anyway + if (!isInheritedEndian && !config.javaFromFileClass.isEmpty && typeProvider.nowClass.params.isEmpty) { + out.puts(s"public static ${type2class(name)} fromFile(String fileName) throws IOException {") + out.inc + out.puts(s"return new ${type2class(name)}(new $fromFileClass(fileName));") + out.dec + out.puts("}") + } } - override def classConstructorHeader(name: String, parentClassName: String, rootClassName: String): Unit = { + override def classConstructorHeader(name: String, parentType: DataType, rootClassName: String, isHybrid: Boolean, params: List[ParamDefSpec]): Unit = { + typeProvider.nowClass.meta.endian match { + case Some(_: CalcEndian) | Some(InheritedEndian) => + out.puts("private Boolean _is_le;") + case _ => + // no _is_le variable + } + + val paramsArg = Utils.join(params.map((p) => + s"${kaitaiType2JavaType(p.dataType)} ${paramName(p.id)}" + ), ", ", ", ", "") + + if (isHybrid) { + // Inherited endian classes can be only internal, so they have mandatory 4th argument + // and 1..3-argument constructors don't make sense + + out.puts + out.puts(s"public ${type2class(name)}($kstreamName _io, ${kaitaiType2JavaType(parentType)} _parent, ${type2class(rootClassName)} _root, boolean _is_le$paramsArg) {") + out.inc + out.puts("super(_io);") + out.puts("this._parent = _parent;") + out.puts("this._root = _root;") + out.puts("this._is_le = _is_le;") + } else { + // Normal 3 constructors, chained into the last + + val paramsRelay = Utils.join(params.map((p) => paramName(p.id)), ", ", ", ", "") + + out.puts + out.puts(s"public ${type2class(name)}($kstreamName _io$paramsArg) {") + out.inc + out.puts(s"this(_io, null, null$paramsRelay);") + out.dec + out.puts("}") + + out.puts + out.puts(s"public ${type2class(name)}($kstreamName _io, ${kaitaiType2JavaType(parentType)} _parent$paramsArg) {") + out.inc + out.puts(s"this(_io, _parent, null$paramsRelay);") + out.dec + out.puts("}") + + out.puts + out.puts(s"public ${type2class(name)}($kstreamName _io, ${kaitaiType2JavaType(parentType)} _parent, ${type2class(rootClassName)} _root$paramsArg) {") + out.inc + out.puts("super(_io);") + out.puts("this._parent = _parent;") + if (name == rootClassName) { + out.puts("this._root = _root == null ? this : _root;") + } else { + out.puts("this._root = _root;") + } + } + + // Store parameters passed to us + params.foreach((p) => handleAssignmentSimple(p.id, paramName(p.id))) + } + + override def runRead(): Unit = + out.puts("_read();") + + override def runReadCalc(): Unit = { out.puts - out.puts(s"public ${type2class(name)}($kstreamName _io) {") + out.puts("if (_is_le == null) {") out.inc - out.puts("super(_io);") - if (name == rootClassName) - out.puts("this._root = this;") - if (!debug) - out.puts("_read();") + out.puts(s"throw new $kstreamName.UndecidedEndiannessError();") out.dec - out.puts("}") - - out.puts - out.puts(s"public ${type2class(name)}($kstreamName _io, ${type2class(parentClassName)} _parent) {") + out.puts("} else if (_is_le) {") out.inc - out.puts("super(_io);") - out.puts("this._parent = _parent;") - if (name == rootClassName) - out.puts("this._root = this;") - if (!debug) - out.puts("_read();") + out.puts("_readLE();") out.dec - out.puts("}") - - out.puts - out.puts(s"public ${type2class(name)}($kstreamName _io, ${type2class(parentClassName)} _parent, ${type2class(rootClassName)} _root) {") + out.puts("} else {") out.inc - out.puts("super(_io);") - out.puts("this._parent = _parent;") - out.puts("this._root = _root;") - if (!debug) - out.puts("_read();") + out.puts("_readBE();") out.dec out.puts("}") + } + override def readHeader(endian: Option[FixedEndian], isEmpty: Boolean) = { val readAccessAndType = if (debug) { "public" } else { "private" } - out.puts(s"$readAccessAndType void _read() {") + val suffix = endian match { + case Some(e) => s"${e.toSuffix.toUpperCase}" + case None => "" + } + out.puts(s"$readAccessAndType void _read$suffix() {") out.inc } - override def classConstructorFooter: Unit = { - universalFooter - } + override def readFooter(): Unit = universalFooter - override def attributeDeclaration(attrName: Identifier, attrType: DataType, condSpec: ConditionalSpec): Unit = { - out.puts(s"private ${kaitaiType2JavaType(attrType, condSpec)} ${idToStr(attrName)};") + override def attributeDeclaration(attrName: Identifier, attrType: DataType, isNullable: Boolean): Unit = { + out.puts(s"private ${kaitaiType2JavaType(attrType, isNullable)} ${idToStr(attrName)};") } - override def attributeReader(attrName: Identifier, attrType: DataType, condSpec: ConditionalSpec): Unit = { - out.puts(s"public ${kaitaiType2JavaType(attrType, condSpec)} ${idToStr(attrName)}() { return ${idToStr(attrName)}; }") + override def attributeReader(attrName: Identifier, attrType: DataType, isNullable: Boolean): Unit = { + out.puts(s"public ${kaitaiType2JavaType(attrType, isNullable)} ${idToStr(attrName)}() { return ${idToStr(attrName)}; }") } override def universalDoc(doc: DocSpec): Unit = { @@ -152,6 +220,18 @@ class JavaCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) out.puts( " */") } + override def attrParseHybrid(leProc: () => Unit, beProc: () => Unit): Unit = { + out.puts("if (_is_le) {") + out.inc + leProc() + out.dec + out.puts("} else {") + out.inc + beProc() + out.dec + out.puts("}") + } + override def attrFixedContentsParse(attrName: Identifier, contents: String): Unit = { out.puts(s"${privateMemberName(attrName)} = $normalIO.ensureFixedContents($contents);") } @@ -172,6 +252,14 @@ class JavaCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) s"8 - (${expression(rotValue)})" } out.puts(s"$destName = $kstreamName.processRotateLeft($srcName, $expr, 1);") + case ProcessCustom(name, args) => + val namespace = name.init.mkString(".") + val procClass = namespace + + (if (namespace.nonEmpty) "." else "") + + type2class(name.last) + val procName = s"_process_${idToStr(varSrc)}" + out.puts(s"$procClass $procName = new $procClass(${args.map(expression).mkString(", ")});") + out.puts(s"$destName = $procName.decode($srcName);") } } @@ -186,7 +274,8 @@ class JavaCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) case NoRepeat => javaName } - out.puts(s"$kstreamName $ioName = new $kstreamName($args);") + importList.add("io.kaitai.struct.ByteBufferKaitaiStream") + out.puts(s"$kstreamName $ioName = new ByteBufferKaitaiStream($args);") ioName } @@ -259,20 +348,35 @@ class JavaCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) if (needRaw) out.puts(s"${privateMemberName(RawIdentifier(id))} = new ArrayList();") out.puts(s"${privateMemberName(id)} = new ${kaitaiType2JavaType(ArrayType(dataType))}();") + out.puts("{") + out.inc + out.puts("int i = 0;") out.puts(s"while (!$io.isEof()) {") out.inc + + importList.add("java.util.ArrayList") } override def handleAssignmentRepeatEos(id: Identifier, expr: String): Unit = { out.puts(s"${privateMemberName(id)}.add($expr);") } + override def condRepeatEosFooter: Unit = { + out.puts("i++;") + out.dec + out.puts("}") + out.dec + out.puts("}") + } + override def condRepeatExprHeader(id: Identifier, io: String, dataType: DataType, needRaw: Boolean, repeatExpr: expr): Unit = { if (needRaw) out.puts(s"${privateMemberName(RawIdentifier(id))} = new ArrayList(Long.valueOf(${expression(repeatExpr)}).intValue());") out.puts(s"${idToStr(id)} = new ${kaitaiType2JavaType(ArrayType(dataType))}(Long.valueOf(${expression(repeatExpr)}).intValue());") out.puts(s"for (int i = 0; i < ${expression(repeatExpr)}; i++) {") out.inc + + importList.add("java.util.ArrayList") } override def handleAssignmentRepeatExpr(id: Identifier, expr: String): Unit = { @@ -286,8 +390,11 @@ class JavaCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) out.puts("{") out.inc out.puts(s"${kaitaiType2JavaType(dataType)} ${translator.doName("_")};") + out.puts("int i = 0;") out.puts("do {") out.inc + + importList.add("java.util.ArrayList") } override def handleAssignmentRepeatUntil(id: Identifier, expr: String, isRaw: Boolean): Unit = { @@ -302,6 +409,7 @@ class JavaCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) override def condRepeatUntilFooter(id: Identifier, io: String, dataType: DataType, needRaw: Boolean, untilExpr: expr): Unit = { typeProvider._currentIteratorType = Some(dataType) + out.puts("i++;") out.dec out.puts(s"} while (!(${expression(untilExpr)}));") out.dec @@ -314,10 +422,10 @@ class JavaCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) override def handleAssignmentTempVar(dataType: DataType, id: String, expr: String): Unit = out.puts(s"${kaitaiType2JavaType(dataType)} $id = $expr;") - override def parseExpr(dataType: DataType, io: String): String = { - dataType match { + override def parseExpr(dataType: DataType, assignType: DataType, io: String, defEndian: Option[FixedEndian]): String = { + val expr = dataType match { case t: ReadableType => - s"$io.read${Utils.capitalize(t.apiCall)}()" + s"$io.read${Utils.capitalize(t.apiCall(defEndian))}()" case blt: BytesLimitType => s"$io.readBytes(${expression(blt.size)})" case _: BytesEosType => @@ -337,9 +445,20 @@ class JavaCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) case Some(fp) => translator.translate(fp) case None => "this" } - s", $parent, _root" + val addEndian = t.classSpec.get.meta.endian match { + case Some(InheritedEndian) => ", _is_le" + case _ => "" + } + s", $parent, _root$addEndian" } - s"new ${types2class(t.name)}($io$addArgs)" + val addParams = Utils.join(t.args.map((a) => translator.translate(a)), ", ", ", ", "") + s"new ${types2class(t.name)}($io$addArgs$addParams)" + } + + if (assignType != dataType) { + s"(${kaitaiType2JavaType(assignType)}) ($expr)" + } else { + expr } } @@ -358,44 +477,104 @@ class JavaCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) override def userTypeDebugRead(id: String): Unit = out.puts(s"$id._read();") - override def switchStart(id: Identifier, on: Ast.expr): Unit = - out.puts(s"switch (${expression(on)}) {") + /** + * Designates switch mode. If false, we're doing real switch-case for this + * attribute. If true, we're doing if-based emulation. + */ + var switchIfs = false + + val NAME_SWITCH_ON = Ast.expr.Name(Ast.identifier(Identifier.SWITCH_ON)) - override def switchCaseStart(condition: Ast.expr): Unit = { - // Java is very specific about what can be used as "condition" in "case - // condition:". - val condStr = condition match { - case Ast.expr.EnumByLabel(enumName, enumVal) => - // If switch is over a enum, only literal enum values are supported, - // and they must be written as "MEMBER", not "SomeEnum.MEMBER". - value2Const(enumVal.name) - case _ => - expression(condition) + override def switchStart(id: Identifier, on: Ast.expr): Unit = { + val onType = translator.detectType(on) + typeProvider._currentSwitchType = Some(onType) + + // Determine switching mode for this construct based on type + switchIfs = onType match { + case _: IntType | _: EnumType | _: StrType => false + case _ => true } - out.puts(s"case $condStr: {") - out.inc + if (switchIfs) { + out.puts("{") + out.inc + out.puts(s"${kaitaiType2JavaType(onType)} ${expression(NAME_SWITCH_ON)} = ${expression(on)};") + } else { + out.puts(s"switch (${expression(on)}) {") + } + } + + def switchCmpExpr(condition: Ast.expr): String = + expression( + Ast.expr.Compare( + NAME_SWITCH_ON, + Ast.cmpop.Eq, + condition + ) + ) + + override def switchCaseFirstStart(condition: Ast.expr): Unit = { + if (switchIfs) { + out.puts(s"if (${switchCmpExpr(condition)}) {") + out.inc + } else { + switchCaseStart(condition) + } + } + + override def switchCaseStart(condition: Ast.expr): Unit = { + if (switchIfs) { + out.puts(s"else if (${switchCmpExpr(condition)}) {") + out.inc + } else { + // Java is very specific about what can be used as "condition" in "case + // condition:". + val condStr = condition match { + case Ast.expr.EnumByLabel(_, enumVal) => + // If switch is over a enum, only literal enum values are supported, + // and they must be written as "MEMBER", not "SomeEnum.MEMBER". + value2Const(enumVal.name) + case _ => + expression(condition) + } + + out.puts(s"case $condStr: {") + out.inc + } } override def switchCaseEnd(): Unit = { - out.puts("break;") - out.dec - out.puts("}") + if (switchIfs) { + out.dec + out.puts("}") + } else { + out.puts("break;") + out.dec + out.puts("}") + } } override def switchElseStart(): Unit = { - out.puts("default: {") - out.inc + if (switchIfs) { + out.puts("else {") + out.inc + } else { + out.puts("default: {") + out.inc + } } - override def switchEnd(): Unit = + override def switchEnd(): Unit = { + if (switchIfs) + out.dec out.puts("}") + } - override def instanceDeclaration(attrName: InstanceIdentifier, attrType: DataType, condSpec: ConditionalSpec): Unit = { + override def instanceDeclaration(attrName: InstanceIdentifier, attrType: DataType, isNullable: Boolean): Unit = { out.puts(s"private ${kaitaiType2JavaTypeBoxed(attrType)} ${idToStr(attrName)};") } - override def instanceHeader(className: String, instName: InstanceIdentifier, dataType: DataType): Unit = { + override def instanceHeader(className: String, instName: InstanceIdentifier, dataType: DataType, isNullable: Boolean): Unit = { out.puts(s"public ${kaitaiType2JavaTypeBoxed(dataType)} ${idToStr(instName)}() {") out.inc } @@ -411,7 +590,7 @@ class JavaCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) out.puts(s"return ${privateMemberName(instName)};") } - override def instanceCalculate(instName: InstanceIdentifier, dataType: DataType, value: expr): Unit = { + override def instanceCalculate(instName: Identifier, dataType: DataType, value: expr): Unit = { val primType = kaitaiType2JavaTypePrim(dataType) val boxedType = kaitaiType2JavaTypeBoxed(dataType) @@ -461,6 +640,9 @@ class JavaCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) out.puts(s"public static $enumClass byId(long id) { return byId.get(id); }") out.dec out.puts("}") + + importList.add("java.util.Map") + importList.add("java.util.HashMap") } override def debugClassSequence(seq: List[AttrSpec]) = { @@ -483,13 +665,13 @@ class JavaCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) override def privateMemberName(id: Identifier): String = s"this.${idToStr(id)}" override def publicMemberName(id: Identifier) = idToStr(id) + + override def localTemporaryName(id: Identifier): String = s"_t_${idToStr(id)}" } object JavaCompiler extends LanguageCompilerStatic with UpperCamelCaseClasses with StreamStructNames { - override def getTranslator(tp: TypeProvider, config: RuntimeConfig) = new JavaTranslator(tp) - override def getCompiler( tp: ClassTypeProvider, config: RuntimeConfig @@ -497,8 +679,8 @@ object JavaCompiler extends LanguageCompilerStatic def kaitaiType2JavaType(attrType: DataType): String = kaitaiType2JavaTypePrim(attrType) - def kaitaiType2JavaType(attrType: DataType, condSpec: ConditionalSpec): String = - if (condSpec.ifExpr.nonEmpty) { + def kaitaiType2JavaType(attrType: DataType, isNullable: Boolean): String = + if (isNullable) { kaitaiType2JavaTypeBoxed(attrType) } else { kaitaiType2JavaTypePrim(attrType) @@ -544,7 +726,7 @@ object JavaCompiler extends LanguageCompilerStatic case ArrayType(_) => kaitaiType2JavaTypeBoxed(attrType) - case SwitchType(_, cases) => kaitaiType2JavaTypePrim(TypeDetector.combineTypes(cases.values)) + case st: SwitchType => kaitaiType2JavaTypePrim(st.combinedType) } } diff --git a/shared/src/main/scala/io/kaitai/struct/languages/JavaScriptCompiler.scala b/shared/src/main/scala/io/kaitai/struct/languages/JavaScriptCompiler.scala index 3fd99a440..f3d5fb3f7 100644 --- a/shared/src/main/scala/io/kaitai/struct/languages/JavaScriptCompiler.scala +++ b/shared/src/main/scala/io/kaitai/struct/languages/JavaScriptCompiler.scala @@ -1,13 +1,13 @@ package io.kaitai.struct.languages -import io.kaitai.struct.datatype.DataType import io.kaitai.struct.datatype.DataType._ +import io.kaitai.struct.datatype.{DataType, FixedEndian, InheritedEndian} import io.kaitai.struct.exprlang.Ast import io.kaitai.struct.exprlang.Ast.expr import io.kaitai.struct.format._ import io.kaitai.struct.languages.components._ -import io.kaitai.struct.translators.{JavaScriptTranslator, TypeProvider} -import io.kaitai.struct.{ClassTypeProvider, LanguageOutputWriter, RuntimeConfig, Utils} +import io.kaitai.struct.translators.JavaScriptTranslator +import io.kaitai.struct.{ClassTypeProvider, RuntimeConfig, Utils} class JavaScriptCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) extends LanguageCompiler(typeProvider, config) @@ -20,45 +20,45 @@ class JavaScriptCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) with FixedContentsUsingArrayByteLiteral { import JavaScriptCompiler._ - override def getStatic = JavaScriptCompiler + override val translator = new JavaScriptTranslator(typeProvider) override def indent: String = " " override def outFileName(topClassName: String): String = s"${type2class(topClassName)}.js" + override def outImports(topClass: ClassSpec) = { + val impList = importList.toList + val quotedImpList = impList.map((x) => s"'$x'") + val defineArgs = quotedImpList.mkString(", ") + val moduleArgs = quotedImpList.map((x) => s"require($x)").mkString(", ") + val argClasses = impList.map((x) => x.split('/').last) + val rootArgs = argClasses.map((x) => s"root.$x").mkString(", ") + + "(function (root, factory) {\n" + + indent + "if (typeof define === 'function' && define.amd) {\n" + + indent * 2 + s"define([$defineArgs], factory);\n" + + indent + "} else if (typeof module === 'object' && module.exports) {\n" + + indent * 2 + s"module.exports = factory($moduleArgs);\n" + + indent + "} else {\n" + + indent * 2 + s"root.${types2class(topClass.name)} = factory($rootArgs);\n" + + indent + "}\n" + + s"}(this, function (${argClasses.mkString(", ")}) {" + } + override def fileHeader(topClassName: String): Unit = { - out.puts(s"// $headerComment") + outHeader.puts(s"// $headerComment") + outHeader.puts + + importList.add("kaitai-struct/KaitaiStream") } override def fileFooter(name: String): Unit = { - out.puts - out.puts("// Export for amd environments") - out.puts("if (typeof define === 'function' && define.amd) {") - out.inc - out.puts(s"define('${type2class(name)}', [], function() {") - out.inc out.puts(s"return ${type2class(name)};") - out.dec - out.puts("});") - out.dec - out.puts("}") - - out.puts - - out.puts("// Export for CommonJS") - out.puts("if (typeof module === 'object' && module && module.exports) {") - out.inc - out.puts(s"module.exports = ${type2class(name)};") - out.dec - out.puts("}") + out.puts("}));") } override def opaqueClassDeclaration(classSpec: ClassSpec): Unit = { - val typeName = classSpec.name.head - out.puts - out.puts("if (typeof require === 'function')") - out.inc - out.puts(s"var ${type2class(typeName)} = require('./${outFileName(typeName)}');") - out.dec + val className = type2class(classSpec.name.head) + importList.add(s"./$className") } override def classHeader(name: List[String]): Unit = { @@ -82,22 +82,31 @@ class JavaScriptCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) out.puts("})();") } - override def classConstructorHeader(name: List[String], parentClassName: List[String], rootClassName: List[String]): Unit = { - out.puts(s"function ${type2class(name.last)}(_io, _parent, _root) {") + override def classConstructorHeader(name: List[String], parentClassName: DataType, rootClassName: List[String], isHybrid: Boolean, params: List[ParamDefSpec]): Unit = { + val endianSuffix = if (isHybrid) { + ", _is_le" + } else { + "" + } + + val paramsList = Utils.join(params.map((p) => paramName(p.id)), ", ", ", ", "") + + out.puts(s"function ${type2class(name.last)}(_io, _parent, _root$endianSuffix$paramsList) {") out.inc out.puts("this._io = _io;") out.puts("this._parent = _parent;") out.puts("this._root = _root || this;") + + if (isHybrid) + out.puts("this._is_le = _is_le;") + + // Store parameters passed to us + params.foreach((p) => handleAssignmentSimple(p.id, paramName(p.id))) + if (debug) { out.puts("this._debug = {};") - out.dec - out.puts("}") - out.puts - out.puts(s"${type2class(name.last)}.prototype._read = function() {") - out.inc - } else { - out.puts } + out.puts } override def classConstructorFooter: Unit = { @@ -105,9 +114,44 @@ class JavaScriptCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) out.puts("}") } - override def attributeDeclaration(attrName: Identifier, attrType: DataType, condSpec: ConditionalSpec): Unit = {} + override def runRead(): Unit = { + out.puts("this._read();") + } - override def attributeReader(attrName: Identifier, attrType: DataType, condSpec: ConditionalSpec): Unit = {} + override def runReadCalc(): Unit = { + out.puts + out.puts(s"if (this._is_le === true) {") + out.inc + out.puts("this._readLE();") + out.dec + out.puts("} else if (this._is_le === false) {") + out.inc + out.puts("this._readBE();") + out.dec + out.puts("} else {") + out.inc + out.puts("throw new KaitaiStream.UndecidedEndiannessError();") + out.dec + out.puts("}") + } + + override def readHeader(endian: Option[FixedEndian], isEmpty: Boolean) = { + val suffix = endian match { + case Some(e) => e.toSuffix.toUpperCase + case None => "" + } + out.puts(s"${type2class(typeProvider.nowClass.name.last)}.prototype._read$suffix = function() {") + out.inc + } + + override def readFooter() = { + out.dec + out.puts("}") + } + + override def attributeDeclaration(attrName: Identifier, attrType: DataType, isNullable: Boolean): Unit = {} + + override def attributeReader(attrName: Identifier, attrType: DataType, isNullable: Boolean): Unit = {} override def universalDoc(doc: DocSpec): Unit = { // JSDoc docstring style: http://usejsdoc.org/about-getting-started.html @@ -128,6 +172,18 @@ class JavaScriptCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) out.puts( " */") } + override def attrParseHybrid(leProc: () => Unit, beProc: () => Unit): Unit = { + out.puts("if (this._is_le) {") + out.inc + leProc() + out.dec + out.puts("} else {") + out.inc + beProc() + out.dec + out.puts("}") + } + override def attrFixedContentsParse(attrName: Identifier, contents: String): Unit = { out.puts(s"${privateMemberName(attrName)} = " + s"$normalIO.ensureFixedContents($contents);") @@ -153,6 +209,15 @@ class JavaScriptCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) s"8 - (${expression(rotValue)})" } out.puts(s"$destName = $kstreamName.processRotateLeft($srcName, $expr, 1);") + case ProcessCustom(name, args) => + val nameInit = name.init + val pkgName = if (nameInit.isEmpty) "" else nameInit.mkString("-") + "/" + val procClass = type2class(name.last) + + importList.add(s"$pkgName$procClass") + + out.puts(s"var _process = new $procClass(${args.map(expression).mkString(", ")});") + out.puts(s"$destName = _process.decode($srcName);") } } @@ -232,6 +297,7 @@ class JavaScriptCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) out.puts(s"${privateMemberName(id)} = [];") if (debug) out.puts(s"this._debug.${idToStr(id)}.arr = [];") + out.puts("var i = 0;") out.puts(s"while (!$io.isEof()) {") out.inc } @@ -241,6 +307,7 @@ class JavaScriptCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) } override def condRepeatEosFooter: Unit = { + out.puts("i++;") out.dec out.puts("}") } @@ -270,6 +337,7 @@ class JavaScriptCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) out.puts(s"${privateMemberName(id)} = []") if (debug) out.puts(s"this._debug.${idToStr(id)}.arr = [];") + out.puts("var i = 0;") out.puts("do {") out.inc } @@ -282,6 +350,7 @@ class JavaScriptCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) override def condRepeatUntilFooter(id: Identifier, io: String, dataType: DataType, needRaw: Boolean, untilExpr: expr): Unit = { typeProvider._currentIteratorType = Some(dataType) + out.puts("i++;") out.dec out.puts(s"} while (!(${expression(untilExpr)}));") } @@ -293,10 +362,10 @@ class JavaScriptCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) override def handleAssignmentTempVar(dataType: DataType, id: String, expr: String): Unit = out.puts(s"var $id = $expr;") - override def parseExpr(dataType: DataType, io: String): String = { + override def parseExpr(dataType: DataType, assignType: DataType, io: String, defEndian: Option[FixedEndian]): String = { dataType match { case t: ReadableType => - s"$io.read${Utils.capitalize(t.apiCall)}()" + s"$io.read${Utils.capitalize(t.apiCall(defEndian))}()" case blt: BytesLimitType => s"$io.readBytes(${expression(blt.size)})" case _: BytesEosType => @@ -308,8 +377,18 @@ class JavaScriptCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) case BitsType(width: Int) => s"$io.readBitsInt($width)" case t: UserType => - val addArgs = if (t.isOpaque) "" else ", this, this._root" - s"new ${type2class(t.name.last)}($io$addArgs)" + val parent = t.forcedParent match { + case Some(USER_TYPE_NO_PARENT) => "null" + case Some(fp) => translator.translate(fp) + case None => "this" + } + val root = if (t.isOpaque) "null" else "this._root" + val addEndian = t.classSpec.get.meta.endian match { + case Some(InheritedEndian) => ", this._is_le" + case _ => "" + } + val addParams = Utils.join(t.args.map((a) => translator.translate(a)), ", ", ", ", "") + s"new ${type2class(t.name.last)}($io, $parent, $root$addEndian$addParams)" } } @@ -329,28 +408,88 @@ class JavaScriptCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) out.puts(s"$id._read();") } - override def switchStart(id: Identifier, on: Ast.expr): Unit = - out.puts(s"switch (${expression(on)}) {") + /** + * Designates switch mode. If false, we're doing real switch-case for this + * attribute. If true, we're doing if-based emulation. + */ + var switchIfs = false + + val NAME_SWITCH_ON = Ast.expr.Name(Ast.identifier(Identifier.SWITCH_ON)) + + override def switchStart(id: Identifier, on: Ast.expr): Unit = { + val onType = translator.detectType(on) + typeProvider._currentSwitchType = Some(onType) + + // Determine switching mode for this construct based on type + switchIfs = onType match { + case _: IntType | _: BooleanType | _: EnumType | _: StrType => false + case _ => true + } + + if (switchIfs) { + out.puts("{") + out.inc + out.puts(s"var ${expression(NAME_SWITCH_ON)} = ${expression(on)};") + } else { + out.puts(s"switch (${expression(on)}) {") + } + } + + def switchCmpExpr(condition: Ast.expr): String = + expression( + Ast.expr.Compare( + NAME_SWITCH_ON, + Ast.cmpop.Eq, + condition + ) + ) + + override def switchCaseFirstStart(condition: Ast.expr): Unit = { + if (switchIfs) { + out.puts(s"if (${switchCmpExpr(condition)}) {") + out.inc + } else { + switchCaseStart(condition) + } + } override def switchCaseStart(condition: Ast.expr): Unit = { - out.puts(s"case ${expression(condition)}:") - out.inc + if (switchIfs) { + out.puts(s"else if (${switchCmpExpr(condition)}) {") + out.inc + } else { + out.puts(s"case ${expression(condition)}:") + out.inc + } } override def switchCaseEnd(): Unit = { - out.puts("break;") - out.dec + if (switchIfs) { + out.dec + out.puts("}") + } else { + out.puts("break;") + out.dec + } } override def switchElseStart(): Unit = { - out.puts("default:") - out.inc + if (switchIfs) { + out.puts("else {") + out.inc + } else { + out.puts("default:") + out.inc + } } - override def switchEnd(): Unit = + override def switchEnd(): Unit = { + if (switchIfs) + out.dec out.puts("}") + } - override def instanceHeader(className: List[String], instName: InstanceIdentifier, dataType: DataType): Unit = { + override def instanceHeader(className: List[String], instName: InstanceIdentifier, dataType: DataType, isNullable: Boolean): Unit = { out.puts(s"Object.defineProperty(${type2class(className.last)}.prototype, '${publicMemberName(instName)}', {") out.inc out.puts("get: function() {") @@ -375,16 +514,26 @@ class JavaScriptCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) out.puts(s"return ${privateMemberName(instName)};") } - override def enumDeclaration(curClass: List[String], enumName: String, enumColl: Seq[(Long, String)]): Unit = { + override def enumDeclaration(curClass: List[String], enumName: String, enumColl: Seq[(Long, EnumValueSpec)]): Unit = { out.puts(s"${type2class(curClass.last)}.${type2class(enumName)} = Object.freeze({") out.inc + + // Name to ID mapping enumColl.foreach { case (id, label) => - out.puts(s"${enumValue(enumName, label)}: $id,") + out.puts(s"${enumValue(enumName, label.name)}: $id,") } out.puts + + // ID to name mapping enumColl.foreach { case (id, label) => - out.puts(s"""$id: "${enumValue(enumName, label)}",""") + val idStr = if (id < 0) { + "\"" + id.toString + "\"" + } else { + id.toString + } + out.puts(s"""$idStr: "${enumValue(enumName, label.name)}",""") } + out.dec out.puts("});") out.puts @@ -416,6 +565,8 @@ class JavaScriptCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) } } + override def localTemporaryName(id: Identifier): String = s"_t_${idToStr(id)}" + private def attrDebugNeeded(attrId: Identifier) = attrId match { case _: NamedIdentifier | _: NumberedIdentifier | _: InstanceIdentifier => true @@ -436,7 +587,6 @@ class JavaScriptCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) object JavaScriptCompiler extends LanguageCompilerStatic with UpperCamelCaseClasses with StreamStructNames { - override def getTranslator(tp: TypeProvider, config: RuntimeConfig) = new JavaScriptTranslator(tp) override def getCompiler( tp: ClassTypeProvider, config: RuntimeConfig diff --git a/shared/src/main/scala/io/kaitai/struct/languages/LuaCompiler.scala b/shared/src/main/scala/io/kaitai/struct/languages/LuaCompiler.scala new file mode 100644 index 000000000..ebba1a88c --- /dev/null +++ b/shared/src/main/scala/io/kaitai/struct/languages/LuaCompiler.scala @@ -0,0 +1,407 @@ +package io.kaitai.struct.languages + +import io.kaitai.struct.{ClassTypeProvider, RuntimeConfig, Utils} +import io.kaitai.struct.datatype.{DataType, FixedEndian, InheritedEndian} +import io.kaitai.struct.datatype.DataType._ +import io.kaitai.struct.exprlang.Ast +import io.kaitai.struct.format._ +import io.kaitai.struct.languages.components._ +import io.kaitai.struct.translators.LuaTranslator + +class LuaCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) + extends LanguageCompiler(typeProvider, config) + with AllocateIOLocalVar + with EveryReadIsExpression + with FixedContentsUsingArrayByteLiteral + with ObjectOrientedLanguage + with SingleOutputFile + with UniversalDoc + with UniversalFooter + with UpperCamelCaseClasses { + + import LuaCompiler._ + + override val translator = new LuaTranslator(typeProvider, importList) + + override def innerClasses = false + override def innerEnums = true + + override def indent: String = " " + override def outFileName(topClassName: String): String = s"$topClassName.lua" + override def outImports(topClass: ClassSpec) = + importList.toList.mkString("", "\n", "\n") + + override def opaqueClassDeclaration(classSpec: ClassSpec): Unit = + out.puts("require(\"" + classSpec.name.head + "\")") + + override def fileHeader(topClassName: String): Unit = { + outHeader.puts(s"-- $headerComment") + outHeader.puts("--") + outHeader.puts("-- This file is compatible with Lua 5.3") + outHeader.puts + + importList.add("local class = require(\"class\")") + importList.add("require(\"kaitaistruct\")") + + out.puts + } + + override def universalFooter: Unit = + out.puts + + override def universalDoc(doc: DocSpec): Unit = { + val docStr = doc.summary match { + case Some(summary) => + val lastChar = summary.last + if (lastChar == '.' || lastChar == '\n') { + summary + } else { + summary + "." + } + case None => + "" + } + val extraNewLine = if (docStr.isEmpty || docStr.last == '\n') "" else "\n" + val refStr = doc.ref match { + case TextRef(text) => + s"See also: $text" + case UrlRef(url, text) => + s"See also: $text ($url)" + case NoRef => + "" + } + + out.putsLines("-- ", "\n" + docStr + extraNewLine + refStr) + } + + override def classHeader(name: List[String]): Unit = { + out.puts(s"${types2class(name)} = class.class($kstructName)") + out.puts + } + override def classFooter(name: List[String]): Unit = + universalFooter + override def classConstructorHeader(name: List[String], parentType: DataType, rootClassName: List[String], isHybrid: Boolean, params: List[ParamDefSpec]): Unit = { + val endianAdd = if (isHybrid) ", is_le" else "" + val paramsList = Utils.join(params.map((p) => paramName(p.id)), "", ", ", ", ") + + out.puts(s"function ${types2class(name)}:_init($paramsList" + s"io, parent, root$endianAdd)") + out.inc + out.puts(s"$kstructName._init(self, io)") + out.puts("self._parent = parent") + out.puts("self._root = root or self") + if (isHybrid) + out.puts("self._is_le = is_le") + + // Store parameters passed to us + params.foreach((p) => handleAssignmentSimple(p.id, paramName(p.id))) + } + override def classConstructorFooter: Unit = { + out.dec + out.puts("end") + out.puts + } + + override def runRead(): Unit = + out.puts("self:_read()") + override def runReadCalc(): Unit = { + out.puts + out.puts(s"if self._is_le then") + out.inc + out.puts("self:_read_le()") + out.dec + out.puts(s"elseif not self._is_le then") + out.inc + out.puts("self:_read_be()") + out.dec + out.puts("else") + out.inc + out.puts("error(\"unable to decide endianness\")") + out.dec + out.puts("end") + } + override def readHeader(endian: Option[FixedEndian], isEmpty: Boolean): Unit = { + val suffix = endian match { + case Some(e) => s"_${e.toSuffix}" + case None => "" + } + + out.puts(s"function ${types2class(typeProvider.nowClass.name)}:_read$suffix()") + out.inc + } + override def readFooter(): Unit = { + out.dec + out.puts("end") + out.puts + } + + override def attributeDeclaration(attrName: Identifier, attrType: DataType, isNullable: Boolean): Unit = + {} + override def attributeReader(attrName: Identifier, attrType: DataType, isNullable: Boolean): Unit = + {} + + override def attrParseHybrid(leProc: () => Unit, beProc: () => Unit): Unit = { + out.puts("if self._is_le then") + out.inc + leProc() + out.dec + out.puts("else") + out.inc + beProc() + out.dec + out.puts("end") + } + + override def attrFixedContentsParse(attrName: Identifier, contents: String): Unit = + out.puts(s"${privateMemberName(attrName)} = self._io:ensure_fixed_contents($contents)") + + override def condIfHeader(expr: Ast.expr): Unit = { + out.puts(s"if ${expression(expr)} then") + out.inc + } + override def condIfFooter(expr: Ast.expr): Unit = { + out.dec + out.puts("end") + } + + override def condRepeatEosHeader(id: Identifier, io: String, dataType: DataType, needRaw: Boolean): Unit = { + if (needRaw) + out.puts(s"${privateMemberName(RawIdentifier(id))} = {}") + out.puts(s"${privateMemberName(id)} = {}") + out.puts("local i = 1") + out.puts(s"while not $io:is_eof() do") + out.inc + } + override def condRepeatEosFooter: Unit = { + out.puts("i = i + 1") + out.dec + out.puts("end") + } + + override def condRepeatExprHeader(id: Identifier, io: String, dataType: DataType, needRaw: Boolean, repeatExpr: Ast.expr): Unit = { + if (needRaw) + out.puts(s"${privateMemberName(RawIdentifier(id))} = {}") + out.puts(s"${privateMemberName(id)} = {}") + out.puts(s"for i = 1, ${expression(repeatExpr)} do") + out.inc + } + override def condRepeatExprFooter: Unit = { + out.dec + out.puts("end") + } + + override def condRepeatUntilHeader(id: Identifier, io: String, datatype: DataType, needRaw: Boolean, repeatExpr: Ast.expr): Unit = { + if (needRaw) + out.puts(s"${privateMemberName(RawIdentifier(id))} = {}") + out.puts(s"${privateMemberName(id)} = {}") + out.puts("local i = 1") + out.puts("while true do") + out.inc + } + override def condRepeatUntilFooter(id: Identifier, io: String, dataType: DataType, needRaw: Boolean, untilExpr: Ast.expr): Unit = { + typeProvider._currentIteratorType = Some(dataType) + out.puts(s"if ${expression(untilExpr)} then") + out.inc + out.puts("break") + out.dec + out.puts("end") + out.puts("i = i + 1") + out.dec + out.puts("end") + out.dec + } + + override def attrProcess(proc: ProcessExpr, varSrc: Identifier, varDest: Identifier): Unit = { + val srcName = privateMemberName(varSrc) + val destName = privateMemberName(varDest) + + proc match { + case ProcessXor(xorValue) => + val procName = translator.detectType(xorValue) match { + case _: IntType => "process_xor_one" + case _: BytesType => "process_xor_many" + } + out.puts(s"$destName = $kstreamName.$procName($srcName, ${expression(xorValue)})") + case ProcessZlib => + throw new RuntimeException("Lua zlib not supported") + case ProcessRotate(isLeft, rotValue) => + val expr = if (isLeft) { + expression(rotValue) + } else { + s"8 - (${expression(rotValue)})" + } + out.puts(s"$destName = $kstreamName.process_rotate_left($srcName, $expr, 1)") + case ProcessCustom(name, args) => + val procName = s"_process_${idToStr(varSrc)}" + + importList.add("require(\"" + s"${name.last}" + "\")") + + out.puts(s"local $procName = ${types2class(name)}(${args.map(expression).mkString(", ")})") + out.puts(s"$destName = $procName:decode($srcName)") + } + } + + override def useIO(ioEx: Ast.expr): String = { + out.puts(s"local _io = ${expression(ioEx)}") + "_io" + } + override def pushPos(io:String): Unit = + out.puts(s"local _pos = $io:pos()") + override def seek(io: String, pos: Ast.expr): Unit = + out.puts(s"$io:seek(${expression(pos)})") + override def popPos(io: String): Unit = + out.puts(s"$io:seek(_pos)") + override def alignToByte(io: String): Unit = + out.puts(s"$io:align_to_byte()") + + override def instanceHeader(className: List[String], instName: InstanceIdentifier, dataType: DataType, isNullable: Boolean): Unit = { + out.puts(s"${types2class(className)}.property.${publicMemberName(instName)} = {}") + out.puts(s"function ${types2class(className)}.property.${publicMemberName(instName)}:get()") + out.inc + } + override def instanceFooter: Unit = { + out.dec + out.puts("end") + out.puts + } + override def instanceCheckCacheAndReturn(instName: InstanceIdentifier): Unit = { + out.puts(s"if self.${idToStr(instName)} ~= nil then") + out.inc + instanceReturn(instName) + out.dec + out.puts("end") + out.puts + } + override def instanceReturn(instName: InstanceIdentifier): Unit = + out.puts(s"return ${privateMemberName(instName)}") + + override def enumDeclaration(curClass: List[String], enumName: String, enumColl: Seq[(Long, EnumValueSpec)]): Unit = { + importList.add("local enum = require(\"enum\")") + + out.puts(s"${types2class(curClass)}.${type2class(enumName)} = enum.Enum {") + out.inc + enumColl.foreach { case (id, label) => out.puts(s"${label.name} = $id,") } + out.dec + out.puts("}") + out.puts + } + + override def idToStr(id: Identifier): String = id match { + case SpecialIdentifier(name) => name + case NamedIdentifier(name) => name + case NumberedIdentifier(idx) => s"_${NumberedIdentifier.TEMPLATE}$idx" + case InstanceIdentifier(name) => s"_m_$name" + case RawIdentifier(innerId) => s"_raw_${idToStr(innerId)}" + } + override def privateMemberName(id: Identifier): String = + s"self.${idToStr(id)}" + override def publicMemberName(id: Identifier): String = id match { + case SpecialIdentifier(name) => name + case NamedIdentifier(name) => name + case InstanceIdentifier(name) => name + case RawIdentifier(innerId) => s"_raw_${publicMemberName(innerId)}" + } + override def localTemporaryName(id: Identifier): String = + s"_t_${idToStr(id)}" + + override def handleAssignmentRepeatEos(id: Identifier, expr: String): Unit = + out.puts(s"${privateMemberName(id)}[i] = $expr") + override def handleAssignmentRepeatExpr(id: Identifier, expr: String): Unit = + out.puts(s"${privateMemberName(id)}[i] = $expr") + override def handleAssignmentRepeatUntil(id: Identifier, expr: String, isRaw: Boolean): Unit = { + val tmpName = translator.doName(if (isRaw) Identifier.ITERATOR2 else Identifier.ITERATOR) + out.puts(s"$tmpName = $expr") + out.puts(s"${privateMemberName(id)}[i] = $tmpName") + } + override def handleAssignmentSimple(id: Identifier, expr: String): Unit = + out.puts(s"${privateMemberName(id)} = $expr") + + override def parseExpr(dataType: DataType, assignType: DataType, io: String, defEndian: Option[FixedEndian]): String = dataType match { + case t: ReadableType => + s"$io:read_${t.apiCall(defEndian)}()" + case blt: BytesLimitType => + s"$io:read_bytes(${expression(blt.size)})" + case _: BytesEosType => + s"$io:read_bytes_full()" + case BytesTerminatedType(terminator, include, consume, eosError, _) => + s"$io:read_bytes_term($terminator, $include, $consume, $eosError)" + case BitsType1 => + s"$io:read_bits_int(1)" + case BitsType(width: Int) => + s"$io:read_bits_int($width)" + case t: UserType => + val addParams = Utils.join(t.args.map((a) => translator.translate(a)), "", ", ", ", ") + val addArgs = if (t.isOpaque) { + "" + } else { + val parent = t.forcedParent match { + case Some(fp) => translator.translate(fp) + case None => "self" + } + val addEndian = t.classSpec.get.meta.endian match { + case Some(InheritedEndian) => ", self._is_le" + case _ => "" + } + s", $parent, self._root$addEndian" + } + s"${types2class(t.classSpec.get.name)}($addParams$io$addArgs)" + } + override def bytesPadTermExpr(expr0: String, padRight: Option[Int], terminator: Option[Int], include: Boolean): String = { + val expr1 = padRight match { + case Some(padByte) => s"$kstreamName.bytes_strip_right($expr0, $padByte)" + case None => expr0 + } + val expr2 = terminator match { + case Some(term) => s"$kstreamName.bytes_terminate($expr1, $term, $include)" + case None => expr1 + } + expr2 + } + + override def switchStart(id: Identifier, on: Ast.expr): Unit = + out.puts(s"local _on = ${expression(on)}") + override def switchCaseFirstStart(condition: Ast.expr): Unit = { + out.puts(s"if _on == ${expression(condition)} then") + out.inc + } + override def switchCaseStart(condition: Ast.expr): Unit = { + out.puts(s"elseif _on == ${expression(condition)} then") + out.inc + } + override def switchCaseEnd(): Unit = + out.dec + override def switchElseStart(): Unit = { + out.puts("else") + out.inc + } + override def switchEnd(): Unit = + out.puts("end") + + override def allocateIO(varName: Identifier, rep: RepeatSpec): String = { + val varStr = privateMemberName(varName) + + val args = rep match { + case RepeatEos | RepeatUntil(_) => s"$varStr[#$varStr]" + case RepeatExpr(_) => s"$varStr[i]" + case NoRepeat => varStr + } + + importList.add("local stringstream = require(\"string_stream\")") + out.puts(s"local _io = $kstreamName(stringstream($args))") + "_io" + } +} + +object LuaCompiler extends LanguageCompilerStatic + with UpperCamelCaseClasses + with StreamStructNames { + override def getCompiler( + tp: ClassTypeProvider, + config: RuntimeConfig + ): LanguageCompiler = new LuaCompiler(tp, config) + + override def kstructName: String = "KaitaiStruct" + override def kstreamName: String = "KaitaiStream" + + def types2class(name: List[String]): String = + name.map(x => type2class(x)).mkString(".") +} diff --git a/shared/src/main/scala/io/kaitai/struct/languages/PHPCompiler.scala b/shared/src/main/scala/io/kaitai/struct/languages/PHPCompiler.scala index a5894f6ce..2bef260ad 100644 --- a/shared/src/main/scala/io/kaitai/struct/languages/PHPCompiler.scala +++ b/shared/src/main/scala/io/kaitai/struct/languages/PHPCompiler.scala @@ -1,12 +1,12 @@ package io.kaitai.struct.languages -import io.kaitai.struct.datatype.DataType import io.kaitai.struct.datatype.DataType._ +import io.kaitai.struct.datatype.{CalcEndian, DataType, FixedEndian, InheritedEndian} import io.kaitai.struct.exprlang.Ast import io.kaitai.struct.format.{NoRepeat, RepeatEos, RepeatExpr, RepeatSpec, _} import io.kaitai.struct.languages.components._ -import io.kaitai.struct.translators.{PHPTranslator, TypeProvider} -import io.kaitai.struct.{ClassTypeProvider, LanguageOutputWriter, RuntimeConfig, Utils} +import io.kaitai.struct.translators.PHPTranslator +import io.kaitai.struct.{ClassTypeProvider, RuntimeConfig, Utils} class PHPCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) extends LanguageCompiler(typeProvider, config) @@ -25,8 +25,6 @@ class PHPCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) override def innerEnums = false - override def getStatic = PHPCompiler - override val translator: PHPTranslator = new PHPTranslator(typeProvider, config) override def universalFooter: Unit = { @@ -69,25 +67,80 @@ class PHPCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) override def classFooter(name: List[String]): Unit = universalFooter - override def classConstructorHeader(name: List[String], parentClassName: List[String], rootClassName: List[String]): Unit = { - out.puts + override def classConstructorHeader(name: List[String], parentType: DataType, rootClassName: List[String], isHybrid: Boolean, params: List[ParamDefSpec]): Unit = { + typeProvider.nowClass.meta.endian match { + case Some(_: CalcEndian) | Some(InheritedEndian) => + out.puts("protected $_m__is_le;") + out.puts + case _ => + // no _is_le variable + } + + val endianAdd = if (isHybrid) ", $is_le = null" else "" + + val paramsArg = Utils.join(params.map((p) => + s"${kaitaiType2NativeType(p.dataType)} ${paramName(p.id)}" + ), "", ", ", ", ") + + // Parameter names + val pIo = paramName(IoIdentifier) + val pParent = paramName(ParentIdentifier) + val pRoot = paramName(RootIdentifier) + + // Types + val tIo = kstreamName + val tParent = kaitaiType2NativeType(parentType) + val tRoot = translator.types2classAbs(rootClassName) + out.puts( - "public function __construct(" + - kstreamName + " $io, " + - translator.types2classAbs(parentClassName) + " $parent = null, " + - translator.types2classAbs(rootClassName) + " $root = null) {" + s"public function __construct($paramsArg" + + s"$tIo $pIo, " + + s"$tParent $pParent = null, " + + s"$tRoot $pRoot = null" + endianAdd + ") {" ) out.inc - out.puts("parent::__construct($io, $parent, $root);") - out.puts("$this->_parse();") + out.puts(s"parent::__construct($pIo, $pParent, $pRoot);") + + if (isHybrid) + handleAssignmentSimple(EndianIdentifier, "$is_le") + + // Store parameters passed to us + params.foreach((p) => handleAssignmentSimple(p.id, paramName(p.id))) + } + + override def runRead(): Unit = + out.puts("$this->_read();") + + override def runReadCalc(): Unit = { + out.puts + out.puts("if (is_null($this->_m__is_le)) {") + out.inc + out.puts("throw new \\RuntimeException(\"Unable to decide on endianness\");") + out.dec + out.puts("} else if ($this->_m__is_le) {") + out.inc + out.puts("$this->_readLE();") + out.dec + out.puts("} else {") + out.inc + out.puts("$this->_readBE();") out.dec out.puts("}") + } - out.puts("private function _parse() {") + override def readHeader(endian: Option[FixedEndian], isEmpty: Boolean) = { + val suffix = endian match { + case Some(e) => s"${e.toSuffix.toUpperCase}" + case None => "" + } + out.puts + out.puts(s"private function _read$suffix() {") out.inc } - override def attributeDeclaration(attrName: Identifier, attrType: DataType, condSpec: ConditionalSpec): Unit = { + override def readFooter(): Unit = universalFooter + + override def attributeDeclaration(attrName: Identifier, attrType: DataType, isNullable: Boolean): Unit = { attrName match { case ParentIdentifier | RootIdentifier | IoIdentifier => // just ignore it for now @@ -96,7 +149,7 @@ class PHPCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) } } - override def attributeReader(attrName: Identifier, attrType: DataType, condSpec: ConditionalSpec): Unit = { + override def attributeReader(attrName: Identifier, attrType: DataType, isNullable: Boolean): Unit = { attrName match { case ParentIdentifier | RootIdentifier => // just ignore it for now @@ -106,10 +159,24 @@ class PHPCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) } override def universalDoc(doc: DocSpec): Unit = { - out.puts - out.puts( "/**") - doc.summary.foreach((summary) => out.putsLines(" * ", summary)) - out.puts( " */") + if (doc.summary.isDefined) { + out.puts + out.puts("/**") + doc.summary.foreach((summary) => out.putsLines(" * ", summary)) + out.puts(" */") + } + } + + override def attrParseHybrid(leProc: () => Unit, beProc: () => Unit): Unit = { + out.puts("if ($this->_m__is_le) {") + out.inc + leProc() + out.dec + out.puts("} else {") + out.inc + beProc() + out.dec + out.puts("}") } override def attrFixedContentsParse(attrName: Identifier, contents: String): Unit = @@ -135,6 +202,13 @@ class PHPCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) s"8 - (${expression(rotValue)})" } out.puts(s"$destName = $kstreamName::processRotateLeft($srcName, $expr, 1);") + case ProcessCustom(name, args) => + val isAbsolute = name.length > 1 + val procClass = name.map((x) => type2class(x)).mkString( + if (isAbsolute) "\\" else "", "\\", "" + ) + out.puts(s"$$_process = new $procClass(${args.map(expression).mkString(", ")});") + out.puts(s"$destName = $$_process->decode($srcName);") } } @@ -177,6 +251,7 @@ class PHPCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) if (needRaw) out.puts(s"${privateMemberName(RawIdentifier(id))} = [];") out.puts(s"${privateMemberName(id)} = [];") + out.puts("$i = 0;") out.puts(s"while (!$io->isEof()) {") out.inc } @@ -185,6 +260,11 @@ class PHPCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) out.puts(s"${privateMemberName(id)}[] = $expr;") } + override def condRepeatEosFooter: Unit = { + out.puts("$i++;") + super.condRepeatEosFooter + } + override def condRepeatExprHeader(id: Identifier, io: String, dataType: DataType, needRaw: Boolean, repeatExpr: Ast.expr): Unit = { if (needRaw) out.puts(s"${privateMemberName(RawIdentifier(id))} = [];") @@ -202,6 +282,7 @@ class PHPCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) if (needRaw) out.puts(s"${privateMemberName(RawIdentifier(id))} = [];") out.puts(s"${privateMemberName(id)} = [];") + out.puts("$i = 0;") out.puts("do {") out.inc } @@ -214,6 +295,7 @@ class PHPCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) override def condRepeatUntilFooter(id: Identifier, io: String, dataType: DataType, needRaw: Boolean, untilExpr: Ast.expr): Unit = { typeProvider._currentIteratorType = Some(dataType) + out.puts("$i++;") out.dec out.puts(s"} while (!(${expression(untilExpr)}));") } @@ -222,10 +304,10 @@ class PHPCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) out.puts(s"${privateMemberName(id)} = $expr;") } - override def parseExpr(dataType: DataType, io: String): String = { + override def parseExpr(dataType: DataType, assignType: DataType, io: String, defEndian: Option[FixedEndian]): String = { dataType match { case t: ReadableType => - s"$io->read${Utils.capitalize(t.apiCall)}()" + s"$io->read${Utils.capitalize(t.apiCall(defEndian))}()" case blt: BytesLimitType => s"$io->readBytes(${expression(blt.size)})" case _: BytesEosType => @@ -237,6 +319,7 @@ class PHPCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) case BitsType(width: Int) => s"$io->readBitsInt($width)" case t: UserType => + val addParams = Utils.join(t.args.map((a) => translator.translate(a)), "", ", ", ", ") val addArgs = if (t.isOpaque) { "" } else { @@ -245,9 +328,13 @@ class PHPCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) case Some(fp) => translator.translate(fp) case None => "$this" } - s", $parent, ${privateMemberName(RootIdentifier)}" + val addEndian = t.classSpec.get.meta.endian match { + case Some(InheritedEndian) => s", ${privateMemberName(EndianIdentifier)}" + case _ => "" + } + s", $parent, ${privateMemberName(RootIdentifier)}$addEndian" } - s"new ${translator.types2classAbs(t.classSpec.get.name)}($io$addArgs)" + s"new ${translator.types2classAbs(t.classSpec.get.name)}($addParams$io$addArgs)" } } @@ -287,7 +374,7 @@ class PHPCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) override def switchEnd(): Unit = universalFooter - override def instanceHeader(className: List[String], instName: InstanceIdentifier, dataType: DataType): Unit = { + override def instanceHeader(className: List[String], instName: InstanceIdentifier, dataType: DataType, isNullable: Boolean): Unit = { out.puts(s"public function ${idToStr(instName)}() {") out.inc } @@ -303,10 +390,11 @@ class PHPCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) out.puts(s"return ${privateMemberName(instName)};") } - override def enumDeclaration(curClass: List[String], enumName: String, enumColl: Seq[(Long, String)]): Unit = { + override def enumDeclaration(curClass: List[String], enumName: String, enumColl: Seq[(Long, EnumValueSpec)]): Unit = { classHeader(curClass ::: List(enumName), None) enumColl.foreach { case (id, label) => - out.puts(s"const ${value2Const(label)} = $id;") + universalDoc(label.doc) + out.puts(s"const ${value2Const(label.name)} = $id;") } universalFooter } @@ -334,6 +422,10 @@ class PHPCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) override def publicMemberName(id: Identifier) = idToStr(id) + override def localTemporaryName(id: Identifier): String = s"$$_t_${idToStr(id)}" + + override def paramName(id: Identifier): String = s"$$${idToStr(id)}" + /** * Determine PHP data type corresponding to a KS data type. Currently unused due to * problems with nullable types (which were introduced only in PHP 7.1). @@ -350,10 +442,16 @@ class PHPCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) case _: StrType | _: BytesType => "string" - case t: UserType => translator.types2classAbs(t.classSpec.get.name) + case t: UserType => translator.types2classAbs(t.classSpec match { + case Some(cs) => cs.name + case None => t.name + }) case t: EnumType => "int" case ArrayType(_) => "array" + + case KaitaiStructType => kstructName + case KaitaiStreamType => kstreamName } } } @@ -361,7 +459,6 @@ class PHPCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) object PHPCompiler extends LanguageCompilerStatic with StreamStructNames with UpperCamelCaseClasses { - override def getTranslator(tp: TypeProvider, config: RuntimeConfig): PHPTranslator = new PHPTranslator(tp, config) override def getCompiler( tp: ClassTypeProvider, config: RuntimeConfig diff --git a/shared/src/main/scala/io/kaitai/struct/languages/PerlCompiler.scala b/shared/src/main/scala/io/kaitai/struct/languages/PerlCompiler.scala index 9e9131ed3..fdf020569 100644 --- a/shared/src/main/scala/io/kaitai/struct/languages/PerlCompiler.scala +++ b/shared/src/main/scala/io/kaitai/struct/languages/PerlCompiler.scala @@ -1,6 +1,6 @@ package io.kaitai.struct.languages -import io.kaitai.struct.datatype.DataType +import io.kaitai.struct.datatype.{DataType, FixedEndian, InheritedEndian} import io.kaitai.struct.datatype.DataType._ import io.kaitai.struct.exprlang.Ast import io.kaitai.struct.exprlang.Ast.expr @@ -20,9 +20,9 @@ class PerlCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) import PerlCompiler._ - override def innerClasses = false + override val translator = new PerlTranslator(typeProvider, importList) - override def getStatic: LanguageCompilerStatic = PerlCompiler + override def innerClasses = false override def universalFooter: Unit = { out.dec @@ -32,14 +32,16 @@ class PerlCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) override def indent: String = " " override def outFileName(topClassName: String): String = s"${type2class(topClassName)}.pm" + override def outImports(topClass: ClassSpec) = + importList.toList.map((x) => s"use $x;").mkString("", "\n", "\n") + override def fileHeader(topClassName: String): Unit = { - out.puts(s"# $headerComment") - out.puts - out.puts("use strict;") - out.puts("use warnings;") - out.puts(s"use $packageName ${KSVersion.minimalRuntime.toPerlVersion};") - out.puts("use Compress::Zlib;") - out.puts("use Encode;") + outHeader.puts(s"# $headerComment") + outHeader.puts + + importList.add("strict") + importList.add("warnings") + importList.add(s"$packageName ${KSVersion.minimalRuntime.toPerlVersion}") } override def fileFooter(topClassName: String): Unit = { @@ -70,16 +72,22 @@ class PerlCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) override def classFooter(name: List[String]): Unit = {} - override def classConstructorHeader(name: List[String], parentClassName: List[String], rootClassName: List[String]): Unit = { + override def classConstructorHeader(name: List[String], parentType: DataType, rootClassName: List[String], isHybrid: Boolean, params: List[ParamDefSpec]): Unit = { + val endianSuffix = if (isHybrid) ", $_is_le" else "" + out.puts out.puts("sub new {") out.inc - out.puts("my ($class, $_io, $_parent, $_root) = @_;") + out.puts("my ($class, $_io, $_parent, $_root" + endianSuffix + ") = @_;") out.puts(s"my $$self = $kstructName->new($$_io);") out.puts out.puts("bless $self, $class;") - out.puts(s"${privateMemberName(ParentIdentifier)} = $$_parent;") - out.puts(s"${privateMemberName(RootIdentifier)} = $$_root || $$self;") + handleAssignmentSimple(ParentIdentifier, "$_parent") + handleAssignmentSimple(RootIdentifier, "$_root || $self;") + + if (isHybrid) + handleAssignmentSimple(EndianIdentifier, "$_is_le") + out.puts } @@ -89,9 +97,44 @@ class PerlCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) universalFooter } - override def attributeDeclaration(attrName: Identifier, attrType: DataType, condSpec: ConditionalSpec): Unit = {} + override def runRead(): Unit = + out.puts("$self->_read();") + + override def runReadCalc(): Unit = { + val isLe = privateMemberName(EndianIdentifier) + + out.puts(s"if (!(defined $isLe)) {") + out.inc + out.puts("die \"Unable to decide on endianness\";") + out.dec + out.puts(s"} elsif ($isLe) {") + out.inc + out.puts("$self->_read_le();") + out.dec + out.puts("} else {") + out.inc + out.puts("$self->_read_be();") + out.dec + out.puts("}") + } + + override def readHeader(endian: Option[FixedEndian], isEmpty: Boolean): Unit = { + val suffix = endian match { + case Some(e) => s"_${e.toSuffix}" + case None => "" + } + out.puts + out.puts(s"sub _read$suffix {") + out.inc + out.puts("my ($self) = @_;") + out.puts + } + + override def readFooter(): Unit = universalFooter + + override def attributeDeclaration(attrName: Identifier, attrType: DataType, isNullable: Boolean): Unit = {} - override def attributeReader(attrName: Identifier, attrType: DataType, condSpec: ConditionalSpec): Unit = { + override def attributeReader(attrName: Identifier, attrType: DataType, isNullable: Boolean): Unit = { attrName match { case RootIdentifier | ParentIdentifier => // ignore, they are already defined in KaitaiStruct class @@ -107,6 +150,18 @@ class PerlCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) } } + override def attrParseHybrid(leProc: () => Unit, beProc: () => Unit): Unit = { + out.puts(s"if (${privateMemberName(EndianIdentifier)}) {") + out.inc + leProc() + out.dec + out.puts("} else {") + out.inc + beProc() + out.dec + out.puts("}") + } + override def attrFixedContentsParse(attrName: Identifier, contents: String): Unit = { out.puts(s"${privateMemberName(attrName)} = $normalIO->ensure_fixed_contents($contents);") } @@ -123,6 +178,7 @@ class PerlCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) } s"$destName = $kstreamName::$procName($srcName, ${expression(xorValue)});" case ProcessZlib => + importList.add("Compress::Zlib") s"$destName = Compress::Zlib::uncompress($srcName);" case ProcessRotate(isLeft, rotValue) => val expr = if (isLeft) { @@ -223,10 +279,10 @@ class PerlCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) override def handleAssignmentSimple(id: Identifier, expr: String): Unit = out.puts(s"${privateMemberName(id)} = $expr;") - override def parseExpr(dataType: DataType, io: String): String = { + override def parseExpr(dataType: DataType, assignType: DataType, io: String, defEndian: Option[FixedEndian]): String = { dataType match { case t: ReadableType => - s"$io->read_${t.apiCall}()" + s"$io->read_${t.apiCall(defEndian)}()" case blt: BytesLimitType => s"$io->read_bytes(${expression(blt.size)})" case _: BytesEosType => @@ -245,7 +301,11 @@ class PerlCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) case Some(fp) => translator.translate(fp) case None => "$self" } - s", $parent, ${privateMemberName(RootIdentifier)}" + val addEndian = t.classSpec.get.meta.endian match { + case Some(InheritedEndian) => s", ${privateMemberName(EndianIdentifier)}" + case _ => "" + } + s", $parent, ${privateMemberName(RootIdentifier)}$addEndian" } s"${types2class(t.classSpec.get.name)}->new($io$addArgs)" } @@ -296,7 +356,7 @@ class PerlCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) def onComparisonExpr(condition: Ast.expr) = Ast.expr.Compare(Ast.expr.Name(Ast.identifier("_on")), Ast.cmpop.Eq, condition) - override def instanceHeader(className: List[String], instName: InstanceIdentifier, dataType: DataType): Unit = { + override def instanceHeader(className: List[String], instName: InstanceIdentifier, dataType: DataType, isNullable: Boolean): Unit = { out.puts out.puts(s"sub ${instName.name} {") out.inc @@ -311,11 +371,11 @@ class PerlCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) out.puts(s"return ${privateMemberName(instName)};") } - override def enumDeclaration(curClass: List[String], enumName: String, enumColl: Seq[(Long, String)]): Unit = { + override def enumDeclaration(curClass: List[String], enumName: String, enumColl: Seq[(Long, EnumValueSpec)]): Unit = { out.puts enumColl.foreach { case (id, label) => - out.puts(s"our ${enumValue(enumName, label)} = $id;") + out.puts(s"our ${enumValue(enumName, label.name)} = $id;") } } @@ -335,6 +395,8 @@ class PerlCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) override def publicMemberName(id: Identifier): String = idToStr(id) + override def localTemporaryName(id: Identifier): String = s"$$_t_${idToStr(id)}" + def boolLiteral(b: Boolean): String = translator.doBoolLiteral(b) def types2class(t: List[String]) = t.map(type2class).mkString("::") @@ -343,7 +405,6 @@ class PerlCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) object PerlCompiler extends LanguageCompilerStatic with UpperCamelCaseClasses with StreamStructNames { - override def getTranslator(tp: TypeProvider, config: RuntimeConfig) = new PerlTranslator(tp) override def getCompiler( tp: ClassTypeProvider, config: RuntimeConfig diff --git a/shared/src/main/scala/io/kaitai/struct/languages/PythonCompiler.scala b/shared/src/main/scala/io/kaitai/struct/languages/PythonCompiler.scala index e74fdef3f..6026981fe 100644 --- a/shared/src/main/scala/io/kaitai/struct/languages/PythonCompiler.scala +++ b/shared/src/main/scala/io/kaitai/struct/languages/PythonCompiler.scala @@ -1,13 +1,13 @@ package io.kaitai.struct.languages +import io.kaitai.struct.datatype.{DataType, FixedEndian, InheritedEndian} +import io.kaitai.struct.datatype.DataType._ import io.kaitai.struct.exprlang.Ast import io.kaitai.struct.exprlang.Ast.expr -import io.kaitai.struct.datatype.DataType -import io.kaitai.struct.datatype.DataType._ import io.kaitai.struct.format._ import io.kaitai.struct.languages.components._ -import io.kaitai.struct.translators.{PythonTranslator, TypeProvider} -import io.kaitai.struct.{ClassTypeProvider, LanguageOutputWriter, RuntimeConfig} +import io.kaitai.struct.translators.PythonTranslator +import io.kaitai.struct.{ClassTypeProvider, RuntimeConfig, StringLanguageOutputWriter, Utils} class PythonCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) extends LanguageCompiler(typeProvider, config) @@ -18,11 +18,14 @@ class PythonCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) with EveryReadIsExpression with AllocateIOLocalVar with FixedContentsUsingArrayByteLiteral + with UniversalDoc with NoNeedForFullClassPath { import PythonCompiler._ - override def getStatic = PythonCompiler + override val translator = new PythonTranslator(typeProvider, importList) + + override def innerDocstrings = true override def universalFooter: Unit = { out.dec @@ -32,16 +35,16 @@ class PythonCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) override def indent: String = " " override def outFileName(topClassName: String): String = s"$topClassName.py" + override def outImports(topClass: ClassSpec) = + importList.toList.mkString("", "\n", "\n") + override def fileHeader(topClassName: String): Unit = { - out.puts(s"# $headerComment") - out.puts - out.puts("import array") - out.puts("import struct") - out.puts("import zlib") - out.puts("from enum import Enum") - out.puts("from pkg_resources import parse_version") - out.puts - out.puts(s"from kaitaistruct import __version__ as ks_version, $kstructName, $kstreamName, BytesIO") + outHeader.puts(s"# $headerComment") + outHeader.puts + + importList.add("from pkg_resources import parse_version") + importList.add(s"from kaitaistruct import __version__ as ks_version, $kstructName, $kstreamName, BytesIO") + out.puts out.puts @@ -63,7 +66,12 @@ class PythonCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) override def opaqueClassDeclaration(classSpec: ClassSpec): Unit = { val name = classSpec.name.head - out.puts(s"from $name import ${type2class(name)}") + val prefix = config.pythonPackage match { + case "" => "" + case "." => "." + case pkg => s"$pkg." + } + out.puts(s"from $prefix$name import ${type2class(name)}") } override def classHeader(name: String): Unit = { @@ -71,38 +79,140 @@ class PythonCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) out.inc } - override def classConstructorHeader(name: String, parentClassName: String, rootClassName: String): Unit = { - out.puts("def __init__(self, _io, _parent=None, _root=None):") + override def classConstructorHeader(name: String, parentType: DataType, rootClassName: String, isHybrid: Boolean, params: List[ParamDefSpec]): Unit = { + val endianAdd = if (isHybrid) ", _is_le=None" else "" + val paramsList = Utils.join(params.map((p) => paramName(p.id)), ", ", ", ", "") + + out.puts(s"def __init__(self$paramsList, _io, _parent=None, _root=None$endianAdd):") out.inc out.puts("self._io = _io") out.puts("self._parent = _parent") out.puts("self._root = _root if _root else self") + + if (isHybrid) + out.puts("self._is_le = _is_le") + + // Store parameters passed to us + params.foreach((p) => handleAssignmentSimple(p.id, paramName(p.id))) + } + + override def runRead(): Unit = { + out.puts("self._read()") + } + + override def runReadCalc(): Unit = { + out.puts + out.puts(s"if self._is_le == True:") + out.inc + out.puts("self._read_le()") + out.dec + out.puts("elif self._is_le == False:") + out.inc + out.puts("self._read_be()") + out.dec + out.puts("else:") + out.inc + //out.puts(s"raise $kstreamName.UndecidedEndiannessError") + out.puts("raise Exception(\"Unable to decide endianness\")") + out.dec + } + + override def readHeader(endian: Option[FixedEndian], isEmpty: Boolean): Unit = { + val suffix = endian match { + case Some(e) => s"_${e.toSuffix}" + case None => "" + } + out.puts(s"def _read$suffix(self):") + out.inc + if (isEmpty) + out.puts("pass") } - override def attributeDeclaration(attrName: Identifier, attrType: DataType, condSpec: ConditionalSpec): Unit = {} + override def readFooter() = universalFooter + + override def attributeDeclaration(attrName: Identifier, attrType: DataType, isNullable: Boolean): Unit = {} + + override def attributeReader(attrName: Identifier, attrType: DataType, isNullable: Boolean): Unit = {} - override def attributeReader(attrName: Identifier, attrType: DataType, condSpec: ConditionalSpec): Unit = {} + override def universalDoc(doc: DocSpec): Unit = { + val docStr = doc.summary match { + case Some(summary) => + val lastChar = summary.last + if (lastChar == '.' || lastChar == '\n') { + summary + } else { + summary + "." + } + case None => + "" + } + + val extraNewline = if (docStr.isEmpty || docStr.last == '\n') "" else "\n" + val refStr = doc.ref match { + case TextRef(text) => + val seeAlso = new StringLanguageOutputWriter("") + seeAlso.putsLines(" ", text) + s"$extraNewline\n.. seealso::\n${seeAlso.result}" + case ref: UrlRef => + val seeAlso = new StringLanguageOutputWriter("") + seeAlso.putsLines(" ", s"${ref.text} - ${ref.url}") + s"$extraNewline\n.. seealso::\n${seeAlso.result}" + case NoRef => + "" + } + + out.putsLines("", "\"\"\"" + docStr + refStr + "\"\"\"") + } override def attrFixedContentsParse(attrName: Identifier, contents: String): Unit = out.puts(s"${privateMemberName(attrName)} = self._io.ensure_fixed_contents($contents)") + override def attrParseHybrid(leProc: () => Unit, beProc: () => Unit): Unit = { + out.puts("if self._is_le:") + out.inc + leProc() + out.dec + out.puts("else:") + out.inc + beProc() + out.dec + } + override def attrProcess(proc: ProcessExpr, varSrc: Identifier, varDest: Identifier): Unit = { + val srcName = privateMemberName(varSrc) + val destName = privateMemberName(varDest) + proc match { case ProcessXor(xorValue) => val procName = translator.detectType(xorValue) match { case _: IntType => "process_xor_one" case _: BytesType => "process_xor_many" } - out.puts(s"${privateMemberName(varDest)} = $kstreamName.$procName(${privateMemberName(varSrc)}, ${expression(xorValue)})") + out.puts(s"$destName = $kstreamName.$procName($srcName, ${expression(xorValue)})") case ProcessZlib => - out.puts(s"${privateMemberName(varDest)} = zlib.decompress(${privateMemberName(varSrc)})") + importList.add("import zlib") + out.puts(s"$destName = zlib.decompress($srcName)") case ProcessRotate(isLeft, rotValue) => val expr = if (isLeft) { expression(rotValue) } else { s"8 - (${expression(rotValue)})" } - out.puts(s"${privateMemberName(varDest)} = $kstreamName.process_rotate_left(${privateMemberName(varSrc)}, $expr, 1)") + out.puts(s"$destName = $kstreamName.process_rotate_left($srcName, $expr, 1)") + case ProcessCustom(name, args) => + val procClass = if (name.length == 1) { + val onlyName = name.head + val className = type2class(onlyName) + importList.add(s"from $onlyName import $className") + className + } else { + val pkgName = name.init.mkString(".") + importList.add(s"import $pkgName") + s"$pkgName.${type2class(name.last)}" + } + + out.puts(s"_process = $procClass(${args.map(expression).mkString(", ")})") + out.puts(s"$destName = _process.decode($srcName)") } } @@ -147,11 +257,16 @@ class PythonCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) if (needRaw) out.puts(s"${privateMemberName(RawIdentifier(id))} = []") out.puts(s"${privateMemberName(id)} = []") + out.puts("i = 0") out.puts(s"while not $io.is_eof():") out.inc } override def handleAssignmentRepeatEos(id: Identifier, expr: String): Unit = out.puts(s"${privateMemberName(id)}.append($expr)") + override def condRepeatEosFooter: Unit = { + out.puts("i += 1") + universalFooter + } override def condRepeatExprHeader(id: Identifier, io: String, dataType: DataType, needRaw: Boolean, repeatExpr: expr): Unit = { if (needRaw) @@ -167,6 +282,7 @@ class PythonCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) if (needRaw) out.puts(s"${privateMemberName(RawIdentifier(id))} = []") out.puts(s"${privateMemberName(id)} = []") + out.puts("i = 0") out.puts("while True:") out.inc } @@ -183,16 +299,17 @@ class PythonCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) out.inc out.puts("break") out.dec + out.puts("i += 1") out.dec } override def handleAssignmentSimple(id: Identifier, expr: String): Unit = out.puts(s"${privateMemberName(id)} = $expr") - override def parseExpr(dataType: DataType, io: String): String = { + override def parseExpr(dataType: DataType, assignType: DataType, io: String, defEndian: Option[FixedEndian]): String = { dataType match { case t: ReadableType => - s"$io.read_${t.apiCall}()" + s"$io.read_${t.apiCall(defEndian)}()" case blt: BytesLimitType => s"$io.read_bytes(${expression(blt.size)})" case _: BytesEosType => @@ -204,6 +321,7 @@ class PythonCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) case BitsType(width: Int) => s"$io.read_bits_int($width)" case t: UserType => + val addParams = Utils.join(t.args.map((a) => translator.translate(a)), "", ", ", ", ") val addArgs = if (t.isOpaque) { "" } else { @@ -211,9 +329,13 @@ class PythonCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) case Some(fp) => translator.translate(fp) case None => "self" } - s", $parent, self._root" + val addEndian = t.classSpec.get.meta.endian match { + case Some(InheritedEndian) => ", self._is_le" + case _ => "" + } + s", $parent, self._root$addEndian" } - s"${types2class(t.classSpec.get.name)}($io$addArgs)" + s"${types2class(t.classSpec.get.name)}($addParams$io$addArgs)" } } @@ -253,7 +375,7 @@ class PythonCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) override def switchEnd(): Unit = {} - override def instanceHeader(className: String, instName: InstanceIdentifier, dataType: DataType): Unit = { + override def instanceHeader(className: String, instName: InstanceIdentifier, dataType: DataType, isNullable: Boolean): Unit = { out.puts("@property") out.puts(s"def ${publicMemberName(instName)}(self):") out.inc @@ -274,6 +396,8 @@ class PythonCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) } override def enumDeclaration(curClass: String, enumName: String, enumColl: Seq[(Long, String)]): Unit = { + importList.add("from enum import Enum") + out.puts out.puts(s"class ${type2class(enumName)}(Enum):") out.inc @@ -303,12 +427,13 @@ class PythonCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) case RawIdentifier(innerId) => s"_raw_${publicMemberName(innerId)}" } } + + override def localTemporaryName(id: Identifier): String = s"_t_${idToStr(id)}" } object PythonCompiler extends LanguageCompilerStatic with UpperCamelCaseClasses with StreamStructNames { - override def getTranslator(tp: TypeProvider, config: RuntimeConfig) = new PythonTranslator(tp) override def getCompiler( tp: ClassTypeProvider, config: RuntimeConfig diff --git a/shared/src/main/scala/io/kaitai/struct/languages/RubyCompiler.scala b/shared/src/main/scala/io/kaitai/struct/languages/RubyCompiler.scala index 05bac2789..1e588698a 100644 --- a/shared/src/main/scala/io/kaitai/struct/languages/RubyCompiler.scala +++ b/shared/src/main/scala/io/kaitai/struct/languages/RubyCompiler.scala @@ -1,13 +1,13 @@ package io.kaitai.struct.languages +import io.kaitai.struct.datatype.DataType._ +import io.kaitai.struct.datatype._ import io.kaitai.struct.exprlang.Ast import io.kaitai.struct.exprlang.Ast.expr -import io.kaitai.struct.datatype.DataType -import io.kaitai.struct.datatype.DataType._ import io.kaitai.struct.format._ import io.kaitai.struct.languages.components._ -import io.kaitai.struct.translators.{RubyTranslator, TypeProvider} -import io.kaitai.struct.{ClassTypeProvider, LanguageOutputWriter, RuntimeConfig} +import io.kaitai.struct.translators.RubyTranslator +import io.kaitai.struct.{ClassTypeProvider, RuntimeConfig, Utils} class RubyCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) extends LanguageCompiler(typeProvider, config) @@ -23,7 +23,7 @@ class RubyCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) import RubyCompiler._ - override def getStatic = RubyCompiler + val translator = new RubyTranslator(typeProvider) override def universalFooter: Unit = { out.dec @@ -33,11 +33,15 @@ class RubyCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) override def outFileName(topClassName: String): String = s"$topClassName.rb" override def indent: String = " " + override def outImports(topClass: ClassSpec) = + importList.toList.map((x) => s"require '$x'").mkString("\n") + "\n" + override def fileHeader(topClassName: String): Unit = { - out.puts(s"# $headerComment") - out.puts - out.puts("require 'kaitai/struct/struct'") - out.puts("require 'zlib'") // TODO: add only if actually used + outHeader.puts(s"# $headerComment") + outHeader.puts + + importList.add("kaitai/struct/struct") + out.puts // API compatibility check @@ -64,33 +68,78 @@ class RubyCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) out.puts("attr_reader :_debug") } - override def classConstructorHeader(name: String, parentClassName: String, rootClassName: String): Unit = { - out.puts("def initialize(_io, _parent = nil, _root = self)") + override def classConstructorHeader(name: String, parentType: DataType, rootClassName: String, isHybrid: Boolean, params: List[ParamDefSpec]): Unit = { + val endianSuffix = if (isHybrid) { + ", _is_le = nil" + } else { + "" + } + + val paramsList = Utils.join(params.map((p) => paramName(p.id)), ", ", ", ", "") + + out.puts(s"def initialize(_io, _parent = nil, _root = self$endianSuffix$paramsList)") out.inc out.puts("super(_io, _parent, _root)") + + if (isHybrid) { + out.puts("@_is_le = _is_le") + } + + // Store parameters passed to us + params.foreach((p) => handleAssignmentSimple(p.id, paramName(p.id))) + if (debug) { out.puts("@_debug = {}") - out.dec - out.puts("end") - out.puts - out.puts("def _read") - out.inc } } - override def classConstructorFooter: Unit = { - if (debug) { - // Actually, it's not constructor in debug mode, but a "_read" method. Make sure it returns an instance of the - // class, just as normal Foo.new call does. - out.puts - out.puts("self") + override def runRead(): Unit = { + out.puts("_read") + } + + override def runReadCalc(): Unit = { + out.puts + out.puts(s"if @_is_le == true") + out.inc + out.puts("_read_le") + out.dec + out.puts("elsif @_is_le == false") + out.inc + out.puts("_read_be") + out.dec + out.puts("else") + out.inc + out.puts("raise Kaitai::Struct::Stream::UndecidedEndiannessError") + out.dec + out.puts("end") + } + + override def readHeader(endian: Option[FixedEndian], isEmpty: Boolean) = { + val suffix = endian match { + case Some(e) => s"_${e.toSuffix}" + case None => "" } + out.puts + out.puts(s"def _read$suffix") + out.inc + } + + override def readFooter() = { + // This is required for debug mode to be able to do stuff like: + // + // obj = Obj.new(...)._read + // + // i.e. drop-in replacement of non-debug mode invocation: + // + // obj = Obj.new(...) + out.puts("self") + universalFooter } - override def attributeDeclaration(attrName: Identifier, attrType: DataType, condSpec: ConditionalSpec): Unit = {} + override def attributeDeclaration(attrName: Identifier, attrType: DataType, isNullable: Boolean): Unit = {} - override def attributeReader(attrName: Identifier, attrType: DataType, condSpec: ConditionalSpec): Unit = { + override def attributeReader(attrName: Identifier, attrType: DataType, isNullable: Boolean): Unit = { attrName match { case RootIdentifier | ParentIdentifier => // ignore, they are already added in Kaitai::Struct::Struct @@ -115,6 +164,18 @@ class RubyCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) } } + override def attrParseHybrid(leProc: () => Unit, beProc: () => Unit): Unit = { + out.puts("if @_is_le") + out.inc + leProc() + out.dec + out.puts("else") + out.inc + beProc() + out.dec + out.puts("end") + } + override def attrFixedContentsParse(attrName: Identifier, contents: String): Unit = out.puts(s"${privateMemberName(attrName)} = $normalIO.ensure_fixed_contents($contents)") @@ -130,6 +191,7 @@ class RubyCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) } s"$destName = $kstreamName::$procName($srcName, ${expression(xorValue)})" case ProcessZlib => + importList.add("zlib") s"$destName = Zlib::Inflate.inflate($srcName)" case ProcessRotate(isLeft, rotValue) => val expr = if (isLeft) { @@ -138,6 +200,10 @@ class RubyCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) s"8 - (${expression(rotValue)})" } s"$destName = $kstreamName::process_rotate_left($srcName, $expr, 1)" + case ProcessCustom(name, args) => + val procClass = name.map((x) => type2class(x)).mkString("::") + out.puts(s"_process = $procClass.new(${args.map(expression).mkString(", ")})") + s"$destName = _process.decode($srcName)" }) } @@ -213,11 +279,16 @@ class RubyCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) out.puts(s"${privateMemberName(RawIdentifier(id))} = []") out.puts(s"${privateMemberName(id)} = []") + out.puts("i = 0") out.puts(s"while not $io.eof?") out.inc } override def handleAssignmentRepeatEos(id: Identifier, expr: String): Unit = out.puts(s"${privateMemberName(id)} << $expr") + override def condRepeatEosFooter: Unit = { + out.puts("i += 1") + super.condRepeatEosFooter + } override def condRepeatExprHeader(id: Identifier, io: String, dataType: DataType, needRaw: Boolean, repeatExpr: expr): Unit = { if (needRaw) @@ -237,6 +308,7 @@ class RubyCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) if (needRaw) out.puts(s"${privateMemberName(RawIdentifier(id))} = []") out.puts(s"${privateMemberName(id)} = []") + out.puts("i = 0") out.puts("begin") out.inc } @@ -249,6 +321,7 @@ class RubyCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) override def condRepeatUntilFooter(id: Identifier, io: String, dataType: DataType, needRaw: Boolean, untilExpr: expr): Unit = { typeProvider._currentIteratorType = Some(dataType) + out.puts("i += 1") out.dec out.puts(s"end until ${expression(untilExpr)}") } @@ -259,10 +332,10 @@ class RubyCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) override def handleAssignmentTempVar(dataType: DataType, id: String, expr: String): Unit = out.puts(s"$id = $expr") - override def parseExpr(dataType: DataType, io: String): String = { + override def parseExpr(dataType: DataType, assignType: DataType, io: String, defEndian: Option[FixedEndian]): String = { dataType match { case t: ReadableType => - s"$io.read_${t.apiCall}" + s"$io.read_${t.apiCall(defEndian)}" case blt: BytesLimitType => s"$io.read_bytes(${expression(blt.size)})" case _: BytesEosType => @@ -274,6 +347,7 @@ class RubyCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) case BitsType(width: Int) => s"$io.read_bits_int($width)" case t: UserType => + val addParams = Utils.join(t.args.map((a) => translator.translate(a)), ", ", ", ", "") val addArgs = if (t.isOpaque) { "" } else { @@ -281,9 +355,13 @@ class RubyCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) case Some(fp) => translator.translate(fp) case None => "self" } - s", $parent, @_root" + val addEndian = t.classSpec.get.meta.endian match { + case Some(InheritedEndian) => ", @_is_le" + case _ => "" + } + s", $parent, @_root$addEndian" } - s"${type2class(t.name.last)}.new($io$addArgs)" + s"${type2class(t.name.last)}.new($io$addArgs$addParams)" } } @@ -321,7 +399,7 @@ class RubyCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) override def switchEnd(): Unit = out.puts("end") - override def instanceHeader(className: String, instName: InstanceIdentifier, dataType: DataType): Unit = { + override def instanceHeader(className: String, instName: InstanceIdentifier, dataType: DataType, isNullable: Boolean): Unit = { out.puts(s"def ${instName.name}") out.inc } @@ -372,11 +450,12 @@ class RubyCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig) override def privateMemberName(id: Identifier): String = s"@${idToStr(id)}" override def publicMemberName(id: Identifier): String = idToStr(id) + + override def localTemporaryName(id: Identifier): String = s"_t_${idToStr(id)}" } object RubyCompiler extends LanguageCompilerStatic with StreamStructNames { - override def getTranslator(tp: TypeProvider, config: RuntimeConfig) = new RubyTranslator(tp) override def getCompiler( tp: ClassTypeProvider, config: RuntimeConfig diff --git a/shared/src/main/scala/io/kaitai/struct/languages/components/AllocateAndStoreIO.scala b/shared/src/main/scala/io/kaitai/struct/languages/components/AllocateAndStoreIO.scala index bb216d6c5..cac6a7fdc 100644 --- a/shared/src/main/scala/io/kaitai/struct/languages/components/AllocateAndStoreIO.scala +++ b/shared/src/main/scala/io/kaitai/struct/languages/components/AllocateAndStoreIO.scala @@ -1,6 +1,8 @@ package io.kaitai.struct.languages.components -import io.kaitai.struct.format.{Identifier, IoStorageIdentifier, RepeatSpec} +import io.kaitai.struct.format.{AttrSpec, Identifier, RepeatSpec} + +import scala.collection.mutable.ListBuffer /** * Allocates new IO and returns attribute identifier that it will be stored @@ -8,5 +10,5 @@ import io.kaitai.struct.format.{Identifier, IoStorageIdentifier, RepeatSpec} * keep track of allocated IOs. */ trait AllocateAndStoreIO { - def allocateIO(varName: Identifier, rep: RepeatSpec): IoStorageIdentifier + def allocateIO(id: Identifier, rep: RepeatSpec, extraAttrs: ListBuffer[AttrSpec]): String } diff --git a/shared/src/main/scala/io/kaitai/struct/languages/components/CommonReads.scala b/shared/src/main/scala/io/kaitai/struct/languages/components/CommonReads.scala new file mode 100644 index 000000000..350d1c091 --- /dev/null +++ b/shared/src/main/scala/io/kaitai/struct/languages/components/CommonReads.scala @@ -0,0 +1,91 @@ +package io.kaitai.struct.languages.components + +import io.kaitai.struct.datatype._ +import io.kaitai.struct.datatype.DataType.{SwitchType, UserTypeFromBytes} +import io.kaitai.struct.exprlang.Ast +import io.kaitai.struct.format._ + +import scala.collection.mutable.ListBuffer + +trait CommonReads extends LanguageCompiler { + override def attrParse(attr: AttrLikeSpec, id: Identifier, extraAttrs: ListBuffer[AttrSpec], defEndian: Option[Endianness]): Unit = { + attrParseIfHeader(id, attr.cond.ifExpr) + + // Manage IO & seeking for ParseInstances + val io = attr match { + case pis: ParseInstanceSpec => + val io = pis.io match { + case None => normalIO + case Some(ex) => useIO(ex) + } + pis.pos.foreach { pos => + pushPos(io) + seek(io, pos) + } + io + case _ => + // no seeking required for sequence attributes + normalIO + } + + if (debug) + attrDebugStart(id, attr.dataType, Some(io), NoRepeat) + + defEndian match { + case Some(_: CalcEndian) | Some(InheritedEndian) => + attrParseHybrid( + () => attrParse0(id, attr, io, extraAttrs, Some(LittleEndian)), + () => attrParse0(id, attr, io, extraAttrs, Some(BigEndian)) + ) + case None => + attrParse0(id, attr, io, extraAttrs, None) + case Some(fe: FixedEndian) => + attrParse0(id, attr, io, extraAttrs, Some(fe)) + } + + if (debug) + attrDebugEnd(id, attr.dataType, io, NoRepeat) + + // More position management after parsing for ParseInstanceSpecs + attr match { + case pis: ParseInstanceSpec => + if (pis.pos.isDefined) + popPos(io) + case _ => // no seeking required for sequence attributes + } + + attrParseIfFooter(attr.cond.ifExpr) + } + + def attrParse0(id: Identifier, attr: AttrLikeSpec, io: String, extraAttrs: ListBuffer[AttrSpec], defEndian: Option[FixedEndian]): Unit = { + attr.cond.repeat match { + case RepeatEos => + condRepeatEosHeader(id, io, attr.dataType, needRaw(attr.dataType)) + attrParse2(id, attr.dataType, io, extraAttrs, attr.cond.repeat, false, defEndian) + condRepeatEosFooter + case RepeatExpr(repeatExpr: Ast.expr) => + condRepeatExprHeader(id, io, attr.dataType, needRaw(attr.dataType), repeatExpr) + attrParse2(id, attr.dataType, io, extraAttrs, attr.cond.repeat, false, defEndian) + condRepeatExprFooter + case RepeatUntil(untilExpr: Ast.expr) => + condRepeatUntilHeader(id, io, attr.dataType, needRaw(attr.dataType), untilExpr) + attrParse2(id, attr.dataType, io, extraAttrs, attr.cond.repeat, false, defEndian) + condRepeatUntilFooter(id, io, attr.dataType, needRaw(attr.dataType), untilExpr) + case NoRepeat => + attrParse2(id, attr.dataType, io, extraAttrs, attr.cond.repeat, false, defEndian) + } + } + + def attrParse2(id: Identifier, dataType: DataType, io: String, extraAttrs: ListBuffer[AttrSpec], rep: RepeatSpec, isRaw: Boolean, defEndian: Option[FixedEndian], assignType: Option[DataType] = None): Unit + + def needRaw(dataType: DataType): Boolean = { + dataType match { + case _: UserTypeFromBytes => true + case st: SwitchType => st.hasSize + case _ => false + } + } + + def attrDebugStart(attrName: Identifier, attrType: DataType, io: Option[String], repeat: RepeatSpec): Unit = {} + def attrDebugEnd(attrName: Identifier, attrType: DataType, io: String, repeat: RepeatSpec): Unit = {} +} diff --git a/shared/src/main/scala/io/kaitai/struct/languages/components/EveryReadIsExpression.scala b/shared/src/main/scala/io/kaitai/struct/languages/components/EveryReadIsExpression.scala index 9ad3f56cd..fcee72026 100644 --- a/shared/src/main/scala/io/kaitai/struct/languages/components/EveryReadIsExpression.scala +++ b/shared/src/main/scala/io/kaitai/struct/languages/components/EveryReadIsExpression.scala @@ -1,10 +1,11 @@ package io.kaitai.struct.languages.components import io.kaitai.struct.Utils -import io.kaitai.struct.exprlang.Ast -import io.kaitai.struct.datatype.DataType import io.kaitai.struct.datatype.DataType._ +import io.kaitai.struct.datatype.{DataType, FixedEndian} +import io.kaitai.struct.exprlang.Ast import io.kaitai.struct.format._ +import io.kaitai.struct.translators.BaseTranslator import scala.collection.mutable.ListBuffer @@ -13,69 +14,25 @@ import scala.collection.mutable.ListBuffer * rvalue. In these languages, "attrStdTypeParse" is replaced with higher-level API: "stdTypeParseExpr" and * "handleAssignment". */ -trait EveryReadIsExpression extends LanguageCompiler with ObjectOrientedLanguage { - override def attrParse(attr: AttrLikeSpec, id: Identifier, extraAttrs: ListBuffer[AttrSpec]): Unit = { - attrParseIfHeader(id, attr.cond.ifExpr) - - // Manage IO & seeking for ParseInstances - val io = attr match { - case pis: ParseInstanceSpec => - val io = pis.io match { - case None => normalIO - case Some(ex) => useIO(ex) - } - pis.pos.foreach { pos => - pushPos(io) - seek(io, pos) - } - io - case _ => - // no seeking required for sequence attributes - normalIO - } - - if (debug) - attrDebugStart(id, attr.dataType, Some(io), NoRepeat) - - attr.cond.repeat match { - case RepeatEos => - condRepeatEosHeader(id, io, attr.dataType, needRaw(attr.dataType)) - attrParse2(id, attr.dataType, io, extraAttrs, attr.cond.repeat, false) - condRepeatEosFooter - case RepeatExpr(repeatExpr: Ast.expr) => - condRepeatExprHeader(id, io, attr.dataType, needRaw(attr.dataType), repeatExpr) - attrParse2(id, attr.dataType, io, extraAttrs, attr.cond.repeat, false) - condRepeatExprFooter - case RepeatUntil(untilExpr: Ast.expr) => - condRepeatUntilHeader(id, io, attr.dataType, needRaw(attr.dataType), untilExpr) - attrParse2(id, attr.dataType, io, extraAttrs, attr.cond.repeat, false) - condRepeatUntilFooter(id, io, attr.dataType, needRaw(attr.dataType), untilExpr) - case NoRepeat => - attrParse2(id, attr.dataType, io, extraAttrs, attr.cond.repeat, false) - } - - if (debug) - attrDebugEnd(id, attr.dataType, io, NoRepeat) - - // More position management after parsing for ParseInstanceSpecs - attr match { - case pis: ParseInstanceSpec => - if (pis.pos.isDefined) - popPos(io) - case _ => // no seeking required for sequence attributes - } - - attrParseIfFooter(attr.cond.ifExpr) - } - - def attrParse2( +trait EveryReadIsExpression + extends LanguageCompiler + with ObjectOrientedLanguage + with CommonReads + with SwitchOps { + val translator: BaseTranslator + + override def attrParse2( id: Identifier, dataType: DataType, io: String, extraAttrs: ListBuffer[AttrSpec], rep: RepeatSpec, - isRaw: Boolean + isRaw: Boolean, + defEndian: Option[FixedEndian], + assignTypeOpt: Option[DataType] = None ): Unit = { + val assignType = assignTypeOpt.getOrElse(dataType) + if (debug && rep != NoRepeat) attrDebugStart(id, dataType, Some(io), rep) @@ -83,19 +40,25 @@ trait EveryReadIsExpression extends LanguageCompiler with ObjectOrientedLanguage case FixedBytesType(c, _) => attrFixedContentsParse(id, c) case t: UserType => - attrUserTypeParse(id, t, io, extraAttrs, rep) + attrUserTypeParse(id, t, io, extraAttrs, rep, defEndian) case t: BytesType => attrBytesTypeParse(id, t, io, extraAttrs, rep, isRaw) - case SwitchType(on, cases) => - attrSwitchTypeParse(id, on, cases, io, extraAttrs, rep) + case st: SwitchType => + val isNullable = if (switchBytesOnlyAsRaw) { + st.isNullableSwitchRaw + } else { + st.isNullable + } + + attrSwitchTypeParse(id, st.on, st.cases, io, extraAttrs, rep, defEndian, isNullable, st.combinedType) case t: StrFromBytesType => val expr = translator.bytesToStr(parseExprBytes(t.bytes, io), Ast.expr.Str(t.encoding)) handleAssignment(id, expr, rep, isRaw) case t: EnumType => - val expr = translator.doEnumById(t.enumSpec.get.name, parseExpr(t.basedOn, io)) + val expr = translator.doEnumById(t.enumSpec.get.name, parseExpr(t.basedOn, t.basedOn, io, defEndian)) handleAssignment(id, expr, rep, isRaw) case _ => - val expr = parseExpr(dataType, io) + val expr = parseExpr(dataType, assignType, io, defEndian) handleAssignment(id, expr, rep, isRaw) } @@ -128,7 +91,7 @@ trait EveryReadIsExpression extends LanguageCompiler with ObjectOrientedLanguage } def parseExprBytes(dataType: BytesType, io: String): String = { - val expr = parseExpr(dataType, io) + val expr = parseExpr(dataType, dataType, io, None) // apply pad stripping and termination dataType match { @@ -141,14 +104,14 @@ trait EveryReadIsExpression extends LanguageCompiler with ObjectOrientedLanguage } } - def attrUserTypeParse(id: Identifier, dataType: UserType, io: String, extraAttrs: ListBuffer[AttrSpec], rep: RepeatSpec): Unit = { + def attrUserTypeParse(id: Identifier, dataType: UserType, io: String, extraAttrs: ListBuffer[AttrSpec], rep: RepeatSpec, defEndian: Option[FixedEndian]): Unit = { val newIO = dataType match { case knownSizeType: UserTypeFromBytes => // we have a fixed buffer, thus we shall create separate IO for it val rawId = RawIdentifier(id) val byteType = knownSizeType.bytes - attrParse2(rawId, byteType, io, extraAttrs, rep, true) + attrParse2(rawId, byteType, io, extraAttrs, rep, true, defEndian) val extraType = rep match { case NoRepeat => byteType @@ -159,9 +122,7 @@ trait EveryReadIsExpression extends LanguageCompiler with ObjectOrientedLanguage this match { case thisStore: AllocateAndStoreIO => - val ourIO = thisStore.allocateIO(rawId, rep) - Utils.addUniqueAttr(extraAttrs, AttrSpec(List(), ourIO, KaitaiStreamType)) - privateMemberName(ourIO) + thisStore.allocateIO(rawId, rep, extraAttrs) case thisLocal: AllocateIOLocalVar => thisLocal.allocateIO(rawId, rep) } @@ -169,7 +130,7 @@ trait EveryReadIsExpression extends LanguageCompiler with ObjectOrientedLanguage // no fixed buffer, just use regular IO io } - val expr = parseExpr(dataType, newIO) + val expr = parseExpr(dataType, dataType, newIO, defEndian) if (!debug) { handleAssignment(id, expr, rep, false) } else { @@ -182,65 +143,45 @@ trait EveryReadIsExpression extends LanguageCompiler with ObjectOrientedLanguage handleAssignmentSimple(id, expr) userTypeDebugRead(privateMemberName(id)) case _ => - val tempVarName = s"_t_${idToStr(id)}" + val tempVarName = localTemporaryName(id) handleAssignmentTempVar(dataType, tempVarName, expr) - handleAssignment(id, tempVarName, rep, false) userTypeDebugRead(tempVarName) + handleAssignment(id, tempVarName, rep, false) } } } - def needRaw(dataType: DataType): Boolean = { - dataType match { - case t: UserTypeFromBytes => true - case _ => false - } - } - - def attrSwitchTypeParse(id: Identifier, on: Ast.expr, cases: Map[Ast.expr, DataType], io: String, extraAttrs: ListBuffer[AttrSpec], rep: RepeatSpec): Unit = { - switchStart(id, on) - - // Pass 1: only normal case clauses - var first = true - - cases.foreach { case (condition, dataType) => - condition match { - case SwitchType.ELSE_CONST => - // skip for now - case _ => - if (first) { - switchCaseFirstStart(condition) - first = false - } else { - switchCaseStart(condition) - } - attrParse2(id, dataType, io, extraAttrs, rep, false) - switchCaseEnd() - } - } - - // Pass 2: else clause, if it is there - cases.foreach { case (condition, dataType) => - condition match { - case SwitchType.ELSE_CONST => - switchElseStart() - if (switchBytesOnlyAsRaw) { - dataType match { - case t: BytesType => - attrParse2(RawIdentifier(id), dataType, io, extraAttrs, rep, false) - case _ => - attrParse2(id, dataType, io, extraAttrs, rep, false) - } - } else { - attrParse2(id, dataType, io, extraAttrs, rep, false) - } - switchElseEnd() - case _ => - // ignore normal case clauses + def attrSwitchTypeParse( + id: Identifier, + on: Ast.expr, + cases: Map[Ast.expr, DataType], + io: String, + extraAttrs: ListBuffer[AttrSpec], + rep: RepeatSpec, + defEndian: Option[FixedEndian], + isNullable: Boolean, + assignType: DataType + ): Unit = { + if (isNullable) + condIfSetNull(id) + + switchCases[DataType](id, on, cases, + (dataType) => { + if (isNullable) + condIfSetNonNull(id) + attrParse2(id, dataType, io, extraAttrs, rep, false, defEndian, Some(assignType)) + }, + (dataType) => if (switchBytesOnlyAsRaw) { + dataType match { + case t: BytesType => + attrParse2(RawIdentifier(id), dataType, io, extraAttrs, rep, false, defEndian, Some(assignType)) + case _ => + attrParse2(id, dataType, io, extraAttrs, rep, false, defEndian, Some(assignType)) + } + } else { + attrParse2(id, dataType, io, extraAttrs, rep, false, defEndian, Some(assignType)) } - } - - switchEnd() + ) } def handleAssignment(id: Identifier, expr: String, rep: RepeatSpec, isRaw: Boolean): Unit = { @@ -252,40 +193,19 @@ trait EveryReadIsExpression extends LanguageCompiler with ObjectOrientedLanguage } } - def attrDebugStart(attrName: Identifier, attrType: DataType, io: Option[String], repeat: RepeatSpec): Unit = {} - def attrDebugEnd(attrName: Identifier, attrType: DataType, io: String, repeat: RepeatSpec): Unit = {} - def handleAssignmentRepeatEos(id: Identifier, expr: String): Unit def handleAssignmentRepeatExpr(id: Identifier, expr: String): Unit def handleAssignmentRepeatUntil(id: Identifier, expr: String, isRaw: Boolean): Unit def handleAssignmentSimple(id: Identifier, expr: String): Unit def handleAssignmentTempVar(dataType: DataType, id: String, expr: String): Unit = ??? - def parseExpr(dataType: DataType, io: String): String + def parseExpr(dataType: DataType, assignType: DataType, io: String, defEndian: Option[FixedEndian]): String def bytesPadTermExpr(expr0: String, padRight: Option[Int], terminator: Option[Int], include: Boolean): String def userTypeDebugRead(id: String): Unit = {} - def instanceCalculate(instName: InstanceIdentifier, dataType: DataType, value: Ast.expr): Unit = { + def instanceCalculate(instName: Identifier, dataType: DataType, value: Ast.expr): Unit = { if (debug) attrDebugStart(instName, dataType, None, NoRepeat) handleAssignmentSimple(instName, expression(value)) } - - def switchStart(id: Identifier, on: Ast.expr): Unit - def switchCaseFirstStart(condition: Ast.expr): Unit = switchCaseStart(condition) - def switchCaseStart(condition: Ast.expr): Unit - def switchCaseEnd(): Unit - def switchElseStart(): Unit - def switchElseEnd(): Unit = switchCaseEnd() - def switchEnd(): Unit - - /** - * Controls parsing of typeless (BytesType) alternative in switch case. If true, - * then target language does not support storing both bytes array and true object - * in the same variable, so we'll use workaround: bytes array will be read as - * _raw_ variable (which would be used anyway for all other cases as well). If - * false (which is default), we'll store *both* true objects and bytes array in - * the same variable. - */ - def switchBytesOnlyAsRaw = false } diff --git a/shared/src/main/scala/io/kaitai/struct/languages/components/ExtraAttrs.scala b/shared/src/main/scala/io/kaitai/struct/languages/components/ExtraAttrs.scala new file mode 100644 index 000000000..a1f9e6f5c --- /dev/null +++ b/shared/src/main/scala/io/kaitai/struct/languages/components/ExtraAttrs.scala @@ -0,0 +1,35 @@ +package io.kaitai.struct.languages.components + +import io.kaitai.struct.datatype.DataType +import io.kaitai.struct.datatype.DataType._ +import io.kaitai.struct.format._ + +/** + * Generates list of extra attributes required to store intermediate / + * virtual stuff for every attribute like: + * + * * buffered raw value byte arrays + * * IO objects (?) + * * unprocessed / postprocessed byte arrays + */ +object ExtraAttrs { + def forAttr(attr: AttrLikeSpec): List[AttrSpec] = + forAttr(attr.id, attr.dataType) + + def forAttr(id: Identifier, dataType: DataType): List[AttrSpec] = { + dataType match { + case bt: BytesType => + bt.process match { + case None => List() + case Some(_) => + val rawId = RawIdentifier(id) + List(AttrSpec(List(), rawId, bt)) + } + case utb: UserTypeFromBytes => + val rawId = RawIdentifier(id) + List(AttrSpec(List(), rawId, utb.bytes)) ++ forAttr(rawId, utb.bytes) + case _ => + List() + } + } +} diff --git a/shared/src/main/scala/io/kaitai/struct/languages/components/FixedContentsUsingArrayByteLiteral.scala b/shared/src/main/scala/io/kaitai/struct/languages/components/FixedContentsUsingArrayByteLiteral.scala index b2ea21b50..c5d4abd9b 100644 --- a/shared/src/main/scala/io/kaitai/struct/languages/components/FixedContentsUsingArrayByteLiteral.scala +++ b/shared/src/main/scala/io/kaitai/struct/languages/components/FixedContentsUsingArrayByteLiteral.scala @@ -1,5 +1,6 @@ package io.kaitai.struct.languages.components +import io.kaitai.struct.exprlang.Ast import io.kaitai.struct.format.Identifier /** @@ -8,6 +9,13 @@ import io.kaitai.struct.format.Identifier */ trait FixedContentsUsingArrayByteLiteral extends LanguageCompiler { def attrFixedContentsParse(attrName: Identifier, contents: Array[Byte]) = - attrFixedContentsParse(attrName, translator.doByteArrayLiteral(contents)) + attrFixedContentsParse( + attrName, + translator.translate( + Ast.expr.List( + contents.map(x => Ast.expr.IntNum(BigInt(x & 0xff))) + ) + ) + ) def attrFixedContentsParse(attrName: Identifier, contents: String): Unit } diff --git a/shared/src/main/scala/io/kaitai/struct/languages/components/GoReads.scala b/shared/src/main/scala/io/kaitai/struct/languages/components/GoReads.scala new file mode 100644 index 000000000..876515960 --- /dev/null +++ b/shared/src/main/scala/io/kaitai/struct/languages/components/GoReads.scala @@ -0,0 +1,109 @@ +package io.kaitai.struct.languages.components + +import io.kaitai.struct.Utils +import io.kaitai.struct.datatype.{BigEndian, DataType, FixedEndian} +import io.kaitai.struct.datatype.DataType._ +import io.kaitai.struct.exprlang.Ast +import io.kaitai.struct.format._ +import io.kaitai.struct.translators.{GoTranslator, TranslatorResult} + +import scala.collection.mutable.ListBuffer + +trait GoReads extends CommonReads with ObjectOrientedLanguage with SwitchOps { + val translator: GoTranslator + + override def attrParse2( + id: Identifier, + dataType: DataType, + io: String, + extraAttrs: ListBuffer[AttrSpec], + rep: RepeatSpec, + isRaw: Boolean, + defEndian: Option[FixedEndian], + assignType: Option[DataType] = None + ): Unit = { + dataType match { + case FixedBytesType(c, _) => + attrFixedContentsParse(id, c) + case t: UserType => + attrUserTypeParse(id, t, io, extraAttrs, rep, defEndian) +// case t: BytesType => +// attrBytesTypeParse(id, t, io, extraAttrs, rep, isRaw) +// case SwitchType(on, cases) => +// attrSwitchTypeParse(id, on, cases, io, extraAttrs, rep) + case t: StrFromBytesType => + val r1 = translator.outVarCheckRes(parseExprBytes(t.bytes, io)) + val expr = translator.bytesToStr(translator.resToStr(r1), Ast.expr.Str(t.encoding)) + handleAssignment(id, expr, rep, isRaw) +// case t: EnumType => +// val expr = translator.doEnumById(t.enumSpec.get.name, parseExpr(t.basedOn, io)) +// handleAssignment(id, expr, rep, isRaw) + case _ => + val expr = parseExpr(dataType, io, defEndian) + val r = translator.outVarCheckRes(expr) + handleAssignment(id, r, rep, isRaw) + } + } + + def parseExprBytes(dataType: BytesType, io: String): String = { + val expr = parseExpr(dataType, io, None) // FIXME +/* + // apply pad stripping and termination + dataType match { + case BytesEosType(terminator, include, padRight, _) => + bytesPadTermExpr(expr, padRight, terminator, include) + case BytesLimitType(_, terminator, include, padRight, _) => + bytesPadTermExpr(expr, padRight, terminator, include) + case _ => + expr + }*/ + expr + } + + def attrUserTypeParse(id: Identifier, dataType: UserType, io: String, extraAttrs: ListBuffer[AttrSpec], rep: RepeatSpec, defEndian: Option[FixedEndian]): Unit = { + val newIO = dataType match { + case knownSizeType: UserTypeFromBytes => + // we have a fixed buffer, thus we shall create separate IO for it + val rawId = RawIdentifier(id) + val byteType = knownSizeType.bytes + + attrParse2(rawId, byteType, io, extraAttrs, rep, true, defEndian) + + val extraType = rep match { + case NoRepeat => byteType + case _ => ArrayType(byteType) + } + + Utils.addUniqueAttr(extraAttrs, AttrSpec(List(), rawId, extraType)) + + this match { + case thisStore: AllocateAndStoreIO => + thisStore.allocateIO(rawId, rep, extraAttrs) + case thisLocal: AllocateIOLocalVar => + thisLocal.allocateIO(rawId, rep) + } + case _: UserTypeInstream => + // no fixed buffer, just use regular IO + io + } + + val expr = translator.userType(dataType, newIO) + handleAssignment(id, expr, rep, false) + } + + def handleAssignment(id: Identifier, expr: TranslatorResult, rep: RepeatSpec, isRaw: Boolean): Unit = { + rep match { + case RepeatEos => handleAssignmentRepeatEos(id, expr) + case RepeatExpr(_) => handleAssignmentRepeatExpr(id, expr) + case RepeatUntil(_) => handleAssignmentRepeatUntil(id, expr, isRaw) + case NoRepeat => handleAssignmentSimple(id, expr) + } + } + + def handleAssignmentRepeatEos(id: Identifier, expr: TranslatorResult): Unit + def handleAssignmentRepeatExpr(id: Identifier, expr: TranslatorResult): Unit + def handleAssignmentRepeatUntil(id: Identifier, expr: TranslatorResult, isRaw: Boolean): Unit + def handleAssignmentSimple(id: Identifier, expr: TranslatorResult): Unit + + def parseExpr(dataType: DataType, io: String, defEndian: Option[FixedEndian]): String +} diff --git a/shared/src/main/scala/io/kaitai/struct/languages/components/LanguageCompiler.scala b/shared/src/main/scala/io/kaitai/struct/languages/components/LanguageCompiler.scala index c2dff3f6a..77ec48c19 100644 --- a/shared/src/main/scala/io/kaitai/struct/languages/components/LanguageCompiler.scala +++ b/shared/src/main/scala/io/kaitai/struct/languages/components/LanguageCompiler.scala @@ -1,9 +1,9 @@ package io.kaitai.struct.languages.components -import io.kaitai.struct.datatype.DataType +import io.kaitai.struct.datatype.{DataType, Endianness, FixedEndian, InheritedEndian} import io.kaitai.struct.exprlang.Ast import io.kaitai.struct.format._ -import io.kaitai.struct.translators.BaseTranslator +import io.kaitai.struct.translators.AbstractTranslator import io.kaitai.struct.{ClassTypeProvider, RuntimeConfig} import scala.collection.mutable.ListBuffer @@ -11,8 +11,9 @@ import scala.collection.mutable.ListBuffer abstract class LanguageCompiler( typeProvider: ClassTypeProvider, config: RuntimeConfig -) { - val translator: BaseTranslator = getStatic.getTranslator(typeProvider, config) +) extends SwitchOps { + + val translator: AbstractTranslator /** * @return compilation results as a map: keys are file names, values are @@ -39,7 +40,16 @@ abstract class LanguageCompiler( */ def innerEnums: Boolean = true - def getStatic: LanguageCompilerStatic + /** + * Determines whether the language needs docstrings to be generated + * inside classes and methods (true, Python-style) or outside them + * (false, JavaDoc-style, majority of other languages). Affects calling + * sequence of rendering methods. + * + * @return true if language needs docstrings to be generated + * inside classes and methods, false otherwise + */ + def innerDocstrings: Boolean = false def debug = config.debug @@ -63,17 +73,23 @@ abstract class LanguageCompiler( def classFooter(name: List[String]): Unit def classForwardDeclaration(name: List[String]): Unit = {} - def classConstructorHeader(name: List[String], parentClassName: List[String], rootClassName: List[String]): Unit + def classConstructorHeader(name: List[String], parentType: DataType, rootClassName: List[String], isHybrid: Boolean, params: List[ParamDefSpec]): Unit def classConstructorFooter: Unit - def classDestructorHeader(name: List[String], parentTypeName: List[String], topClassName: List[String]): Unit = {} + def classDestructorHeader(name: List[String], parentType: DataType, topClassName: List[String]): Unit = {} def classDestructorFooter: Unit = {} - def attributeDeclaration(attrName: Identifier, attrType: DataType, condSpec: ConditionalSpec): Unit - def attributeReader(attrName: Identifier, attrType: DataType, condSpec: ConditionalSpec): Unit + def runRead(): Unit + def runReadCalc(): Unit + def readHeader(endian: Option[FixedEndian], isEmpty: Boolean): Unit + def readFooter(): Unit + + def attributeDeclaration(attrName: Identifier, attrType: DataType, isNullable: Boolean): Unit + def attributeReader(attrName: Identifier, attrType: DataType, isNullable: Boolean): Unit def attributeDoc(id: Identifier, doc: DocSpec): Unit = {} - def attrParse(attr: AttrLikeSpec, id: Identifier, extraAttrs: ListBuffer[AttrSpec]): Unit + def attrParse(attr: AttrLikeSpec, id: Identifier, extraAttrs: ListBuffer[AttrSpec], defEndian: Option[Endianness]): Unit + def attrParseHybrid(leProc: () => Unit, beProc: () => Unit): Unit def attrDestructor(attr: AttrLikeSpec, id: Identifier): Unit = {} def attrFixedContentsParse(attrName: Identifier, contents: Array[Byte]): Unit @@ -103,14 +119,14 @@ abstract class LanguageCompiler( def instanceClear(instName: InstanceIdentifier): Unit = {} def instanceSetCalculated(instName: InstanceIdentifier): Unit = {} - def instanceDeclaration(attrName: InstanceIdentifier, attrType: DataType, condSpec: ConditionalSpec) = attributeDeclaration(attrName, attrType, condSpec) - def instanceHeader(className: List[String], instName: InstanceIdentifier, dataType: DataType): Unit + def instanceDeclaration(attrName: InstanceIdentifier, attrType: DataType, isNullable: Boolean): Unit = attributeDeclaration(attrName, attrType, isNullable) + def instanceHeader(className: List[String], instName: InstanceIdentifier, dataType: DataType, isNullable: Boolean): Unit def instanceFooter: Unit def instanceCheckCacheAndReturn(instName: InstanceIdentifier): Unit def instanceReturn(instName: InstanceIdentifier): Unit - def instanceCalculate(instName: InstanceIdentifier, dataType: DataType, value: Ast.expr) + def instanceCalculate(instName: Identifier, dataType: DataType, value: Ast.expr) - def enumDeclaration(curClass: List[String], enumName: String, enumColl: Seq[(Long, String)]): Unit + def enumDeclaration(curClass: List[String], enumName: String, enumColl: Seq[(Long, EnumValueSpec)]): Unit /** * Outputs class' attributes sequence identifiers as some sort of an ordered sequence, diff --git a/shared/src/main/scala/io/kaitai/struct/languages/components/LanguageCompilerStatic.scala b/shared/src/main/scala/io/kaitai/struct/languages/components/LanguageCompilerStatic.scala index 9ec6e474d..93d0dc023 100644 --- a/shared/src/main/scala/io/kaitai/struct/languages/components/LanguageCompilerStatic.scala +++ b/shared/src/main/scala/io/kaitai/struct/languages/components/LanguageCompilerStatic.scala @@ -6,7 +6,6 @@ import io.kaitai.struct.translators.{BaseTranslator, TypeProvider} trait LanguageCompilerStatic { def getCompiler(tp: ClassTypeProvider, config: RuntimeConfig): LanguageCompiler - def getTranslator(tp: TypeProvider, config: RuntimeConfig): BaseTranslator } object LanguageCompilerStatic { @@ -14,13 +13,17 @@ object LanguageCompilerStatic { "cpp_stl" -> CppCompiler, "csharp" -> CSharpCompiler, "graphviz" -> GraphvizClassCompiler, + "go" -> GoCompiler, "java" -> JavaCompiler, "javascript" -> JavaScriptCompiler, + "lua" -> LuaCompiler, "perl" -> PerlCompiler, "php" -> PHPCompiler, "python" -> PythonCompiler, "ruby" -> RubyCompiler ) + val CLASS_TO_NAME: Map[LanguageCompilerStatic, String] = NAME_TO_CLASS.map(_.swap) + def byString(langName: String): LanguageCompilerStatic = NAME_TO_CLASS(langName) } diff --git a/shared/src/main/scala/io/kaitai/struct/languages/components/NoNeedForFullClassPath.scala b/shared/src/main/scala/io/kaitai/struct/languages/components/NoNeedForFullClassPath.scala index 963f47a20..63be66119 100644 --- a/shared/src/main/scala/io/kaitai/struct/languages/components/NoNeedForFullClassPath.scala +++ b/shared/src/main/scala/io/kaitai/struct/languages/components/NoNeedForFullClassPath.scala @@ -1,7 +1,7 @@ package io.kaitai.struct.languages.components import io.kaitai.struct.datatype.DataType -import io.kaitai.struct.format.InstanceIdentifier +import io.kaitai.struct.format._ trait NoNeedForFullClassPath { def classHeader(name: List[String]): Unit = @@ -12,15 +12,15 @@ trait NoNeedForFullClassPath { classFooter(name.last) def classFooter(name: String): Unit - def classConstructorHeader(name: List[String], parentClassName: List[String], rootClassName: List[String]): Unit = - classConstructorHeader(name.last, parentClassName.last, rootClassName.last) - def classConstructorHeader(name: String, parentClassName: String, rootClassName: String): Unit + def classConstructorHeader(name: List[String], parentType: DataType, rootClassName: List[String], isHybrid: Boolean, params: List[ParamDefSpec]): Unit = + classConstructorHeader(name.last, parentType, rootClassName.last, isHybrid, params) + def classConstructorHeader(name: String, parentType: DataType, rootClassName: String, isHybrid: Boolean, params: List[ParamDefSpec]): Unit - def instanceHeader(className: List[String], instName: InstanceIdentifier, dataType: DataType): Unit = - instanceHeader(className.last, instName, dataType) - def instanceHeader(className: String, instName: InstanceIdentifier, dataType: DataType): Unit + def instanceHeader(className: List[String], instName: InstanceIdentifier, dataType: DataType, isNullable: Boolean): Unit = + instanceHeader(className.last, instName, dataType, isNullable) + def instanceHeader(className: String, instName: InstanceIdentifier, dataType: DataType, isNullable: Boolean): Unit - def enumDeclaration(curClass: List[String], enumName: String, enumColl: Seq[(Long, String)]): Unit = - enumDeclaration(curClass.last, enumName, enumColl) + def enumDeclaration(curClass: List[String], enumName: String, enumColl: Seq[(Long, EnumValueSpec)]): Unit = + enumDeclaration(curClass.last, enumName, enumColl.map((x) => (x._1, x._2.name))) def enumDeclaration(curClass: String, enumName: String, enumColl: Seq[(Long, String)]): Unit } diff --git a/shared/src/main/scala/io/kaitai/struct/languages/components/ObjectOrientedLanguage.scala b/shared/src/main/scala/io/kaitai/struct/languages/components/ObjectOrientedLanguage.scala index 30dcf2da4..02d00c943 100644 --- a/shared/src/main/scala/io/kaitai/struct/languages/components/ObjectOrientedLanguage.scala +++ b/shared/src/main/scala/io/kaitai/struct/languages/components/ObjectOrientedLanguage.scala @@ -39,5 +39,23 @@ trait ObjectOrientedLanguage extends LanguageCompiler { */ def publicMemberName(id: Identifier): String + /** + * Renders identifier as a proper reference to a local temporary + * variable appropriately named to hold a temporary reference to + * this field. + * + * @param id identifier to render + * @return identifier as string + */ + def localTemporaryName(id: Identifier): String + + /** + * Renders identifier as a parameter (method argument) name. + * Default implementation just calls [[idToStr]]. + * @param id + * @return + */ + def paramName(id: Identifier): String = idToStr(id) + override def normalIO: String = privateMemberName(IoIdentifier) } diff --git a/shared/src/main/scala/io/kaitai/struct/languages/components/SingleOutputFile.scala b/shared/src/main/scala/io/kaitai/struct/languages/components/SingleOutputFile.scala index cf154f286..11d4c77ae 100644 --- a/shared/src/main/scala/io/kaitai/struct/languages/components/SingleOutputFile.scala +++ b/shared/src/main/scala/io/kaitai/struct/languages/components/SingleOutputFile.scala @@ -1,14 +1,32 @@ package io.kaitai.struct.languages.components -import io.kaitai.struct.StringLanguageOutputWriter +import io.kaitai.struct.{ImportList, StringLanguageOutputWriter, Utils} import io.kaitai.struct.format.ClassSpec +import scala.collection.mutable.ListBuffer + /** * Common trait for languages that have one output file per ClassSpec. + * This file is considered to be composed of: + * + * * a header + * * imports list + * * output body */ trait SingleOutputFile extends LanguageCompiler { + val outHeader = new StringLanguageOutputWriter(indent) val out = new StringLanguageOutputWriter(indent) override def results(topClass: ClassSpec) = - Map(outFileName(topClass.nameAsStr) -> out.result) + Map(outFileName(topClass.nameAsStr) -> + (outHeader.result + outImports(topClass) + out.result) + ) + + val importList = new ImportList + + /** + * Generates imports clauses in target language format + * @return import + */ + def outImports(topClass: ClassSpec) = "" } diff --git a/shared/src/main/scala/io/kaitai/struct/languages/components/SwitchOps.scala b/shared/src/main/scala/io/kaitai/struct/languages/components/SwitchOps.scala new file mode 100644 index 000000000..df9ce90fa --- /dev/null +++ b/shared/src/main/scala/io/kaitai/struct/languages/components/SwitchOps.scala @@ -0,0 +1,80 @@ +package io.kaitai.struct.languages.components + +import io.kaitai.struct.datatype.DataType.SwitchType +import io.kaitai.struct.exprlang.Ast +import io.kaitai.struct.format.Identifier + +/** + * An interface for switching operations. + */ +trait SwitchOps { + def switchStart(id: Identifier, on: Ast.expr): Unit + def switchCaseFirstStart(condition: Ast.expr): Unit = switchCaseStart(condition) + def switchCaseStart(condition: Ast.expr): Unit + def switchCaseEnd(): Unit + def switchElseStart(): Unit + def switchElseEnd(): Unit = switchCaseEnd() + def switchEnd(): Unit + + /** + * Controls parsing of typeless (BytesType) alternative in switch case. If true, + * then target language does not support storing both bytes array and true object + * in the same variable, so we'll use workaround: bytes array will be read as + * _raw_ variable (which would be used anyway for all other cases as well). If + * false (which is default), we'll store *both* true objects and bytes array in + * the same variable. + */ + def switchBytesOnlyAsRaw = false + + /** + * Generate switch cases by calling case procedures. Suitable for a wide variety of + * target languages that something remotely resembling C-like `switch`-`case` statement. + * Thanks to customizable argument type for case procedures, can be used for switch type + * handling and a variety of other cases (i.e. switching between customizable endianness, + * etc). + * @param id attribute identifier + * @param on on expression to decide upon + * @param cases cases map: keys should be expressions, values are arbitrary typed objects + * that will be passed to case procedures + * @param normalCaseProc procedure that would handle "normal" (i.e. non-else case) + * @param elseCaseProc procedure that would handle "else" case + * @tparam T type of object to pass to procedures + */ + def switchCases[T]( + id: Identifier, + on: Ast.expr, + cases: Map[Ast.expr, T], + normalCaseProc: (T) => Unit, + elseCaseProc: (T) => Unit + ): Unit = { + switchStart(id, on) + + // Pass 1: only normal case clauses + var first = true + + cases.foreach { case (condition, result) => + condition match { + case SwitchType.ELSE_CONST => + // skip for now + case _ => + if (first) { + switchCaseFirstStart(condition) + first = false + } else { + switchCaseStart(condition) + } + normalCaseProc(result) + switchCaseEnd() + } + } + + // Pass 2: else clause, if it is there + cases.get(SwitchType.ELSE_CONST).foreach { (result) => + switchElseStart() + elseCaseProc(result) + switchElseEnd() + } + + switchEnd() + } +} diff --git a/shared/src/main/scala/io/kaitai/struct/precompile/CalculateSeqSizes.scala b/shared/src/main/scala/io/kaitai/struct/precompile/CalculateSeqSizes.scala new file mode 100644 index 000000000..40c586d97 --- /dev/null +++ b/shared/src/main/scala/io/kaitai/struct/precompile/CalculateSeqSizes.scala @@ -0,0 +1,134 @@ +package io.kaitai.struct.precompile + +import io.kaitai.struct.Log +import io.kaitai.struct.datatype.DataType +import io.kaitai.struct.datatype.DataType._ +import io.kaitai.struct.exprlang.Ast +import io.kaitai.struct.format._ + +class CalculateSeqSizes(specs: ClassSpecs) { + def run(): Unit = specs.forEachRec(CalculateSeqSizes.getSeqSize) +} + +object CalculateSeqSizes { + def sizeMultiply(sizeElement: Sized, repeat: RepeatSpec) = { + sizeElement match { + case FixedSized(elementSize) => + repeat match { + case NoRepeat => + sizeElement + case RepeatExpr(expr) => + evaluateIntLiteral(expr) match { + case Some(count) => FixedSized(elementSize * count) + case None => DynamicSized + } + case _: RepeatUntil | RepeatEos => + DynamicSized + } + case _ => sizeElement + } + } + + def getSeqSize(curClass: ClassSpec): Sized = { + curClass.seqSize match { + case DynamicSized | _: FixedSized => + // do nothing, it's already calculated + case StartedCalculationSized => + // recursive size dependency encountered => we won't be able to determine + // let's break the infinite loop + curClass.seqSize = DynamicSized + case NotCalculatedSized => + // launch the calculation + curClass.seqSize = StartedCalculationSized + val seqSize = forEachSeqAttr(curClass, (attr, seqPos, sizeElement, sizeContainer) => {}) + curClass.seqSize = seqSize match { + case Some(size) => FixedSized(size) + case None => DynamicSized + } + } + + Log.seqSizes.info(() => s"sizeof(${curClass.nameAsStr}) = ${curClass.seqSize}") + curClass.seqSize + } + + /** + * Traverses type's sequence of attributes, calling operation for every attribute. + * Operation is called with arguments (attr, seqPos, sizeElement, sizeContainer) + * @param curClass type specification to traverse + * @param op operation to apply to every sequence attribute + * @return total size of sequence, if possible (i.e. it's fixed size) + */ + def forEachSeqAttr(curClass: ClassSpec, op: (AttrSpec, Option[Int], Sized, Sized) => Unit): Option[Int] = { + var seqPos: Option[Int] = Some(0) + curClass.seq.foreach { attr => + val sizeElement = dataTypeBitsSize(attr.dataType) + val sizeContainer = sizeMultiply(sizeElement, attr.cond.repeat) + + op(attr, seqPos, sizeElement, sizeContainer) + + seqPos = (seqPos, sizeContainer) match { + case (Some(pos), FixedSized(siz)) => Some(pos + siz) + case _ => None + } + } + seqPos + } + + /** + * Determines how many bits occupies given data type. + * + * @param dataType data type to analyze + * @return number of bits or None, if it's impossible to determine a priori + */ + def dataTypeBitsSize(dataType: DataType): Sized = { + dataType match { + case BitsType1 => FixedSized(1) + case BitsType(width) => FixedSized(width) + case EnumType(_, basedOn) => dataTypeBitsSize(basedOn) + case ut: UserTypeInstream => getSeqSize(ut.classSpec.get) + case _ => + dataTypeByteSize(dataType) match { + case FixedSized(x) => FixedSized(x * 8) + case otherSize => otherSize + } + } + } + + /** + * Determines how many bytes occupies a given data type. + * + * @param dataType data type to analyze + * @return number of bytes or None, if it's impossible to determine a priori + */ + def dataTypeByteSize(dataType: DataType): Sized = { + dataType match { + case _: Int1Type => FixedSized(1) + case IntMultiType(_, width, _) => FixedSized(width.width) + case FixedBytesType(contents, _) => FixedSized(contents.length) + case FloatMultiType(width, _) => FixedSized(width.width) + case _: BytesEosType => DynamicSized + case blt: BytesLimitType => evaluateIntLiteral(blt.size) match { + case Some(x) => FixedSized(x) + case None => DynamicSized + } + case _: BytesTerminatedType => DynamicSized + case StrFromBytesType(basedOn, _) => dataTypeByteSize(basedOn) + case utb: UserTypeFromBytes => dataTypeByteSize(utb.bytes) + case st: SwitchType => DynamicSized // FIXME: it's really possible get size if st.hasSize + } + } + + /** + * Evaluates the expression, if possible to get the result without introduction + * of any variables or anything. + * + * @param expr expression to evaluate + * @return integer result or None + */ + def evaluateIntLiteral(expr: Ast.expr): Option[Int] = { + expr match { + case Ast.expr.IntNum(x) => Some(x.toInt) + case _ => None + } + } +} diff --git a/shared/src/main/scala/io/kaitai/struct/precompile/Exceptions.scala b/shared/src/main/scala/io/kaitai/struct/precompile/Exceptions.scala index a251b2fe1..8ecdcbaba 100644 --- a/shared/src/main/scala/io/kaitai/struct/precompile/Exceptions.scala +++ b/shared/src/main/scala/io/kaitai/struct/precompile/Exceptions.scala @@ -9,12 +9,18 @@ import io.kaitai.struct.format.ClassSpec * @param path YAML path components in file * @param file file to report as erroneous, None means "main compilation unit" */ -class ErrorInInput(err: Throwable, path: List[String] = List(), file: Option[String] = None) - extends RuntimeException(ErrorInInput.message(err, path, file)) +case class ErrorInInput(err: Throwable, val path: List[String] = List(), val file: Option[String] = None) + extends RuntimeException(ErrorInInput.message(err, path, file), err) object ErrorInInput { - private def message(err: Throwable, path: List[String], file: Option[String]) = - s"${file.getOrElse("(main)")}: /${path.mkString("/")}: ${err.getMessage}" + private def message(err: Throwable, path: List[String], file: Option[String]) = { + val fileStr = file match { + case Some(x) => x.replace('\\', '/') + case None => "(main)" + } + val msg = Option(err.getMessage).getOrElse(err.toString) + s"$fileStr: /${path.mkString("/")}: $msg" + } } /** diff --git a/shared/src/main/scala/io/kaitai/struct/precompile/LoadImports.scala b/shared/src/main/scala/io/kaitai/struct/precompile/LoadImports.scala index 1a7649da0..1cb0c0304 100644 --- a/shared/src/main/scala/io/kaitai/struct/precompile/LoadImports.scala +++ b/shared/src/main/scala/io/kaitai/struct/precompile/LoadImports.scala @@ -13,6 +13,8 @@ import scala.concurrent.Future * @param specs collection of [[ClassSpec]] entries to work on */ class LoadImports(specs: ClassSpecs) { + import LoadImports._ + /** * Recursively loads and processes all .ksy files referenced in * `meta/import` section of given class spec and all nested classes. @@ -22,30 +24,37 @@ class LoadImports(specs: ClassSpecs) { * * @param curClass class spec to start recursive import from */ - def processClass(curClass: ClassSpec): Future[List[ClassSpec]] = { - Log.importOps.info(() => s".. LoadImports: processing class ${curClass.nameAsStr}") + def processClass(curClass: ClassSpec, workDir: ImportPath): Future[List[ClassSpec]] = { + Log.importOps.info(() => s".. LoadImports: processing class ${curClass.nameAsStr} (workDir = $workDir)") - val thisMetaFuture: Future[List[ClassSpec]] = curClass.meta match { - case Some(meta) => - Future.sequence(meta.imports.zipWithIndex.map { case (name, idx) => - loadImport(name, meta.path ++ List("imports", idx.toString), Some(curClass.nameAsStr)) - }).map((x) => x.flatten) - case None => - Future { List() } - } + val thisMetaFuture: Future[List[ClassSpec]] = + Future.sequence(curClass.meta.imports.zipWithIndex.map { case (name, idx) => + loadImport( + name, + curClass.meta.path ++ List("imports", idx.toString), + Some(curClass.nameAsStr), + workDir + ) + }).map((x) => x.flatten) val nestedFuture: Future[Iterable[ClassSpec]] = Future.sequence(curClass.types.map({ - case (_, nestedClass) => processClass(nestedClass) + case (_, nestedClass) => processClass(nestedClass, workDir) })).map((listOfLists) => listOfLists.flatten) Future.sequence(List(thisMetaFuture, nestedFuture)).map((x) => x.flatten) } - private def loadImport(name: String, path: List[String], inFile: Option[String]): Future[List[ClassSpec]] = { - val futureSpec = if (name.startsWith("/")) { - specs.importAbsolute(name.substring(1), path, inFile) - } else { - specs.importRelative(name, path, inFile) + private def loadImport(name: String, path: List[String], inFile: Option[String], workDir: ImportPath): Future[List[ClassSpec]] = { + Log.importOps.info(() => s".. LoadImports: loadImport($name, workDir = $workDir)") + + val impPath = ImportPath.fromString(name) + val fullPath = ImportPath.add(workDir, impPath) + + val futureSpec = fullPath match { + case RelativeImportPath(p) => + specs.importRelative(p.mkString("/"), path, inFile) + case AbsoluteImportPath(p) => + specs.importAbsolute(p.mkString("/"), path, inFile) } futureSpec.flatMap { case optSpec => @@ -60,7 +69,7 @@ class LoadImports(specs: ClassSpecs) { // import* methods due to caching, but we won't rely on it here. if (!specs.contains(specName)) { specs(specName) = spec - processClass(spec) + processClass(spec, ImportPath.updateWorkDir(workDir, impPath)) } else { Future { List() } } @@ -70,3 +79,38 @@ class LoadImports(specs: ClassSpecs) { } } } + +object LoadImports { + sealed trait ImportPath { + def baseDir: ImportPath + } + case class RelativeImportPath(path: List[String]) extends ImportPath { + override def baseDir: ImportPath = RelativeImportPath(path.init) + } + case class AbsoluteImportPath(path: List[String]) extends ImportPath { + override def baseDir: ImportPath = AbsoluteImportPath(path.init) + } + val BasePath = RelativeImportPath(List()) + + object ImportPath { + def fromString(s: String): ImportPath = if (s.startsWith("/")) { + AbsoluteImportPath(s.substring(1).split("/", -1).toList) + } else { + RelativeImportPath(s.split("/", -1).toList) + } + + def add(curWorkDir: ImportPath, newPath: ImportPath): ImportPath = { + (curWorkDir, newPath) match { + case (_, AbsoluteImportPath(newPathAbs)) => + AbsoluteImportPath(newPathAbs) + case (RelativeImportPath(curDir), RelativeImportPath(newPathRel)) => + RelativeImportPath(curDir ++ newPathRel) + case (AbsoluteImportPath(curDir), RelativeImportPath(newPathRel)) => + AbsoluteImportPath(curDir ++ newPathRel) + } + } + + def updateWorkDir(curWorkDir: ImportPath, newPath: ImportPath): ImportPath = + add(curWorkDir, newPath).baseDir + } +} diff --git a/shared/src/main/scala/io/kaitai/struct/precompile/ParentTypes.scala b/shared/src/main/scala/io/kaitai/struct/precompile/ParentTypes.scala index 0a0beebf5..63f242196 100644 --- a/shared/src/main/scala/io/kaitai/struct/precompile/ParentTypes.scala +++ b/shared/src/main/scala/io/kaitai/struct/precompile/ParentTypes.scala @@ -3,11 +3,14 @@ package io.kaitai.struct.precompile import io.kaitai.struct.{ClassTypeProvider, Log} import io.kaitai.struct.datatype.DataType import io.kaitai.struct.datatype.DataType.{ArrayType, SwitchType, UserType} -import io.kaitai.struct.exprlang.Ast import io.kaitai.struct.format._ import io.kaitai.struct.translators.TypeDetector -object ParentTypes { +class ParentTypes(classSpecs: ClassSpecs) { + def run(): Unit = { + classSpecs.foreach { case (_, curClass) => markup(curClass) } + } + def markup(curClass: ClassSpec): Unit = { Log.typeProcParent.info(() => s"markupParentTypes(${curClass.nameAsStr})") @@ -40,7 +43,7 @@ object ParentTypes { Log.typeProcParent.info(() => s"..... no parent type added") None case Some(parent) => - val provider = new ClassTypeProvider(curClass) + val provider = new ClassTypeProvider(classSpecs, curClass) val detector = new TypeDetector(provider) val parentType = detector.detectType(parent) Log.typeProcParent.info(() => s"..... enforced parent type = $parentType") diff --git a/shared/src/main/scala/io/kaitai/struct/precompile/ResolveTypes.scala b/shared/src/main/scala/io/kaitai/struct/precompile/ResolveTypes.scala index 37ba4d487..1fe9eb0c6 100644 --- a/shared/src/main/scala/io/kaitai/struct/precompile/ResolveTypes.scala +++ b/shared/src/main/scala/io/kaitai/struct/precompile/ResolveTypes.scala @@ -10,11 +10,7 @@ import io.kaitai.struct.format._ * converts names into ClassSpec / EnumSpec references. */ class ResolveTypes(specs: ClassSpecs, opaqueTypes: Boolean) { - def run(): Unit = - specs.foreach { case (_, spec) => - // FIXME: grab exception and rethrow more localized one, with a specName? - resolveUserTypes(spec) - } + def run(): Unit = specs.forEachRec(resolveUserTypes) /** * Resolves user types and enum types recursively starting from a certain @@ -32,10 +28,6 @@ class ResolveTypes(specs: ClassSpecs, opaqueTypes: Boolean) { // ignore all other types of instances } } - - curClass.types.foreach { case (_, nestedClass) => - resolveUserTypes(nestedClass) - } } def resolveUserTypeForAttr(curClass: ClassSpec, attr: AttrLikeSpec): Unit = diff --git a/shared/src/main/scala/io/kaitai/struct/precompile/SpecsValueTypeDerive.scala b/shared/src/main/scala/io/kaitai/struct/precompile/SpecsValueTypeDerive.scala index 5d10845a5..74686d1a1 100644 --- a/shared/src/main/scala/io/kaitai/struct/precompile/SpecsValueTypeDerive.scala +++ b/shared/src/main/scala/io/kaitai/struct/precompile/SpecsValueTypeDerive.scala @@ -12,7 +12,7 @@ class SpecsValueTypeDerive(specs: ClassSpecs) { Log.typeProcValue.info(() => s"### SpecsValueTypeDerive: iteration #$iterNum") specs.foreach { case (specName, spec) => Log.typeProcValue.info(() => s"#### $specName") - val thisChanged = new ValueTypesDeriver(spec).run() + val thisChanged = new ValueTypesDeriver(specs, spec).run() Log.typeProcValue.info(() => ".... => " + (if (thisChanged) "changed" else "no changes")) hasChanged |= thisChanged } diff --git a/shared/src/main/scala/io/kaitai/struct/precompile/TypeValidator.scala b/shared/src/main/scala/io/kaitai/struct/precompile/TypeValidator.scala index d6f37d613..6bd2b1210 100644 --- a/shared/src/main/scala/io/kaitai/struct/precompile/TypeValidator.scala +++ b/shared/src/main/scala/io/kaitai/struct/precompile/TypeValidator.scala @@ -1,6 +1,6 @@ package io.kaitai.struct.precompile -import io.kaitai.struct.ClassTypeProvider +import io.kaitai.struct.{ClassTypeProvider, Log} import io.kaitai.struct.datatype.DataType import io.kaitai.struct.datatype.DataType._ import io.kaitai.struct.exprlang.Ast @@ -11,17 +11,21 @@ import scala.reflect.ClassTag /** * Validates all expressions used inside the given ClassSpec to use expected types. + * @param specs bundle of class specifications (used only to find external references) * @param topClass class to start check with */ -class TypeValidator(topClass: ClassSpec) { - val provider = new ClassTypeProvider(topClass) +class TypeValidator(specs: ClassSpecs, topClass: ClassSpec) { + val provider = new ClassTypeProvider(specs, topClass) val detector = new TypeDetector(provider) /** * Starts the check from top-level class. */ - def run(): Unit = - validateClass(topClass) + def run(): Unit = specs.forEachTopLevel { (specName, curClass) => + Log.typeValid.info(() => s"validating top level class '$specName'") + provider.topClass = curClass + curClass.forEachRec(validateClass) + } /** * Performs validation of a single ClassSpec: would validate @@ -31,6 +35,7 @@ class TypeValidator(topClass: ClassSpec) { * @param curClass class to check */ def validateClass(curClass: ClassSpec): Unit = { + Log.typeValid.info(() => s"validateClass(${curClass.nameAsStr})") provider.nowClass = curClass curClass.seq.foreach(validateAttr) @@ -43,10 +48,6 @@ class TypeValidator(topClass: ClassSpec) { // TODO } } - - curClass.types.foreach { case (_, nestedClass) => - validateClass(nestedClass) - } } /** @@ -55,6 +56,8 @@ class TypeValidator(topClass: ClassSpec) { * @param attr attribute to check */ def validateAttr(attr: AttrLikeSpec) { + Log.typeValid.info(() => s"validateAttr(${attr.id.humanReadable})") + val path = attr.path attr.cond.ifExpr.foreach((ifExpr) => @@ -77,10 +80,21 @@ class TypeValidator(topClass: ClassSpec) { /** * Validates single non-composite data type, checking all expressions * inside data type definition. + * * @param dataType data type to check * @param path original .ksy path to make error messages more meaningful */ def validateDataType(dataType: DataType, path: List[String]) { + // validate args vs params + dataType match { + case ut: UserType => + // we only validate non-opaque types, opaque are unverifiable by definition + if (!ut.isOpaque) + validateArgsVsParams(ut.args, ut.classSpec.get.params, path ++ List("type")) + case _ => + // no args or params in non-user types + } + dataType match { case blt: BytesLimitType => checkAssert[IntType](blt.size, "integer", path, "size") @@ -105,12 +119,38 @@ class TypeValidator(topClass: ClassSpec) { } catch { case tme: TypeMismatchError => throw new YAMLParseException(tme.getMessage, casePath) + case err: Throwable => + throw new ErrorInInput(err, casePath) } } validateDataType(caseType, casePath) } } + /** + * Validates that arguments given for a certain type match list of parameters + * declared for that type. + * @param args arguments given in invocation + * @param params parameters declared in a user type + * @param path path where invocation happens + * @return + */ + def validateArgsVsParams(args: Seq[Ast.expr], params: List[ParamDefSpec], path: List[String]): Unit = { + if (args.size != params.size) + throw YAMLParseException.invalidParamCount(params.size, args.size, path) + + args.indices.foreach { (i) => + val arg = args(i) + val param = params(i) + val tArg = detector.detectType(arg) + val tParam = param.dataType + + if (!TypeDetector.canAssign(tArg, tParam)) { + throw YAMLParseException.paramMismatch(i, tArg, param.id.humanReadable, tParam, path) + } + } + } + /** * Checks that expression's type conforms to a given datatype, otherwise * throw a human-readable exception, with some pointers that would help @@ -134,11 +174,19 @@ class TypeValidator(topClass: ClassSpec) { try { detector.detectType(expr) match { case _: T => // good + case st: SwitchType => + st.combinedType match { + case _: T => // good + case actual => + throw YAMLParseException.exprType(expectStr, actual, path ++ List(pathKey)) + } case actual => throw YAMLParseException.exprType(expectStr, actual, path ++ List(pathKey)) } } catch { + case err: InvalidIdentifier => + throw new ErrorInInput(err, path ++ List(pathKey)) case err: ExpressionError => - throw new YAMLParseException(err.getMessage, path ++ List(pathKey)) + throw new ErrorInInput(err, path ++ List(pathKey)) } } } diff --git a/shared/src/main/scala/io/kaitai/struct/precompile/ValueTypesDeriver.scala b/shared/src/main/scala/io/kaitai/struct/precompile/ValueTypesDeriver.scala index 6f3e6195c..50cf82c30 100644 --- a/shared/src/main/scala/io/kaitai/struct/precompile/ValueTypesDeriver.scala +++ b/shared/src/main/scala/io/kaitai/struct/precompile/ValueTypesDeriver.scala @@ -1,11 +1,11 @@ package io.kaitai.struct.precompile -import io.kaitai.struct.format.{ClassSpec, ValueInstanceSpec, YAMLParseException} +import io.kaitai.struct.format.{ClassSpec, ClassSpecs, ValueInstanceSpec, YAMLParseException} import io.kaitai.struct.translators.TypeDetector import io.kaitai.struct.{ClassTypeProvider, Log} -class ValueTypesDeriver(topClass: ClassSpec) { - val provider = new ClassTypeProvider(topClass) +class ValueTypesDeriver(specs: ClassSpecs, topClass: ClassSpec) { + val provider = new ClassTypeProvider(specs, topClass) val detector = new TypeDetector(provider) def run(): Boolean = @@ -34,10 +34,7 @@ class ValueTypesDeriver(topClass: ClassSpec) { hasUndecided = true // just ignore, we're not there yet, probably we'll get it on next iteration case err: ExpressionError => - throw new YAMLParseException( - err.getMessage, - vi.path ++ List("value") - ) + throw new ErrorInInput(err, vi.path ++ List("value")) } case Some(_) => // already derived, do nothing diff --git a/shared/src/main/scala/io/kaitai/struct/translators/AbstractTranslator.scala b/shared/src/main/scala/io/kaitai/struct/translators/AbstractTranslator.scala new file mode 100644 index 000000000..a96140ea7 --- /dev/null +++ b/shared/src/main/scala/io/kaitai/struct/translators/AbstractTranslator.scala @@ -0,0 +1,20 @@ +package io.kaitai.struct.translators + +import io.kaitai.struct.exprlang.Ast + +/** + * Translators are per-target language classes which implement translation + * of Kaitai Struct expression language into expression in target language. + * + * This simplest, most abstract form of translator provides only a single + * `translate` method, which takes KS expression and returns string in + * target language. + */ +trait AbstractTranslator { + /** + * Translates KS expression into an expression in some target language. + * @param v KS expression to translate + * @return expression in target language as string + */ + def translate(v: Ast.expr): String +} diff --git a/shared/src/main/scala/io/kaitai/struct/translators/BaseTranslator.scala b/shared/src/main/scala/io/kaitai/struct/translators/BaseTranslator.scala index 73e411f10..2c2c808cb 100644 --- a/shared/src/main/scala/io/kaitai/struct/translators/BaseTranslator.scala +++ b/shared/src/main/scala/io/kaitai/struct/translators/BaseTranslator.scala @@ -5,7 +5,43 @@ import io.kaitai.struct.datatype.DataType._ import io.kaitai.struct.exprlang.Ast import io.kaitai.struct.precompile.TypeMismatchError -abstract class BaseTranslator(val provider: TypeProvider) extends TypeDetector(provider) { +/** + * BaseTranslator is a common semi-abstract implementation of a translator + * API (i.e. [[AbstractTranslator]]), which fits target languages that + * follow "every KS expression is translatable into expression" paradigm. + * Main [[AbstractTranslator.translate]] method is implemented as a huge + * case matching, which usually just calls relevant abstract methods for + * every particular piece of KS expression, i.e. literals, operations, + * method calls, etc. + * + * Given that there are many of these abstract methods, to make it more + * maintainable, they are grouped into several abstract traits: + * [[CommonLiterals]], [[CommonOps]]. + * + * This translator implementation also handles user-defined types and + * fields properly - it uses given [[TypeProvider]] to resolve these. + * + * @param provider TypeProvider that will answer queries on user types + */ +abstract class BaseTranslator(val provider: TypeProvider) + extends TypeDetector(provider) + with AbstractTranslator + with CommonLiterals + with CommonOps + with CommonMethods[String] { + + /** + * Translates KS expression into an expression in some target language. + * Note that this implementation may throw errors subclassed off the + * [[io.kaitai.struct.precompile.ExpressionError]] when encountering + * some sort of logical error in expression (i.e. invalid usage of + * operator, type mismatch, etc). Typically, one's supposed to catch + * and rethrow it, wrapped in [[io.kaitai.struct.precompile.ErrorInInput]] + * to assist error reporting in KSC. + * + * @param v KS expression to translate + * @return expression in target language as string + */ def translate(v: Ast.expr): String = { v match { case Ast.expr.IntNum(n) => @@ -24,8 +60,13 @@ abstract class BaseTranslator(val provider: TypeProvider) extends TypeDetector(p doEnumByLabel(enumSpec.name, label.name) case Ast.expr.Name(name: Ast.identifier) => doLocalName(name.name) - case Ast.expr.UnaryOp(op: Ast.unaryop, v: Ast.expr) => - s"${unaryOp(op)}${translate(v)}" + case Ast.expr.UnaryOp(op: Ast.unaryop, inner: Ast.expr) => + unaryOp(op) + (inner match { + case Ast.expr.IntNum(_) | Ast.expr.FloatNum(_) => + translate(inner) + case _ => + s"(${translate(inner)})" + }) case Ast.expr.Compare(left: Ast.expr, op: Ast.cmpop, right: Ast.expr) => (detectType(left), detectType(right)) match { case (_: NumericType, _: NumericType) => @@ -40,6 +81,8 @@ abstract class BaseTranslator(val provider: TypeProvider) extends TypeDetector(p } case (_: StrType, _: StrType) => doStrCompareOp(left, op, right) + case (_: BytesType, _: BytesType) => + doBytesCompareOp(left, op, right) case (EnumType(ltype, _), EnumType(rtype, _)) => if (ltype != rtype) { throw new TypeMismatchError(s"can't compare enums type $ltype and $rtype") @@ -69,56 +112,10 @@ abstract class BaseTranslator(val provider: TypeProvider) extends TypeDetector(p case idxType => throw new TypeMismatchError(s"can't use $idx as array index (need int, got $idxType)") } - case Ast.expr.Attribute(value: Ast.expr, attr: Ast.identifier) => - val valType = detectType(value) - valType match { - case _: UserType => - userTypeField(value, attr.name) - case _: StrType => - attr.name match { - case "length" => strLength(value) - case "reverse" => strReverse(value) - case "to_i" => strToInt(value, Ast.expr.IntNum(10)) - } - case _: IntType => - attr.name match { - case "to_s" => intToStr(value, Ast.expr.IntNum(10)) - } - case ArrayType(inType) => - attr.name match { - case "first" => arrayFirst(value) - case "last" => arrayLast(value) - case "size" => arraySize(value) - } - case KaitaiStreamType => - attr.name match { - case "size" => kaitaiStreamSize(value) - case "eof" => kaitaiStreamEof(value) - case "pos" => kaitaiStreamPos(value) - } - case et: EnumType => - attr.name match { - case "to_i" => enumToInt(value, et) - case _ => throw new TypeMismatchError(s"called invalid attribute '${attr.name}' on expression of type $valType") - } - case _: BooleanType => - attr.name match { - case "to_i" => boolToInt(value) - case _ => throw new TypeMismatchError(s"called invalid attribute '${attr.name}' on expression of type $valType") - } - } - case Ast.expr.Call(func: Ast.expr, args: Seq[Ast.expr]) => - func match { - case Ast.expr.Attribute(obj: Ast.expr, methodName: Ast.identifier) => - val objType = detectType(obj) - (objType, methodName.name) match { - // TODO: check argument quantity - case (_: StrType, "substring") => strSubstring(obj, args(0), args(1)) - case (_: StrType, "to_i") => strToInt(obj, args(0)) - case (_: BytesType, "to_s") => bytesToStr(translate(obj), args(0)) - case _ => throw new TypeMismatchError(s"don't know how to call method '$methodName' of object type '$objType'") - } - } + case call: Ast.expr.Attribute => + translateAttribute(call) + case call: Ast.expr.Call => + translateCall(call) case Ast.expr.List(values: Seq[Ast.expr]) => val t = detectArrayType(values) t match { @@ -142,169 +139,37 @@ abstract class BaseTranslator(val provider: TypeProvider) extends TypeDetector(p } } - def numericBinOp(left: Ast.expr, op: Ast.operator, right: Ast.expr) = { - s"(${translate(left)} ${binOp(op)} ${translate(right)})" - } - - def binOp(op: Ast.operator): String = { - op match { - case Ast.operator.Add => "+" - case Ast.operator.Sub => "-" - case Ast.operator.Mult => "*" - case Ast.operator.Div => "/" - case Ast.operator.Mod => "%" - case Ast.operator.BitAnd => "&" - case Ast.operator.BitOr => "|" - case Ast.operator.BitXor => "^" - case Ast.operator.LShift => "<<" - case Ast.operator.RShift => ">>" - } - } - - def doNumericCompareOp(left: Ast.expr, op: Ast.cmpop, right: Ast.expr): String = - s"${translate(left)} ${cmpOp(op)} ${translate(right)}" - - def doStrCompareOp(left: Ast.expr, op: Ast.cmpop, right: Ast.expr): String = - s"${translate(left)} ${cmpOp(op)} ${translate(right)}" - - def doEnumCompareOp(left: Ast.expr, op: Ast.cmpop, right: Ast.expr): String = - s"${translate(left)} ${cmpOp(op)} ${translate(right)}" - - def cmpOp(op: Ast.cmpop): String = { - op match { - case Ast.cmpop.Lt => "<" - case Ast.cmpop.LtE => "<=" - case Ast.cmpop.Gt => ">" - case Ast.cmpop.GtE => ">=" - case Ast.cmpop.Eq => "==" - case Ast.cmpop.NotEq => "!=" - } - } - - def doBooleanOp(op: Ast.boolop, values: Seq[Ast.expr]): String = { - val opStr = s"${booleanOp(op)}" - val dividerStr = s") $opStr (" - val valuesStr = values.map(translate).mkString("(", dividerStr, ")") - - // Improve compatibility for statements like: ( ... && ... || ... ) ? ... : ... - s" ($valuesStr) " - } - - def booleanOp(op: Ast.boolop): String = op match { - case Ast.boolop.Or => "||" - case Ast.boolop.And => "&&" - } - - def unaryOp(op: Ast.unaryop): String = op match { - case Ast.unaryop.Invert => "~" - case Ast.unaryop.Minus => "-" - case Ast.unaryop.Not => "!" - } - def doSubscript(container: Ast.expr, idx: Ast.expr): String def doIfExp(condition: Ast.expr, ifTrue: Ast.expr, ifFalse: Ast.expr): String def doCast(value: Ast.expr, typeName: String): String = translate(value) - // Literals - def doIntLiteral(n: BigInt): String = n.toString - def doFloatLiteral(n: Any): String = n.toString - - def doStringLiteral(s: String): String = { - val encoded = s.toCharArray.map((code) => - if (code <= 0xff) { - strLiteralAsciiChar(code) - } else { - strLiteralUnicode(code) - } - ).mkString - "\"" + encoded + "\"" - } - def doBoolLiteral(n: Boolean): String = n.toString def doArrayLiteral(t: DataType, value: Seq[Ast.expr]): String = "[" + value.map((v) => translate(v)).mkString(", ") + "]" def doByteArrayLiteral(arr: Seq[Byte]): String = "[" + arr.map(_ & 0xff).mkString(", ") + "]" - /** - * Handle ASCII character conversion for inlining into string literals. - * Default implementation consults [[asciiCharQuoteMap]] first, then - * just dumps it as is if it's a printable ASCII charcter, or calls - * [[strLiteralGenericCC]] if it's a control character. - * @param code character code to convert into string for inclusion in - * a string literal - */ - def strLiteralAsciiChar(code: Char): String = { - asciiCharQuoteMap.get(code) match { - case Some(encoded) => encoded - case None => - if (code >= 0x20 && code < 0x80) { - Character.toString(code) - } else { - strLiteralGenericCC(code) - } - } - } - - /** - * Converts generic control character code into something that's allowed - * inside a string literal. Default implementation uses octal encoding, - * which is ok for most C-derived languages. - * - * Note that we use strictly 3 octal digits to work around potential - * problems with following decimal digits, i.e. "\0" + "2" that would be - * parsed as single character "\02" = "\x02", instead of two characters - * "\x00\x32". - * @param code character code to represent - * @return string literal representation of given code - */ - def strLiteralGenericCC(code: Char): String = - "\\%03o".format(code.toInt) - - /** - * Converts Unicode (typically, non-ASCII) character code into something - * that's allowed inside a string literal. Default implementation uses - * Unicode 4-digit hex encoding, which is ok for most C-derived languages. - * @param code character code to represent - * @return string literal representation of given code - */ - def strLiteralUnicode(code: Char): String = - "\\u%04x".format(code.toInt) - - /** - * Character quotation map for inclusion in string literals. - * Default implementation includes bare minimum that seems - * to be available in all languages. - */ - val asciiCharQuoteMap: Map[Char, String] = Map( - '\t' -> "\\t", - '\n' -> "\\n", - '\r' -> "\\r", - '"' -> "\\\"", - '\\' -> "\\\\" - ) - def doLocalName(s: String): String = doName(s) def doName(s: String): String - def userTypeField(value: Ast.expr, attrName: String): String = - s"${translate(value)}.${doName(attrName)}" + def userTypeField(userType: UserType, value: Ast.expr, attrName: String): String = + anyField(value, attrName) + def doEnumByLabel(enumTypeAbs: List[String], label: String): String def doEnumById(enumTypeAbs: List[String], id: String): String // Predefined methods of various types def strConcat(left: Ast.expr, right: Ast.expr): String = s"${translate(left)} + ${translate(right)}" - def strToInt(s: Ast.expr, base: Ast.expr): String - def enumToInt(value: Ast.expr, et: EnumType): String def boolToInt(value: Ast.expr): String = doIfExp(value, Ast.expr.IntNum(1), Ast.expr.IntNum(0)) - def intToStr(i: Ast.expr, base: Ast.expr): String - def bytesToStr(bytesExpr: String, encoding: Ast.expr): String - def strLength(s: Ast.expr): String - def strReverse(s: Ast.expr): String - def strSubstring(s: Ast.expr, from: Ast.expr, to: Ast.expr): String - def arrayFirst(a: Ast.expr): String - def arrayLast(a: Ast.expr): String - def arraySize(a: Ast.expr): String + def kaitaiStreamSize(value: Ast.expr): String = anyField(value, "size") + def kaitaiStreamEof(value: Ast.expr): String = anyField(value, "is_eof") + def kaitaiStreamPos(value: Ast.expr): String = anyField(value, "pos") + + // Special convenience definition method + helper + override def bytesToStr(value: Ast.expr, expr: Ast.expr): String = + bytesToStr(translate(value), expr) + def bytesToStr(value: String, expr: Ast.expr): String - def kaitaiStreamSize(value: Ast.expr): String = userTypeField(value, "size") - def kaitaiStreamEof(value: Ast.expr): String = userTypeField(value, "is_eof") - def kaitaiStreamPos(value: Ast.expr): String = userTypeField(value, "pos") + // Helper that does simple "one size fits all" attribute calling, if it is useful + // for the language + def anyField(value: Ast.expr, attrName: String): String = + s"${translate(value)}.${doName(attrName)}" } diff --git a/shared/src/main/scala/io/kaitai/struct/translators/CSharpTranslator.scala b/shared/src/main/scala/io/kaitai/struct/translators/CSharpTranslator.scala index 7e7737a59..26a030b89 100644 --- a/shared/src/main/scala/io/kaitai/struct/translators/CSharpTranslator.scala +++ b/shared/src/main/scala/io/kaitai/struct/translators/CSharpTranslator.scala @@ -1,13 +1,14 @@ package io.kaitai.struct.translators -import io.kaitai.struct.Utils +import io.kaitai.struct.{ImportList, Utils} import io.kaitai.struct.datatype.DataType import io.kaitai.struct.datatype.DataType._ import io.kaitai.struct.exprlang.Ast import io.kaitai.struct.exprlang.Ast._ +import io.kaitai.struct.format.Identifier import io.kaitai.struct.languages.CSharpCompiler -class CSharpTranslator(provider: TypeProvider) extends BaseTranslator(provider) { +class CSharpTranslator(provider: TypeProvider, importList: ImportList) extends BaseTranslator(provider) { override def doArrayLiteral(t: DataType, value: Seq[expr]): String = { val nativeType = CSharpCompiler.kaitaiType2NativeType(t) val commaStr = value.map((v) => translate(v)).mkString(", ") @@ -43,10 +44,15 @@ class CSharpTranslator(provider: TypeProvider) extends BaseTranslator(provider) } override def doName(s: String) = - if (s.startsWith("_")) - s"M${Utils.upperCamelCase(s)}" - else + if (s.startsWith("_")) { + s match { + case Identifier.SWITCH_ON => "on" + case Identifier.INDEX => "i" + case _ => s"M${Utils.upperCamelCase(s)}" + } + } else { s"${Utils.upperCamelCase(s)}" + } override def doEnumByLabel(enumTypeAbs: List[String], label: String): String = s"${enumClass(enumTypeAbs)}.${Utils.upperCamelCase(label)}" @@ -68,6 +74,9 @@ class CSharpTranslator(provider: TypeProvider) extends BaseTranslator(provider) } } + override def doBytesCompareOp(left: Ast.expr, op: Ast.cmpop, right: Ast.expr): String = + s"(${CSharpCompiler.kstreamName}.ByteArrayCompare(${translate(left)}, ${translate(right)}) ${cmpOp(op)} 0)" + override def doSubscript(container: expr, idx: expr): String = s"${translate(container)}[${translate(idx)}]" override def doIfExp(condition: expr, ifTrue: expr, ifFalse: expr): String = @@ -76,12 +85,18 @@ class CSharpTranslator(provider: TypeProvider) extends BaseTranslator(provider) s"((${Utils.upperCamelCase(typeName)}) (${translate(value)}))" // Predefined methods of various types - override def strToInt(s: expr, base: expr): String = + override def strToInt(s: expr, base: expr): String = { + importList.add("System") s"Convert.ToInt64(${translate(s)}, ${translate(base)})" + } override def enumToInt(v: expr, et: EnumType): String = translate(v) - override def intToStr(i: expr, base: expr): String = - s"Convert.ToString(${translate(i)}, ${translate(base)})" + override def floatToInt(v: expr): String = + s"(long) (${translate(v)})" + override def intToStr(i: expr, base: expr): String = { + importList.add("System") + s"Convert.ToString((long) (${translate(i)}), ${translate(base)})" + } override def bytesToStr(bytesExpr: String, encoding: Ast.expr): String = s"System.Text.Encoding.GetEncoding(${translate(encoding)}).GetString($bytesExpr)" override def strLength(s: expr): String = @@ -99,8 +114,16 @@ class CSharpTranslator(provider: TypeProvider) extends BaseTranslator(provider) s"${translate(a)}[0]" override def arrayLast(a: expr): String = { val v = translate(a) - s"$v[$v.Length - 1]" + s"$v[$v.Count - 1]" } override def arraySize(a: expr): String = s"${translate(a)}.Count" + override def arrayMin(a: Ast.expr): String = { + importList.add("System.Linq") + s"${translate(a)}.Min()" + } + override def arrayMax(a: Ast.expr): String = { + importList.add("System.Linq") + s"${translate(a)}.Max()" + } } diff --git a/shared/src/main/scala/io/kaitai/struct/translators/CommonLiterals.scala b/shared/src/main/scala/io/kaitai/struct/translators/CommonLiterals.scala new file mode 100644 index 000000000..29ee8ee4a --- /dev/null +++ b/shared/src/main/scala/io/kaitai/struct/translators/CommonLiterals.scala @@ -0,0 +1,80 @@ +package io.kaitai.struct.translators + +/** + * Implementations of translations of literals in C-style, common to many + * languages. + */ +trait CommonLiterals { + def doIntLiteral(n: BigInt): String = n.toString + def doFloatLiteral(n: Any): String = n.toString + + def doStringLiteral(s: String): String = { + val encoded = s.toCharArray.map((code) => + if (code <= 0xff) { + strLiteralAsciiChar(code) + } else { + strLiteralUnicode(code) + } + ).mkString + "\"" + encoded + "\"" + } + def doBoolLiteral(n: Boolean): String = n.toString + + /** + * Handle ASCII character conversion for inlining into string literals. + * Default implementation consults [[asciiCharQuoteMap]] first, then + * just dumps it as is if it's a printable ASCII charcter, or calls + * [[strLiteralGenericCC]] if it's a control character. + * @param code character code to convert into string for inclusion in + * a string literal + */ + def strLiteralAsciiChar(code: Char): String = { + asciiCharQuoteMap.get(code) match { + case Some(encoded) => encoded + case None => + if (code >= 0x20 && code < 0x80) { + Character.toString(code) + } else { + strLiteralGenericCC(code) + } + } + } + + /** + * Converts generic control character code into something that's allowed + * inside a string literal. Default implementation uses octal encoding, + * which is ok for most C-derived languages. + * + * Note that we use strictly 3 octal digits to work around potential + * problems with following decimal digits, i.e. "\0" + "2" that would be + * parsed as single character "\02" = "\x02", instead of two characters + * "\x00\x32". + * @param code character code to represent + * @return string literal representation of given code + */ + def strLiteralGenericCC(code: Char): String = + "\\%03o".format(code.toInt) + + /** + * Converts Unicode (typically, non-ASCII) character code into something + * that's allowed inside a string literal. Default implementation uses + * Unicode 4-digit hex encoding, which is ok for most C-derived languages. + * @param code character code to represent + * @return string literal representation of given code + */ + def strLiteralUnicode(code: Char): String = + "\\u%04x".format(code.toInt) + + /** + * Character quotation map for inclusion in string literals. + * Default implementation includes bare minimum that seems + * to be available in all languages. + */ + val asciiCharQuoteMap: Map[Char, String] = Map( + '\t' -> "\\t", + '\n' -> "\\n", + '\r' -> "\\r", + '"' -> "\\\"", + '\\' -> "\\\\" + ) +} diff --git a/shared/src/main/scala/io/kaitai/struct/translators/CommonMethods.scala b/shared/src/main/scala/io/kaitai/struct/translators/CommonMethods.scala new file mode 100644 index 000000000..b71faa868 --- /dev/null +++ b/shared/src/main/scala/io/kaitai/struct/translators/CommonMethods.scala @@ -0,0 +1,99 @@ +package io.kaitai.struct.translators + +import io.kaitai.struct.datatype.DataType._ +import io.kaitai.struct.exprlang.Ast +import io.kaitai.struct.precompile.TypeMismatchError + +abstract trait CommonMethods[T] extends TypeDetector { + def translateAttribute(call: Ast.expr.Attribute): T = { + val attr = call.attr + val value = call.value + val valType = detectType(value) + valType match { + case ut: UserType => + userTypeField(ut, value, attr.name) + case _: StrType => + attr.name match { + case "length" => strLength(value) + case "reverse" => strReverse(value) + case "to_i" => strToInt(value, Ast.expr.IntNum(10)) + } + case _: IntType => + attr.name match { + case "to_s" => intToStr(value, Ast.expr.IntNum(10)) + } + case _: FloatType => + attr.name match { + case "to_i" => floatToInt(value) + } + case ArrayType(inType) => + attr.name match { + case "first" => arrayFirst(value) + case "last" => arrayLast(value) + case "size" => arraySize(value) + case "min" => arrayMin(value) + case "max" => arrayMax(value) + } + case KaitaiStreamType => + attr.name match { + case "size" => kaitaiStreamSize(value) + case "eof" => kaitaiStreamEof(value) + case "pos" => kaitaiStreamPos(value) + } + case et: EnumType => + attr.name match { + case "to_i" => enumToInt(value, et) + case _ => throw new TypeMismatchError(s"called invalid attribute '${attr.name}' on expression of type $valType") + } + case _: BooleanType => + attr.name match { + case "to_i" => boolToInt(value) + case _ => throw new TypeMismatchError(s"called invalid attribute '${attr.name}' on expression of type $valType") + } + } + } + + def translateCall(call: Ast.expr.Call): T = { + val func = call.func + val args = call.args + + func match { + case Ast.expr.Attribute(obj: Ast.expr, methodName: Ast.identifier) => + val objType = detectType(obj) + (objType, methodName.name) match { + // TODO: check argument quantity + case (_: StrType, "substring") => strSubstring(obj, args(0), args(1)) + case (_: StrType, "to_i") => strToInt(obj, args(0)) + case (_: BytesType, "to_s") => bytesToStr(obj, args(0)) + case _ => throw new TypeMismatchError(s"don't know how to call method '$methodName' of object type '$objType'") + } + } + } + + def userTypeField(ut: UserType, value: Ast.expr, name: String): T + + def strLength(s: Ast.expr): T + def strReverse(s: Ast.expr): T + def strToInt(s: Ast.expr, base: Ast.expr): T + def strSubstring(s: Ast.expr, from: Ast.expr, to: Ast.expr): T + + def bytesToStr(value: Ast.expr, expr: Ast.expr): T + + def intToStr(value: Ast.expr, num: Ast.expr): T + + def floatToInt(value: Ast.expr): T + + def kaitaiStreamSize(value: Ast.expr): T + def kaitaiStreamEof(value: Ast.expr): T + def kaitaiStreamPos(value: Ast.expr): T + + def arrayFirst(a: Ast.expr): T + def arrayLast(a: Ast.expr): T + def arraySize(a: Ast.expr): T + def arrayMin(a: Ast.expr): T + def arrayMax(a: Ast.expr): T + + def enumToInt(value: Ast.expr, et: EnumType): T + + def boolToInt(value: Ast.expr): T +} diff --git a/shared/src/main/scala/io/kaitai/struct/translators/CommonOps.scala b/shared/src/main/scala/io/kaitai/struct/translators/CommonOps.scala new file mode 100644 index 000000000..a55019e55 --- /dev/null +++ b/shared/src/main/scala/io/kaitai/struct/translators/CommonOps.scala @@ -0,0 +1,67 @@ +package io.kaitai.struct.translators + +import io.kaitai.struct.exprlang.Ast + +trait CommonOps extends AbstractTranslator { + def numericBinOp(left: Ast.expr, op: Ast.operator, right: Ast.expr) = { + s"(${translate(left)} ${binOp(op)} ${translate(right)})" + } + + def binOp(op: Ast.operator): String = { + op match { + case Ast.operator.Add => "+" + case Ast.operator.Sub => "-" + case Ast.operator.Mult => "*" + case Ast.operator.Div => "/" + case Ast.operator.Mod => "%" + case Ast.operator.BitAnd => "&" + case Ast.operator.BitOr => "|" + case Ast.operator.BitXor => "^" + case Ast.operator.LShift => "<<" + case Ast.operator.RShift => ">>" + } + } + + def doNumericCompareOp(left: Ast.expr, op: Ast.cmpop, right: Ast.expr): String = + s"${translate(left)} ${cmpOp(op)} ${translate(right)}" + + def doStrCompareOp(left: Ast.expr, op: Ast.cmpop, right: Ast.expr): String = + s"${translate(left)} ${cmpOp(op)} ${translate(right)}" + + def doEnumCompareOp(left: Ast.expr, op: Ast.cmpop, right: Ast.expr): String = + s"${translate(left)} ${cmpOp(op)} ${translate(right)}" + + def doBytesCompareOp(left: Ast.expr, op: Ast.cmpop, right: Ast.expr): String = + s"${translate(left)} ${cmpOp(op)} ${translate(right)}" + + def cmpOp(op: Ast.cmpop): String = { + op match { + case Ast.cmpop.Lt => "<" + case Ast.cmpop.LtE => "<=" + case Ast.cmpop.Gt => ">" + case Ast.cmpop.GtE => ">=" + case Ast.cmpop.Eq => "==" + case Ast.cmpop.NotEq => "!=" + } + } + + def doBooleanOp(op: Ast.boolop, values: Seq[Ast.expr]): String = { + val opStr = s"${booleanOp(op)}" + val dividerStr = s") $opStr (" + val valuesStr = values.map(translate).mkString("(", dividerStr, ")") + + // Improve compatibility for statements like: ( ... && ... || ... ) ? ... : ... + s" ($valuesStr) " + } + + def booleanOp(op: Ast.boolop): String = op match { + case Ast.boolop.Or => "||" + case Ast.boolop.And => "&&" + } + + def unaryOp(op: Ast.unaryop): String = op match { + case Ast.unaryop.Invert => "~" + case Ast.unaryop.Minus => "-" + case Ast.unaryop.Not => "!" + } +} diff --git a/shared/src/main/scala/io/kaitai/struct/translators/CppTranslator.scala b/shared/src/main/scala/io/kaitai/struct/translators/CppTranslator.scala index 0378829dc..2ee150df3 100644 --- a/shared/src/main/scala/io/kaitai/struct/translators/CppTranslator.scala +++ b/shared/src/main/scala/io/kaitai/struct/translators/CppTranslator.scala @@ -2,7 +2,7 @@ package io.kaitai.struct.translators import java.nio.charset.Charset -import io.kaitai.struct.Utils +import io.kaitai.struct.{ImportList, Utils} import io.kaitai.struct.exprlang.Ast import io.kaitai.struct.exprlang.Ast.expr import io.kaitai.struct.datatype.DataType @@ -10,7 +10,7 @@ import io.kaitai.struct.datatype.DataType._ import io.kaitai.struct.format.Identifier import io.kaitai.struct.languages.CppCompiler -class CppTranslator(provider: TypeProvider) extends BaseTranslator(provider) { +class CppTranslator(provider: TypeProvider, importListSrc: ImportList) extends BaseTranslator(provider) { val CHARSET_UTF8 = Charset.forName("UTF-8") /** @@ -66,12 +66,13 @@ class CppTranslator(provider: TypeProvider) extends BaseTranslator(provider) { } } - override def userTypeField(value: expr, attrName: String): String = + override def anyField(value: expr, attrName: String): String = s"${translate(value)}->${doName(attrName)}" override def doName(s: String) = s match { case Identifier.ITERATOR => "_" case Identifier.ITERATOR2 => "_buf" + case Identifier.INDEX => "i" case _ => s"$s()" } @@ -108,7 +109,9 @@ class CppTranslator(provider: TypeProvider) extends BaseTranslator(provider) { override def enumToInt(v: expr, et: EnumType): String = translate(v) override def boolToInt(v: expr): String = - translate(v) + s"((${translate(v)}) ? 1 : 0)" + override def floatToInt(v: expr): String = + s"static_cast(${translate(v)})" override def intToStr(i: expr, base: expr): String = { val baseStr = translate(base) baseStr match { @@ -134,4 +137,14 @@ class CppTranslator(provider: TypeProvider) extends BaseTranslator(provider) { s"${translate(a)}->back()" override def arraySize(a: expr): String = s"${translate(a)}->size()" + override def arrayMin(a: expr): String = { + importListSrc.add("algorithm") + val v = translate(a) + s"*std::min_element($v->begin(), $v->end())" + } + override def arrayMax(a: expr): String = { + importListSrc.add("algorithm") + val v = translate(a) + s"*std::max_element($v->begin(), $v->end())" + } } diff --git a/shared/src/main/scala/io/kaitai/struct/translators/GoTranslator.scala b/shared/src/main/scala/io/kaitai/struct/translators/GoTranslator.scala new file mode 100644 index 000000000..b4be487eb --- /dev/null +++ b/shared/src/main/scala/io/kaitai/struct/translators/GoTranslator.scala @@ -0,0 +1,322 @@ +package io.kaitai.struct.translators + +import io.kaitai.struct.datatype.DataType._ +import io.kaitai.struct.exprlang.Ast +import io.kaitai.struct.format.{ClassSpec, Identifier} +import io.kaitai.struct.languages.GoCompiler +import io.kaitai.struct.precompile.TypeMismatchError +import io.kaitai.struct.{ImportList, StringLanguageOutputWriter, Utils} + +sealed trait TranslatorResult +case class ResultString(s: String) extends TranslatorResult +case class ResultLocalVar(n: Int) extends TranslatorResult + +class GoTranslator(out: StringLanguageOutputWriter, provider: TypeProvider, importList: ImportList) + extends TypeDetector(provider) + with AbstractTranslator + with CommonLiterals + with CommonOps + with CommonMethods[TranslatorResult] { + + var returnRes: Option[String] = None + + override def translate(v: Ast.expr): String = resToStr(translateExpr(v)) + + def resToStr(r: TranslatorResult): String = r match { + case ResultString(s) => s + case ResultLocalVar(n) => localVarName(n) + } + + def translateExpr(v: Ast.expr): TranslatorResult = { + v match { + case Ast.expr.IntNum(n) => + trIntLiteral(n) + case Ast.expr.FloatNum(n) => + trFloatLiteral(n) + case Ast.expr.Str(s) => + trStringLiteral(s) + case Ast.expr.Bool(n) => + trBoolLiteral(n) + +// case Ast.expr.BoolOp(op, values) => + case Ast.expr.BinOp(left: Ast.expr, op: Ast.operator, right: Ast.expr) => + (detectType(left), detectType(right), op) match { + case (_: NumericType, _: NumericType, _) => + trNumericBinOp(left, op, right) + case (_: StrType, _: StrType, Ast.operator.Add) => + trStrConcat(left, right) + case (ltype, rtype, _) => + throw new TypeMismatchError(s"can't do $ltype $op $rtype") + } +// case Ast.expr.UnaryOp(op, operand) => +// case Ast.expr.IfExp(condition, ifTrue, ifFalse) => +// case Ast.expr.Compare(left, ops, right) => +// case Ast.expr.EnumByLabel(enumName, label) => +// case Ast.expr.EnumById(enumName, id) => +// case Ast.expr.CastToType(value, typeName) => +// case Ast.expr.Subscript(value, idx) => + case Ast.expr.Name(name: Ast.identifier) => + trLocalName(name.name) +// case Ast.expr.List(elts) => + case call: Ast.expr.Attribute => + translateAttribute(call) + case call: Ast.expr.Call => + translateCall(call) + } + } + + def trIntLiteral(n: BigInt): TranslatorResult = ResultString(doIntLiteral(n)) + def trFloatLiteral(n: BigDecimal): TranslatorResult = ResultString(doFloatLiteral(n)) + def trStringLiteral(s: String): TranslatorResult = ResultString(doStringLiteral(s)) + def trBoolLiteral(n: Boolean): TranslatorResult = ResultString(doBoolLiteral(n)) + + def trNumericBinOp(left: Ast.expr, op: Ast.operator, right: Ast.expr) = + ResultString(numericBinOp(left, op, right)) + + def trStrConcat(left: Ast.expr, right: Ast.expr): TranslatorResult = + ResultString(translate(left) + " + " + translate(right)) + +// override def doArrayLiteral(t: DataType, value: Seq[Ast.expr]): String = { +// val javaType = JavaCompiler.kaitaiType2JavaTypeBoxed(t) +// val commaStr = value.map((v) => translate(v)).mkString(", ") +// s"new ArrayList<$javaType>(Arrays.asList($commaStr))" +// } +// +// override def doByteArrayLiteral(arr: Seq[Byte]): String = +// s"new byte[] { ${arr.mkString(", ")} }" + + override def numericBinOp(left: Ast.expr, op: Ast.operator, right: Ast.expr) = { + (detectType(left), detectType(right), op) match { + case (_: IntType, _: IntType, Ast.operator.Mod) => + s"${GoCompiler.kstreamName}.mod(${translate(left)}, ${translate(right)})" + case _ => + super.numericBinOp(left, op, right) + } + } + + def trLocalName(s: String): TranslatorResult = { + s match { + case Identifier.ROOT | + Identifier.PARENT | + Identifier.IO => + ResultString(s"this.${specialName(s)}") + + // These can be local only + case Identifier.ITERATOR | + Identifier.ITERATOR2 => + ResultString(specialName(s)) + + case _ => + if (provider.isLazy(s)) { + outVarCheckRes(s"this.${Utils.upperCamelCase(s)}()") + } else { + ResultString(s"this.${Utils.upperCamelCase(s)}") + } + } + } + + def specialName(id: String): String = id match { + case Identifier.ROOT | Identifier.PARENT | Identifier.IO => + id + case Identifier.ITERATOR => + "_it" + case Identifier.ITERATOR2 => + "_buf" + } + +// override def doEnumByLabel(enumTypeAbs: List[String], label: String): String = +// s"${enumClass(enumTypeAbs)}.${label.toUpperCase}" +// override def doEnumById(enumTypeAbs: List[String], id: String): String = +// s"${enumClass(enumTypeAbs)}.byId($id)" + + def enumClass(enumTypeAbs: List[String]): String = { + val enumTypeRel = Utils.relClass(enumTypeAbs, provider.nowClass.name) + enumTypeRel.map((x) => Utils.upperCamelCase(x)).mkString(".") + } + + override def doStrCompareOp(left: Ast.expr, op: Ast.cmpop, right: Ast.expr) = { + if (op == Ast.cmpop.Eq) { + s"${translate(left)}.equals(${translate(right)})" + } else if (op == Ast.cmpop.NotEq) { + s"!(${translate(left)}).equals(${translate(right)})" + } else { + s"(${translate(left)}.compareTo(${translate(right)}) ${cmpOp(op)} 0)" + } + } + + override def doBytesCompareOp(left: Ast.expr, op: Ast.cmpop, right: Ast.expr): String = { + op match { + case Ast.cmpop.Eq => + s"Arrays.equals(${translate(left)}, ${translate(right)})" + case Ast.cmpop.NotEq => + s"!Arrays.equals(${translate(left)}, ${translate(right)})" + case _ => + s"(${GoCompiler.kstreamName}.byteArrayCompare(${translate(left)}, ${translate(right)}) ${cmpOp(op)} 0)" + } + } + +// override def doSubscript(container: Ast.expr, idx: Ast.expr): String = +// s"${translate(container)}.get((int) ${translate(idx)})" +// override def doIfExp(condition: Ast.expr, ifTrue: Ast.expr, ifFalse: Ast.expr): String = +// s"(${translate(condition)} ? ${translate(ifTrue)} : ${translate(ifFalse)})" +// override def doCast(value: Ast.expr, typeName: String): String = +// s"((${Utils.upperCamelCase(typeName)}) (${translate(value)}))" + + // Predefined methods of various types +// override def strToInt(s: Ast.expr, base: Ast.expr): String = +// s"Long.parseLong(${translate(s)}, ${translate(base)})" +// override def enumToInt(v: Ast.expr, et: EnumType): String = +// s"${translate(v)}.id()" +// override def intToStr(i: Ast.expr, base: Ast.expr): String = +// s"Long.toString(${translate(i)}, ${translate(base)})" + + val IMPORT_CHARMAP = "golang.org/x/text/encoding/charmap" + + val ENCODINGS = Map( + "cp437" -> ("charmap.CodePage437", IMPORT_CHARMAP), + "iso8859-1" -> ("charmap.ISO8859_1", IMPORT_CHARMAP), + "iso8859-2" -> ("charmap.ISO8859_2", IMPORT_CHARMAP), + "iso8859-3" -> ("charmap.ISO8859_3", IMPORT_CHARMAP), + "iso8859-4" -> ("charmap.ISO8859_4", IMPORT_CHARMAP), + "sjis" -> ("japanese.ShiftJIS", "golang.org/x/text/encoding/japanese"), + "big5" -> ("traditionalchinese.Big5", "golang.org/x/text/encoding/traditionalchinese") + ) + + def bytesToStr(bytesExpr: String, encoding: Ast.expr): TranslatorResult = { + val enc = encoding match { + case Ast.expr.Str(s) => s + case _ => throw new RuntimeException("Variable encodings are not supported in Go yet") + } + + enc.toLowerCase match { + case "ascii" | "utf-8" | "utf8" => + // no conversion + // FIXME: may be add some checks for valid ASCII/UTF-8 + ResultString(s"string($bytesExpr)") + case encStr => + ENCODINGS.get(encStr) match { + case Some((decoderSrc, importName)) => + importList.add(importName) + outVarCheckRes(s"kaitai.BytesToStr($bytesExpr, $decoderSrc.NewDecoder())") + case None => + throw new RuntimeException(s"encoding '$encStr' in not supported in Go") + } + } + } + +// override def strLength(s: Ast.expr): String = +// s"${translate(s)}.length()" +// override def strReverse(s: Ast.expr): String = +// s"new StringBuilder(${translate(s)}).reverse().toString()" +// override def strSubstring(s: Ast.expr, from: Ast.expr, to: Ast.expr): String = +// s"${translate(s)}.substring(${translate(from)}, ${translate(to)})" + +// override def arrayFirst(a: Ast.expr): String = +// s"${translate(a)}.get(0)" +// override def arrayLast(a: Ast.expr): String = { +// val v = translate(a) +// s"$v.get($v.size() - 1)" +// } +// override def arraySize(a: Ast.expr): String = +// s"${translate(a)}.size()" +// override def arrayMin(a: Ast.expr): String = +// s"Collections.min(${translate(a)})" +// override def arrayMax(a: Ast.expr): String = +// s"Collections.max(${translate(a)})" + + override def userTypeField(ut: UserType, value: Ast.expr, name: String): TranslatorResult = { + val valueStr = translate(value) + + val (call, twoOuts) = name match { + case Identifier.ROOT | + Identifier.PARENT | + Identifier.IO => + (specialName(name), false) + case _ => + (Utils.upperCamelCase(name), provider.isLazy(ut.classSpec.get, name)) + } + + if (twoOuts) { + outVarCheckRes(s"$valueStr.$call()") + } else { + ResultString(s"$valueStr.$call") + } + } + + override def strLength(s: Ast.expr): TranslatorResult = { + importList.add("unicode/utf8") + ResultString(s"utf8.RuneCountInString(${translate(s)})") + } + + override def strReverse(s: Ast.expr): TranslatorResult = ??? + + override def strToInt(s: Ast.expr, base: Ast.expr): TranslatorResult = ??? + + override def strSubstring(s: Ast.expr, from: Ast.expr, to: Ast.expr): TranslatorResult = ??? + + override def bytesToStr(value: Ast.expr, expr: Ast.expr): TranslatorResult = ??? + + override def intToStr(value: Ast.expr, num: Ast.expr): TranslatorResult = ??? + + override def floatToInt(value: Ast.expr): TranslatorResult = + ResultString(s"int(${translate(value)})") + + override def kaitaiStreamSize(value: Ast.expr): TranslatorResult = ??? + + override def kaitaiStreamEof(value: Ast.expr): TranslatorResult = ??? + + override def kaitaiStreamPos(value: Ast.expr): TranslatorResult = ??? + + override def arrayFirst(a: Ast.expr): TranslatorResult = ??? + + override def arrayLast(a: Ast.expr): TranslatorResult = ??? + + override def arraySize(a: Ast.expr): TranslatorResult = ??? + + override def arrayMin(a: Ast.expr): TranslatorResult = ??? + + override def arrayMax(a: Ast.expr): TranslatorResult = ??? + + override def enumToInt(value: Ast.expr, et: EnumType): TranslatorResult = ??? + + override def boolToInt(value: Ast.expr): TranslatorResult = ??? + + def userType(dataType: UserType, io: String) = { + val v = allocateLocalVar() + out.puts(s"${localVarName(v)} := new(${GoCompiler.types2class(dataType.classSpec.get.name)})") + out.puts(s"err = ${localVarName(v)}.Read($io, this, this._root)") + outAddErrCheck() + ResultLocalVar(v) + } + + def outVarCheckRes(expr: String): ResultLocalVar = { + val v1 = allocateLocalVar() + out.puts(s"${localVarName(v1)}, err := $expr") + outAddErrCheck() + ResultLocalVar(v1) + } + + private + var localVarNum = 0 + + def allocateLocalVar(): Int = { + localVarNum += 1 + localVarNum + } + + def localVarName(n: Int) = s"tmp$n" + + def outAddErrCheck() { + out.puts("if err != nil {") + out.inc + + val noValueAndErr = returnRes match { + case None => "err" + case Some(r) => s"$r, err" + } + + out.puts(s"return $noValueAndErr") + out.dec + out.puts("}") + } +} diff --git a/shared/src/main/scala/io/kaitai/struct/translators/JavaScriptTranslator.scala b/shared/src/main/scala/io/kaitai/struct/translators/JavaScriptTranslator.scala index 35c611e85..3605adea4 100644 --- a/shared/src/main/scala/io/kaitai/struct/translators/JavaScriptTranslator.scala +++ b/shared/src/main/scala/io/kaitai/struct/translators/JavaScriptTranslator.scala @@ -4,15 +4,31 @@ import io.kaitai.struct.Utils import io.kaitai.struct.datatype.DataType._ import io.kaitai.struct.exprlang.Ast import io.kaitai.struct.exprlang.Ast.expr +import io.kaitai.struct.format.Identifier import io.kaitai.struct.languages.JavaScriptCompiler class JavaScriptTranslator(provider: TypeProvider) extends BaseTranslator(provider) { + /** + * JavaScript rendition of common control character that would use hex form, + * not octal. "Octal" control character string literals might be accepted + * in non-strict JS mode, but in strict mode only hex or unicode are ok. + * Here we'll use hex, as they are shorter. + * + * @see https://github.com/kaitai-io/kaitai_struct/issues/279 + * @param code character code to represent + * @return string literal representation of given code + */ + override def strLiteralGenericCC(code: Char): String = + "\\x%02x".format(code.toInt) + override def numericBinOp(left: Ast.expr, op: Ast.operator, right: Ast.expr) = { (detectType(left), detectType(right), op) match { case (_: IntType, _: IntType, Ast.operator.Div) => s"Math.floor(${translate(left)} / ${translate(right)})" case (_: IntType, _: IntType, Ast.operator.Mod) => s"${JavaScriptCompiler.kstreamName}.mod(${translate(left)}, ${translate(right)})" + case (_: IntType, _: IntType, Ast.operator.RShift) => + s"(${translate(left)} >>> ${translate(right)})" case _ => super.numericBinOp(left, op, right) } @@ -21,6 +37,8 @@ class JavaScriptTranslator(provider: TypeProvider) extends BaseTranslator(provid override def doLocalName(s: String) = { s match { case "_" => s + case Identifier.SWITCH_ON => "on" + case Identifier.INDEX => "i" case _ => s"this.${doName(s)}" } } @@ -38,6 +56,9 @@ class JavaScriptTranslator(provider: TypeProvider) extends BaseTranslator(provid // Just an integer, without any casts / resolutions - one would have to look up constants manually label + override def doBytesCompareOp(left: Ast.expr, op: Ast.cmpop, right: Ast.expr): String = + s"(${JavaScriptCompiler.kstreamName}.byteArrayCompare(${translate(left)}, ${translate(right)}) ${cmpOp(op)} 0)" + override def doSubscript(container: expr, idx: expr): String = s"${translate(container)}[${translate(idx)}]" override def doIfExp(condition: expr, ifTrue: expr, ifFalse: expr): String = @@ -63,6 +84,20 @@ class JavaScriptTranslator(provider: TypeProvider) extends BaseTranslator(provid override def boolToInt(v: expr): String = s"(${translate(v)} | 0)" + /** + * Converts a float to an integer in JavaScript. There are many methods to + * do so, here we use the fastest one, but it requires ES6+. OTOH, it is + * relatively easy to add compatibility polyfill for non-supporting environments + * (see MDN page). + * + * @see http://stackoverflow.com/a/596503/487064 + * @see https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Math/trunc + * @param v float expression to convert + * @return string rendition of conversion + */ + override def floatToInt(v: expr): String = + s"Math.trunc(${translate(v)})" + override def intToStr(i: expr, base: expr): String = s"(${translate(i)}).toString(${translate(base)})" @@ -87,6 +122,10 @@ class JavaScriptTranslator(provider: TypeProvider) extends BaseTranslator(provid } override def arraySize(a: expr): String = s"${translate(a)}.length" + override def arrayMin(a: expr): String = + s"${JavaScriptCompiler.kstreamName}.arrayMin(${translate(a)})" + override def arrayMax(a: expr): String = + s"${JavaScriptCompiler.kstreamName}.arrayMax(${translate(a)})" override def kaitaiStreamEof(value: Ast.expr): String = s"${translate(value)}.isEof()" diff --git a/shared/src/main/scala/io/kaitai/struct/translators/JavaTranslator.scala b/shared/src/main/scala/io/kaitai/struct/translators/JavaTranslator.scala index 2ba560c6b..9d7dea40e 100644 --- a/shared/src/main/scala/io/kaitai/struct/translators/JavaTranslator.scala +++ b/shared/src/main/scala/io/kaitai/struct/translators/JavaTranslator.scala @@ -1,6 +1,6 @@ package io.kaitai.struct.translators -import io.kaitai.struct.Utils +import io.kaitai.struct.{ImportList, Utils} import io.kaitai.struct.exprlang.Ast import io.kaitai.struct.exprlang.Ast._ import io.kaitai.struct.datatype.DataType @@ -8,7 +8,7 @@ import io.kaitai.struct.datatype.DataType._ import io.kaitai.struct.format.Identifier import io.kaitai.struct.languages.JavaCompiler -class JavaTranslator(provider: TypeProvider) extends BaseTranslator(provider) { +class JavaTranslator(provider: TypeProvider, importList: ImportList) extends BaseTranslator(provider) { override def doIntLiteral(n: BigInt): String = { val literal = n.toString val suffix = if (n > Int.MaxValue) "L" else "" @@ -67,6 +67,8 @@ class JavaTranslator(provider: TypeProvider) extends BaseTranslator(provider) { case Identifier.IO => "_io()" case Identifier.ITERATOR => "_it" case Identifier.ITERATOR2 => "_buf" + case Identifier.SWITCH_ON => "on" + case Identifier.INDEX => "i" case _ => s"${Utils.lowerCamelCase(s)}()" } @@ -90,6 +92,19 @@ class JavaTranslator(provider: TypeProvider) extends BaseTranslator(provider) { } } + override def doBytesCompareOp(left: Ast.expr, op: Ast.cmpop, right: Ast.expr): String = { + op match { + case Ast.cmpop.Eq => + importList.add("java.util.Arrays") + s"Arrays.equals(${translate(left)}, ${translate(right)})" + case Ast.cmpop.NotEq => + importList.add("java.util.Arrays") + s"!Arrays.equals(${translate(left)}, ${translate(right)})" + case _ => + s"(${JavaCompiler.kstreamName}.byteArrayCompare(${translate(left)}, ${translate(right)}) ${cmpOp(op)} 0)" + } + } + override def doSubscript(container: expr, idx: expr): String = { val idxStr = translate(idx); val contStr = translate(container); @@ -100,6 +115,7 @@ class JavaTranslator(provider: TypeProvider) extends BaseTranslator(provider) { s"${contStr}.get(${idxArgStr})" } + override def doIfExp(condition: expr, ifTrue: expr, ifFalse: expr): String = s"(${translate(condition)} ? ${translate(ifTrue)} : ${translate(ifFalse)})" override def doCast(value: Ast.expr, typeName: String): String = @@ -110,10 +126,14 @@ class JavaTranslator(provider: TypeProvider) extends BaseTranslator(provider) { s"Long.parseLong(${translate(s)}, ${translate(base)})" override def enumToInt(v: expr, et: EnumType): String = s"${translate(v)}.id()" + override def floatToInt(v: expr): String = + s"(int) (${translate(v)} + 0)" override def intToStr(i: expr, base: expr): String = s"Long.toString(${translate(i)}, ${translate(base)})" - override def bytesToStr(bytesExpr: String, encoding: Ast.expr): String = + override def bytesToStr(bytesExpr: String, encoding: Ast.expr): String = { + importList.add("java.nio.charset.Charset") s"new String($bytesExpr, Charset.forName(${translate(encoding)}))" + } override def strLength(s: expr): String = s"${translate(s)}.length()" override def strReverse(s: expr): String = @@ -129,4 +149,12 @@ class JavaTranslator(provider: TypeProvider) extends BaseTranslator(provider) { } override def arraySize(a: expr): String = s"${translate(a)}.size()" + override def arrayMin(a: Ast.expr): String = { + importList.add("java.util.Collections") + s"Collections.min(${translate(a)})" + } + override def arrayMax(a: Ast.expr): String = { + importList.add("java.util.Collections") + s"Collections.max(${translate(a)})" + } } diff --git a/shared/src/main/scala/io/kaitai/struct/translators/LuaTranslator.scala b/shared/src/main/scala/io/kaitai/struct/translators/LuaTranslator.scala new file mode 100644 index 000000000..f854a534b --- /dev/null +++ b/shared/src/main/scala/io/kaitai/struct/translators/LuaTranslator.scala @@ -0,0 +1,157 @@ +package io.kaitai.struct.translators + +import io.kaitai.struct.ImportList +import io.kaitai.struct.datatype.DataType +import io.kaitai.struct.datatype.DataType._ +import io.kaitai.struct.format.Identifier +import io.kaitai.struct.exprlang.Ast +import io.kaitai.struct.languages.LuaCompiler + +class LuaTranslator(provider: TypeProvider, importList: ImportList) extends BaseTranslator(provider) { + override val asciiCharQuoteMap: Map[Char, String] = Map( + '\t' -> "\\t", + '\n' -> "\\n", + '\r' -> "\\r", + '"' -> "\\\"", + '\\' -> "\\\\", + + '\7' -> "\\a", + '\b' -> "\\b", + '\13' -> "\\v", + '\f' -> "\\f", + '\33' -> "\\027" + ) + + override def strLiteralUnicode(code: Char): String = + "\\u{%04x}".format(code.toInt) + + override def doSubscript(container: Ast.expr, idx: Ast.expr): String = { + // Lua indexes start at 1, so we need to offset them + val fixedIdx = idx match { + case Ast.expr.IntNum(n) => Ast.expr.IntNum(n + 1) + case _ => idx + } + + s"${translate(container)}[${translate(fixedIdx)}]" + } + override def doIfExp(condition: Ast.expr, ifTrue: Ast.expr, ifFalse: Ast.expr): String = + s"(((${translate(condition)}) and (${translate(ifTrue)})) or (${translate(ifFalse)}))" + + override def doBoolLiteral(n: Boolean): String = + if (n) "true" else "false" + override def doArrayLiteral(t: DataType, value: Seq[Ast.expr]): String = + "{" + value.map((v) => translate(v)).mkString(", ") + "}" + override def doByteArrayLiteral(arr: Seq[Byte]): String = + "\"" + decEscapeByteArray(arr) + "\"" + + override def doLocalName(s: String) = s match { + case Identifier.ITERATOR => "_" + case Identifier.INDEX => "i" + case _ => s"self.${doName(s)}" + } + override def doName(s: String): String = + s + override def doEnumByLabel(enumTypeAbs: List[String], label: String): String = + s"${LuaCompiler.types2class(enumTypeAbs)}.$label" + override def doEnumById(enumTypeAbs: List[String], id: String): String = + s"${LuaCompiler.types2class(enumTypeAbs)}($id)" + + // This is very hacky because integers and booleans cannot be compared + override def doNumericCompareOp(left: Ast.expr, op: Ast.cmpop, right: Ast.expr): String = { + val bool2Int = (n: Boolean) => { if (n) "1" else "0" } + (left, right) match { + case (Ast.expr.Bool(l), Ast.expr.Bool(r)) => s"${bool2Int(l)} ${cmpOp(op)} ${bool2Int(r)}" + case (Ast.expr.Bool(l), r) => s"${bool2Int(l)} ${cmpOp(op)} ${translate(r)}" + case (l, Ast.expr.Bool(r)) => s"${translate(l)} ${cmpOp(op)} ${bool2Int(r)}" + case _ => super.doNumericCompareOp(left, op, right) + } + } + + override def strConcat(left: Ast.expr, right: Ast.expr): String = + s"${translate(left)} .. ${translate(right)}" + override def strToInt(s: Ast.expr, base: Ast.expr): String = { + val baseStr = translate(base) + val add = baseStr match { + case "10" => "" + case _ => s", $baseStr" + } + s"tonumber(${translate(s)}$add)" + } + override def enumToInt(v: Ast.expr, et: EnumType): String = + s"${translate(v)}.value" + override def boolToInt(v: Ast.expr): String = + s"(${translate(v)} and 1 or 0)" + override def floatToInt(v: Ast.expr): String = + s"(${translate(v)} > 0) and math.floor(${translate(v)}) or math.ceil(${translate(v)})" + override def intToStr(i: Ast.expr, base: Ast.expr): String = { + val baseStr = translate(base) + baseStr match { + case "10" => s"tostring(${translate(i)})" + case _ => throw new UnsupportedOperationException(baseStr) + } + } + override def bytesToStr(bytesExpr: String, encoding: Ast.expr): String = { + importList.add("local str_decode = require(\"string_decode\")") + + s"str_decode.decode($bytesExpr, ${translate(encoding)})" + } + override def strLength(s: Ast.expr): String = + s"string.len(${translate(s)})" + override def strReverse(s: Ast.expr): String = + s"string.reverse(${translate(s)})" + override def strSubstring(s: Ast.expr, from: Ast.expr, to: Ast.expr): String = + s"string.sub(${translate(s)}, ${translate(from)}, ${translate(to)})" + + override def arrayFirst(a: Ast.expr): String = + s"${translate(a)}[1]" + override def arrayLast(a: Ast.expr): String = { + val table = translate(a) + s"${table}[#${table}]" + } + override def arraySize(a: Ast.expr): String = + s"#${translate(a)}" + override def arrayMin(a: Ast.expr): String = { + importList.add("local utils = require(\"utils\")") + + s"utils.array_min(${translate(a)})" + } + override def arrayMax(a: Ast.expr): String ={ + importList.add("local utils = require(\"utils\")") + + s"utils.array_max(${translate(a)})" + } + + override def kaitaiStreamSize(value: Ast.expr): String = + s"${translate(value)}:size()" + override def kaitaiStreamEof(value: Ast.expr): String = + s"${translate(value)}:is_eof()" + override def kaitaiStreamPos(value: Ast.expr): String = + s"${translate(value)}:pos()" + + override def binOp(op: Ast.operator): String = op match { + case Ast.operator.BitXor => "~" + case _ => super.binOp(op) + } + override def cmpOp(op: Ast.cmpop): String = op match { + case Ast.cmpop.NotEq => "~=" + case _ => super.cmpOp(op) + } + override def booleanOp(op: Ast.boolop): String = op match { + case Ast.boolop.Or => "or" + case Ast.boolop.And => "and" + } + override def unaryOp(op: Ast.unaryop): String = op match { + case Ast.unaryop.Not => "not" + case _ => super.unaryOp(op) + } + + /** + * Converts byte array (Seq[Byte]) into decimal-escaped Lua-style literal + * characters (i.e. like \255). + * + * @param arr byte array to escape + * @return array contents decimal-escaped as string + */ + private def decEscapeByteArray(arr: Seq[Byte]): String = + arr.map((x) => "\\%03d".format(x & 0xff)).mkString +} diff --git a/shared/src/main/scala/io/kaitai/struct/translators/PHPTranslator.scala b/shared/src/main/scala/io/kaitai/struct/translators/PHPTranslator.scala index 404131a26..4b0aa9699 100644 --- a/shared/src/main/scala/io/kaitai/struct/translators/PHPTranslator.scala +++ b/shared/src/main/scala/io/kaitai/struct/translators/PHPTranslator.scala @@ -41,13 +41,14 @@ class PHPTranslator(provider: TypeProvider, config: RuntimeConfig) extends BaseT } } - override def userTypeField(value: expr, attrName: String): String = + override def anyField(value: expr, attrName: String): String = s"${translate(value)}->${doName(attrName)}" override def doLocalName(s: String) = { s match { case Identifier.ITERATOR => "$_" case Identifier.ITERATOR2 => "$_buf" + case Identifier.INDEX => "$i" case _ => s"$$this->${doName(s)}" } } @@ -80,6 +81,9 @@ class PHPTranslator(provider: TypeProvider, config: RuntimeConfig) extends BaseT override def boolToInt(v: expr): String = s"intval(${translate(v)})" + override def floatToInt(v: expr): String = + s"intval(${translate(v)})" + override def intToStr(i: expr, base: expr): String = { val baseStr = translate(base) baseStr match { @@ -101,11 +105,17 @@ class PHPTranslator(provider: TypeProvider, config: RuntimeConfig) extends BaseT override def arrayFirst(a: expr): String = s"${translate(a)}[0]" override def arrayLast(a: expr): String = { + // For huge debate on efficiency of PHP last element of array methods, see: + // http://stackoverflow.com/a/41795859/487064 val v = translate(a) - s"$v[$v.length - 1]" + s"$v[count($v) - 1]" } override def arraySize(a: expr): String = s"count(${translate(a)})" + override def arrayMin(a: Ast.expr): String = + s"min(${translate(a)})" + override def arrayMax(a: Ast.expr): String = + s"max(${translate(a)})" val namespaceRef = if (config.phpNamespace.isEmpty) { "" diff --git a/shared/src/main/scala/io/kaitai/struct/translators/PerlTranslator.scala b/shared/src/main/scala/io/kaitai/struct/translators/PerlTranslator.scala index a7f95d06d..cc6c30ef7 100644 --- a/shared/src/main/scala/io/kaitai/struct/translators/PerlTranslator.scala +++ b/shared/src/main/scala/io/kaitai/struct/translators/PerlTranslator.scala @@ -1,12 +1,12 @@ package io.kaitai.struct.translators +import io.kaitai.struct.ImportList import io.kaitai.struct.datatype.DataType import io.kaitai.struct.datatype.DataType._ import io.kaitai.struct.exprlang.Ast -import io.kaitai.struct.exprlang.Ast.expr import io.kaitai.struct.format.Identifier -class PerlTranslator(provider: TypeProvider) extends BaseTranslator(provider) { +class PerlTranslator(provider: TypeProvider, importList: ImportList) extends BaseTranslator(provider) { // http://perldoc.perl.org/perlrebackslash.html#Character-Escapes override val asciiCharQuoteMap: Map[Char, String] = Map( '\t' -> "\\t", @@ -44,18 +44,19 @@ class PerlTranslator(provider: TypeProvider) extends BaseTranslator(provider) { override def doBoolLiteral(n: Boolean): String = if (n) "1" else "0" - override def doArrayLiteral(t: DataType, value: Seq[expr]): String = + override def doArrayLiteral(t: DataType, value: Seq[Ast.expr]): String = "(" + value.map((v) => translate(v)).mkString(", ") + ")" override def doByteArrayLiteral(arr: Seq[Byte]): String = s"pack('C*', (${arr.map(_ & 0xff).mkString(", ")}))" - override def userTypeField(value: expr, attrName: String): String = + override def anyField(value: Ast.expr, attrName: String): String = s"${translate(value)}->${doName(attrName)}" override def doLocalName(s: String) = { s match { case "_" | "_on" => "$" + s + case Identifier.INDEX => doName(s) case _ => s"$$self->${doName(s)}" } } @@ -64,6 +65,7 @@ class PerlTranslator(provider: TypeProvider) extends BaseTranslator(provider) { s match { case Identifier.ITERATOR => "$_" case Identifier.ITERATOR2 => "$_buf" + case Identifier.INDEX => "$i" case _ => s"$s()" } } @@ -86,9 +88,12 @@ class PerlTranslator(provider: TypeProvider) extends BaseTranslator(provider) { s"${translate(left)} $opStr ${translate(right)}" } - override def doSubscript(container: expr, idx: expr): String = - s"${translate(container)}[${translate(idx)}]" - override def doIfExp(condition: expr, ifTrue: expr, ifFalse: expr): String = + override def doBytesCompareOp(left: Ast.expr, op: Ast.cmpop, right: Ast.expr): String = + doStrCompareOp(left, op, right) + + override def doSubscript(container: Ast.expr, idx: Ast.expr): String = + s"@{${translate(container)}}[${translate(idx)}]" + override def doIfExp(condition: Ast.expr, ifTrue: Ast.expr, ifFalse: Ast.expr): String = s"(${translate(condition)} ? ${translate(ifTrue)} : ${translate(ifFalse)})" // Predefined methods of various types @@ -106,10 +111,12 @@ class PerlTranslator(provider: TypeProvider) extends BaseTranslator(provider) { case _ => throw new UnsupportedOperationException(baseStr) } } - override def enumToInt(v: expr, et: EnumType): String = + override def enumToInt(v: Ast.expr, et: EnumType): String = translate(v) - override def boolToInt(v: expr): String = + override def boolToInt(v: Ast.expr): String = translate(v) + override def floatToInt(v: Ast.expr): String = + s"int(${translate(v)})" override def intToStr(i: Ast.expr, base: Ast.expr): String = { val baseStr = translate(base) val format = baseStr match { @@ -124,8 +131,10 @@ class PerlTranslator(provider: TypeProvider) extends BaseTranslator(provider) { s"sprintf('$format', ${translate(i)})" } - override def bytesToStr(bytesExpr: String, encoding: Ast.expr): String = + override def bytesToStr(bytesExpr: String, encoding: Ast.expr): String = { + importList.add("Encode") s"Encode::decode(${translate(encoding)}, $bytesExpr)" + } override def strLength(value: Ast.expr): String = s"length(${translate(value)})" override def strReverse(value: Ast.expr): String = @@ -133,12 +142,28 @@ class PerlTranslator(provider: TypeProvider) extends BaseTranslator(provider) { override def strSubstring(s: Ast.expr, from: Ast.expr, to: Ast.expr): String = s"${translate(s)}[${translate(from)}:${translate(to)}]" - override def arrayFirst(a: expr): String = - s"${translate(a)}[0]" - override def arrayLast(a: expr): String = - s"${translate(a)}[-1]" - override def arraySize(a: expr): String = - s"scalar(${translate(a)})" + override def arrayFirst(a: Ast.expr): String = + s"@{${translate(a)}}[0]" + override def arrayLast(a: Ast.expr): String = + s"@{${translate(a)}}[-1]" + override def arraySize(a: Ast.expr): String = + s"scalar(@{${translate(a)}})" + override def arrayMin(a: Ast.expr): String = { + val funcName = detectType(a).asInstanceOf[ArrayType].elType match { + case _: StrType => "minstr" + case _ => "min" + } + importList.add("List::Util") + s"List::Util::$funcName(@{${translate(a)}})" + } + override def arrayMax(a: Ast.expr): String = { + val funcName = detectType(a).asInstanceOf[ArrayType].elType match { + case _: StrType => "maxstr" + case _ => "max" + } + importList.add("List::Util") + s"List::Util::$funcName(@{${translate(a)}})" + } override def kaitaiStreamSize(value: Ast.expr): String = s"${translate(value)}->size()" diff --git a/shared/src/main/scala/io/kaitai/struct/translators/PythonTranslator.scala b/shared/src/main/scala/io/kaitai/struct/translators/PythonTranslator.scala index 90f183323..ef6fa6b63 100644 --- a/shared/src/main/scala/io/kaitai/struct/translators/PythonTranslator.scala +++ b/shared/src/main/scala/io/kaitai/struct/translators/PythonTranslator.scala @@ -1,10 +1,12 @@ package io.kaitai.struct.translators +import io.kaitai.struct.{ImportList, Utils} import io.kaitai.struct.datatype.DataType._ import io.kaitai.struct.exprlang.Ast +import io.kaitai.struct.format.Identifier import io.kaitai.struct.languages.PythonCompiler -class PythonTranslator(provider: TypeProvider) extends BaseTranslator(provider) { +class PythonTranslator(provider: TypeProvider, importList: ImportList) extends BaseTranslator(provider) { override def numericBinOp(left: Ast.expr, op: Ast.operator, right: Ast.expr) = { (detectType(left), detectType(right), op) match { case (_: IntType, _: IntType, Ast.operator.Div) => @@ -34,12 +36,14 @@ class PythonTranslator(provider: TypeProvider) extends BaseTranslator(provider) '\b' -> "\\b" ) - override def doByteArrayLiteral(arr: Seq[Byte]): String = - s"struct.pack('${arr.length}b', ${arr.mkString(", ")})" + override def doByteArrayLiteral(arr: Seq[Byte]): String = { + "b\"" + Utils.hexEscapeByteArray(arr) + "\"" + } override def doLocalName(s: String) = { s match { - case "_" => s + case Identifier.ITERATOR => "_" + case Identifier.INDEX => "i" case _ => s"self.${doName(s)}" } } @@ -78,6 +82,8 @@ class PythonTranslator(provider: TypeProvider) extends BaseTranslator(provider) s"${translate(v)}.value" override def boolToInt(v: Ast.expr): String = s"int(${translate(v)})" + override def floatToInt(v: Ast.expr): String = + s"int(${translate(v)})" override def intToStr(i: Ast.expr, base: Ast.expr): String = { val baseStr = translate(base) val func = baseStr match { @@ -105,6 +111,10 @@ class PythonTranslator(provider: TypeProvider) extends BaseTranslator(provider) s"${translate(a)}[-1]" override def arraySize(a: Ast.expr): String = s"len(${translate(a)})" + override def arrayMin(a: Ast.expr): String = + s"min(${translate(a)})" + override def arrayMax(a: Ast.expr): String = + s"max(${translate(a)})" override def kaitaiStreamSize(value: Ast.expr): String = s"${translate(value)}.size()" diff --git a/shared/src/main/scala/io/kaitai/struct/translators/RubyTranslator.scala b/shared/src/main/scala/io/kaitai/struct/translators/RubyTranslator.scala index d8c4c46ca..52975cee5 100644 --- a/shared/src/main/scala/io/kaitai/struct/translators/RubyTranslator.scala +++ b/shared/src/main/scala/io/kaitai/struct/translators/RubyTranslator.scala @@ -2,6 +2,8 @@ package io.kaitai.struct.translators import io.kaitai.struct.datatype.DataType.EnumType import io.kaitai.struct.exprlang.Ast +import io.kaitai.struct.exprlang.Ast.expr +import io.kaitai.struct.format.Identifier import io.kaitai.struct.languages.RubyCompiler class RubyTranslator(provider: TypeProvider) extends BaseTranslator(provider) { @@ -25,7 +27,12 @@ class RubyTranslator(provider: TypeProvider) extends BaseTranslator(provider) { '\b' -> "\\b" ) - override def doName(s: String) = s + override def doName(s: String) = { + s match { + case Identifier.INDEX => "i" // FIXME: probably would clash with attribute named "i" + case _ => s + } + } override def doEnumByLabel(enumTypeAbs: List[String], label: String): String = s":${enumTypeAbs.last}_$label" @@ -47,6 +54,8 @@ class RubyTranslator(provider: TypeProvider) extends BaseTranslator(provider) { } override def enumToInt(v: Ast.expr, et: EnumType): String = s"${RubyCompiler.inverseEnumName(et.name.last.toUpperCase)}[${translate(v)}]" + override def floatToInt(v: Ast.expr): String = + s"(${translate(v)}).to_i" override def intToStr(i: Ast.expr, base: Ast.expr): String = translate(i) + s".to_s(${translate(base)})" override def bytesToStr(bytesExpr: String, encoding: Ast.expr): String = @@ -64,6 +73,10 @@ class RubyTranslator(provider: TypeProvider) extends BaseTranslator(provider) { s"${translate(a)}.last" override def arraySize(a: Ast.expr): String = s"${translate(a)}.length" + override def arrayMin(a: expr): String = + s"${translate(a)}.min" + override def arrayMax(a: expr): String = + s"${translate(a)}.max" override def kaitaiStreamEof(value: Ast.expr): String = s"${translate(value)}.eof?" diff --git a/shared/src/main/scala/io/kaitai/struct/translators/TypeDetector.scala b/shared/src/main/scala/io/kaitai/struct/translators/TypeDetector.scala index c3ec8caa5..89be684ba 100644 --- a/shared/src/main/scala/io/kaitai/struct/translators/TypeDetector.scala +++ b/shared/src/main/scala/io/kaitai/struct/translators/TypeDetector.scala @@ -15,7 +15,26 @@ import io.kaitai.struct.precompile.{TypeMismatchError, TypeUndecidedError} class TypeDetector(provider: TypeProvider) { import TypeDetector._ + /** + * Detects type of a given expression. If it returns a SwitchType, it + * effectively flattens it to a resulting combined type. + * @param v expression + * @return data type + */ def detectType(v: Ast.expr): DataType = { + detectTypeRaw(v) match { + case st: SwitchType => st.combinedType + case other => other + } + } + + /** + * Detects type of a given expression, raw, without any switch type + * flattening. + * @param v expression + * @return data type + */ + def detectTypeRaw(v: Ast.expr): DataType = { v match { case Ast.expr.IntNum(x) => if (x < 0 || x > 255) { @@ -110,9 +129,14 @@ class TypeDetector(provider: TypeProvider) { case "to_s" => CalcStrType case _ => throw new TypeMismatchError(s"called invalid attribute '${attr.name}' on expression of type $valType") } + case _: FloatType => + attr.name match { + case "to_i" => CalcIntType + case _ => throw new TypeMismatchError(s"called invalid attribute '${attr.name}' on expression of type $valType") + } case ArrayType(inType) => attr.name match { - case "first" | "last" => inType + case "first" | "last" | "min" | "max" => inType case "size" => CalcIntType case _ => throw new TypeMismatchError(s"called invalid attribute '${attr.name}' on expression of type $valType") } @@ -195,6 +219,7 @@ object TypeDetector { case (_: StrType, _: StrType) => // ok case (_: NumericType, _: NumericType) => // ok case (_: BooleanType, _: BooleanType) => // ok + case (_: BytesType, _: BytesType) => // ok case (EnumType(name1, _), EnumType(name2, _)) => if (name1 != name2) { throw new TypeMismatchError(s"can't compare different enums '$name1' and '$name2'") @@ -242,6 +267,7 @@ object TypeDetector { } case (_: IntType, _: IntType) => CalcIntType case (_: NumericType, _: NumericType) => CalcFloatType + case (_: BytesType, _: BytesType) => CalcBytesType case (t1: UserType, t2: UserType) => // Two user types can differ in reserved size and/or processing, but that doesn't matter in case of // type combining - we treat them the same as long as they result in same class spec or have same @@ -285,10 +311,49 @@ object TypeDetector { * @return type that can accommodate values of both source types without any data loss */ def combineTypesAndFail(t1: DataType, t2: DataType): DataType = { - combineTypes(t1, t2) match { - case AnyType => - throw new TypeMismatchError(s"can't combine output types: $t1 vs $t2") - case ct => ct + if (t1 == AnyType || t2 == AnyType) { + // combining existing AnyTypes is not a crime :) + AnyType + } else { + combineTypes(t1, t2) match { + case AnyType => + throw new TypeMismatchError(s"can't combine output types: $t1 vs $t2") + case ct => ct + } + } + } + + /** + * Returns true if one can assign value of type `src` into a variable / parameter of type `dst`. + * @param src data type of source value to be assigned + * @param dst destination data type to be assigned into + * @return true if assign if possible + */ + def canAssign(src: DataType, dst: DataType): Boolean = { + if (src == dst) { + // Obviously, if types are equal, they'll fit into one another + true + } else { + (src, dst) match { + case (_, AnyType) => true + case (_: IntType, _: IntType) => true + case (_: FloatType, _: FloatType) => true + case (_: BooleanType, _: BooleanType) => true + case (_: StrType, _: StrType) => true + case (_: UserType, KaitaiStructType) => true + case (t1: UserType, t2: UserType) => + (t1.classSpec, t2.classSpec) match { + case (None, None) => + // opaque classes are assignable if their names match + t1.name == t2.name + case (Some(cs1), Some(cs2)) => + // normal user types are assignable if their class specs match + cs1 == cs2 + case (_, _) => + false + } + case (_, _) => false + } } } } diff --git a/shared/src/main/scala/io/kaitai/struct/translators/TypeProvider.scala b/shared/src/main/scala/io/kaitai/struct/translators/TypeProvider.scala index 3a9135b4d..74f9207d5 100644 --- a/shared/src/main/scala/io/kaitai/struct/translators/TypeProvider.scala +++ b/shared/src/main/scala/io/kaitai/struct/translators/TypeProvider.scala @@ -14,4 +14,6 @@ trait TypeProvider { def determineType(inClass: ClassSpec, attrName: String): DataType def resolveEnum(enumName: String): EnumSpec def resolveType(typeName: String): DataType + def isLazy(attrName: String): Boolean + def isLazy(inClass: ClassSpec, attrName: String): Boolean }