diff --git a/.github/workflows/build.yml b/.github/workflows/build-gradle.yml similarity index 53% rename from .github/workflows/build.yml rename to .github/workflows/build-gradle.yml index 4f70fa6..f166543 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build-gradle.yml @@ -7,12 +7,14 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - java: [ '8.x', '9.x', '10.x', '11.x', '12.x', '13.x' ] + java: [ '13.x' ] steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v3 - name: Set up JDK ${{ matrix.java }} uses: actions/setup-java@v1 with: java-version: ${{ matrix.java }} - - name: Build with Maven - run: mvn -B package --file pom.xml + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 + - name: Run build with Gradle Wrapper + run: chmod +x ./gradlew && ./gradlew build diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e0fd471 --- /dev/null +++ b/.gitignore @@ -0,0 +1,43 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ +.versions/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/Example.png b/Example.png new file mode 100644 index 0000000..fa0c17d Binary files /dev/null and b/Example.png differ diff --git a/README.md b/README.md index 3ea7272..e3ee1f6 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,12 @@ Its functionality is akin to similar projects: ... but this Java version is significantly simpler and is no longer under active development. +> Note: This tool is a CLI tool that provides a GUI interface. + +## Overview + +![Example Visual](docs/Example.png) + 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. @@ -26,12 +32,39 @@ that can read described data structure from a file / stream and give access to it in a nice, easy-to-comprehend API. ## Build -Install java, maven and, if on windows, git-bash +Install java, gradle and, if on windows, git-bash Run in console: ```bash -mvn install +gradlew build KaitaiStructGUI +``` + +## How to use + +Since this tool is accessed via the command line, here is a quick introduction on how to use it. For example: + +```bash +java -jar ./build/libs/kaitai_struct_gui-0.11.jar -ksy ./docs/png.ksy ./docs/Example.png +``` + +> Note: To apply dark-mode, just use `-dark` as an extra argument. + +### Parameter overview + +```text +Usage: [options] Path to the binary + Options: + -h, --help + Show this help message. + Default: false + -dark + Enables dark layout. + Default: false + -java + Path to generated Java file + -ksy + Path to the .ksy file ``` ## Licensing @@ -59,4 +92,8 @@ Vis tool depends on the following libraries: * [kaitai_struct_compiler](https://github.com/kaitai_struct_compiler) — GPLv3+ license * [fastparse](http://www.lihaoyi.com/fastparse/) — MIT license * [snakeyaml](https://bitbucket.org/asomov/snakeyaml) — Apache 2.0 license -* [JHexView](https://github.com/Mingun/JHexView) — LGPL-2.1 license +* [JHexViewer](https://github.com/rendner/jhexviewer) — MIT license +* [Proto4j-IconManager](https://github.com/Proto4j/proto4j-iconmgr) — MIT License +* [Apache-Batik](https://xmlgraphics.apache.org/batik) — Apache 2.0 license +* [JetBrains-Icons](https://jetbrains.design/intellij/resources/icons_list/) — Apache 2.0 license +* [JCommander](https://jcommander.org) — Apache 2.0 license \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..3641dae --- /dev/null +++ b/build.gradle @@ -0,0 +1,73 @@ +plugins { + id 'java' + +} + +group 'io.kaitai' +version '0.11' + +compileJava.options.encoding = 'UTF-8' +// Version 13 is needed because proto4j-iconmgr is available +// only at version 13+ +sourceCompatibility = 13 +targetCompatibility = 13 + +repositories { + mavenCentral() + +} + +dependencies { + // Library used for displaying SVG images + implementation 'org.apache.xmlgraphics:batik-all:1.16' + + // Just comment out this line and the library won't be included in the + // project build. + implementation 'com.formdev:flatlaf:2.6' + + implementation fileTree('lib') + implementation 'io.kaitai:kaitai-struct-runtime:0.10' + implementation 'io.kaitai:kaitai-struct-compiler_2.12:0.10' + implementation 'com.beust:jcommander:1.72' + implementation 'io.github.proto4j:proto4j-iconmgr:0.1' + implementation 'org.mdkt.compiler:InMemoryJavaCompiler:1.3.0' + + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' +} + + +jar { + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + manifest { + attributes('Main-Class': 'io.kaitai.struct.visualizer.Main') + } +} + +//noinspection ConfigurationAvoidance +task KaitaiStructGUI(type: Jar) { + manifest.from jar.manifest + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + from { + configurations.runtimeClasspath.collect { + it.isDirectory() ? it : zipTree(it) + } + } { + // preventing signed jars into the final JAR + exclude "META-INF/*.SF" + exclude "META-INF/*.DSA" + exclude "META-INF/*.RSA" + } + with jar +} + +java { + withSourcesJar() + // Comment out the next statement if you don't want ot generate + // the Javadoc files + // withJavadocJar() +} + +test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/docs/Example.png b/docs/Example.png new file mode 100644 index 0000000..b55cc39 Binary files /dev/null and b/docs/Example.png differ diff --git a/docs/Example_dark.png b/docs/Example_dark.png new file mode 100644 index 0000000..6912fd7 Binary files /dev/null and b/docs/Example_dark.png differ diff --git a/docs/png.ksy b/docs/png.ksy new file mode 100644 index 0000000..16bc151 --- /dev/null +++ b/docs/png.ksy @@ -0,0 +1,428 @@ +meta: + id: png + title: PNG (Portable Network Graphics) file + file-extension: + - png + - apng + xref: + forensicswiki: Portable_Network_Graphics_(PNG) + iso: 15948:2004 + justsolve: + - PNG + - APNG + loc: fdd000153 + mime: + - image/png + - image/apng + - image/vnd.mozilla.apng + pronom: + - fmt/11 # PNG 1.0 + - fmt/12 # PNG 1.1 + - fmt/13 # PNG 1.2 + - fmt/935 # APNG + rfc: 2083 + wikidata: + - Q178051 # PNG + - Q433224 # APNG + license: CC0-1.0 + ks-version: 0.9 + endian: be +doc: | + Test files for APNG can be found at the following locations: + + * + * +seq: + # https://www.w3.org/TR/PNG/#5PNG-file-signature + - id: magic + contents: [137, 80, 78, 71, 13, 10, 26, 10] + # https://www.w3.org/TR/PNG/#11IHDR + # Always appears first, stores values referenced by other chunks + - id: ihdr_len + type: u4 + valid: 13 + - id: ihdr_type + contents: "IHDR" + - id: ihdr + type: ihdr_chunk + - id: ihdr_crc + size: 4 + # The rest of the chunks + - id: chunks + type: chunk + repeat: until + repeat-until: _.type == "IEND" or _io.eof +types: + chunk: + seq: + - id: len + type: u4 + - id: type + type: str + size: 4 + encoding: UTF-8 + - id: body + size: len + type: + switch-on: type + cases: + # Critical chunks + # '"IHDR"': ihdr_chunk + '"PLTE"': plte_chunk + # IDAT = raw + # IEND = empty, thus raw + + # Ancillary chunks + '"cHRM"': chrm_chunk + '"gAMA"': gama_chunk + # iCCP + # sBIT + '"sRGB"': srgb_chunk + '"bKGD"': bkgd_chunk + # hIST + # tRNS + '"pHYs"': phys_chunk + # sPLT + '"tIME"': time_chunk + '"iTXt"': international_text_chunk + '"tEXt"': text_chunk + '"zTXt"': compressed_text_chunk + + # animated PNG chunks + '"acTL"': animation_control_chunk + '"fcTL"': frame_control_chunk + '"fdAT"': frame_data_chunk + - id: crc + size: 4 + ihdr_chunk: + doc-ref: https://www.w3.org/TR/PNG/#11IHDR + seq: + - id: width + type: u4 + - id: height + type: u4 + - id: bit_depth + type: u1 + - id: color_type + type: u1 + enum: color_type + - id: compression_method + type: u1 + - id: filter_method + type: u1 + - id: interlace_method + type: u1 + plte_chunk: + doc-ref: https://www.w3.org/TR/PNG/#11PLTE + seq: + - id: entries + type: rgb + repeat: eos + rgb: + seq: + - id: r + type: u1 + - id: g + type: u1 + - id: b + type: u1 + chrm_chunk: + doc-ref: https://www.w3.org/TR/PNG/#11cHRM + seq: + - id: white_point + type: point + - id: red + type: point + - id: green + type: point + - id: blue + type: point + point: + seq: + - id: x_int + type: u4 + - id: y_int + type: u4 + instances: + x: + value: x_int / 100000.0 + y: + value: y_int / 100000.0 + gama_chunk: + doc-ref: https://www.w3.org/TR/PNG/#11gAMA + seq: + - id: gamma_int + type: u4 + instances: + gamma_ratio: + value: 100000.0 / gamma_int + srgb_chunk: + doc-ref: https://www.w3.org/TR/PNG/#11sRGB + seq: + - id: render_intent + type: u1 + enum: intent + enums: + intent: + 0: perceptual + 1: relative_colorimetric + 2: saturation + 3: absolute_colorimetric + bkgd_chunk: + doc: | + Background chunk stores default background color to display this + image against. Contents depend on `color_type` of the image. + doc-ref: https://www.w3.org/TR/PNG/#11bKGD + seq: + - id: bkgd + type: + switch-on: _root.ihdr.color_type + cases: + color_type::greyscale: bkgd_greyscale + color_type::greyscale_alpha: bkgd_greyscale + color_type::truecolor: bkgd_truecolor + color_type::truecolor_alpha: bkgd_truecolor + color_type::indexed: bkgd_indexed + bkgd_greyscale: + doc: Background chunk for greyscale images. + seq: + - id: value + type: u2 + bkgd_truecolor: + doc: Background chunk for truecolor images. + seq: + - id: red + type: u2 + - id: green + type: u2 + - id: blue + type: u2 + bkgd_indexed: + doc: Background chunk for images with indexed palette. + seq: + - id: palette_index + type: u1 + phys_chunk: + doc: | + "Physical size" chunk stores data that allows to translate + logical pixels into physical units (meters, etc) and vice-versa. + doc-ref: https://www.w3.org/TR/PNG/#11pHYs + seq: + - id: pixels_per_unit_x + type: u4 + doc: | + Number of pixels per physical unit (typically, 1 meter) by X + axis. + - id: pixels_per_unit_y + type: u4 + doc: | + Number of pixels per physical unit (typically, 1 meter) by Y + axis. + - id: unit + type: u1 + enum: phys_unit + time_chunk: + doc: | + Time chunk stores time stamp of last modification of this image, + up to 1 second precision in UTC timezone. + doc-ref: https://www.w3.org/TR/PNG/#11tIME + seq: + - id: year + type: u2 + - id: month + type: u1 + - id: day + type: u1 + - id: hour + type: u1 + - id: minute + type: u1 + - id: second + type: u1 + international_text_chunk: + doc: | + International text chunk effectively allows to store key-value string pairs in + PNG container. Both "key" (keyword) and "value" (text) parts are + given in pre-defined subset of iso8859-1 without control + characters. + doc-ref: https://www.w3.org/TR/PNG/#11iTXt + seq: + - id: keyword + type: strz + encoding: UTF-8 + doc: Indicates purpose of the following text data. + - id: compression_flag + type: u1 + doc: | + 0 = text is uncompressed, 1 = text is compressed with a + method specified in `compression_method`. + - id: compression_method + type: u1 + enum: compression_methods + - id: language_tag + type: strz + encoding: ASCII + doc: | + Human language used in `translated_keyword` and `text` + attributes - should be a language code conforming to ISO + 646.IRV:1991. + - id: translated_keyword + type: strz + encoding: UTF-8 + doc: | + Keyword translated into language specified in + `language_tag`. Line breaks are not allowed. + - id: text + type: str + encoding: UTF-8 + size-eos: true + doc: | + Text contents ("value" of this key-value pair), written in + language specified in `language_tag`. Line breaks are + allowed. + text_chunk: + doc: | + Text chunk effectively allows to store key-value string pairs in + PNG container. Both "key" (keyword) and "value" (text) parts are + given in pre-defined subset of iso8859-1 without control + characters. + doc-ref: https://www.w3.org/TR/PNG/#11tEXt + seq: + - id: keyword + type: strz + encoding: iso8859-1 + doc: Indicates purpose of the following text data. + - id: text + type: str + size-eos: true + encoding: iso8859-1 + compressed_text_chunk: + doc: | + Compressed text chunk effectively allows to store key-value + string pairs in PNG container, compressing "value" part (which + can be quite lengthy) with zlib compression. + doc-ref: https://www.w3.org/TR/PNG/#11zTXt + seq: + - id: keyword + type: strz + encoding: UTF-8 + doc: Indicates purpose of the following text data. + - id: compression_method + type: u1 + enum: compression_methods + - id: text_datastream + process: zlib + size-eos: true + animation_control_chunk: + doc-ref: https://wiki.mozilla.org/APNG_Specification#.60acTL.60:_The_Animation_Control_Chunk + seq: + - id: num_frames + type: u4 + doc: Number of frames, must be equal to the number of `frame_control_chunk`s + - id: num_plays + type: u4 + doc: Number of times to loop, 0 indicates infinite looping. + frame_control_chunk: + doc-ref: https://wiki.mozilla.org/APNG_Specification#.60fcTL.60:_The_Frame_Control_Chunk + seq: + - id: sequence_number + type: u4 + doc: Sequence number of the animation chunk + - id: width + type: u4 + valid: + min: 1 + max: _root.ihdr.width + doc: Width of the following frame + - id: height + type: u4 + valid: + min: 1 + max: _root.ihdr.height + doc: Height of the following frame + - id: x_offset + type: u4 + valid: + max: _root.ihdr.width - width + doc: X position at which to render the following frame + - id: y_offset + type: u4 + valid: + max: _root.ihdr.height - height + doc: Y position at which to render the following frame + - id: delay_num + type: u2 + doc: Frame delay fraction numerator + - id: delay_den + type: u2 + doc: Frame delay fraction denominator + - id: dispose_op + type: u1 + enum: dispose_op_values + doc: Type of frame area disposal to be done after rendering this frame + - id: blend_op + type: u1 + enum: blend_op_values + doc: Type of frame area rendering for this frame + instances: + delay: + value: 'delay_num / (delay_den == 0 ? 100.0 : delay_den)' + doc: Time to display this frame, in seconds + frame_data_chunk: + doc-ref: https://wiki.mozilla.org/APNG_Specification#.60fdAT.60:_The_Frame_Data_Chunk + seq: + - id: sequence_number + type: u4 + doc: | + Sequence number of the animation chunk. The fcTL and fdAT chunks + have a 4 byte sequence number. Both chunk types share the sequence. + The first fcTL chunk must contain sequence number 0, and the sequence + numbers in the remaining fcTL and fdAT chunks must be in order, with + no gaps or duplicates. + - id: frame_data + size-eos: true + doc: | + Frame data for the frame. At least one fdAT chunk is required for + each frame. The compressed datastream is the concatenation of the + contents of the data fields of all the fdAT chunks within a frame. +enums: + color_type: + 0: greyscale + 2: truecolor + 3: indexed + 4: greyscale_alpha + 6: truecolor_alpha + phys_unit: + 0: unknown + 1: meter + compression_methods: + 0: zlib + dispose_op_values: + 0: + id: none + doc: | + No disposal is done on this frame before rendering the next; + the contents of the output buffer are left as is. + doc-ref: https://wiki.mozilla.org/APNG_Specification#.60fcTL.60:_The_Frame_Control_Chunk + 1: + id: background + doc: | + The frame's region of the output buffer is to be cleared to + fully transparent black before rendering the next frame. + doc-ref: https://wiki.mozilla.org/APNG_Specification#.60fcTL.60:_The_Frame_Control_Chunk + 2: + id: previous + doc: | + The frame's region of the output buffer is to be reverted + to the previous contents before rendering the next frame. + doc-ref: https://wiki.mozilla.org/APNG_Specification#.60fcTL.60:_The_Frame_Control_Chunk + blend_op_values: + 0: + id: source + doc: | + All color components of the frame, including alpha, + overwrite the current contents of the frame's output buffer region. + 1: + id: over + doc: | + The frame is composited onto the output buffer based on its alpha \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..ccebba7 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..42defcc --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip +networkTimeout=10000 +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..79a61d4 --- /dev/null +++ b/gradlew @@ -0,0 +1,244 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..93e3f59 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/kaitai_struct_visualizer_java.iml b/kaitai_struct_visualizer_java.iml deleted file mode 100644 index d512bbb..0000000 --- a/kaitai_struct_visualizer_java.iml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/lib/HexLib.jar b/lib/HexLib.jar deleted file mode 100644 index af8318d..0000000 Binary files a/lib/HexLib.jar and /dev/null differ diff --git a/lib/JHexViewer-1.1.jar b/lib/JHexViewer-1.1.jar new file mode 100644 index 0000000..089e1c4 Binary files /dev/null and b/lib/JHexViewer-1.1.jar differ diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..9d00316 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'kaitai_struct_gui' diff --git a/src/main/java/io/github/proto4j/kaitai/vis/AllIcons.java b/src/main/java/io/github/proto4j/kaitai/vis/AllIcons.java new file mode 100644 index 0000000..a8a1875 --- /dev/null +++ b/src/main/java/io/github/proto4j/kaitai/vis/AllIcons.java @@ -0,0 +1,24 @@ +// GENERATED FILE - DO NOT MODIFY +package io.github.proto4j.kaitai.vis; + +import io.github.proto4j.ui.icon.IconManager; +import io.github.proto4j.ui.icon.SVGIconManager; + +import javax.swing.*; +import java.awt.*; + +public final class AllIcons { + private static final IconManager manager = SVGIconManager.getInstance(AllIcons.class); + + private static Icon load(String path, long key, int flags) { + return manager.loadIcon(path, key, flags); + } + + public static final class DataTypes { + public static final Icon Array = load("/icons/dataTypes/array", -0xe85453dL, 0x0); + public static final Icon Enum = load("/icons/dataTypes/enum", -0xe85453eL, 0x2); + public static final Icon Struct = load("/icons/dataTypes/struct", -0xe854540L, 0x2); + public static final Icon Unknown = load("/icons/dataTypes/unknown", -0xe854542L, 0x2); + public static final Icon Value = load("/icons/dataTypes/value", -0xe854544L, 0x2); + } +} \ No newline at end of file diff --git a/src/main/java/io/github/proto4j/kaitai/vis/FlatLafTheme.java b/src/main/java/io/github/proto4j/kaitai/vis/FlatLafTheme.java new file mode 100644 index 0000000..00e7852 --- /dev/null +++ b/src/main/java/io/github/proto4j/kaitai/vis/FlatLafTheme.java @@ -0,0 +1,186 @@ +package io.github.proto4j.kaitai.vis; //@date 29.07.2023 + +import cms.rendner.hexviewer.common.geom.HDimension; +import cms.rendner.hexviewer.common.utils.AsciiUtils; +import cms.rendner.hexviewer.view.JHexViewer; +import cms.rendner.hexviewer.view.components.areas.bytes.ByteArea; +import cms.rendner.hexviewer.view.components.areas.bytes.model.colors.IByteColorProvider; +import cms.rendner.hexviewer.view.components.areas.common.AreaComponent; +import cms.rendner.hexviewer.view.components.areas.common.painter.background.DefaultBackgroundPainter; +import cms.rendner.hexviewer.view.components.areas.offset.model.colors.IOffsetColorProvider; +import cms.rendner.hexviewer.view.components.highlighter.DefaultHighlighter; +import cms.rendner.hexviewer.view.themes.AbstractTheme; + +import javax.swing.*; +import javax.swing.border.Border; +import javax.swing.tree.TreePath; +import java.awt.*; +import java.util.Map; + +public class FlatLafTheme extends AbstractTheme { + + public static String KEY_BYTE_NULL = "Label.disabledForeground"; + public static String KEY_FOREGROUND = "Label.foreground"; + public static String KEY_CARET = "TextArea.caretForeground"; + public static String KEY_SELECTION_FOREGROUND = "TextArea.selectionForeground"; + public static String KEY_SELECTION_BACKGROUND = "TextArea.selectionBackground"; + public static String KEY_BACKGROUND = "Panel.background"; + public static String KEY_BORDER = "Separator.foreground"; + + public static String KEY_OFFSET_FOREGROUND = "List.foreground"; + public static String KEY_OFFSET_ACTIVE_FOREGROUND = "List.selectionForeground"; + public static String KEY_OFFSET_ACTIVE_BACKGROUND = "List.selectionBackground"; + + public FlatLafTheme() { + } + + + @Override + protected void adjustColorProviders(JHexViewer hexViewer) { + super.adjustColorProviders(hexViewer); + hexViewer.getOffsetArea().setColorProvider(new OffsetColorProvider(hexViewer)); + hexViewer.getHexArea().setColorProvider(new ByteAreaColorProvider(hexViewer, hexViewer.getHexArea())); + hexViewer.getTextArea().setColorProvider(new ByteAreaColorProvider(hexViewer, hexViewer.getTextArea())); + } + + @Override + protected void adjustPainters(JHexViewer hexViewer) { + super.adjustPainters(hexViewer); + setAreaBackgroundPainter(hexViewer.getOffsetArea(), new BorderPainter()); + setAreaBackgroundPainter(hexViewer.getTextArea(), new BorderPainter()); + setAreaBackgroundPainter(hexViewer.getHexArea(), new BorderPainter()); + } + + @Override + protected void adjustComponentDefaults(JHexViewer hexViewer) { + super.adjustComponentDefaults(hexViewer); + hexViewer.setBackground(UIManager.getColor(KEY_BACKGROUND)); + } + + private static final class OffsetColorProvider implements IOffsetColorProvider { + private final JHexViewer hexViewer; + + private OffsetColorProvider(JHexViewer hexViewer) { + this.hexViewer = hexViewer; + } + + @Override + public Color getRowElementForeground(int rowIndex) { + return hexViewer.isShowOffsetCaretIndicator() && isCaretRowIndex(rowIndex) + ? UIManager.getColor(KEY_OFFSET_ACTIVE_FOREGROUND) + : UIManager.getColor(KEY_OFFSET_FOREGROUND); + } + + @Override + public Color getRowElementBackground(int rowIndex) { + return hexViewer.isShowOffsetCaretIndicator() && isCaretRowIndex(rowIndex) + ? UIManager.getColor(KEY_OFFSET_ACTIVE_BACKGROUND) + : UIManager.getColor(KEY_BACKGROUND); + } + + @Override + public Color getBackground() { + return UIManager.getColor(KEY_BACKGROUND); + } + + private boolean isCaretRowIndex(final int rowIndex) { + return hexViewer.getCaret().map(caret -> + { + final long caretIndex = caret.getDot(); + final int caretRowIndex = hexViewer.byteIndexToRowIndex(caretIndex); + return rowIndex == caretRowIndex; + }).orElse(Boolean.FALSE); + } + } + + private static final class ByteAreaColorProvider implements IByteColorProvider { + + private final JHexViewer hexViewer; + private final ByteArea area; + + private ByteAreaColorProvider(JHexViewer hexViewer, ByteArea area) { + this.hexViewer = hexViewer; + this.area = area; + } + + @Override + public Color getCaret() { + return UIManager.getColor(KEY_CARET); + } + + @Override + public Color getSelection() { + return hexViewer.getCaretFocusedArea() == area + ? UIManager.getColor(KEY_SELECTION_BACKGROUND).brighter() + : UIManager.getColor(KEY_SELECTION_BACKGROUND); + } + + @Override + public Color getBackground() { + return UIManager.getColor(KEY_BACKGROUND); + } + + @Override + public Color getRowElementForeground(int byteValue, long offset, int rowIndex, int elementInRowIndex) { + if (AsciiUtils.NULL == byteValue) { + return UIManager.getColor(KEY_BYTE_NULL); + } + return isSelected(offset) ? UIManager.getColor(KEY_SELECTION_FOREGROUND) : UIManager.getColor(KEY_FOREGROUND); + } + + @Override + public Color getDefaultHighlight() { + return UIManager.getColor(KEY_SELECTION_BACKGROUND); + } + + private boolean isSelected(final long offset) { + return hexViewer.getCaret() + .map(caret -> caret.hasSelection() && caret.getSelectionStart() <= offset && offset <= caret.getSelectionEnd()) + .orElse(Boolean.FALSE); + } + } + + private static final class BorderPainter extends DefaultBackgroundPainter { + private final Border separator; + + public BorderPainter() { + this(UIManager.getColor(KEY_BORDER)); + } + + public BorderPainter(final Color color) { + this.separator = BorderFactory.createMatteBorder(0, 0, 0, 1, color); + } + + @Override + public void paint(Graphics2D g, JHexViewer hexViewer, AreaComponent component) { + super.paint(g, hexViewer, component); + separator.paintBorder(component, g, 0, 0, component.getWidth(), component.getHeight()); + } + } + + public static class HighlightPainter extends DefaultHighlighter.DefaultHighlightPainter { + + public HighlightPainter(Color fallbackColor) { + super(fallbackColor); + } + + @Override + protected Color getColor(ByteArea area) { + return this.fallbackColor; + } + + @Override + public void paint(Graphics2D g, JHexViewer hexViewer, ByteArea area, HDimension rowElementsHDimension, long byteStartIndex, long byteEndIndex) { + super.paint(g, hexViewer, area, rowElementsHDimension, byteStartIndex, byteEndIndex); + final Rectangle startByteRect = area.getByteRect(byteStartIndex); + final Rectangle endByteRect = area.getByteRect(byteEndIndex); + + g.setColor(getColor(null).darker()); + if (startByteRect.y == endByteRect.y) { + // same line + final Rectangle r = startByteRect.union(endByteRect); + g.drawRect(r.x - 1, r.y - 1, r.width + 1, r.height + 1); + } + } + } +} diff --git a/src/main/java/io/github/proto4j/kaitai/vis/JVis.java b/src/main/java/io/github/proto4j/kaitai/vis/JVis.java new file mode 100644 index 0000000..df270a3 --- /dev/null +++ b/src/main/java/io/github/proto4j/kaitai/vis/JVis.java @@ -0,0 +1,270 @@ +package io.github.proto4j.kaitai.vis; //@date 28.07.2023 + +import cms.rendner.hexviewer.common.ranges.ByteRange; +import cms.rendner.hexviewer.model.data.IDataModel; +import cms.rendner.hexviewer.model.data.file.MappedFileData; +import cms.rendner.hexviewer.view.JHexViewer; +import cms.rendner.hexviewer.view.components.caret.CaretEvent; +import cms.rendner.hexviewer.view.components.caret.ICaret; +import cms.rendner.hexviewer.view.components.caret.ICaretListener; +import cms.rendner.hexviewer.view.components.damager.DefaultDamager; +import cms.rendner.hexviewer.view.components.highlighter.DefaultHighlighter; +import cms.rendner.hexviewer.view.components.highlighter.IHighlighter; +import io.github.proto4j.kaitai.vis.tree.KaitaiTreeNode; +import io.github.proto4j.kaitai.vis.tree.StructTreeCellRenderer; +import io.github.proto4j.kaitai.vis.tree.StructTreeModel; +import io.kaitai.struct.KaitaiStruct; + +import javax.swing.*; +import javax.swing.event.TreeSelectionEvent; +import javax.swing.event.TreeSelectionListener; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.TreeNode; +import javax.swing.tree.TreePath; +import java.awt.*; +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Method; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; + +public class JVis extends JPanel { + + private final JTree tree; + private final JHexViewer hexView; + + private CaretListener caretListener; + private FlatLafTheme theme; + + public JVis() { + this.hexView = new JHexViewer(); + this.setupHexView(); + + this.tree = new JTree(new DefaultMutableTreeNode("")); + this.setupTree(); + + setLayout(new BorderLayout()); + JSplitPane splitPane = new JSplitPane( + JSplitPane.HORIZONTAL_SPLIT, + new JScrollPane(tree), + new JScrollPane(hexView) + ); + splitPane.setDividerLocation(300); + add(splitPane); + } + + public JHexViewer getHexView() { + return hexView; + } + + public FlatLafTheme getTheme() { + return theme; + } + + public JTree getTree() { + return tree; + } + + public CaretListener getCaretListener() { + return caretListener; + } + + public void display(final KaitaiStruct struct, final String filePath) throws ReflectiveOperationException, IOException { + struct._io().seek(0); // reset the stream + Method method = struct.getClass().getDeclaredMethod("_read"); + method.setAccessible(true); + method.invoke(struct); + + StructTreeModel treeModel = new StructTreeModel("", struct); + tree.setModel(treeModel); + // reset the stream again to read all bytes + struct._io().seek(0); + IDataModel dataModel = new MappedFileData(new File(filePath));//new RawDataModel(struct._io().readBytesFull()); + hexView.setDataModel(dataModel); + // Populate map for the hex viewer selection listener + Map spans = new HashMap<>(); + populateTreeSpans(spans, treeModel.getRoot(), new TreePath(treeModel.getRoot())); + IHighlighter highlighter = new DefaultHighlighter(); + hexView.setDamager(new DefaultDamager()); + hexView.setHighlighter(highlighter); + caretListener.spans = spans; + } + + protected void populateTreeSpans(Map destination, KaitaiTreeNode node, TreePath path) { + if (node.isLeaf()) { + ByteRange absSpan = node.getSpan(); + if (absSpan != null) { + destination.put(path, absSpan); + System.out.println(node.getName() + " " + absSpan); + } + } else { + final Enumeration children = node.children(); + while (children.hasMoreElements()) { + final TreeNode child = children.nextElement(); + if (child instanceof KaitaiTreeNode) { + populateTreeSpans(destination, (KaitaiTreeNode) child, path.pathByAddingChild(child)); + } + } + } + } + + protected void setupHexView() { + theme = new FlatLafTheme(); + theme.applyTo(hexView); + + hexView.setShowOffsetCaretIndicator(true); + hexView.setBytesPerRow(16); + hexView.setPreferredVisibleRowCount(23); + hexView.getCaret().ifPresent(c -> c.addCaretListener(caretListener = new CaretListener())); + } + + protected void setupTree() { + tree.setFont(new Font("Monospaced", Font.PLAIN, 12)); + tree.setShowsRootHandles(true); + tree.setCellRenderer(new JVisTreeCellRenderer()); + tree.addTreeSelectionListener(new SelectionListener()); + } + + private static final class JVisTreeCellRenderer extends StructTreeCellRenderer { + + @Override + protected Icon getIconForNode(KaitaiTreeNode node) { + switch (node.getType()) { + case ARRAY: + return AllIcons.DataTypes.Array; + case VALUE: + return AllIcons.DataTypes.Value; + case STRUCT: + return AllIcons.DataTypes.Struct; + case ENUM: + return AllIcons.DataTypes.Enum; + default: + return AllIcons.DataTypes.Unknown; + } + } + } + + protected final class SelectionListener implements TreeSelectionListener { + @Override + public void valueChanged(TreeSelectionEvent e) { + if (tree.getSelectionPaths() == null) { + return; + } + + hexView.getHighlighter().ifPresent(IHighlighter::removeAllHighlights); + for (TreePath path : tree.getSelectionPaths()) { + Object node = path.getLastPathComponent(); + if (node instanceof KaitaiTreeNode) { + ByteRange span = ((KaitaiTreeNode) node).getSpan(); + if (span != null && span.getStart() != span.getEnd()) { + hexView.getHighlighter().ifPresent(h -> { + // The end index should be excluded + h.addHighlight(span.getStart(), span.getEnd() - 1, new FlatLafTheme.HighlightPainter(((KaitaiTreeNode) node).getColor())); + }); + hexView.getCaret().ifPresent(c -> { + c.moveCaret( + span.getStart(), // offset + false, // withSelection + true // scrollToCaret + ); + }); + } + } + } + } + } + + protected final class CaretListener implements ICaretListener { + + private Map spans; + + @Override + public void caretPositionChanged(CaretEvent caretEvent) { + if (this.spans == null) { + return; + } + // TODO... + + ICaret caret = hexView.getCaret().orElseThrow(); + long start = caret.getDot(); + long end = caret.getDot(); + if (caret.hasSelection()) { + start = caret.getSelectionStart(); + end = caret.getSelectionEnd(); + } + + for (Map.Entry entry : spans.entrySet()) { + TreePath path = entry.getKey(); + ByteRange span = entry.getValue(); + + final long spanStart = span.getStart(); + final long spanEnd = span.getEnd(); + + boolean select; + if (start < spanStart) { + /* + The left edge of the selection is before the span. + + Select the tree node only if the right edge of the selection is either + inside the span or goes past the right edge of the span. + + (1) The right edge of the selection is inside the span or goes past the + right edge of the span. + + Example: + + selection v-------v + bytes 0 1 2 3 4 5 6 7 8 9 + span ^---^ + + or + + selection v-------------v + bytes 0 1 2 3 4 5 6 7 8 9 + span ^---^ + + (2) The right edge of the span is to the left of the span. + + Example: + + selection v---v + bytes 0 1 2 3 4 5 6 7 8 9 + span ^---^ + */ + select = end > spanStart; + } else { + /* + The left edge of the selection is either inside the span + or past the right edge of the span. + + Select the tree node only if the left edge of the selection is inside + the span. + + (1) The left edge of the selection is inside the span. + + Example: + + selection v-----v + bytes 0 1 2 3 4 5 6 7 8 9 + span ^-----^ + + (2) The left edge of the selection is to the right of the span. + + Example: + + selection v---v + bytes 0 1 2 3 4 5 6 7 8 9 + span ^-----^ + */ + select = start < spanEnd; + } + + if (select) { + tree.getSelectionModel().setSelectionPath(path); + break; + } + } + } + } +} diff --git a/src/main/java/io/github/proto4j/kaitai/vis/KsyCompiler.java b/src/main/java/io/github/proto4j/kaitai/vis/KsyCompiler.java new file mode 100644 index 0000000..66274d0 --- /dev/null +++ b/src/main/java/io/github/proto4j/kaitai/vis/KsyCompiler.java @@ -0,0 +1,161 @@ +package io.github.proto4j.kaitai.vis; //@date 30.07.2023 + +import io.kaitai.struct.*; +import io.kaitai.struct.format.ClassSpec; +import io.kaitai.struct.format.KSVersion; +import io.kaitai.struct.formats.JavaClassSpecs; +import io.kaitai.struct.formats.JavaKSYParser; +import io.kaitai.struct.languages.JavaCompiler$; +import org.mdkt.compiler.InMemoryJavaCompiler; + +import java.lang.instrument.IllegalClassFormatException; +import java.lang.reflect.Constructor; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * The KsyCompiler class is responsible for compiling Kaitai Struct (.ksy) files + * into Java classes and creating instances of those classes. + *

