diff --git a/CHANGES.md b/CHANGES.md index 979ef79270..2881a2ae85 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,10 @@ Next Release (4.0.1) ==================== +Features +-------- +* More win32 initialization file APIs - [@quipsy](https://github.com/quipsy). + Release 4.0 =========== diff --git a/contrib/platform/src/com/sun/jna/platform/win32/Kernel32.java b/contrib/platform/src/com/sun/jna/platform/win32/Kernel32.java index aa3bf9abb4..7cbf58ceb6 100644 --- a/contrib/platform/src/com/sun/jna/platform/win32/Kernel32.java +++ b/contrib/platform/src/com/sun/jna/platform/win32/Kernel32.java @@ -1940,4 +1940,65 @@ boolean Process32Next(HANDLE hSnapshot, *

*/ boolean WritePrivateProfileString(String lpAppName, String lpKeyName, String lpString, String lpFileName); + + /** + * Retrieves all the keys and values for the specified section of an initialization file. + * + *

+ * Each string has the following format: {@code key=string}. + *

+ *

+ * This operation is atomic; no updates to the specified initialization file are allowed while the key name and value pairs for the section are being copied + * to the buffer pointed to by the {@code lpReturnedString} parameter. + *

+ * + * @param lpAppName + * The name of the section in the initialization file. + * @param lpReturnedString + * A buffer that receives the key name and value pairs associated with the named section. The buffer is filled with one or more {@code null} + * -terminated strings; the last string is followed by a second {@code null} character. + * @param nSize + * The size of the buffer pointed to by the {@code lpReturnedString} parameter, in characters. The maximum profile section size is 32,767 + * characters. + * @param lpFileName + * The name of the initialization file. If this parameter does not contain a full path to the file, the system searches for the file in the + * Windows directory. + * @return The number of characters copied to the buffer, not including the terminating null character. If the buffer is not large enough to contain all the + * key name and value pairs associated with the named section, the return value is equal to {@code nSize} minus two. + */ + DWORD GetPrivateProfileSection(String lpAppName, char[] lpReturnedString, DWORD nSize, String lpFileName); + + /** + * Retrieves the names of all sections in an initialization file. + *

+ * This operation is atomic; no updates to the initialization file are allowed while the section names are being copied to the buffer. + *

+ * + * @param lpszReturnBuffer + * A pointer to a buffer that receives the section names associated with the named file. The buffer is filled with one or more {@code null} + * -terminated strings; the last string is followed by a second {@code null} character. + * @param nSize + * size of the buffer pointed to by the {@code lpszReturnBuffer} parameter, in characters. + * @param lpFileName + * The name of the initialization file. If this parameter is {@code NULL}, the function searches the Win.ini file. If this parameter does not + * contain a full path to the file, the system searches for the file in the Windows directory. + * @return The return value specifies the number of characters copied to the specified buffer, not including the terminating {@code null} character. If the + * buffer is not large enough to contain all the section names associated with the specified initialization file, the return value is equal to the + * size specified by {@code nSize} minus two. + */ + DWORD GetPrivateProfileSectionNames(char[] lpszReturnBuffer, DWORD nSize, String lpFileName); + + /** + * @param lpAppName + * The name of the section in which data is written. This section name is typically the name of the calling application. + * @param lpString + * The new key names and associated values that are to be written to the named section. This string is limited to 65,535 bytes. Must be filled + * with zero or many {@code null}-terminated strings of the form {@code key=value}, appended by an additional {@code null} byte to terminate the + * list. + * @param lpFileName + * The name of the initialization file. If this parameter does not contain a full path for the file, the function searches the Windows directory + * for the file. If the file does not exist and lpFileName does not contain a full path, the function creates the file in the Windows directory. + * @return If the function succeeds, the return value is nonzero. If the function fails, the return value is zero. + */ + boolean WritePrivateProfileSection(String lpAppName, String lpString, String lpFileName); } diff --git a/contrib/platform/src/com/sun/jna/platform/win32/Kernel32Util.java b/contrib/platform/src/com/sun/jna/platform/win32/Kernel32Util.java index 5f66a9114d..07df8531ff 100644 --- a/contrib/platform/src/com/sun/jna/platform/win32/Kernel32Util.java +++ b/contrib/platform/src/com/sun/jna/platform/win32/Kernel32Util.java @@ -12,6 +12,8 @@ */ package com.sun.jna.platform.win32; +import static java.util.Arrays.asList; + import java.io.File; import java.io.FileNotFoundException; import java.util.ArrayList; @@ -307,6 +309,66 @@ public static final void writePrivateProfileString(final String appName, final S throw new Win32Exception(Kernel32.INSTANCE.GetLastError()); } + /** + * Retrieves all the keys and values for the specified section of an initialization file. + * + *

+ * Each string has the following format: {@code key=string}. + *

+ *

+ * This operation is atomic; no updates to the specified initialization file are allowed while this method is executed. + *

+ * + * @param appName + * The name of the section in the initialization file. + * @param fileName + * The name of the initialization file. If this parameter does not contain a full path to the file, the system searches for the file in the + * Windows directory. + * @return The key name and value pairs associated with the named section. + */ + public static final List getPrivateProfileSection(final String appName, final String fileName) { + final char buffer[] = new char[32768]; // Maximum section size according to MSDN (http://msdn.microsoft.com/en-us/library/windows/desktop/ms724348(v=vs.85).aspx) + if (Kernel32.INSTANCE.GetPrivateProfileSection(appName, buffer, new DWORD(buffer.length), fileName).intValue() == 0) + throw new Win32Exception(Kernel32.INSTANCE.GetLastError()); + return asList(Native.toStrings(buffer)); + } + + /** + * Retrieves the names of all sections in an initialization file. + *

+ * This operation is atomic; no updates to the initialization file are allowed while this method is executed. + *

+ * + * @param fileName + * The name of the initialization file. If this parameter is {@code NULL}, the function searches the Win.ini file. If this parameter does not + * contain a full path to the file, the system searches for the file in the Windows directory. + * @return the section names associated with the named file. + */ + public static final List getPrivateProfileSectionNames(final String fileName) { + final char buffer[] = new char[65536]; // Maximum INI file size according to MSDN (http://support.microsoft.com/kb/78346) + if (Kernel32.INSTANCE.GetPrivateProfileSectionNames(buffer, new DWORD(buffer.length), fileName).intValue() == 0) + throw new Win32Exception(Kernel32.INSTANCE.GetLastError()); + return asList(Native.toStrings(buffer)); + } + + /** + * @param appName + * The name of the section in which data is written. This section name is typically the name of the calling application. + * @param strings + * The new key names and associated values that are to be written to the named section. Each entry must be of the form {@code key=value}. + * @param fileName + * The name of the initialization file. If this parameter does not contain a full path for the file, the function searches the Windows directory + * for the file. If the file does not exist and lpFileName does not contain a full path, the function creates the file in the Windows directory. + */ + public static final void writePrivateProfileSection(final String appName, final List strings, final String fileName) { + final StringBuilder buffer = new StringBuilder(); + for (final String string : strings) + buffer.append(string).append('\0'); + buffer.append('\0'); + if (!(Kernel32.INSTANCE.WritePrivateProfileSection(appName, buffer.toString(), fileName))) + throw new Win32Exception(Kernel32.INSTANCE.GetLastError()); + } + /** * Convenience method to get the processor information. Takes care of auto-growing the array. * diff --git a/contrib/platform/test/com/sun/jna/platform/win32/Kernel32Test.java b/contrib/platform/test/com/sun/jna/platform/win32/Kernel32Test.java index 2b17055d66..552a0827de 100644 --- a/contrib/platform/test/com/sun/jna/platform/win32/Kernel32Test.java +++ b/contrib/platform/test/com/sun/jna/platform/win32/Kernel32Test.java @@ -12,6 +12,13 @@ */ package com.sun.jna.platform.win32; +import static com.sun.jna.platform.win32.RegexMatcher.matches; +import static org.hamcrest.CoreMatchers.anyOf; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.hasItems; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; @@ -22,12 +29,15 @@ import java.util.ArrayList; import java.util.Calendar; import java.util.Date; +import java.util.LinkedList; import java.util.List; import java.util.Random; import java.util.TimeZone; import junit.framework.TestCase; +import org.junit.Test; + import com.sun.jna.Memory; import com.sun.jna.Native; import com.sun.jna.NativeMappedConverter; @@ -606,4 +616,96 @@ public final void testWritePrivateProfileString() throws IOException { tmp.delete(); } } + + @Test + public final void testGetPrivateProfileSection() throws IOException { + // given + final File tmp = File.createTempFile(getName(), "ini"); + tmp.deleteOnExit(); + try { + final PrintWriter writer = new PrintWriter(new BufferedWriter(new FileWriter(tmp))); + try { + writer.println("[X]"); + writer.println("A=1"); + writer.println("B=X"); + } finally { + writer.close(); + } + + // when + final char[] buffer = new char[9]; + final DWORD len = Kernel32.INSTANCE.GetPrivateProfileSection("X", buffer, new DWORD(buffer.length), tmp.getCanonicalPath()); + + // then + assertThat("Wrong length", len, is(new DWORD(7))); + assertThat("Wrong content", buffer, is(anyOf(equalTo("A=1\0B=X\0\0".toCharArray()), equalTo("B=X\0A=1\0\0".toCharArray())))); + } finally { + tmp.delete(); + } + } + + @Test + public final void testGetPrivateProfileSectionNames() throws IOException { + // given + final File tmp = File.createTempFile(getName(), "ini"); + tmp.deleteOnExit(); + try { + final PrintWriter writer = new PrintWriter(new BufferedWriter(new FileWriter(tmp))); + try { + writer.println("[S1]"); + writer.println("[S2]"); + } finally { + writer.close(); + } + + // when + final char[] buffer = new char[7]; + final DWORD len = Kernel32.INSTANCE.GetPrivateProfileSectionNames(buffer, new DWORD(buffer.length), tmp.getCanonicalPath()); + + // then + assertThat("Wrong length", len, is(new DWORD(5))); + assertThat("Wrong content", buffer, is(anyOf(equalTo("S1\0S2\0\0".toCharArray()), equalTo("S2\0S1\0\0".toCharArray())))); + } finally { + tmp.delete(); + } + } + + @SuppressWarnings("unchecked") + @Test + public final void testWritePrivateProfileSection() throws IOException { + // given + final File tmp = File.createTempFile(getName(), "ini"); + tmp.deleteOnExit(); + try { + final PrintWriter writer = new PrintWriter(new BufferedWriter(new FileWriter(tmp))); + try { + writer.println("[S1]"); + writer.println("A=1"); + writer.println("B=X"); + } finally { + writer.close(); + } + + // when + final boolean result = Kernel32.INSTANCE.WritePrivateProfileSection("S1", "A=3\0E=Z\0\0", tmp.getCanonicalPath()); + + // then + assertThat("Wrong result", result, is(true)); + assertThat(readAllLines(tmp), hasItems(is("[S1]"), matches("A\\s*=\\s*3"), matches("E\\s*=\\s*Z"))); + } finally { + tmp.delete(); + } + } + + private static final List readAllLines(final File file) throws IOException { + final List lines = new LinkedList(); + final BufferedReader reader = new BufferedReader(new FileReader(file)); + try { + for (String line = reader.readLine(); line != null; line = reader.readLine()) + lines.add(line); + } finally { + reader.close(); + } + return lines; + } } diff --git a/contrib/platform/test/com/sun/jna/platform/win32/Kernel32UtilTest.java b/contrib/platform/test/com/sun/jna/platform/win32/Kernel32UtilTest.java index e57a161708..18f1762a26 100644 --- a/contrib/platform/test/com/sun/jna/platform/win32/Kernel32UtilTest.java +++ b/contrib/platform/test/com/sun/jna/platform/win32/Kernel32UtilTest.java @@ -12,6 +12,13 @@ */ package com.sun.jna.platform.win32; +import static com.sun.jna.platform.win32.RegexMatcher.matches; +import static java.util.Arrays.asList; +import static org.hamcrest.CoreMatchers.hasItems; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.junit.Assert.assertThat; + import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; @@ -19,9 +26,12 @@ import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; +import java.util.List; import junit.framework.TestCase; +import org.junit.Test; + import com.sun.jna.platform.win32.WinNT.LARGE_INTEGER; import com.sun.jna.platform.win32.WinNT.LOGICAL_PROCESSOR_RELATIONSHIP; @@ -182,6 +192,98 @@ public final void testWritePrivateProfileString() throws IOException { reader.close(); } + @Test + public final void testGetPrivateProfileSection() throws IOException { + // given + final File tmp = File.createTempFile("testGetPrivateProfileSection"(), "ini"); + tmp.deleteOnExit(); + try { + final PrintWriter writer = new PrintWriter(new BufferedWriter(new FileWriter(tmp))); + try { + writer.println("[X]"); + writer.println("A=1"); + writer.println("B=X"); + } finally { + writer.close(); + } + + // when + final List section = Kernel32Util.getPrivateProfileSection("X", tmp.getCanonicalPath()); + + // then + assertThat(section, hasItems("A=1", "B=X")); + } finally { + tmp.delete(); + } + } + + @Test + public final void testGetPrivateProfileSectionNames() throws IOException { + // given + final File tmp = File.createTempFile("testGetPrivateProfileSectionNames", "ini"); + tmp.deleteOnExit(); + try { + final PrintWriter writer = new PrintWriter(new BufferedWriter(new FileWriter(tmp))); + try { + writer.println("[S1]"); + writer.println("A=1"); + writer.println("B=X"); + writer.println("[S2]"); + writer.println("C=2"); + writer.println("D=Y"); + } finally { + writer.close(); + } + + // when + final List section = Kernel32Util.getPrivateProfileSectionNames(tmp.getCanonicalPath()); + + // then + assertThat(section, hasItems("S1", "S2")); + } finally { + tmp.delete(); + } + } + + @Test + public final void testWritePrivateProfileSection() throws IOException { + // given + final File tmp = File.createTempFile("testWritePrivateProfileSecion", "ini"); + tmp.deleteOnExit(); + try { + final PrintWriter writer = new PrintWriter(new BufferedWriter(new FileWriter(tmp))); + try { + writer.println("[S1]"); + writer.println("A=1"); + writer.println("B=X"); + writer.println("[S2]"); + writer.println("C=2"); + writer.println("D=Y"); + } finally { + writer.close(); + } + + // when + Kernel32Util.writePrivateProfileSection("S1", asList("A=3", "E=Z"), tmp.getCanonicalPath()); + + // then + final BufferedReader reader = new BufferedReader(new FileReader(tmp)); + try { + assertThat(reader.readLine(), is("[S1]")); + assertThat(reader.readLine(), matches("A\\s*=\\s*3")); + assertThat(reader.readLine(), matches("E\\s*=\\s*Z")); + assertThat(reader.readLine(), is("[S2]")); + assertThat(reader.readLine(), matches("C\\s*=\\s*2")); + assertThat(reader.readLine(), matches("D\\s*=\\s*Y")); + assertThat(reader.readLine(), is(nullValue())); + } finally { + reader.close(); + } + } finally { + tmp.delete(); + } + } + public final void testGetLogicalProcessorInformation() { WinNT.SYSTEM_LOGICAL_PROCESSOR_INFORMATION[] informationArray = Kernel32Util.getLogicalProcessorInformation(); assertTrue(informationArray.length >= 1); // docs say so diff --git a/contrib/platform/test/com/sun/jna/platform/win32/RegexMatcher.java b/contrib/platform/test/com/sun/jna/platform/win32/RegexMatcher.java new file mode 100644 index 0000000000..ad88413c82 --- /dev/null +++ b/contrib/platform/test/com/sun/jna/platform/win32/RegexMatcher.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2013 Markus Karg, All Rights Reserved + * + * This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + */ + +package com.sun.jna.platform.win32; + +import org.hamcrest.Description; +import org.hamcrest.TypeSafeMatcher; + +/** + * This Hamcrest matcher asserts that a {@link String} matches a provided regex pattern. + * + * @author Markus KARG (markus[at]headcrashing[dot]eu) + */ +final class RegexMatcher extends TypeSafeMatcher { + private final String regex; + + public RegexMatcher(final String regex) { + this.regex = regex; + } + + @Override + public final boolean matchesSafely(final String string) { + return string.matches(this.regex); + } + + @Override + public final void describeTo(final Description description) { + description.appendText("matches regex ").appendValue(this.regex); + } + + public final static RegexMatcher matches(final String regex) { + return new RegexMatcher(regex); + } +} diff --git a/lib/hamcrest-core-1.3.jar b/lib/hamcrest-core-1.3.jar new file mode 100644 index 0000000000..9d5fe16e3d Binary files /dev/null and b/lib/hamcrest-core-1.3.jar differ diff --git a/lib/junit.jar b/lib/junit.jar index 674d71e89e..aaf7444849 100755 Binary files a/lib/junit.jar and b/lib/junit.jar differ diff --git a/src/com/sun/jna/Native.java b/src/com/sun/jna/Native.java index 7163c84c7a..2bb0acfa96 100644 --- a/src/com/sun/jna/Native.java +++ b/src/com/sun/jna/Native.java @@ -344,6 +344,17 @@ public static String toString(char[] buf) { return s; } + /** + * Converts a null-terminated sequence of null-terminated strings into an array of Java {@link String}s. + * + * @param buffer + * Contains a null-terminated sequence of null-terminated strings. + * @return {@code buffer}'s content converted to Java {@link String}s. + */ + public static final String[] toStrings(final char[] buffer) { + return new String(buffer).split("\0"); + } + /** Map a library interface to the current process, providing * the explicit interface class. * Native libraries loaded via this method may be found in diff --git a/test/com/sun/jna/NativeTest.java b/test/com/sun/jna/NativeTest.java index a946ca8d90..59cf136c6b 100644 --- a/test/com/sun/jna/NativeTest.java +++ b/test/com/sun/jna/NativeTest.java @@ -12,6 +12,9 @@ */ package com.sun.jna; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + import java.io.File; import java.util.Arrays; import java.util.Collections; @@ -275,6 +278,18 @@ public void testByteArrayToString() { byte[] buf = { 'a', 'b', 'c', '\0', 'd', 'e' }; assertEquals("Wrong String generated", "abc", Native.toString(buf)); } + + @Test + public final void shouldConvertSequenceToStrings() { + // given + final char[] buffer = "ABC\0DEF\0GHI\0\0".toCharArray(); + + // when + final String[] strings = /* Native */N.toStrings(buffer); + + // then + assertThat(strings, is(new String[] { "ABC", "DEF", "GHI" })); + } public void testToByteArray() { final String VALUE = getName();