All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.kaitai.struct.JavaMain.scala Maven / Gradle / Ivy

package io.kaitai.struct

import java.io.{File, FileWriter}
import java.net.URLDecoder

import io.kaitai.struct.CompileLog._
import io.kaitai.struct.JavaMain.CLIConfig
import io.kaitai.struct.format.{ClassSpec, ClassSpecs, KSVersion, YAMLParseException}
import io.kaitai.struct.formats.JavaKSYParser
import io.kaitai.struct.languages.components.LanguageCompilerStatic
import io.kaitai.struct.precompile.ErrorInInput

object JavaMain {
  KSVersion.current = BuildInfo.version

  case class CLIConfig(
    verbose: Seq[String] = Seq(),
    srcFiles: Seq[File] = Seq(),
    outDir: File = new File("."),
    targets: Seq[String] = Seq(),
    throwExceptions: Boolean = false,
    jsonOutput: Boolean = false,
    importPaths: Seq[String] = Seq(),
    runtime: RuntimeConfig = RuntimeConfig()
  )

  val ALL_LANGS = LanguageCompilerStatic.NAME_TO_CLASS.keySet
  val VALID_LANGS = ALL_LANGS + "all"

  def parseCommandLine(args: Array[String]): Option[CLIConfig] = {
    val parser = new scopt.OptionParser[CLIConfig](BuildInfo.name) {
      override def showUsageOnError = true

      head(BuildInfo.name, BuildInfo.version)

      arg[File]("...") unbounded() action { (x, c) =>
        c.copy(srcFiles = c.srcFiles :+ x) } text("source files (.ksy)")

      //      opt[File]('o', "outfile") valueName("") action { (x, c) =>
      //        c.copy(outDir = x)
      //      } text("output filename (only if processing 1 file)")

      opt[String]('t', "target") required() unbounded() valueName("") action { (x, c) =>
        // TODO: make support for something like "-t java,python"
        if (x == "all") {
          c.copy(targets = ALL_LANGS.toSeq)
        } else {
          c.copy(targets = c.targets :+ x)
        }
      } text(s"target languages (${VALID_LANGS.mkString(", ")})") validate { x =>
        if (VALID_LANGS.contains(x)) {
          success
        } else {
          failure(s"'$x' is not a valid target language; valid ones are: ${VALID_LANGS.mkString(", ")}")
        }
      }

      opt[File]('d', "outdir") valueName("") action { (x, c) =>
        c.copy(outDir = x)
      } text("output directory (filenames will be auto-generated)")

      val importPathExample = List("", "", "...").mkString(File.pathSeparator)
      opt[String]('I', "import-path") valueName(importPathExample) action { (x, c) =>
        c.copy(importPaths = c.importPaths ++ x.split(File.pathSeparatorChar))
      } text(".ksy library search path(s) for imports (see also KSPATH env variable)")

      opt[String]("go-package") valueName("") action { (x, c) =>
        c.copy(runtime = c.runtime.copy(goPackage = x))
      } text("Go package (Go only, default: none)")

      opt[String]("java-package") valueName("") action { (x, c) =>
        c.copy(runtime = c.runtime.copy(javaPackage = x))
      } text("Java package (Java only, default: root package)")

      opt[String]("java-from-file-class") valueName("") action { (x, c) =>
        c.copy(runtime = c.runtime.copy(javaFromFileClass = x))
      } text(s"Java class to be invoked in fromFile() helper (default: ${RuntimeConfig().javaFromFileClass})")

      opt[String]("dotnet-namespace") valueName("") action { (x, c) =>
        c.copy(runtime = c.runtime.copy(dotNetNamespace = x))
      } text(".NET Namespace (.NET only, default: Kaitai)")

      opt[String]("php-namespace") valueName("") action { (x, c) =>
        c.copy(runtime = c.runtime.copy(phpNamespace = x))
      } text("PHP Namespace (PHP only, default: root package)")

      opt[String]("python-package") valueName("") action { (x, c) =>
        c.copy(runtime = c.runtime.copy(pythonPackage = x))
      } text("Python package (Python only, default: root package)")

      opt[Boolean]("opaque-types") action { (x, c) =>
        c.copy(runtime = c.runtime.copy(opaqueTypes = x))
      } text("opaque types allowed, default: false")

      opt[Unit]("ksc-exceptions") action { (x, c) =>
        c.copy(throwExceptions = true)
      } text("ksc throws exceptions instead of human-readable error messages")

      opt[Unit]("ksc-json-output") action { (x, c) =>
        c.copy(jsonOutput = true)
      } text("output compilation results as JSON to stdout")

      opt[String]("verbose") action { (x, c) =>
        // TODO: make support for something like "--verbose file,parent"
        if (x == "all") {
          c.copy(verbose = Log.VALID_SUBSYS)
        } else {
          c.copy(verbose = c.verbose :+ x)
        }
      } text("verbose output") validate { x =>
        if (x == "all" || Log.VALID_SUBSYS.contains(x)) {
          success
        } else {
          failure(s"'$x' is not a valid verbosity flag; valid ones are: ${Log.VALID_SUBSYS.mkString(", ")}")
        }
      }

      opt[Unit]("debug") action { (x, c) =>
        c.copy(runtime = c.runtime.copy(debug = true))
      } text("enable debugging helpers (mostly used by visualization tools)")
      help("help") text("display this help and exit")
      version("version") text("output version information and exit")
    }

    parser.parse(args, CLIConfig())
  }

