diff --git a/.gitignore b/.gitignore index 047c338..427f007 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,4 @@ build/ /.mvn /build_release /upload/ +cp.txt diff --git a/cp.txt b/cp.txt deleted file mode 100644 index bb68717..0000000 --- a/cp.txt +++ /dev/null @@ -1 +0,0 @@ -C:\Users\Administrator\.m2\repository\org\apache\commons\commons-compress\1.28.0\commons-compress-1.28.0.jar;C:\Users\Administrator\.m2\repository\commons-codec\commons-codec\1.19.0\commons-codec-1.19.0.jar;C:\Users\Administrator\.m2\repository\commons-io\commons-io\2.20.0\commons-io-2.20.0.jar;C:\Users\Administrator\.m2\repository\org\apache\commons\commons-lang3\3.18.0\commons-lang3-3.18.0.jar \ No newline at end of file diff --git a/pom.xml b/pom.xml index f2bcbf3..4a5b51c 100644 --- a/pom.xml +++ b/pom.xml @@ -7,11 +7,25 @@ top.gtb520.java.gnu gnu_java_utils_library 1.6-SNAPSHOT + jar + + GNU Java Utils Library + 轻量级 Java 工具库:压缩/解压、终端彩色输出、文件创建与管理、文件加密/解密、随机数生成、硬件标识与运行时信息。 + https://github.com/databasemysql233-debug/gnu_java_utils_library + + + + Apache License, Version 2.0 + https://www.apache.org/licenses/LICENSE-2.0 + repo + + - 25 - 25 + 25 UTF-8 + 5.13.0 + 3.6.0 @@ -23,9 +37,34 @@ org.junit.jupiter junit-jupiter - 5.13.0 + ${junit.version} test - \ No newline at end of file + + + + org.apache.maven.plugins + maven-surefire-plugin + ${surefire.version} + + + org.apache.maven.plugins + maven-jar-plugin + 3.4.2 + + + + true + + + top.gtb520.java.gnu.utils.library + + + + + + + + diff --git a/src/main/java/top/gtb520/java/gnu/java/utils/library/utils/compress/CompressPNG.java b/src/main/java/top/gtb520/java/gnu/java/utils/library/utils/compress/CompressPNG.java index 3d1a9dd..e5c02af 100644 --- a/src/main/java/top/gtb520/java/gnu/java/utils/library/utils/compress/CompressPNG.java +++ b/src/main/java/top/gtb520/java/gnu/java/utils/library/utils/compress/CompressPNG.java @@ -5,61 +5,92 @@ import javax.imageio.ImageIO; import javax.imageio.ImageWriteParam; import javax.imageio.ImageWriter; import javax.imageio.stream.ImageOutputStream; -import java.io.File; import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; import java.util.Iterator; public class CompressPNG { - /*** + /** * 压缩PNG图片 * 传入未压缩的PNG图片文件,和压缩阈值,返回压缩后的PNG图片文件 * 5个等级的压缩阈值,从低到高,THRESHOLD_1 最低,THRESHOLD_5 最高 * + * 注意:写入过程先写同目录临时文件、成功后原子替换原文件,避免写入失败损坏原图; + * JDK 内置 PNG writer 对 compressionQuality 的支持取决于实现,档位效果可能有限。 + * * @param pngFile 未压缩的PNG图片文件 - * @param threshold 压缩阈值 - * @return 压缩后的PNG图片文件 + * @param threshold 压缩阈值,不能为 null + * @return 压缩后的PNG图片文件;文件不存在或不是 PNG 时返回 null + * @throws IllegalArgumentException 当 threshold 为 null 时 */ public static File PNG(File pngFile, PNGThreshold threshold) { - if (!pngFile.exists()) { - System.out.println("文件不存在"); + if (threshold == null) { + throw new IllegalArgumentException("压缩阈值不能为 null"); + } + if (pngFile == null || !pngFile.exists()) { return null; } - if (pngFile.getName().endsWith(".png") || pngFile.getName().endsWith(".PNG")) { + if (!pngFile.getName().toLowerCase().endsWith(".png")) { + return null; + } + + File tmp = null; + try { + BufferedImage image = ImageIO.read(pngFile); + if (image == null) { + return null; + } + + Iterator writers = ImageIO.getImageWritersByFormatName("png"); + if (!writers.hasNext()) { + return null; + } + ImageWriter writer = writers.next(); + try { - BufferedImage image = ImageIO.read(pngFile); - if (image == null) { - System.out.println("无法读取PNG文件"); - return null; - } - - Iterator writers = ImageIO.getImageWritersByFormatName("png"); - if (!writers.hasNext()) { - System.out.println("未找到PNG写入器"); - return null; - } - ImageWriter writer = writers.next(); - - try (ImageOutputStream ios = ImageIO.createImageOutputStream(pngFile)) { + tmp = File.createTempFile(pngFile.getName() + ".", ".tmp", + pngFile.getAbsoluteFile().getParentFile()); + try (ImageOutputStream ios = ImageIO.createImageOutputStream(tmp)) { writer.setOutput(ios); ImageWriteParam param = writer.getDefaultWriteParam(); - param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); - param.setCompressionQuality(getCompressionQuality(threshold)); + try { + param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); + param.setCompressionQuality(getCompressionQuality(threshold)); + } catch (UnsupportedOperationException e) { + // 部分 PNG writer 不支持显式压缩质量,退回到默认参数 + param = writer.getDefaultWriteParam(); + } writer.write(null, new IIOImage(image, null, null), param); - } finally { - writer.dispose(); } - return pngFile; - } catch (Exception e) { - e.printStackTrace(); - return null; + // 原子替换原文件,避免原地写入失败损坏原图 + try { + Files.move(tmp.toPath(), pngFile.toPath(), + StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + } catch (AtomicMoveNotSupportedException e) { + Files.move(tmp.toPath(), pngFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + } + } finally { + writer.dispose(); + if (tmp != null) { + try { + Files.deleteIfExists(tmp.toPath()); + } catch (IOException ignored) { + // 清理失败不影响结果 + } + } } - } else { - System.out.println("文件不是PNG格式,无法压缩"); + + return pngFile; + } catch (Exception e) { + return null; } - return null; } private static float getCompressionQuality(PNGThreshold threshold) { diff --git a/src/main/java/top/gtb520/java/gnu/java/utils/library/utils/compress/compress.java b/src/main/java/top/gtb520/java/gnu/java/utils/library/utils/compress/compress.java index 2bb5da9..13e8ac7 100644 --- a/src/main/java/top/gtb520/java/gnu/java/utils/library/utils/compress/compress.java +++ b/src/main/java/top/gtb520/java/gnu/java/utils/library/utils/compress/compress.java @@ -3,6 +3,7 @@ package top.gtb520.java.gnu.java.utils.library.utils.compress; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; +import org.apache.commons.compress.archivers.tar.TarConstants; import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream; import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream; @@ -34,6 +35,7 @@ public class compress { if (!Files.isDirectory(destPath)) { throw new IllegalArgumentException("目标路径必须是有效的目录"); } + Path destRoot = destPath.toAbsolutePath().normalize(); try (InputStream fileIn = Files.newInputStream(Paths.get(sourceFilePanth)); BufferedInputStream bufferedIn = new BufferedInputStream(fileIn); @@ -41,8 +43,12 @@ public class compress { TarArchiveInputStream tarIn = new TarArchiveInputStream(gzipIn)) { TarArchiveEntry entry; - while ((entry = tarIn.getNextTarEntry()) != null) { - Path targetPath = destPath.resolve(entry.getName()); + while ((entry = tarIn.getNextEntry()) != null) { + // 拒绝符号链接/硬链接条目,防止解压逃逸 + if (entry.isSymbolicLink() || entry.getLinkFlag() == TarConstants.LF_LINK) { + throw new IllegalArgumentException("压缩包包含不支持的链接条目: " + entry.getName()); + } + Path targetPath = resolveInside(destRoot, entry.getName()); if (entry.isDirectory()) { Files.createDirectories(targetPath); } else { @@ -53,6 +59,18 @@ public class compress { } } + /** + * 将压缩包条目名解析为解压目录内的路径; + * 条目越过目标目录(如 ../ 或绝对路径)时抛出 IllegalArgumentException。 + */ + private static Path resolveInside(Path destRoot, String entryName) { + Path targetPath = destRoot.resolve(entryName).normalize(); + if (!targetPath.startsWith(destRoot)) { + throw new IllegalArgumentException("压缩包包含非法路径条目: " + entryName); + } + return targetPath; + } + /** * 压缩Tar.Gz压缩包 * @@ -78,7 +96,7 @@ public class compress { @Override public java.nio.file.FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws java.io.IOException { String entryName = sourcePath.relativize(file).toString().replace('\\', '/'); - TarArchiveEntry entry = new TarArchiveEntry(file.toFile(), entryName); + TarArchiveEntry entry = new TarArchiveEntry(file, entryName); tarOut.putArchiveEntry(entry); Files.copy(file, tarOut); tarOut.closeArchiveEntry(); @@ -97,7 +115,7 @@ public class compress { } }); } else { - TarArchiveEntry entry = new TarArchiveEntry(sourcePath.toFile(), sourcePath.getFileName().toString()); + TarArchiveEntry entry = new TarArchiveEntry(sourcePath, sourcePath.getFileName().toString()); tarOut.putArchiveEntry(entry); Files.copy(sourcePath, tarOut); tarOut.closeArchiveEntry(); @@ -166,14 +184,19 @@ public class compress { if (!Files.isDirectory(destPath)) { throw new IllegalArgumentException("目标路径必须是有效的目录"); } - + Path destRoot = destPath.toAbsolutePath().normalize(); + try (InputStream fileIn = Files.newInputStream(Paths.get(sourceFilePanth)); BufferedInputStream bufferedIn = new BufferedInputStream(fileIn); ZipArchiveInputStream zipIn = new ZipArchiveInputStream(bufferedIn)) { - + ZipArchiveEntry entry; while ((entry = zipIn.getNextEntry()) != null) { - Path targetPath = destPath.resolve(entry.getName()); + // 拒绝符号链接条目,防止解压逃逸 + if (entry.isUnixSymlink()) { + throw new IllegalArgumentException("压缩包包含不支持的符号链接条目: " + entry.getName()); + } + Path targetPath = resolveInside(destRoot, entry.getName()); if (entry.isDirectory()) { Files.createDirectories(targetPath); } else { diff --git a/src/main/java/top/gtb520/java/gnu/java/utils/library/utils/file/CreateFile.java b/src/main/java/top/gtb520/java/gnu/java/utils/library/utils/file/CreateFile.java index 790c192..f68c8c6 100644 --- a/src/main/java/top/gtb520/java/gnu/java/utils/library/utils/file/CreateFile.java +++ b/src/main/java/top/gtb520/java/gnu/java/utils/library/utils/file/CreateFile.java @@ -5,15 +5,17 @@ import java.io.IOException; import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardCopyOption; import java.nio.file.StandardOpenOption; -import java.util.concurrent.CompletableFuture; // 创建文件 public class CreateFile { - /*** - * 创建文件,创建空白文件 - * isBin为True时创建一个空白的二进制文件,否则创建一个空白的普通文件 - * 异步IO操作 + /** + * 创建文件,创建空白文件。 + * isBin为True时创建一个空白的二进制文件,否则创建一个空白的普通文件。 + * + * 注意:文件已存在时不覆盖、不写入文件头,直接返回该文件; + * 创建失败(如路径无权限)返回 null。 * * @param FileName 文件名 * @param isBin 是否二进制文件 @@ -23,62 +25,50 @@ public class CreateFile { * @return 文件 */ public static File NewFile(String FileName, boolean isBin, BinFileType binFileType) { - File TargetFile = null; - byte[] BufferByte = new byte[1024]; - + if (FileName == null) { + return null; + } + File targetFile; try { - TargetFile = new File(FileName); - if (TargetFile.createNewFile()) { + targetFile = new File(FileName); + if (targetFile.createNewFile()) { if (isBin && binFileType != null) { // 根据二进制文件类型写入文件头标识 byte[] header = switch (binFileType) { case ELF -> new byte[]{0x7F, 'E', 'L', 'F'}; case PE -> new byte[]{'M', 'Z'}; }; - try (OutputStream os = Files.newOutputStream(TargetFile.toPath(), + try (OutputStream os = Files.newOutputStream(targetFile.toPath(), StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)) { os.write(header); os.flush(); } } } + return targetFile; } catch (IOException e) { - TargetFile = null; + return null; } - - return TargetFile; } - /*** - * 以字节流方式保存文件 - * 异步IO操作 + /** + * 以字节流方式保存文件到目标路径(覆盖同名文件)。 * - * @param file 文件 + * @param file 源文件 * @param TargePath 目标路径 * @param FileName 文件名 * * @return 是否保存成功,True为保存成功,False为保存失败 */ public static boolean StreamSaveFile(File file, Path TargePath, String FileName) { + if (file == null || !file.isFile() || TargePath == null || FileName == null || FileName.isEmpty()) { + return false; + } + Path targetFile = TargePath.resolve(FileName); try { - Path targetFile = TargePath.resolve(FileName); - return CompletableFuture.supplyAsync(() -> { - try (OutputStream os = Files.newOutputStream(targetFile, - StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) { - byte[] buffer = new byte[1024]; - int bytesRead; - try (var is = Files.newInputStream(file.toPath())) { - while ((bytesRead = is.read(buffer)) != -1) { - os.write(buffer, 0, bytesRead); - } - } - os.flush(); - return true; - } catch (IOException e) { - return false; - } - }).get(); - } catch (Exception e) { + Files.copy(file.toPath(), targetFile, StandardCopyOption.REPLACE_EXISTING); + return true; + } catch (IOException e) { return false; } } diff --git a/src/main/java/top/gtb520/java/gnu/java/utils/library/utils/file/EncryptionAndDecryption.java b/src/main/java/top/gtb520/java/gnu/java/utils/library/utils/file/EncryptionAndDecryption.java index e1f0c3a..8665590 100644 --- a/src/main/java/top/gtb520/java/gnu/java/utils/library/utils/file/EncryptionAndDecryption.java +++ b/src/main/java/top/gtb520/java/gnu/java/utils/library/utils/file/EncryptionAndDecryption.java @@ -1,202 +1,330 @@ package top.gtb520.java.gnu.java.utils.library.utils.file; +import javax.crypto.Cipher; +import javax.crypto.CipherInputStream; +import javax.crypto.CipherOutputStream; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.DataInputStream; import java.io.File; +import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.file.AtomicMoveNotSupportedException; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardCopyOption; import java.nio.file.StandardOpenOption; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.util.concurrent.CompletableFuture; - -import javax.crypto.BadPaddingException; -import javax.crypto.Cipher; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.KeyGenerator; -import javax.crypto.NoSuchPaddingException; -import javax.crypto.SecretKey; +import java.nio.file.attribute.PosixFilePermissions; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.spec.PKCS8EncodedKeySpec; +/** + * 文件加密/解密工具类。 + * + * + * + * 加密/解密均为流式处理(支持大文件),先写临时文件再原子替换原文件,避免中途失败损坏数据。 + * 非法入参(文件/密钥路径不存在、为 null)返回 {@code null},实际加解密失败抛出 {@link RuntimeException}。 + */ public class EncryptionAndDecryption { - /*** - * 文件加密 - * 异步执行加密操作 - * 加密后覆盖原文件,不添加额外后缀 - * 密钥保存到 keyPath 目录下,密钥名为 原文件名.key + + private static final int AES_KEY_BITS = 256; + private static final int DES_KEY_BITS = 56; + private static final int RSA_KEY_BITS = 2048; + private static final int GCM_IV_BYTES = 12; + private static final int GCM_TAG_BITS = 128; + private static final String AES_GCM = "AES/GCM/NoPadding"; + private static final String DES_ECB = "DES/ECB/PKCS5Padding"; + private static final String RSA_WRAP = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding"; + + /** + * 文件加密。加密后覆盖原文件(原子替换),密钥保存到 keyPath 目录下,密钥名为 原文件名.key。 * - * @param file 文件 - * @param type 加密类型 - * @param keyPath 密钥文件目录路径,密钥名为 文件名.key - * @return 加密后的文件(原文件),失败时返回 null + * @param file 要加密的文件 + * @param type 加密类型 + * @param keyPath 密钥文件目录路径 + * @return 加密后的文件(原文件路径);入参非法时返回 null */ public static File EncryptionFile(File file, EncryptionType type, Path keyPath) { - File TargetFile = null; - if (file == null || !file.exists() || !file.isFile()) { return null; } - if (keyPath == null) { + if (type == null || keyPath == null) { return null; } - try { - TargetFile = CompletableFuture.supplyAsync(() -> { - try { - // 读取原文件内容 - byte[] plainBytes = Files.readAllBytes(file.toPath()); + Files.createDirectories(keyPath); + Path keyFilePath = keyPath.resolve(file.getName() + ".key"); - // 根据加密类型生成对称密钥 - SecretKey secretKey = generateSecretKey(type); - byte[] encryptedBytes = encryptBytes(plainBytes, secretKey, type); - - // 将密钥写入 keyPath 目录,密钥名为 文件名.key - Files.createDirectories(keyPath); - String keyFileName = file.getName() + ".key"; - Path keyFilePath = keyPath.resolve(keyFileName); - try (OutputStream kos = Files.newOutputStream(keyFilePath, - StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) { - kos.write(secretKey.getEncoded()); - kos.flush(); - } - - // 加密后覆盖写回原文件 - try (OutputStream os = Files.newOutputStream(file.toPath(), - StandardOpenOption.TRUNCATE_EXISTING)) { - os.write(encryptedBytes); - os.flush(); - } - return file; - } catch (Exception e) { - return null; + switch (type) { + case AES -> { + SecretKey key = generateKey("AES", AES_KEY_BITS); + writeKeyFile(keyFilePath, key.getEncoded()); + encryptAesGcm(file, key); } - }).get(); + case DES -> { + SecretKey key = generateKey("DES", DES_KEY_BITS); + writeKeyFile(keyFilePath, key.getEncoded()); + transform(file, buildCipher(DES_ECB, Cipher.ENCRYPT_MODE, key, null), true); + } + case RSA -> { + KeyPair keyPair = generateRsaKeyPair(); + // 密钥文件保存 PKCS#8 私钥 + writeKeyFile(keyFilePath, keyPair.getPrivate().getEncoded()); + encryptRsaHybrid(file, keyPair.getPublic()); + } + default -> throw new IllegalArgumentException("不支持的加密类型: " + type); + } + return file; } catch (Exception e) { - TargetFile = null; + throw new RuntimeException("加密失败: " + file, e); } - - return TargetFile; } - /*** - * 生成对称密钥 + /** + * 文件解密。解密后覆盖写回原文件(原子替换)。 * - * @param type 加密类型 - * @return 对称密钥 + * @param file 加密文件 + * @param type 加密类型,须与加密时一致 + * @param keyfile 密钥文件,从中读取解密密钥 + * @return 解密后的文件(原文件路径);入参非法时返回 null */ - private static SecretKey generateSecretKey(EncryptionType type) throws NoSuchAlgorithmException { - String algorithm = switch (type) { - case AES -> "AES"; - case DES -> "DES"; - case RSA -> "AES"; // RSA 为非对称加密,此处用 AES 做对称加密替代 - }; + public static File DecryptionFile(File file, EncryptionType type, File keyfile) { + if (file == null || !file.exists() || !file.isFile()) { + return null; + } + if (type == null || keyfile == null || !keyfile.exists() || !keyfile.isFile()) { + return null; + } + try { + byte[] keyBytes = Files.readAllBytes(keyfile.toPath()); + switch (type) { + case AES -> decryptAesGcm(file, new SecretKeySpec(keyBytes, "AES")); + case DES -> transform(file, buildCipher(DES_ECB, Cipher.DECRYPT_MODE, + new SecretKeySpec(keyBytes, "DES"), null), false); + case RSA -> { + PrivateKey privateKey = KeyFactory.getInstance("RSA") + .generatePrivate(new PKCS8EncodedKeySpec(keyBytes)); + decryptRsaHybrid(file, privateKey); + } + default -> throw new IllegalArgumentException("不支持的加密类型: " + type); + } + return file; + } catch (Exception e) { + throw new RuntimeException("解密失败: " + file, e); + } + } + + // ==================== AES-GCM ==================== + + private static void encryptAesGcm(File file, SecretKey key) throws Exception { + byte[] iv = new byte[GCM_IV_BYTES]; + new SecureRandom().nextBytes(iv); + Cipher cipher = buildCipher(AES_GCM, Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(GCM_TAG_BITS, iv)); + + Path tmp = tempSibling(file); + try (OutputStream out = new BufferedOutputStream(Files.newOutputStream(tmp))) { + out.write(iv); // 密文格式: IV(12) || 密文 + out.flush(); + try (CipherOutputStream cos = new CipherOutputStream(out, cipher); + InputStream in = Files.newInputStream(file.toPath())) { + in.transferTo(cos); + } + } + moveAtomically(tmp, file); + } + + private static void decryptAesGcm(File file, SecretKey key) throws Exception { + try (InputStream in = new BufferedInputStream(Files.newInputStream(file.toPath()))) { + byte[] iv = in.readNBytes(GCM_IV_BYTES); + if (iv.length != GCM_IV_BYTES) { + throw new IOException("密文过短或已损坏,无法读取 IV"); + } + Cipher cipher = buildCipher(AES_GCM, Cipher.DECRYPT_MODE, key, new GCMParameterSpec(GCM_TAG_BITS, iv)); + Path tmp = tempSibling(file); + try (CipherInputStream cis = new CipherInputStream(in, cipher); + OutputStream out = new BufferedOutputStream(Files.newOutputStream(tmp))) { + cis.transferTo(out); + } + moveAtomically(tmp, file); + } + } + + // ==================== RSA 混合加密 ==================== + + private static void encryptRsaHybrid(File file, PublicKey publicKey) throws Exception { + // 随机 AES-256 会话密钥,RSA-OAEP 包裹后存入文件头 + SecretKey sessionKey = generateKey("AES", AES_KEY_BITS); + byte[] iv = new byte[GCM_IV_BYTES]; + new SecureRandom().nextBytes(iv); + + Cipher wrapCipher = Cipher.getInstance(RSA_WRAP); + wrapCipher.init(Cipher.WRAP_MODE, publicKey); + byte[] wrappedKey = wrapCipher.wrap(sessionKey); + + Cipher dataCipher = buildCipher(AES_GCM, Cipher.ENCRYPT_MODE, sessionKey, + new GCMParameterSpec(GCM_TAG_BITS, iv)); + + // 密文格式: 包裹密钥长度(4字节) || 包裹密钥 || IV(12) || 密文 + Path tmp = tempSibling(file); + try (OutputStream out = new BufferedOutputStream(Files.newOutputStream(tmp))) { + out.write(ByteBuffer.allocate(4).putInt(wrappedKey.length).array()); + out.write(wrappedKey); + out.write(iv); + out.flush(); + try (CipherOutputStream cos = new CipherOutputStream(out, dataCipher); + InputStream in = Files.newInputStream(file.toPath())) { + in.transferTo(cos); + } + } + moveAtomically(tmp, file); + } + + private static void decryptRsaHybrid(File file, PrivateKey privateKey) throws Exception { + try (DataInputStream in = new DataInputStream(new BufferedInputStream(Files.newInputStream(file.toPath())))) { + int wrappedKeyLength = in.readInt(); + if (wrappedKeyLength <= 0 || wrappedKeyLength > 4096) { + throw new IOException("密文头损坏: 包裹密钥长度非法"); + } + byte[] wrappedKey = in.readNBytes(wrappedKeyLength); + if (wrappedKey.length != wrappedKeyLength) { + throw new IOException("密文头损坏: 包裹密钥不完整"); + } + byte[] iv = in.readNBytes(GCM_IV_BYTES); + if (iv.length != GCM_IV_BYTES) { + throw new IOException("密文头损坏: IV 不完整"); + } + + Cipher unwrapCipher = Cipher.getInstance(RSA_WRAP); + unwrapCipher.init(Cipher.UNWRAP_MODE, privateKey); + SecretKey sessionKey = (SecretKey) unwrapCipher.unwrap(wrappedKey, "AES", Cipher.SECRET_KEY); + + Cipher dataCipher = buildCipher(AES_GCM, Cipher.DECRYPT_MODE, sessionKey, + new GCMParameterSpec(GCM_TAG_BITS, iv)); + Path tmp = tempSibling(file); + try (CipherInputStream cis = new CipherInputStream(in, dataCipher); + OutputStream out = new BufferedOutputStream(Files.newOutputStream(tmp))) { + cis.transferTo(out); + } + moveAtomically(tmp, file); + } + } + + // ==================== 通用工具 ==================== + + private static SecretKey generateKey(String algorithm, int keyBits) throws Exception { KeyGenerator keyGen = KeyGenerator.getInstance(algorithm); - int keySize = switch (type) { - case AES -> 128; - case DES -> 56; - case RSA -> 128; - }; - keyGen.init(keySize); + keyGen.init(keyBits); return keyGen.generateKey(); } - /*** - * 对字节数据进行加密 - * - * @param data 原始数据 - * @param key 密钥 - * @param type 加密类型 - * @return 加密后的数据 - */ - private static byte[] encryptBytes(byte[] data, SecretKey key, EncryptionType type) - throws NoSuchAlgorithmException, NoSuchPaddingException, - InvalidKeyException, IllegalBlockSizeException, BadPaddingException { - String algorithm = switch (type) { - case AES -> "AES/ECB/PKCS5Padding"; - case DES -> "DES/ECB/PKCS5Padding"; - case RSA -> "AES/ECB/PKCS5Padding"; - }; - Cipher cipher = Cipher.getInstance(algorithm); - cipher.init(Cipher.ENCRYPT_MODE, key); - return cipher.doFinal(data); + private static KeyPair generateRsaKeyPair() throws Exception { + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); + keyGen.initialize(RSA_KEY_BITS); + return keyGen.generateKeyPair(); } - /*** - * 文件解密 - * 异步执行解密操作 - * 解密后覆盖写回原文件,不添加额外后缀 - * - * @param file 加密文件 - * @param type 加密类型,须与加密时一致 - * @param keyfile 密钥文件,从中读取解密密钥 - * @return 解密后的文件(原文件),失败时返回 null + private static Cipher buildCipher(String algorithm, int mode, SecretKey key, + java.security.spec.AlgorithmParameterSpec params) throws Exception { + Cipher cipher = Cipher.getInstance(algorithm); + if (params == null) { + cipher.init(mode, key); + } else { + cipher.init(mode, key, params); + } + return cipher; + } + + /** + * 流式加/解密:读原文件 -> Cipher 流 -> 临时文件 -> 原子替换原文件。 */ - public static File DecryptionFile(File file, EncryptionType type, File keyfile) { - File TargetFile = null; - - if (file == null || !file.exists() || !file.isFile()) { - return null; - } - if (keyfile == null || !keyfile.exists() || !keyfile.isFile()) { - return null; - } - - try { - TargetFile = CompletableFuture.supplyAsync(() -> { - try { - // 从密钥文件读取密钥 - byte[] keyBytes = Files.readAllBytes(keyfile.toPath()); - String algorithm = switch (type) { - case AES -> "AES"; - case DES -> "DES"; - case RSA -> "AES"; - }; - SecretKey secretKey = new javax.crypto.spec.SecretKeySpec(keyBytes, algorithm); - - // 读取加密文件内容 - byte[] encryptedBytes = Files.readAllBytes(file.toPath()); - - // 解密 - byte[] decryptedBytes = decryptBytes(encryptedBytes, secretKey, type); - - // 解密后覆盖写回原文件 - try (OutputStream os = Files.newOutputStream(file.toPath(), - StandardOpenOption.TRUNCATE_EXISTING)) { - os.write(decryptedBytes); - os.flush(); - } - return file; - } catch (Exception e) { - return null; + private static void transform(File file, Cipher cipher, boolean encrypt) throws Exception { + Path tmp = tempSibling(file); + if (encrypt) { + try (OutputStream out = new BufferedOutputStream(Files.newOutputStream(tmp))) { + try (CipherOutputStream cos = new CipherOutputStream(out, cipher); + InputStream in = Files.newInputStream(file.toPath())) { + in.transferTo(cos); } - }).get(); - } catch (Exception e) { - TargetFile = null; + } + } else { + try (InputStream in = new BufferedInputStream(Files.newInputStream(file.toPath()))) { + try (CipherInputStream cis = new CipherInputStream(in, cipher); + OutputStream out = new BufferedOutputStream(Files.newOutputStream(tmp))) { + cis.transferTo(out); + } + } } - - return TargetFile; + moveAtomically(tmp, file); } - /*** - * 对字节数据进行解密 - * - * @param data 加密数据 - * @param key 密钥 - * @param type 加密类型 - * @return 解密后的数据 + /** + * 在原文件同目录创建临时文件,保证原子移动(rename)时在同一文件系统内。 */ - private static byte[] decryptBytes(byte[] data, SecretKey key, EncryptionType type) - throws NoSuchAlgorithmException, NoSuchPaddingException, - InvalidKeyException, IllegalBlockSizeException, BadPaddingException { - String algorithm = switch (type) { - case AES -> "AES/ECB/PKCS5Padding"; - case DES -> "DES/ECB/PKCS5Padding"; - case RSA -> "AES/ECB/PKCS5Padding"; - }; - Cipher cipher = Cipher.getInstance(algorithm); - cipher.init(Cipher.DECRYPT_MODE, key); - return cipher.doFinal(data); + private static Path tempSibling(File file) throws IOException { + Path dir = file.toPath().toAbsolutePath().getParent(); + return Files.createTempFile(dir, file.getName() + ".", ".tmp"); + } + + /** + * 原子替换原文件;文件系统不支持原子移动时回退为普通覆盖移动。 + */ + private static void moveAtomically(Path tmp, File target) throws IOException { + try { + Files.move(tmp, target.toPath(), StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + } catch (AtomicMoveNotSupportedException e) { + Files.move(tmp, target.toPath(), StandardCopyOption.REPLACE_EXISTING); + } + } + + private static void writeKeyFile(Path keyFilePath, byte[] keyBytes) throws IOException { + try (OutputStream os = Files.newOutputStream(keyFilePath, + StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) { + os.write(keyBytes); + os.flush(); + } + restrictKeyFilePermissions(keyFilePath); + } + + /** + * POSIX 系统上将密钥文件限制为仅属主可读写;Windows 等不支持的文件系统跳过。 + */ + private static void restrictKeyFilePermissions(Path keyFilePath) throws IOException { + try { + Files.setPosixFilePermissions(keyFilePath, PosixFilePermissions.fromString("rw-------")); + } catch (UnsupportedOperationException ignored) { + // Windows 等不支持 POSIX 权限的文件系统,无需处理 + } } public enum EncryptionType { - AES, DES, RSA; + AES, + + /** + * 已废弃:DES 密钥仅 56 位,已被暴力破解,仅用于兼容旧版本,新代码请使用 {@link #AES}。 + */ + @Deprecated + DES, + + /** + * RSA 混合加密:RSA-2048/OAEP 包裹随机 AES-256 会话密钥,数据使用 AES-GCM 加密。 + */ + RSA; } } diff --git a/src/main/java/top/gtb520/java/gnu/java/utils/library/utils/print/echo.java b/src/main/java/top/gtb520/java/gnu/java/utils/library/utils/print/echo.java index e3a500b..bcdd0b0 100644 --- a/src/main/java/top/gtb520/java/gnu/java/utils/library/utils/print/echo.java +++ b/src/main/java/top/gtb520/java/gnu/java/utils/library/utils/print/echo.java @@ -12,17 +12,37 @@ public class echo { /** * 使用自定义颜色码在终端打印彩色文本 * 支持三种格式: - * - "#RRGGBB" 十六进制颜色(如 "#FFC0CB") + * - "#RRGGBB" / "#RGB" 十六进制颜色(如 "#FFC0CB") * - ANSI 颜色代码(如 "31" 表示红色) * - ANSI 扩展色(如 "38;5;208" 表示扩展色) * * @param Message 要打印的文本内容 * @param HexColor 颜色代码,支持 "#RRGGBB"、ANSI 颜色代码或扩展色 + * @throws IllegalArgumentException 当颜色代码格式非法或为 null 时 */ public static void ColorHexPrintln(String Message, String HexColor) { - if (HexColor.startsWith("#") && (HexColor.length() == 7 || HexColor.length() == 4)) { - // #RRGGBB 或 #RGB 格式,转换为 ANSI 真彩色序列 - String hex = HexColor.substring(1); + if (Message == null) { + Message = ""; + } + String ansi = toAnsiColorCode(HexColor); + System.out.println("\033[" + ansi + "m" + Message + "\033[0m"); + } + + /** + * 将颜色代码转换为 ANSI 颜色序列中的参数部分,并对输入做合法性校验。 + * + * @param hexColor 颜色代码:"#RRGGBB"、"#RGB" 或纯数字/分号的 ANSI 参数 + * @return ANSI 参数(如 "38;2;255;192;203"、"31") + */ + private static String toAnsiColorCode(String hexColor) { + if (hexColor == null) { + throw new IllegalArgumentException("颜色码不能为 null"); + } + if (hexColor.startsWith("#")) { + String hex = hexColor.substring(1); + if ((hex.length() != 3 && hex.length() != 6) || !isHex(hex)) { + throw new IllegalArgumentException("无效的颜色格式: " + hexColor); + } int r, g, b; if (hex.length() == 3) { // #RGB 简写格式,将每个字符扩展为两位(如 F -> FF) @@ -34,20 +54,35 @@ public class echo { g = Integer.parseInt(hex.substring(2, 4), 16); b = Integer.parseInt(hex.substring(4, 6), 16); } - System.out.println("\033[38;2;" + r + ";" + g + ";" + b + "m" + Message + "\033[0m"); - } else { - // 原有 ANSI 颜色代码直接拼接 - System.out.println("\033[" + HexColor + "m" + Message + "\033[0m"); + return "38;2;" + r + ";" + g + ";" + b; } + // 仅允许数字与分号,防止向终端注入任意转义序列 + if (!hexColor.matches("[0-9]+(;[0-9]+)*")) { + throw new IllegalArgumentException("无效的ANSI颜色码: " + hexColor); + } + return hexColor; + } + + private static boolean isHex(String s) { + for (int i = 0; i < s.length(); i++) { + if (Character.digit(s.charAt(i), 16) < 0) { + return false; + } + } + return true; } /** * 使用预定义的颜色名称在终端打印彩色文本 * * @param Message 要打印的文本内容 - * @param ColorName 颜色名称,支持:RED、GREEN、YELLOW、BLUE、PURPLE、CYAN;传入不支持的名称则按默认颜色输出 + * @param ColorName 颜色名称,支持:RED、GREEN、YELLOW、BLUE、PURPLE、CYAN;传入 null 则按默认颜色输出 */ public static void ColorPrintln(String Message, ColorNameEnum ColorName) { + if (ColorName == null) { + System.out.println(Message); + return; + } // 根据颜色名称映射对应的 ANSI 颜色转义序列 switch (ColorName) { case RED: @@ -76,7 +111,7 @@ public class echo { /** * 在单行内打印多种颜色的文字 - * + * * 颜色代码仿照Minecraft格式,如: * &4(红色)、&c(浅红色)、 * &6(橙色)、&e(黄色)、 @@ -89,9 +124,12 @@ public class echo { * &l(加粗)、&n(下划线)、 * &o(斜体)、&k(乱码)、 * &m(删除线)、&r(重置)。 - * + * + * 颜色码会重置之前的格式;加粗/下划线/斜体/删除线等格式码互相叠加; + * 例如 &4&l 得到"粗体红色",&n&9 得到"下划线蓝色"。 + * * 示例:&6Welcome &2to &4Project - * + * * @param Message 包含Minecraft格式代码的文本 */ public static void ColorPrintlnPlus(String Message) { @@ -100,7 +138,9 @@ public class echo { return; } - StringBuilder result = new StringBuilder(); + StringBuilder result = new StringBuilder(Message.length() + 32); + // 当前叠加的格式序列(加粗/下划线等),颜色码切换后需要重新应用 + StringBuilder activeFormat = new StringBuilder(); boolean obfuscated = false; int i = 0; @@ -110,11 +150,26 @@ public class echo { char code = Character.toLowerCase(Message.charAt(i + 1)); String ansi = getAnsiCode(code); if (ansi != null) { - // 先重置,再应用新格式(避免格式叠加冲突) - result.append("\033[0m"); - result.append(ansi); - // &k 开启乱码模式,其他颜色/格式码关闭乱码模式 - obfuscated = (code == 'k'); + if (isColorCode(code)) { + // 颜色码:重置后应用新颜色,再恢复叠加的格式 + result.append("\033[0m").append(ansi).append(activeFormat); + obfuscated = false; + } else if (code == 'r') { + // 重置:清空颜色与全部格式 + result.append("\033[0m"); + activeFormat.setLength(0); + obfuscated = false; + } else if (code == 'k') { + // 乱码模式开启 + activeFormat.append(ansi); + result.append(ansi); + obfuscated = true; + } else { + // 加粗/下划线/斜体/删除线:格式叠加生效 + activeFormat.append(ansi); + result.append(ansi); + obfuscated = false; + } i += 2; continue; } @@ -136,6 +191,13 @@ public class echo { System.out.println(result.toString()); } + /** + * 是否为 Minecraft 颜色码(0-9、a-f) + */ + private static boolean isColorCode(char code) { + return (code >= '0' && code <= '9') || (code >= 'a' && code <= 'f'); + } + /** * 根据 Minecraft 格式代码字符返回对应的 ANSI 转义序列 * diff --git a/src/main/java/top/gtb520/java/gnu/java/utils/library/utils/processing/hwid/getHWID.java b/src/main/java/top/gtb520/java/gnu/java/utils/library/utils/processing/hwid/getHWID.java index 8475d23..75133e7 100644 --- a/src/main/java/top/gtb520/java/gnu/java/utils/library/utils/processing/hwid/getHWID.java +++ b/src/main/java/top/gtb520/java/gnu/java/utils/library/utils/processing/hwid/getHWID.java @@ -1,16 +1,30 @@ package top.gtb520.java.gnu.java.utils.library.utils.processing.hwid; import java.io.BufferedReader; -import java.io.FileReader; +import java.io.IOException; +import java.io.InputStream; import java.io.InputStreamReader; import java.net.NetworkInterface; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import java.security.MessageDigest; import java.util.ArrayList; import java.util.Enumeration; import java.util.List; +import java.util.concurrent.TimeUnit; +/** + * 硬件标识符(HWID)生成器。 + * + *