+ * Below are some usage examples of this class: + *

    + *
  1. Compiling a Kaitai Struct (.ksy) file to Java source code and obtaining + * the generated code: + *
    {@code
    + *          String ksyFilePath = "path/to/your/file.ksy";
    + *          String javaSourceCode = KsyCompiler.compileToJava(ksyFilePath);
    + *          System.out.println(javaSourceCode);
    + *         }
    + *
  2. + *
  3. Creating a new class from Java source code: + *
    {@code
    + *          String javaSourceCode = "public class MyKaitaiStruct extends KaitaiStruct { ... }";
    + *          Class kaitaiClass = KsyCompiler.createClass(javaSourceCode);
    + *     }
    + *
  4. + *
  5. Extracting the class name from Java source code: + *
    {@code
    + *          Class kaitaiClass = ...; // Get the class object somehow
    + *          KaitaiStream stream = ...; // Provide the KaitaiStream
    + *          KaitaiStruct instance = KsyCompiler.newInstance(kaitaiClass, stream);
    + *     }
    + *
  6. + *
+ * + * @see KaitaiStruct + * @see InMemoryJavaCompiler + */ +public final class KsyCompiler { + // The default package name for the generated Java classes + public static final String PACKAGE = "io.kaitai.struct.visualized"; + + /** + * Compile the given Kaitai Struct (.ksy) file into Java source code and return it as a string. + * + * @param ksyFilePath The path to the Kaitai Struct (.ksy) file to be compiled. + * @return The Java source code generated from the .ksy file. + */ + public static String compileToJava(String ksyFilePath) { + KSVersion.current_$eq(Version.version()); + ClassSpec spec = JavaKSYParser.fileNameToSpec(ksyFilePath); + JavaClassSpecs javaClassSpecs = new JavaClassSpecs(null, null, spec); + + final RuntimeConfig config = new RuntimeConfig( + false,// autoRead - do not call `_read` automatically in constructor + true, // readStoresPos - enable generation of a position info which is accessed in DebugAids later + true, // opaqueTypes + null, // cppConfig + null, // goPackage + new JavaRuntimeConfig( + PACKAGE, + // Class to be invoked in `fromFile` helper methods + "io.kaitai.struct.ByteBufferKaitaiStream", + // Exception class expected to be thrown on end-of-stream errors + "java.nio.BufferUnderflowException" + ), + null, // dotNetNamespace + null, // phpNamespace + null, // pythonPackage + null, // nimModule + null // nimOpaque + ); + + Main.importAndPrecompile(javaClassSpecs, config); + CompileLog.SpecSuccess result = Main.compile(javaClassSpecs, spec, JavaCompiler$.MODULE$, config); + CompileLog.FileSuccess file = result.files().apply(0); + return file.contents(); + } + + /** + * Create a new class from the given Java source code and return its class object. + * + * @param javaSourceCode The Java source code for the class to be created. + * @return The Class object representing the newly created class. + * @throws Exception If there is an error during the class creation process. + */ + public static Class createClass(String javaSourceCode) + throws Exception { + return createClass(javaSourceCode, getClassName(javaSourceCode)); + } + + public static Class createClass(String javaSourceCode, String simpleName) + throws Exception { + return createClass(javaSourceCode, PACKAGE, simpleName); + } + + /** + * Create a new class from the given Java source code with the specified simple name + * and return its class object. + * + * @param javaSourceCode The Java source code for the class to be created. + * @param simpleName The simple name to be used for the newly created class. + * @return The Class object representing the newly created class. + * @throws Exception If there is an error during the class creation process. + */ + @SuppressWarnings("unchecked") + public static Class createClass(String javaSourceCode, String packageName, String simpleName) + throws Exception { + final String name = String.join(".", packageName, simpleName); + final Class cls = InMemoryJavaCompiler.newInstance().compile(name, javaSourceCode); + + if (KaitaiStruct.class.isAssignableFrom(cls)) { + return (Class) cls; + } else { + throw new IllegalClassFormatException(String.format( + "the compiled class is not assignable from \"%s\". The compiled class is \"%s\", and its superclass is \"%s\".", + KaitaiStruct.class, cls, cls.getSuperclass() + )); + } + } + + /** + * Create a new instance of the given class using the provided KaitaiStream. + * + * @param cls The class for which to create a new instance. + * @param stream The KaitaiStream to be used for initializing the instance. + * @param The type of the KaitaiStruct class. + * @return A new instance of the KaitaiStruct class. + * @throws ReflectiveOperationException If there is an error during class instantiation. + */ + public static T newInstance(Class cls, KaitaiStream stream) + throws ReflectiveOperationException { + Constructor ctor = cls.getDeclaredConstructor(KaitaiStream.class); + return ctor.newInstance(stream); + } + + /** + * Get the class name from the given Java source code. + * + * @param javaSourceCode The Java source code from which to extract the class name. + * @return The class name extracted from the Java source code. + */ + public static String getClassName(final String javaSourceCode) { + final Pattern pattern = Pattern.compile("public class (.+?) extends KaitaiStruct.*", Pattern.DOTALL); + + Matcher matcher = pattern.matcher(javaSourceCode); + if (!matcher.find()) { + return null; + } + return matcher.group(1); + } +} diff --git a/src/main/java/io/github/proto4j/kaitai/vis/tree/ArrayNode.java b/src/main/java/io/github/proto4j/kaitai/vis/tree/ArrayNode.java new file mode 100644 index 0000000..c93863c --- /dev/null +++ b/src/main/java/io/github/proto4j/kaitai/vis/tree/ArrayNode.java @@ -0,0 +1,178 @@ +package io.github.proto4j.kaitai.vis.tree; //@date 28.07.2023 + +import cms.rendner.hexviewer.common.ranges.ByteRange; + +import javax.swing.tree.TreeNode; +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; + +/** + * The ArrayNode class represents an array node in the Kaitai Struct visualization + * tool's tree hierarchy that contains a collection of elements. + */ +public class ArrayNode extends KaitaiTreeNode { + + // The class of the elements in the array + private final Class valueType; + + // Start offsets for each element in the array + private final Integer[] startOffsets; + + // End offsets for each element in the array + private final Integer[] endOffsets; + + // Cached child nodes created from the array elements + private KaitaiTreeNode[] children; + + /** + * Constructor for creating an ArrayNode instance. + * + * @param name The name of the ArrayNode. + * @param parent The parent node of the ArrayNode. + * @param value The value associated with the ArrayNode (a list representing the array). + * @param span The ByteRange representing the span of the array in the original data. + * @param valueType The Class object representing the type of the elements in the array. + * @param startOffsets The start offsets for each element in the array. + * @param endOffsets The end offsets for each element in the array. + */ + public ArrayNode(String name, + TreeNode parent, + Object value, + ByteRange span, + Class valueType, + Integer[] startOffsets, + Integer[] endOffsets) { + super(name, parent, value, span); + this.valueType = valueType; + this.startOffsets = startOffsets; + this.endOffsets = endOffsets; + } + + /** + * Get the List representation of the array value associated with the ArrayNode. + * + * @return The List representation of the array value. + */ + @Override + public List getValue() { + return (List) value; + } + + /** + * Get the type of the elements in the array associated with the ArrayNode. + * + * @return The Class object representing the type of the elements in the array. + */ + public Class getValueType() { + return valueType; + } + + /** + * Get the child node at the specified childIndex. + * + * @param childIndex The index of the child node to retrieve. + * @return The child node at the specified index. + */ + @Override + public KaitaiTreeNode getChildAt(int childIndex) { + return getChildren()[childIndex]; + } + + /** + * Get the number of children nodes of the ArrayNode. + * + * @return The number of children nodes. + */ + @Override + public int getChildCount() { + return getChildren().length; + } + + /** + * Get the index of the specified child node. + * + * @param node The child node for which to find the index. + * @return The index of the specified child node. + */ + @Override + public int getIndex(TreeNode node) { + return Arrays.asList(getChildren()).indexOf((KaitaiTreeNode) node); + } + + /** + * Get an enumeration of ArrayNode's children nodes. + * + * @return An enumeration of ArrayNode's children nodes. + */ + @Override + public Enumeration children() { + return Collections.enumeration(List.of(getChildren())); + } + + /** + * Check if ArrayNode allows children nodes. + * + * @return True, indicating that ArrayNode allows children. + */ + @Override + public boolean getAllowsChildren() { + return true; + } + + /** + * Check if the ArrayNode is a leaf node. + * + * @return True if the ArrayNode has no elements (is empty), indicating that it is a leaf node. + */ + @Override + public boolean isLeaf() { + return getValue().isEmpty(); + } + + /** + * Get the child nodes corresponding to the elements of the array. + *

+ * If the child nodes have not been created yet, this method will create + * them and cache them. + * + * @return An array of KaitaiTreeNode representing the child nodes of the ArrayNode. + */ + public KaitaiTreeNode[] getChildren() { + if (children == null) { + // If the children have not been created yet, create them now and cache them + children = new KaitaiTreeNode[getValue().size()]; + for (int i = 0; i < getValue().size(); i++) { + int start = startOffsets[i]; + int end = endOffsets[i]; + final ByteRange current = getSpan(); + if (start < current.getStart()) { + start += current.getStart(); + end += current.getStart(); + } + + final ByteRange span = new ByteRange(start, end); + final String name = String.format("[%d]", i); // special name for array elements + Object value = this.getValue().get(i); + try { + // Create a child node based on the element value, valueType, and span + children[i] = create(name, value, valueType, span); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + return children; + } + + /** + * Get the type of the ArrayNode, which is always ARRAY. + * + * @return The Type of the ArrayNode (ARRAY). + */ + @Override + public Type getType() { + return Type.ARRAY; + } +} diff --git a/src/main/java/io/github/proto4j/kaitai/vis/tree/CategorizedNode.java b/src/main/java/io/github/proto4j/kaitai/vis/tree/CategorizedNode.java new file mode 100644 index 0000000..abe5410 --- /dev/null +++ b/src/main/java/io/github/proto4j/kaitai/vis/tree/CategorizedNode.java @@ -0,0 +1,54 @@ +package io.github.proto4j.kaitai.vis.tree;//@date 29.07.2023 + +import java.awt.*; + +/** + * The CategorizedNode interface represents categorized nodes, providing information + * about their type and color. + * + * @see Type + */ +public interface CategorizedNode { + + /** + * Get the type of the categorized node. + * + * @return The {@link Type} of the categorized node (STRUCT, ENUM, VALUE, ARRAY). + */ + Type getType(); + + /** + * Get the color associated with the categorized node. + * + * @return The {@link Color} of the categorized node. + */ + Color getColor(); + + /** + * Specifies the possible types of categorized nodes. + */ + enum Type { + /** + * Represents a structured node, typically containing other nodes. + */ + STRUCT, + + /** + * Represents an enumeration node, which contains a set of named values. + */ + ENUM, + + /** + * Represents a single value node, such as a primitive value or a constant. + * For example, a numeric value or a string. + */ + VALUE, + + /** + * Represents an array node, which contains a collection of elements. + * For example, an array or a list. + */ + ARRAY + } +} + diff --git a/src/main/java/io/github/proto4j/kaitai/vis/tree/KaitaiTreeNode.java b/src/main/java/io/github/proto4j/kaitai/vis/tree/KaitaiTreeNode.java new file mode 100644 index 0000000..9041949 --- /dev/null +++ b/src/main/java/io/github/proto4j/kaitai/vis/tree/KaitaiTreeNode.java @@ -0,0 +1,185 @@ +package io.github.proto4j.kaitai.vis.tree;//@date 29.07.2023 + +import cms.rendner.hexviewer.common.ranges.ByteRange; +import io.github.proto4j.kaitai.vis.util.ColorSpec; +import io.kaitai.struct.KaitaiStruct; + +import javax.swing.tree.TreeNode; +import java.awt.*; +import java.lang.reflect.Array; + +/** + * The abstract class KaitaiTreeNode serves as the base implementation for tree nodes + * in the Kaitai Struct visualization tool. + * + * @see ArrayNode + * @see StructNode + * @see SimpleNode + */ +public abstract class KaitaiTreeNode implements TreeNode, CategorizedNode { + + // Common properties for tree nodes + protected final String name; + protected final TreeNode parent; + protected final Object value; + protected final ByteRange span; + + protected Color color; + + /** + * Basic constructor for creating a KaitaiTreeNode instance. + * + * @param name The name of the tree node. + * @param parent The parent node of the tree node. + * @param value The value associated with the tree node. + * @param span The ByteRange representing the span of the tree node's value in the original data. + */ + public KaitaiTreeNode(String name, TreeNode parent, Object value, ByteRange span) { + this.name = name; + this.parent = parent; + this.value = value; + this.span = span; + this.color = ColorSpec.random(); + } + + /** + * Convert the value of a KaitaiTreeNode to a string representation. + *

+ * The following list tries to illustrate how different value types will be + * represented: + *

  • Array: {@code []}
  • + *
  • Struct: {@code struct }
  • + *
  • Value: {@code String.valueOf()}
  • + *
  • Enum: {@code :: ()}
  • + * + * @param node The KaitaiTreeNode for which to convert the value to a string. + * @return A string representation of the node's value. + */ + public static String valueToString(KaitaiTreeNode node) { + if (node instanceof ArrayNode) { + String name = ((ArrayNode) node).getValueType().getSimpleName(); + return String.format("%s[%d]", name, node.getChildCount()); + } else if (node instanceof StructNode) { + String name = node.getValue().getClass().getSimpleName(); + return "struct " + name; + } else { + SimpleNode simpleNode = (SimpleNode) node; + if (simpleNode.getType() == Type.VALUE) { + Object value = simpleNode.getValue(); + String format = String.valueOf(value); + if (value instanceof byte[]) { + byte[] data = (byte[]) value; + StringBuilder sb = new StringBuilder(); + sb.append("["); + int maxIter = Math.min(data.length, 8); + + for (int i = 0; i < maxIter; ++i) { + sb.append(String.format("0x%02X", data[i])); + if (i < maxIter - 1) { + sb.append(", "); + } else if (i == maxIter - 1 && data.length > maxIter) { + sb.append(", ..."); + } + } + + sb.append("]"); + format = sb.toString(); + } + + return simpleNode.getValueType().getSimpleName() + " = " + format; + } + // Enum Type is a bit special + Class*/> enumClass = simpleNode.getValueType(); + try { + Object values = enumClass.getMethod("values").invoke(null); + for (int i = 0; i < Array.getLength(values); i++) { + Enum value = (Enum) Array.get(values, i); + if (((Enum) simpleNode.value).ordinal() == value.ordinal()) { + return String.format("%s::%s (%#x)", + enumClass.getSimpleName(), value.name(), + value.ordinal()); + } + } + return String.format("%s::??? (%s)", enumClass.getSimpleName(), simpleNode.value); + } catch (Exception e) { + return String.valueOf(simpleNode.getValue()); + } + } + } + + /** + * Get the color associated with the tree node. + * + * @return The Color of the tree node. + */ + @Override + public Color getColor() { + return color; + } + + /** + * Get the parent node of the tree node. + * + * @return The parent TreeNode of the tree node. + */ + @Override + public TreeNode getParent() { + return parent; + } + + /** + * Get the value associated with the tree node. + * + * @return The value of the tree node. + */ + public Object getValue() { + return value; + } + + /** + * Get the name of the tree node. + * + * @return The name of the tree node. + */ + public String getName() { + return name; + } + + /** + * Get the ByteRange representing the span of the tree node's value in the original data. + * + * @return The ByteRange representing the span of the tree node's value. + */ + public ByteRange getSpan() { + return span; + } + + /** + * Create a new KaitaiTreeNode instance based on the provided parameters. + * This method is used to create child nodes of the current node. + * + * @param name The name of the new node. + * @param value The value of the new node. + * @param valueType The type of the value (class) of the new node. + * @param span The ByteRange representing the span of the new node's value in the original data. + * @return A new KaitaiTreeNode instance representing the child node. + * @throws ReflectiveOperationException If there is an error during the creation of the new node. + */ + protected final KaitaiTreeNode create(String name, Object value, Class valueType, ByteRange span) throws ReflectiveOperationException { + return value instanceof KaitaiStruct + ? new StructNode(name, this, span, (KaitaiStruct) value, StructTreeModel.State.SHOW) + : new SimpleNode(name, this, value, span, valueType); + + } + + /** + * Convert the tree node and its value to a string representation. + * + * @return A string representing the tree node and its value. + */ + @Override + public String toString() { + return String.format("%s: %s", getName(), valueToString(this)); + } + +} diff --git a/src/main/java/io/github/proto4j/kaitai/vis/tree/SimpleNode.java b/src/main/java/io/github/proto4j/kaitai/vis/tree/SimpleNode.java new file mode 100644 index 0000000..5929d86 --- /dev/null +++ b/src/main/java/io/github/proto4j/kaitai/vis/tree/SimpleNode.java @@ -0,0 +1,124 @@ +package io.github.proto4j.kaitai.vis.tree; //@date 28.07.2023 + +import cms.rendner.hexviewer.common.ranges.ByteRange; + +import javax.swing.tree.TreeNode; +import java.util.Collections; +import java.util.Enumeration; +/** + * The SimpleNode class represents a leaf node in the Kaitai Struct visualization + * tool's tree hierarchy. + */ +public class SimpleNode extends KaitaiTreeNode { + + // The class of the value associated with the SimpleNode + private final Class valueType; + + /** + * Constructor for creating a SimpleNode instance. + * + * @param name The name of the SimpleNode. + * @param parent The parent node of the SimpleNode. + * @param value The value associated with the SimpleNode. + * @param span The ByteRange representing the span of the value in the original data. + * @param valueType The Class object representing the type of the value. + */ + public SimpleNode(String name, TreeNode parent, Object value, ByteRange span, Class valueType) { + super(name, parent, value, span); + this.valueType = valueType; + } + + /** + * Get the type of the value associated with the SimpleNode. + * + * @return The Class object representing the type of the value. + */ + public Class getValueType() { + return valueType; + } + + /** + * Get the child node at the specified childIndex. + * Since SimpleNode is a leaf node, it throws an IndexOutOfBoundsException as it has no children. + * + * @param childIndex The index of the child node to retrieve. + * @return The child node at the specified index. + * @throws IndexOutOfBoundsException Always thrown since SimpleNode has no children. + */ + @Override + public TreeNode getChildAt(int childIndex) { + throw new IndexOutOfBoundsException("SimpleNode has no children!"); + } + + /** + * Get the number of children nodes of the SimpleNode. + * Since SimpleNode is a leaf node, it always returns 0. + * + * @return The number of children nodes (always 0 for SimpleNode). + */ + @Override + public int getChildCount() { + return 0; + } + + /** + * Get the index of the specified child node. + *

    + * Since SimpleNode has no children, it always returns -1, indicating + * that the node is not a child of SimpleNode. + * + * @param node The child node for which to find the index. + * @return The index of the specified child node (always -1 for SimpleNode). + */ + @Override + public int getIndex(TreeNode node) { + return -1; + } + + /** + * Check if the SimpleNode is a leaf node. + * Since SimpleNode has no children, it always returns true. + * + * @return True, indicating that SimpleNode is a leaf node. + */ + @Override + public boolean isLeaf() { + return true; + } + + /** + * Get an enumeration of SimpleNode's children nodes. + * Since SimpleNode has no children, it returns an empty enumeration. + * + * @return An enumeration of SimpleNode's children nodes (always empty). + */ + @Override + public Enumeration children() { + return Collections.emptyEnumeration(); + } + + /** + * Check if SimpleNode allows children nodes. + * Since SimpleNode is a leaf node and has no children, it always returns false. + * + * @return False, indicating that SimpleNode does not allow children. + */ + @Override + public boolean getAllowsChildren() { + return false; + } + + /** + * Get the type of the SimpleNode, either VALUE or ENUM based on the type of the value associated with it. + * + * @return The Type of the SimpleNode (VALUE or ENUM). + */ + @Override + public Type getType() { + // Determine the Type of the SimpleNode based on the type of the value + if (value instanceof Enum) { + return Type.ENUM; + } + return Type.VALUE; + } +} diff --git a/src/main/java/io/github/proto4j/kaitai/vis/tree/StructNode.java b/src/main/java/io/github/proto4j/kaitai/vis/tree/StructNode.java new file mode 100644 index 0000000..5501651 --- /dev/null +++ b/src/main/java/io/github/proto4j/kaitai/vis/tree/StructNode.java @@ -0,0 +1,268 @@ +package io.github.proto4j.kaitai.vis.tree; //@date 28.07.2023 + +import cms.rendner.hexviewer.common.ranges.ByteRange; +import io.kaitai.struct.KaitaiStruct; + +import javax.swing.tree.TreeNode; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.util.*; + +/** + * A node that corresponds to a parsed Kaitai Struct. + */ +public class StructNode extends KaitaiTreeNode { + + // The Kaitai Struct object associated with the StructNode + private final KaitaiStruct kaitaiStruct; + + private final List fields; + private final List instances; + + private final Map attributeStart; + private final Map attributeEnd; + + private final Map> arrayStart; + private final Map> arrayEnd; + + // Cached child nodes created from fields and instances of the structure + private List children; + // State of the StructNode in the StructTreeModel (unused) + private StructTreeModel.State state; + + /** + * Constructor for creating a StructNode instance representing the entire structure. + * + * @param name The name of the StructNode. + * @param parent The parent node of the StructNode. + * @param kaitaiStruct The Kaitai Struct object associated with the StructNode. + * @param state The state of the StructNode in the StructTreeModel. + * @throws ReflectiveOperationException If an error occurs while initializing the StructNode. + */ + public StructNode(String name, TreeNode parent, KaitaiStruct kaitaiStruct, StructTreeModel.State state) + throws ReflectiveOperationException { + this(name, parent, new ByteRange(0L, kaitaiStruct._io().pos()), kaitaiStruct, state); + } + + /** + * Constructor for creating a StructNode instance representing a specific portion of the structure. + * + * @param name The name of the StructNode. + * @param parent The parent node of the StructNode. + * @param span The ByteRange representing the span of the structure in the original data. + * @param kaitaiStruct The Kaitai Struct object associated with the StructNode. + * @param state The state of the StructNode in the StructTreeModel. + * @throws ReflectiveOperationException If an error occurs while initializing the StructNode. + */ + @SuppressWarnings("unchecked") + public StructNode(String name, TreeNode parent, ByteRange span, KaitaiStruct kaitaiStruct, StructTreeModel.State state) + throws ReflectiveOperationException { + super(name, parent, kaitaiStruct, span); + this.kaitaiStruct = kaitaiStruct; + this.state = state; + + final Class type = kaitaiStruct.getClass(); + final String[] names = (String[]) type.getField("_seqFields").get(null); + + // Separate methods representing fields and instances + List order = Arrays.asList(names); + this.instances = new ArrayList<>(); + this.fields = new ArrayList<>(); + for (Method method : type.getDeclaredMethods()) { + // Skip static methods, i.e., "fromFile" + // Skip all internal methods, i.e., "_io", "_parent", "_root" + if (Modifier.isStatic(method.getModifiers()) || method.getName().charAt(0) == '_') { + continue; + } + + if (order.contains(method.getName())) { + fields.add(method); + } else { + instances.add(method); + } + } + + // Sort fields based on the order in the structure + fields.sort((o1, o2) -> { + final int pos1 = order.indexOf(o1.getName()); + final int pos2 = order.indexOf(o2.getName()); + return pos1 - pos2; + }); + + this.children = null; + this.attributeStart = (Map) type.getDeclaredField("_attrStart").get(kaitaiStruct); + this.attributeEnd = (Map) type.getDeclaredField("_attrEnd").get(kaitaiStruct); + this.arrayStart = (Map>) type.getDeclaredField("_arrStart").get(kaitaiStruct); + this.arrayEnd = (Map>) type.getDeclaredField("_arrEnd").get(kaitaiStruct); + } + + /** + * Get the type of the StructNode, which is always STRUCT. + * + * @return The Type of the StructNode (STRUCT). + */ + @Override + public Type getType() { + return Type.STRUCT; + } + + /** + * Get the state of the StructNode in the StructTreeModel. + * + * @return The state of the StructNode. + */ + public StructTreeModel.State getState() { + return state; + } + + /** + * Set the state of the StructNode in the StructTreeModel. + * + * @param state The new state to be set for the StructNode. + */ + public void setState(StructTreeModel.State state) { + this.state = state; + } + + /** + * Get the Kaitai Struct object associated with the StructNode. + * + * @return The Kaitai Struct object associated with the StructNode. + */ + @Override + public KaitaiStruct getValue() { + return kaitaiStruct; + } + + /** + * Get the child node at the specified childIndex. + * + * @param childIndex The index of the child node to retrieve. + * @return The child node at the specified index. + */ + @Override + public KaitaiTreeNode getChildAt(int childIndex) { + return this.getChildren().get(childIndex); + } + + /** + * Get the number of children nodes of the StructNode. + * + * @return The number of children nodes. + */ + @Override + public int getChildCount() { + return this.getChildren().size(); + } + + /** + * Get the index of the specified child node. + * + * @param node The child node for which to find the index. + * @return The index of the specified child node. + */ + @Override + public int getIndex(TreeNode node) { + return this.getChildren().indexOf((KaitaiTreeNode) node); + } + + /** + * Check if StructNode allows children nodes. + * + * @return True, indicating that StructNode allows children. + */ + @Override + public boolean getAllowsChildren() { + return true; + } + + /** + * Check if the StructNode is a leaf node. + * + * @return True if the StructNode has no children (fields and instances), indicating that it is a leaf node. + */ + @Override + public boolean isLeaf() { + return this.getChildren().isEmpty(); + } + + /** + * Get an enumeration of StructNode's children nodes. + * + * @return An enumeration of StructNode's children nodes. + */ + @Override + public Enumeration children() { + return Collections.enumeration(this.getChildren()); + } + + /** + * Get the child nodes corresponding to the fields and instances within the structure. + * + * @return A list of KaitaiTreeNode representing the child nodes of the StructNode. + */ + public List getChildren() { + if (this.children == null) { + children = new ArrayList<>(); + try { + // Create child nodes for fields + for (Method method : this.fields) { + children.add(createNode(method)); + } + // Create child nodes for instances + for (Method method : this.instances) { + children.add(createNode(method)); + } + } catch (ReflectiveOperationException e) { + // REVISIT: Handle any reflective operation exception + } + } + return children; + } + + /** + * Create a child node based on the given accessor method representing a field or instance. + * + * @param accessor The method representing the field or instance accessor. + * @return A KaitaiTreeNode representing the created child node. + * @throws ReflectiveOperationException If an error occurs during reflective operations. + */ + private KaitaiTreeNode createNode(Method accessor) throws ReflectiveOperationException { + // Invoke the accessor method to get the value of the field or instance + Object value = accessor.invoke(kaitaiStruct); + String name = accessor.getName(); + + // Optional field could be not presented in the maps if it's missing in input + // "value" instances don't present in the maps + Integer start = attributeStart.get(name); + Integer end = attributeEnd.get(name); + boolean present = start != null && end != null; + + ByteRange current = getSpan(); + if (current != null && present) { + if (current.getStart() > start) { + start += Long.valueOf(current.getStart()).intValue(); + end += Long.valueOf(current.getStart()).intValue(); + } + } + + ByteRange span = present ? new ByteRange(start, end) : null; + + if (present && List.class.isAssignableFrom(accessor.getReturnType())) { + // Handle arrays or lists + Integer[] startOffsets = arrayStart.get(name).toArray(new Integer[0]); + Integer[] endOffsets = arrayEnd.get(name).toArray(new Integer[0]); + + // We are sure that the return type is a generic with one Class parameter, + // because KaitaiStruct java generator generates fields/methods with an ArrayList static type + ParameterizedType returnType = (ParameterizedType) accessor.getGenericReturnType(); + java.lang.reflect.Type elementType = returnType.getActualTypeArguments()[0]; + + return new ArrayNode(name, this, value, span, (Class) elementType, startOffsets, endOffsets); + } + + // For regular fields or instances, create a SimpleNode or StructNode as child node + return create(name, value, accessor.getReturnType(), span); + } +} diff --git a/src/main/java/io/github/proto4j/kaitai/vis/tree/StructTreeCellRenderer.java b/src/main/java/io/github/proto4j/kaitai/vis/tree/StructTreeCellRenderer.java new file mode 100644 index 0000000..83ce777 --- /dev/null +++ b/src/main/java/io/github/proto4j/kaitai/vis/tree/StructTreeCellRenderer.java @@ -0,0 +1,23 @@ +package io.github.proto4j.kaitai.vis.tree;//@date 28.07.2023 + +import javax.swing.*; +import javax.swing.tree.DefaultTreeCellRenderer; +import java.awt.*; + +public abstract class StructTreeCellRenderer extends DefaultTreeCellRenderer { + + @Override + public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus) { + super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus); + if (value instanceof KaitaiTreeNode) { + KaitaiTreeNode node = (KaitaiTreeNode) value; + Icon layeredIcon = this.getIconForNode(node); + if (layeredIcon != null) { + setIcon(layeredIcon); + } + } + return this; + } + + protected abstract Icon getIconForNode(KaitaiTreeNode node); +} diff --git a/src/main/java/io/github/proto4j/kaitai/vis/tree/StructTreeModel.java b/src/main/java/io/github/proto4j/kaitai/vis/tree/StructTreeModel.java new file mode 100644 index 0000000..d18beeb --- /dev/null +++ b/src/main/java/io/github/proto4j/kaitai/vis/tree/StructTreeModel.java @@ -0,0 +1,72 @@ +package io.github.proto4j.kaitai.vis.tree; //@date 28.07.2023 + +import io.kaitai.struct.KaitaiStruct; + +import javax.swing.event.EventListenerList; +import javax.swing.event.TreeModelListener; +import javax.swing.tree.TreeModel; +import javax.swing.tree.TreeNode; +import javax.swing.tree.TreePath; + +public class StructTreeModel implements TreeModel { + + protected Object root; + protected EventListenerList listenerList = new EventListenerList(); + + public StructTreeModel(Object root) { + this.root = root; + } + + public StructTreeModel(String name, KaitaiStruct struct) throws ReflectiveOperationException { + this(new StructNode(name, null, struct, State.SHOW)); + } + + // + // Default implementations for methods in the TreeModel interface. + // + + public boolean isLeaf(Object node) { + return getChildCount(node) == 0; + } + + public void valueForPathChanged(TreePath path, Object newValue) { + } + + public int getIndexOfChild(Object parent, Object child) { + for (int i = 0; i < getChildCount(parent); i++) { + if (getChild(parent, i).equals(child)) { + return i; + } + } + return -1; + } + + public void addTreeModelListener(TreeModelListener l) { + listenerList.add(TreeModelListener.class, l); + } + + public void removeTreeModelListener(TreeModelListener l) { + listenerList.remove(TreeModelListener.class, l); + } + + @Override + public StructNode getRoot() { + return (StructNode) root; + } + + @Override + public Object getChild(Object parent, int index) { + return parent instanceof TreeNode ? ((TreeNode) parent).getChildAt(index) : null; + } + + @Override + public int getChildCount(Object parent) { + return parent instanceof TreeNode ? ((TreeNode) parent).getChildCount() : 0; + } + + // TODO: Remove this enum + public enum State { + SHOW, + HIDE + } +} diff --git a/src/main/java/io/github/proto4j/kaitai/vis/util/ColorSpec.java b/src/main/java/io/github/proto4j/kaitai/vis/util/ColorSpec.java new file mode 100644 index 0000000..eded595 --- /dev/null +++ b/src/main/java/io/github/proto4j/kaitai/vis/util/ColorSpec.java @@ -0,0 +1,48 @@ +package io.github.proto4j.kaitai.vis.util; //@date 28.07.2023 + +import java.awt.*; + +// Simple way to generate a wide range of colors +public final class ColorSpec { + public static final int[] COLOR_SPEC = { + 0xCC2929, // 6: i*0x1C00 (+) + 0xC7CC29, // 6: i*0x1C0000 (-) + 0x29CC32, // 6: i*0x1C (+) + 0x29BECC, // 6: i*0x1C00 (-) + 0x3B29CC, // 6: i*0x1C0000 (+) + 0xCC29B5, // 6: i*0x1C (-) + }; + public static final int[] COLOR_OFFSETS = { + 0x1C00, + 0x1C0000, + 0x1C + }; + public static final Color[] COLORS = new Color[6 * 6]; + + static { + int index = 0; + for (int i = 0; i < COLOR_SPEC.length; i++) { + int start = COLOR_SPEC[i]; + int offset = COLOR_OFFSETS[i % COLOR_OFFSETS.length]; + boolean negative = i % 2 != 0; + + for (int j = 0; j < 6; j++) { + int value; + if (negative) { + value = start - (j * offset); + } else { + value = start + (j * offset); + } + COLORS[index] = new Color((value << 8) | 0xD7, true); + index++; + } + } + } + + private ColorSpec() { + } + + public static Color random() { + return COLORS[(int) (Math.random() * COLORS.length)]; + } +} diff --git a/src/main/java/io/kaitai/struct/visualizer/DataNode.java b/src/main/java/io/kaitai/struct/visualizer/DataNode.java deleted file mode 100644 index 5a5cde9..0000000 --- a/src/main/java/io/kaitai/struct/visualizer/DataNode.java +++ /dev/null @@ -1,202 +0,0 @@ -package io.kaitai.struct.visualizer; - -import io.kaitai.struct.KaitaiStruct; - -import javax.swing.*; -import javax.swing.tree.DefaultMutableTreeNode; -import javax.swing.tree.DefaultTreeModel; -import javax.swing.tree.MutableTreeNode; -import java.beans.PropertyChangeListener; -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.util.ArrayList; -import java.util.List; - -public class DataNode extends DefaultMutableTreeNode { - private boolean explored = false; - private final int depth; - private Object value; - private final Method method; - private final String name; - private final Integer posStart; - private final Integer posEnd; - - public DataNode(int depth, Object value, String name) { - this(depth, value, null, name, null, null); - } - - private DataNode(int depth, Object value, Method method, Integer posStart, Integer posEnd) { - this(depth, value, method, null, posStart, posEnd); - } - - private DataNode(int depth, Object value, Method method, String name, Integer posStart, Integer posEnd) { - this.depth = depth; - this.value = value; - this.method = method; - if (name != null) { - this.name = name; - } else { - this.name = method != null ? method.getName() : "?"; - } - this.posStart = posStart; - this.posEnd = posEnd; - - add(new DefaultMutableTreeNode("Loading...", false)); - setAllowsChildren(true); - - updateVisual(); - } - - public Integer posStart() { - return posStart; - } - - public Integer posEnd() { - return posEnd; - } - - private void updateVisual() { - StringBuilder sb = new StringBuilder(name); - if (value != null) { - if (value instanceof byte[]) { - sb.append(" = "); - byte[] bytes = (byte[]) value; - for (int i = 0; i < 10 && i < bytes.length; i++) { - sb.append(String.format("%02x ", bytes[i])); - } - } else if (value instanceof ArrayList) { - ArrayList list = (ArrayList) value; - sb.append(String.format(" (%d = 0x%x entries)", list.size(), list.size())); - } else if (value instanceof KaitaiStruct) { - // do not expand - } else { - sb.append(" = "); - sb.append(value.toString()); - } - } - setUserObject(sb.toString()); - } - - private void setChildren(List children) { - removeAllChildren(); - setAllowsChildren(children.size() > 0); - for (MutableTreeNode node : children) { - add(node); - } - explored = true; - } - - @Override - public boolean isLeaf() { - return false; - } - - public void explore(final DefaultTreeModel model, final PropertyChangeListener progressListener) { - if (explored) - return; - - SwingWorker, Void> worker = new SwingWorker, Void>() { - @Override - protected List doInBackground() throws Exception { - // Here access database if needed - setProgress(0); - final List children = new ArrayList<>(); - - System.out.println("exploring field " + name + ", value = " + value); - - // Wasn't loaded yet? - if (value == null) { - DataNode parentNode = (DataNode) parent; - System.out.println("parentNode: name = " + parentNode.name + "; value = " + parentNode.value); - value = method.invoke(parentNode.value); - } - - // Still null? - if (value == null) { - value = "[null]"; - updateVisual(); - return children; - } - - Class cl = value.getClass(); - System.out.println("cl = " + cl); - - if (isImmediate(value, cl)) { - updateVisual(); - return children; - } - - if (value instanceof ArrayList) { - ArrayList list = (ArrayList) value; - - for (int i = 0; i < list.size(); i++) { - Object el = list.get(i); - String arrayIdxStr = String.format("%04d", i); - - children.add(new DataNode(depth + 1, el, arrayIdxStr)); - } - } else if (value instanceof KaitaiStruct) { - DebugAids debug = DebugAids.fromStruct((KaitaiStruct) value); - - for (Method m : cl.getDeclaredMethods()) { - // Ignore static methods, i.e. "fromFile" - if (Modifier.isStatic(m.getModifiers())) - continue; - - String methodName = m.getName(); - - // Ignore all internal methods, i.e. "_io", "_parent", "_root" - if (methodName.charAt(0) == '_') - continue; - - try { - Field field = cl.getDeclaredField(methodName); - field.setAccessible(true); - Object curValue = field.get(value); - - Integer posStart = debug.getStart(methodName); - Integer posEnd = debug.getEnd(methodName); - - DataNode dn = new DataNode(depth + 1, curValue, m, posStart, posEnd); - children.add(dn); - } catch (NoSuchFieldException e) { - System.out.println("no field, ignoring method " + methodName); - } - } - } - setProgress(0); - return children; - } - - @Override - protected void done() { - try { - setChildren(get()); - model.nodeStructureChanged(DataNode.this); - } catch (Exception e) { - e.printStackTrace(); - // Notify user of error. - } - super.done(); - } - }; - if (progressListener != null) { - worker.getPropertyChangeSupport().addPropertyChangeListener("progress", progressListener); - } - worker.execute(); - } - - public static boolean isImmediate(Object value, Class cl) { - return cl.isPrimitive() || - value instanceof Byte || - value instanceof Short || - value instanceof Integer || - value instanceof Long || - value instanceof Float || - value instanceof Double || - value instanceof String || - value instanceof Boolean || - value instanceof byte[]; - } -} diff --git a/src/main/java/io/kaitai/struct/visualizer/DebugAids.java b/src/main/java/io/kaitai/struct/visualizer/DebugAids.java deleted file mode 100644 index 3da451a..0000000 --- a/src/main/java/io/kaitai/struct/visualizer/DebugAids.java +++ /dev/null @@ -1,60 +0,0 @@ -package io.kaitai.struct.visualizer; - -import io.kaitai.struct.KaitaiStruct; - -import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.Map; - -public class DebugAids { - private Map attrStart; - private Map attrEnd; - private Map> arrStart; - private Map> arrEnd; - - private DebugAids( - Map attrStart, - Map attrEnd, - Map> arrStart, - Map> arrEnd - ) { - this.attrStart = attrStart; - this.attrEnd = attrEnd; - this.arrStart = arrStart; - this.arrEnd = arrEnd; - } - - public Integer getStart(String attrName) { - return attrStart.get(attrName); - } - - public Integer getEnd(String attrName) { - return attrEnd.get(attrName); - } - - public Integer getStart(String attrName, int idx) { - ArrayList positions = arrStart.get(attrName); - return (positions != null) ? positions.get(idx) : null; - } - - public Integer getEnd(String attrName, int idx) { - ArrayList positions = arrEnd.get(attrName); - return (positions != null) ? positions.get(idx) : null; - } - - public static DebugAids fromStruct(KaitaiStruct struct) throws NoSuchFieldException, IllegalAccessException { - Class ksyClass = struct.getClass(); - - Field fAttrStart = ksyClass.getDeclaredField("_attrStart"); - Field fAttrEnd = ksyClass.getDeclaredField("_attrEnd"); - Field fArrStart = ksyClass.getDeclaredField("_arrStart"); - Field fArrEnd = ksyClass.getDeclaredField("_arrEnd"); - - return new DebugAids( - (Map) fAttrStart.get(struct), - (Map) fAttrEnd.get(struct), - (Map>) fArrStart.get(struct), - (Map>) fArrEnd.get(struct) - ); - } -} diff --git a/src/main/java/io/kaitai/struct/visualizer/Main.java b/src/main/java/io/kaitai/struct/visualizer/Main.java new file mode 100644 index 0000000..900e30b --- /dev/null +++ b/src/main/java/io/kaitai/struct/visualizer/Main.java @@ -0,0 +1,126 @@ +package io.kaitai.struct.visualizer; //@date 28.07.2023 + +import com.beust.jcommander.JCommander; +import com.beust.jcommander.Parameter; +import com.formdev.flatlaf.FlatDarkLaf; +import com.formdev.flatlaf.FlatLightLaf; +import io.github.proto4j.kaitai.vis.JVis; +import io.github.proto4j.kaitai.vis.KsyCompiler; +import io.kaitai.struct.KaitaiStream; +import io.kaitai.struct.KaitaiStruct; +import io.kaitai.struct.RandomAccessFileKaitaiStream; + +import javax.swing.*; +import java.awt.*; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.nio.file.Files; +import java.nio.file.Path; + +public class Main implements Runnable { + @Parameter(names = "-ksy", description = "Path to the .ksy file") + String ksyPath; + + @Parameter(description = "Path to the binary", required = true) + String binaryPath; + + @Parameter(names = "-java", description = "Path to generated Java file") + String javaPath; + + @Parameter(names = "-dark", description = "Enables dark layout.") + boolean darkLaf; + + @Parameter(names = {"-h", "--help"}, description = "Show this help message.") + boolean showHelp; + + public static void main(String[] args) { + Main main = new Main(); + JCommander commander = JCommander.newBuilder() + .addObject(main) + .build(); + + try { + commander.parse(args); + } catch (Exception e) { + if (!main.showHelp) { + System.out.println(e.toString()); + } + commander.usage(); + System.exit(1); + } + + if (main.showHelp) { + commander.usage(); + System.exit(0); + } + + SwingUtilities.invokeLater(main); + } + + + @Override + public void run() { + verifyArgs(); + setupLaf(); + + String javaSourceCode = null; + if (this.ksyPath != null) { + javaSourceCode = KsyCompiler.compileToJava(this.ksyPath); + } + + if (this.javaPath != null) try { + javaSourceCode = Files.readString(Path.of(this.javaPath)); + } catch (IOException e) { + e.printStackTrace(); + System.exit(1); + } + + JVis vis = new JVis(); + if (javaSourceCode != null) try { + Class cls = KsyCompiler.createClass(javaSourceCode); + KaitaiStream stream = new RandomAccessFileKaitaiStream(this.binaryPath); + KaitaiStruct struct = KsyCompiler.newInstance(cls, stream); + + vis.display(struct, this.binaryPath); + } catch (Exception e) { + if (e instanceof InvocationTargetException) { + ((InvocationTargetException) e).getTargetException().printStackTrace(); + } else { + e.printStackTrace(); + } + System.exit(1); + } + + show(vis); + } + + private void verifyArgs() { + if (this.javaPath == null && this.ksyPath == null) { + throw new IllegalArgumentException("Either Ksy-File or Java-File has to be provided!"); + } + } + + private void setupLaf() { + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + if (this.darkLaf) { + FlatDarkLaf.setup(); + } else { + FlatLightLaf.setup(); + } + } catch (Exception ex) { + ex.printStackTrace(); + System.exit(1); + } + } + + private void show(JVis vis) { + final JFrame jframe = new JFrame("Kaitai Struct Visualizer"); + + jframe.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); + jframe.getContentPane().setLayout(new BorderLayout()); + jframe.getContentPane().add(vis); + jframe.pack(); + jframe.setVisible(true); + } +} diff --git a/src/main/java/io/kaitai/struct/visualizer/MainWindow.java b/src/main/java/io/kaitai/struct/visualizer/MainWindow.java deleted file mode 100644 index 48bab46..0000000 --- a/src/main/java/io/kaitai/struct/visualizer/MainWindow.java +++ /dev/null @@ -1,26 +0,0 @@ -package io.kaitai.struct.visualizer; - -import javax.swing.*; -import java.io.IOException; - -public class MainWindow extends JFrame { - private static final String APP_NAME = "Kaitai Struct Visualizer"; - private static final String VERSION = "0.8"; - - private VisualizerPanel vis; - - public MainWindow() throws IOException { - super(APP_NAME + " v" + VERSION); - vis = new VisualizerPanel(); - setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); - getContentPane().add(vis.getSplitPane()); - pack(); - setVisible(true); - } - - public static void main(final String arg[]) throws Exception { - UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); - MainWindow mw = new MainWindow(); - mw.vis.loadAll(arg[0], arg[1]); - } -} diff --git a/src/main/java/io/kaitai/struct/visualizer/VisualizerPanel.java b/src/main/java/io/kaitai/struct/visualizer/VisualizerPanel.java deleted file mode 100644 index 9021c91..0000000 --- a/src/main/java/io/kaitai/struct/visualizer/VisualizerPanel.java +++ /dev/null @@ -1,253 +0,0 @@ -package io.kaitai.struct.visualizer; - -import java.awt.Color; -import java.awt.Font; -import java.io.IOException; -import java.lang.reflect.Constructor; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import io.kaitai.struct.ByteBufferKaitaiStream; -import io.kaitai.struct.CompileLog; -import io.kaitai.struct.JavaRuntimeConfig; -import io.kaitai.struct.KaitaiStream; -import io.kaitai.struct.KaitaiStruct; -import io.kaitai.struct.Main; -import io.kaitai.struct.RuntimeConfig; -import io.kaitai.struct.Version; -import io.kaitai.struct.format.ClassSpec; -import io.kaitai.struct.format.KSVersion; -import io.kaitai.struct.formats.JavaClassSpecs; -import io.kaitai.struct.formats.JavaKSYParser; -import io.kaitai.struct.languages.JavaCompiler$; - -import org.mdkt.compiler.InMemoryJavaCompiler; - -import javax.swing.event.TreeExpansionEvent; -import javax.swing.event.TreeSelectionEvent; -import javax.swing.event.TreeSelectionListener; -import javax.swing.event.TreeWillExpandListener; -import javax.swing.tree.DefaultTreeModel; -import javax.swing.tree.ExpandVetoException; -import javax.swing.tree.TreePath; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.JSplitPane; -import javax.swing.JTree; - -import tv.porst.jhexview.JHexView; -import tv.porst.jhexview.SimpleDataProvider; - -public class VisualizerPanel extends JPanel { - /** Package to generate classes in. */ - private static final String DEST_PACKAGE = "io.kaitai.struct.visualized"; - /** - * Regexp with 2 groups: class name and type parameters. Type parameters - * must be parsed with {@link #PARAMETER_NAME}. - */ - private static final Pattern TOP_CLASS_NAME_AND_PARAMETERS = Pattern.compile( - "public class (.+?) extends KaitaiStruct.*" + - "public \\1\\(KaitaiStream _io, KaitaiStruct _parent, \\1 _root(.*?)\\)", - Pattern.DOTALL - ); - /** Regexp, used to get parameter names from the generated source. */ - private static final Pattern PARAMETER_NAME = Pattern.compile(", \\S+ ([^,\\s]+)"); - - /** Color of hex editor section headers. */ - private static final Color HEADER = new Color(0x0000c0); - /** Color of hex data in HEX and ASCII sections. */ - private static final Color UNMODIFIED = Color.BLACK; - /** Background color selected hex data in HEX and ASCII sections. */ - private static final Color SELECTION = new Color(0xc0c0c0); - - private final JTree tree = new JTree(); - private final DefaultTreeModel model = new DefaultTreeModel(null); - private final JHexView hexEditor = new JHexView(); - private final JSplitPane splitPane; - - private KaitaiStruct struct; - - public VisualizerPanel() throws IOException { - super(); - JScrollPane treeScroll = new JScrollPane(tree); - - hexEditor.setSeparatorsVisible(false); - hexEditor.setBytesPerColumn(1); - hexEditor.setColumnSpacing(8); - hexEditor.setHeaderFontStyle(Font.BOLD); - - hexEditor.setFontColorHeader(HEADER); - hexEditor.setFontColorOffsetView(HEADER); - - hexEditor.setFontColorHexView1(UNMODIFIED); - hexEditor.setFontColorHexView2(UNMODIFIED); - hexEditor.setFontColorAsciiView(UNMODIFIED); - - hexEditor.setSelectionColor(SELECTION); - - hexEditor.setBackgroundColorOffsetView(hexEditor.getBackground()); - hexEditor.setBackgroundColorHexView(hexEditor.getBackground()); - hexEditor.setBackgroundColorAsciiView(hexEditor.getBackground()); - - splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, treeScroll, hexEditor); - - tree.setShowsRootHandles(true); - KaitaiTreeListener treeListener = new KaitaiTreeListener(); - tree.addTreeWillExpandListener(treeListener); - tree.addTreeSelectionListener(treeListener); - tree.setModel(model); - } - - public void loadAll(String dataFileName, String ksyFileName) throws Exception { - parseFileWithKSY(ksyFileName, dataFileName); - loadStruct(); - } - - private void loadStruct() throws IOException { - struct._io().seek(0); - byte[] buf = struct._io().readBytesFull(); - hexEditor.setData(new SimpleDataProvider(buf)); - hexEditor.setDefinitionStatus(JHexView.DefinitionStatus.DEFINED); - - final DataNode root = new DataNode(0, struct, "[root]"); - model.setRoot(root); - root.explore(model /*, progressListener */, null); - } - - public JSplitPane getSplitPane() { - return splitPane; - } - - /** - * Compiles a given .ksy file into Java class source. - * @param ksyFileName - * @return Java class source code as a string - */ - private static String compileKSY(String ksyFileName) { - KSVersion.current_$eq(Version.version()); - final ClassSpec spec = JavaKSYParser.fileNameToSpec(ksyFileName); - final JavaClassSpecs specs = new JavaClassSpecs(null, null, spec); - - final RuntimeConfig config = new RuntimeConfig( - false,// autoRead - do not call `_read` automatically in constructor - true, // readStoresPos - enable generation of a position info which is accessed in DebugAids later - true, // opaqueTypes - null, // cppConfig - null, // goPackage - new JavaRuntimeConfig( - DEST_PACKAGE, - // Class to be invoked in `fromFile` helper methods - "io.kaitai.struct.ByteBufferKaitaiStream", - // Exception class expected to be thrown on end-of-stream errors - "java.nio.BufferUnderflowException" - ), - null, // dotNetNamespace - null, // phpNamespace - null, // pythonPackage - null, // nimModule - null // nimOpaque - ); - - Main.importAndPrecompile(specs, config).value(); - final CompileLog.SpecSuccess result = Main.compile(specs, spec, JavaCompiler$.MODULE$, config); - return result.files().apply(0).contents(); - } - - /** - * Compiles Java source (given as a string) into bytecode and loads it into current JVM. - * @param javaSrc Java source as a string - * @return Class reference, which can be used to instantiate the class, call its - * static methods, etc. - * @throws Exception - */ - private void parseFileWithKSY(String ksyFileName, String binaryFileName) throws Exception { - final String javaSrc = compileKSY(ksyFileName); - final Matcher m = TOP_CLASS_NAME_AND_PARAMETERS.matcher(javaSrc); - if (!m.find()) { - throw new RuntimeException("Unable to find top-level class in generated .java"); - } - // Parse parameter names - final ArrayList paramNames = new ArrayList<>(); - final Matcher p = PARAMETER_NAME.matcher(m.group(2)); - while (p.find()) { - paramNames.add(p.group(1)); - } - - final Class ksyClass = InMemoryJavaCompiler.newInstance().compile(DEST_PACKAGE + "." + m.group(1), javaSrc); - struct = construct(ksyClass, paramNames, binaryFileName); - - // Find and run "_read" that does actual parsing - // TODO: wrap this in try-catch block - Method readMethod = ksyClass.getMethod("_read"); - readMethod.invoke(struct); - } - private static KaitaiStruct construct(Class ksyClass, List paramNames, String binaryFileName) throws Exception { - final Constructor c = findConstructor(ksyClass); - final Class[] types = c.getParameterTypes(); - final Object[] args = new Object[types.length]; - args[0] = new ByteBufferKaitaiStream(binaryFileName); - for (int i = 3; i < args.length; ++i) { - args[i] = getDefaultValue(types[i]); - } - // TODO: get parameters from user - return (KaitaiStruct)c.newInstance(args); - } - private static Constructor findConstructor(Class ksyClass) { - for (final Constructor c : ksyClass.getDeclaredConstructors()) { - final Class[] types = c.getParameterTypes(); - if (types.length >= 3 - && types[0] == KaitaiStream.class - && types[1] == KaitaiStruct.class - && types[2] == ksyClass - ) { - return c; - } - } - throw new IllegalArgumentException(ksyClass + " has no KaitaiStruct-generated constructor"); - } - private static Object getDefaultValue(Class clazz) { - if (clazz == boolean.class) return false; - if (clazz == char.class ) return (char)0; - if (clazz == byte.class ) return (byte)0; - if (clazz == short.class ) return (short)0; - if (clazz == int.class ) return 0; - if (clazz == long.class ) return 0L; - if (clazz == float.class ) return 0.0f; - if (clazz == double.class ) return 0.0; - return null; - } - - public class KaitaiTreeListener implements TreeWillExpandListener, TreeSelectionListener { - @Override - public void treeWillExpand(TreeExpansionEvent event) throws ExpandVetoException { - TreePath path = event.getPath(); - if (path.getLastPathComponent() instanceof DataNode) { - DataNode node = (DataNode) path.getLastPathComponent(); - node.explore(model /* , progressListener */, null); - } - } - - @Override - public void treeWillCollapse(TreeExpansionEvent event) throws ExpandVetoException { - } - - @Override - public void valueChanged(TreeSelectionEvent event) { - hexEditor.getSelectionModel().clearSelection(); - for (final TreePath path : tree.getSelectionPaths()) { - final Object selected = path.getLastPathComponent(); - if (!(selected instanceof DataNode)) continue; - - final DataNode node = (DataNode)selected; - final Integer start = node.posStart(); - final Integer end = node.posEnd(); - if (start == null || end == null) continue; - // Selection in nibbles, so multiply by 2 - hexEditor.getSelectionModel().addSelectionInterval(2*start, 2*end-1); - } - } - } -} diff --git a/src/main/resources/icons/dataTypes/array.svg b/src/main/resources/icons/dataTypes/array.svg new file mode 100644 index 0000000..1946176 --- /dev/null +++ b/src/main/resources/icons/dataTypes/array.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/main/resources/icons/dataTypes/enum.svg b/src/main/resources/icons/dataTypes/enum.svg new file mode 100644 index 0000000..94a4508 --- /dev/null +++ b/src/main/resources/icons/dataTypes/enum.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/main/resources/icons/dataTypes/enum_dark.svg b/src/main/resources/icons/dataTypes/enum_dark.svg new file mode 100644 index 0000000..628e629 --- /dev/null +++ b/src/main/resources/icons/dataTypes/enum_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/main/resources/icons/dataTypes/struct.svg b/src/main/resources/icons/dataTypes/struct.svg new file mode 100644 index 0000000..e8fd3e9 --- /dev/null +++ b/src/main/resources/icons/dataTypes/struct.svg @@ -0,0 +1,67 @@ + + + + + + + + + diff --git a/src/main/resources/icons/dataTypes/struct_dark.svg b/src/main/resources/icons/dataTypes/struct_dark.svg new file mode 100644 index 0000000..d9bc4f1 --- /dev/null +++ b/src/main/resources/icons/dataTypes/struct_dark.svg @@ -0,0 +1,67 @@ + + + + + + + + + diff --git a/src/main/resources/icons/dataTypes/unknown.svg b/src/main/resources/icons/dataTypes/unknown.svg new file mode 100644 index 0000000..650a130 --- /dev/null +++ b/src/main/resources/icons/dataTypes/unknown.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/main/resources/icons/dataTypes/unknown_dark.svg b/src/main/resources/icons/dataTypes/unknown_dark.svg new file mode 100644 index 0000000..01fa4f6 --- /dev/null +++ b/src/main/resources/icons/dataTypes/unknown_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/main/resources/icons/dataTypes/value.svg b/src/main/resources/icons/dataTypes/value.svg new file mode 100644 index 0000000..a8e65ef --- /dev/null +++ b/src/main/resources/icons/dataTypes/value.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/main/resources/icons/dataTypes/value_dark.svg b/src/main/resources/icons/dataTypes/value_dark.svg new file mode 100644 index 0000000..851c5e8 --- /dev/null +++ b/src/main/resources/icons/dataTypes/value_dark.svg @@ -0,0 +1,9 @@ + + + + + + + + +