Repository: joernio/joern (scala)

Issue:
[Bug] changeExtensionTo does not account for multiple dots in file name in jssrc
The `changeExtensionTo` method (part of better.files.File) splits file names on the first occurrence of a dot and hence will incorrectly rename any file with multiple extensions, resulting in a `java.io.FileNotFoundException` when trying to read the file. 

**To Reproduce**
Steps to reproduce the behavior:
1. `importCode` the attached example in joern
[example.zip](https://github.com/joernio/joern/files/11353008/example.zip)

2. Observe the following error:
```
java.io.FileNotFoundException: /home/pr0me/Documents/code/phd/xample/script.ejs (No such file or directory)
	at java.base/java.io.FileInputStream.open0(Native Method)
	at java.base/java.io.FileInputStream.open(FileInputStream.java:219)
	at java.base/java.io.FileInputStream.<init>(FileInputStream.java:157)
	at scala.io.Source$.fromFile(Source.scala:94)
	at scala.io.Source$.fromFile(Source.scala:79)
	at io.shiftleft.utils.IOUtils$.readLinesInFile(IOUtils.scala:77)
	at io.joern.jssrc2cpg.parser.BabelJsonParser$.readFile(BabelJsonParser.scala:35)
	at io.joern.jssrc2cpg.passes.AstCreationPass.$anonfun$runOnPart$1(AstCreationPass.scala:48)
```
The original file name is `script.js.ejs`.

**Expected behavior**
File renaming should account for the possibility of multiple file extensions.

**Desktop (please complete the following information):**
 - OS: Linux pop-os 6.0.12-76060006-generic 22.04
 - Joern Version: 1.1.1646
 - javac 11.0.17

**Additional context**
I confirmed that the error is due to `changeExtensionTo`.
This 
```scala
val test_file_0 = File("./xx.js.md")
val altered_0 = test_file_0.changeExtensionTo(".js")
logger.error(s"TEST 0: ${test_file_0} -> ${altered_0}")
val test_file_1 = File("./xx.md.md")
val altered_1 = test_file_0.changeExtensionTo(".js")
logger.error(s"TEST 1: ${test_file_1} -> ${altered_1}")
```
will result in
```
TEST 0: ./xx.js.md -> ./xx.js
TEST 1: ./xx.md.md -> ./xx.js
```
meaning that is also not only due to the extension and the replacement being the same.
From what I saw, `changeExtensionTo` is only used once in joern but I still was not sure what the best place to fix this would be.

Pull request description:
Fixes: https://github.com/joernio/joern/issues/2614

Patch:
diff --git a/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/utils/AstGenRunner.scala b/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/utils/AstGenRunner.scala
index 30b1babe9afc..e12ddcebf482 100644
--- a/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/utils/AstGenRunner.scala
+++ b/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/utils/AstGenRunner.scala
@@ -203,6 +203,14 @@ class AstGenRunner(config: Config) {
     }
   }
 
+  /** Changes the file-extension by renaming this file; if file does not have an extension, it adds the extension. If
+    * file does not exist (or is a directory) no change is done and the current file is returned.
+    */
+  private def changeExtensionTo(file: File, extension: String): File = {
+    val newName = s"${file.nameWithoutExtension(includeAll = false)}.${extension.stripPrefix(".")}"
+    if (file.isRegularFile) file.renameTo(newName) else if (file.notExists) File(newName) else file
+  }
+
   private def processEjsFiles(in: File, out: File, ejsFiles: List[String]): Try[Seq[String]] = {
     val tmpJsFiles = ejsFiles.map { ejsFilePath =>
       val ejsFile             = File(ejsFilePath)
@@ -210,12 +218,11 @@ class AstGenRunner(config: Config) {
       if (isTranspiledFile(maybeTranspiledFile.pathAsString)) {
         maybeTranspiledFile
       } else {
-        val ls                = SourceFiles.retrieveLineSeparator(ejsFilePath)
-        val sourceFileContent = IOUtils.readLinesInFile(ejsFile.path).mkString("", ls, ls)
+        val sourceFileContent = IOUtils.readEntireFile(ejsFile.path)
         val preprocessContent = new EjsPreprocessor().preprocess(sourceFileContent)
         (out / in.relativize(ejsFile).toString).parent.createDirectoryIfNotExists(createParents = true)
         val newEjsFile = ejsFile.copyTo(out / in.relativize(ejsFile).toString)
-        val jsFile     = newEjsFile.changeExtensionTo(".js").writeText(preprocessContent)
+        val jsFile     = changeExtensionTo(newEjsFile, ".js").writeText(preprocessContent)
         newEjsFile.createFile().writeText(sourceFileContent)
         jsFile
       }
@@ -229,7 +236,7 @@ class AstGenRunner(config: Config) {
       val jsonContent = IOUtils.readLinesInFile(jsonFile.path).mkString
       val json        = ujson.read(jsonContent)
       val fileName    = json("fullName").str
-      val newFileName = fileName.replace(".js", ".ejs")
+      val newFileName = fileName.patch(fileName.lastIndexOf(".js"), ".ejs", 3)
       json("relativeName") = newFileName
       json("fullName") = newFileName
       jsonFile.writeText(json.toString())


Test patch:
diff --git a/joern-cli/frontends/jssrc2cpg/src/test/scala/io/joern/jssrc2cpg/preprocessing/EjsPassTest.scala b/joern-cli/frontends/jssrc2cpg/src/test/scala/io/joern/jssrc2cpg/preprocessing/EjsPassTest.scala
index 96a00e5f6592..1e55706db69e 100644
--- a/joern-cli/frontends/jssrc2cpg/src/test/scala/io/joern/jssrc2cpg/preprocessing/EjsPassTest.scala
+++ b/joern-cli/frontends/jssrc2cpg/src/test/scala/io/joern/jssrc2cpg/preprocessing/EjsPassTest.scala
@@ -6,6 +6,19 @@ import io.shiftleft.semanticcpg.language._
 class EjsPassTest extends AbstractPassTest {
 
   "ejs files" should {
+
+    "be renamed correctly " in AstFixture(
+      """
+        |<body>
+        |<h1>Welcome <%= user.name %></h1>
+        |</body>
+        |""".stripMargin,
+      "index.js.ejs"
+    ) { cpg =>
+      cpg.file.name.l shouldBe List("index.js.ejs")
+      cpg.call.code.l.sorted shouldBe List("user.name")
+    }
+
     "be handled correctly" in AstFixture(
       """
         |<body>