Compare commits
4
Commits
bf3b92d206
...
30f2c04d13
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30f2c04d13 | ||
|
|
15357768c7 | ||
|
|
b1d4fed408 | ||
|
|
cfea5ed26b |
No files matched your search
@@ -44,3 +44,4 @@ build/
|
|||||||
/.mvn
|
/.mvn
|
||||||
/build_release
|
/build_release
|
||||||
/upload/
|
/upload/
|
||||||
|
cp.txt
|
||||||
@@ -1,20 +1,36 @@
|
|||||||
1.5-SNAPSHOT
|
1.7-SNAPSHOT
|
||||||
|
|
||||||
此版本包含:
|
此版本包含:
|
||||||
1. 压缩/解压 (`compress`)
|
1. 压缩/解压 (`compress`)
|
||||||
|
1.1 Tar.Gz / Zip 压缩与解压
|
||||||
|
1.2 PNG 图片压缩
|
||||||
2. 终端彩色输出 (`echo`)
|
2. 终端彩色输出 (`echo`)
|
||||||
2.1 预定义颜色打印 — `ColorPrintln`
|
2.1 预定义颜色打印 — `ColorPrintln`
|
||||||
2.2 自定义颜色打印 — `ColorHexPrintln`
|
2.2 自定义颜色打印 — `ColorHexPrintln`
|
||||||
2.3 Minecraft 风格多色打印 — `ColorPrintlnPlus`
|
2.3 Minecraft 风格多色打印 — `ColorPrintlnPlus`(格式码支持叠加)
|
||||||
3. 文件创建与保存 (`file/CreateFile`)
|
3. 文件创建与保存 (`file/CreateFile`)
|
||||||
4. 文件加密/解密 (`file/EncryptionAndDecryption`)
|
4. 文件加密/解密 (`file/EncryptionAndDecryption`)
|
||||||
|
4.1 AES-256/GCM 加密
|
||||||
|
4.2 RSA-2048/OAEP 混合加密
|
||||||
|
4.3 DES 加密(已废弃,仅兼容旧版本)
|
||||||
5. 随机生成器 (`random`)
|
5. 随机生成器 (`random`)
|
||||||
6. 硬件标识生成器 (`hwid`)
|
6. 硬件标识生成器 (`hwid`)
|
||||||
7. 版本信息 (`version`)
|
7. 版本信息 (`version`)
|
||||||
8. 运行时信息 (`runtime`)
|
8. 运行时信息 (`runtime`)
|
||||||
6.1 获取运行时临时目录
|
8.1 获取运行时临时目录
|
||||||
6.2 新建临时目录
|
8.2 新建临时目录
|
||||||
6.3 获取临时目录
|
8.3 获取当前运行的 JAR 文件路径
|
||||||
6.4 获取当前运行的JAR文件路径
|
|
||||||
|
|
||||||
在 1.4-SNAPSHOT 的基础上调整了API接口,修复了一些BUG
|
1.7 更新内容:
|
||||||
|
- 安全:修复 Zip-Slip 路径穿越漏洞,解压时校验条目路径并拒绝符号/硬链接条目
|
||||||
|
- 安全:AES 升级为 AES-256/GCM(随机 IV + 认证标签),RSA 实现真实混合加密
|
||||||
|
(RSA-2048/OAEP 包裹随机 AES-256 会话密钥),密钥文件限制为仅属主可读写
|
||||||
|
- 健壮性:加解密改为流式处理(支持大文件),临时文件 + 原子替换,中途失败不损坏原数据
|
||||||
|
- 修复:随机数上界 Integer.MAX_VALUE 溢出;ColorPrintlnPlus 格式码叠加(颜色+加粗等)
|
||||||
|
- 修复:getHWID 命令执行增加超时并并发排空输出流,Windows 改用 PowerShell CIM(wmic 回退),
|
||||||
|
修正 Linux 磁盘序列号命令,MAC 优先物理网卡
|
||||||
|
- 修复:getJarPath 经 URI 解析(正确处理空格与 URL 编码),临时目录改用唯一命名
|
||||||
|
- 工程:测试全部迁移至 JUnit 5(53 个用例),pom 补充元数据与插件版本,
|
||||||
|
版本号统一由 pom / MANIFEST Implementation-Version 提供
|
||||||
|
|
||||||
|
在 1.6-SNAPSHOT 的基础上进行安全加固与缺陷修复。
|
||||||
@@ -41,7 +41,7 @@ src/main/java/top/gtb520/java/gnu/java/utils/library/
|
|||||||
|
|
||||||
### 1. 压缩/解压 (`compress`)
|
### 1. 压缩/解压 (`compress`)
|
||||||
|
|
||||||
支持 **Tar.Gz** 和 **Zip** 两种格式的压缩与解压,支持单文件和目录递归处理。
|
支持 **Tar.Gz** 和 **Zip** 两种格式的压缩与解压,支持单文件和目录递归处理;另提供 PNG 图片压缩。
|
||||||
|
|
||||||
| 方法 | 说明 | 参数 |
|
| 方法 | 说明 | 参数 |
|
||||||
|------------------------------------|--------------------------|-------------------------------------|
|
|------------------------------------|--------------------------|-------------------------------------|
|
||||||
@@ -49,6 +49,10 @@ src/main/java/top/gtb520/java/gnu/java/utils/library/
|
|||||||
| `compress.UnTarGz(source, dest)` | 解压 `.tar.gz` 到指定目录 | `source` 压缩包路径,`dest` 解压目标目录 |
|
| `compress.UnTarGz(source, dest)` | 解压 `.tar.gz` 到指定目录 | `source` 压缩包路径,`dest` 解压目标目录 |
|
||||||
| `compress.MakeZip(source, dest)` | 将文件或目录压缩为 `.zip` | `source` 源路径,`dest` 压缩包输出路径 |
|
| `compress.MakeZip(source, dest)` | 将文件或目录压缩为 `.zip` | `source` 源路径,`dest` 压缩包输出路径 |
|
||||||
| `compress.UnZip(source, dest)` | 解压 `.zip` 到指定目录 | `source` 压缩包路径,`dest` 解压目标目录 |
|
| `compress.UnZip(source, dest)` | 解压 `.zip` 到指定目录 | `source` 压缩包路径,`dest` 解压目标目录 |
|
||||||
|
| `CompressPNG.PNG(pngFile, threshold)` | 压缩 PNG 图片,返回原文件 | `pngFile` PNG 文件,`threshold` 压缩阈值(1~5 档) |
|
||||||
|
|
||||||
|
> **安全说明**:解压时会校验条目路径,压缩包内含 `../` 越界路径或符号/硬链接条目将抛出
|
||||||
|
> `IllegalArgumentException`(防 Zip-Slip 攻击)。PNG 压缩采用临时文件 + 原子替换,写入失败不会损坏原图。
|
||||||
|
|
||||||
**使用示例:**
|
**使用示例:**
|
||||||
|
|
||||||
@@ -195,12 +199,12 @@ main.echo.ColorPrintlnPlus("&a多色 &c文本");
|
|||||||
|
|
||||||
### 3. 文件创建与保存 (`file/CreateFile`)
|
### 3. 文件创建与保存 (`file/CreateFile`)
|
||||||
|
|
||||||
支持创建普通/二进制文件,以及异步字节流文件保存。
|
支持创建普通/二进制文件,以及字节流文件保存。
|
||||||
|
|
||||||
| 方法 | 说明 | 参数 |
|
| 方法 | 说明 | 参数 |
|
||||||
|-------------------------------------------------------|---------------------------------------|-------------------------------------------------------------|
|
|-------------------------------------------------------|---------------------------------------|-------------------------------------------------------------|
|
||||||
| `CreateFile.NewFile(FileName, isBin, binFileType)` | 创建空白文件,二进制文件自动写入文件头 | `FileName` 文件名,`isBin` 是否二进制,`binFileType` 二进制类型(普通文件传 `null`) |
|
| `CreateFile.NewFile(FileName, isBin, binFileType)` | 创建空白文件,二进制文件自动写入文件头;文件已存在时直接返回不覆盖 | `FileName` 文件名,`isBin` 是否二进制,`binFileType` 二进制类型(普通文件传 `null`) |
|
||||||
| `CreateFile.StreamSaveFile(file, TargePath, FileName)` | 以字节流方式异步保存文件到目标路径 | `file` 源文件,`TargePath` 目标目录,`FileName` 保存的文件名 |
|
| `CreateFile.StreamSaveFile(file, TargePath, FileName)` | 以字节流方式保存文件到目标路径(覆盖同名文件) | `file` 源文件,`TargePath` 目标目录,`FileName` 保存的文件名 |
|
||||||
|
|
||||||
**二进制文件类型 (`BinFileType`):**
|
**二进制文件类型 (`BinFileType`):**
|
||||||
|
|
||||||
@@ -224,29 +228,32 @@ File elfFile = CreateFile.NewFile("/path/to/program", true, BinFileType.ELF);
|
|||||||
// 创建 PE 二进制文件(自动写入 MZ 文件头)
|
// 创建 PE 二进制文件(自动写入 MZ 文件头)
|
||||||
File peFile = CreateFile.NewFile("/path/to/app.exe", true, BinFileType.PE);
|
File peFile = CreateFile.NewFile("/path/to/app.exe", true, BinFileType.PE);
|
||||||
|
|
||||||
// 异步字节流保存文件
|
// 字节流保存文件(覆盖同名文件)
|
||||||
boolean success = CreateFile.StreamSaveFile(sourceFile, Path.of("/path/to/dest"), "output.bin");
|
boolean success = CreateFile.StreamSaveFile(sourceFile, Path.of("/path/to/dest"), "output.bin");
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. 文件加密/解密 (`file/EncryptionAndDecryption`)
|
### 4. 文件加密/解密 (`file/EncryptionAndDecryption`)
|
||||||
|
|
||||||
支持 **AES**、**DES** 对称加密,加密后覆盖原文件,密钥单独保存到指定目录。解密时从密钥文件读取密钥,解密后覆盖还原原文件。
|
支持 **AES**、**RSA** 加密与 **DES**(已废弃,仅兼容旧版本)解密,加密后覆盖原文件,密钥单独保存到指定目录。解密时从密钥文件读取密钥,解密后覆盖还原原文件。
|
||||||
|
|
||||||
| 方法 | 说明 | 参数 |
|
| 方法 | 说明 | 参数 |
|
||||||
|----------------------------------------------------------------|--------------------------|-----------------------------------------------|
|
|----------------------------------------------------------------|--------------------------|-----------------------------------------------|
|
||||||
| `EncryptionAndDecryption.EncryptionFile(file, type, keyPath)` | 异步加密文件,覆盖原文件,密钥保存到 keyPath | `file` 文件,`type` 加密类型,`keyPath` 密钥目录路径 |
|
| `EncryptionAndDecryption.EncryptionFile(file, type, keyPath)` | 加密文件,覆盖原文件,密钥保存到 keyPath | `file` 文件,`type` 加密类型,`keyPath` 密钥目录路径 |
|
||||||
| `EncryptionAndDecryption.DecryptionFile(file, type, keyfile)` | 异步解密文件,覆盖还原原文件 | `file` 加密文件,`type` 加密类型,`keyfile` 密钥文件 |
|
| `EncryptionAndDecryption.DecryptionFile(file, type, keyfile)` | 解密文件,覆盖还原原文件 | `file` 加密文件,`type` 加密类型,`keyfile` 密钥文件 |
|
||||||
|
|
||||||
**加密类型 (`EncryptionType`):**
|
**加密类型 (`EncryptionType`):**
|
||||||
|
|
||||||
| 枚举值 | 算法 | 密钥长度 |
|
| 枚举值 | 算法 | 密钥长度 |
|
||||||
|-------|---------------------|---------|
|
|-------|---------------------|---------|
|
||||||
| `AES` | AES/ECB/PKCS5Padding | 128 bit |
|
| `AES` | AES-256/GCM(随机 IV + 认证标签) | 256 bit |
|
||||||
| `DES` | DES/ECB/PKCS5Padding | 56 bit |
|
| `RSA` | RSA-2048/OAEP 混合加密(RSA 包裹随机 AES-256 会话密钥,数据用 AES-GCM 加密) | 2048 bit |
|
||||||
| `RSA` | AES(对称替代) | 128 bit |
|
| `DES` | DES/ECB/PKCS5Padding(已废弃,不安全) | 56 bit |
|
||||||
|
|
||||||
> 加密后密钥文件命名为 `原文件名.key`,保存在 `keyPath` 目录下。
|
> 加密后密钥文件命名为 `原文件名.key`,保存在 `keyPath` 目录下:AES/DES 保存原始密钥,RSA 保存 PKCS#8 私钥;
|
||||||
> 加密/解密均覆盖写回原文件,不添加额外后缀。
|
> POSIX 系统上密钥文件权限自动限制为仅属主可读写。
|
||||||
|
> 加密/解密均为流式处理,支持大文件;先写临时文件、成功后原子替换原文件,中途失败不会损坏原数据。
|
||||||
|
> 入参非法(文件/密钥不存在或为 null)返回 `null`;加解密实际失败(密钥错误、密文损坏等)抛出 `RuntimeException`。
|
||||||
|
> 新代码请使用 `AES` 或 `RSA`,不要使用已废弃的 `DES`。
|
||||||
|
|
||||||
```java
|
```java
|
||||||
import top.gtb520.java.gnu.java.utils.library.utils.file.EncryptionAndDecryption;
|
import top.gtb520.java.gnu.java.utils.library.utils.file.EncryptionAndDecryption;
|
||||||
@@ -276,10 +283,12 @@ File decrypted = EncryptionAndDecryption.DecryptionFile(file, EncryptionType.AES
|
|||||||
```java
|
```java
|
||||||
import top.gtb520.java.gnu.java.utils.library.utils.processing.random.defaults;
|
import top.gtb520.java.gnu.java.utils.library.utils.processing.random.defaults;
|
||||||
|
|
||||||
int num = defaults.random(1, 100); // 生成 1~100 的随机整数
|
int num = defaults.random(1, 100); // 生成 1~100 的随机整数(包含两端)
|
||||||
String str = defaults.randomString(16); // 生成 16 位随机字符串(大小写字母+数字)
|
String str = defaults.randomString(16); // 生成 16 位随机字符串(大小写字母+数字)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> `random(start, end)` 结果包含两端;`randomString(length)` 的长度不能为负数,否则抛出 `IllegalArgumentException`。
|
||||||
|
|
||||||
### 6. 硬件标识生成器 (`hwid`)
|
### 6. 硬件标识生成器 (`hwid`)
|
||||||
|
|
||||||
组合 CPU、主板、MAC 地址、硬盘序列号等信息,通过 SHA-256 生成唯一的硬件标识符(HWID)。支持 Windows、macOS、Linux 及 Android 平台。
|
组合 CPU、主板、MAC 地址、硬盘序列号等信息,通过 SHA-256 生成唯一的硬件标识符(HWID)。支持 Windows、macOS、Linux 及 Android 平台。
|
||||||
@@ -295,6 +304,9 @@ String hwid = getHWID.generateHWID();
|
|||||||
System.out.println("HWID: " + hwid);
|
System.out.println("HWID: " + hwid);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> 单项硬件信息获取失败时自动跳过该组件(如无序列号的硬件);全部失败时返回 `"UNKNOWN_HWID"`。
|
||||||
|
> MAC 地址优先取非回环、非虚拟网卡,避免虚拟网卡导致 HWID 漂移。
|
||||||
|
|
||||||
### 7. 版本信息 (`version`)
|
### 7. 版本信息 (`version`)
|
||||||
|
|
||||||
提供项目版本信息及编译时 Java/OpenJDK 版本常量。
|
提供项目版本信息及编译时 Java/OpenJDK 版本常量。
|
||||||
@@ -304,6 +316,8 @@ System.out.println("HWID: " + hwid);
|
|||||||
| `version.GetVersion()` | 获取项目版本号 |
|
| `version.GetVersion()` | 获取项目版本号 |
|
||||||
| `version.GetJavaVersion()` | 获取编译时 Java 版本 |
|
| `version.GetJavaVersion()` | 获取编译时 Java 版本 |
|
||||||
|
|
||||||
|
> `GetVersion()` 打包为 JAR 时读取 MANIFEST 中的 `Implementation-Version`,类路径运行(如测试)时回退到内置常量。
|
||||||
|
|
||||||
```java
|
```java
|
||||||
import top.gtb520.java.gnu.java.utils.library.version;
|
import top.gtb520.java.gnu.java.utils.library.version;
|
||||||
|
|
||||||
@@ -332,10 +346,10 @@ main.version.getOpenJDK64BitServerVM(); // OpenJDK 64-Bit Server VM
|
|||||||
|
|
||||||
| 方法 | 说明 |
|
| 方法 | 说明 |
|
||||||
|-------------------------------|------------------------------------------|
|
|-------------------------------|------------------------------------------|
|
||||||
| `RunEnv.getJarPath()` | 获取当前运行的 JAR 路径 |
|
| `RunEnv.getJarPath()` | 获取当前运行的 JAR 路径(非 JAR 运行时返回类路径目录) |
|
||||||
| `RunEnv.getTempDir()` | 获取系统临时目录路径 |
|
| `RunEnv.getTempDir()` | 获取系统临时目录路径 |
|
||||||
| `RunEnv.newTempDir()` | 在系统临时目录下新建时间戳子目录,返回路径;已存在则返回 `null` |
|
| `RunEnv.newTempDir()` | 在系统临时目录下新建唯一临时子目录,返回路径;创建失败抛出 `RuntimeException` |
|
||||||
| `RunEnv.newJarThisTempDir()` | 在 JAR 所在目录下新建时间戳子目录,返回路径;已存在则返回 `null` |
|
| `RunEnv.newJarThisTempDir()` | 在 JAR 所在目录下新建唯一临时子目录,返回路径;JAR 路径无父目录时返回 `null` |
|
||||||
|
|
||||||
**Java 运行时环境 (`JavaRunTimeEnv`):**
|
**Java 运行时环境 (`JavaRunTimeEnv`):**
|
||||||
|
|
||||||
@@ -390,9 +404,19 @@ mvn compile
|
|||||||
mvn package
|
mvn package
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 测试
|
||||||
|
|
||||||
|
项目使用 JUnit 5 编写测试(共 53 个用例),运行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mvn test
|
||||||
|
```
|
||||||
|
|
||||||
|
测试覆盖压缩/解压往返、Zip-Slip 恶意压缩包防护、AES/DES/RSA 加解密往返(含 8MB 大文件流式测试与错误密钥不损坏密文)、随机数边界(含 `Integer.MAX_VALUE` 上界)、彩色输出格式叠加与注入防护、临时目录创建与 HWID 稳定性。
|
||||||
|
|
||||||
## 使用
|
## 使用
|
||||||
|
|
||||||
编译成功后,将 `target/gnu-java-utils-library-1.6-SNAPSHOT.jar` 放入项目依赖中,或在 Releases 中下载最新版 JAR 包。
|
编译成功后,将 `target/gnu_java_utils_library-1.7-SNAPSHOT.jar` 放入项目依赖中,或在 Releases 中下载最新版 JAR 包。
|
||||||
|
|
||||||
**pom.xml 引入本地依赖:**
|
**pom.xml 引入本地依赖:**
|
||||||
|
|
||||||
@@ -401,9 +425,9 @@ mvn package
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>top.gtb520.java.gnu</groupId>
|
<groupId>top.gtb520.java.gnu</groupId>
|
||||||
<artifactId>gnu_java_utils_library</artifactId>
|
<artifactId>gnu_java_utils_library</artifactId>
|
||||||
<version>1.6-SNAPSHOT</version>
|
<version>1.7-SNAPSHOT</version>
|
||||||
<scope>system</scope>
|
<scope>system</scope>
|
||||||
<systemPath>${project.basedir}/lib/gnu_java_utils_library-1.6-SNAPSHOT.jar</systemPath>
|
<systemPath>${project.basedir}/lib/gnu_java_utils_library-1.7-SNAPSHOT.jar</systemPath>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
```
|
```
|
||||||
@@ -414,10 +438,17 @@ mvn package
|
|||||||
|
|
||||||
- **压缩时**:源路径不存在
|
- **压缩时**:源路径不存在
|
||||||
- **解压时**:目标路径不是有效的目录
|
- **解压时**:目标路径不是有效的目录
|
||||||
|
- **解压时**:压缩包包含越界路径(如 `../`)或符号/硬链接条目(防 Zip-Slip 攻击)
|
||||||
|
|
||||||
随机数方法在以下情况会抛出 `IllegalArgumentException`:
|
随机数方法在以下情况会抛出 `IllegalArgumentException`:
|
||||||
|
|
||||||
- **起始值大于结束值时**
|
- **起始值大于结束值时**
|
||||||
|
- **随机字符串长度为负数时**
|
||||||
|
|
||||||
|
加密/解密方法的错误约定:
|
||||||
|
|
||||||
|
- **入参非法**(文件/密钥不存在或为 `null`):返回 `null`
|
||||||
|
- **加解密实际失败**(密钥错误、密文损坏等):抛出 `RuntimeException`
|
||||||
|
|
||||||
## 许可证
|
## 许可证
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -6,12 +6,26 @@
|
|||||||
|
|
||||||
<groupId>top.gtb520.java.gnu</groupId>
|
<groupId>top.gtb520.java.gnu</groupId>
|
||||||
<artifactId>gnu_java_utils_library</artifactId>
|
<artifactId>gnu_java_utils_library</artifactId>
|
||||||
<version>1.6-SNAPSHOT</version>
|
<version>1.7-SNAPSHOT</version>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
|
<name>GNU Java Utils Library</name>
|
||||||
|
<description>轻量级 Java 工具库:压缩/解压、终端彩色输出、文件创建与管理、文件加密/解密、随机数生成、硬件标识与运行时信息。</description>
|
||||||
|
<url>https://github.com/databasemysql233-debug/gnu_java_utils_library</url>
|
||||||
|
|
||||||
|
<licenses>
|
||||||
|
<license>
|
||||||
|
<name>Apache License, Version 2.0</name>
|
||||||
|
<url>https://www.apache.org/licenses/LICENSE-2.0</url>
|
||||||
|
<distribution>repo</distribution>
|
||||||
|
</license>
|
||||||
|
</licenses>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<maven.compiler.source>25</maven.compiler.source>
|
<maven.compiler.release>25</maven.compiler.release>
|
||||||
<maven.compiler.target>25</maven.compiler.target>
|
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
<junit.version>5.13.0</junit.version>
|
||||||
|
<surefire.version>3.6.0</surefire.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
@@ -23,9 +37,34 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.junit.jupiter</groupId>
|
<groupId>org.junit.jupiter</groupId>
|
||||||
<artifactId>junit-jupiter</artifactId>
|
<artifactId>junit-jupiter</artifactId>
|
||||||
<version>5.13.0</version>
|
<version>${junit.version}</version>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</project>
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-surefire-plugin</artifactId>
|
||||||
|
<version>${surefire.version}</version>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-jar-plugin</artifactId>
|
||||||
|
<version>3.4.2</version>
|
||||||
|
<configuration>
|
||||||
|
<archive>
|
||||||
|
<manifest>
|
||||||
|
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
|
||||||
|
</manifest>
|
||||||
|
<manifestEntries>
|
||||||
|
<Automatic-Module-Name>top.gtb520.java.gnu.utils.library</Automatic-Module-Name>
|
||||||
|
</manifestEntries>
|
||||||
|
</archive>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
|
</project>
|
||||||
+63
-32
@@ -5,61 +5,92 @@ import javax.imageio.ImageIO;
|
|||||||
import javax.imageio.ImageWriteParam;
|
import javax.imageio.ImageWriteParam;
|
||||||
import javax.imageio.ImageWriter;
|
import javax.imageio.ImageWriter;
|
||||||
import javax.imageio.stream.ImageOutputStream;
|
import javax.imageio.stream.ImageOutputStream;
|
||||||
import java.io.File;
|
|
||||||
import java.awt.image.BufferedImage;
|
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;
|
import java.util.Iterator;
|
||||||
|
|
||||||
public class CompressPNG {
|
public class CompressPNG {
|
||||||
/***
|
/**
|
||||||
* 压缩PNG图片
|
* 压缩PNG图片
|
||||||
* 传入未压缩的PNG图片文件,和压缩阈值,返回压缩后的PNG图片文件
|
* 传入未压缩的PNG图片文件,和压缩阈值,返回压缩后的PNG图片文件
|
||||||
* 5个等级的压缩阈值,从低到高,THRESHOLD_1 最低,THRESHOLD_5 最高
|
* 5个等级的压缩阈值,从低到高,THRESHOLD_1 最低,THRESHOLD_5 最高
|
||||||
*
|
*
|
||||||
|
* 注意:写入过程先写同目录临时文件、成功后原子替换原文件,避免写入失败损坏原图;
|
||||||
|
* JDK 内置 PNG writer 对 compressionQuality 的支持取决于实现,档位效果可能有限。
|
||||||
|
*
|
||||||
* @param pngFile 未压缩的PNG图片文件
|
* @param pngFile 未压缩的PNG图片文件
|
||||||
* @param threshold 压缩阈值
|
* @param threshold 压缩阈值,不能为 null
|
||||||
* @return 压缩后的PNG图片文件
|
* @return 压缩后的PNG图片文件;文件不存在或不是 PNG 时返回 null
|
||||||
|
* @throws IllegalArgumentException 当 threshold 为 null 时
|
||||||
*/
|
*/
|
||||||
public static File PNG(File pngFile, PNGThreshold threshold) {
|
public static File PNG(File pngFile, PNGThreshold threshold) {
|
||||||
if (!pngFile.exists()) {
|
if (threshold == null) {
|
||||||
System.out.println("文件不存在");
|
throw new IllegalArgumentException("压缩阈值不能为 null");
|
||||||
|
}
|
||||||
|
if (pngFile == null || !pngFile.exists()) {
|
||||||
return null;
|
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<ImageWriter> writers = ImageIO.getImageWritersByFormatName("png");
|
||||||
|
if (!writers.hasNext()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
ImageWriter writer = writers.next();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
BufferedImage image = ImageIO.read(pngFile);
|
tmp = File.createTempFile(pngFile.getName() + ".", ".tmp",
|
||||||
if (image == null) {
|
pngFile.getAbsoluteFile().getParentFile());
|
||||||
System.out.println("无法读取PNG文件");
|
try (ImageOutputStream ios = ImageIO.createImageOutputStream(tmp)) {
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("png");
|
|
||||||
if (!writers.hasNext()) {
|
|
||||||
System.out.println("未找到PNG写入器");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
ImageWriter writer = writers.next();
|
|
||||||
|
|
||||||
try (ImageOutputStream ios = ImageIO.createImageOutputStream(pngFile)) {
|
|
||||||
writer.setOutput(ios);
|
writer.setOutput(ios);
|
||||||
|
|
||||||
ImageWriteParam param = writer.getDefaultWriteParam();
|
ImageWriteParam param = writer.getDefaultWriteParam();
|
||||||
param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
|
try {
|
||||||
param.setCompressionQuality(getCompressionQuality(threshold));
|
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);
|
writer.write(null, new IIOImage(image, null, null), param);
|
||||||
} finally {
|
|
||||||
writer.dispose();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return pngFile;
|
// 原子替换原文件,避免原地写入失败损坏原图
|
||||||
} catch (Exception e) {
|
try {
|
||||||
e.printStackTrace();
|
Files.move(tmp.toPath(), pngFile.toPath(),
|
||||||
return null;
|
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) {
|
private static float getCompressionQuality(PNGThreshold threshold) {
|
||||||
|
|||||||
@@ -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.TarArchiveEntry;
|
||||||
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
|
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
|
||||||
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
|
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.ZipArchiveEntry;
|
||||||
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
|
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
|
||||||
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
|
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
|
||||||
@@ -34,6 +35,7 @@ public class compress {
|
|||||||
if (!Files.isDirectory(destPath)) {
|
if (!Files.isDirectory(destPath)) {
|
||||||
throw new IllegalArgumentException("目标路径必须是有效的目录");
|
throw new IllegalArgumentException("目标路径必须是有效的目录");
|
||||||
}
|
}
|
||||||
|
Path destRoot = destPath.toAbsolutePath().normalize();
|
||||||
|
|
||||||
try (InputStream fileIn = Files.newInputStream(Paths.get(sourceFilePanth));
|
try (InputStream fileIn = Files.newInputStream(Paths.get(sourceFilePanth));
|
||||||
BufferedInputStream bufferedIn = new BufferedInputStream(fileIn);
|
BufferedInputStream bufferedIn = new BufferedInputStream(fileIn);
|
||||||
@@ -41,8 +43,12 @@ public class compress {
|
|||||||
TarArchiveInputStream tarIn = new TarArchiveInputStream(gzipIn)) {
|
TarArchiveInputStream tarIn = new TarArchiveInputStream(gzipIn)) {
|
||||||
|
|
||||||
TarArchiveEntry entry;
|
TarArchiveEntry entry;
|
||||||
while ((entry = tarIn.getNextTarEntry()) != null) {
|
while ((entry = tarIn.getNextEntry()) != null) {
|
||||||
Path targetPath = destPath.resolve(entry.getName());
|
// 拒绝符号链接/硬链接条目,防止解压逃逸
|
||||||
|
if (entry.isSymbolicLink() || entry.getLinkFlag() == TarConstants.LF_LINK) {
|
||||||
|
throw new IllegalArgumentException("压缩包包含不支持的链接条目: " + entry.getName());
|
||||||
|
}
|
||||||
|
Path targetPath = resolveInside(destRoot, entry.getName());
|
||||||
if (entry.isDirectory()) {
|
if (entry.isDirectory()) {
|
||||||
Files.createDirectories(targetPath);
|
Files.createDirectories(targetPath);
|
||||||
} else {
|
} 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压缩包
|
* 压缩Tar.Gz压缩包
|
||||||
*
|
*
|
||||||
@@ -78,7 +96,7 @@ public class compress {
|
|||||||
@Override
|
@Override
|
||||||
public java.nio.file.FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws java.io.IOException {
|
public java.nio.file.FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws java.io.IOException {
|
||||||
String entryName = sourcePath.relativize(file).toString().replace('\\', '/');
|
String entryName = sourcePath.relativize(file).toString().replace('\\', '/');
|
||||||
TarArchiveEntry entry = new TarArchiveEntry(file.toFile(), entryName);
|
TarArchiveEntry entry = new TarArchiveEntry(file, entryName);
|
||||||
tarOut.putArchiveEntry(entry);
|
tarOut.putArchiveEntry(entry);
|
||||||
Files.copy(file, tarOut);
|
Files.copy(file, tarOut);
|
||||||
tarOut.closeArchiveEntry();
|
tarOut.closeArchiveEntry();
|
||||||
@@ -97,7 +115,7 @@ public class compress {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
TarArchiveEntry entry = new TarArchiveEntry(sourcePath.toFile(), sourcePath.getFileName().toString());
|
TarArchiveEntry entry = new TarArchiveEntry(sourcePath, sourcePath.getFileName().toString());
|
||||||
tarOut.putArchiveEntry(entry);
|
tarOut.putArchiveEntry(entry);
|
||||||
Files.copy(sourcePath, tarOut);
|
Files.copy(sourcePath, tarOut);
|
||||||
tarOut.closeArchiveEntry();
|
tarOut.closeArchiveEntry();
|
||||||
@@ -166,14 +184,19 @@ public class compress {
|
|||||||
if (!Files.isDirectory(destPath)) {
|
if (!Files.isDirectory(destPath)) {
|
||||||
throw new IllegalArgumentException("目标路径必须是有效的目录");
|
throw new IllegalArgumentException("目标路径必须是有效的目录");
|
||||||
}
|
}
|
||||||
|
Path destRoot = destPath.toAbsolutePath().normalize();
|
||||||
|
|
||||||
try (InputStream fileIn = Files.newInputStream(Paths.get(sourceFilePanth));
|
try (InputStream fileIn = Files.newInputStream(Paths.get(sourceFilePanth));
|
||||||
BufferedInputStream bufferedIn = new BufferedInputStream(fileIn);
|
BufferedInputStream bufferedIn = new BufferedInputStream(fileIn);
|
||||||
ZipArchiveInputStream zipIn = new ZipArchiveInputStream(bufferedIn)) {
|
ZipArchiveInputStream zipIn = new ZipArchiveInputStream(bufferedIn)) {
|
||||||
|
|
||||||
ZipArchiveEntry entry;
|
ZipArchiveEntry entry;
|
||||||
while ((entry = zipIn.getNextEntry()) != null) {
|
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()) {
|
if (entry.isDirectory()) {
|
||||||
Files.createDirectories(targetPath);
|
Files.createDirectories(targetPath);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -5,15 +5,17 @@ import java.io.IOException;
|
|||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
import java.nio.file.StandardOpenOption;
|
import java.nio.file.StandardOpenOption;
|
||||||
import java.util.concurrent.CompletableFuture;
|
|
||||||
|
|
||||||
// 创建文件
|
// 创建文件
|
||||||
public class CreateFile {
|
public class CreateFile {
|
||||||
/***
|
/**
|
||||||
* 创建文件,创建空白文件
|
* 创建文件,创建空白文件。
|
||||||
* isBin为True时创建一个空白的二进制文件,否则创建一个空白的普通文件
|
* isBin为True时创建一个空白的二进制文件,否则创建一个空白的普通文件。
|
||||||
* 异步IO操作
|
*
|
||||||
|
* 注意:文件已存在时不覆盖、不写入文件头,直接返回该文件;
|
||||||
|
* 创建失败(如路径无权限)返回 null。
|
||||||
*
|
*
|
||||||
* @param FileName 文件名
|
* @param FileName 文件名
|
||||||
* @param isBin 是否二进制文件
|
* @param isBin 是否二进制文件
|
||||||
@@ -23,62 +25,50 @@ public class CreateFile {
|
|||||||
* @return 文件
|
* @return 文件
|
||||||
*/
|
*/
|
||||||
public static File NewFile(String FileName, boolean isBin, BinFileType binFileType) {
|
public static File NewFile(String FileName, boolean isBin, BinFileType binFileType) {
|
||||||
File TargetFile = null;
|
if (FileName == null) {
|
||||||
byte[] BufferByte = new byte[1024];
|
return null;
|
||||||
|
}
|
||||||
|
File targetFile;
|
||||||
try {
|
try {
|
||||||
TargetFile = new File(FileName);
|
targetFile = new File(FileName);
|
||||||
if (TargetFile.createNewFile()) {
|
if (targetFile.createNewFile()) {
|
||||||
if (isBin && binFileType != null) {
|
if (isBin && binFileType != null) {
|
||||||
// 根据二进制文件类型写入文件头标识
|
// 根据二进制文件类型写入文件头标识
|
||||||
byte[] header = switch (binFileType) {
|
byte[] header = switch (binFileType) {
|
||||||
case ELF -> new byte[]{0x7F, 'E', 'L', 'F'};
|
case ELF -> new byte[]{0x7F, 'E', 'L', 'F'};
|
||||||
case PE -> new byte[]{'M', 'Z'};
|
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)) {
|
StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)) {
|
||||||
os.write(header);
|
os.write(header);
|
||||||
os.flush();
|
os.flush();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return targetFile;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
TargetFile = null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return TargetFile;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/***
|
/**
|
||||||
* 以字节流方式保存文件
|
* 以字节流方式保存文件到目标路径(覆盖同名文件)。
|
||||||
* 异步IO操作
|
|
||||||
*
|
*
|
||||||
* @param file 文件
|
* @param file 源文件
|
||||||
* @param TargePath 目标路径
|
* @param TargePath 目标路径
|
||||||
* @param FileName 文件名
|
* @param FileName 文件名
|
||||||
*
|
*
|
||||||
* @return 是否保存成功,True为保存成功,False为保存失败
|
* @return 是否保存成功,True为保存成功,False为保存失败
|
||||||
*/
|
*/
|
||||||
public static boolean StreamSaveFile(File file, Path TargePath, String FileName) {
|
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 {
|
try {
|
||||||
Path targetFile = TargePath.resolve(FileName);
|
Files.copy(file.toPath(), targetFile, StandardCopyOption.REPLACE_EXISTING);
|
||||||
return CompletableFuture.supplyAsync(() -> {
|
return true;
|
||||||
try (OutputStream os = Files.newOutputStream(targetFile,
|
} catch (IOException e) {
|
||||||
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) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+287
-159
@@ -1,202 +1,330 @@
|
|||||||
package top.gtb520.java.gnu.java.utils.library.utils.file;
|
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.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.file.AtomicMoveNotSupportedException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
import java.nio.file.StandardOpenOption;
|
import java.nio.file.StandardOpenOption;
|
||||||
import java.security.InvalidKeyException;
|
import java.nio.file.attribute.PosixFilePermissions;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.KeyFactory;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.security.KeyPair;
|
||||||
|
import java.security.KeyPairGenerator;
|
||||||
import javax.crypto.BadPaddingException;
|
import java.security.PrivateKey;
|
||||||
import javax.crypto.Cipher;
|
import java.security.PublicKey;
|
||||||
import javax.crypto.IllegalBlockSizeException;
|
import java.security.SecureRandom;
|
||||||
import javax.crypto.KeyGenerator;
|
import java.security.spec.PKCS8EncodedKeySpec;
|
||||||
import javax.crypto.NoSuchPaddingException;
|
|
||||||
import javax.crypto.SecretKey;
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件加密/解密工具类。
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link EncryptionType#AES}:AES-256/GCM,随机 12 字节 IV + 128 位认证标签,密文 = IV + 密文;</li>
|
||||||
|
* <li>{@link EncryptionType#RSA}:RSA-2048 混合加密(RSA-OAEP 包裹随机 AES-256 密钥,数据用 AES-GCM 加密),
|
||||||
|
* 密钥文件保存 PKCS#8 私钥;</li>
|
||||||
|
* <li>{@link EncryptionType#DES}:仅兼容旧版本,密钥太短,已废弃,新代码请使用 AES。</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* 加密/解密均为流式处理(支持大文件),先写临时文件再原子替换原文件,避免中途失败损坏数据。
|
||||||
|
* 非法入参(文件/密钥路径不存在、为 null)返回 {@code null},实际加解密失败抛出 {@link RuntimeException}。
|
||||||
|
*/
|
||||||
public class EncryptionAndDecryption {
|
public class EncryptionAndDecryption {
|
||||||
/***
|
|
||||||
* 文件加密
|
private static final int AES_KEY_BITS = 256;
|
||||||
* 异步执行加密操作
|
private static final int DES_KEY_BITS = 56;
|
||||||
* 加密后覆盖原文件,不添加额外后缀
|
private static final int RSA_KEY_BITS = 2048;
|
||||||
* 密钥保存到 keyPath 目录下,密钥名为 原文件名.key
|
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 file 要加密的文件
|
||||||
* @param type 加密类型
|
* @param type 加密类型
|
||||||
* @param keyPath 密钥文件目录路径,密钥名为 文件名.key
|
* @param keyPath 密钥文件目录路径
|
||||||
* @return 加密后的文件(原文件),失败时返回 null
|
* @return 加密后的文件(原文件路径);入参非法时返回 null
|
||||||
*/
|
*/
|
||||||
public static File EncryptionFile(File file, EncryptionType type, Path keyPath) {
|
public static File EncryptionFile(File file, EncryptionType type, Path keyPath) {
|
||||||
File TargetFile = null;
|
|
||||||
|
|
||||||
if (file == null || !file.exists() || !file.isFile()) {
|
if (file == null || !file.exists() || !file.isFile()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (keyPath == null) {
|
if (type == null || keyPath == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
TargetFile = CompletableFuture.supplyAsync(() -> {
|
Files.createDirectories(keyPath);
|
||||||
try {
|
Path keyFilePath = keyPath.resolve(file.getName() + ".key");
|
||||||
// 读取原文件内容
|
|
||||||
byte[] plainBytes = Files.readAllBytes(file.toPath());
|
|
||||||
|
|
||||||
// 根据加密类型生成对称密钥
|
switch (type) {
|
||||||
SecretKey secretKey = generateSecretKey(type);
|
case AES -> {
|
||||||
byte[] encryptedBytes = encryptBytes(plainBytes, secretKey, type);
|
SecretKey key = generateKey("AES", AES_KEY_BITS);
|
||||||
|
writeKeyFile(keyFilePath, key.getEncoded());
|
||||||
// 将密钥写入 keyPath 目录,密钥名为 文件名.key
|
encryptAesGcm(file, 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;
|
|
||||||
}
|
}
|
||||||
}).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) {
|
} catch (Exception e) {
|
||||||
TargetFile = null;
|
throw new RuntimeException("加密失败: " + file, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
return TargetFile;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/***
|
/**
|
||||||
* 生成对称密钥
|
* 文件解密。解密后覆盖写回原文件(原子替换)。
|
||||||
*
|
*
|
||||||
* @param type 加密类型
|
* @param file 加密文件
|
||||||
* @return 对称密钥
|
* @param type 加密类型,须与加密时一致
|
||||||
|
* @param keyfile 密钥文件,从中读取解密密钥
|
||||||
|
* @return 解密后的文件(原文件路径);入参非法时返回 null
|
||||||
*/
|
*/
|
||||||
private static SecretKey generateSecretKey(EncryptionType type) throws NoSuchAlgorithmException {
|
public static File DecryptionFile(File file, EncryptionType type, File keyfile) {
|
||||||
String algorithm = switch (type) {
|
if (file == null || !file.exists() || !file.isFile()) {
|
||||||
case AES -> "AES";
|
return null;
|
||||||
case DES -> "DES";
|
}
|
||||||
case RSA -> "AES"; // RSA 为非对称加密,此处用 AES 做对称加密替代
|
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);
|
KeyGenerator keyGen = KeyGenerator.getInstance(algorithm);
|
||||||
int keySize = switch (type) {
|
keyGen.init(keyBits);
|
||||||
case AES -> 128;
|
|
||||||
case DES -> 56;
|
|
||||||
case RSA -> 128;
|
|
||||||
};
|
|
||||||
keyGen.init(keySize);
|
|
||||||
return keyGen.generateKey();
|
return keyGen.generateKey();
|
||||||
}
|
}
|
||||||
|
|
||||||
/***
|
private static KeyPair generateRsaKeyPair() throws Exception {
|
||||||
* 对字节数据进行加密
|
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
|
||||||
*
|
keyGen.initialize(RSA_KEY_BITS);
|
||||||
* @param data 原始数据
|
return keyGen.generateKeyPair();
|
||||||
* @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 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);
|
||||||
* @param file 加密文件
|
} else {
|
||||||
* @param type 加密类型,须与加密时一致
|
cipher.init(mode, key, params);
|
||||||
* @param keyfile 密钥文件,从中读取解密密钥
|
}
|
||||||
* @return 解密后的文件(原文件),失败时返回 null
|
return cipher;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流式加/解密:读原文件 -> Cipher 流 -> 临时文件 -> 原子替换原文件。
|
||||||
*/
|
*/
|
||||||
public static File DecryptionFile(File file, EncryptionType type, File keyfile) {
|
private static void transform(File file, Cipher cipher, boolean encrypt) throws Exception {
|
||||||
File TargetFile = null;
|
Path tmp = tempSibling(file);
|
||||||
|
if (encrypt) {
|
||||||
if (file == null || !file.exists() || !file.isFile()) {
|
try (OutputStream out = new BufferedOutputStream(Files.newOutputStream(tmp))) {
|
||||||
return null;
|
try (CipherOutputStream cos = new CipherOutputStream(out, cipher);
|
||||||
}
|
InputStream in = Files.newInputStream(file.toPath())) {
|
||||||
if (keyfile == null || !keyfile.exists() || !keyfile.isFile()) {
|
in.transferTo(cos);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}).get();
|
}
|
||||||
} catch (Exception e) {
|
} else {
|
||||||
TargetFile = null;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
moveAtomically(tmp, file);
|
||||||
return TargetFile;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/***
|
/**
|
||||||
* 对字节数据进行解密
|
* 在原文件同目录创建临时文件,保证原子移动(rename)时在同一文件系统内。
|
||||||
*
|
|
||||||
* @param data 加密数据
|
|
||||||
* @param key 密钥
|
|
||||||
* @param type 加密类型
|
|
||||||
* @return 解密后的数据
|
|
||||||
*/
|
*/
|
||||||
private static byte[] decryptBytes(byte[] data, SecretKey key, EncryptionType type)
|
private static Path tempSibling(File file) throws IOException {
|
||||||
throws NoSuchAlgorithmException, NoSuchPaddingException,
|
Path dir = file.toPath().toAbsolutePath().getParent();
|
||||||
InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
|
return Files.createTempFile(dir, file.getName() + ".", ".tmp");
|
||||||
String algorithm = switch (type) {
|
}
|
||||||
case AES -> "AES/ECB/PKCS5Padding";
|
|
||||||
case DES -> "DES/ECB/PKCS5Padding";
|
/**
|
||||||
case RSA -> "AES/ECB/PKCS5Padding";
|
* 原子替换原文件;文件系统不支持原子移动时回退为普通覆盖移动。
|
||||||
};
|
*/
|
||||||
Cipher cipher = Cipher.getInstance(algorithm);
|
private static void moveAtomically(Path tmp, File target) throws IOException {
|
||||||
cipher.init(Cipher.DECRYPT_MODE, key);
|
try {
|
||||||
return cipher.doFinal(data);
|
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 {
|
public enum EncryptionType {
|
||||||
AES, DES, RSA;
|
AES,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 已废弃:DES 密钥仅 56 位,已被暴力破解,仅用于兼容旧版本,新代码请使用 {@link #AES}。
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
|
DES,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RSA 混合加密:RSA-2048/OAEP 包裹随机 AES-256 会话密钥,数据使用 AES-GCM 加密。
|
||||||
|
*/
|
||||||
|
RSA;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -12,17 +12,37 @@ public class echo {
|
|||||||
/**
|
/**
|
||||||
* 使用自定义颜色码在终端打印彩色文本
|
* 使用自定义颜色码在终端打印彩色文本
|
||||||
* 支持三种格式:
|
* 支持三种格式:
|
||||||
* - "#RRGGBB" 十六进制颜色(如 "#FFC0CB")
|
* - "#RRGGBB" / "#RGB" 十六进制颜色(如 "#FFC0CB")
|
||||||
* - ANSI 颜色代码(如 "31" 表示红色)
|
* - ANSI 颜色代码(如 "31" 表示红色)
|
||||||
* - ANSI 扩展色(如 "38;5;208" 表示扩展色)
|
* - ANSI 扩展色(如 "38;5;208" 表示扩展色)
|
||||||
*
|
*
|
||||||
* @param Message 要打印的文本内容
|
* @param Message 要打印的文本内容
|
||||||
* @param HexColor 颜色代码,支持 "#RRGGBB"、ANSI 颜色代码或扩展色
|
* @param HexColor 颜色代码,支持 "#RRGGBB"、ANSI 颜色代码或扩展色
|
||||||
|
* @throws IllegalArgumentException 当颜色代码格式非法或为 null 时
|
||||||
*/
|
*/
|
||||||
public static void ColorHexPrintln(String Message, String HexColor) {
|
public static void ColorHexPrintln(String Message, String HexColor) {
|
||||||
if (HexColor.startsWith("#") && (HexColor.length() == 7 || HexColor.length() == 4)) {
|
if (Message == null) {
|
||||||
// #RRGGBB 或 #RGB 格式,转换为 ANSI 真彩色序列
|
Message = "";
|
||||||
String hex = HexColor.substring(1);
|
}
|
||||||
|
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;
|
int r, g, b;
|
||||||
if (hex.length() == 3) {
|
if (hex.length() == 3) {
|
||||||
// #RGB 简写格式,将每个字符扩展为两位(如 F -> FF)
|
// #RGB 简写格式,将每个字符扩展为两位(如 F -> FF)
|
||||||
@@ -34,20 +54,35 @@ public class echo {
|
|||||||
g = Integer.parseInt(hex.substring(2, 4), 16);
|
g = Integer.parseInt(hex.substring(2, 4), 16);
|
||||||
b = Integer.parseInt(hex.substring(4, 6), 16);
|
b = Integer.parseInt(hex.substring(4, 6), 16);
|
||||||
}
|
}
|
||||||
System.out.println("\033[38;2;" + r + ";" + g + ";" + b + "m" + Message + "\033[0m");
|
return "38;2;" + r + ";" + g + ";" + b;
|
||||||
} else {
|
|
||||||
// 原有 ANSI 颜色代码直接拼接
|
|
||||||
System.out.println("\033[" + HexColor + "m" + Message + "\033[0m");
|
|
||||||
}
|
}
|
||||||
|
// 仅允许数字与分号,防止向终端注入任意转义序列
|
||||||
|
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 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) {
|
public static void ColorPrintln(String Message, ColorNameEnum ColorName) {
|
||||||
|
if (ColorName == null) {
|
||||||
|
System.out.println(Message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
// 根据颜色名称映射对应的 ANSI 颜色转义序列
|
// 根据颜色名称映射对应的 ANSI 颜色转义序列
|
||||||
switch (ColorName) {
|
switch (ColorName) {
|
||||||
case RED:
|
case RED:
|
||||||
@@ -76,7 +111,7 @@ public class echo {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 在单行内打印多种颜色的文字
|
* 在单行内打印多种颜色的文字
|
||||||
*
|
*
|
||||||
* 颜色代码仿照Minecraft格式,如:
|
* 颜色代码仿照Minecraft格式,如:
|
||||||
* &4(红色)、&c(浅红色)、
|
* &4(红色)、&c(浅红色)、
|
||||||
* &6(橙色)、&e(黄色)、
|
* &6(橙色)、&e(黄色)、
|
||||||
@@ -89,9 +124,12 @@ public class echo {
|
|||||||
* &l(加粗)、&n(下划线)、
|
* &l(加粗)、&n(下划线)、
|
||||||
* &o(斜体)、&k(乱码)、
|
* &o(斜体)、&k(乱码)、
|
||||||
* &m(删除线)、&r(重置)。
|
* &m(删除线)、&r(重置)。
|
||||||
*
|
*
|
||||||
|
* 颜色码会重置之前的格式;加粗/下划线/斜体/删除线等格式码互相叠加;
|
||||||
|
* 例如 &4&l 得到"粗体红色",&n&9 得到"下划线蓝色"。
|
||||||
|
*
|
||||||
* 示例:&6Welcome &2to &4Project
|
* 示例:&6Welcome &2to &4Project
|
||||||
*
|
*
|
||||||
* @param Message 包含Minecraft格式代码的文本
|
* @param Message 包含Minecraft格式代码的文本
|
||||||
*/
|
*/
|
||||||
public static void ColorPrintlnPlus(String Message) {
|
public static void ColorPrintlnPlus(String Message) {
|
||||||
@@ -100,7 +138,9 @@ public class echo {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
StringBuilder result = new StringBuilder();
|
StringBuilder result = new StringBuilder(Message.length() + 32);
|
||||||
|
// 当前叠加的格式序列(加粗/下划线等),颜色码切换后需要重新应用
|
||||||
|
StringBuilder activeFormat = new StringBuilder();
|
||||||
boolean obfuscated = false;
|
boolean obfuscated = false;
|
||||||
int i = 0;
|
int i = 0;
|
||||||
|
|
||||||
@@ -110,11 +150,26 @@ public class echo {
|
|||||||
char code = Character.toLowerCase(Message.charAt(i + 1));
|
char code = Character.toLowerCase(Message.charAt(i + 1));
|
||||||
String ansi = getAnsiCode(code);
|
String ansi = getAnsiCode(code);
|
||||||
if (ansi != null) {
|
if (ansi != null) {
|
||||||
// 先重置,再应用新格式(避免格式叠加冲突)
|
if (isColorCode(code)) {
|
||||||
result.append("\033[0m");
|
// 颜色码:重置后应用新颜色,再恢复叠加的格式
|
||||||
result.append(ansi);
|
result.append("\033[0m").append(ansi).append(activeFormat);
|
||||||
// &k 开启乱码模式,其他颜色/格式码关闭乱码模式
|
obfuscated = false;
|
||||||
obfuscated = (code == 'k');
|
} 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;
|
i += 2;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -136,6 +191,13 @@ public class echo {
|
|||||||
System.out.println(result.toString());
|
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 转义序列
|
* 根据 Minecraft 格式代码字符返回对应的 ANSI 转义序列
|
||||||
*
|
*
|
||||||
|
|||||||
+250
-250
@@ -1,16 +1,30 @@
|
|||||||
package top.gtb520.java.gnu.java.utils.library.utils.processing.hwid;
|
package top.gtb520.java.gnu.java.utils.library.utils.processing.hwid;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.FileReader;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.net.NetworkInterface;
|
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.security.MessageDigest;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Enumeration;
|
import java.util.Enumeration;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 硬件标识符(HWID)生成器。
|
||||||
|
*
|
||||||
|
* <p>组合 CPU、主板、MAC 地址、硬盘序列号与操作系统信息,通过 SHA-256 生成 64 位十六进制标识。
|
||||||
|
* 所有外部命令通过 {@link ProcessBuilder} 执行,带超时并并发排空输出流,避免挂起。
|
||||||
|
* 部分硬件信息获取失败时自动降级(跳过该组件),全部失败时返回 {@code "UNKNOWN_HWID"}。</p>
|
||||||
|
*/
|
||||||
public class getHWID {
|
public class getHWID {
|
||||||
private final static boolean Debug = false;
|
private final static boolean Debug = false;
|
||||||
|
private static final long COMMAND_TIMEOUT_SECONDS = 10;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成硬件标识符(HWID)
|
* 生成硬件标识符(HWID)
|
||||||
@@ -20,48 +34,23 @@ public class getHWID {
|
|||||||
try {
|
try {
|
||||||
List<String> hardwareInfo = new ArrayList<>();
|
List<String> hardwareInfo = new ArrayList<>();
|
||||||
|
|
||||||
// 1. 获取CPU信息
|
addIfPresent(hardwareInfo, "CPU", getCPUInfo());
|
||||||
String cpuInfo = getCPUInfo();
|
addIfPresent(hardwareInfo, "MB", getMotherboardInfo());
|
||||||
if (cpuInfo != null && !cpuInfo.isEmpty()) {
|
addIfPresent(hardwareInfo, "MAC", getMacAddress());
|
||||||
hardwareInfo.add("CPU:" + cpuInfo);
|
addIfPresent(hardwareInfo, "DISK", getDiskSerialNumber());
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 获取主板序列号
|
String osInfo = System.getProperty("os.name") + "_" + System.getProperty("os.version");
|
||||||
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");
|
|
||||||
hardwareInfo.add("OS:" + osInfo);
|
hardwareInfo.add("OS:" + osInfo);
|
||||||
|
|
||||||
// 6. 组合所有硬件信息并生成哈希
|
|
||||||
StringBuilder combined = new StringBuilder();
|
StringBuilder combined = new StringBuilder();
|
||||||
for (String info : hardwareInfo) {
|
for (String info : hardwareInfo) {
|
||||||
if (Debug) {
|
if (Debug) {
|
||||||
System.out.println("info: " + info);
|
System.out.println("info: " + info);
|
||||||
System.out.println("hardwareInfo: " + hardwareInfo);
|
|
||||||
System.out.println("combined: " + combined);
|
|
||||||
}
|
}
|
||||||
combined.append(info).append("|");
|
combined.append(info).append("|");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Debug) {
|
if (Debug) {
|
||||||
System.out.println("HWID combined: " + combined.toString());
|
System.out.println("HWID combined: " + combined);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用SHA-256生成固定长度的哈希值
|
// 使用SHA-256生成固定长度的哈希值
|
||||||
@@ -73,12 +62,18 @@ public class getHWID {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void addIfPresent(List<String> list, String label, String value) {
|
||||||
|
if (value != null && !value.isEmpty()) {
|
||||||
|
list.add(label + ":" + value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检测是否为Android系统
|
* 检测是否为Android系统
|
||||||
*/
|
*/
|
||||||
private static boolean isAndroid() {
|
private static boolean isAndroid() {
|
||||||
try {
|
try {
|
||||||
String osName = System.getProperty("os.name").toLowerCase();
|
String osName = System.getProperty("os.name", "").toLowerCase();
|
||||||
String javaVmName = System.getProperty("java.vm.name", "").toLowerCase();
|
String javaVmName = System.getProperty("java.vm.name", "").toLowerCase();
|
||||||
String javaSpecVendor = System.getProperty("java.specification.vendor", "").toLowerCase();
|
String javaSpecVendor = System.getProperty("java.specification.vendor", "").toLowerCase();
|
||||||
|
|
||||||
@@ -86,7 +81,7 @@ public class getHWID {
|
|||||||
return osName.contains("android") ||
|
return osName.contains("android") ||
|
||||||
javaVmName.contains("android") ||
|
javaVmName.contains("android") ||
|
||||||
javaSpecVendor.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) {
|
} catch (Exception e) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -96,55 +91,43 @@ public class getHWID {
|
|||||||
* 获取CPU信息
|
* 获取CPU信息
|
||||||
*/
|
*/
|
||||||
private static String getCPUInfo() {
|
private static String getCPUInfo() {
|
||||||
try {
|
// Android系统特殊处理
|
||||||
// Android系统特殊处理
|
if (isAndroid()) {
|
||||||
if (isAndroid()) {
|
return getAndroidCPUInfo();
|
||||||
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();
|
private static String readLinuxCpuInfo() {
|
||||||
Process process;
|
try (BufferedReader reader = Files.newBufferedReader(Path.of("/proc/cpuinfo"), StandardCharsets.UTF_8)) {
|
||||||
|
|
||||||
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())
|
|
||||||
);
|
|
||||||
|
|
||||||
String line;
|
String line;
|
||||||
StringBuilder result = new StringBuilder();
|
|
||||||
|
|
||||||
while ((line = reader.readLine()) != null) {
|
while ((line = reader.readLine()) != null) {
|
||||||
line = line.trim();
|
line = line.trim();
|
||||||
if (!line.isEmpty()) {
|
if (line.startsWith("model name")) {
|
||||||
if (os.contains("win") && !line.equals("ProcessorId")) {
|
String[] parts = line.split(":", 2);
|
||||||
return line;
|
if (parts.length > 1) {
|
||||||
} else if (os.contains("mac")) {
|
return parts[1].trim();
|
||||||
return line;
|
|
||||||
} else if (line.startsWith("model name")) {
|
|
||||||
return line.split(":", 2)[1].trim();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (IOException ignored) {
|
||||||
} catch (Exception e) {
|
// 读取失败,返回 null
|
||||||
// 静默失败,返回null
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -153,210 +136,156 @@ public class getHWID {
|
|||||||
* 获取Android CPU信息
|
* 获取Android CPU信息
|
||||||
*/
|
*/
|
||||||
private static String getAndroidCPUInfo() {
|
private static String getAndroidCPUInfo() {
|
||||||
StringBuilder cpuInfo = new StringBuilder();
|
List<String> infoList = new ArrayList<>();
|
||||||
|
try (BufferedReader reader = Files.newBufferedReader(Path.of("/proc/cpuinfo"), StandardCharsets.UTF_8)) {
|
||||||
try {
|
|
||||||
// 方法1: 读取 /proc/cpuinfo
|
|
||||||
BufferedReader reader = new BufferedReader(new FileReader("/proc/cpuinfo"));
|
|
||||||
String line;
|
String line;
|
||||||
List<String> infoList = new ArrayList<>();
|
|
||||||
|
|
||||||
while ((line = reader.readLine()) != null) {
|
while ((line = reader.readLine()) != null) {
|
||||||
line = line.trim();
|
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);
|
String[] parts = line.split(":", 2);
|
||||||
if (parts.length > 1) {
|
if (parts.length > 1) {
|
||||||
infoList.add("Processor:" + parts[1].trim());
|
infoList.add(parts[0].trim() + ":" + 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());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
reader.close();
|
} catch (IOException ignored) {
|
||||||
|
|
||||||
if (!infoList.isEmpty()) {
|
|
||||||
return String.join("|", infoList);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
// 静默失败
|
// 静默失败
|
||||||
}
|
}
|
||||||
|
|
||||||
// 方法2: 如果读取文件失败,使用System.getProperty
|
if (!infoList.isEmpty()) {
|
||||||
try {
|
return String.join("|", infoList);
|
||||||
String abi = System.getProperty("os.arch", "unknown");
|
|
||||||
String cores = Runtime.getRuntime().availableProcessors() + "";
|
|
||||||
return "ABI:" + abi + "|Cores:" + cores;
|
|
||||||
} catch (Exception e) {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 方法2: 如果读取文件失败,使用系统属性
|
||||||
|
String abi = System.getProperty("os.arch", "unknown");
|
||||||
|
String cores = Runtime.getRuntime().availableProcessors() + "";
|
||||||
|
return "ABI:" + abi + "|Cores:" + cores;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取主板信息
|
* 获取主板信息
|
||||||
*/
|
*/
|
||||||
private static String getMotherboardInfo() {
|
private static String getMotherboardInfo() {
|
||||||
try {
|
// Android系统特殊处理
|
||||||
// Android系统特殊处理
|
if (isAndroid()) {
|
||||||
if (isAndroid()) {
|
return getAndroidMotherboardInfo();
|
||||||
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");
|
||||||
}
|
}
|
||||||
|
return sn;
|
||||||
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 null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取Android主板信息
|
* 获取Android主板信息
|
||||||
|
*
|
||||||
|
* <p>注意:Android 系统属性 ro.product.* 无法通过 System.getProperty 获取,
|
||||||
|
* 需要 android.os.Build;此处仅作为尽力而为的尝试。</p>
|
||||||
*/
|
*/
|
||||||
private static String getAndroidMotherboardInfo() {
|
private static String getAndroidMotherboardInfo() {
|
||||||
try {
|
List<String> infoList = new ArrayList<>();
|
||||||
List<String> infoList = new ArrayList<>();
|
|
||||||
|
|
||||||
// 读取Android设备特性
|
String brand = System.getProperty("ro.product.brand", "");
|
||||||
String brand = System.getProperty("ro.product.brand", "");
|
String manufacturer = System.getProperty("ro.product.manufacturer", "");
|
||||||
String manufacturer = System.getProperty("ro.product.manufacturer", "");
|
String model = System.getProperty("ro.product.model", "");
|
||||||
String model = System.getProperty("ro.product.model", "");
|
String device = System.getProperty("ro.product.device", "");
|
||||||
String device = System.getProperty("ro.product.device", "");
|
String board = System.getProperty("ro.product.board", "");
|
||||||
String board = System.getProperty("ro.product.board", "");
|
|
||||||
|
|
||||||
if (!brand.isEmpty()) infoList.add("Brand:" + brand);
|
if (!brand.isEmpty()) infoList.add("Brand:" + brand);
|
||||||
if (!manufacturer.isEmpty()) infoList.add("Manufacturer:" + manufacturer);
|
if (!manufacturer.isEmpty()) infoList.add("Manufacturer:" + manufacturer);
|
||||||
if (!model.isEmpty()) infoList.add("Model:" + model);
|
if (!model.isEmpty()) infoList.add("Model:" + model);
|
||||||
if (!device.isEmpty()) infoList.add("Device:" + device);
|
if (!device.isEmpty()) infoList.add("Device:" + device);
|
||||||
if (!board.isEmpty()) infoList.add("Board:" + board);
|
if (!board.isEmpty()) infoList.add("Board:" + board);
|
||||||
|
|
||||||
if (!infoList.isEmpty()) {
|
return infoList.isEmpty() ? null : String.join("|", infoList);
|
||||||
return String.join("|", infoList);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
// 静默失败
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取MAC地址
|
* 获取MAC地址
|
||||||
|
* 优先返回非回环、非虚拟网卡的 MAC,避免虚拟网卡导致 HWID 漂移;全部为虚拟网卡时回退到第一个可用 MAC。
|
||||||
*/
|
*/
|
||||||
private static String getMacAddress() {
|
private static String getMacAddress() {
|
||||||
try {
|
try {
|
||||||
Enumeration<NetworkInterface> networkInterfaces =
|
Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
|
||||||
NetworkInterface.getNetworkInterfaces();
|
if (networkInterfaces == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String fallback = null;
|
||||||
while (networkInterfaces.hasMoreElements()) {
|
while (networkInterfaces.hasMoreElements()) {
|
||||||
NetworkInterface network = networkInterfaces.nextElement();
|
NetworkInterface network = networkInterfaces.nextElement();
|
||||||
byte[] mac = network.getHardwareAddress();
|
try {
|
||||||
|
byte[] mac = network.getHardwareAddress();
|
||||||
if (mac != null && mac.length > 0) {
|
if (mac == null || mac.length == 0) {
|
||||||
StringBuilder sb = new StringBuilder();
|
continue;
|
||||||
for (int i = 0; i < mac.length; i++) {
|
|
||||||
sb.append(String.format("%02X%s", mac[i],
|
|
||||||
(i < mac.length - 1) ? "-" : ""));
|
|
||||||
}
|
}
|
||||||
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) {
|
} 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() {
|
private static String getDiskSerialNumber() {
|
||||||
try {
|
// Android系统特殊处理
|
||||||
// Android系统特殊处理
|
if (isAndroid()) {
|
||||||
if (isAndroid()) {
|
return getAndroidStorageInfo();
|
||||||
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");
|
||||||
}
|
}
|
||||||
|
return sn;
|
||||||
String os = System.getProperty("os.name").toLowerCase();
|
} else if (os.contains("mac")) {
|
||||||
|
String out = runCommand(List.of(
|
||||||
if (os.contains("win")) {
|
"sh", "-c", "system_profiler SPSerialATADataType | grep 'Serial Number'"));
|
||||||
Process process = Runtime.getRuntime().exec(
|
if (out != null) {
|
||||||
new String[]{"wmic", "diskdrive", "get", "SerialNumber"}
|
for (String line : out.split("\\R")) {
|
||||||
);
|
|
||||||
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) {
|
|
||||||
if (line.contains(":")) {
|
if (line.contains(":")) {
|
||||||
return line.split(":", 2)[1].trim();
|
return line.split(":", 2)[1].trim();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (os.contains("linux")) {
|
||||||
} catch (Exception e) {
|
// Linux:lsblk 输出物理磁盘序列号
|
||||||
// 静默失败
|
return firstLine(runCommand(List.of("lsblk", "-dno", "serial")));
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -365,36 +294,110 @@ public class getHWID {
|
|||||||
* 获取Android存储信息
|
* 获取Android存储信息
|
||||||
*/
|
*/
|
||||||
private static String getAndroidStorageInfo() {
|
private static String getAndroidStorageInfo() {
|
||||||
|
List<String> 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<String> command) {
|
||||||
|
ProcessBuilder pb = new ProcessBuilder(command);
|
||||||
|
Process process;
|
||||||
try {
|
try {
|
||||||
// 读取 /proc/mounts 或使用系统属性
|
process = pb.start();
|
||||||
List<String> infoList = new ArrayList<>();
|
} catch (IOException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// 尝试获取存储相关信息
|
// 并发排空 stderr,避免进程因管道写满而阻塞
|
||||||
String internalStorage = System.getProperty("user.dir", "");
|
Thread stderrDrain = new Thread(() -> drain(process.getErrorStream()), "hwid-stderr-drain");
|
||||||
if (!internalStorage.isEmpty()) {
|
stderrDrain.setDaemon(true);
|
||||||
infoList.add("InternalDir:" + internalStorage);
|
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");
|
* 排空输入流(丢弃内容),用于 stderr。
|
||||||
if (externalStorage.exists()) {
|
*/
|
||||||
infoList.add("ExternalStorage:available");
|
private static void drain(InputStream in) {
|
||||||
} else {
|
try (InputStream ignored = in) {
|
||||||
infoList.add("ExternalStorage:not_available");
|
while (in.read() != -1) {
|
||||||
|
// 丢弃
|
||||||
}
|
}
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
// 忽略
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 获取数据目录
|
/**
|
||||||
java.io.File dataDir = new java.io.File("/data");
|
* 将输入流按行读取到 StringBuilder。
|
||||||
if (dataDir.exists()) {
|
*/
|
||||||
infoList.add("DataDir:exists");
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -405,8 +408,8 @@ public class getHWID {
|
|||||||
private static String generateHash(String input) {
|
private static String generateHash(String input) {
|
||||||
try {
|
try {
|
||||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||||
byte[] hash = digest.digest(input.getBytes("UTF-8"));
|
byte[] hash = digest.digest(input.getBytes(StandardCharsets.UTF_8));
|
||||||
StringBuilder hexString = new StringBuilder();
|
StringBuilder hexString = new StringBuilder(hash.length * 2);
|
||||||
|
|
||||||
for (byte b : hash) {
|
for (byte b : hash) {
|
||||||
String hex = Integer.toHexString(0xff & b);
|
String hex = Integer.toHexString(0xff & b);
|
||||||
@@ -415,12 +418,9 @@ public class getHWID {
|
|||||||
}
|
}
|
||||||
hexString.append(hex);
|
hexString.append(hex);
|
||||||
}
|
}
|
||||||
|
|
||||||
return hexString.toString();
|
return hexString.toString();
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
throw new IllegalStateException("SHA-256 摘要不可用", e);
|
||||||
return input.hashCode() + "";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+22
-26
@@ -3,46 +3,42 @@ package top.gtb520.java.gnu.java.utils.library.utils.processing.random;
|
|||||||
import java.util.concurrent.ThreadLocalRandom;
|
import java.util.concurrent.ThreadLocalRandom;
|
||||||
|
|
||||||
public class defaults {
|
public class defaults {
|
||||||
|
|
||||||
|
private static final String ALL_CHARACTERS =
|
||||||
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据输入的范围生成随机数
|
* 根据输入的范围生成随机数(包含两端)。
|
||||||
|
*
|
||||||
* @param start_scope 起始范围
|
* @param start_scope 起始范围
|
||||||
* @param end_scope 结束范围
|
* @param end_scope 结束范围
|
||||||
* @return 随机数
|
* @return 随机数
|
||||||
|
* @throws IllegalArgumentException 当起始值大于结束值时
|
||||||
*/
|
*/
|
||||||
public static int random(int start_scope, int end_scope) {
|
public static int random(int start_scope, int end_scope) {
|
||||||
int random_double;
|
|
||||||
|
|
||||||
// 处理边界,确保起始范围小于等于结束范围
|
|
||||||
if (start_scope > end_scope) {
|
if (start_scope > end_scope) {
|
||||||
System.out.println("起始范围不能大于结束范围");
|
throw new IllegalArgumentException("起始范围不能大于结束范围: " + start_scope + " > " + end_scope);
|
||||||
throw new IllegalArgumentException("起始范围不能大于结束范围");
|
|
||||||
}
|
}
|
||||||
// 生成随机数
|
// 使用 long 上界,避免 end_scope == Integer.MAX_VALUE 时 +1 溢出为负数
|
||||||
random_double = ThreadLocalRandom.current().nextInt(start_scope, end_scope + 1);
|
return (int) ThreadLocalRandom.current().nextLong(start_scope, (long) end_scope + 1);
|
||||||
|
|
||||||
return random_double;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成指定长度的随机字符串
|
* 生成指定长度的随机字符串(大小写字母 + 数字)。
|
||||||
|
*
|
||||||
* @param length 字符串长度
|
* @param length 字符串长度
|
||||||
* @return 随机字符串
|
* @return 随机字符串
|
||||||
|
* @throws IllegalArgumentException 当长度为负数时
|
||||||
*/
|
*/
|
||||||
public static String randomString(int length) {
|
public static String randomString(int length) {
|
||||||
String alphabetsInUpperCase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
if (length < 0) {
|
||||||
String alphabetsInLowerCase = "abcdefghijklmnopqrstuvwxyz";
|
throw new IllegalArgumentException("字符串长度不能为负数: " + length);
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,19 +2,40 @@ package top.gtb520.java.gnu.java.utils.library.utils.runtime;
|
|||||||
|
|
||||||
import top.gtb520.java.gnu.java.utils.library.main;
|
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.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
|
||||||
// 获取运行时的信息
|
// 获取运行时的信息
|
||||||
public class RunEnv {
|
public class RunEnv {
|
||||||
// 获取当前运行的JAR文件路径
|
|
||||||
|
private static final String TEMP_DIR_PREFIX = "gjul-";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前运行的 JAR 文件路径。
|
||||||
|
* 非 JAR 运行(IDE/类路径)时返回类路径目录;解析失败时回退到当前工作目录。
|
||||||
|
*/
|
||||||
public static String getJarPath() {
|
public static String getJarPath() {
|
||||||
String path = main.class.getProtectionDomain().getCodeSource().getLocation().getPath();
|
try {
|
||||||
if (System.getProperty("os.name").startsWith("Windows")) {
|
java.net.URL location = main.class.getProtectionDomain().getCodeSource().getLocation();
|
||||||
path = path.replace('/', '\\');
|
if (location == null) {
|
||||||
//去除路径最前面的 "\\"
|
return Paths.get("").toAbsolutePath().toString();
|
||||||
path = path.substring(1);
|
}
|
||||||
|
// 通过 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");
|
return System.getProperty("java.io.tmpdir");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 新建临时目录,失败时返回 null
|
/**
|
||||||
|
* 在系统临时目录下新建唯一临时子目录,返回其路径。
|
||||||
|
*/
|
||||||
public static String newTempDir() {
|
public static String newTempDir() {
|
||||||
return createTempDir(Path.of(getTempDir()));
|
return createTempDir(Path.of(getTempDir()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 在 当前运行的JAR文件路径 下 新建临时目录,失败时返回 null
|
/**
|
||||||
|
* 在当前运行的 JAR 文件所在目录下新建唯一临时子目录,返回其路径;
|
||||||
|
* JAR 路径无父目录时返回 null。
|
||||||
|
*/
|
||||||
public static String newJarThisTempDir() {
|
public static String newJarThisTempDir() {
|
||||||
Path parent = Path.of(getJarPath()).getParent();
|
Path parent = Path.of(getJarPath()).getParent();
|
||||||
if (parent == null) {
|
if (parent == null) {
|
||||||
@@ -36,17 +62,14 @@ public class RunEnv {
|
|||||||
return createTempDir(parent);
|
return createTempDir(parent);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 在指定目录下创建以时间戳命名的临时目录,返回目录路径;目录已存在时返回 null
|
/**
|
||||||
|
* 在指定目录下创建唯一的临时子目录,返回目录路径;创建失败抛出 RuntimeException。
|
||||||
|
*/
|
||||||
private static String createTempDir(Path parent) {
|
private static String createTempDir(Path parent) {
|
||||||
Path tempDir = parent.resolve(String.valueOf(System.currentTimeMillis()));
|
try {
|
||||||
if (!tempDir.toFile().exists()) {
|
return Files.createTempDirectory(parent, TEMP_DIR_PREFIX).toString();
|
||||||
try {
|
} catch (IOException e) {
|
||||||
tempDir.toFile().mkdirs();
|
throw new RuntimeException("在 " + parent + " 下创建临时目录失败", e);
|
||||||
} catch (Exception e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
return tempDir.toString();
|
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,14 +2,18 @@ package top.gtb520.java.gnu.java.utils.library;
|
|||||||
|
|
||||||
public class version {
|
public class version {
|
||||||
// 项目的版本信息,不代表实时运行环境
|
// 项目的版本信息,不代表实时运行环境
|
||||||
public static final String VERSION = "1.5";
|
// 与 pom.xml 保持一致;构建 JAR 时优先读取 MANIFEST 的 Implementation-Version
|
||||||
|
public static final String VERSION = "1.7";
|
||||||
public static final String JAVA_VERSION = "25";
|
public static final String JAVA_VERSION = "25";
|
||||||
public static final String OpenJDK_VERSION = "25.0.0.0.1";
|
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_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 static final String OpenJDK_64Bit_Server_VM = "25.0.0.0.1+36-GA";
|
||||||
|
|
||||||
public String GetVersion() {
|
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() {
|
public String GetJavaVersion() {
|
||||||
return JAVA_VERSION;
|
return JAVA_VERSION;
|
||||||
|
|||||||
@@ -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!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 测试完成 ==========");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-205
@@ -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++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+72
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
+167
@@ -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")));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
-335
@@ -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++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+24
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
+62
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
-47
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-142
@@ -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++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-8
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,62 @@
|
|||||||
package top.gtb520.java.gnu.java.utils.library.utils.runtime;
|
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 {
|
class RunEnvTest {
|
||||||
public static void main() {
|
|
||||||
System.out.println(getJarPath());
|
@Test
|
||||||
System.out.printf(newJarThisTempDir() + "\n");
|
void getTempDirNotNull() {
|
||||||
System.out.println(newTempDir());
|
assertNotNull(RunEnv.getTempDir());
|
||||||
|
assertFalse(RunEnv.getTempDir().isEmpty());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user