Skip to content

Commit 6d70cdb

Browse files
[swiftsrc2cpg] Added support for binary .plist files (#5691)
1 parent 5db8a97 commit 6d70cdb

File tree

4 files changed

+118
-3
lines changed

4 files changed

+118
-3
lines changed

joern-cli/frontends/swiftsrc2cpg/build.sbt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ libraryDependencies ++= Seq(
2626
"io.shiftleft" %% "codepropertygraph" % Versions.cpg,
2727
"com.lihaoyi" %% "upickle" % Versions.upickle,
2828
// we want to use also Google Gson for its streaming abilities for very large Json files:
29-
"com.google.code.gson" % "gson" % Versions.gson,
29+
"com.google.code.gson" % "gson" % Versions.gson,
30+
// to handle property list files of various formats (i.e., binary and plain XML)
31+
"com.googlecode.plist" % "dd-plist" % "1.28",
3032
"org.scalatest" %% "scalatest" % Versions.scalatest % Test
3133
)
3234

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,54 @@
11
package io.joern.swiftsrc2cpg.passes
22

3+
import com.dd.plist.PropertyListParser
34
import io.joern.swiftsrc2cpg.Config
45
import io.joern.swiftsrc2cpg.utils.AstGenRunner
56
import io.joern.x2cpg.passes.frontend.XConfigFileCreationPass
67
import io.shiftleft.codepropertygraph.generated.Cpg
8+
import io.shiftleft.codepropertygraph.generated.nodes.NewConfigFile
9+
import io.shiftleft.semanticcpg.utils.FileUtil.*
10+
import io.shiftleft.utils.IOUtils
11+
import org.slf4j.LoggerFactory
712

813
import java.nio.file.Path
14+
import scala.util.{Failure, Success, Try}
915

1016
class ConfigFileCreationPass(cpg: Cpg, config: Config)
1117
extends XConfigFileCreationPass(
1218
cpg,
1319
config = config.withDefaultIgnoredFilesRegex(AstGenRunner.AstGenDefaultIgnoreRegex)
1420
) {
1521

16-
override val configFileFilters: List[Path => Boolean] = List(extensionFilter(".plist"), extensionFilter(".xib"))
22+
private val logger = LoggerFactory.getLogger(this.getClass)
1723

24+
private val PlistExt: String = ".plist"
25+
26+
override val configFileFilters: List[Path => Boolean] = List(extensionFilter(PlistExt), extensionFilter(".xib"))
27+
28+
private def isBinaryPlist(file: Path): Boolean = {
29+
if (!file.extension().contains(PlistExt)) return false
30+
Try(IOUtils.readLinesInFile(file)) match {
31+
case Success(dataBeginning :: _) => dataBeginning.trim.startsWith("bplist")
32+
case _ => false
33+
}
34+
}
35+
36+
override def runOnPart(diffGraph: DiffGraphBuilder, file: Path): Unit = {
37+
val parseResult = if (isBinaryPlist(file)) {
38+
val sourceComment = s"\n<!--This file was generated from ${file.toAbsolutePath}-->"
39+
Try((PropertyListParser.parse(file.toFile).toXMLPropertyList, sourceComment))
40+
} else {
41+
Try((IOUtils.readEntireFile(file), ""))
42+
}
43+
parseResult match {
44+
case Success((content, sourceComment)) =>
45+
val configFileContent = s"$content$sourceComment"
46+
val name = configFileName(file)
47+
val configNode = NewConfigFile().name(name).content(configFileContent)
48+
logger.debug(s"Adding config file $name")
49+
diffGraph.addNode(configNode)
50+
case Failure(error) =>
51+
logger.warn(s"Unable to create config file node for ${file.toAbsolutePath}: $error")
52+
}
53+
}
1854
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package io.joern.swiftsrc2cpg.passes.config
2+
3+
import com.dd.plist.PropertyListConverter
4+
import flatgraph.DNode
5+
import io.joern.swiftsrc2cpg.Config
6+
import io.joern.swiftsrc2cpg.passes.ConfigFileCreationPass
7+
import io.shiftleft.codepropertygraph.generated.GraphSchema
8+
import io.shiftleft.codepropertygraph.generated.nodes.NewConfigFile
9+
import io.shiftleft.semanticcpg.utils.FileUtil
10+
import org.scalatest.funspec.AnyFunSpec
11+
import org.scalatest.matchers.should.Matchers
12+
13+
import java.nio.file.Files
14+
15+
class ConfigFileCreationPassTests extends AnyFunSpec with Matchers {
16+
17+
private class TestDiffGraphBuilder extends io.shiftleft.codepropertygraph.generated.DiffGraphBuilder(GraphSchema) {
18+
val nodes = scala.collection.mutable.Buffer[DNode]()
19+
override def addNode(newNode: DNode): this.type = {
20+
this.nodes.append(newNode)
21+
this
22+
}
23+
}
24+
25+
private val Xml: String =
26+
"""<?xml version="1.0" encoding="UTF-8"?>
27+
|<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
28+
|<plist version="1.0">
29+
|<dict>
30+
| <key>Example</key>
31+
| <string>value</string>
32+
|</dict>
33+
|</plist>""".stripMargin
34+
// com.dd.plist.NSObject#toXMLPropertyList uses line separator when converting
35+
.replace("\n", System.lineSeparator())
36+
// com.dd.plist.NSObject#toXMLPropertyList uses tabs when converting
37+
.replace(" ", "\t")
38+
39+
describe("ConfigFileCreationPass") {
40+
it("creates config node for an XML plist") {
41+
FileUtil.usingTemporaryFile("test", ".plist") { xmlFile =>
42+
Files.write(xmlFile, Xml.getBytes("UTF-8"))
43+
44+
val pass = new ConfigFileCreationPass(null /* cpg not used in here */, Config())
45+
val diff = new TestDiffGraphBuilder
46+
47+
pass.runOnPart(diff, xmlFile)
48+
diff.nodes.nonEmpty shouldBe true
49+
val node = diff.nodes.collectFirst { case n: NewConfigFile => n }.get
50+
node.name should (startWith("test") and endWith(".plist"))
51+
node.content shouldBe Xml
52+
}
53+
}
54+
55+
it("creates config node for a binary plist") {
56+
FileUtil.usingTemporaryFile("testbin-src", ".plist") { xmlFile =>
57+
FileUtil.usingTemporaryFile("testbin", ".plist") { binFile =>
58+
Files.write(xmlFile, Xml.getBytes("UTF-8"))
59+
PropertyListConverter.convertToBinary(xmlFile, binFile)
60+
61+
val pass = new ConfigFileCreationPass(null /* cpg not used in here */, Config())
62+
val diff = new TestDiffGraphBuilder
63+
64+
pass.runOnPart(diff, binFile)
65+
66+
diff.nodes.nonEmpty shouldBe true
67+
val node = diff.nodes.collectFirst { case n: NewConfigFile => n }.get
68+
node.name should (startWith("testbin") and endWith(".plist"))
69+
// binary should have been converted to XML content:
70+
node.content should startWith(Xml)
71+
// and the content should have a comment referencing the original file at the very end
72+
node.content.linesIterator.toList.last should startWith("<!--This file was generated from")
73+
}
74+
}
75+
}
76+
}
77+
}

joern-cli/frontends/x2cpg/src/main/scala/io/joern/x2cpg/passes/frontend/XConfigFileCreationPass.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ abstract class XConfigFileCreationPass(cpg: Cpg, private val rootDir: Option[Str
5454
}
5555
}
5656

57-
private def configFileName(configFile: Path): String = {
57+
protected def configFileName(configFile: Path): String = {
5858
Try(Paths.get(rootDir.getOrElse(cpg.metaData.root.head)).toAbsolutePath)
5959
.map(_.relativize(configFile.toAbsolutePath).toString)
6060
.getOrElse(configFile.fileName)

0 commit comments

Comments
 (0)