Skip to content
This repository was archived by the owner on Feb 18, 2025. It is now read-only.

Commit 0f6d161

Browse files
committed
Avoid escaping issues by Base64-encoding command to Powershell
1 parent 9f3c18c commit 0f6d161

File tree

2 files changed

+66
-45
lines changed

2 files changed

+66
-45
lines changed

src/main/java/io/github/soc/directories/Util.java

Lines changed: 57 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import java.io.BufferedReader;
44
import java.io.IOException;
55
import java.io.InputStreamReader;
6+
import java.lang.reflect.Method;
67
import java.nio.charset.Charset;
78
import java.util.Locale;
89

@@ -12,6 +13,7 @@ private Util() {
1213
throw new Error();
1314
}
1415

16+
1517
static final String operatingSystemName = System.getProperty("os.name");
1618
static final char operatingSystem;
1719
static final char LIN = 'l';
@@ -36,6 +38,29 @@ else if (os.contains("sunos"))
3638
throw new UnsupportedOperatingSystemException("directories are not supported on " + operatingSystemName);
3739
}
3840

41+
private static Object base64Encoder = null;
42+
private static Method base64EncodeMethod = null;
43+
// This string needs to end up being a multiple of 3 bytes after conversion to UTF-16. (It is currently 1200 bytes.)
44+
// This is because Base64 converts 3 bytes to 4 letters; other numbers of bytes would introduce padding, which
45+
// would make it harder to simply concatenate this precomputed string with whatever directories the user requests.
46+
static final String SCRIPT_START_BASE64 = operatingSystem == 'w' ? toUTF16LEBase64("& {\n" +
47+
"[Console]::OutputEncoding = [System.Text.Encoding]::UTF8\n" +
48+
"Add-Type @\"\n" +
49+
"using System;\n" +
50+
"using System.Runtime.InteropServices;\n" +
51+
"public class Dir {\n" +
52+
" [DllImport(\"shell32.dll\")]\n" +
53+
" private static extern int SHGetKnownFolderPath([MarshalAs(UnmanagedType.LPStruct)] Guid rfid, uint dwFlags, IntPtr hToken, out IntPtr pszPath);\n" +
54+
" public static string GetKnownFolderPath(string rfid) {\n" +
55+
" IntPtr pszPath;\n" +
56+
" if (SHGetKnownFolderPath(new Guid(rfid), 0, IntPtr.Zero, out pszPath) != 0) return \"\";\n" +
57+
" string path = Marshal.PtrToStringUni(pszPath);\n" +
58+
" Marshal.FreeCoTaskMem(pszPath);\n" +
59+
" return path;\n" +
60+
" }\n" +
61+
"}\n" +
62+
"\"@\n") : null;
63+
3964
static void requireNonNull(Object value) {
4065
if (value == null)
4166
throw new NullPointerException();
@@ -112,63 +137,50 @@ static String[] getXDGUserDirs(String... dirs) {
112137

113138
static String[] getWinDirs(String... guids) {
114139

115-
// See https://www.oracle.com/technetwork/java/javase/8u231-relnotes-5592812.html#JDK-8221858
116-
// Vague attempt at replicating the logic followed by ProcessBuilder, just so that the first
117-
// attempt succeeds and only one command needs to be run.
118-
final String prop = System.getProperty("jdk.lang.Process.allowAmbiguousCommands");
119-
final boolean doubleEscapeQuotes = prop == null ? System.getSecurityManager() == null : !"false".equalsIgnoreCase(prop);
120-
final String[] initialResult = getWinDirs(doubleEscapeQuotes, guids);
121-
122-
for (String s : initialResult) {
123-
if (s != null)
124-
return initialResult;
125-
}
126-
127-
// First attempt failed, let's try to escape differently.
128-
return getWinDirs(!doubleEscapeQuotes, guids);
129-
}
130-
131-
static String[] getWinDirs(boolean doubleEscapeQuotes, String... guids) {
132-
133-
// Deal with legacy or safe handling of quotes by the JDK.
134-
// Safe handling may be enabled for JDKs >= 1.8.0_231, under some conditions.
135-
// See https://www.oracle.com/technetwork/java/javase/8u231-relnotes-5592812.html#JDK-8221858
136-
// or https://github.com/AdoptOpenJDK/openjdk-jdk8u/commit/048eb42afa11ac217dcdb690d5b266fcb910771f
137-
final String q = doubleEscapeQuotes ? "\\\"" : "\"";
138-
139140
int guidsLength = guids.length;
140141
StringBuilder buf = new StringBuilder(guidsLength * 68);
141142
for (int i = 0; i < guidsLength; i++) {
142-
buf.append("[Dir]::GetKnownFolderPath(" + q);
143+
buf.append("[Dir]::GetKnownFolderPath(\"");
143144
buf.append(guids[i]);
144-
buf.append(q + ")\n");
145+
buf.append("\")\n");
145146
}
146147

148+
String encodedCommand = SCRIPT_START_BASE64 + toUTF16LEBase64(buf.toString() + "}");
149+
147150
return runCommands(guidsLength, Charset.forName("UTF-8"),
148151
"powershell.exe",
149152
"-Command",
150-
"& {\n" +
151-
"[Console]::OutputEncoding = [System.Text.Encoding]::UTF8\n" +
152-
"Add-Type @" + q + "\n" +
153-
"using System;\n" +
154-
"using System.Runtime.InteropServices;\n" +
155-
"public class Dir {\n" +
156-
" [DllImport(" + q + "shell32.dll" + q + ")]\n" +
157-
" private static extern int SHGetKnownFolderPath([MarshalAs(UnmanagedType.LPStruct)] Guid rfid, uint dwFlags, IntPtr hToken, out IntPtr pszPath);\n" +
158-
" public static string GetKnownFolderPath(string rfid) {\n" +
159-
" IntPtr pszPath;\n" +
160-
" if (SHGetKnownFolderPath(new Guid(rfid), 0, IntPtr.Zero, out pszPath) != 0) return " + q + q + ";\n" +
161-
" string path = Marshal.PtrToStringUni(pszPath);\n" +
162-
" Marshal.FreeCoTaskMem(pszPath);\n" +
163-
" return path;\n" +
164-
" }\n" +
165-
"}\n" +
166-
q + "@\n" +
167-
buf.toString() +
168-
"}"
153+
"-EncodedCommand",
154+
encodedCommand
169155
);
170156
}
171157

158+
private static String toUTF16LEBase64(String script) {
159+
byte[] scriptInUtf16LEBytes = script.getBytes(Charset.forName("UTF-16LE"));
160+
if (base64EncodeMethod == null) {
161+
initBase64Encoding();
162+
}
163+
try {
164+
return (String) base64EncodeMethod.invoke(base64Encoder, scriptInUtf16LEBytes);
165+
} catch (Exception e) {
166+
throw new RuntimeException("Base64 encoding failed!", e);
167+
}
168+
}
169+
170+
private static void initBase64Encoding() {
171+
try {
172+
base64Encoder = Class.forName("java.util.Base64").getMethod("getEncoder").invoke(null);
173+
base64EncodeMethod = base64Encoder.getClass().getMethod("encodeToString", byte[].class);
174+
} catch (Exception e1) {
175+
try {
176+
base64EncodeMethod = Class.forName("sun.misc.BASE64Encoder").getMethod("encode", byte[].class);
177+
} catch (Exception e2) {
178+
throw new RuntimeException(
179+
"Could not find any viable Base64 encoder! (java.util.Base64 failed with: " + e1.getMessage() + ")", e2);
180+
}
181+
}
182+
}
183+
172184
private static String[] runCommands(int expectedResultLines, Charset charset, String... commands) {
173185
final ProcessBuilder processBuilder = new ProcessBuilder(commands);
174186
Process process;

src/test/java/io/github/soc/directories/UtilTest.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import org.junit.Test;
44

55
import static org.junit.Assert.assertEquals;
6+
import static org.junit.Assert.assertFalse;
67

78
public final class UtilTest {
89

@@ -170,4 +171,12 @@ public void testWindowsApplicationPath06() {
170171
final String actual = Util.windowsApplicationPath(inputQual, inputOrga, inputProj);
171172
assertEquals("Foo Bar\\Baz Qux", actual);
172173
}
174+
175+
@Test
176+
public void testPowershellBase64StringIsNotPadded() {
177+
if (Util.operatingSystem == 'w') {
178+
assertFalse(Util.SCRIPT_START_BASE64.endsWith("="));
179+
}
180+
}
181+
173182
}

0 commit comments

Comments
 (0)