组合 CPU、主板、MAC 地址、硬盘序列号与操作系统信息,通过 SHA-256 生成 64 位十六进制标识。 + * 所有外部命令通过 {@link ProcessBuilder} 执行,带超时并并发排空输出流,避免挂起。 + * 部分硬件信息获取失败时自动降级(跳过该组件),全部失败时返回 {@code "UNKNOWN_HWID"}。

+ */ public class getHWID { private final static boolean Debug = false; + private static final long COMMAND_TIMEOUT_SECONDS = 10; /** * 生成硬件标识符(HWID) @@ -20,48 +34,23 @@ public class getHWID { try { List hardwareInfo = new ArrayList<>(); - // 1. 获取CPU信息 - String cpuInfo = getCPUInfo(); - if (cpuInfo != null && !cpuInfo.isEmpty()) { - hardwareInfo.add("CPU:" + cpuInfo); - } + addIfPresent(hardwareInfo, "CPU", getCPUInfo()); + addIfPresent(hardwareInfo, "MB", getMotherboardInfo()); + addIfPresent(hardwareInfo, "MAC", getMacAddress()); + addIfPresent(hardwareInfo, "DISK", getDiskSerialNumber()); - // 2. 获取主板序列号 - String motherboardInfo = getMotherboardInfo(); - if (motherboardInfo != null && !motherboardInfo.isEmpty()) { - hardwareInfo.add("MB:" + motherboardInfo); - } - - // 3. 获取MAC地址 - String macAddress = getMacAddress(); - if (macAddress != null && !macAddress.isEmpty()) { - hardwareInfo.add("MAC:" + macAddress); - } - - // 4. 获取硬盘序列号 - String diskInfo = getDiskSerialNumber(); - if (diskInfo != null && !diskInfo.isEmpty()) { - hardwareInfo.add("DISK:" + diskInfo); - } - - // 5. 获取操作系统信息 - String osInfo = System.getProperty("os.name") + "_" + - System.getProperty("os.version"); + String osInfo = System.getProperty("os.name") + "_" + System.getProperty("os.version"); hardwareInfo.add("OS:" + osInfo); - // 6. 组合所有硬件信息并生成哈希 StringBuilder combined = new StringBuilder(); for (String info : hardwareInfo) { if (Debug) { System.out.println("info: " + info); - System.out.println("hardwareInfo: " + hardwareInfo); - System.out.println("combined: " + combined); } combined.append(info).append("|"); } - if (Debug) { - System.out.println("HWID combined: " + combined.toString()); + System.out.println("HWID combined: " + combined); } // 使用SHA-256生成固定长度的哈希值 @@ -73,12 +62,18 @@ public class getHWID { } } + private static void addIfPresent(List list, String label, String value) { + if (value != null && !value.isEmpty()) { + list.add(label + ":" + value); + } + } + /** * 检测是否为Android系统 */ private static boolean isAndroid() { try { - String osName = System.getProperty("os.name").toLowerCase(); + String osName = System.getProperty("os.name", "").toLowerCase(); String javaVmName = System.getProperty("java.vm.name", "").toLowerCase(); String javaSpecVendor = System.getProperty("java.specification.vendor", "").toLowerCase(); @@ -86,7 +81,7 @@ public class getHWID { return osName.contains("android") || javaVmName.contains("android") || javaSpecVendor.contains("android") || - new java.io.File("/system/bin/app_process").exists(); + Files.exists(Path.of("/system/bin/app_process")); } catch (Exception e) { return false; } @@ -96,55 +91,43 @@ public class getHWID { * 获取CPU信息 */ private static String getCPUInfo() { - try { - // Android系统特殊处理 - if (isAndroid()) { - return getAndroidCPUInfo(); + // Android系统特殊处理 + if (isAndroid()) { + return getAndroidCPUInfo(); + } + + String os = System.getProperty("os.name", "").toLowerCase(); + if (os.contains("win")) { + // Windows:优先使用 PowerShell CIM(wmic 在较新 Windows 中已被移除) + String id = firstLine(runCommand(List.of( + "powershell", "-NoProfile", "-Command", + "Get-CimInstance Win32_Processor | Select-Object -ExpandProperty ProcessorId"))); + if (id == null || id.isEmpty()) { + id = firstNonHeaderLine(runCommand(List.of("wmic", "cpu", "get", "ProcessorId")), "ProcessorId"); } + return id; + } else if (os.contains("mac")) { + return firstLine(runCommand(List.of("/usr/sbin/sysctl", "-n", "machdep.cpu.brand_string"))); + } else { + // Linux:直接读取 /proc/cpuinfo,避免外部进程 + return readLinuxCpuInfo(); + } + } - String os = System.getProperty("os.name").toLowerCase(); - Process process; - - if (os.contains("win")) { - // Windows: 获取ProcessorId - process = Runtime.getRuntime().exec( - new String[]{"wmic", "cpu", "get", "ProcessorId"} - ); - } else if (os.contains("mac")) { - // macOS - process = Runtime.getRuntime().exec( - new String[]{"/usr/sbin/sysctl", "-n", "machdep.cpu.brand_string"} - ); - } else { - // Linux: 获取CPU型号 - process = Runtime.getRuntime().exec( - new String[]{"cat", "/proc/cpuinfo"} - ); - } - - process.waitFor(); - BufferedReader reader = new BufferedReader( - new InputStreamReader(process.getInputStream()) - ); - + private static String readLinuxCpuInfo() { + try (BufferedReader reader = Files.newBufferedReader(Path.of("/proc/cpuinfo"), StandardCharsets.UTF_8)) { String line; - StringBuilder result = new StringBuilder(); - while ((line = reader.readLine()) != null) { line = line.trim(); - if (!line.isEmpty()) { - if (os.contains("win") && !line.equals("ProcessorId")) { - return line; - } else if (os.contains("mac")) { - return line; - } else if (line.startsWith("model name")) { - return line.split(":", 2)[1].trim(); + if (line.startsWith("model name")) { + String[] parts = line.split(":", 2); + if (parts.length > 1) { + return parts[1].trim(); } } } - - } catch (Exception e) { - // 静默失败,返回null + } catch (IOException ignored) { + // 读取失败,返回 null } return null; } @@ -153,210 +136,156 @@ public class getHWID { * 获取Android CPU信息 */ private static String getAndroidCPUInfo() { - StringBuilder cpuInfo = new StringBuilder(); - - try { - // 方法1: 读取 /proc/cpuinfo - BufferedReader reader = new BufferedReader(new FileReader("/proc/cpuinfo")); + List infoList = new ArrayList<>(); + try (BufferedReader reader = Files.newBufferedReader(Path.of("/proc/cpuinfo"), StandardCharsets.UTF_8)) { String line; - List infoList = new ArrayList<>(); - while ((line = reader.readLine()) != null) { line = line.trim(); - // 提取关键信息 - if (line.startsWith("Processor") || line.startsWith("model name")) { + if (line.startsWith("Processor") || line.startsWith("model name") + || line.startsWith("Hardware") || line.startsWith("CPU implementer") + || line.startsWith("CPU architecture") || line.startsWith("CPU variant") + || line.startsWith("CPU part")) { String[] parts = line.split(":", 2); if (parts.length > 1) { - infoList.add("Processor:" + parts[1].trim()); - } - } else if (line.startsWith("Hardware")) { - String[] parts = line.split(":", 2); - if (parts.length > 1) { - infoList.add("Hardware:" + parts[1].trim()); - } - } else if (line.startsWith("CPU implementer")) { - String[] parts = line.split(":", 2); - if (parts.length > 1) { - infoList.add("Implementer:" + parts[1].trim()); - } - } else if (line.startsWith("CPU architecture")) { - String[] parts = line.split(":", 2); - if (parts.length > 1) { - infoList.add("Architecture:" + parts[1].trim()); - } - } else if (line.startsWith("CPU variant")) { - String[] parts = line.split(":", 2); - if (parts.length > 1) { - infoList.add("Variant:" + parts[1].trim()); - } - } else if (line.startsWith("CPU part")) { - String[] parts = line.split(":", 2); - if (parts.length > 1) { - infoList.add("Part:" + parts[1].trim()); + infoList.add(parts[0].trim() + ":" + parts[1].trim()); } } } - reader.close(); - - if (!infoList.isEmpty()) { - return String.join("|", infoList); - } - - } catch (Exception e) { + } catch (IOException ignored) { // 静默失败 } - // 方法2: 如果读取文件失败,使用System.getProperty - try { - String abi = System.getProperty("os.arch", "unknown"); - String cores = Runtime.getRuntime().availableProcessors() + ""; - return "ABI:" + abi + "|Cores:" + cores; - } catch (Exception e) { - return null; + if (!infoList.isEmpty()) { + return String.join("|", infoList); } + + // 方法2: 如果读取文件失败,使用系统属性 + String abi = System.getProperty("os.arch", "unknown"); + String cores = Runtime.getRuntime().availableProcessors() + ""; + return "ABI:" + abi + "|Cores:" + cores; } /** * 获取主板信息 */ private static String getMotherboardInfo() { - try { - // Android系统特殊处理 - if (isAndroid()) { - return getAndroidMotherboardInfo(); + // Android系统特殊处理 + if (isAndroid()) { + return getAndroidMotherboardInfo(); + } + + String os = System.getProperty("os.name", "").toLowerCase(); + if (os.contains("win")) { + String sn = firstLine(runCommand(List.of( + "powershell", "-NoProfile", "-Command", + "Get-CimInstance Win32_BaseBoard | Select-Object -ExpandProperty SerialNumber"))); + if (sn == null || sn.isEmpty()) { + sn = firstNonHeaderLine(runCommand(List.of("wmic", "baseboard", "get", "SerialNumber")), "SerialNumber"); } - - String os = System.getProperty("os.name").toLowerCase(); - - if (os.contains("win")) { - Process process = Runtime.getRuntime().exec( - new String[]{"wmic", "baseboard", "get", "SerialNumber"} - ); - process.waitFor(); - BufferedReader reader = new BufferedReader( - new InputStreamReader(process.getInputStream()) - ); - - String line; - while ((line = reader.readLine()) != null) { - line = line.trim(); - if (!line.isEmpty() && !line.equals("SerialNumber")) { - return line; - } - } - } - - } catch (Exception e) { - // 静默失败 + return sn; } return null; } /** * 获取Android主板信息 + * + *

注意:Android 系统属性 ro.product.* 无法通过 System.getProperty 获取, + * 需要 android.os.Build;此处仅作为尽力而为的尝试。

*/ private static String getAndroidMotherboardInfo() { - try { - List infoList = new ArrayList<>(); + List infoList = new ArrayList<>(); - // 读取Android设备特性 - String brand = System.getProperty("ro.product.brand", ""); - String manufacturer = System.getProperty("ro.product.manufacturer", ""); - String model = System.getProperty("ro.product.model", ""); - String device = System.getProperty("ro.product.device", ""); - String board = System.getProperty("ro.product.board", ""); + String brand = System.getProperty("ro.product.brand", ""); + String manufacturer = System.getProperty("ro.product.manufacturer", ""); + String model = System.getProperty("ro.product.model", ""); + String device = System.getProperty("ro.product.device", ""); + String board = System.getProperty("ro.product.board", ""); - if (!brand.isEmpty()) infoList.add("Brand:" + brand); - if (!manufacturer.isEmpty()) infoList.add("Manufacturer:" + manufacturer); - if (!model.isEmpty()) infoList.add("Model:" + model); - if (!device.isEmpty()) infoList.add("Device:" + device); - if (!board.isEmpty()) infoList.add("Board:" + board); + if (!brand.isEmpty()) infoList.add("Brand:" + brand); + if (!manufacturer.isEmpty()) infoList.add("Manufacturer:" + manufacturer); + if (!model.isEmpty()) infoList.add("Model:" + model); + if (!device.isEmpty()) infoList.add("Device:" + device); + if (!board.isEmpty()) infoList.add("Board:" + board); - if (!infoList.isEmpty()) { - return String.join("|", infoList); - } - - } catch (Exception e) { - // 静默失败 - } - return null; + return infoList.isEmpty() ? null : String.join("|", infoList); } /** * 获取MAC地址 + * 优先返回非回环、非虚拟网卡的 MAC,避免虚拟网卡导致 HWID 漂移;全部为虚拟网卡时回退到第一个可用 MAC。 */ private static String getMacAddress() { try { - Enumeration networkInterfaces = - NetworkInterface.getNetworkInterfaces(); - + Enumeration networkInterfaces = NetworkInterface.getNetworkInterfaces(); + if (networkInterfaces == null) { + return null; + } + String fallback = null; while (networkInterfaces.hasMoreElements()) { NetworkInterface network = networkInterfaces.nextElement(); - byte[] mac = network.getHardwareAddress(); - - if (mac != null && mac.length > 0) { - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < mac.length; i++) { - sb.append(String.format("%02X%s", mac[i], - (i < mac.length - 1) ? "-" : "")); + try { + byte[] mac = network.getHardwareAddress(); + if (mac == null || mac.length == 0) { + continue; } - return sb.toString(); + String formatted = formatMac(mac); + if (fallback == null) { + fallback = formatted; + } + if (network.isLoopback() || network.isVirtual()) { + continue; + } + return formatted; + } catch (IOException ignored) { + // 该网卡信息不可用,继续尝试下一个 } } - + return fallback; } catch (Exception e) { - // 静默失败 + return null; } - return null; + } + + private static String formatMac(byte[] mac) { + StringBuilder sb = new StringBuilder(mac.length * 3); + for (int i = 0; i < mac.length; i++) { + sb.append(String.format("%02X%s", mac[i], (i < mac.length - 1) ? "-" : "")); + } + return sb.toString(); } /** * 获取硬盘序列号 */ private static String getDiskSerialNumber() { - try { - // Android系统特殊处理 - if (isAndroid()) { - return getAndroidStorageInfo(); + // Android系统特殊处理 + if (isAndroid()) { + return getAndroidStorageInfo(); + } + + String os = System.getProperty("os.name", "").toLowerCase(); + if (os.contains("win")) { + String sn = firstLine(runCommand(List.of( + "powershell", "-NoProfile", "-Command", + "Get-CimInstance Win32_DiskDrive | Select-Object -ExpandProperty SerialNumber"))); + if (sn == null || sn.isEmpty()) { + sn = firstNonHeaderLine(runCommand(List.of("wmic", "diskdrive", "get", "SerialNumber")), "SerialNumber"); } - - String os = System.getProperty("os.name").toLowerCase(); - - if (os.contains("win")) { - Process process = Runtime.getRuntime().exec( - new String[]{"wmic", "diskdrive", "get", "SerialNumber"} - ); - process.waitFor(); - BufferedReader reader = new BufferedReader( - new InputStreamReader(process.getInputStream()) - ); - - String line; - while ((line = reader.readLine()) != null) { - line = line.trim(); - if (!line.isEmpty() && !line.equals("SerialNumber")) { - return line; - } - } - } else if (os.contains("mac") || os.contains("linux")) { - Process process = Runtime.getRuntime().exec( - new String[]{"sh", "-c", "system_profiler SPSerialATADataType | grep 'Serial Number'"} - ); - process.waitFor(); - BufferedReader reader = new BufferedReader( - new InputStreamReader(process.getInputStream()) - ); - - String line; - if ((line = reader.readLine()) != null) { + return sn; + } else if (os.contains("mac")) { + String out = runCommand(List.of( + "sh", "-c", "system_profiler SPSerialATADataType | grep 'Serial Number'")); + if (out != null) { + for (String line : out.split("\\R")) { if (line.contains(":")) { return line.split(":", 2)[1].trim(); } } } - - } catch (Exception e) { - // 静默失败 + } else if (os.contains("linux")) { + // Linux:lsblk 输出物理磁盘序列号 + return firstLine(runCommand(List.of("lsblk", "-dno", "serial"))); } return null; } @@ -365,36 +294,110 @@ public class getHWID { * 获取Android存储信息 */ private static String getAndroidStorageInfo() { + List infoList = new ArrayList<>(); + + String internalStorage = System.getProperty("user.dir", ""); + if (!internalStorage.isEmpty()) { + infoList.add("InternalDir:" + internalStorage); + } + + infoList.add(Files.exists(Path.of("/sdcard")) ? "ExternalStorage:available" : "ExternalStorage:not_available"); + if (Files.exists(Path.of("/data"))) { + infoList.add("DataDir:exists"); + } + + return infoList.isEmpty() ? null : String.join("|", infoList); + } + + /** + * 执行外部命令:带超时、并发排空 stdout/stderr,超时强制终止。 + * + * @param command 命令与参数 + * @return 命令 stdout 内容(trim 后);启动失败或超时返回 null + */ + private static String runCommand(List command) { + ProcessBuilder pb = new ProcessBuilder(command); + Process process; try { - // 读取 /proc/mounts 或使用系统属性 - List infoList = new ArrayList<>(); + process = pb.start(); + } catch (IOException e) { + return null; + } - // 尝试获取存储相关信息 - String internalStorage = System.getProperty("user.dir", ""); - if (!internalStorage.isEmpty()) { - infoList.add("InternalDir:" + internalStorage); + // 并发排空 stderr,避免进程因管道写满而阻塞 + Thread stderrDrain = new Thread(() -> drain(process.getErrorStream()), "hwid-stderr-drain"); + stderrDrain.setDaemon(true); + stderrDrain.start(); + + StringBuilder stdout = new StringBuilder(); + try { + drainInto(process.getInputStream(), stdout); + if (!process.waitFor(COMMAND_TIMEOUT_SECONDS, TimeUnit.SECONDS)) { + process.destroyForcibly(); + return null; } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + process.destroyForcibly(); + return null; + } + return stdout.toString().trim(); + } - // 检查外部存储是否可用 - java.io.File externalStorage = new java.io.File("/sdcard"); - if (externalStorage.exists()) { - infoList.add("ExternalStorage:available"); - } else { - infoList.add("ExternalStorage:not_available"); + /** + * 排空输入流(丢弃内容),用于 stderr。 + */ + private static void drain(InputStream in) { + try (InputStream ignored = in) { + while (in.read() != -1) { + // 丢弃 } + } catch (IOException ignored) { + // 忽略 + } + } - // 获取数据目录 - java.io.File dataDir = new java.io.File("/data"); - if (dataDir.exists()) { - infoList.add("DataDir:exists"); + /** + * 将输入流按行读取到 StringBuilder。 + */ + private static void drainInto(InputStream in, StringBuilder out) { + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(in, Charset.defaultCharset()))) { + String line; + while ((line = reader.readLine()) != null) { + if (out.length() > 0) { + out.append(System.lineSeparator()); + } + out.append(line); } + } catch (IOException ignored) { + // 忽略 + } + } - if (!infoList.isEmpty()) { - return String.join("|", infoList); + /** + * 取输出中的第一行(trim 后);输出为空时返回 null。 + */ + private static String firstLine(String text) { + if (text == null || text.isEmpty()) { + return null; + } + int idx = text.indexOf('\n'); + return (idx < 0 ? text : text.substring(0, idx)).trim(); + } + + /** + * 取输出中第一个非空且不等于表头的行(wmic 输出首行是列名)。 + */ + private static String firstNonHeaderLine(String text, String header) { + if (text == null) { + return null; + } + for (String line : text.split("\\R")) { + String trimmed = line.trim(); + if (!trimmed.isEmpty() && !trimmed.equals(header)) { + return trimmed; } - - } catch (Exception e) { - // 静默失败 } return null; } @@ -405,8 +408,8 @@ public class getHWID { private static String generateHash(String input) { try { MessageDigest digest = MessageDigest.getInstance("SHA-256"); - byte[] hash = digest.digest(input.getBytes("UTF-8")); - StringBuilder hexString = new StringBuilder(); + byte[] hash = digest.digest(input.getBytes(StandardCharsets.UTF_8)); + StringBuilder hexString = new StringBuilder(hash.length * 2); for (byte b : hash) { String hex = Integer.toHexString(0xff & b); @@ -415,12 +418,9 @@ public class getHWID { } hexString.append(hex); } - return hexString.toString(); - } catch (Exception e) { - e.printStackTrace(); - return input.hashCode() + ""; + throw new IllegalStateException("SHA-256 摘要不可用", e); } } } diff --git a/src/main/java/top/gtb520/java/gnu/java/utils/library/utils/processing/random/defaults.java b/src/main/java/top/gtb520/java/gnu/java/utils/library/utils/processing/random/defaults.java index c21c073..44b489d 100644 --- a/src/main/java/top/gtb520/java/gnu/java/utils/library/utils/processing/random/defaults.java +++ b/src/main/java/top/gtb520/java/gnu/java/utils/library/utils/processing/random/defaults.java @@ -3,46 +3,42 @@ package top.gtb520.java.gnu.java.utils.library.utils.processing.random; import java.util.concurrent.ThreadLocalRandom; public class defaults { + + private static final String ALL_CHARACTERS = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + /** - * 根据输入的范围生成随机数 + * 根据输入的范围生成随机数(包含两端)。 + * * @param start_scope 起始范围 - * @param end_scope 结束范围 + * @param end_scope 结束范围 * @return 随机数 + * @throws IllegalArgumentException 当起始值大于结束值时 */ public static int random(int start_scope, int end_scope) { - int random_double; - - // 处理边界,确保起始范围小于等于结束范围 if (start_scope > end_scope) { - System.out.println("起始范围不能大于结束范围"); - throw new IllegalArgumentException("起始范围不能大于结束范围"); + throw new IllegalArgumentException("起始范围不能大于结束范围: " + start_scope + " > " + end_scope); } - // 生成随机数 - random_double = ThreadLocalRandom.current().nextInt(start_scope, end_scope + 1); - - return random_double; + // 使用 long 上界,避免 end_scope == Integer.MAX_VALUE 时 +1 溢出为负数 + return (int) ThreadLocalRandom.current().nextLong(start_scope, (long) end_scope + 1); } /** - * 生成指定长度的随机字符串 + * 生成指定长度的随机字符串(大小写字母 + 数字)。 + * * @param length 字符串长度 * @return 随机字符串 + * @throws IllegalArgumentException 当长度为负数时 */ public static String randomString(int length) { - String alphabetsInUpperCase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; - String alphabetsInLowerCase = "abcdefghijklmnopqrstuvwxyz"; - String numbers = "0123456789"; - // create a super set of all characters - String allCharacters = alphabetsInLowerCase + alphabetsInUpperCase + numbers; - // initialize a string to hold result - StringBuffer randomString = new StringBuffer(); - // loop for 10 times - for (int i = 0; i < length; i++) { - // generate a random number between 0 and length of all characters - int randomIndex = (int)(Math.random() * allCharacters.length()); - // retrieve character at index and add it to result - randomString.append(allCharacters.charAt(randomIndex)); + if (length < 0) { + throw new IllegalArgumentException("字符串长度不能为负数: " + length); } - return randomString.toString(); + ThreadLocalRandom random = ThreadLocalRandom.current(); + StringBuilder result = new StringBuilder(length); + for (int i = 0; i < length; i++) { + result.append(ALL_CHARACTERS.charAt(random.nextInt(ALL_CHARACTERS.length()))); + } + return result.toString(); } } diff --git a/src/main/java/top/gtb520/java/gnu/java/utils/library/utils/runtime/RunEnv.java b/src/main/java/top/gtb520/java/gnu/java/utils/library/utils/runtime/RunEnv.java index 214a9d1..aa8f5bc 100644 --- a/src/main/java/top/gtb520/java/gnu/java/utils/library/utils/runtime/RunEnv.java +++ b/src/main/java/top/gtb520/java/gnu/java/utils/library/utils/runtime/RunEnv.java @@ -2,19 +2,40 @@ package top.gtb520.java.gnu.java.utils.library.utils.runtime; import top.gtb520.java.gnu.java.utils.library.main; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; // 获取运行时的信息 public class RunEnv { - // 获取当前运行的JAR文件路径 + + private static final String TEMP_DIR_PREFIX = "gjul-"; + + /** + * 获取当前运行的 JAR 文件路径。 + * 非 JAR 运行(IDE/类路径)时返回类路径目录;解析失败时回退到当前工作目录。 + */ public static String getJarPath() { - String path = main.class.getProtectionDomain().getCodeSource().getLocation().getPath(); - if (System.getProperty("os.name").startsWith("Windows")) { - path = path.replace('/', '\\'); - //去除路径最前面的 "\\" - path = path.substring(1); + try { + java.net.URL location = main.class.getProtectionDomain().getCodeSource().getLocation(); + if (location == null) { + return Paths.get("").toAbsolutePath().toString(); + } + // 通过 URI 解析路径,自动处理 %20 等 URL 编码 + return Paths.get(location.toURI()).toString(); + } catch (URISyntaxException | IllegalArgumentException e) { + try { + String path = main.class.getProtectionDomain().getCodeSource().getLocation().getPath(); + if (path != null && !path.isEmpty()) { + return path; + } + } catch (Exception ignored) { + // 忽略,走最终回退 + } + return Paths.get("").toAbsolutePath().toString(); } - return path; } // 获取临时目录 @@ -22,12 +43,17 @@ public class RunEnv { return System.getProperty("java.io.tmpdir"); } - // 新建临时目录,失败时返回 null + /** + * 在系统临时目录下新建唯一临时子目录,返回其路径。 + */ public static String newTempDir() { return createTempDir(Path.of(getTempDir())); } - // 在 当前运行的JAR文件路径 下 新建临时目录,失败时返回 null + /** + * 在当前运行的 JAR 文件所在目录下新建唯一临时子目录,返回其路径; + * JAR 路径无父目录时返回 null。 + */ public static String newJarThisTempDir() { Path parent = Path.of(getJarPath()).getParent(); if (parent == null) { @@ -36,17 +62,14 @@ public class RunEnv { return createTempDir(parent); } - // 在指定目录下创建以时间戳命名的临时目录,返回目录路径;目录已存在时返回 null + /** + * 在指定目录下创建唯一的临时子目录,返回目录路径;创建失败抛出 RuntimeException。 + */ private static String createTempDir(Path parent) { - Path tempDir = parent.resolve(String.valueOf(System.currentTimeMillis())); - if (!tempDir.toFile().exists()) { - try { - tempDir.toFile().mkdirs(); - } catch (Exception e) { - throw new RuntimeException(e); - } - return tempDir.toString(); + try { + return Files.createTempDirectory(parent, TEMP_DIR_PREFIX).toString(); + } catch (IOException e) { + throw new RuntimeException("在 " + parent + " 下创建临时目录失败", e); } - return null; } } diff --git a/src/main/java/top/gtb520/java/gnu/java/utils/library/version.java b/src/main/java/top/gtb520/java/gnu/java/utils/library/version.java index 6de5b89..22512fb 100644 --- a/src/main/java/top/gtb520/java/gnu/java/utils/library/version.java +++ b/src/main/java/top/gtb520/java/gnu/java/utils/library/version.java @@ -2,14 +2,18 @@ package top.gtb520.java.gnu.java.utils.library; public class version { // 项目的版本信息,不代表实时运行环境 - public static final String VERSION = "1.5"; + // 与 pom.xml 保持一致;构建 JAR 时优先读取 MANIFEST 的 Implementation-Version + public static final String VERSION = "1.6"; public static final String JAVA_VERSION = "25"; public static final String OpenJDK_VERSION = "25.0.0.0.1"; public static final String OpenJDK_Runtime_Environment = "25.0.0.0.1+36-GA"; public static final String OpenJDK_64Bit_Server_VM = "25.0.0.0.1+36-GA"; public String GetVersion() { - return VERSION; + // 打包为 JAR 时读取 MANIFEST 中的 Implementation-Version,类路径运行(如测试)时回退到常量 + Package pkg = version.class.getPackage(); + String implementationVersion = (pkg == null) ? null : pkg.getImplementationVersion(); + return (implementationVersion == null || implementationVersion.isEmpty()) ? VERSION : implementationVersion; } public String GetJavaVersion() { return JAVA_VERSION; diff --git a/src/test/java/ColorPrintlnPlusTest2.java b/src/test/java/ColorPrintlnPlusTest2.java deleted file mode 100644 index 2275d5d..0000000 --- a/src/test/java/ColorPrintlnPlusTest2.java +++ /dev/null @@ -1,7 +0,0 @@ -import top.gtb520.java.gnu.java.utils.library.utils.print.echo; - -public class ColorPrintlnPlusTest2 { - public static void main() { - echo.ColorPrintlnPlus("&dHello &2World&6!"); - } -} diff --git a/src/test/java/EncryptionAndDecryptionTest.java b/src/test/java/EncryptionAndDecryptionTest.java deleted file mode 100644 index bed7479..0000000 --- a/src/test/java/EncryptionAndDecryptionTest.java +++ /dev/null @@ -1,206 +0,0 @@ -import java.io.File; -import java.nio.file.Files; -import java.nio.file.Path; - -import top.gtb520.java.gnu.java.utils.library.utils.file.EncryptionAndDecryption; -import top.gtb520.java.gnu.java.utils.library.utils.file.EncryptionAndDecryption.EncryptionType; - -public class EncryptionAndDecryptionTest { - - static int passCount = 0; - static int failCount = 0; - - public static void main(String[] args) throws Exception { - System.out.println("========== EncryptionAndDecryption 测试开始 =========="); - - // 正常场景:AES 加密/解密 - testEncryptDecryptAES(); - // 正常场景:DES 加密/解密 - testEncryptDecryptDES(); - // 正常场景:RSA 加密/解密 - testEncryptDecryptRSA(); - // 正常场景:验证密钥文件生成 - testKeyFileGenerated(); - // 正常场景:加密后文件内容发生变化 - testFileContentChangedAfterEncrypt(); - - // 异常场景:file 为 null - testNullFile(); - // 异常场景:keyPath 为 null - testNullKeyPath(); - // 异常场景:keyfile 为 null(解密) - testNullKeyFile(); - // 异常场景:文件不存在 - testFileNotExist(); - // 异常场景:密钥文件不存在(解密) - testKeyFileNotExist(); - - System.out.println("========== 测试结果 =========="); - System.out.println("通过: " + passCount); - System.out.println("失败: " + failCount); - System.out.println("总计: " + (passCount + failCount)); - - if (failCount > 0) { - System.out.println("存在失败的测试用例!"); - System.exit(1); - } else { - System.out.println("全部测试通过!"); - } - } - - /** - * 创建临时测试文件并写入测试内容 - */ - static File createTempFile(String prefix) throws Exception { - Path tempDir = Files.createTempDirectory("enc_test_"); - File tempFile = tempDir.resolve(prefix + ".txt").toFile(); - Files.writeString(tempFile.toPath(), "Hello, EncryptionAndDecryption Test! 测试内容123"); - return tempFile; - } - - /** - * 创建临时密钥目录 - */ - static Path createTempKeyPath() throws Exception { - return Files.createTempDirectory("enc_key_"); - } - - static void assertEquals(Object expected, Object actual, String testName) { - if (expected == null && actual == null) { - passCount++; - System.out.println("[PASS] " + testName); - } else if (expected != null && expected.equals(actual)) { - passCount++; - System.out.println("[PASS] " + testName); - } else { - failCount++; - System.out.println("[FAIL] " + testName + " | 期望: " + expected + ", 实际: " + actual); - } - } - - static void assertTrue(boolean condition, String testName) { - if (condition) { - passCount++; - System.out.println("[PASS] " + testName); - } else { - failCount++; - System.out.println("[FAIL] " + testName); - } - } - - // ==================== 正常场景 ==================== - - static void testEncryptDecryptAES() throws Exception { - String testName = "AES 加密后解密,内容还原"; - File file = createTempFile("aes_test"); - Path keyPath = createTempKeyPath(); - String originalContent = Files.readString(file.toPath()); - - File encrypted = EncryptionAndDecryption.EncryptionFile(file, EncryptionType.AES, keyPath); - assertTrue(encrypted != null, testName + " - 加密返回非null"); - - File keyFile = keyPath.resolve(file.getName() + ".key").toFile(); - File decrypted = EncryptionAndDecryption.DecryptionFile(file, EncryptionType.AES, keyFile); - assertTrue(decrypted != null, testName + " - 解密返回非null"); - - String decryptedContent = Files.readString(file.toPath()); - assertEquals(originalContent, decryptedContent, testName); - } - - static void testEncryptDecryptDES() throws Exception { - String testName = "DES 加密后解密,内容还原"; - File file = createTempFile("des_test"); - Path keyPath = createTempKeyPath(); - String originalContent = Files.readString(file.toPath()); - - File encrypted = EncryptionAndDecryption.EncryptionFile(file, EncryptionType.DES, keyPath); - assertTrue(encrypted != null, testName + " - 加密返回非null"); - - File keyFile = keyPath.resolve(file.getName() + ".key").toFile(); - File decrypted = EncryptionAndDecryption.DecryptionFile(file, EncryptionType.DES, keyFile); - assertTrue(decrypted != null, testName + " - 解密返回非null"); - - String decryptedContent = Files.readString(file.toPath()); - assertEquals(originalContent, decryptedContent, testName); - } - - static void testEncryptDecryptRSA() throws Exception { - String testName = "RSA 加密后解密,内容还原"; - File file = createTempFile("rsa_test"); - Path keyPath = createTempKeyPath(); - String originalContent = Files.readString(file.toPath()); - - File encrypted = EncryptionAndDecryption.EncryptionFile(file, EncryptionType.RSA, keyPath); - assertTrue(encrypted != null, testName + " - 加密返回非null"); - - File keyFile = keyPath.resolve(file.getName() + ".key").toFile(); - File decrypted = EncryptionAndDecryption.DecryptionFile(file, EncryptionType.RSA, keyFile); - assertTrue(decrypted != null, testName + " - 解密返回非null"); - - String decryptedContent = Files.readString(file.toPath()); - assertEquals(originalContent, decryptedContent, testName); - } - - static void testKeyFileGenerated() throws Exception { - String testName = "加密后密钥文件正确生成"; - File file = createTempFile("key_test"); - Path keyPath = createTempKeyPath(); - - EncryptionAndDecryption.EncryptionFile(file, EncryptionType.AES, keyPath); - - File keyFile = keyPath.resolve(file.getName() + ".key").toFile(); - assertTrue(keyFile.exists(), testName + " - 密钥文件存在"); - assertTrue(keyFile.length() > 0, testName + " - 密钥文件非空"); - } - - static void testFileContentChangedAfterEncrypt() throws Exception { - String testName = "加密后文件内容与原文不同"; - File file = createTempFile("content_test"); - Path keyPath = createTempKeyPath(); - byte[] originalBytes = Files.readAllBytes(file.toPath()); - - EncryptionAndDecryption.EncryptionFile(file, EncryptionType.AES, keyPath); - - byte[] encryptedBytes = Files.readAllBytes(file.toPath()); - assertTrue(!java.util.Arrays.equals(originalBytes, encryptedBytes), testName); - } - - // ==================== 异常场景 ==================== - - static void testNullFile() throws Exception { - String testName = "file 为 null 时加密返回 null"; - Path keyPath = createTempKeyPath(); - File result = EncryptionAndDecryption.EncryptionFile(null, EncryptionType.AES, keyPath); - assertEquals(null, result, testName); - } - - static void testNullKeyPath() throws Exception { - String testName = "keyPath 为 null 时加密返回 null"; - File file = createTempFile("null_keypath"); - File result = EncryptionAndDecryption.EncryptionFile(file, EncryptionType.AES, null); - assertEquals(null, result, testName); - } - - static void testNullKeyFile() throws Exception { - String testName = "keyfile 为 null 时解密返回 null"; - File file = createTempFile("null_keyfile"); - File result = EncryptionAndDecryption.DecryptionFile(file, EncryptionType.AES, null); - assertEquals(null, result, testName); - } - - static void testFileNotExist() throws Exception { - String testName = "文件不存在时加密返回 null"; - Path keyPath = createTempKeyPath(); - File notExist = new File("non_existent_file_12345.txt"); - File result = EncryptionAndDecryption.EncryptionFile(notExist, EncryptionType.AES, keyPath); - assertEquals(null, result, testName); - } - - static void testKeyFileNotExist() throws Exception { - String testName = "密钥文件不存在时解密返回 null"; - File file = createTempFile("no_keyfile"); - File notExistKey = new File("non_existent_key_12345.key"); - File result = EncryptionAndDecryption.DecryptionFile(file, EncryptionType.AES, notExistKey); - assertEquals(null, result, testName); - } -} diff --git a/src/test/java/HwidTest.java b/src/test/java/HwidTest.java deleted file mode 100644 index 714456c..0000000 --- a/src/test/java/HwidTest.java +++ /dev/null @@ -1,16 +0,0 @@ -import top.gtb520.java.gnu.java.utils.library.main; -import top.gtb520.java.gnu.java.utils.library.utils.processing.hwid.getHWID; - -public class HwidTest { - public static void main() { - String Hwid_Str; - Hwid_Str = main.hwid.getHWID(); - System.out.println("HWID: " + Hwid_Str); - System.out.println("HWID length: " + Hwid_Str.length()); - - String Hwid_Str2; - Hwid_Str2 = getHWID.generateHWID(); - System.out.println("HWID: " + Hwid_Str2); - System.out.println("HWID length: " + Hwid_Str2.length()); - } -} diff --git a/src/test/java/compressTest.java b/src/test/java/compressTest.java deleted file mode 100644 index 167282f..0000000 --- a/src/test/java/compressTest.java +++ /dev/null @@ -1,281 +0,0 @@ -import top.gtb520.java.gnu.java.utils.library.utils.compress.compress; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; - -public class compressTest { - - static int passed = 0; - static int failed = 0; - - public static void main(String[] args) throws Exception { - System.out.println("========== compress 测试开始 =========="); - - testMakeZipAndUnZipSingleFile(); - testMakeZipAndUnZipDirectory(); - testMakeTarGzAndUnTarGzSingleFile(); - testMakeTarGzAndUnTarGzDirectory(); - testMakeZipSourceNotExist(); - testUnZipDestNotDirectory(); - testMakeTarGzSourceNotExist(); - testUnTarGzDestNotDirectory(); - - System.out.println("========================================"); - System.out.println("测试完成: 通过 " + passed + ", 失败 " + failed); - if (failed > 0) { - System.exit(1); - } - } - - /** - * 测试单文件的 Zip 压缩与解压 - */ - static void testMakeZipAndUnZipSingleFile() throws Exception { - String testName = "testMakeZipAndUnZipSingleFile"; - Path tempDir = Files.createTempDirectory("compress_test_"); - try { - // 准备源文件 - Path sourceFile = tempDir.resolve("source.txt"); - Files.writeString(sourceFile, "Hello, Zip!"); - - // 压缩 - Path zipFile = tempDir.resolve("output.zip"); - compress.MakeZip(sourceFile.toString(), zipFile.toString()); - assertCondition(testName + " - 压缩文件应存在", Files.exists(zipFile)); - - // 解压 - Path unzipDir = tempDir.resolve("unzip"); - Files.createDirectories(unzipDir); - compress.UnZip(zipFile.toString(), unzipDir.toString()); - - Path resultFile = unzipDir.resolve("source.txt"); - assertCondition(testName + " - 解压文件应存在", Files.exists(resultFile)); - assertCondition(testName + " - 内容应一致", - "Hello, Zip!".equals(Files.readString(resultFile))); - - } finally { - deleteTempDir(tempDir); - } - } - - /** - * 测试目录的 Zip 压缩与解压 - */ - static void testMakeZipAndUnZipDirectory() throws Exception { - String testName = "testMakeZipAndUnZipDirectory"; - Path tempDir = Files.createTempDirectory("compress_test_"); - try { - // 准备源目录结构 - Path sourceDir = tempDir.resolve("source"); - Files.createDirectories(sourceDir.resolve("sub")); - Files.writeString(sourceDir.resolve("a.txt"), "file a"); - Files.writeString(sourceDir.resolve("sub/b.txt"), "file b"); - - // 压缩 - Path zipFile = tempDir.resolve("dir_output.zip"); - compress.MakeZip(sourceDir.toString(), zipFile.toString()); - assertCondition(testName + " - 压缩文件应存在", Files.exists(zipFile)); - - // 解压 - Path unzipDir = tempDir.resolve("unzip_dir"); - Files.createDirectories(unzipDir); - compress.UnZip(zipFile.toString(), unzipDir.toString()); - - assertCondition(testName + " - 文件a应存在", - Files.exists(unzipDir.resolve("a.txt"))); - assertCondition(testName + " - 文件b应存在", - Files.exists(unzipDir.resolve("sub/b.txt"))); - assertCondition(testName + " - 文件a内容应一致", - "file a".equals(Files.readString(unzipDir.resolve("a.txt")))); - assertCondition(testName + " - 文件b内容应一致", - "file b".equals(Files.readString(unzipDir.resolve("sub/b.txt")))); - - } finally { - deleteTempDir(tempDir); - } - } - - /** - * 测试单文件的 Tar.Gz 压缩与解压 - */ - static void testMakeTarGzAndUnTarGzSingleFile() throws Exception { - String testName = "testMakeTarGzAndUnTarGzSingleFile"; - Path tempDir = Files.createTempDirectory("compress_test_"); - try { - // 准备源文件 - Path sourceFile = tempDir.resolve("source.txt"); - Files.writeString(sourceFile, "Hello, TarGz!"); - - // 压缩 - Path tarGzFile = tempDir.resolve("output.tar.gz"); - compress.MakeTarGz(sourceFile.toString(), tarGzFile.toString()); - assertCondition(testName + " - 压缩文件应存在", Files.exists(tarGzFile)); - - // 解压 - Path untarDir = tempDir.resolve("untar"); - Files.createDirectories(untarDir); - compress.UnTarGz(tarGzFile.toString(), untarDir.toString()); - - Path resultFile = untarDir.resolve("source.txt"); - assertCondition(testName + " - 解压文件应存在", Files.exists(resultFile)); - assertCondition(testName + " - 内容应一致", - "Hello, TarGz!".equals(Files.readString(resultFile))); - - } finally { - deleteTempDir(tempDir); - } - } - - /** - * 测试目录的 Tar.Gz 压缩与解压 - */ - static void testMakeTarGzAndUnTarGzDirectory() throws Exception { - String testName = "testMakeTarGzAndUnTarGzDirectory"; - Path tempDir = Files.createTempDirectory("compress_test_"); - try { - // 准备源目录结构 - Path sourceDir = tempDir.resolve("source"); - Files.createDirectories(sourceDir.resolve("sub")); - Files.writeString(sourceDir.resolve("a.txt"), "file a"); - Files.writeString(sourceDir.resolve("sub/b.txt"), "file b"); - - // 压缩 - Path tarGzFile = tempDir.resolve("dir_output.tar.gz"); - compress.MakeTarGz(sourceDir.toString(), tarGzFile.toString()); - assertCondition(testName + " - 压缩文件应存在", Files.exists(tarGzFile)); - - // 解压 - Path untarDir = tempDir.resolve("untar_dir"); - Files.createDirectories(untarDir); - compress.UnTarGz(tarGzFile.toString(), untarDir.toString()); - - assertCondition(testName + " - 文件a应存在", - Files.exists(untarDir.resolve("a.txt"))); - assertCondition(testName + " - 文件b应存在", - Files.exists(untarDir.resolve("sub/b.txt"))); - assertCondition(testName + " - 文件a内容应一致", - "file a".equals(Files.readString(untarDir.resolve("a.txt")))); - assertCondition(testName + " - 文件b内容应一致", - "file b".equals(Files.readString(untarDir.resolve("sub/b.txt")))); - - } finally { - deleteTempDir(tempDir); - } - } - - /** - * 测试压缩不存在的源路径应抛出异常 - */ - static void testMakeZipSourceNotExist() { - String testName = "testMakeZipSourceNotExist"; - try { - compress.MakeZip("not_exist_path", "output.zip"); - assertCondition(testName + " - 应抛出异常", false); - } catch (IllegalArgumentException e) { - assertCondition(testName + " - 应抛出IllegalArgumentException", true); - } catch (Exception e) { - assertCondition(testName + " - 异常类型不正确: " + e.getClass().getName(), false); - } - } - - /** - * 测试解压到非目录路径应抛出异常 - */ - static void testUnZipDestNotDirectory() throws Exception { - String testName = "testUnZipDestNotDirectory"; - Path tempDir = Files.createTempDirectory("compress_test_"); - try { - // 创建一个文件作为解压目标(非目录) - Path notDir = tempDir.resolve("not_a_dir.txt"); - Files.writeString(notDir, "I am a file"); - - // 先创建一个合法zip - Path sourceFile = tempDir.resolve("src.txt"); - Files.writeString(sourceFile, "test"); - Path zipFile = tempDir.resolve("test.zip"); - compress.MakeZip(sourceFile.toString(), zipFile.toString()); - - try { - compress.UnZip(zipFile.toString(), notDir.toString()); - assertCondition(testName + " - 应抛出异常", false); - } catch (IllegalArgumentException e) { - assertCondition(testName + " - 应抛出IllegalArgumentException", true); - } catch (Exception e) { - assertCondition(testName + " - 异常类型不正确: " + e.getClass().getName(), false); - } - } finally { - deleteTempDir(tempDir); - } - } - - /** - * 测试TarGz压缩不存在的源路径应抛出异常 - */ - static void testMakeTarGzSourceNotExist() { - String testName = "testMakeTarGzSourceNotExist"; - try { - compress.MakeTarGz("not_exist_path", "output.tar.gz"); - assertCondition(testName + " - 应抛出异常", false); - } catch (IllegalArgumentException e) { - assertCondition(testName + " - 应抛出IllegalArgumentException", true); - } catch (Exception e) { - assertCondition(testName + " - 异常类型不正确: " + e.getClass().getName(), false); - } - } - - /** - * 测试TarGz解压到非目录路径应抛出异常 - */ - static void testUnTarGzDestNotDirectory() throws Exception { - String testName = "testUnTarGzDestNotDirectory"; - Path tempDir = Files.createTempDirectory("compress_test_"); - try { - Path notDir = tempDir.resolve("not_a_dir.txt"); - Files.writeString(notDir, "I am a file"); - - // 先创建一个合法tar.gz - Path sourceFile = tempDir.resolve("src.txt"); - Files.writeString(sourceFile, "test"); - Path tarGzFile = tempDir.resolve("test.tar.gz"); - compress.MakeTarGz(sourceFile.toString(), tarGzFile.toString()); - - try { - compress.UnTarGz(tarGzFile.toString(), notDir.toString()); - assertCondition(testName + " - 应抛出异常", false); - } catch (IllegalArgumentException e) { - assertCondition(testName + " - 应抛出IllegalArgumentException", true); - } catch (Exception e) { - assertCondition(testName + " - 异常类型不正确: " + e.getClass().getName(), false); - } - } finally { - deleteTempDir(tempDir); - } - } - - // ========== 辅助方法 ========== - - static void assertCondition(String message, boolean condition) { - if (condition) { - System.out.println("[PASS] " + message); - passed++; - } else { - System.out.println("[FAIL] " + message); - failed++; - } - } - - static void deleteTempDir(Path dir) { - try { - Files.walk(dir) - .sorted(java.util.Comparator.reverseOrder()) - .forEach(path -> { - try { - Files.deleteIfExists(path); - } catch (IOException ignored) { - } - }); - } catch (IOException ignored) { - } - } -} diff --git a/src/test/java/echo.java b/src/test/java/echo.java deleted file mode 100644 index 8d53d57..0000000 --- a/src/test/java/echo.java +++ /dev/null @@ -1,24 +0,0 @@ -import top.gtb520.java.gnu.java.utils.library.main; -import top.gtb520.java.gnu.java.utils.library.utils.print.ColorNameEnum; - -public class echo { - static void main(String[] args) { - main.echo.Println("YELLOW", ColorNameEnum.YELLOW); - main.echo.Println("BLUE", ColorNameEnum.BLUE); - main.echo.Println("CYAN", ColorNameEnum.CYAN); - main.echo.Println("PURPLE", ColorNameEnum.PURPLE); - main.echo.Println("GREEN", ColorNameEnum.GREEN); - main.echo.Println("RED", ColorNameEnum.RED); - - ColorHexPrintlnTest(); - - System.out.println("111111111111111111"); - main.echo.Println("头上带点绿,生活过得去", ColorNameEnum.GREEN); - } - - static void ColorHexPrintlnTest() { - main.echo.ColorHexPrintln("ColorHexPrintlnTest_RED", "31"); - main.echo.ColorHexPrintln("自定义橙色", "38;5;208"); - main.echo.ColorHexPrintln("十六进制粉色", "#FFC0CB"); - } -} diff --git a/src/test/java/echoTest.java b/src/test/java/echoTest.java deleted file mode 100644 index a0dc83f..0000000 --- a/src/test/java/echoTest.java +++ /dev/null @@ -1,32 +0,0 @@ -import top.gtb520.java.gnu.java.utils.library.main; -import top.gtb520.java.gnu.java.utils.library.utils.print.ColorNameEnum; - -public class echoTest { - public static void main(String[] args) { - System.out.println("========== echo 测试开始 =========="); - - // 测试 #RRGGBB 格式 - main.echo.ColorHexPrintln("十六进制粉色 #FFC0CB", "#FFC0CB"); - main.echo.ColorHexPrintln("十六进制红色 #FF0000", "#FF0000"); - main.echo.ColorHexPrintln("十六进制绿色 #00FF00", "#00FF00"); - main.echo.ColorHexPrintln("十六进制蓝色 #0000FF", "#0000FF"); - - // 测试 #RGB 简写格式 - main.echo.ColorHexPrintln("RGB简写粉色 #F0F", "#F0F"); - main.echo.ColorHexPrintln("RGB简写白色 #FFF", "#FFF"); - - // 测试原有 ANSI 颜色代码(兼容性) - main.echo.ColorHexPrintln("ANSI红色 31", "31"); - main.echo.ColorHexPrintln("ANSI扩展色 38;5;208", "38;5;208"); - - // 测试预定义颜色枚举 - main.echo.Println("枚举红色", ColorNameEnum.RED); - main.echo.Println("枚举蓝色", ColorNameEnum.BLUE); - main.echo.Println("枚举绿色", ColorNameEnum.GREEN); - main.echo.Println("枚举黄色", ColorNameEnum.YELLOW); - main.echo.Println("枚举青色", ColorNameEnum.CYAN); - main.echo.Println("枚举紫色", ColorNameEnum.PURPLE); - - System.out.println("========== echo 测试完成 =========="); - } -} diff --git a/src/test/java/top/gtb520/java/gnu/java/utils/library/utils/compress/CompressPNGTest.java b/src/test/java/top/gtb520/java/gnu/java/utils/library/utils/compress/CompressPNGTest.java deleted file mode 100644 index 11ff033..0000000 --- a/src/test/java/top/gtb520/java/gnu/java/utils/library/utils/compress/CompressPNGTest.java +++ /dev/null @@ -1,205 +0,0 @@ -package top.gtb520.java.gnu.java.utils.library.utils.compress; - -import javax.imageio.ImageIO; -import java.awt.*; -import java.awt.image.BufferedImage; -import java.io.File; -import java.io.IOException; - -public class CompressPNGTest { - - static int passed = 0; - static int failed = 0; - - public static void main(String[] args) { - System.out.println("========== CompressPNG 测试开始 =========="); - - testCompressWithEachThreshold(); - testNonPNGFile(); - testNonExistentFile(); - testCompressedFileIsValidPNG(); - testCompressedFileSmallerOrEqual(); - - System.out.println("========================================"); - System.out.println("测试完成: 通过 " + passed + ", 失败 " + failed); - if (failed > 0) { - System.exit(1); - } - } - - /** - * 测试每个压缩阈值均能正常压缩并返回原文件 - */ - static void testCompressWithEachThreshold() { - String testName = "testCompressWithEachThreshold"; - for (CompressPNG.PNGThreshold threshold : CompressPNG.PNGThreshold.values()) { - File pngFile = null; - try { - pngFile = createTestPNG("test_threshold_" + threshold.name()); - long originalSize = pngFile.length(); - - File result = CompressPNG.PNG(pngFile, threshold); - - boolean returnedSameFile = result != null && result.equals(pngFile); - assertCondition(testName + " [" + threshold.name() + "] - 应返回原文件引用", returnedSameFile); - - boolean fileExists = result != null && result.exists() && result.length() > 0; - assertCondition(testName + " [" + threshold.name() + "] - 压缩后文件应存在且非空", fileExists); - - System.out.println(" [" + threshold.name() + "] 原始: " + originalSize + " bytes, 压缩后: " + (result != null ? result.length() : "N/A") + " bytes"); - } catch (Exception e) { - assertCondition(testName + " [" + threshold.name() + "] - 不应抛出异常: " + e.getMessage(), false); - } finally { - deleteTestFile(pngFile); - } - } - } - - /** - * 测试传入非PNG文件应返回null - */ - static void testNonPNGFile() { - String testName = "testNonPNGFile"; - File txtFile = null; - try { - txtFile = File.createTempFile("test_not_png", ".txt"); - java.nio.file.Files.writeString(txtFile.toPath(), "this is not a png"); - - File result = CompressPNG.PNG(txtFile, CompressPNG.PNGThreshold.THRESHOLD_3); - assertCondition(testName + " - 非PNG文件应返回null", result == null); - } catch (Exception e) { - assertCondition(testName + " - 不应抛出异常: " + e.getMessage(), false); - } finally { - deleteTestFile(txtFile); - } - } - - /** - * 测试传入不存在的文件应返回null - */ - static void testNonExistentFile() { - String testName = "testNonExistentFile"; - try { - File nonExistent = new File(System.getProperty("java.io.tmpdir"), "non_existent_file.png"); - File result = CompressPNG.PNG(nonExistent, CompressPNG.PNGThreshold.THRESHOLD_3); - assertCondition(testName + " - 不存在的文件应返回null", result == null); - } catch (Exception e) { - assertCondition(testName + " - 不应抛出异常: " + e.getMessage(), false); - } - } - - /** - * 测试压缩后的文件仍然是有效的PNG图片 - */ - static void testCompressedFileIsValidPNG() { - String testName = "testCompressedFileIsValidPNG"; - File pngFile = null; - try { - pngFile = createTestPNG("test_valid_png"); - CompressPNG.PNG(pngFile, CompressPNG.PNGThreshold.THRESHOLD_3); - - BufferedImage compressed = ImageIO.read(pngFile); - boolean isValidPNG = compressed != null; - assertCondition(testName + " - 压缩后的文件应是有效的PNG图片", isValidPNG); - - if (isValidPNG) { - assertCondition(testName + " - 压缩后图片宽度应一致", compressed.getWidth() == 100); - assertCondition(testName + " - 压缩后图片高度应一致", compressed.getHeight() == 100); - } - } catch (Exception e) { - assertCondition(testName + " - 不应抛出异常: " + e.getMessage(), false); - } finally { - deleteTestFile(pngFile); - } - } - - /** - * 测试压缩后文件大小不超过原始文件 - */ - static void testCompressedFileSmallerOrEqual() { - String testName = "testCompressedFileSmallerOrEqual"; - File pngFile = null; - try { - // 创建一个较大的、有压缩空间的测试图片(纯色+渐变) - pngFile = createLargeTestPNG("test_compression_ratio", 500, 500); - long originalSize = pngFile.length(); - - File result = CompressPNG.PNG(pngFile, CompressPNG.PNGThreshold.THRESHOLD_5); - - if (result != null) { - long compressedSize = result.length(); - assertCondition(testName + " - 压缩后文件大小(" + compressedSize + ")应不超过原始大小(" + originalSize + ")", - compressedSize <= originalSize); - System.out.println(" 原始: " + originalSize + " bytes, THRESHOLD_5压缩后: " + compressedSize + " bytes"); - } else { - assertCondition(testName + " - 压缩不应返回null", false); - } - } catch (Exception e) { - assertCondition(testName + " - 不应抛出异常: " + e.getMessage(), false); - } finally { - deleteTestFile(pngFile); - } - } - - // ========== 辅助方法 ========== - - /** - * 创建一个100x100的测试PNG图片 - */ - static File createTestPNG(String name) throws IOException { - File file = File.createTempFile(name, ".png"); - BufferedImage image = new BufferedImage(100, 100, BufferedImage.TYPE_INT_ARGB); - Graphics2D g2d = image.createGraphics(); - // 绘制渐变图案,使图片有一定的压缩空间 - for (int y = 0; y < 100; y++) { - for (int x = 0; x < 100; x++) { - image.setRGB(x, y, new Color(x * 2, y * 2, (x + y)).getRGB()); - } - } - g2d.dispose(); - ImageIO.write(image, "png", file); - return file; - } - - /** - * 创建一个指定尺寸的测试PNG图片(包含更多颜色变化以增加压缩效果差异) - */ - static File createLargeTestPNG(String name, int width, int height) throws IOException { - File file = File.createTempFile(name, ".png"); - BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); - Graphics2D g2d = image.createGraphics(); - // 绘制大面积纯色区域(压缩率高) - g2d.setColor(Color.WHITE); - g2d.fillRect(0, 0, width, height); - g2d.setColor(Color.RED); - g2d.fillRect(0, 0, width / 2, height / 2); - g2d.setColor(Color.BLUE); - g2d.fillRect(width / 2, 0, width / 2, height / 2); - g2d.setColor(Color.GREEN); - g2d.fillRect(0, height / 2, width / 2, height / 2); - g2d.setColor(Color.YELLOW); - g2d.fillRect(width / 2, height / 2, width / 2, height / 2); - g2d.dispose(); - ImageIO.write(image, "png", file); - return file; - } - - /** - * 安全删除测试文件 - */ - static void deleteTestFile(File file) { - if (file != null && file.exists()) { - file.delete(); - } - } - - static void assertCondition(String message, boolean condition) { - if (condition) { - System.out.println("[PASS] " + message); - passed++; - } else { - System.out.println("[FAIL] " + message); - failed++; - } - } -} diff --git a/src/test/java/top/gtb520/java/gnu/java/utils/library/utils/compress/CompressPngTest.java b/src/test/java/top/gtb520/java/gnu/java/utils/library/utils/compress/CompressPngTest.java new file mode 100644 index 0000000..baaa98f --- /dev/null +++ b/src/test/java/top/gtb520/java/gnu/java/utils/library/utils/compress/CompressPngTest.java @@ -0,0 +1,72 @@ +package top.gtb520.java.gnu.java.utils.library.utils.compress; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import javax.imageio.ImageIO; +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.*; + +class CompressPngTest { + + @TempDir + Path tempDir; + + private File createTestPng(String name) throws Exception { + BufferedImage image = new BufferedImage(100, 100, BufferedImage.TYPE_INT_ARGB); + Graphics2D g2d = image.createGraphics(); + for (int y = 0; y < 100; y++) { + for (int x = 0; x < 100; x++) { + image.setRGB(x, y, new Color(x * 2, y * 2, (x + y)).getRGB()); + } + } + g2d.dispose(); + File file = tempDir.resolve(name + ".png").toFile(); + ImageIO.write(image, "png", file); + return file; + } + + @Test + void compressWithEachThreshold() throws Exception { + for (CompressPNG.PNGThreshold threshold : CompressPNG.PNGThreshold.values()) { + File pngFile = createTestPng("test_" + threshold.name()); + File result = CompressPNG.PNG(pngFile, threshold); + + assertNotNull(result, threshold + " 压缩应返回文件"); + assertEquals(pngFile, result, "应返回原文件引用"); + assertTrue(result.exists() && result.length() > 0); + + // 压缩后仍是有效 PNG,尺寸保持不变 + BufferedImage readBack = ImageIO.read(pngFile); + assertNotNull(readBack); + assertEquals(100, readBack.getWidth()); + assertEquals(100, readBack.getHeight()); + } + } + + @Test + void nonPngFileReturnsNull() throws Exception { + File txt = tempDir.resolve("note.txt").toFile(); + Files.writeString(txt.toPath(), "this is not a png"); + assertNull(CompressPNG.PNG(txt, CompressPNG.PNGThreshold.THRESHOLD_3)); + } + + @Test + void missingFileReturnsNull() { + File missing = tempDir.resolve("missing.png").toFile(); + assertNull(CompressPNG.PNG(missing, CompressPNG.PNGThreshold.THRESHOLD_3)); + } + + @Test + void nullThresholdThrows() throws Exception { + File pngFile = createTestPng("null_threshold"); + assertThrows(IllegalArgumentException.class, + () -> CompressPNG.PNG(pngFile, null)); + } +} diff --git a/src/test/java/top/gtb520/java/gnu/java/utils/library/utils/compress/CompressTest.java b/src/test/java/top/gtb520/java/gnu/java/utils/library/utils/compress/CompressTest.java new file mode 100644 index 0000000..e698572 --- /dev/null +++ b/src/test/java/top/gtb520/java/gnu/java/utils/library/utils/compress/CompressTest.java @@ -0,0 +1,167 @@ +package top.gtb520.java.gnu.java.utils.library.utils.compress; + +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; +import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; +import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream; +import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.BufferedOutputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.*; + +class CompressTest { + + @TempDir + Path tempDir; + + private Path writeFile(String name, String content) throws Exception { + Path file = tempDir.resolve(name); + Files.writeString(file, content); + return file; + } + + private Path newDir(String name) throws Exception { + return Files.createDirectory(tempDir.resolve(name)); + } + + // ==================== Zip ==================== + + @Test + void zipRoundTripSingleFile() throws Exception { + Path source = writeFile("source.txt", "Hello, Zip!"); + Path zip = tempDir.resolve("output.zip"); + compress.MakeZip(source.toString(), zip.toString()); + assertTrue(Files.exists(zip)); + + Path dest = newDir("unzip"); + compress.UnZip(zip.toString(), dest.toString()); + + Path result = dest.resolve("source.txt"); + assertTrue(Files.exists(result)); + assertEquals("Hello, Zip!", Files.readString(result)); + } + + @Test + void zipRoundTripDirectory() throws Exception { + Path source = tempDir.resolve("source"); + Files.createDirectories(source.resolve("sub")); + Files.writeString(source.resolve("a.txt"), "file a"); + Files.writeString(source.resolve("sub/b.txt"), "file b"); + + Path zip = tempDir.resolve("dir_output.zip"); + compress.MakeZip(source.toString(), zip.toString()); + + Path dest = newDir("unzip_dir"); + compress.UnZip(zip.toString(), dest.toString()); + + assertEquals("file a", Files.readString(dest.resolve("a.txt"))); + assertEquals("file b", Files.readString(dest.resolve("sub/b.txt"))); + } + + @Test + void makeZipSourceNotExistThrows() { + assertThrows(IllegalArgumentException.class, + () -> compress.MakeZip(tempDir.resolve("not_exist").toString(), + tempDir.resolve("out.zip").toString())); + } + + @Test + void unZipDestNotDirectoryThrows() throws Exception { + Path notDir = writeFile("not_a_dir.txt", "I am a file"); + assertThrows(IllegalArgumentException.class, + () -> compress.UnZip(tempDir.resolve("any.zip").toString(), notDir.toString())); + } + + @Test + void unZipRejectsPathTraversal() throws Exception { + // 构造包含 ../evil.txt 条目的恶意 zip + Path zip = tempDir.resolve("evil.zip"); + try (OutputStream out = new BufferedOutputStream(Files.newOutputStream(zip)); + ZipArchiveOutputStream zos = new ZipArchiveOutputStream(out)) { + ZipArchiveEntry entry = new ZipArchiveEntry("../evil.txt"); + zos.putArchiveEntry(entry); + zos.write("evil".getBytes(StandardCharsets.UTF_8)); + zos.closeArchiveEntry(); + } + + Path dest = newDir("dest"); + assertThrows(IllegalArgumentException.class, () -> compress.UnZip(zip.toString(), dest.toString())); + // 不得写出目标目录之外 + assertFalse(Files.exists(tempDir.resolve("evil.txt"))); + } + + // ==================== Tar.Gz ==================== + + @Test + void tarGzRoundTripSingleFile() throws Exception { + Path source = writeFile("source.txt", "Hello, TarGz!"); + Path tarGz = tempDir.resolve("output.tar.gz"); + compress.MakeTarGz(source.toString(), tarGz.toString()); + assertTrue(Files.exists(tarGz)); + + Path dest = newDir("untar"); + compress.UnTarGz(tarGz.toString(), dest.toString()); + + Path result = dest.resolve("source.txt"); + assertTrue(Files.exists(result)); + assertEquals("Hello, TarGz!", Files.readString(result)); + } + + @Test + void tarGzRoundTripDirectory() throws Exception { + Path source = tempDir.resolve("source"); + Files.createDirectories(source.resolve("sub")); + Files.writeString(source.resolve("a.txt"), "file a"); + Files.writeString(source.resolve("sub/b.txt"), "file b"); + + Path tarGz = tempDir.resolve("dir_output.tar.gz"); + compress.MakeTarGz(source.toString(), tarGz.toString()); + + Path dest = newDir("untar_dir"); + compress.UnTarGz(tarGz.toString(), dest.toString()); + + assertEquals("file a", Files.readString(dest.resolve("a.txt"))); + assertEquals("file b", Files.readString(dest.resolve("sub/b.txt"))); + } + + @Test + void makeTarGzSourceNotExistThrows() { + assertThrows(IllegalArgumentException.class, + () -> compress.MakeTarGz(tempDir.resolve("not_exist").toString(), + tempDir.resolve("out.tar.gz").toString())); + } + + @Test + void unTarGzDestNotDirectoryThrows() throws Exception { + Path notDir = writeFile("not_a_dir.txt", "I am a file"); + assertThrows(IllegalArgumentException.class, + () -> compress.UnTarGz(tempDir.resolve("any.tar.gz").toString(), notDir.toString())); + } + + @Test + void unTarGzRejectsPathTraversal() throws Exception { + // 构造包含 ../evil.txt 条目的恶意 tar.gz + Path tarGz = tempDir.resolve("evil.tar.gz"); + try (OutputStream out = new BufferedOutputStream(Files.newOutputStream(tarGz)); + GzipCompressorOutputStream gz = new GzipCompressorOutputStream(out); + TarArchiveOutputStream tos = new TarArchiveOutputStream(gz)) { + TarArchiveEntry entry = new TarArchiveEntry("../evil.txt"); + entry.setSize(4); + tos.putArchiveEntry(entry); + tos.write("evil".getBytes(StandardCharsets.UTF_8)); + tos.closeArchiveEntry(); + tos.finish(); + } + + Path dest = newDir("dest"); + assertThrows(IllegalArgumentException.class, () -> compress.UnTarGz(tarGz.toString(), dest.toString())); + assertFalse(Files.exists(tempDir.resolve("evil.txt"))); + } +} diff --git a/src/test/java/top/gtb520/java/gnu/java/utils/library/utils/file/EncryptionTest.java b/src/test/java/top/gtb520/java/gnu/java/utils/library/utils/file/EncryptionTest.java new file mode 100644 index 0000000..2db9f8d --- /dev/null +++ b/src/test/java/top/gtb520/java/gnu/java/utils/library/utils/file/EncryptionTest.java @@ -0,0 +1,145 @@ +package top.gtb520.java.gnu.java.utils.library.utils.file; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; + +import static org.junit.jupiter.api.Assertions.*; + +class EncryptionTest { + + @TempDir + Path tempDir; + + private File tempFile(String name, String content) throws Exception { + Path file = tempDir.resolve(name); + Files.writeString(file, content); + return file.toFile(); + } + + private Path newKeyDir(String name) throws Exception { + return Files.createDirectory(tempDir.resolve(name)); + } + + @Test + void aesRoundTrip() throws Exception { + File file = tempFile("aes.txt", "Hello AES-GCM! 测试内容123"); + String original = Files.readString(file.toPath()); + Path keyPath = newKeyDir("aes_keys"); + + assertNotNull(EncryptionAndDecryption.EncryptionFile( + file, EncryptionAndDecryption.EncryptionType.AES, keyPath)); + + // 加密后内容与原文件不同 + assertFalse(Arrays.equals(original.getBytes(StandardCharsets.UTF_8), Files.readAllBytes(file.toPath()))); + + // 密钥文件已生成且非空 + File keyFile = keyPath.resolve("aes.txt.key").toFile(); + assertTrue(keyFile.exists()); + assertTrue(keyFile.length() > 0); + + assertNotNull(EncryptionAndDecryption.DecryptionFile( + file, EncryptionAndDecryption.EncryptionType.AES, keyFile)); + assertEquals(original, Files.readString(file.toPath())); + } + + @Test + @SuppressWarnings("deprecation") + void desRoundTrip() throws Exception { + File file = tempFile("des.txt", "Hello DES! 兼容旧版本"); + String original = Files.readString(file.toPath()); + Path keyPath = newKeyDir("des_keys"); + + assertNotNull(EncryptionAndDecryption.EncryptionFile( + file, EncryptionAndDecryption.EncryptionType.DES, keyPath)); + + File keyFile = keyPath.resolve("des.txt.key").toFile(); + assertNotNull(EncryptionAndDecryption.DecryptionFile( + file, EncryptionAndDecryption.EncryptionType.DES, keyFile)); + assertEquals(original, Files.readString(file.toPath())); + } + + @Test + void rsaRoundTrip() throws Exception { + File file = tempFile("rsa.txt", "Hello RSA hybrid! 混合加密测试"); + String original = Files.readString(file.toPath()); + Path keyPath = newKeyDir("rsa_keys"); + + assertNotNull(EncryptionAndDecryption.EncryptionFile( + file, EncryptionAndDecryption.EncryptionType.RSA, keyPath)); + assertFalse(Arrays.equals(original.getBytes(StandardCharsets.UTF_8), Files.readAllBytes(file.toPath()))); + + File keyFile = keyPath.resolve("rsa.txt.key").toFile(); + assertTrue(keyFile.length() > 0); + + assertNotNull(EncryptionAndDecryption.DecryptionFile( + file, EncryptionAndDecryption.EncryptionType.RSA, keyFile)); + assertEquals(original, Files.readString(file.toPath())); + } + + @Test + void largeFileRoundTripIsStreamed() throws Exception { + // 8MB 文件验证流式加解密不会 OOM + File file = tempFile("large.bin", "init"); + byte[] chunk = new byte[64 * 1024]; + for (int i = 0; i < chunk.length; i++) { + chunk[i] = (byte) (i * 31 + 7); + } + try (var out = Files.newOutputStream(file.toPath())) { + for (int i = 0; i < 128; i++) { + out.write(chunk); + } + } + byte[] original = Files.readAllBytes(file.toPath()); + Path keyPath = newKeyDir("large_keys"); + + assertNotNull(EncryptionAndDecryption.EncryptionFile( + file, EncryptionAndDecryption.EncryptionType.AES, keyPath)); + File keyFile = keyPath.resolve("large.bin.key").toFile(); + assertNotNull(EncryptionAndDecryption.DecryptionFile( + file, EncryptionAndDecryption.EncryptionType.AES, keyFile)); + assertArrayEquals(original, Files.readAllBytes(file.toPath())); + } + + @Test + void invalidInputsReturnNull() throws Exception { + File file = tempFile("valid.txt", "valid"); + assertNull(EncryptionAndDecryption.EncryptionFile(null, + EncryptionAndDecryption.EncryptionType.AES, tempDir)); + assertNull(EncryptionAndDecryption.EncryptionFile( + tempDir.resolve("not_exist.txt").toFile(), + EncryptionAndDecryption.EncryptionType.AES, tempDir)); + assertNull(EncryptionAndDecryption.EncryptionFile( + file, EncryptionAndDecryption.EncryptionType.AES, null)); + assertNull(EncryptionAndDecryption.EncryptionFile( + file, null, tempDir)); + assertNull(EncryptionAndDecryption.DecryptionFile( + file, EncryptionAndDecryption.EncryptionType.AES, null)); + assertNull(EncryptionAndDecryption.DecryptionFile( + file, EncryptionAndDecryption.EncryptionType.AES, + tempDir.resolve("not_exist.key").toFile())); + } + + @Test + void wrongKeyThrowsAndDoesNotCorruptFile() throws Exception { + File file = tempFile("target.txt", "secret content"); + File other = tempFile("other.txt", "other content"); + Path keyPath = newKeyDir("keys"); + + EncryptionAndDecryption.EncryptionFile(file, EncryptionAndDecryption.EncryptionType.AES, keyPath); + EncryptionAndDecryption.EncryptionFile(other, EncryptionAndDecryption.EncryptionType.AES, keyPath); + + byte[] encryptedBytes = Files.readAllBytes(file.toPath()); + File wrongKey = keyPath.resolve("other.txt.key").toFile(); + + // GCM 认证失败 → 抛 RuntimeException,且原密文不被破坏 + assertThrows(RuntimeException.class, () -> EncryptionAndDecryption.DecryptionFile( + file, EncryptionAndDecryption.EncryptionType.AES, wrongKey)); + assertArrayEquals(encryptedBytes, Files.readAllBytes(file.toPath())); + } +} diff --git a/src/test/java/top/gtb520/java/gnu/java/utils/library/utils/print/ColorPrintlnPlusTest.java b/src/test/java/top/gtb520/java/gnu/java/utils/library/utils/print/ColorPrintlnPlusTest.java deleted file mode 100644 index 9515fff..0000000 --- a/src/test/java/top/gtb520/java/gnu/java/utils/library/utils/print/ColorPrintlnPlusTest.java +++ /dev/null @@ -1,335 +0,0 @@ -package top.gtb520.java.gnu.java.utils.library.utils.print; - -import java.io.ByteArrayOutputStream; -import java.io.PrintStream; - -public class ColorPrintlnPlusTest { - - static int passed = 0; - static int failed = 0; - - public static void main(String[] args) { - System.out.println("========== ColorPrintlnPlus 测试开始 =========="); - - testNullInput(); - testPlainMessageNoFormatCode(); - testSingleColorCode(); - testMultipleColorCodes(); - testFormatCodeBold(); - testFormatCodeUnderline(); - testFormatCodeItalic(); - testFormatCodeStrikethrough(); - testFormatCodeReset(); - testObfuscatedMode(); - testObfuscatedModeDifferentEachCall(); - testObfuscatedModeTurnsOffOnNewCode(); - testInvalidCodeKeptAsIs(); - testAmpersandAtEndOfString(); - testCaseInsensitiveCodes(); - testAllColorCodes(); - testVisualOutput(); - - System.out.println("========================================"); - System.out.println("测试完成: 通过 " + passed + ", 失败 " + failed); - if (failed > 0) { - System.exit(1); - } - } - - /** - * 捕获 ColorPrintlnPlus 输出到字符串 - */ - static String captureOutput(String input) { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - PrintStream original = System.out; - System.setOut(new PrintStream(baos)); - try { - echo.ColorPrintlnPlus(input); - } finally { - System.setOut(original); - } - return baos.toString(); - } - - /** - * 测试 null 输入 - */ - static void testNullInput() { - String testName = "testNullInput"; - try { - String output = captureOutput(null); - assertCondition(testName + " - null输入应输出空行", output.trim().isEmpty()); - } catch (Exception e) { - assertCondition(testName + " - 不应抛出异常: " + e.getMessage(), false); - } - } - - /** - * 测试无格式代码的纯文本 - */ - static void testPlainMessageNoFormatCode() { - String testName = "testPlainMessageNoFormatCode"; - try { - String output = captureOutput("Hello World"); - assertCondition(testName + " - 应包含原文本", output.contains("Hello World")); - assertCondition(testName + " - 应以重置序列结尾", output.contains("\033[0m")); - } catch (Exception e) { - assertCondition(testName + " - 不应抛出异常: " + e.getMessage(), false); - } - } - - /** - * 测试单个颜色代码 - */ - static void testSingleColorCode() { - String testName = "testSingleColorCode"; - try { - String output = captureOutput("&4Red Text"); - // &4 应转换为深红色 ANSI 序列 \033[31m - assertCondition(testName + " - 应包含红色ANSI序列", output.contains("\033[31m")); - assertCondition(testName + " - 应包含文本内容", output.contains("Red Text")); - assertCondition(testName + " - 不应包含格式代码&4", !output.contains("&4")); - } catch (Exception e) { - assertCondition(testName + " - 不应抛出异常: " + e.getMessage(), false); - } - } - - /** - * 测试多个颜色代码 - */ - static void testMultipleColorCodes() { - String testName = "testMultipleColorCodes"; - try { - String output = captureOutput("&6Welcome &2to &4Project"); - // &6 → 橙色 \033[38;5;214m - assertCondition(testName + " - 应包含橙色ANSI序列", output.contains("\033[38;5;214m")); - // &2 → 深绿色 \033[32m - assertCondition(testName + " - 应包含深绿色ANSI序列", output.contains("\033[32m")); - // &4 → 深红色 \033[31m - assertCondition(testName + " - 应包含深红色ANSI序列", output.contains("\033[31m")); - assertCondition(testName + " - 应包含Welcome", output.contains("Welcome")); - assertCondition(testName + " - 应包含to", output.contains("to")); - assertCondition(testName + " - 应包含Project", output.contains("Project")); - } catch (Exception e) { - assertCondition(testName + " - 不应抛出异常: " + e.getMessage(), false); - } - } - - /** - * 测试加粗格式码 &l - */ - static void testFormatCodeBold() { - String testName = "testFormatCodeBold"; - try { - String output = captureOutput("&lBold Text"); - assertCondition(testName + " - 应包含加粗ANSI序列", output.contains("\033[1m")); - assertCondition(testName + " - 应包含文本", output.contains("Bold Text")); - } catch (Exception e) { - assertCondition(testName + " - 不应抛出异常: " + e.getMessage(), false); - } - } - - /** - * 测试下划线格式码 &n - */ - static void testFormatCodeUnderline() { - String testName = "testFormatCodeUnderline"; - try { - String output = captureOutput("&nUnderlined"); - assertCondition(testName + " - 应包含下划线ANSI序列", output.contains("\033[4m")); - } catch (Exception e) { - assertCondition(testName + " - 不应抛出异常: " + e.getMessage(), false); - } - } - - /** - * 测试斜体格式码 &o - */ - static void testFormatCodeItalic() { - String testName = "testFormatCodeItalic"; - try { - String output = captureOutput("&oItalic"); - assertCondition(testName + " - 应包含斜体ANSI序列", output.contains("\033[3m")); - } catch (Exception e) { - assertCondition(testName + " - 不应抛出异常: " + e.getMessage(), false); - } - } - - /** - * 测试删除线格式码 &m - */ - static void testFormatCodeStrikethrough() { - String testName = "testFormatCodeStrikethrough"; - try { - String output = captureOutput("&mStrike"); - assertCondition(testName + " - 应包含删除线ANSI序列", output.contains("\033[9m")); - } catch (Exception e) { - assertCondition(testName + " - 不应抛出异常: " + e.getMessage(), false); - } - } - - /** - * 测试重置格式码 &r - */ - static void testFormatCodeReset() { - String testName = "testFormatCodeReset"; - try { - String output = captureOutput("&4Red&r Normal"); - // &r 应产生重置序列 \033[0m - assertCondition(testName + " - 应包含重置序列", output.contains("\033[0m")); - assertCondition(testName + " - 应包含Normal", output.contains("Normal")); - } catch (Exception e) { - assertCondition(testName + " - 不应抛出异常: " + e.getMessage(), false); - } - } - - /** - * 测试乱码模式 &k:字符被替换为随机字符 - */ - static void testObfuscatedMode() { - String testName = "testObfuscatedMode"; - try { - String output = captureOutput("&kSecret"); - // 输出不应包含原始文本 "Secret"(因为被替换为随机字符) - // 但应包含乱码ANSI序列 \033[8m - assertCondition(testName + " - 应包含乱码ANSI序列", output.contains("\033[8m")); - // 输出中"Secret"被替换,长度应与原词相同(6个字符区域) - assertCondition(testName + " - 不应包含原始文本Secret", !output.contains("Secret")); - } catch (Exception e) { - assertCondition(testName + " - 不应抛出异常: " + e.getMessage(), false); - } - } - - /** - * 测试乱码模式每次调用产生不同结果 - */ - static void testObfuscatedModeDifferentEachCall() { - String testName = "testObfuscatedModeDifferentEachCall"; - try { - String output1 = captureOutput("&kAAAAAAAAAA"); - String output2 = captureOutput("&kAAAAAAAAAA"); - // 两次调用的乱码结果应不同(10个字符全部随机,几乎不可能相同) - assertCondition(testName + " - 两次调用应产生不同乱码", !output1.equals(output2)); - } catch (Exception e) { - assertCondition(testName + " - 不应抛出异常: " + e.getMessage(), false); - } - } - - /** - * 测试乱码模式在遇到新格式码时关闭 - */ - static void testObfuscatedModeTurnsOffOnNewCode() { - String testName = "testObfuscatedModeTurnsOffOnNewCode"; - try { - String output = captureOutput("&kHidden&aVisible"); - // &a 应关闭乱码并开启绿色 - assertCondition(testName + " - 应包含绿色ANSI序列", output.contains("\033[92m")); - // "Visible" 应保持原文 - assertCondition(testName + " - 新格式码后的文本应保持原文", output.contains("Visible")); - // "Hidden" 不应出现 - assertCondition(testName + " - 乱码区域的文本不应出现", !output.contains("Hidden")); - } catch (Exception e) { - assertCondition(testName + " - 不应抛出异常: " + e.getMessage(), false); - } - } - - /** - * 测试非合法格式码保持原样 - */ - static void testInvalidCodeKeptAsIs() { - String testName = "testInvalidCodeKeptAsIs"; - try { - String output = captureOutput("&zUnknown"); - // &z 不是合法格式码,应保持原样 - assertCondition(testName + " - 非合法格式码应保持原样", output.contains("&zUnknown")); - } catch (Exception e) { - assertCondition(testName + " - 不应抛出异常: " + e.getMessage(), false); - } - } - - /** - * 测试 & 在字符串末尾 - */ - static void testAmpersandAtEndOfString() { - String testName = "testAmpersandAtEndOfString"; - try { - String output = captureOutput("Text&"); - // 末尾的 & 后无字符,应保持原样 - assertCondition(testName + " - 末尾的&应保持原样", output.contains("Text&")); - } catch (Exception e) { - assertCondition(testName + " - 不应抛出异常: " + e.getMessage(), false); - } - } - - /** - * 测试格式码大小写不敏感 - */ - static void testCaseInsensitiveCodes() { - String testName = "testCaseInsensitiveCodes"; - try { - String outputLower = captureOutput("&4red"); - String outputUpper = captureOutput("&4red"); - assertCondition(testName + " - 小写格式码应正常工作", outputLower.contains("\033[31m")); - assertCondition(testName + " - 大写格式码应正常工作", outputUpper.contains("\033[31m")); - } catch (Exception e) { - assertCondition(testName + " - 不应抛出异常: " + e.getMessage(), false); - } - } - - /** - * 测试所有16种颜色码均能正确转换 - */ - static void testAllColorCodes() { - String testName = "testAllColorCodes"; - String[][] colorMap = { - {"0", "\033[30m"}, {"1", "\033[34m"}, {"2", "\033[32m"}, {"3", "\033[36m"}, - {"4", "\033[31m"}, {"5", "\033[35m"}, {"6", "\033[38;5;214m"}, {"7", "\033[37m"}, - {"8", "\033[90m"}, {"9", "\033[94m"}, {"a", "\033[92m"}, {"b", "\033[96m"}, - {"c", "\033[91m"}, {"d", "\033[95m"}, {"e", "\033[93m"}, {"f", "\033[97m"} - }; - try { - boolean allPassed = true; - for (String[] pair : colorMap) { - String output = captureOutput("&" + pair[0] + "x"); - if (!output.contains(pair[1])) { - assertCondition(testName + " - &" + pair[0] + " 应转换为 " + pair[1], false); - allPassed = false; - } - } - if (allPassed) { - assertCondition(testName + " - 全部16种颜色码转换正确", true); - } - } catch (Exception e) { - assertCondition(testName + " - 不应抛出异常: " + e.getMessage(), false); - } - } - - /** - * 视觉测试:在终端直接展示各种效果 - */ - static void testVisualOutput() { - System.out.println("--- 视觉测试(请人工确认)---"); - echo.ColorPrintlnPlus("&4深红色 &c浅红色 &6橙色 &e黄色 &2深绿色 &a浅绿色"); - echo.ColorPrintlnPlus("&b浅青色 &3深青色 &1深蓝色 &9浅蓝色 &d粉色 &5深紫色"); - echo.ColorPrintlnPlus("&f白色 &7灰色 &8深灰色 &0黑色"); - echo.ColorPrintlnPlus("&l加粗文字 &n下划线文字 &o斜体文字 &m删除线文字"); - echo.ColorPrintlnPlus("&6Welcome &2to &4Project"); - echo.ColorPrintlnPlus("&k这是一段乱码文字每次不同"); - echo.ColorPrintlnPlus("&4红色 &l加粗红色 &r重置为普通"); - System.out.println("--- 视觉测试结束 ---"); - // 视觉测试直接计为通过 - passed++; - } - - // ========== 辅助方法 ========== - - static void assertCondition(String message, boolean condition) { - if (condition) { - System.out.println("[PASS] " + message); - passed++; - } else { - System.out.println("[FAIL] " + message); - failed++; - } - } -} diff --git a/src/test/java/top/gtb520/java/gnu/java/utils/library/utils/print/EchoTest.java b/src/test/java/top/gtb520/java/gnu/java/utils/library/utils/print/EchoTest.java new file mode 100644 index 0000000..227ba9b --- /dev/null +++ b/src/test/java/top/gtb520/java/gnu/java/utils/library/utils/print/EchoTest.java @@ -0,0 +1,175 @@ +package top.gtb520.java.gnu.java.utils.library.utils.print; + +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.*; + +class EchoTest { + + /** + * 捕获 echo 工具类打印到 System.out 的内容 + */ + private static String capture(Runnable action) { + PrintStream original = System.out; + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + System.setOut(new PrintStream(baos, true, StandardCharsets.UTF_8)); + action.run(); + } finally { + System.setOut(original); + } + return baos.toString(StandardCharsets.UTF_8); + } + + // ==================== ColorPrintln ==================== + + @Test + void colorPrintlnUsesAnsiCode() { + String out = capture(() -> echo.ColorPrintln("x", ColorNameEnum.RED)); + assertTrue(out.contains("\033[31m")); + assertTrue(out.contains("x")); + assertTrue(out.contains("\033[0m")); + } + + @Test + void colorPrintlnNullEnumFallsBackToPlain() { + String out = capture(() -> echo.ColorPrintln("plain", null)); + assertTrue(out.contains("plain")); + assertFalse(out.contains("\033[")); + } + + // ==================== ColorHexPrintln ==================== + + @Test + void hexColorRRGGBB() { + String out = capture(() -> echo.ColorHexPrintln("x", "#FFC0CB")); + assertTrue(out.contains("\033[38;2;255;192;203m")); + } + + @Test + void hexColorShorthand() { + String out = capture(() -> echo.ColorHexPrintln("x", "#F0F")); + assertTrue(out.contains("\033[38;2;255;0;255m")); + } + + @Test + void ansiCodePassthrough() { + String out = capture(() -> echo.ColorHexPrintln("x", "38;5;208")); + assertTrue(out.contains("\033[38;5;208m")); + } + + @Test + void invalidHexThrows() { + assertThrows(IllegalArgumentException.class, () -> echo.ColorHexPrintln("x", "#GGGGGG")); + assertThrows(IllegalArgumentException.class, () -> echo.ColorHexPrintln("x", "#12345")); + } + + @Test + void nullColorThrows() { + assertThrows(IllegalArgumentException.class, () -> echo.ColorHexPrintln("x", null)); + } + + @Test + void maliciousAnsiCodeThrows() { + // 防止注入任意转义序列 + assertThrows(IllegalArgumentException.class, () -> echo.ColorHexPrintln("x", "31;0m\u001b[2J")); + } + + // ==================== ColorPrintlnPlus ==================== + + @Test + void plusNullInputPrintsEmptyLine() { + String out = capture(() -> echo.ColorPrintlnPlus(null)); + assertEquals(System.lineSeparator(), out); + } + + @Test + void plusSingleColorCode() { + String out = capture(() -> echo.ColorPrintlnPlus("&4Red Text")); + assertTrue(out.contains("\033[31m")); + assertTrue(out.contains("Red Text")); + assertFalse(out.contains("&4")); + } + + @Test + void plusMultipleColorCodes() { + String out = capture(() -> echo.ColorPrintlnPlus("&6Welcome &2to &4Project")); + assertTrue(out.contains("\033[38;5;214m")); + assertTrue(out.contains("\033[32m")); + assertTrue(out.contains("\033[31m")); + assertTrue(out.contains("Welcome")); + assertTrue(out.contains("to")); + assertTrue(out.contains("Project")); + } + + @Test + void plusStacksColorAndFormat() { + // 修复:颜色码 + 格式码应叠加生效(粗体红色) + String out = capture(() -> echo.ColorPrintlnPlus("&4&lBoldRed")); + int redIndex = out.indexOf("\033[31m"); + int boldIndex = out.indexOf("\033[1m"); + assertTrue(redIndex >= 0, "应包含红色序列"); + assertTrue(boldIndex > redIndex, "加粗序列应叠加在颜色之后"); + assertTrue(out.contains("BoldRed")); + } + + @Test + void plusFormatCodes() { + assertTrue(capture(() -> echo.ColorPrintlnPlus("&lBold")).contains("\033[1m")); + assertTrue(capture(() -> echo.ColorPrintlnPlus("&nUnder")).contains("\033[4m")); + assertTrue(capture(() -> echo.ColorPrintlnPlus("&oItalic")).contains("\033[3m")); + assertTrue(capture(() -> echo.ColorPrintlnPlus("&mStrike")).contains("\033[9m")); + } + + @Test + void plusResetCode() { + String out = capture(() -> echo.ColorPrintlnPlus("&4Red&r Normal")); + assertTrue(out.contains("\033[0m")); + assertTrue(out.contains("Normal")); + } + + @Test + void plusObfuscatedMode() { + String out = capture(() -> echo.ColorPrintlnPlus("&kSecret")); + assertTrue(out.contains("\033[8m")); + assertFalse(out.contains("Secret")); + } + + @Test + void plusObfuscationTurnsOffOnNewCode() { + String out = capture(() -> echo.ColorPrintlnPlus("&kHidden&aVisible")); + assertTrue(out.contains("\033[92m")); + assertTrue(out.contains("Visible")); + assertFalse(out.contains("Hidden")); + } + + @Test + void plusInvalidCodeKeptAsIs() { + String out = capture(() -> echo.ColorPrintlnPlus("&zUnknown")); + assertTrue(out.contains("&zUnknown")); + } + + @Test + void plusAmpersandAtEndKept() { + String out = capture(() -> echo.ColorPrintlnPlus("Text&")); + assertTrue(out.contains("Text&")); + } + + @Test + void plusAllColorCodes() { + String[][] colorMap = { + {"0", "\033[30m"}, {"1", "\033[34m"}, {"2", "\033[32m"}, {"3", "\033[36m"}, + {"4", "\033[31m"}, {"5", "\033[35m"}, {"6", "\033[38;5;214m"}, {"7", "\033[37m"}, + {"8", "\033[90m"}, {"9", "\033[94m"}, {"a", "\033[92m"}, {"b", "\033[96m"}, + {"c", "\033[91m"}, {"d", "\033[95m"}, {"e", "\033[93m"}, {"f", "\033[97m"} + }; + for (String[] pair : colorMap) { + String out = capture(() -> echo.ColorPrintlnPlus("&" + pair[0] + "x")); + assertTrue(out.contains(pair[1]), "&" + pair[0] + " 应转换为 " + pair[1]); + } + } +} diff --git a/src/test/java/top/gtb520/java/gnu/java/utils/library/utils/processing/hwid/HwidTest.java b/src/test/java/top/gtb520/java/gnu/java/utils/library/utils/processing/hwid/HwidTest.java new file mode 100644 index 0000000..8ed97b7 --- /dev/null +++ b/src/test/java/top/gtb520/java/gnu/java/utils/library/utils/processing/hwid/HwidTest.java @@ -0,0 +1,24 @@ +package top.gtb520.java.gnu.java.utils.library.utils.processing.hwid; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class HwidTest { + + @Test + void generateHwidHasExpectedFormat() { + String hwid = getHWID.generateHWID(); + assertNotNull(hwid); + // 成功时是 64 位十六进制;全部硬件信息获取失败时降级为 UNKNOWN_HWID + assertTrue("UNKNOWN_HWID".equals(hwid) || hwid.matches("[0-9a-f]{64}"), + "HWID 应为64位小写十六进制或 UNKNOWN_HWID,实际: " + hwid); + } + + @Test + void generateHwidIsStableAcrossCalls() { + String first = getHWID.generateHWID(); + String second = getHWID.generateHWID(); + assertEquals(first, second, "同一台机器两次调用应得到相同的 HWID"); + } +} diff --git a/src/test/java/top/gtb520/java/gnu/java/utils/library/utils/processing/random/RandomTest.java b/src/test/java/top/gtb520/java/gnu/java/utils/library/utils/processing/random/RandomTest.java new file mode 100644 index 0000000..54c9e30 --- /dev/null +++ b/src/test/java/top/gtb520/java/gnu/java/utils/library/utils/processing/random/RandomTest.java @@ -0,0 +1,62 @@ +package top.gtb520.java.gnu.java.utils.library.utils.processing.random; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class RandomTest { + + @Test + void randomWithinRange() { + for (int i = 0; i < 1000; i++) { + int result = defaults.random(1, 10); + assertTrue(result >= 1 && result <= 10, "随机数应在[1,10]范围内: " + result); + } + } + + @Test + void randomSameStartAndEnd() { + assertEquals(5, defaults.random(5, 5)); + } + + @Test + void randomNegativeRange() { + for (int i = 0; i < 1000; i++) { + int result = defaults.random(-10, -1); + assertTrue(result >= -10 && result <= -1); + } + } + + @Test + void randomMaxValueBoundaryDoesNotOverflow() { + // 上界为 Integer.MAX_VALUE 时 +1 不再溢出 + for (int i = 0; i < 1000; i++) { + int result = defaults.random(0, Integer.MAX_VALUE); + assertTrue(result >= 0); + } + int result = defaults.random(Integer.MAX_VALUE - 10, Integer.MAX_VALUE); + assertTrue(result >= Integer.MAX_VALUE - 10 && result <= Integer.MAX_VALUE); + } + + @Test + void randomReversedRangeThrows() { + assertThrows(IllegalArgumentException.class, () -> defaults.random(10, 1)); + } + + @Test + void randomStringLengthAndCharset() { + String result = defaults.randomString(16); + assertEquals(16, result.length()); + assertTrue(result.matches("[A-Za-z0-9]{16}"), "应只包含大小写字母和数字: " + result); + } + + @Test + void randomStringZeroLength() { + assertEquals("", defaults.randomString(0)); + } + + @Test + void randomStringNegativeLengthThrows() { + assertThrows(IllegalArgumentException.class, () -> defaults.randomString(-1)); + } +} diff --git a/src/test/java/top/gtb520/java/gnu/java/utils/library/utils/processing/random/Test.java b/src/test/java/top/gtb520/java/gnu/java/utils/library/utils/processing/random/Test.java deleted file mode 100644 index f08e695..0000000 --- a/src/test/java/top/gtb520/java/gnu/java/utils/library/utils/processing/random/Test.java +++ /dev/null @@ -1,47 +0,0 @@ -package top.gtb520.java.gnu.java.utils.library.utils.processing.random; - -import top.gtb520.java.gnu.java.utils.library.main; - -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.ThreadLocalRandom; - -// 毫无意义的随机数测试代码,在循环中执行随机数生成 10 亿次 -public class Test { - public static void main(String[] args) { - int totalTasks = 1_000_000_000; - // 根据CPU核心数创建固定大小的线程池,避免创建过多线程导致上下文切换开销 - int threadCount = Runtime.getRuntime().availableProcessors(); - ExecutorService executor = Executors.newFixedThreadPool(threadCount); - - // 将总任务量平均分配给每个线程 - int tasksPerThread = totalTasks / threadCount; - - for (int t = 0; t < threadCount; t++) { - final int start = t * tasksPerThread; - // 处理余数,确保最后一个线程承担剩余任务 - final int end = (t == threadCount - 1) ? totalTasks : start + tasksPerThread; - - executor.submit(() -> { - // 【核心优化1】使用 ThreadLocalRandom 替代共享 Random - // 每个线程拥有独立的 seed,完全无锁,避免高并发下的 CAS 竞争 - ThreadLocalRandom random = ThreadLocalRandom.current(); - - // 【核心优化2】将 I/O 与计算分离 - // 在实际业务中,应只返回计算结果,而不是在循环中打印 - for (int i = start; i < end; i++) { - int random_i = random.nextInt(1, 1000000001); // [1, 1000000000] - - // 如果确实需要输出,建议降低频率(例如每1000万次输出一次) -// if (i % 10000 == 0) { -// System.out.println("随机数: " + random_i + " 当前循环位置: " + i); -// } - System.out.println("随机数: " + random_i + " 当前循环位置: " + i); - } - }); - } - - // 优雅关闭线程池 - executor.shutdown(); - } -} diff --git a/src/test/java/top/gtb520/java/gnu/java/utils/library/utils/processing/random/defauleTest.java b/src/test/java/top/gtb520/java/gnu/java/utils/library/utils/processing/random/defauleTest.java deleted file mode 100644 index 75292e5..0000000 --- a/src/test/java/top/gtb520/java/gnu/java/utils/library/utils/processing/random/defauleTest.java +++ /dev/null @@ -1,142 +0,0 @@ -package top.gtb520.java.gnu.java.utils.library.utils.processing.random; - -public class defauleTest { - - static int passed = 0; - static int failed = 0; - - public static void main(String[] args) { - System.out.println("========== defaults.random 测试开始 =========="); - - testRandomInRange(); - testRandomSameStartAndEnd(); - testRandomNegativeRange(); - testRandomReversedRange(); - testRandomLargeRange(); - testRandomDistribution(); - - System.out.println("========================================"); - System.out.println("测试完成: 通过 " + passed + ", 失败 " + failed); - if (failed > 0) { - System.exit(1); - } - } - - /** - * 测试正常范围内的随机数 - */ - static void testRandomInRange() { - String testName = "testRandomInRange"; - try { - for (int i = 0; i < 100; i++) { - int result = defaults.random(1, 10); - if (result < 1 || result > 10) { - assertCondition(testName + " - 随机数应在[1,10]范围内,实际: " + result, false); - return; - } - } - assertCondition(testName + " - 100次随机数均在[1,10]范围内", true); - } catch (Exception e) { - assertCondition(testName + " - 不应抛出异常: " + e.getMessage(), false); - } - } - - /** - * 测试起始值等于结束值 - */ - static void testRandomSameStartAndEnd() { - String testName = "testRandomSameStartAndEnd"; - try { - int result = defaults.random(5, 5); - assertCondition(testName + " - 结果应等于5,实际: " + result, result == 5); - } catch (Exception e) { - assertCondition(testName + " - 不应抛出异常: " + e.getMessage(), false); - } - } - - /** - * 测试负数范围 - */ - static void testRandomNegativeRange() { - String testName = "testRandomNegativeRange"; - try { - for (int i = 0; i < 100; i++) { - int result = defaults.random(-10, -1); - if (result < -10 || result > -1) { - assertCondition(testName + " - 随机数应在[-10,-1]范围内,实际: " + result, false); - return; - } - } - assertCondition(testName + " - 100次随机数均在[-10,-1]范围内", true); - } catch (Exception e) { - assertCondition(testName + " - 不应抛出异常: " + e.getMessage(), false); - } - } - - /** - * 测试起始值大于结束值应抛出异常 - */ - static void testRandomReversedRange() { - String testName = "testRandomReversedRange"; - try { - defaults.random(10, 1); - assertCondition(testName + " - 应抛出IllegalArgumentException", false); - } catch (IllegalArgumentException e) { - assertCondition(testName + " - 应抛出IllegalArgumentException", true); - } catch (Exception e) { - assertCondition(testName + " - 异常类型不正确: " + e.getClass().getName(), false); - } - } - - /** - * 测试大范围随机数 - */ - static void testRandomLargeRange() { - String testName = "testRandomLargeRange"; - try { - for (int i = 0; i < 100; i++) { - int result = defaults.random(-1000000, 1000000); - if (result < -1000000 || result > 1000000) { - assertCondition(testName + " - 随机数超出范围,实际: " + result, false); - return; - } - } - assertCondition(testName + " - 100次随机数均在[-1000000,1000000]范围内", true); - } catch (Exception e) { - assertCondition(testName + " - 不应抛出异常: " + e.getMessage(), false); - } - } - - /** - * 测试随机数分布(验证不会总是返回同一个值) - */ - static void testRandomDistribution() { - String testName = "testRandomDistribution"; - try { - int first = defaults.random(1, 1000000); - boolean hasDifferent = false; - for (int i = 0; i < 100; i++) { - int result = defaults.random(1, 1000000); - if (result != first) { - hasDifferent = true; - break; - } - } - assertCondition(testName + " - 100次调用应产生不同随机数", hasDifferent); - } catch (Exception e) { - assertCondition(testName + " - 不应抛出异常: " + e.getMessage(), false); - } - } - - // ========== 辅助方法 ========== - - static void assertCondition(String message, boolean condition) { - if (condition) { - System.out.println("[PASS] " + message); - passed++; - } else { - System.out.println("[FAIL] " + message); - failed++; - } - } -} diff --git a/src/test/java/top/gtb520/java/gnu/java/utils/library/utils/processing/random/randomStringTest.java b/src/test/java/top/gtb520/java/gnu/java/utils/library/utils/processing/random/randomStringTest.java deleted file mode 100644 index 06d09c6..0000000 --- a/src/test/java/top/gtb520/java/gnu/java/utils/library/utils/processing/random/randomStringTest.java +++ /dev/null @@ -1,8 +0,0 @@ -package top.gtb520.java.gnu.java.utils.library.utils.processing.random; - -public class randomStringTest { - public static void main(String[] args) { - defaults.random(1, 100); - defaults.randomString(10); - } -} diff --git a/src/test/java/top/gtb520/java/gnu/java/utils/library/utils/runtime/RunEnvTest.java b/src/test/java/top/gtb520/java/gnu/java/utils/library/utils/runtime/RunEnvTest.java index ef02f61..7bb0d83 100644 --- a/src/test/java/top/gtb520/java/gnu/java/utils/library/utils/runtime/RunEnvTest.java +++ b/src/test/java/top/gtb520/java/gnu/java/utils/library/utils/runtime/RunEnvTest.java @@ -1,11 +1,62 @@ package top.gtb520.java.gnu.java.utils.library.utils.runtime; -import static top.gtb520.java.gnu.java.utils.library.utils.runtime.RunEnv.*; +import org.junit.jupiter.api.Test; + +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.*; class RunEnvTest { - public static void main() { - System.out.println(getJarPath()); - System.out.printf(newJarThisTempDir() + "\n"); - System.out.println(newTempDir()); + + @Test + void getTempDirNotNull() { + assertNotNull(RunEnv.getTempDir()); + assertFalse(RunEnv.getTempDir().isEmpty()); } -} \ No newline at end of file + + @Test + void newTempDirCreatesUniqueDirectories() throws Exception { + String first = RunEnv.newTempDir(); + String second = RunEnv.newTempDir(); + try { + assertNotNull(first); + assertNotNull(second); + assertNotEquals(first, second, "连续两次调用应得到不同的目录"); + assertTrue(Files.isDirectory(Path.of(first))); + assertTrue(Files.isDirectory(Path.of(second))); + } finally { + Files.deleteIfExists(Path.of(first)); + Files.deleteIfExists(Path.of(second)); + } + } + + @Test + void getJarPathResolvesToExistingLocation() { + // 测试环境下 code source 指向 target/test-classes 或 classes 目录 + String jarPath = RunEnv.getJarPath(); + assertNotNull(jarPath); + assertFalse(jarPath.isEmpty()); + assertTrue(Files.exists(Path.of(jarPath)), "getJarPath 应返回存在的路径: " + jarPath); + } + + @Test + void newJarThisTempDirCreatesDirectory() throws Exception { + Path parent = Path.of(RunEnv.getJarPath()).getParent(); + if (parent == null || !Files.isDirectory(parent)) { + // 类路径解析出的位置没有父目录时跳过 + return; + } + String created = RunEnv.newJarThisTempDir(); + try { + assertNotNull(created); + assertTrue(Files.isDirectory(Path.of(created))); + assertEquals(parent.toAbsolutePath().normalize(), + Path.of(created).getParent().toAbsolutePath().normalize()); + } finally { + if (created != null) { + Files.deleteIfExists(Path.of(created)); + } + } + } +}