Skip to content

Commit 839600b

Browse files
committed
Extract parsing command line file to a separate class + add unit tests
1 parent 29b3d9e commit 839600b

File tree

4 files changed

+201
-38
lines changed

4 files changed

+201
-38
lines changed

platform/android/java/lib/build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ apply from: "../scripts/publish-module.gradle"
1111

1212
dependencies {
1313
implementation "androidx.fragment:fragment:$versions.fragmentVersion"
14+
15+
testImplementation "junit:junit:4.13.2"
1416
}
1517

1618
def pathToRootDir = "../../../../"
@@ -74,6 +76,7 @@ android {
7476
main {
7577
manifest.srcFile 'AndroidManifest.xml'
7678
java.srcDirs = ['src']
79+
test.java.srcDirs = ['srcTest/java']
7780
res.srcDirs = ['res']
7881
aidl.srcDirs = ['aidl']
7982
assets.srcDirs = ['assets']

platform/android/java/lib/src/org/godotengine/godot/Godot.kt

Lines changed: 11 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import org.godotengine.godot.io.directory.DirectoryAccessHandler
5656
import org.godotengine.godot.io.file.FileAccessHandler
5757
import org.godotengine.godot.plugin.GodotPluginRegistry
5858
import org.godotengine.godot.tts.GodotTTS
59+
import org.godotengine.godot.utils.CommandLineFileParser
5960
import org.godotengine.godot.utils.GodotNetUtils
6061
import org.godotengine.godot.utils.PermissionsUtil
6162
import org.godotengine.godot.utils.PermissionsUtil.requestPermission
@@ -68,7 +69,7 @@ import org.godotengine.godot.xr.XRMode
6869
import java.io.File
6970
import java.io.FileInputStream
7071
import java.io.InputStream
71-
import java.nio.charset.StandardCharsets
72+
import java.lang.Exception
7273
import java.security.MessageDigest
7374
import java.util.*
7475

@@ -120,6 +121,7 @@ class Godot(private val context: Context) : SensorEventListener {
120121
val directoryAccessHandler = DirectoryAccessHandler(context)
121122
val fileAccessHandler = FileAccessHandler(context)
122123
val netUtils = GodotNetUtils(context)
124+
private val commandLineFileParser = CommandLineFileParser()
123125

124126
/**
125127
* Tracks whether [onCreate] was completed successfully.
@@ -908,47 +910,18 @@ class Godot(private val context: Context) : SensorEventListener {
908910
}
909911

910912
private fun getCommandLine(): MutableList<String> {
911-
val original: MutableList<String> = parseCommandLine()
913+
val commandLine = try {
914+
commandLineFileParser.parseCommandLine(requireActivity().assets.open("_cl_"))
915+
} catch (ignored: Exception) {
916+
mutableListOf()
917+
}
918+
912919
val hostCommandLine = primaryHost?.commandLine
913920
if (!hostCommandLine.isNullOrEmpty()) {
914-
original.addAll(hostCommandLine)
921+
commandLine.addAll(hostCommandLine)
915922
}
916-
return original
917-
}
918923

919-
private fun parseCommandLine(): MutableList<String> {
920-
val inputStream: InputStream
921-
return try {
922-
inputStream = requireActivity().assets.open("_cl_")
923-
val len = ByteArray(4)
924-
var r = inputStream.read(len)
925-
if (r < 4) {
926-
return mutableListOf()
927-
}
928-
val argc =
929-
(len[3].toInt() and 0xFF) shl 24 or ((len[2].toInt() and 0xFF) shl 16) or ((len[1].toInt() and 0xFF) shl 8) or (len[0].toInt() and 0xFF)
930-
val cmdline = ArrayList<String>(argc)
931-
for (i in 0 until argc) {
932-
r = inputStream.read(len)
933-
if (r < 4) {
934-
return mutableListOf()
935-
}
936-
val strlen =
937-
(len[3].toInt() and 0xFF) shl 24 or ((len[2].toInt() and 0xFF) shl 16) or ((len[1].toInt() and 0xFF) shl 8) or (len[0].toInt() and 0xFF)
938-
if (strlen > 65535) {
939-
return mutableListOf()
940-
}
941-
val arg = ByteArray(strlen)
942-
r = inputStream.read(arg)
943-
if (r == strlen) {
944-
cmdline.add(String(arg, StandardCharsets.UTF_8))
945-
}
946-
}
947-
cmdline
948-
} catch (e: Exception) {
949-
// The _cl_ file can be missing with no adverse effect
950-
mutableListOf()
951-
}
924+
return commandLine
952925
}
953926

954927
/**
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/**************************************************************************/
2+
/* CommandLineFileParser.kt */
3+
/**************************************************************************/
4+
/* This file is part of: */
5+
/* GODOT ENGINE */
6+
/* https://godotengine.org */
7+
/**************************************************************************/
8+
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
9+
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
10+
/* */
11+
/* Permission is hereby granted, free of charge, to any person obtaining */
12+
/* a copy of this software and associated documentation files (the */
13+
/* "Software"), to deal in the Software without restriction, including */
14+
/* without limitation the rights to use, copy, modify, merge, publish, */
15+
/* distribute, sublicense, and/or sell copies of the Software, and to */
16+
/* permit persons to whom the Software is furnished to do so, subject to */
17+
/* the following conditions: */
18+
/* */
19+
/* The above copyright notice and this permission notice shall be */
20+
/* included in all copies or substantial portions of the Software. */
21+
/* */
22+
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
23+
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
24+
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
25+
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
26+
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
27+
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
28+
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
29+
/**************************************************************************/
30+
31+
package org.godotengine.godot.utils
32+
33+
import java.io.InputStream
34+
import java.nio.charset.StandardCharsets
35+
import java.util.ArrayList
36+
37+
/**
38+
* A class that parses the content of file storing command line params. Usually, this file is saved
39+
* in `assets/_cl_` on exporting an apk
40+
*
41+
* Returns a mutable list of command lines
42+
*/
43+
internal class CommandLineFileParser {
44+
fun parseCommandLine(inputStream: InputStream): MutableList<String> {
45+
return try {
46+
val headerBytes = ByteArray(4)
47+
var argBytes = inputStream.read(headerBytes)
48+
if (argBytes < 4) {
49+
return mutableListOf()
50+
}
51+
val argc = decodeHeaderIntValue(headerBytes)
52+
53+
val cmdline = ArrayList<String>(argc)
54+
for (i in 0 until argc) {
55+
argBytes = inputStream.read(headerBytes)
56+
if (argBytes < 4) {
57+
return mutableListOf()
58+
}
59+
val strlen = decodeHeaderIntValue(headerBytes)
60+
61+
if (strlen > 65535) {
62+
return mutableListOf()
63+
}
64+
65+
val arg = ByteArray(strlen)
66+
argBytes = inputStream.read(arg)
67+
if (argBytes == strlen) {
68+
cmdline.add(String(arg, StandardCharsets.UTF_8))
69+
}
70+
}
71+
cmdline
72+
} catch (e: Exception) {
73+
// The _cl_ file can be missing with no adverse effect
74+
mutableListOf()
75+
}
76+
}
77+
78+
private fun decodeHeaderIntValue(headerBytes: ByteArray): Int =
79+
(headerBytes[3].toInt() and 0xFF) shl 24 or
80+
((headerBytes[2].toInt() and 0xFF) shl 16) or
81+
((headerBytes[1].toInt() and 0xFF) shl 8) or
82+
(headerBytes[0].toInt() and 0xFF)
83+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/**************************************************************************/
2+
/* CommandLineFileParserTest.kt */
3+
/**************************************************************************/
4+
/* This file is part of: */
5+
/* GODOT ENGINE */
6+
/* https://godotengine.org */
7+
/**************************************************************************/
8+
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
9+
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
10+
/* */
11+
/* Permission is hereby granted, free of charge, to any person obtaining */
12+
/* a copy of this software and associated documentation files (the */
13+
/* "Software"), to deal in the Software without restriction, including */
14+
/* without limitation the rights to use, copy, modify, merge, publish, */
15+
/* distribute, sublicense, and/or sell copies of the Software, and to */
16+
/* permit persons to whom the Software is furnished to do so, subject to */
17+
/* the following conditions: */
18+
/* */
19+
/* The above copyright notice and this permission notice shall be */
20+
/* included in all copies or substantial portions of the Software. */
21+
/* */
22+
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
23+
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
24+
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
25+
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
26+
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
27+
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
28+
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
29+
/**************************************************************************/
30+
31+
package org.godotengine.godot.utils
32+
33+
import org.junit.Test
34+
import org.junit.runner.RunWith
35+
import org.junit.runners.Parameterized
36+
import java.io.ByteArrayInputStream
37+
import java.io.InputStream
38+
39+
// Godot saves command line params in the `assets/_cl_` file on exporting an apk. By default,
40+
// without any other commands specified in `command_line/extra_args` in Export window, the content
41+
// of that _cl_ file consists of only the `--xr_mode_regular` and `--use_immersive` flags.
42+
// The `CL_` prefix here refers to that file
43+
private val CL_DEFAULT_NO_EXTRA_ARGS = byteArrayOf(2, 0, 0, 0, 17, 0, 0, 0, 45, 45, 120, 114, 95, 109, 111, 100, 101, 95, 114, 101, 103, 117, 108, 97, 114, 15, 0, 0, 0, 45, 45, 117, 115, 101, 95, 105, 109, 109, 101, 114, 115, 105, 118, 101)
44+
private val CL_ONE_EXTRA_ARG = byteArrayOf(3, 0, 0, 0, 15, 0, 0, 0, 45, 45, 117, 110, 105, 116, 95, 116, 101, 115, 116, 95, 97, 114, 103, 17, 0, 0, 0, 45, 45, 120, 114, 95, 109, 111, 100, 101, 95, 114, 101, 103, 117, 108, 97, 114, 15, 0, 0, 0, 45, 45, 117, 115, 101, 95, 105, 109, 109, 101, 114, 115, 105, 118, 101)
45+
private val CL_TWO_EXTRA_ARGS = byteArrayOf(4, 0, 0, 0, 16, 0, 0, 0, 45, 45, 117, 110, 105, 116, 95, 116, 101, 115, 116, 95, 97, 114, 103, 49, 16, 0, 0, 0, 45, 45, 117, 110, 105, 116, 95, 116, 101, 115, 116, 95, 97, 114, 103, 50, 17, 0, 0, 0, 45, 45, 120, 114, 95, 109, 111, 100, 101, 95, 114, 101, 103, 117, 108, 97, 114, 15, 0, 0, 0, 45, 45, 117, 115, 101, 95, 105, 109, 109, 101, 114, 115, 105, 118, 101)
46+
private val CL_EMPTY = byteArrayOf()
47+
private val CL_HEADER_TOO_SHORT = byteArrayOf(0, 0, 0)
48+
private val CL_INCOMPLETE_FIRST_ARG = byteArrayOf(2, 0, 0, 0, 17, 0, 0)
49+
private val CL_LENGTH_TOO_LONG_IN_FIRST_ARG = byteArrayOf(2, 0, 0, 0, 17, 0, 0, 45, 45, 120, 114, 95, 109, 111, 100, 101, 95, 114, 101, 103, 117, 108, 97, 114, 15, 0, 0, 0, 45, 45, 117, 115, 101, 95, 105, 109, 109, 101, 114, 115, 105, 118, 101)
50+
private val CL_MISMATCHED_ARG_LENGTH_AND_HEADER_ONE_ARG = byteArrayOf(2, 0, 0, 0, 10, 0, 0, 0, 45, 45, 120, 114)
51+
private val CL_MISMATCHED_ARG_LENGTH_AND_HEADER_IN_FIRST_ARG = byteArrayOf(2, 0, 0, 0, 17, 0, 0, 0, 45, 45, 120, 114, 95, 109, 111, 100, 101, 95, 114, 101, 103, 117, 108, 97, 15, 0, 0, 0, 45, 45, 117, 115, 101, 95, 105, 109, 109, 101, 114, 115, 105, 118, 101)
52+
53+
@RunWith(Parameterized::class)
54+
class CommandLineFileParserTest(
55+
private val inputStreamArg: InputStream,
56+
private val expectedResult: List<String>,
57+
) {
58+
59+
private val commandLineFileParser = CommandLineFileParser()
60+
61+
companion object {
62+
@JvmStatic
63+
@Parameterized.Parameters
64+
fun data() = listOf(
65+
arrayOf(ByteArrayInputStream(CL_EMPTY), listOf<String>()),
66+
arrayOf(ByteArrayInputStream(CL_HEADER_TOO_SHORT), listOf<String>()),
67+
68+
arrayOf(ByteArrayInputStream(CL_DEFAULT_NO_EXTRA_ARGS), listOf(
69+
"--xr_mode_regular",
70+
"--use_immersive",
71+
)),
72+
73+
arrayOf(ByteArrayInputStream(CL_ONE_EXTRA_ARG), listOf(
74+
"--unit_test_arg",
75+
"--xr_mode_regular",
76+
"--use_immersive",
77+
)),
78+
79+
arrayOf(ByteArrayInputStream(CL_TWO_EXTRA_ARGS), listOf(
80+
"--unit_test_arg1",
81+
"--unit_test_arg2",
82+
"--xr_mode_regular",
83+
"--use_immersive",
84+
)),
85+
86+
arrayOf(ByteArrayInputStream(CL_INCOMPLETE_FIRST_ARG), listOf<String>()),
87+
arrayOf(ByteArrayInputStream(CL_LENGTH_TOO_LONG_IN_FIRST_ARG), listOf<String>()),
88+
arrayOf(ByteArrayInputStream(CL_MISMATCHED_ARG_LENGTH_AND_HEADER_ONE_ARG), listOf<String>()),
89+
arrayOf(ByteArrayInputStream(CL_MISMATCHED_ARG_LENGTH_AND_HEADER_IN_FIRST_ARG), listOf<String>()),
90+
)
91+
}
92+
93+
@Test
94+
fun `Given inputStream, When parsing command line, Then a correct list is returned`() {
95+
// given
96+
val inputStream = inputStreamArg
97+
98+
// when
99+
val result = commandLineFileParser.parseCommandLine(inputStream)
100+
101+
// then
102+
assert(result == expectedResult) { "Expected: $expectedResult Actual: $result" }
103+
}
104+
}

0 commit comments

Comments
 (0)