|
| 1 | +package dev.dirs.impl; |
| 2 | + |
| 3 | +import dev.dirs.Constants; |
| 4 | + |
| 5 | +import java.lang.foreign.AddressLayout; |
| 6 | +import java.lang.foreign.Arena; |
| 7 | +import java.lang.foreign.FunctionDescriptor; |
| 8 | +import java.lang.foreign.GroupLayout; |
| 9 | +import java.lang.foreign.Linker; |
| 10 | +import java.lang.foreign.MemoryLayout; |
| 11 | +import java.lang.foreign.MemorySegment; |
| 12 | +import java.lang.foreign.SymbolLookup; |
| 13 | +import java.lang.foreign.ValueLayout; |
| 14 | +import java.lang.invoke.MethodHandle; |
| 15 | + |
| 16 | +import static dev.dirs.impl.Util.isNullOrEmpty; |
| 17 | +import static dev.dirs.impl.Util.stringLength; |
| 18 | +import static java.lang.foreign.ValueLayout.JAVA_BYTE; |
| 19 | +import static java.lang.foreign.ValueLayout.JAVA_CHAR; |
| 20 | + |
| 21 | +public final class Windows { |
| 22 | + |
| 23 | + private Windows() {} |
| 24 | + |
| 25 | + static { |
| 26 | + if (Constants.operatingSystem == 'w') { |
| 27 | + System.loadLibrary("ole32"); |
| 28 | + System.loadLibrary("shell32"); |
| 29 | + } |
| 30 | + } |
| 31 | + |
| 32 | + private static final SymbolLookup SYMBOL_LOOKUP = SymbolLookup.loaderLookup().or(Linker.nativeLinker().defaultLookup()); |
| 33 | + private static final ValueLayout.OfByte C_CHAR = ValueLayout.JAVA_BYTE; |
| 34 | + private static final ValueLayout.OfShort C_SHORT = ValueLayout.JAVA_SHORT; |
| 35 | + private static final AddressLayout C_POINTER = ValueLayout.ADDRESS |
| 36 | + .withTargetLayout(MemoryLayout.sequenceLayout(java.lang.Long.MAX_VALUE, JAVA_BYTE)); |
| 37 | + private static final ValueLayout.OfInt C_LONG = ValueLayout.JAVA_INT; |
| 38 | + |
| 39 | + public static String getProfileDir() { |
| 40 | + return getDir("{5E6C858F-0E22-4760-9AFE-EA3317B67173}"); |
| 41 | + } |
| 42 | + |
| 43 | + public static String getMusicDir() { |
| 44 | + return getDir("{4BD8D571-6D19-48D3-BE97-422220080E43}"); |
| 45 | + } |
| 46 | + |
| 47 | + public static String getDesktopDir() { |
| 48 | + return getDir("{B4BFCC3A-DB2C-424C-B029-7FE99A87C641}"); |
| 49 | + } |
| 50 | + |
| 51 | + public static String getDocumentsDir() { |
| 52 | + return getDir("{FDD39AD0-238F-46AF-ADB4-6C85480369C7}"); |
| 53 | + } |
| 54 | + |
| 55 | + public static String getDownloadsDir() { |
| 56 | + return getDir("{374DE290-123F-4565-9164-39C4925E467B}"); |
| 57 | + } |
| 58 | + |
| 59 | + public static String getPicturesDir() { |
| 60 | + return getDir("{33E28130-4E1E-4676-835A-98395C3BC3BB}"); |
| 61 | + } |
| 62 | + |
| 63 | + public static String getPublicDir() { |
| 64 | + return getDir("{DFDF76A2-C82A-4D63-906A-5644AC457385}"); |
| 65 | + } |
| 66 | + |
| 67 | + public static String getTemplatesDir() { |
| 68 | + return getDir("{A63293E8-664E-48DB-A079-DF759E0509F7}"); |
| 69 | + } |
| 70 | + |
| 71 | + public static String getVideosDir() { |
| 72 | + return getDir("{18989B1D-99B5-455B-841C-AB7C74E4DDFC}"); |
| 73 | + } |
| 74 | + |
| 75 | + public static String getRoamingAppDataDir() { |
| 76 | + return getDir("{3EB685DB-65F9-4CF6-A03A-E3EF65729F3D}"); |
| 77 | + } |
| 78 | + |
| 79 | + public static String getLocalAppDataDir() { |
| 80 | + return getDir("{F1B32785-6FBA-4FCF-9D55-7B8E7F157091}"); |
| 81 | + } |
| 82 | + |
| 83 | + public static String applicationPath(String qualifier, String organization, String application) { |
| 84 | + StringBuilder buf = new StringBuilder(Math.max(stringLength(organization) + stringLength(application), 0)); |
| 85 | + boolean orgPresent = !isNullOrEmpty(organization); |
| 86 | + boolean appPresent = !isNullOrEmpty(application); |
| 87 | + if (orgPresent) { |
| 88 | + buf.append(organization); |
| 89 | + if (appPresent) |
| 90 | + buf.append('\\'); |
| 91 | + } |
| 92 | + if (appPresent) |
| 93 | + buf.append(application); |
| 94 | + return buf.toString(); |
| 95 | + } |
| 96 | + |
| 97 | + private static String getDir(String folderId) { |
| 98 | + try (var arena = Arena.ofConfined()) { |
| 99 | + MemorySegment guidSegment = arena.allocate(GUID_LAYOUT); |
| 100 | + if (CLSIDFromString(createSegmentFromString(folderId, arena), guidSegment) != 0) { |
| 101 | + throw new AssertionError("failed converting string " + folderId + " to KnownFolderId"); |
| 102 | + } |
| 103 | + MemorySegment path = arena.allocate(C_POINTER); |
| 104 | + SHGetKnownFolderPath(guidSegment, 0, MemorySegment.NULL, path); |
| 105 | + return createStringFromSegment(path.get(C_POINTER, 0)); |
| 106 | + } |
| 107 | + } |
| 108 | + |
| 109 | + /** |
| 110 | + * Creates a memory segment as a copy of a Java string. |
| 111 | + * <p> |
| 112 | + * The memory segment contains a copy of the string (null-terminated, UTF-16/wide characters). |
| 113 | + * </p> |
| 114 | + * |
| 115 | + * @param str the string to copy |
| 116 | + * @param arena the arena for the memory segment |
| 117 | + * @return the resulting memory segment |
| 118 | + */ |
| 119 | + private static MemorySegment createSegmentFromString(String str, Arena arena) { |
| 120 | + // allocate segment (including space for terminating null) |
| 121 | + var segment = arena.allocate(JAVA_CHAR, str.length() + 1L); |
| 122 | + // copy characters |
| 123 | + segment.copyFrom(MemorySegment.ofArray(str.toCharArray())); |
| 124 | + return segment; |
| 125 | + } |
| 126 | + |
| 127 | + /** |
| 128 | + * Creates a copy of the string in the memory segment. |
| 129 | + * <p> |
| 130 | + * The string must be a null-terminated UTF-16 (wide character) string. |
| 131 | + * </p> |
| 132 | + * |
| 133 | + * @param segment the memory segment |
| 134 | + * @return copied string |
| 135 | + */ |
| 136 | + private static String createStringFromSegment(MemorySegment segment) { |
| 137 | + var len = 0; |
| 138 | + while (segment.get(JAVA_CHAR, len) != 0) { |
| 139 | + len += 2; |
| 140 | + } |
| 141 | + |
| 142 | + return new String(segment.asSlice(0, len).toArray(JAVA_CHAR)); |
| 143 | + } |
| 144 | + |
| 145 | + private static MemorySegment findOrThrow(String symbol) { |
| 146 | + return SYMBOL_LOOKUP.find(symbol) |
| 147 | + .orElseThrow(() -> new UnsatisfiedLinkError("unresolved symbol: " + symbol)); |
| 148 | + } |
| 149 | + |
| 150 | + private static final GroupLayout GUID_LAYOUT = MemoryLayout.structLayout( |
| 151 | + C_LONG.withName("Data1"), |
| 152 | + C_SHORT.withName("Data2"), |
| 153 | + C_SHORT.withName("Data3"), |
| 154 | + MemoryLayout.sequenceLayout(8, C_CHAR).withName("Data4")) |
| 155 | + .withName("_GUID"); |
| 156 | + |
| 157 | + /** |
| 158 | + * {@snippet lang=c : |
| 159 | + * extern HRESULT CLSIDFromString(LPCOLESTR lpsz, LPCLSID pclsid) |
| 160 | + * } |
| 161 | + */ |
| 162 | + private static int CLSIDFromString(MemorySegment lpsz, MemorySegment pclsid) { |
| 163 | + var handle = CLSIDFromString.HANDLE; |
| 164 | + try { |
| 165 | + return (int) handle.invokeExact(lpsz, pclsid); |
| 166 | + } catch (Throwable throwable) { |
| 167 | + throw new AssertionError("failed to invoke `CLSIDFromString`", throwable); |
| 168 | + } |
| 169 | + } |
| 170 | + |
| 171 | + private static class CLSIDFromString { |
| 172 | + public static final FunctionDescriptor DESC = FunctionDescriptor.of(C_LONG, C_POINTER, C_POINTER); |
| 173 | + |
| 174 | + public static final MethodHandle HANDLE = Linker.nativeLinker() |
| 175 | + .downcallHandle(findOrThrow("CLSIDFromString"), DESC); |
| 176 | + } |
| 177 | + |
| 178 | + /** |
| 179 | + * {@snippet lang=c : |
| 180 | + * extern HRESULT SHGetKnownFolderPath(const KNOWNFOLDERID *const rfid, DWORD dwFlags, HANDLE hToken, PWSTR *ppszPath) |
| 181 | + * } |
| 182 | + */ |
| 183 | + private static int SHGetKnownFolderPath(MemorySegment rfid, int dwFlags, MemorySegment hToken, MemorySegment ppszPath) { |
| 184 | + var handle = SHGetKnownFolderPath.HANDLE; |
| 185 | + try { |
| 186 | + return (int) handle.invokeExact(rfid, dwFlags, hToken, ppszPath); |
| 187 | + } catch (Throwable throwable) { |
| 188 | + throw new AssertionError("failed to invoke `SHGetKnownFolderPath`", throwable); |
| 189 | + } |
| 190 | + } |
| 191 | + |
| 192 | + private static class SHGetKnownFolderPath { |
| 193 | + public static final FunctionDescriptor DESC = FunctionDescriptor.of(C_LONG, C_POINTER, C_LONG, C_POINTER, C_POINTER); |
| 194 | + |
| 195 | + public static final MethodHandle HANDLE = Linker.nativeLinker() |
| 196 | + .downcallHandle(findOrThrow("SHGetKnownFolderPath"), DESC); |
| 197 | + } |
| 198 | + |
| 199 | +} |
0 commit comments