Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 53 additions & 12 deletions app/src/main/java/com/winlator/core/envvars/EnvVars.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,12 @@ public void put(String name, Object value) {

public void putAll(String values) {
if (values == null || values.isEmpty()) return;
String[] parts = values.split(" ");
for (String part : parts) {
for (String part : splitOnUnescapedSpaces(values)) {
int index = part.indexOf("=");
String name = part.substring(0, index);
String value = part.substring(index+1);
// tolerate stray tokens (legacy data corrupted by old unescaped serializer)
if (index < 0) continue;
String name = unescape(part.substring(0, index));
String value = unescape(part.substring(index + 1));
data.put(name, value);
}
}
Expand Down Expand Up @@ -53,22 +54,24 @@ public boolean isEmpty() {
return data.isEmpty();
}

// canonical persistence form: escape so putAll round-trips losslessly
@NonNull
@Override
public String toString() {
return String.join(" ", toStringArray());
StringBuilder sb = new StringBuilder();
for (String key : data.keySet()) {
if (sb.length() > 0) sb.append(' ');
sb.append(escape(key)).append('=').append(escape(data.get(key)));
}
return sb.toString();
}

// for shell composition (env KEY=val ... cmd) — same escape rules
public String toEscapedString() {
String result = "";
for (String key : data.keySet()) {
if (!result.isEmpty()) result += " ";
String value = data.get(key);
result += key+"="+value.replace(" ", "\\ ");
}
return result;
return toString();
}

// for execve envp — values must be raw, no escaping
public String[] toStringArray() {
String[] stringArray = new String[data.size()];
int index = 0;
Expand All @@ -81,4 +84,42 @@ public String[] toStringArray() {
public Iterator<String> iterator() {
return data.keySet().iterator();
}

private static String escape(String s) {
// escape backslash FIRST so we don't double-escape the slashes we add for spaces
return s.replace("\\", "\\\\").replace(" ", "\\ ");
}

private static String unescape(String s) {
StringBuilder sb = new StringBuilder(s.length());
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c == '\\' && i + 1 < s.length()) {
sb.append(s.charAt(++i));
} else {
sb.append(c);
}
}
return sb.toString();
}

private static java.util.List<String> splitOnUnescapedSpaces(String s) {
java.util.List<String> out = new java.util.ArrayList<>();
StringBuilder cur = new StringBuilder();
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c == '\\' && i + 1 < s.length()) {
cur.append(c).append(s.charAt(++i));
} else if (c == ' ') {
if (cur.length() > 0) {
out.add(cur.toString());
cur.setLength(0);
}
} else {
cur.append(c);
}
}
if (cur.length() > 0) out.add(cur.toString());
return out;
}
}
99 changes: 99 additions & 0 deletions app/src/test/java/com/winlator/core/envvars/EnvVarsTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package com.winlator.core.envvars

import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test

class EnvVarsTest {

@Test
fun roundTripPlainValues() {
val src = EnvVars().apply {
put("DXVK_HUD", "1")
put("WINEDEBUG", "fixme-all")
}
val parsed = EnvVars(src.toString())
assertEquals("1", parsed.get("DXVK_HUD"))
assertEquals("fixme-all", parsed.get("WINEDEBUG"))
}

// crash repro: spaces in value used to throw StringIndexOutOfBoundsException on re-parse
@Test
fun roundTripValueWithSpaces() {
val src = EnvVars().apply {
put("DXVK_CONFIG", "d3d9.presentInterval = 1;")
put("WINEDEBUG", "fixme-all")
}
val parsed = EnvVars(src.toString())
assertEquals("d3d9.presentInterval = 1;", parsed.get("DXVK_CONFIG"))
assertEquals("fixme-all", parsed.get("WINEDEBUG"))
}

@Test
fun roundTripValueWithMultipleSpacesEqualsAndSemicolons() {
// multi-clause DXVK config: spaces, '=', ';' all inside one value
val src = EnvVars().apply {
put("DXVK_CONFIG", "d3d9.presentInterval = 1; d3d9.somethingElse = 2")
}
val parsed = EnvVars(src.toString())
assertEquals("d3d9.presentInterval = 1; d3d9.somethingElse = 2", parsed.get("DXVK_CONFIG"))
}

@Test
fun roundTripValueWithSemicolonOnly() {
val src = EnvVars().apply { put("DXVK_CONFIG", "d3d9.presentInterval=1;") }
val parsed = EnvVars(src.toString())
assertEquals("d3d9.presentInterval=1;", parsed.get("DXVK_CONFIG"))
}

@Test
fun roundTripValueWithBackslash() {
val src = EnvVars().apply { put("WINEPATH", "C:\\Program Files\\foo") }
val parsed = EnvVars(src.toString())
assertEquals("C:\\Program Files\\foo", parsed.get("WINEPATH"))
}

@Test
fun roundTripValueWithEscapedSpaceLiteral() {
// value already contains a backslash followed by a space — must survive round-trip
val src = EnvVars().apply { put("FOO", "a\\ b") }
val parsed = EnvVars(src.toString())
assertEquals("a\\ b", parsed.get("FOO"))
}

@Test
fun roundTripEmptyValue() {
val src = EnvVars().apply { put("EMPTY", "") }
val parsed = EnvVars(src.toString())
assertEquals("", parsed.get("EMPTY"))
assertTrue(parsed.has("EMPTY"))
}

@Test
fun toStringArrayReturnsRawValuesForExecve() {
// execve envp must NOT contain backslash escapes — it's the actual env vector
val src = EnvVars().apply { put("DXVK_CONFIG", "d3d9.presentInterval = 1;") }
assertArrayEquals(arrayOf("DXVK_CONFIG=d3d9.presentInterval = 1;"), src.toStringArray())
}

@Test
fun emptyAndNullParseSafely() {
assertTrue(EnvVars("").isEmpty)
assertTrue(EnvVars(null).isEmpty)
}

@Test
fun multipleVarsPreserveOrderAndSpaces() {
val src = EnvVars().apply {
put("A", "with space")
put("B", "no_space")
put("C", "another with spaces")
}
val parsed = EnvVars(src.toString())
assertEquals(listOf("A", "B", "C"), parsed.toList())
assertEquals("with space", parsed.get("A"))
assertEquals("no_space", parsed.get("B"))
assertEquals("another with spaces", parsed.get("C"))
}
}
Loading