diff --git a/hitool-core/src/main/java/hitool/core/beanutils/BeanPropertyUtils.java b/hitool-core/src/main/java/hitool/core/beanutils/BeanPropertyUtils.java index e407c0f..171e78f 100644 --- a/hitool-core/src/main/java/hitool/core/beanutils/BeanPropertyUtils.java +++ b/hitool-core/src/main/java/hitool/core/beanutils/BeanPropertyUtils.java @@ -248,12 +248,14 @@ public static void setProperty(Object bean, String name, Object value, Class fieldType = getPropertyUtils().getPropertyType(propertyVlaue, name3); if (fieldType != null && fieldType.isArray()) { int length = current == null ? 0 : Array.getLength(current); - Object array = Array.newInstance(fieldType.getComponentType(), Math.max(length, index + 1)); - if (length > 0) { - System.arraycopy(current, 0, array, 0, length); + if (length <= index) { + Object array = Array.newInstance(fieldType.getComponentType(), index + 1); + if (length > 0) { + System.arraycopy(current, 0, array, 0, length); + } + getPropertyUtils().setProperty(propertyVlaue, name3, array); } - Array.set(array, index, value); - getPropertyUtils().setProperty(propertyVlaue, name3, array); + BeanUtils.setProperty(propertyVlaue, name2, value); } else if (current instanceof List) { List list = (List) current; while (list.size() <= index) { diff --git a/hitool-core/src/main/java/hitool/core/compress/BZip2Utils.java b/hitool-core/src/main/java/hitool/core/compress/BZip2Utils.java index 5534a64..2a3ae46 100644 --- a/hitool-core/src/main/java/hitool/core/compress/BZip2Utils.java +++ b/hitool-core/src/main/java/hitool/core/compress/BZip2Utils.java @@ -14,6 +14,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.nio.file.Files; import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream; import org.apache.commons.compress.compressors.bzip2.BZip2CompressorOutputStream; @@ -176,6 +177,9 @@ public static void decompress(File srcFile, boolean delete) throws IOException { public static void decompress(File srcFile, File destDir) throws IOException { File destFile = new File(destDir, FilenameUtils.getBaseName(srcFile.getName())); + if (Files.exists(destFile.toPath()) && Files.isSameFile(srcFile.toPath(), destFile.toPath())) { + throw new IOException("Decompression output must differ from compressed input: " + srcFile); + } try(InputStream input = new FileInputStream(srcFile); OutputStream output = new FileOutputStream(destFile);) { BZip2Utils.decompress(input, output); diff --git a/hitool-core/src/main/java/hitool/core/compress/GZipUtils.java b/hitool-core/src/main/java/hitool/core/compress/GZipUtils.java index 391b0bb..0542b8c 100644 --- a/hitool-core/src/main/java/hitool/core/compress/GZipUtils.java +++ b/hitool-core/src/main/java/hitool/core/compress/GZipUtils.java @@ -14,6 +14,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.nio.file.Files; import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream; import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream; @@ -194,6 +195,9 @@ public static void decompress(File srcFile, boolean delete) throws IOException { public static void decompress(File srcFile, File destDir) throws IOException { File destFile = new File(destDir, FilenameUtils.getBaseName(srcFile.getName())); + if (Files.exists(destFile.toPath()) && Files.isSameFile(srcFile.toPath(), destFile.toPath())) { + throw new IOException("Decompression output must differ from compressed input: " + srcFile); + } try (InputStream input = new FileInputStream(srcFile); OutputStream output = new FileOutputStream(destFile);){ GZipUtils.decompress(input, output); diff --git a/hitool-core/src/test/java/hitool/core/beanutils/BeanPropertyIndexedConversionTest.java b/hitool-core/src/test/java/hitool/core/beanutils/BeanPropertyIndexedConversionTest.java new file mode 100644 index 0000000..25b9b54 --- /dev/null +++ b/hitool-core/src/test/java/hitool/core/beanutils/BeanPropertyIndexedConversionTest.java @@ -0,0 +1,77 @@ +package hitool.core.beanutils; + +import org.junit.Test; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; + +public class BeanPropertyIndexedConversionTest { + @Test + public void convertsFormValuesForExistingAndExpandedNumericArrays() { + Root root = new Root(); + root.setChild(new Child()); + root.getChild().setNumbers(new int[] {1, 4}); + + BeanPropertyUtils.setProperty(root, "child.numbers[0]", "2", true); + assertArrayEquals(new int[] {2, 4}, root.getChild().getNumbers()); + + BeanPropertyUtils.setProperty(root, "child.numbers[3]", new String[] {"7"}, true); + assertArrayEquals(new int[] {2, 4, 0, 7}, root.getChild().getNumbers()); + } + + @Test + public void initializesNullArraysAndPreservesStringNulls() { + Root root = new Root(); + root.setChild(new Child()); + BeanPropertyUtils.setProperty(root, "child.numbers[0]", "3", true); + assertArrayEquals(new int[] {3}, root.getChild().getNumbers()); + + BeanPropertyUtils.setProperty(root, "child.labels[1]", new String[] {"label"}, true); + assertEquals("label", root.getChild().getLabels()[1]); + assertNull(root.getChild().getLabels()[0]); + BeanPropertyUtils.setProperty(root, "child.labels[0]", null, true); + assertNull(root.getChild().getLabels()[0]); + } + + @Test + public void retainsBeanUtilsConversionForMalformedAndNullNumericValues() { + Root root = new Root(); + root.setChild(new Child()); + root.getChild().setNumbers(new int[] {5, 9}); + // BeanUtils 1.11.0 converts malformed or null primitive values to zero. + BeanPropertyUtils.setProperty(root, "child.numbers[0]", "not-a-number", true); + assertArrayEquals(new int[] {0, 9}, root.getChild().getNumbers()); + BeanPropertyUtils.setProperty(root, "child.numbers[1]", null, true); + assertArrayEquals(new int[] {0, 0}, root.getChild().getNumbers()); + } + + @Test + public void invalidIndexDoesNotChangeExistingElements() { + Root root = new Root(); + root.setChild(new Child()); + root.getChild().setNumbers(new int[] {5}); + try { + BeanPropertyUtils.setProperty(root, "child.numbers[invalid]", "2", true); + fail("Expected invalid indexed property to fail"); + } catch (RuntimeException expected) { + assertArrayEquals(new int[] {5}, root.getChild().getNumbers()); + } + } + + public static class Root { + private Child child; + public Child getChild() { return child; } + public void setChild(Child child) { this.child = child; } + } + + public static class Child { + private int[] numbers; + private String[] labels; + public int[] getNumbers() { return numbers; } + public void setNumbers(int[] numbers) { this.numbers = numbers; } + public String[] getLabels() { return labels; } + public void setLabels(String[] labels) { this.labels = labels; } + } +} diff --git a/hitool-core/src/test/java/hitool/core/compress/CompressionSelfOverwriteTest.java b/hitool-core/src/test/java/hitool/core/compress/CompressionSelfOverwriteTest.java new file mode 100644 index 0000000..cb42c54 --- /dev/null +++ b/hitool-core/src/test/java/hitool/core/compress/CompressionSelfOverwriteTest.java @@ -0,0 +1,79 @@ +package hitool.core.compress; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.Test; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.fail; + +public class CompressionSelfOverwriteTest { + @Test + public void gzipRejectsExtensionlessInputAndSameFileAlias() throws Exception { + byte[] compressed = GZipUtils.compress("payload".getBytes(StandardCharsets.UTF_8)); + Path dir = Files.createTempDirectory("hitool-gzip-safety-"); + try { + Path source = dir.resolve("payload"); + Files.write(source, compressed); + expectIOException(() -> GZipUtils.decompress(source.toFile(), false)); + assertArrayEquals(compressed, Files.readAllBytes(source)); + expectIOException(() -> GZipUtils.decompress(source.toFile(), dir.toFile())); + assertArrayEquals(compressed, Files.readAllBytes(source)); + + Path named = dir.resolve("named.gz"); + Files.write(named, compressed); + Path alias = dir.resolve("named"); + Files.createSymbolicLink(alias, named.getFileName()); + expectIOException(() -> GZipUtils.decompress(named.toFile(), dir.toFile())); + assertArrayEquals(compressed, Files.readAllBytes(named)); + } finally { + Files.deleteIfExists(dir.resolve("named")); + Files.deleteIfExists(dir.resolve("named.gz")); + Files.deleteIfExists(dir.resolve("payload")); + Files.deleteIfExists(dir); + } + } + + @Test + public void bzip2RejectsExtensionlessInputAndSameFileAlias() throws Exception { + byte[] compressed = BZip2Utils.compress("payload".getBytes(StandardCharsets.UTF_8)); + Path dir = Files.createTempDirectory("hitool-bzip2-safety-"); + try { + Path source = dir.resolve("payload"); + Files.write(source, compressed); + expectIOException(() -> BZip2Utils.decompress(source.toFile(), false)); + assertArrayEquals(compressed, Files.readAllBytes(source)); + expectIOException(() -> BZip2Utils.decompress(source.toFile(), dir.toFile())); + assertArrayEquals(compressed, Files.readAllBytes(source)); + + Path named = dir.resolve("named.bz2"); + Files.write(named, compressed); + Path alias = dir.resolve("named"); + Files.createSymbolicLink(alias, named.getFileName()); + expectIOException(() -> BZip2Utils.decompress(named.toFile(), dir.toFile())); + assertArrayEquals(compressed, Files.readAllBytes(named)); + } finally { + Files.deleteIfExists(dir.resolve("named")); + Files.deleteIfExists(dir.resolve("named.bz2")); + Files.deleteIfExists(dir.resolve("payload")); + Files.deleteIfExists(dir); + } + } + + private void expectIOException(ThrowingAction action) throws Exception { + try { + action.run(); + fail("Expected IOException before opening the output file"); + } catch (IOException expected) { + // The compressed source must remain intact. + } + } + + private interface ThrowingAction { + void run() throws Exception; + } +}