  /**
    * Insanely hacky method that relies on some JVM black magic to get
    * application "home". From that, we can check if it has a "formats" subdir,
    * and if it does, consider that an additional default ksy imports path.
    * @return additional import search path ("default", platform-specific one),
    *         if it's found
    */
  private def homePath: Option[String] = {
    // fStr is mysterious and probably unreliable, but that's the best I've
    // came up with. It uses java.security API, in which CodeSource is used
    // to have indicative "code location", but AFAIK, in Sun/Oracle applications
    // it is only used as URL for comparison purposes. It is URL-encoded, so
    // we need to decode it as well.
    //
    // Linux, from IDE:
    // $HOME/git/kaitai_struct/compiler/jvm/target/scala-2.11/classes/
    //
    // Linux, from stage:
    // $HOME/git/kaitai_struct/compiler/jvm/target/universal/stage/lib/io.kaitai.kaitai-struct-compiler-0.7-SNAPSHOT.jar
    //
    // Linux, from "sbt compilerJVM/run"
    // $HOME/git/kaitai_struct/compiler/jvm/target/scala-2.11/classes/
    //
    // Linux, from universal, custom install path:
    // /tmp/a%20b/kaitai-struct-compiler-0.7-SNAPSHOT/lib/io.kaitai.kaitai-struct-compiler-0.7-SNAPSHOT.jar
    //
    // Linux, from Debian install:
    // /usr/share/kaitai-struct-compiler/lib/io.kaitai.kaitai-struct-compiler-0.7-SNAPSHOT.jar
    //
    // Windows, default install path:
    // /C:/Program%20Files/kaitai-struct-compiler/lib/io.kaitai.kaitai-struct-compiler-0.7-SNAPSHOT.jar
    //
    // Windows, custom install path with spaces and non-latin chars:
    // /G:/%d0%b3%d0%b4%d0%b5-%d1%82%d0%be%20%d1%82%d0%b0%d0%bc/lib/io.kaitai.kaitai-struct-compiler-0.7-SNAPSHOT.jar

    val fStr = classOf[JavaMain].getProtectionDomain.getCodeSource.getLocation.getPath
    Log.importOps.info(() => s"home path: location = $fStr")

    if (fStr.endsWith(".jar")) {
      val fDec = URLDecoder.decode(fStr, "UTF-8")
      Log.importOps.info(() => s"... URL-decoded = $fDec")

      val homeFile = new File(fDec).getParentFile.getParentFile
      Log.importOps.info(() => s"... home = $homeFile")

      if (homeFile.exists) {
        val homeFormat = new File(homeFile, "formats")
        Log.importOps.info(() => s"... formats = $homeFormat")
        if (homeFormat.exists) {
          Some(homeFormat.toString)
        } else {
          Log.importOps.info(() => "... home formats dir doesn't exist => fail")
          None
        }
      } else {
        Log.importOps.info(() => s"... home doesn't exist => no home import paths")
        None
      }
    } else {
      Log.importOps.info(() => s"... not a jar, we're not running a packaged app => no home")
      None
    }
  }

  private def envPaths: List[String] =
    sys.env.get("KSPATH").toList.flatMap((x) => x.split(File.pathSeparatorChar))

  def main(args: Array[String]): Unit = {
    parseCommandLine(args) match {
      case None => System.exit(1)
      case Some(config0) =>
        Log.initFromVerboseFlag(config0.verbose)
        val config = config0.copy(importPaths = config0.importPaths ++ envPaths ++ homePath)
        new JavaMain(config).run()
    }
  }
}

class JavaMain(config: CLIConfig) {
  import JavaMain._

  def run(): Unit = {
    val logs: Map[String, InputEntry] = config.srcFiles.map { srcFile =>
      val log = if (config.throwExceptions) {
        compileOneInput(srcFile.toString)
      } else {
        try {
          compileOneInput(srcFile.toString)
        } catch {
          case ex: Throwable =>
            InputFailure(List(exceptionToCompileError(ex, srcFile.toString)))
        }
      }
      srcFile.toString -> log
    }.toMap

    if (config.jsonOutput) {
      Console.println(JSON.mapToJson(logs))
    } else {
      if (logsHaveErrors(logs))
        System.exit(2)
    }
  }

  private def logsHaveErrors(logs: Map[String, InputEntry]): Boolean =
    logs.values.map(_.hasErrors).max

  private def compileOneInput(srcFile: String) = {
    Log.fileOps.info(() => s"parsing $srcFile...")
    val specs = JavaKSYParser.localFileToSpecs(srcFile, config)

    val output: Map[String, Map[String, SpecEntry]] = config.targets match {
      case Seq(lang) =>
        // single target, just use target directory as is
        val out = compileOneLang(specs, lang, config.outDir.toString)
        Map(lang -> out)
      case _ =>
        // multiple targets, use additional directories
        compileAllLangs(specs, config)
    }
    InputSuccess(
      specs.firstSpec.nameAsStr,
      output
    )
  }

  def compileAllLangs(specs: ClassSpecs, config: CLIConfig): Map[String, Map[String, SpecEntry]] = {
    config.targets.map { lang =>
      lang -> compileOneLang(specs, lang, s"${config.outDir}/$lang")
    }.toMap
  }

  def compileOneLang(specs: ClassSpecs, langStr: String, outDir: String): Map[String, SpecEntry] = {
    Log.fileOps.info(() => s"... compiling it for $langStr... ")
    val lang = LanguageCompilerStatic.byString(langStr)
    specs.map { case (_, classSpec) =>
      val res = try {
        compileSpecAndWriteToFile(specs, classSpec, lang, outDir)
      } catch {
        case ex: Throwable =>
          if (config.throwExceptions)
            ex.printStackTrace()
          SpecFailure(List(exceptionToCompileError(ex, classSpec.nameAsStr)))
      }
      classSpec.nameAsStr -> res
    }.toMap
  }

  def compileSpecAndWriteToFile(
    specs: ClassSpecs,
    spec: ClassSpec,
    lang: LanguageCompilerStatic,
    outDir: String
  ): SpecSuccess = {
    val res = Main.compile(specs, spec, lang, config.runtime)
    res.files.foreach { (file) =>
      Log.fileOps.info(() => s".... writing ${file.fileName}")

      val outPath = new File(outDir + "/" + file.fileName)

      // Ensure that all directories leading to this path exist
      val parentPath = outPath.getParentFile
      parentPath.mkdirs

      val fw = new FileWriter(outPath)
      fw.write(file.contents)
      fw.close()
    }
    res
  }

  private def exceptionToCompileError(ex: Throwable, srcFile: String): CompileError = {
    if (!config.jsonOutput)
      Console.println(ex.getMessage)
    ex match {
      case ype: YAMLParseException =>
        CompileError("(main)", ype.path, ype.msg)
      case e: ErrorInInput =>
        val file = e.file.getOrElse(srcFile)
        val msg = Option(e.getCause) match {
          case Some(cause) => cause.getMessage
          case None => e.getMessage
        }
        CompileError(file, e.path, msg)
      case _ =>
        CompileError(srcFile, List(), ex.getMessage)
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy