Compare commits

...
38 Commits
Author SHA1 Message Date
database-mysql b1d4fed408 chore: 版本号升级为 1.7-SNAPSHOT 2026-10-18 16:33:02 +08:00
database-mysql cfea5ed26b fix: 修复安全漏洞与功能缺陷,测试迁移至 JUnit 5
- compress: 修复 Zip-Slip 路径穿越(UnZip/UnTarGz 校验条目路径并拒绝链接条目)
- encrypt: AES 升级为 AES-256/GCM,RSA 改为真实混合加密,流式处理大文件,
  临时文件+原子替换避免损坏原文,密钥文件限制为属主可读写,DES 标记废弃
- random: 修复上界 Integer.MAX_VALUE 溢出,randomString 参数校验
- echo: ColorPrintlnPlus 格式码叠加(颜色+加粗等),颜色码输入校验防终端注入
- hwid: ProcessBuilder+超时+并发排空输出流,Windows 改用 PowerShell CIM(wmic 回退),
  修正 Linux 磁盘序列号命令,MAC 优先物理网卡,哈希失败不再回退 hashCode
- runtime: getJarPath 经 URI 解析(处理空格/编码),临时目录改用 Files.createTempDirectory
- file: CreateFile 移除死代码与假异步;CompressPNG 临时文件+原子替换,阈值空校验
- build: pom 补元数据/插件版本/manifest 版本注入,version 与 pom 对齐并读取 Implementation-Version
- test: 全部测试迁移为 JUnit 5(53 个用例),删除 System.exit 式手写测试与 10 亿次循环测试
- chore: 删除 cp.txt 并加入 .gitignore
2026-10-18 16:30:47 +08:00
database-mysql bf3b92d206 修复了README一些问题 2026-09-24 02:39:53 +08:00
database-mysql 8e160d7c70 Merge pull request 'dev' (#2) from dev into main
Reviewed-on: #2
2026-09-24 02:22:36 +08:00
database-mysql fb4ec202b1 修复了README一些问题 2026-09-24 02:20:05 +08:00
database-mysql e2e9b0e8a9 修复了一些BUG 2026-09-24 02:19:12 +08:00
database-mysql 6e7a1bfbee Create PNG Image Compress 2026-09-20 22:58:46 +08:00
database-mysql bc5bfe0d00 Create PNG Image Compress 2026-09-20 22:57:12 +08:00
database-mysql 341ec1b0f6 修复了一些BUG 2026-08-14 04:46:38 +08:00
database-mysql 9817a41801 INFO.md 2026-08-14 00:26:55 +08:00
database-mysql e3263fc56e 版本更新 2026-08-12 03:58:18 +08:00
database-mysql ec1728f4e2 Merge branch 'main' of https://git.opensource-studio.info/database-mysql/gnu_java_utils_library 2026-08-12 03:54:51 +08:00
database-mysql 7464c1fc9f Merge branch 'dev' 2026-08-12 03:53:52 +08:00
database-mysql faa29bb664 Merge pull request 'dev文件加解密操作' (#1) from dev into main
Reviewed-on: #1
2026-08-12 03:53:40 +08:00
database-mysql a0d6c6570f 文件加解密操作 2026-08-12 03:50:36 +08:00
database-mysql 9d452ded62 文件加解密操作 2026-08-12 03:43:22 +08:00
database-mysql e31384a848 新增/未测试 文件加解密操作 2026-08-12 03:09:26 +08:00
database-mysql c38b8857d5 新增/未测试 文件操作 2026-08-12 03:00:02 +08:00
database-mysql 1729ea3319 更新 ColorNameEnum注释 2026-08-11 19:19:31 +08:00
database-mysql 7bb822db9f 更新 README/INFO 2026-08-11 01:11:34 +08:00
database-mysql 3ec2e132c3 更新 README/pom.xml 2026-08-10 20:18:26 +08:00
database-mysql c8462d5c68 添加 临时目录相关方法 2026-08-10 20:14:39 +08:00
database-mysql 441891652c 添加 临时目录相关方法 2026-08-10 20:12:21 +08:00
database-mysql 03e223df9b 添加 注释 2026-08-10 18:49:27 +08:00
database-mysql 0304fed324 添加 RunTime相关方法与类 2026-08-10 06:49:33 +08:00
database-mysql 6934ac8663 添加 VERSION接口 2026-08-10 06:27:34 +08:00
database-mysql 072d2a841b 更新 注释 2026-08-10 06:24:44 +08:00
database-mysql 4fd86d332f 添加 获取版本方法 2026-08-10 06:14:50 +08:00
database-mysql eadcca5b59 更新 README 2026-08-08 19:27:19 +08:00
database-mysql e557dcde11 更新 README 2026-08-08 19:26:31 +08:00
database-mysql 00b0636a0d 更新 README 2026-08-08 19:24:02 +08:00
database-mysql ac361cebcf 添加 版本信息 2026-08-08 19:07:12 +08:00
database-mysql 9ebb6f8354 更新 README 2026-08-08 08:27:22 +08:00
database-mysql d73881fd86 更新 README 2026-08-08 08:27:02 +08:00
database-mysql 1e577622b3 更新 README 2026-08-08 08:08:47 +08:00
database-mysql a2bfbf1005 添加功能 ColorPrintlnPlus/仿照Minecraft类型的文本终端打印 2026-08-08 04:08:18 +08:00
database-mysql 31024f33c5 修复一些已知的BUG 2026-08-06 03:40:16 +08:00
database-mysql 8ce618e03d 修改 README 2026-08-06 03:32:37 +08:00
31 changed files with 2335 additions and 915 deletions

No files matched your search

+5 -1
View File
@@ -38,6 +38,10 @@ build/
### Mac OS ###
.DS_Store
### Other ###
/target
/.idea
/.mvn
/.mvn
/build_release
/upload/
cp.txt
+20
View File
@@ -0,0 +1,20 @@
1.5-SNAPSHOT
此版本包含:
1. 压缩/解压 (`compress`)
2. 终端彩色输出 (`echo`)
2.1 预定义颜色打印 — `ColorPrintln`
2.2 自定义颜色打印 — `ColorHexPrintln`
2.3 Minecraft 风格多色打印 — `ColorPrintlnPlus`
3. 文件创建与保存 (`file/CreateFile`)
4. 文件加密/解密 (`file/EncryptionAndDecryption`)
5. 随机生成器 (`random`)
6. 硬件标识生成器 (`hwid`)
7. 版本信息 (`version`)
8. 运行时信息 (`runtime`)
6.1 获取运行时临时目录
6.2 新建临时目录
6.3 获取临时目录
6.4 获取当前运行的JAR文件路径
在 1.4-SNAPSHOT 的基础上调整了API接口,修复了一些BUG
+303 -69
View File
@@ -1,38 +1,54 @@
# GNU Java Utils Library
一个轻量级 Java 工具库,提供常用的压缩/解压与终端彩色输出功能。
一个轻量级 Java 工具库,提供压缩/解压、终端彩色输出、文件创建与管理、文件加密/解密、随机数生成、硬件标识等功能。
> 注:此项目/仓库已同步到 Github:[https://github.com/databasemysql233-debug/gnu_java_utils_library.git](https://github.com/databasemysql233-debug/gnu_java_utils_library.git)
> 原项目/仓库链接:[https://git.opensource-studio.info/database-mysql/gnu_java_utils_library.git](https://git.opensource-studio.info/database-mysql/gnu_java_utils_library.git)
## 技术栈
- **Java 25**
- **Maven** 构建
- **Apache Commons Compress 1.28.0**(压缩功能依赖)
- **JUnit Jupiter 5.13.0**(测试框架)
## 项目结构
```
src/main/java/top/gtb520/java/gnu/java/utils/library/
├── main.java # 统一入口接口
├── utils/
│ ├── compress/
│ │ └── compress.java # 压缩/解压工具类
│ └── print/
│ ├── echo.java # 终端彩色输出工具类
│ └── ColorNameEnum.java # 颜色名称枚举
├── main.java # 统一入口接口
├── version.java # 项目版本信息
└── utils/
├── compress/
│ └── compress.java # 压缩/解压工具类
├── file/
│ ├── CreateFile.java # 文件创建/保存工具类
│ └── EncryptionAndDecryption.java # 文件加密/解密工具类
├── print/
│ ├── echo.java # 终端彩色输出工具类
│ └── ColorNameEnum.java # 颜色名称枚举
└── processing/
├── random/
│ └── defaults.java # 随机数/字符串生成器
├── hwid/
│ └── getHWID.java # 硬件标识生成器
└── runtime/
├── RunEnv.java # 运行环境信息
└── JavaRunTimeEnv.java # Java 运行时环境信息
```
## 功能模块
### 1. 压缩/解压 (`compress`)
支持 **Tar.Gz** 和 **Zip** 两种压缩格式的压缩与解压操作,支持单文件和整个目录的递归处理。
支持 **Tar.Gz** 和 **Zip** 两种格式的压缩与解压,支持单文件和目录递归处理。
| 方法 | 说明 | 参数 |
|------|------|------|
| `compress.MakeTarGz(source, dest)` | 将文件或目录压缩为 `.tar.gz` | `source` 源路径,`dest` 压缩包输出路径 |
| `compress.UnTarGz(source, dest)` | 解压 `.tar.gz` 到指定目录 | `source` 压缩包路径,`dest` 解压目标目录 |
| `compress.MakeZip(source, dest)` | 将文件或目录压缩为 `.zip` | `source` 源路径,`dest` 压缩包输出路径 |
| `compress.UnZip(source, dest)` | 解压 `.zip` 到指定目录 | `source` 压缩包路径,`dest` 解压目标目录 |
| 方法 | 说明 | 参数 |
|------------------------------------|--------------------------|-------------------------------------|
| `compress.MakeTarGz(source, dest)` | 将文件或目录压缩为 `.tar.gz` | `source` 源路径,`dest` 压缩包输出路径 |
| `compress.UnTarGz(source, dest)` | 解压 `.tar.gz` 到指定目录 | `source` 压缩包路径,`dest` 解压目标目录 |
| `compress.MakeZip(source, dest)` | 将文件或目录压缩为 `.zip` | `source` 源路径,`dest` 压缩包输出路径 |
| `compress.UnZip(source, dest)` | 解压 `.zip` 到指定目录 | `source` 压缩包路径,`dest` 解压目标目录 |
**使用示例:**
@@ -63,119 +79,335 @@ main.compress.TarGz(source, dest);
### 2. 终端彩色输出 (`echo`)
基于 ANSI 转义序列,在终端中输出彩色文本。
基于 ANSI 转义序列,在终端中输出彩色文本。提供三种打印方式:
| 方法 | 说明 | 参数 |
|------|------|------|
| `echo.ColorPrintln(message, color)` | 使用预定义颜色打印 | `message` 文本内容,`color` 颜色枚举 |
| `echo.ColorHexPrintln(message, hexColor)` | 使用自定义 ANSI 颜色码打印 | `message` 文本内容,`hexColor` ANSI 颜色代码 |
#### 2.1 预定义颜色打印 — `ColorPrintln`
使用颜色枚举快速输出。
| 方法 | 说明 | 参数 |
|-------------------------------------|-------------|--------------------------------|
| `echo.ColorPrintln(message, color)` | 使用预定义颜色打印 | `message` 文本,`color` 颜色枚举 |
**支持的颜色枚举 (`ColorNameEnum`):**
| 枚举值 | 颜色 |
|--------|------|
| `RED` | 红色 |
| `GREEN` | 绿色 |
| 枚举值 | 颜色 |
|----------|----|
| `RED` | 红色 |
| `GREEN` | 绿色 |
| `YELLOW` | 黄色 |
| `BLUE` | 蓝色 |
| `BLUE` | 蓝色 |
| `PURPLE` | 紫色 |
| `CYAN` | 青色 |
**使用示例:**
| `CYAN` | 青色 |
```java
import top.gtb520.java.gnu.java.utils.library.utils.print.echo;
import top.gtb520.java.gnu.java.utils.library.utils.print.ColorNameEnum;
// 使用预定义颜色
echo.ColorPrintln("错误信息", ColorNameEnum.RED);
echo.ColorPrintln("成功信息", ColorNameEnum.GREEN);
```
// 使用自定义 ANSI 颜色码
echo.ColorHexPrintln("自定义橙色", "38;5;208");
#### 2.2 自定义颜色打印 — `ColorHexPrintln`
支持 `#RRGGBB`、`#RGB` 十六进制颜色及 ANSI 颜色代码。
| 方法 | 说明 | 参数 |
|-------------------------------------------|------------------|-------------------------------------|
| `echo.ColorHexPrintln(message, hexColor)` | 使用自定义颜色码打印 | `message` 文本,`hexColor` 颜色代码 |
**支持的颜色格式:**
| 格式 | 示例 | 说明 |
|-------------|-------------|------------------------|
| `#RRGGBB` | `#FFC0CB` | 十六进制真彩色,自动转 ANSI 序列 |
| `#RGB` | `#F0F` | 十六进制简写,自动扩展后转 ANSI 序列 |
| ANSI 颜色码 | `31` | 标准 ANSI 颜色(如 31=红色) |
| ANSI 扩展色 | `38;5;208` | 256 色扩展(如 208=橙色) |
```java
// 十六进制颜色
echo.ColorHexPrintln("粉色文本", "#FFC0CB");
echo.ColorHexPrintln("简写青色", "#0FF");
// ANSI 颜色码
echo.ColorHexPrintln("红色文本", "31");
echo.ColorHexPrintln("扩展橙色", "38;5;208");
```
#### 2.3 Minecraft 风格多色打印 — `ColorPrintlnPlus`
在单行内使用 `&X` 格式码打印多种颜色和文本效果,仿照 Minecraft 格式化语法。
| 方法 | 说明 | 参数 |
|------------------------------------|-----------------------|-----------------------------|
| `echo.ColorPrintlnPlus(message)` | 解析 `&X` 格式码后打印多色文本 | `message` 含格式码的文本 |
**颜色代码:**
| 代码 | 颜色 | 代码 | 颜色 |
|------|--------|------|--------|
| `&0` | 黑色 | `&8` | 深灰色 |
| `&1` | 深蓝色 | `&9` | 浅蓝色 |
| `&2` | 深绿色 | `&a` | 浅绿色 |
| `&3` | 深青色 | `&b` | 浅青色 |
| `&4` | 深红色 | `&c` | 浅红色 |
| `&5` | 深紫色 | `&d` | 粉色 |
| `&6` | 橙色 | `&e` | 黄色 |
| `&7` | 灰色 | `&f` | 白色 |
**格式代码:**
| 代码 | 效果 | 代码 | 效果 |
|------|------|------|------|
| `&l` | 加粗 | `&k` | 乱码(随机字符替换) |
| `&m` | 删除线 | `&r` | 重置 |
| `&n` | 下划线 | | |
| `&o` | 斜体 | | |
> 格式码大小写不敏感,`&A` 与 `&a` 等效。非合法格式码(如 `&z`)将原样保留。
```java
import top.gtb520.java.gnu.java.utils.library.utils.print.echo;
// 多色文本
echo.ColorPrintlnPlus("&6Welcome &2to &4Project");
// 混合格式效果
echo.ColorPrintlnPlus("&4&l粗体红色 &n&9下划线蓝色");
// 乱码效果(每次调用生成不同随机字符)
echo.ColorPrintlnPlus("&k这是一段乱码文字");
// 重置后恢复普通文本
echo.ColorPrintlnPlus("&4红色文字 &r普通文字");
```
也可以通过统一入口 `main` 接口调用:
```java
import top.gtb520.java.gnu.java.utils.library.main;
import top.gtb520.java.gnu.java.utils.library.utils.print.ColorNameEnum;
main.echo.Println("警告信息", ColorNameEnum.YELLOW);
main.echo.ColorHexPrintln("自定义颜色", "31");
main.echo.ColorHexPrintln("自定义颜色", "#FF0000");
main.echo.ColorPrintlnPlus("&a多色 &c文本");
```
### 3. 随机生成器 (`random`)
### 3. 文件创建与保存 (`file/CreateFile`)
随机生成器,提供生成随机字符串、数字、UUID 等功能。
支持创建普通/二进制文件,以及异步字节流文件保存。
| 方法 | 说明 | 参数 |
|------|------|---------------------------------|
| `processing.random.defaults.random(start_scope, end_scope)` | 根据输入的范围生成随机数 | `start_scope` 起始数,`end_scope` 结束数 |
| `processing.random.defaults.randomString(int length)` | 生成指定长度的随机字符串 | `length` 字符串长度 |
| 方法 | 说明 | 参数 |
|-------------------------------------------------------|---------------------------------------|-------------------------------------------------------------|
| `CreateFile.NewFile(FileName, isBin, binFileType)` | 创建空白文件,二进制文件自动写入文件头 | `FileName` 文件名,`isBin` 是否二进制,`binFileType` 二进制类型(普通文件传 `null`) |
| `CreateFile.StreamSaveFile(file, TargePath, FileName)` | 以字节流方式异步保存文件到目标路径 | `file` 源文件,`TargePath` 目标目录,`FileName` 保存的文件名 |
**使用示例:**
**二进制文件类型 (`BinFileType`):**
| 枚举值 | 文件头标识 |
|-------|----------------|
| `ELF` | `7F 45 4C 46` |
| `PE` | `4D 5A` |
```java
package top.gtb520.java.gnu.java.utils.library.utils.processing.random;
import top.gtb520.java.gnu.java.utils.library.utils.file.CreateFile;
import top.gtb520.java.gnu.java.utils.library.utils.file.CreateFile.BinFileType;
import java.io.File;
import java.nio.file.Path;
defaults.random(1, 100);
defaults.randomString(10);
```
// 创建普通空白文件
File plainFile = CreateFile.NewFile("/path/to/file.txt", false, null);
### 4. HWID生成
// 创建 ELF 二进制文件(自动写入 ELF 文件头)
File elfFile = CreateFile.NewFile("/path/to/program", true, BinFileType.ELF);
HWID生成器,提供生成硬件ID的功能。
// 创建 PE 二进制文件(自动写入 MZ 文件头)
File peFile = CreateFile.NewFile("/path/to/app.exe", true, BinFileType.PE);
| 方法 | 说明 | 参数 |
|---------------------------------------------------------------------------------------|--------|---|
| `processing.hwid.getHWID.generateHWID()` | 生成HWID | 无 |
// 异步字节流保存文件
boolean success = CreateFile.StreamSaveFile(sourceFile, Path.of("/path/to/dest"), "output.bin");
```
**使用示例:**
### 4. 文件加密/解密 (`file/EncryptionAndDecryption`)
支持 **AES**、**DES** 对称加密,加密后覆盖原文件,密钥单独保存到指定目录。解密时从密钥文件读取密钥,解密后覆盖还原原文件。
| 方法 | 说明 | 参数 |
|----------------------------------------------------------------|--------------------------|-----------------------------------------------|
| `EncryptionAndDecryption.EncryptionFile(file, type, keyPath)` | 异步加密文件,覆盖原文件,密钥保存到 keyPath | `file` 文件,`type` 加密类型,`keyPath` 密钥目录路径 |
| `EncryptionAndDecryption.DecryptionFile(file, type, keyfile)` | 异步解密文件,覆盖还原原文件 | `file` 加密文件,`type` 加密类型,`keyfile` 密钥文件 |
**加密类型 (`EncryptionType`):**
| 枚举值 | 算法 | 密钥长度 |
|-------|---------------------|---------|
| `AES` | AES/ECB/PKCS5Padding | 128 bit |
| `DES` | DES/ECB/PKCS5Padding | 56 bit |
| `RSA` | AES(对称替代) | 128 bit |
> 加密后密钥文件命名为 `原文件名.key`,保存在 `keyPath` 目录下。
> 加密/解密均覆盖写回原文件,不添加额外后缀。
```java
import top.gtb520.java.gnu.java.utils.library.utils.file.EncryptionAndDecryption;
import top.gtb520.java.gnu.java.utils.library.utils.file.EncryptionAndDecryption.EncryptionType;
import java.io.File;
import java.nio.file.Path;
import java.nio.file.Files;
File file = new File("/path/to/data.txt");
Path keyDir = Path.of("/path/to/keys");
// 加密文件(密钥保存到 keyDir/data.txt.key)
File encrypted = EncryptionAndDecryption.EncryptionFile(file, EncryptionType.AES, keyDir);
// 解密文件(从密钥文件读取密钥)
File keyFile = keyDir.resolve("data.txt.key").toFile();
File decrypted = EncryptionAndDecryption.DecryptionFile(file, EncryptionType.AES, keyFile);
```
### 5. 随机生成器 (`random`)
| 方法 | 说明 | 参数 |
|---------------------------------------------|--------------|------------------------------|
| `defaults.random(start_scope, end_scope)` | 生成指定范围内的随机数 | `start_scope` 起始,`end_scope` 结束 |
| `defaults.randomString(length)` | 生成指定长度的随机字符串 | `length` 字符串长度 |
```java
import top.gtb520.java.gnu.java.utils.library.utils.processing.random.defaults;
int num = defaults.random(1, 100); // 生成 1~100 的随机整数
String str = defaults.randomString(16); // 生成 16 位随机字符串(大小写字母+数字)
```
### 6. 硬件标识生成器 (`hwid`)
组合 CPU、主板、MAC 地址、硬盘序列号等信息,通过 SHA-256 生成唯一的硬件标识符(HWID)。支持 Windows、macOS、Linux 及 Android 平台。
| 方法 | 说明 | 参数 |
|------------------------|----------|----|
| `getHWID.generateHWID()` | 生成硬件标识符 | 无 |
```java
import top.gtb520.java.gnu.java.utils.library.utils.processing.hwid.getHWID;
String Hwid_Str;
Hwid_Str = getHWID.generateHWID();
System.out.println("HWID: " + Hwid_Str);
System.out.println("HWID length: " + Hwid_Str.length());
```
String hwid = getHWID.generateHWID();
System.out.println("HWID: " + hwid);
```
### 7. 版本信息 (`version`)
提供项目版本信息及编译时 Java/OpenJDK 版本常量。
| 方法 | 说明 |
|----------------------------|----------------|
| `version.GetVersion()` | 获取项目版本号 |
| `version.GetJavaVersion()` | 获取编译时 Java 版本 |
```java
import top.gtb520.java.gnu.java.utils.library.version;
version v = new version();
System.out.println("项目版本: " + v.GetVersion());
System.out.println("Java版本: " + v.GetJavaVersion());
```
也可以通过统一入口调用:
```java
import top.gtb520.java.gnu.java.utils.library.main;
main.version.getVersion(); // 项目版本
main.version.getJavaVersion(); // Java 版本
main.version.getOpenJDKVersion(); // OpenJDK 版本
main.version.getOpenJDKRuntimeEnvironment(); // OpenJDK 运行时环境
main.version.getOpenJDK64BitServerVM(); // OpenJDK 64-Bit Server VM
```
### 8. 运行时信息 (`runtime`)
获取当前 Java 运行时环境信息,包括 JAR 路径、JVM 版本、位数等。
**运行环境 (`RunEnv`):**
| 方法 | 说明 |
|-------------------------------|------------------------------------------|
| `RunEnv.getJarPath()` | 获取当前运行的 JAR 路径 |
| `RunEnv.getTempDir()` | 获取系统临时目录路径 |
| `RunEnv.newTempDir()` | 在系统临时目录下新建时间戳子目录,返回路径;已存在则返回 `null` |
| `RunEnv.newJarThisTempDir()` | 在 JAR 所在目录下新建时间戳子目录,返回路径;已存在则返回 `null` |
**Java 运行时环境 (`JavaRunTimeEnv`):**
| 方法 | 说明 |
|----------------------------------------|---------------|
| `JavaRunTimeEnv.JavaVersion()` | Java 版本号 |
| `JavaRunTimeEnv.JavaRuntimeEnvironmentVersion()` | 运行时环境版本号 |
| `JavaRunTimeEnv.JavaVMVersion()` | 虚拟机版本号 |
| `JavaRunTimeEnv.JavaBits()` | JDK 位数(如 64) |
| `JavaRunTimeEnv.JavaVMName()` | 虚拟机名称 |
```java
import top.gtb520.java.gnu.java.utils.library.utils.runtime.RunEnv;
import top.gtb520.java.gnu.java.utils.library.utils.runtime.JavaRunTimeEnv;
System.out.println("JAR路径: " + RunEnv.getJarPath());
System.out.println("临时目录: " + RunEnv.getTempDir());
System.out.println("新建临时目录: " + RunEnv.newTempDir());
System.out.println("JAR目录临时子目录: " + RunEnv.newJarThisTempDir());
System.out.println("Java版本: " + JavaRunTimeEnv.JavaVersion());
System.out.println("JVM位数: " + JavaRunTimeEnv.JavaBits());
```
也可以通过统一入口调用:
```java
import top.gtb520.java.gnu.java.utils.library.main;
main.runtime.getJarPath(); // JAR 路径
main.runtime.getJavaVersion(); // Java 版本
main.runtime.getJavaRuntimeEnvironmentVersion(); // 运行时环境版本
main.runtime.getJavaVMVersion(); // 虚拟机版本
main.runtime.getJavaBits(); // JDK 位数
main.runtime.getJavaVMName(); // 虚拟机名称
// runtime.runenv 子接口
main.runtime.runenv.getTempDir(); // 系统临时目录
main.runtime.runenv.newTempDir(); // 新建临时目录
main.runtime.runenv.newJarThisTempDir(); // JAR 目录下新建临时目录
main.runtime.runenv.getJarThisTempDir(); // 同 newJarThisTempDir
```
## 构建
```bash
mvn compile
```
或
或打包为 JAR:
```bash
mvn package
```
## 使用
编译成功后,将 `target/gnu-java-utils-library-1.0.0.jar` 放入项目依赖中。</br>
或者在releases中下载最新版jar包,放入项目依赖中。
#### 使用示例:
编译成功后,将 `target/gnu_java_utils_library-1.7-SNAPSHOT.jar` 放入项目依赖中,或在 Releases 中下载最新版 JAR 包。
**pom.xml 引入本地依赖:**
**pom.xml**
```xml
<dependencies>
<!-- 引入本地依赖,lib/gnu_java_utils_library-1.0-SNAPSHOT.jar -->
<dependency>
<groupId>top.gtb520.java.gnu</groupId>
<artifactId>gnu_java_utils_library</artifactId>
<version>1.0-SNAPSHOT</version>
<version>1.7-SNAPSHOT</version>
<scope>system</scope>
<!-- ${project.basedir} 为项目目录, /lib/ 为lib目录,xxxx.jar 依赖的Jar包-->
<systemPath>${project.basedir}/lib/gnu_java_utils_library-1.0-SNAPSHOT.jar</systemPath>
<systemPath>${project.basedir}/lib/gnu_java_utils_library-1.7-SNAPSHOT.jar</systemPath>
</dependency>
</dependencies>
```
## 异常处理
压缩/解压方法在以下情况会抛出 `IllegalArgumentException`:
@@ -183,8 +415,10 @@ mvn package
- **压缩时**:源路径不存在
- **解压时**:目标路径不是有效的目录
随机数方法在以下情况会抛出 `IllegalArgumentException`:
- **起始值大于结束值时**
## 许可证
Apache License 2.0
禁止商业用途
-1
View File
@@ -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
+49 -4
View File
@@ -6,12 +6,26 @@
<groupId>top.gtb520.java.gnu</groupId>
<artifactId>gnu_java_utils_library</artifactId>
<version>1.1-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>
<maven.compiler.source>25</maven.compiler.source>
<maven.compiler.target>25</maven.compiler.target>
<maven.compiler.release>25</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<junit.version>5.13.0</junit.version>
<surefire.version>3.6.0</surefire.version>
</properties>
<dependencies>
@@ -20,6 +34,37 @@
<artifactId>commons-compress</artifactId>
<version>1.28.0</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</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>
@@ -1,6 +1,14 @@
package top.gtb520.java.gnu.java.utils.library;
import top.gtb520.java.gnu.java.utils.library.utils.print.ColorNameEnum;
import top.gtb520.java.gnu.java.utils.library.utils.compress.CompressPNG.PNGThreshold;
import java.io.File;
import java.nio.file.Path;
import static top.gtb520.java.gnu.java.utils.library.utils.file.CreateFile.BinFileType;
import static top.gtb520.java.gnu.java.utils.library.utils.file.EncryptionAndDecryption.EncryptionType;
public interface main {
// 压缩包操作相关接口与方法
@@ -24,6 +32,11 @@ public interface main {
static void UnZip(String sourceFilePanth, String destFilePanth) throws Exception {
top.gtb520.java.gnu.java.utils.library.utils.compress.compress.UnZip(sourceFilePanth, destFilePanth);
}
// 压缩png图片
static File CompressPNG(File pngFile, PNGThreshold threshold) {
return top.gtb520.java.gnu.java.utils.library.utils.compress.CompressPNG.PNG(pngFile, threshold);
}
}
// 终端打印接口与方法
@@ -37,12 +50,17 @@ public interface main {
static void ColorHexPrintln(String Message, String ColorCode) {
top.gtb520.java.gnu.java.utils.library.utils.print.echo.ColorHexPrintln(Message, ColorCode);
}
// 在单行内打印多种颜色的文字,包含Minecraft格式代码的文本,仿照Minecraft类型的文本终端打印
static void ColorPrintlnPlus(String Message) {
top.gtb520.java.gnu.java.utils.library.utils.print.echo.ColorPrintlnPlus(Message);
}
}
// 随机数生成接口与方法
interface random {
// 默认随机数生成
static int defaule(int start_scope, int end_scope) {
static int defaults(int start_scope, int end_scope) {
return top.gtb520.java.gnu.java.utils.library.utils.processing.random.defaults.random(start_scope, end_scope);
}
@@ -58,4 +76,95 @@ public interface main {
return top.gtb520.java.gnu.java.utils.library.utils.processing.hwid.getHWID.generateHWID();
}
}
// 项目版本信息获取接口与方法
interface version {
static String getVersion() {
return top.gtb520.java.gnu.java.utils.library.version.VERSION;
}
static String getJavaVersion() {
return top.gtb520.java.gnu.java.utils.library.version.JAVA_VERSION;
}
static String getOpenJDKVersion() {
return top.gtb520.java.gnu.java.utils.library.version.OpenJDK_VERSION;
}
static String getOpenJDKRuntimeEnvironment() {
return top.gtb520.java.gnu.java.utils.library.version.OpenJDK_Runtime_Environment;
}
static String getOpenJDK64BitServerVM() {
return top.gtb520.java.gnu.java.utils.library.version.OpenJDK_64Bit_Server_VM;
}
}
// 运行时信息获取接口与方法
interface runtime {
static String getJarPath() {
return top.gtb520.java.gnu.java.utils.library.utils.runtime.RunEnv.getJarPath();
}
static String getJavaVersion() {
return top.gtb520.java.gnu.java.utils.library.utils.runtime.JavaRunTimeEnv.JavaVersion();
}
static String getJavaRuntimeEnvironmentVersion() {
return top.gtb520.java.gnu.java.utils.library.utils.runtime.JavaRunTimeEnv.JavaRuntimeEnvironmentVersion();
}
static String getJavaVMVersion() {
return top.gtb520.java.gnu.java.utils.library.utils.runtime.JavaRunTimeEnv.JavaVMVersion();
}
static String getJavaBits() {
return top.gtb520.java.gnu.java.utils.library.utils.runtime.JavaRunTimeEnv.JavaBits();
}
static String getJavaVMName() {
return top.gtb520.java.gnu.java.utils.library.utils.runtime.JavaRunTimeEnv.JavaVMName();
}
// 运行时等相关方法与接口
interface runenv {
// 获取运行时临时目录
static String newJarThisTempDir() {
return top.gtb520.java.gnu.java.utils.library.utils.runtime.RunEnv.newJarThisTempDir();
}
// 新建临时目录
static String newTempDir() {
return top.gtb520.java.gnu.java.utils.library.utils.runtime.RunEnv.newTempDir();
}
// 获取临时目录
static String getTempDir() {
return top.gtb520.java.gnu.java.utils.library.utils.runtime.RunEnv.getTempDir();
}
// 获取当前运行的JAR文件路径
static String getJarThisTempDir() {
return top.gtb520.java.gnu.java.utils.library.utils.runtime.RunEnv.newJarThisTempDir();
}
}
}
// 文件操作接口与方法
interface file {
interface CreateFile {
// 创建文件,创建空白文件,异步IO
static File NewFile(String FileName, boolean isBin, BinFileType binFileType) {
return top.gtb520.java.gnu.java.utils.library.utils.file.CreateFile.NewFile(FileName, isBin, binFileType);
}
// 以字节流方式保存文件,异步IO
static boolean StreamSaveFile(File file, Path TargePath, String FileName) {
return top.gtb520.java.gnu.java.utils.library.utils.file.CreateFile.StreamSaveFile(file, TargePath, FileName);
}
}
// 文件加解密操作
interface EncryptionAndDecryption {
// 文件加密
static File FileEncrypt(File file, EncryptionType type, Path keyPath) throws Exception {
return top.gtb520.java.gnu.java.utils.library.utils.file.EncryptionAndDecryption.EncryptionFile(file, type, keyPath);
}
// 文件解密
static File DecryptionFile(File file, EncryptionType type, File keyfile) throws Exception {
return top.gtb520.java.gnu.java.utils.library.utils.file.EncryptionAndDecryption.DecryptionFile(file, type, keyfile);
}
}
}
}
@@ -0,0 +1,113 @@
package top.gtb520.java.gnu.java.utils.library.utils.compress;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.stream.ImageOutputStream;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.Iterator;
public class CompressPNG {
/**
* 压缩PNG图片
* 传入未压缩的PNG图片文件,和压缩阈值,返回压缩后的PNG图片文件
* 5个等级的压缩阈值,从低到高,THRESHOLD_1 最低,THRESHOLD_5 最高
*
* 注意:写入过程先写同目录临时文件、成功后原子替换原文件,避免写入失败损坏原图;
* JDK 内置 PNG writer 对 compressionQuality 的支持取决于实现,档位效果可能有限。
*
* @param pngFile 未压缩的PNG图片文件
* @param threshold 压缩阈值,不能为 null
* @return 压缩后的PNG图片文件;文件不存在或不是 PNG 时返回 null
* @throws IllegalArgumentException 当 threshold 为 null 时
*/
public static File PNG(File pngFile, PNGThreshold threshold) {
if (threshold == null) {
throw new IllegalArgumentException("压缩阈值不能为 null");
}
if (pngFile == null || !pngFile.exists()) {
return null;
}
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 {
tmp = File.createTempFile(pngFile.getName() + ".", ".tmp",
pngFile.getAbsoluteFile().getParentFile());
try (ImageOutputStream ios = ImageIO.createImageOutputStream(tmp)) {
writer.setOutput(ios);
ImageWriteParam param = writer.getDefaultWriteParam();
try {
param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
param.setCompressionQuality(getCompressionQuality(threshold));
} catch (UnsupportedOperationException e) {
// 部分 PNG writer 不支持显式压缩质量,退回到默认参数
param = writer.getDefaultWriteParam();
}
writer.write(null, new IIOImage(image, null, null), param);
}
// 原子替换原文件,避免原地写入失败损坏原图
try {
Files.move(tmp.toPath(), pngFile.toPath(),
StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
} catch (AtomicMoveNotSupportedException e) {
Files.move(tmp.toPath(), pngFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
}
} finally {
writer.dispose();
if (tmp != null) {
try {
Files.deleteIfExists(tmp.toPath());
} catch (IOException ignored) {
// 清理失败不影响结果
}
}
}
return pngFile;
} catch (Exception e) {
return null;
}
}
private static float getCompressionQuality(PNGThreshold threshold) {
return switch (threshold) {
case THRESHOLD_1 -> 0.7f;
case THRESHOLD_2 -> 0.5f;
case THRESHOLD_3 -> 0.3f;
case THRESHOLD_4 -> 0.1f;
case THRESHOLD_5 -> 0.0f;
};
}
public enum PNGThreshold {
THRESHOLD_1,
THRESHOLD_2,
THRESHOLD_3,
THRESHOLD_4,
THRESHOLD_5;
}
}
@@ -3,6 +3,7 @@ package top.gtb520.java.gnu.java.utils.library.utils.compress;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
import org.apache.commons.compress.archivers.tar.TarConstants;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
@@ -34,6 +35,7 @@ public class compress {
if (!Files.isDirectory(destPath)) {
throw new IllegalArgumentException("目标路径必须是有效的目录");
}
Path destRoot = destPath.toAbsolutePath().normalize();
try (InputStream fileIn = Files.newInputStream(Paths.get(sourceFilePanth));
BufferedInputStream bufferedIn = new BufferedInputStream(fileIn);
@@ -41,8 +43,12 @@ public class compress {
TarArchiveInputStream tarIn = new TarArchiveInputStream(gzipIn)) {
TarArchiveEntry entry;
while ((entry = tarIn.getNextTarEntry()) != null) {
Path targetPath = destPath.resolve(entry.getName());
while ((entry = tarIn.getNextEntry()) != null) {
// 拒绝符号链接/硬链接条目,防止解压逃逸
if (entry.isSymbolicLink() || entry.getLinkFlag() == TarConstants.LF_LINK) {
throw new IllegalArgumentException("压缩包包含不支持的链接条目: " + entry.getName());
}
Path targetPath = resolveInside(destRoot, entry.getName());
if (entry.isDirectory()) {
Files.createDirectories(targetPath);
} else {
@@ -53,6 +59,18 @@ public class compress {
}
}
/**
* 将压缩包条目名解析为解压目录内的路径;
* 条目越过目标目录(如 ../ 或绝对路径)时抛出 IllegalArgumentException。
*/
private static Path resolveInside(Path destRoot, String entryName) {
Path targetPath = destRoot.resolve(entryName).normalize();
if (!targetPath.startsWith(destRoot)) {
throw new IllegalArgumentException("压缩包包含非法路径条目: " + entryName);
}
return targetPath;
}
/**
* 压缩Tar.Gz压缩包
*
@@ -78,7 +96,7 @@ public class compress {
@Override
public java.nio.file.FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws java.io.IOException {
String entryName = sourcePath.relativize(file).toString().replace('\\', '/');
TarArchiveEntry entry = new TarArchiveEntry(file.toFile(), entryName);
TarArchiveEntry entry = new TarArchiveEntry(file, entryName);
tarOut.putArchiveEntry(entry);
Files.copy(file, tarOut);
tarOut.closeArchiveEntry();
@@ -97,7 +115,7 @@ public class compress {
}
});
} else {
TarArchiveEntry entry = new TarArchiveEntry(sourcePath.toFile(), sourcePath.getFileName().toString());
TarArchiveEntry entry = new TarArchiveEntry(sourcePath, sourcePath.getFileName().toString());
tarOut.putArchiveEntry(entry);
Files.copy(sourcePath, tarOut);
tarOut.closeArchiveEntry();
@@ -166,14 +184,19 @@ public class compress {
if (!Files.isDirectory(destPath)) {
throw new IllegalArgumentException("目标路径必须是有效的目录");
}
Path destRoot = destPath.toAbsolutePath().normalize();
try (InputStream fileIn = Files.newInputStream(Paths.get(sourceFilePanth));
BufferedInputStream bufferedIn = new BufferedInputStream(fileIn);
ZipArchiveInputStream zipIn = new ZipArchiveInputStream(bufferedIn)) {
ZipArchiveEntry entry;
while ((entry = zipIn.getNextEntry()) != null) {
Path targetPath = destPath.resolve(entry.getName());
// 拒绝符号链接条目,防止解压逃逸
if (entry.isUnixSymlink()) {
throw new IllegalArgumentException("压缩包包含不支持的符号链接条目: " + entry.getName());
}
Path targetPath = resolveInside(destRoot, entry.getName());
if (entry.isDirectory()) {
Files.createDirectories(targetPath);
} else {
@@ -0,0 +1,79 @@
package top.gtb520.java.gnu.java.utils.library.utils.file;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
// 创建文件
public class CreateFile {
/**
* 创建文件,创建空白文件。
* isBin为True时创建一个空白的二进制文件,否则创建一个空白的普通文件。
*
* 注意:文件已存在时不覆盖、不写入文件头,直接返回该文件;
* 创建失败(如路径无权限)返回 null。
*
* @param FileName 文件名
* @param isBin 是否二进制文件
* @param binFileType 二进制文件类型,如果是普通文件则不需要设置次值,将其设置为null即可,
* 参考isBin参数,isBin参数为False则此参数设置为null
*
* @return 文件
*/
public static File NewFile(String FileName, boolean isBin, BinFileType binFileType) {
if (FileName == null) {
return null;
}
File targetFile;
try {
targetFile = new File(FileName);
if (targetFile.createNewFile()) {
if (isBin && binFileType != null) {
// 根据二进制文件类型写入文件头标识
byte[] header = switch (binFileType) {
case ELF -> new byte[]{0x7F, 'E', 'L', 'F'};
case PE -> new byte[]{'M', 'Z'};
};
try (OutputStream os = Files.newOutputStream(targetFile.toPath(),
StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)) {
os.write(header);
os.flush();
}
}
}
return targetFile;
} catch (IOException e) {
return null;
}
}
/**
* 以字节流方式保存文件到目标路径(覆盖同名文件)。
*
* @param file 源文件
* @param TargePath 目标路径
* @param FileName 文件名
*
* @return 是否保存成功,True为保存成功,False为保存失败
*/
public static boolean StreamSaveFile(File file, Path TargePath, String FileName) {
if (file == null || !file.isFile() || TargePath == null || FileName == null || FileName.isEmpty()) {
return false;
}
Path targetFile = TargePath.resolve(FileName);
try {
Files.copy(file.toPath(), targetFile, StandardCopyOption.REPLACE_EXISTING);
return true;
} catch (IOException e) {
return false;
}
}
public enum BinFileType {
ELF, PE;
}
}
@@ -0,0 +1,330 @@
package top.gtb520.java.gnu.java.utils.library.utils.file;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.PosixFilePermissions;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.spec.PKCS8EncodedKeySpec;
/**
* 文件加密/解密工具类。
*
* <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 {
private static final int AES_KEY_BITS = 256;
private static final int DES_KEY_BITS = 56;
private static final int RSA_KEY_BITS = 2048;
private static final int GCM_IV_BYTES = 12;
private static final int GCM_TAG_BITS = 128;
private static final String AES_GCM = "AES/GCM/NoPadding";
private static final String DES_ECB = "DES/ECB/PKCS5Padding";
private static final String RSA_WRAP = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding";
/**
* 文件加密。加密后覆盖原文件(原子替换),密钥保存到 keyPath 目录下,密钥名为 原文件名.key。
*
* @param file 要加密的文件
* @param type 加密类型
* @param keyPath 密钥文件目录路径
* @return 加密后的文件(原文件路径);入参非法时返回 null
*/
public static File EncryptionFile(File file, EncryptionType type, Path keyPath) {
if (file == null || !file.exists() || !file.isFile()) {
return null;
}
if (type == null || keyPath == null) {
return null;
}
try {
Files.createDirectories(keyPath);
Path keyFilePath = keyPath.resolve(file.getName() + ".key");
switch (type) {
case AES -> {
SecretKey key = generateKey("AES", AES_KEY_BITS);
writeKeyFile(keyFilePath, key.getEncoded());
encryptAesGcm(file, key);
}
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) {
throw new RuntimeException("加密失败: " + file, e);
}
}
/**
* 文件解密。解密后覆盖写回原文件(原子替换)。
*
* @param file 加密文件
* @param type 加密类型,须与加密时一致
* @param keyfile 密钥文件,从中读取解密密钥
* @return 解密后的文件(原文件路径);入参非法时返回 null
*/
public static File DecryptionFile(File file, EncryptionType type, File keyfile) {
if (file == null || !file.exists() || !file.isFile()) {
return null;
}
if (type == null || keyfile == null || !keyfile.exists() || !keyfile.isFile()) {
return null;
}
try {
byte[] keyBytes = Files.readAllBytes(keyfile.toPath());
switch (type) {
case AES -> decryptAesGcm(file, new SecretKeySpec(keyBytes, "AES"));
case DES -> transform(file, buildCipher(DES_ECB, Cipher.DECRYPT_MODE,
new SecretKeySpec(keyBytes, "DES"), null), false);
case RSA -> {
PrivateKey privateKey = KeyFactory.getInstance("RSA")
.generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
decryptRsaHybrid(file, privateKey);
}
default -> throw new IllegalArgumentException("不支持的加密类型: " + type);
}
return file;
} catch (Exception e) {
throw new RuntimeException("解密失败: " + file, e);
}
}
// ==================== AES-GCM ====================
private static void encryptAesGcm(File file, SecretKey key) throws Exception {
byte[] iv = new byte[GCM_IV_BYTES];
new SecureRandom().nextBytes(iv);
Cipher cipher = buildCipher(AES_GCM, Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(GCM_TAG_BITS, iv));
Path tmp = tempSibling(file);
try (OutputStream out = new BufferedOutputStream(Files.newOutputStream(tmp))) {
out.write(iv); // 密文格式: IV(12) || 密文
out.flush();
try (CipherOutputStream cos = new CipherOutputStream(out, cipher);
InputStream in = Files.newInputStream(file.toPath())) {
in.transferTo(cos);
}
}
moveAtomically(tmp, file);
}
private static void decryptAesGcm(File file, SecretKey key) throws Exception {
try (InputStream in = new BufferedInputStream(Files.newInputStream(file.toPath()))) {
byte[] iv = in.readNBytes(GCM_IV_BYTES);
if (iv.length != GCM_IV_BYTES) {
throw new IOException("密文过短或已损坏,无法读取 IV");
}
Cipher cipher = buildCipher(AES_GCM, Cipher.DECRYPT_MODE, key, new GCMParameterSpec(GCM_TAG_BITS, iv));
Path tmp = tempSibling(file);
try (CipherInputStream cis = new CipherInputStream(in, cipher);
OutputStream out = new BufferedOutputStream(Files.newOutputStream(tmp))) {
cis.transferTo(out);
}
moveAtomically(tmp, file);
}
}
// ==================== RSA 混合加密 ====================
private static void encryptRsaHybrid(File file, PublicKey publicKey) throws Exception {
// 随机 AES-256 会话密钥,RSA-OAEP 包裹后存入文件头
SecretKey sessionKey = generateKey("AES", AES_KEY_BITS);
byte[] iv = new byte[GCM_IV_BYTES];
new SecureRandom().nextBytes(iv);
Cipher wrapCipher = Cipher.getInstance(RSA_WRAP);
wrapCipher.init(Cipher.WRAP_MODE, publicKey);
byte[] wrappedKey = wrapCipher.wrap(sessionKey);
Cipher dataCipher = buildCipher(AES_GCM, Cipher.ENCRYPT_MODE, sessionKey,
new GCMParameterSpec(GCM_TAG_BITS, iv));
// 密文格式: 包裹密钥长度(4字节) || 包裹密钥 || IV(12) || 密文
Path tmp = tempSibling(file);
try (OutputStream out = new BufferedOutputStream(Files.newOutputStream(tmp))) {
out.write(ByteBuffer.allocate(4).putInt(wrappedKey.length).array());
out.write(wrappedKey);
out.write(iv);
out.flush();
try (CipherOutputStream cos = new CipherOutputStream(out, dataCipher);
InputStream in = Files.newInputStream(file.toPath())) {
in.transferTo(cos);
}
}
moveAtomically(tmp, file);
}
private static void decryptRsaHybrid(File file, PrivateKey privateKey) throws Exception {
try (DataInputStream in = new DataInputStream(new BufferedInputStream(Files.newInputStream(file.toPath())))) {
int wrappedKeyLength = in.readInt();
if (wrappedKeyLength <= 0 || wrappedKeyLength > 4096) {
throw new IOException("密文头损坏: 包裹密钥长度非法");
}
byte[] wrappedKey = in.readNBytes(wrappedKeyLength);
if (wrappedKey.length != wrappedKeyLength) {
throw new IOException("密文头损坏: 包裹密钥不完整");
}
byte[] iv = in.readNBytes(GCM_IV_BYTES);
if (iv.length != GCM_IV_BYTES) {
throw new IOException("密文头损坏: IV 不完整");
}
Cipher unwrapCipher = Cipher.getInstance(RSA_WRAP);
unwrapCipher.init(Cipher.UNWRAP_MODE, privateKey);
SecretKey sessionKey = (SecretKey) unwrapCipher.unwrap(wrappedKey, "AES", Cipher.SECRET_KEY);
Cipher dataCipher = buildCipher(AES_GCM, Cipher.DECRYPT_MODE, sessionKey,
new GCMParameterSpec(GCM_TAG_BITS, iv));
Path tmp = tempSibling(file);
try (CipherInputStream cis = new CipherInputStream(in, dataCipher);
OutputStream out = new BufferedOutputStream(Files.newOutputStream(tmp))) {
cis.transferTo(out);
}
moveAtomically(tmp, file);
}
}
// ==================== 通用工具 ====================
private static SecretKey generateKey(String algorithm, int keyBits) throws Exception {
KeyGenerator keyGen = KeyGenerator.getInstance(algorithm);
keyGen.init(keyBits);
return keyGen.generateKey();
}
private static KeyPair generateRsaKeyPair() throws Exception {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(RSA_KEY_BITS);
return keyGen.generateKeyPair();
}
private static Cipher buildCipher(String algorithm, int mode, SecretKey key,
java.security.spec.AlgorithmParameterSpec params) throws Exception {
Cipher cipher = Cipher.getInstance(algorithm);
if (params == null) {
cipher.init(mode, key);
} else {
cipher.init(mode, key, params);
}
return cipher;
}
/**
* 流式加/解密:读原文件 -> Cipher 流 -> 临时文件 -> 原子替换原文件。
*/
private static void transform(File file, Cipher cipher, boolean encrypt) throws Exception {
Path tmp = tempSibling(file);
if (encrypt) {
try (OutputStream out = new BufferedOutputStream(Files.newOutputStream(tmp))) {
try (CipherOutputStream cos = new CipherOutputStream(out, cipher);
InputStream in = Files.newInputStream(file.toPath())) {
in.transferTo(cos);
}
}
} else {
try (InputStream in = new BufferedInputStream(Files.newInputStream(file.toPath()))) {
try (CipherInputStream cis = new CipherInputStream(in, cipher);
OutputStream out = new BufferedOutputStream(Files.newOutputStream(tmp))) {
cis.transferTo(out);
}
}
}
moveAtomically(tmp, file);
}
/**
* 在原文件同目录创建临时文件,保证原子移动(rename)时在同一文件系统内。
*/
private static Path tempSibling(File file) throws IOException {
Path dir = file.toPath().toAbsolutePath().getParent();
return Files.createTempFile(dir, file.getName() + ".", ".tmp");
}
/**
* 原子替换原文件;文件系统不支持原子移动时回退为普通覆盖移动。
*/
private static void moveAtomically(Path tmp, File target) throws IOException {
try {
Files.move(tmp, target.toPath(), StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
} catch (AtomicMoveNotSupportedException e) {
Files.move(tmp, target.toPath(), StandardCopyOption.REPLACE_EXISTING);
}
}
private static void writeKeyFile(Path keyFilePath, byte[] keyBytes) throws IOException {
try (OutputStream os = Files.newOutputStream(keyFilePath,
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
os.write(keyBytes);
os.flush();
}
restrictKeyFilePermissions(keyFilePath);
}
/**
* POSIX 系统上将密钥文件限制为仅属主可读写;Windows 等不支持的文件系统跳过。
*/
private static void restrictKeyFilePermissions(Path keyFilePath) throws IOException {
try {
Files.setPosixFilePermissions(keyFilePath, PosixFilePermissions.fromString("rw-------"));
} catch (UnsupportedOperationException ignored) {
// Windows 等不支持 POSIX 权限的文件系统,无需处理
}
}
public enum EncryptionType {
AES,
/**
* 已废弃:DES 密钥仅 56 位,已被暴力破解,仅用于兼容旧版本,新代码请使用 {@link #AES}。
*/
@Deprecated
DES,
/**
* RSA 混合加密:RSA-2048/OAEP 包裹随机 AES-256 会话密钥,数据使用 AES-GCM 加密。
*/
RSA;
}
}
@@ -1,5 +1,6 @@
package top.gtb520.java.gnu.java.utils.library.utils.print;
// 颜色名称枚举
public enum ColorNameEnum {
RED, GREEN, YELLOW, BLUE, PURPLE, CYAN;
}
@@ -1,5 +1,7 @@
package top.gtb520.java.gnu.java.utils.library.utils.print;
import java.util.concurrent.ThreadLocalRandom;
/**
* 终端彩色输出工具类
* 提供基于 ANSI 转义序列的终端彩色打印功能
@@ -10,17 +12,37 @@ public class echo {
/**
* 使用自定义颜色码在终端打印彩色文本
* 支持三种格式:
* - "#RRGGBB" 十六进制颜色(如 "#FFC0CB")
* - "#RRGGBB" / "#RGB" 十六进制颜色(如 "#FFC0CB")
* - ANSI 颜色代码(如 "31" 表示红色)
* - ANSI 扩展色(如 "38;5;208" 表示扩展色)
*
* @param Message 要打印的文本内容
* @param HexColor 颜色代码,支持 "#RRGGBB"、ANSI 颜色代码或扩展色
* @throws IllegalArgumentException 当颜色代码格式非法或为 null 时
*/
public static void ColorHexPrintln(String Message, String HexColor) {
if (HexColor.startsWith("#") && (HexColor.length() == 7 || HexColor.length() == 4)) {
// #RRGGBB 或 #RGB 格式,转换为 ANSI 真彩色序列
String hex = HexColor.substring(1);
if (Message == null) {
Message = "";
}
String ansi = toAnsiColorCode(HexColor);
System.out.println("\033[" + ansi + "m" + Message + "\033[0m");
}
/**
* 将颜色代码转换为 ANSI 颜色序列中的参数部分,并对输入做合法性校验。
*
* @param hexColor 颜色代码:"#RRGGBB"、"#RGB" 或纯数字/分号的 ANSI 参数
* @return ANSI 参数(如 "38;2;255;192;203"、"31")
*/
private static String toAnsiColorCode(String hexColor) {
if (hexColor == null) {
throw new IllegalArgumentException("颜色码不能为 null");
}
if (hexColor.startsWith("#")) {
String hex = hexColor.substring(1);
if ((hex.length() != 3 && hex.length() != 6) || !isHex(hex)) {
throw new IllegalArgumentException("无效的颜色格式: " + hexColor);
}
int r, g, b;
if (hex.length() == 3) {
// #RGB 简写格式,将每个字符扩展为两位(如 F -> FF)
@@ -32,20 +54,35 @@ public class echo {
g = Integer.parseInt(hex.substring(2, 4), 16);
b = Integer.parseInt(hex.substring(4, 6), 16);
}
System.out.println("\033[38;2;" + r + ";" + g + ";" + b + "m" + Message + "\033[0m");
} else {
// 原有 ANSI 颜色代码直接拼接
System.out.println("\033[" + HexColor + "m" + Message + "\033[0m");
return "38;2;" + r + ";" + g + ";" + b;
}
// 仅允许数字与分号,防止向终端注入任意转义序列
if (!hexColor.matches("[0-9]+(;[0-9]+)*")) {
throw new IllegalArgumentException("无效的ANSI颜色码: " + hexColor);
}
return hexColor;
}
private static boolean isHex(String s) {
for (int i = 0; i < s.length(); i++) {
if (Character.digit(s.charAt(i), 16) < 0) {
return false;
}
}
return true;
}
/**
* 使用预定义的颜色名称在终端打印彩色文本
*
* @param Message 要打印的文本内容
* @param ColorName 颜色名称,支持:RED、GREEN、YELLOW、BLUE、PURPLE、CYAN;传入不支持的名称则按默认颜色输出
* @param ColorName 颜色名称,支持:RED、GREEN、YELLOW、BLUE、PURPLE、CYAN;传入 null 则按默认颜色输出
*/
public static void ColorPrintln(String Message, ColorNameEnum ColorName) {
if (ColorName == null) {
System.out.println(Message);
return;
}
// 根据颜色名称映射对应的 ANSI 颜色转义序列
switch (ColorName) {
case RED:
@@ -71,4 +108,139 @@ public class echo {
System.out.println(Message);
}
}
/**
* 在单行内打印多种颜色的文字
*
* 颜色代码仿照Minecraft格式,如:
* &4(红色)、&c(浅红色)、
* &6(橙色)、&e(黄色)、
* &2(绿色)、&a(浅绿色)、
* &b(浅青色)、&3(深青色)、
* &1(蓝色)、&9(浅蓝色)、
* &d(粉色)、&5(紫色)、
* &f(白色)、&7(灰色)、
* &8(深灰色)、&0(黑色)、
* &l(加粗)、&n(下划线)、
* &o(斜体)、&k(乱码)、
* &m(删除线)、&r(重置)。
*
* 颜色码会重置之前的格式;加粗/下划线/斜体/删除线等格式码互相叠加;
* 例如 &4&l 得到"粗体红色",&n&9 得到"下划线蓝色"。
*
* 示例:&6Welcome &2to &4Project
*
* @param Message 包含Minecraft格式代码的文本
*/
public static void ColorPrintlnPlus(String Message) {
if (Message == null) {
System.out.println();
return;
}
StringBuilder result = new StringBuilder(Message.length() + 32);
// 当前叠加的格式序列(加粗/下划线等),颜色码切换后需要重新应用
StringBuilder activeFormat = new StringBuilder();
boolean obfuscated = false;
int i = 0;
while (i < Message.length()) {
// 检测格式代码:& 后跟一个字符
if (Message.charAt(i) == '&' && i + 1 < Message.length()) {
char code = Character.toLowerCase(Message.charAt(i + 1));
String ansi = getAnsiCode(code);
if (ansi != null) {
if (isColorCode(code)) {
// 颜色码:重置后应用新颜色,再恢复叠加的格式
result.append("\033[0m").append(ansi).append(activeFormat);
obfuscated = false;
} else if (code == 'r') {
// 重置:清空颜色与全部格式
result.append("\033[0m");
activeFormat.setLength(0);
obfuscated = false;
} else if (code == 'k') {
// 乱码模式开启
activeFormat.append(ansi);
result.append(ansi);
obfuscated = true;
} else {
// 加粗/下划线/斜体/删除线:格式叠加生效
activeFormat.append(ansi);
result.append(ansi);
obfuscated = false;
}
i += 2;
continue;
}
}
// 普通字符
char ch = Message.charAt(i);
if (obfuscated && ch != '\n' && ch != '\r') {
// 乱码模式:将可见字符替换为随机字符
result.append(randomChar());
} else {
result.append(ch);
}
i++;
}
// 结尾重置格式
result.append("\033[0m");
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 转义序列
*
* @param code & 后的小写字符
* @return ANSI 转义序列,若字符不是合法格式代码则返回 null
*/
private static String getAnsiCode(char code) {
switch (code) {
// ===== 颜色码 =====
case '0': return "\033[30m"; // 黑色
case '1': return "\033[34m"; // 深蓝色
case '2': return "\033[32m"; // 深绿色
case '3': return "\033[36m"; // 深青色
case '4': return "\033[31m"; // 深红色
case '5': return "\033[35m"; // 深紫色
case '6': return "\033[38;5;214m"; // 橙色(Gold)
case '7': return "\033[37m"; // 灰色
case '8': return "\033[90m"; // 深灰色
case '9': return "\033[94m"; // 浅蓝色
case 'a': return "\033[92m"; // 浅绿色
case 'b': return "\033[96m"; // 浅青色
case 'c': return "\033[91m"; // 浅红色
case 'd': return "\033[95m"; // 粉色
case 'e': return "\033[93m"; // 黄色
case 'f': return "\033[97m"; // 白色
// ===== 格式码 =====
case 'l': return "\033[1m"; // 加粗
case 'm': return "\033[9m"; // 删除线
case 'n': return "\033[4m"; // 下划线
case 'o': return "\033[3m"; // 斜体
case 'k': return "\033[8m"; // 乱码(隐藏文字,配合随机字符替换实现)
case 'r': return "\033[0m"; // 重置
default: return null; // 非合法格式码,原样保留
}
}
/**
* 生成一个随机可见字符,用于乱码模式
*
* @return 随机可见字符
*/
private static char randomChar() {
// 从可打印 ASCII 字符范围(33~126)中随机选取
return (char) ThreadLocalRandom.current().nextInt(33, 127);
}
}
@@ -1,16 +1,30 @@
package top.gtb520.java.gnu.java.utils.library.utils.processing.hwid;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.NetworkInterface;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* 硬件标识符(HWID)生成器。
*
* <p>组合 CPU、主板、MAC 地址、硬盘序列号与操作系统信息,通过 SHA-256 生成 64 位十六进制标识。
* 所有外部命令通过 {@link ProcessBuilder} 执行,带超时并并发排空输出流,避免挂起。
* 部分硬件信息获取失败时自动降级(跳过该组件),全部失败时返回 {@code "UNKNOWN_HWID"}。</p>
*/
public class getHWID {
private final static boolean Debug = false;
private static final long COMMAND_TIMEOUT_SECONDS = 10;
/**
* 生成硬件标识符(HWID)
@@ -20,48 +34,23 @@ public class getHWID {
try {
List<String> hardwareInfo = new ArrayList<>();
// 1. 获取CPU信息
String cpuInfo = getCPUInfo();
if (cpuInfo != null && !cpuInfo.isEmpty()) {
hardwareInfo.add("CPU:" + cpuInfo);
}
addIfPresent(hardwareInfo, "CPU", getCPUInfo());
addIfPresent(hardwareInfo, "MB", getMotherboardInfo());
addIfPresent(hardwareInfo, "MAC", getMacAddress());
addIfPresent(hardwareInfo, "DISK", getDiskSerialNumber());
// 2. 获取主板序列号
String motherboardInfo = getMotherboardInfo();
if (motherboardInfo != null && !motherboardInfo.isEmpty()) {
hardwareInfo.add("MB:" + motherboardInfo);
}
// 3. 获取MAC地址
String macAddress = getMacAddress();
if (macAddress != null && !macAddress.isEmpty()) {
hardwareInfo.add("MAC:" + macAddress);
}
// 4. 获取硬盘序列号
String diskInfo = getDiskSerialNumber();
if (diskInfo != null && !diskInfo.isEmpty()) {
hardwareInfo.add("DISK:" + diskInfo);
}
// 5. 获取操作系统信息
String osInfo = System.getProperty("os.name") + "_" +
System.getProperty("os.version");
String osInfo = System.getProperty("os.name") + "_" + System.getProperty("os.version");
hardwareInfo.add("OS:" + osInfo);
// 6. 组合所有硬件信息并生成哈希
StringBuilder combined = new StringBuilder();
for (String info : hardwareInfo) {
if (Debug) {
System.out.println("info: " + info);
System.out.println("hardwareInfo: " + hardwareInfo);
System.out.println("combined: " + combined);
}
combined.append(info).append("|");
}
if (Debug) {
System.out.println("HWID combined: " + combined.toString());
System.out.println("HWID combined: " + combined);
}
// 使用SHA-256生成固定长度的哈希值
@@ -73,12 +62,18 @@ public class getHWID {
}
}
private static void addIfPresent(List<String> list, String label, String value) {
if (value != null && !value.isEmpty()) {
list.add(label + ":" + value);
}
}
/**
* 检测是否为Android系统
*/
private static boolean isAndroid() {
try {
String osName = System.getProperty("os.name").toLowerCase();
String osName = System.getProperty("os.name", "").toLowerCase();
String javaVmName = System.getProperty("java.vm.name", "").toLowerCase();
String javaSpecVendor = System.getProperty("java.specification.vendor", "").toLowerCase();
@@ -86,7 +81,7 @@ public class getHWID {
return osName.contains("android") ||
javaVmName.contains("android") ||
javaSpecVendor.contains("android") ||
new java.io.File("/system/bin/app_process").exists();
Files.exists(Path.of("/system/bin/app_process"));
} catch (Exception e) {
return false;
}
@@ -96,55 +91,43 @@ public class getHWID {
* 获取CPU信息
*/
private static String getCPUInfo() {
try {
// Android系统特殊处理
if (isAndroid()) {
return getAndroidCPUInfo();
// Android系统特殊处理
if (isAndroid()) {
return getAndroidCPUInfo();
}
String os = System.getProperty("os.name", "").toLowerCase();
if (os.contains("win")) {
// Windows:优先使用 PowerShell CIM(wmic 在较新 Windows 中已被移除)
String id = firstLine(runCommand(List.of(
"powershell", "-NoProfile", "-Command",
"Get-CimInstance Win32_Processor | Select-Object -ExpandProperty ProcessorId")));
if (id == null || id.isEmpty()) {
id = firstNonHeaderLine(runCommand(List.of("wmic", "cpu", "get", "ProcessorId")), "ProcessorId");
}
return id;
} else if (os.contains("mac")) {
return firstLine(runCommand(List.of("/usr/sbin/sysctl", "-n", "machdep.cpu.brand_string")));
} else {
// Linux:直接读取 /proc/cpuinfo,避免外部进程
return readLinuxCpuInfo();
}
}
String os = System.getProperty("os.name").toLowerCase();
Process process;
if (os.contains("win")) {
// Windows: 获取ProcessorId
process = Runtime.getRuntime().exec(
new String[]{"wmic", "cpu", "get", "ProcessorId"}
);
} else if (os.contains("mac")) {
// macOS
process = Runtime.getRuntime().exec(
new String[]{"/usr/sbin/sysctl", "-n", "machdep.cpu.brand_string"}
);
} else {
// Linux: 获取CPU型号
process = Runtime.getRuntime().exec(
new String[]{"cat", "/proc/cpuinfo"}
);
}
process.waitFor();
BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream())
);
private static String readLinuxCpuInfo() {
try (BufferedReader reader = Files.newBufferedReader(Path.of("/proc/cpuinfo"), StandardCharsets.UTF_8)) {
String line;
StringBuilder result = new StringBuilder();
while ((line = reader.readLine()) != null) {
line = line.trim();
if (!line.isEmpty()) {
if (os.contains("win") && !line.equals("ProcessorId")) {
return line;
} else if (os.contains("mac")) {
return line;
} else if (line.startsWith("model name")) {
return line.split(":", 2)[1].trim();
if (line.startsWith("model name")) {
String[] parts = line.split(":", 2);
if (parts.length > 1) {
return parts[1].trim();
}
}
}
} catch (Exception e) {
// 静默失败,返回null
} catch (IOException ignored) {
// 读取失败,返回 null
}
return null;
}
@@ -153,210 +136,156 @@ public class getHWID {
* 获取Android CPU信息
*/
private static String getAndroidCPUInfo() {
StringBuilder cpuInfo = new StringBuilder();
try {
// 方法1: 读取 /proc/cpuinfo
BufferedReader reader = new BufferedReader(new FileReader("/proc/cpuinfo"));
List<String> infoList = new ArrayList<>();
try (BufferedReader reader = Files.newBufferedReader(Path.of("/proc/cpuinfo"), StandardCharsets.UTF_8)) {
String line;
List<String> infoList = new ArrayList<>();
while ((line = reader.readLine()) != null) {
line = line.trim();
// 提取关键信息
if (line.startsWith("Processor") || line.startsWith("model name")) {
if (line.startsWith("Processor") || line.startsWith("model name")
|| line.startsWith("Hardware") || line.startsWith("CPU implementer")
|| line.startsWith("CPU architecture") || line.startsWith("CPU variant")
|| line.startsWith("CPU part")) {
String[] parts = line.split(":", 2);
if (parts.length > 1) {
infoList.add("Processor:" + parts[1].trim());
}
} else if (line.startsWith("Hardware")) {
String[] parts = line.split(":", 2);
if (parts.length > 1) {
infoList.add("Hardware:" + parts[1].trim());
}
} else if (line.startsWith("CPU implementer")) {
String[] parts = line.split(":", 2);
if (parts.length > 1) {
infoList.add("Implementer:" + parts[1].trim());
}
} else if (line.startsWith("CPU architecture")) {
String[] parts = line.split(":", 2);
if (parts.length > 1) {
infoList.add("Architecture:" + parts[1].trim());
}
} else if (line.startsWith("CPU variant")) {
String[] parts = line.split(":", 2);
if (parts.length > 1) {
infoList.add("Variant:" + parts[1].trim());
}
} else if (line.startsWith("CPU part")) {
String[] parts = line.split(":", 2);
if (parts.length > 1) {
infoList.add("Part:" + parts[1].trim());
infoList.add(parts[0].trim() + ":" + parts[1].trim());
}
}
}
reader.close();
if (!infoList.isEmpty()) {
return String.join("|", infoList);
}
} catch (Exception e) {
} catch (IOException ignored) {
// 静默失败
}
// 方法2: 如果读取文件失败,使用System.getProperty
try {
String abi = System.getProperty("os.arch", "unknown");
String cores = Runtime.getRuntime().availableProcessors() + "";
return "ABI:" + abi + "|Cores:" + cores;
} catch (Exception e) {
return null;
if (!infoList.isEmpty()) {
return String.join("|", infoList);
}
// 方法2: 如果读取文件失败,使用系统属性
String abi = System.getProperty("os.arch", "unknown");
String cores = Runtime.getRuntime().availableProcessors() + "";
return "ABI:" + abi + "|Cores:" + cores;
}
/**
* 获取主板信息
*/
private static String getMotherboardInfo() {
try {
// Android系统特殊处理
if (isAndroid()) {
return getAndroidMotherboardInfo();
// Android系统特殊处理
if (isAndroid()) {
return getAndroidMotherboardInfo();
}
String os = System.getProperty("os.name", "").toLowerCase();
if (os.contains("win")) {
String sn = firstLine(runCommand(List.of(
"powershell", "-NoProfile", "-Command",
"Get-CimInstance Win32_BaseBoard | Select-Object -ExpandProperty SerialNumber")));
if (sn == null || sn.isEmpty()) {
sn = firstNonHeaderLine(runCommand(List.of("wmic", "baseboard", "get", "SerialNumber")), "SerialNumber");
}
String os = System.getProperty("os.name").toLowerCase();
if (os.contains("win")) {
Process process = Runtime.getRuntime().exec(
new String[]{"wmic", "baseboard", "get", "SerialNumber"}
);
process.waitFor();
BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream())
);
String line;
while ((line = reader.readLine()) != null) {
line = line.trim();
if (!line.isEmpty() && !line.equals("SerialNumber")) {
return line;
}
}
}
} catch (Exception e) {
// 静默失败
return sn;
}
return null;
}
/**
* 获取Android主板信息
*
* <p>注意:Android 系统属性 ro.product.* 无法通过 System.getProperty 获取,
* 需要 android.os.Build;此处仅作为尽力而为的尝试。</p>
*/
private static String getAndroidMotherboardInfo() {
try {
List<String> infoList = new ArrayList<>();
List<String> infoList = new ArrayList<>();
// 读取Android设备特性
String brand = System.getProperty("ro.product.brand", "");
String manufacturer = System.getProperty("ro.product.manufacturer", "");
String model = System.getProperty("ro.product.model", "");
String device = System.getProperty("ro.product.device", "");
String board = System.getProperty("ro.product.board", "");
String brand = System.getProperty("ro.product.brand", "");
String manufacturer = System.getProperty("ro.product.manufacturer", "");
String model = System.getProperty("ro.product.model", "");
String device = System.getProperty("ro.product.device", "");
String board = System.getProperty("ro.product.board", "");
if (!brand.isEmpty()) infoList.add("Brand:" + brand);
if (!manufacturer.isEmpty()) infoList.add("Manufacturer:" + manufacturer);
if (!model.isEmpty()) infoList.add("Model:" + model);
if (!device.isEmpty()) infoList.add("Device:" + device);
if (!board.isEmpty()) infoList.add("Board:" + board);
if (!brand.isEmpty()) infoList.add("Brand:" + brand);
if (!manufacturer.isEmpty()) infoList.add("Manufacturer:" + manufacturer);
if (!model.isEmpty()) infoList.add("Model:" + model);
if (!device.isEmpty()) infoList.add("Device:" + device);
if (!board.isEmpty()) infoList.add("Board:" + board);
if (!infoList.isEmpty()) {
return String.join("|", infoList);
}
} catch (Exception e) {
// 静默失败
}
return null;
return infoList.isEmpty() ? null : String.join("|", infoList);
}
/**
* 获取MAC地址
* 优先返回非回环、非虚拟网卡的 MAC,避免虚拟网卡导致 HWID 漂移;全部为虚拟网卡时回退到第一个可用 MAC。
*/
private static String getMacAddress() {
try {
Enumeration<NetworkInterface> networkInterfaces =
NetworkInterface.getNetworkInterfaces();
Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
if (networkInterfaces == null) {
return null;
}
String fallback = null;
while (networkInterfaces.hasMoreElements()) {
NetworkInterface network = networkInterfaces.nextElement();
byte[] mac = network.getHardwareAddress();
if (mac != null && mac.length > 0) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < mac.length; i++) {
sb.append(String.format("%02X%s", mac[i],
(i < mac.length - 1) ? "-" : ""));
try {
byte[] mac = network.getHardwareAddress();
if (mac == null || mac.length == 0) {
continue;
}
return sb.toString();
String formatted = formatMac(mac);
if (fallback == null) {
fallback = formatted;
}
if (network.isLoopback() || network.isVirtual()) {
continue;
}
return formatted;
} catch (IOException ignored) {
// 该网卡信息不可用,继续尝试下一个
}
}
return fallback;
} catch (Exception e) {
// 静默失败
return null;
}
return null;
}
private static String formatMac(byte[] mac) {
StringBuilder sb = new StringBuilder(mac.length * 3);
for (int i = 0; i < mac.length; i++) {
sb.append(String.format("%02X%s", mac[i], (i < mac.length - 1) ? "-" : ""));
}
return sb.toString();
}
/**
* 获取硬盘序列号
*/
private static String getDiskSerialNumber() {
try {
// Android系统特殊处理
if (isAndroid()) {
return getAndroidStorageInfo();
// Android系统特殊处理
if (isAndroid()) {
return getAndroidStorageInfo();
}
String os = System.getProperty("os.name", "").toLowerCase();
if (os.contains("win")) {
String sn = firstLine(runCommand(List.of(
"powershell", "-NoProfile", "-Command",
"Get-CimInstance Win32_DiskDrive | Select-Object -ExpandProperty SerialNumber")));
if (sn == null || sn.isEmpty()) {
sn = firstNonHeaderLine(runCommand(List.of("wmic", "diskdrive", "get", "SerialNumber")), "SerialNumber");
}
String os = System.getProperty("os.name").toLowerCase();
if (os.contains("win")) {
Process process = Runtime.getRuntime().exec(
new String[]{"wmic", "diskdrive", "get", "SerialNumber"}
);
process.waitFor();
BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream())
);
String line;
while ((line = reader.readLine()) != null) {
line = line.trim();
if (!line.isEmpty() && !line.equals("SerialNumber")) {
return line;
}
}
} else if (os.contains("mac") || os.contains("linux")) {
Process process = Runtime.getRuntime().exec(
new String[]{"sh", "-c", "system_profiler SPSerialATADataType | grep 'Serial Number'"}
);
process.waitFor();
BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream())
);
String line;
if ((line = reader.readLine()) != null) {
return sn;
} else if (os.contains("mac")) {
String out = runCommand(List.of(
"sh", "-c", "system_profiler SPSerialATADataType | grep 'Serial Number'"));
if (out != null) {
for (String line : out.split("\\R")) {
if (line.contains(":")) {
return line.split(":", 2)[1].trim();
}
}
}
} catch (Exception e) {
// 静默失败
} else if (os.contains("linux")) {
// Linux:lsblk 输出物理磁盘序列号
return firstLine(runCommand(List.of("lsblk", "-dno", "serial")));
}
return null;
}
@@ -365,36 +294,110 @@ public class getHWID {
* 获取Android存储信息
*/
private static String getAndroidStorageInfo() {
List<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 {
// 读取 /proc/mounts 或使用系统属性
List<String> infoList = new ArrayList<>();
process = pb.start();
} catch (IOException e) {
return null;
}
// 尝试获取存储相关信息
String internalStorage = System.getProperty("user.dir", "");
if (!internalStorage.isEmpty()) {
infoList.add("InternalDir:" + internalStorage);
// 并发排空 stderr,避免进程因管道写满而阻塞
Thread stderrDrain = new Thread(() -> drain(process.getErrorStream()), "hwid-stderr-drain");
stderrDrain.setDaemon(true);
stderrDrain.start();
StringBuilder stdout = new StringBuilder();
try {
drainInto(process.getInputStream(), stdout);
if (!process.waitFor(COMMAND_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
process.destroyForcibly();
return null;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
process.destroyForcibly();
return null;
}
return stdout.toString().trim();
}
// 检查外部存储是否可用
java.io.File externalStorage = new java.io.File("/sdcard");
if (externalStorage.exists()) {
infoList.add("ExternalStorage:available");
} else {
infoList.add("ExternalStorage:not_available");
/**
* 排空输入流(丢弃内容),用于 stderr。
*/
private static void drain(InputStream in) {
try (InputStream ignored = in) {
while (in.read() != -1) {
// 丢弃
}
} catch (IOException ignored) {
// 忽略
}
}
// 获取数据目录
java.io.File dataDir = new java.io.File("/data");
if (dataDir.exists()) {
infoList.add("DataDir:exists");
/**
* 将输入流按行读取到 StringBuilder。
*/
private static void drainInto(InputStream in, StringBuilder out) {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(in, Charset.defaultCharset()))) {
String line;
while ((line = reader.readLine()) != null) {
if (out.length() > 0) {
out.append(System.lineSeparator());
}
out.append(line);
}
} catch (IOException ignored) {
// 忽略
}
}
if (!infoList.isEmpty()) {
return String.join("|", infoList);
/**
* 取输出中的第一行(trim 后);输出为空时返回 null。
*/
private static String firstLine(String text) {
if (text == null || text.isEmpty()) {
return null;
}
int idx = text.indexOf('\n');
return (idx < 0 ? text : text.substring(0, idx)).trim();
}
/**
* 取输出中第一个非空且不等于表头的行(wmic 输出首行是列名)。
*/
private static String firstNonHeaderLine(String text, String header) {
if (text == null) {
return null;
}
for (String line : text.split("\\R")) {
String trimmed = line.trim();
if (!trimmed.isEmpty() && !trimmed.equals(header)) {
return trimmed;
}
} catch (Exception e) {
// 静默失败
}
return null;
}
@@ -405,8 +408,8 @@ public class getHWID {
private static String generateHash(String input) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(input.getBytes("UTF-8"));
StringBuilder hexString = new StringBuilder();
byte[] hash = digest.digest(input.getBytes(StandardCharsets.UTF_8));
StringBuilder hexString = new StringBuilder(hash.length * 2);
for (byte b : hash) {
String hex = Integer.toHexString(0xff & b);
@@ -415,12 +418,9 @@ public class getHWID {
}
hexString.append(hex);
}
return hexString.toString();
} catch (Exception e) {
e.printStackTrace();
return input.hashCode() + "";
throw new IllegalStateException("SHA-256 摘要不可用", e);
}
}
}
@@ -3,46 +3,42 @@ package top.gtb520.java.gnu.java.utils.library.utils.processing.random;
import java.util.concurrent.ThreadLocalRandom;
public class defaults {
private static final String ALL_CHARACTERS =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
/**
* 根据输入的范围生成随机数
* 根据输入的范围生成随机数(包含两端)。
*
* @param start_scope 起始范围
* @param end_scope 结束范围
* @param end_scope 结束范围
* @return 随机数
* @throws IllegalArgumentException 当起始值大于结束值时
*/
public static int random(int start_scope, int end_scope) {
int random_double;
// 处理边界,确保起始范围小于等于结束范围
if (start_scope > end_scope) {
System.out.println("起始范围不能大于结束范围");
throw new IllegalArgumentException("起始范围不能大于结束范围");
throw new IllegalArgumentException("起始范围不能大于结束范围: " + start_scope + " > " + end_scope);
}
// 生成随机数
random_double = ThreadLocalRandom.current().nextInt(start_scope, end_scope + 1);
return random_double;
// 使用 long 上界,避免 end_scope == Integer.MAX_VALUE 时 +1 溢出为负数
return (int) ThreadLocalRandom.current().nextLong(start_scope, (long) end_scope + 1);
}
/**
* 生成指定长度的随机字符串
* 生成指定长度的随机字符串(大小写字母 + 数字)。
*
* @param length 字符串长度
* @return 随机字符串
* @throws IllegalArgumentException 当长度为负数时
*/
public static String randomString(int length) {
String alphabetsInUpperCase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
String alphabetsInLowerCase = "abcdefghijklmnopqrstuvwxyz";
String numbers = "0123456789";
// create a super set of all characters
String allCharacters = alphabetsInLowerCase + alphabetsInUpperCase + numbers;
// initialize a string to hold result
StringBuffer randomString = new StringBuffer();
// loop for 10 times
for (int i = 0; i < length; i++) {
// generate a random number between 0 and length of all characters
int randomIndex = (int)(Math.random() * allCharacters.length());
// retrieve character at index and add it to result
randomString.append(allCharacters.charAt(randomIndex));
if (length < 0) {
throw new IllegalArgumentException("字符串长度不能为负数: " + length);
}
return randomString.toString();
ThreadLocalRandom random = ThreadLocalRandom.current();
StringBuilder result = new StringBuilder(length);
for (int i = 0; i < length; i++) {
result.append(ALL_CHARACTERS.charAt(random.nextInt(ALL_CHARACTERS.length())));
}
return result.toString();
}
}
@@ -0,0 +1,30 @@
package top.gtb520.java.gnu.java.utils.library.utils.runtime;
// 运行时Java环境信息
public class JavaRunTimeEnv {
// Java版本号
public static String JavaVersion() {
return System.getProperty("java.version");
}
// Java运行时环境版本号
public static String JavaRuntimeEnvironmentVersion() {
return System.getProperty("java.runtime.version");
}
// 获取虚拟机版本
public static String JavaVMVersion() {
return System.getProperty("java.vm.version");
}
// 获取 JDK 位数,64 位通常包含 amd64 或 64
public static String JavaBits() {
return System.getProperty("sun.arch.data.model");
}
// 获取JavaVM名称
public static String JavaVMName() {
return System.getProperty("java.vm.name");
}
}
@@ -0,0 +1,75 @@
package top.gtb520.java.gnu.java.utils.library.utils.runtime;
import top.gtb520.java.gnu.java.utils.library.main;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
// 获取运行时的信息
public class RunEnv {
private static final String TEMP_DIR_PREFIX = "gjul-";
/**
* 获取当前运行的 JAR 文件路径。
* 非 JAR 运行(IDE/类路径)时返回类路径目录;解析失败时回退到当前工作目录。
*/
public static String getJarPath() {
try {
java.net.URL location = main.class.getProtectionDomain().getCodeSource().getLocation();
if (location == null) {
return Paths.get("").toAbsolutePath().toString();
}
// 通过 URI 解析路径,自动处理 %20 等 URL 编码
return Paths.get(location.toURI()).toString();
} catch (URISyntaxException | IllegalArgumentException e) {
try {
String path = main.class.getProtectionDomain().getCodeSource().getLocation().getPath();
if (path != null && !path.isEmpty()) {
return path;
}
} catch (Exception ignored) {
// 忽略,走最终回退
}
return Paths.get("").toAbsolutePath().toString();
}
}
// 获取临时目录
public static String getTempDir() {
return System.getProperty("java.io.tmpdir");
}
/**
* 在系统临时目录下新建唯一临时子目录,返回其路径。
*/
public static String newTempDir() {
return createTempDir(Path.of(getTempDir()));
}
/**
* 在当前运行的 JAR 文件所在目录下新建唯一临时子目录,返回其路径;
* JAR 路径无父目录时返回 null。
*/
public static String newJarThisTempDir() {
Path parent = Path.of(getJarPath()).getParent();
if (parent == null) {
return null;
}
return createTempDir(parent);
}
/**
* 在指定目录下创建唯一的临时子目录,返回目录路径;创建失败抛出 RuntimeException。
*/
private static String createTempDir(Path parent) {
try {
return Files.createTempDirectory(parent, TEMP_DIR_PREFIX).toString();
} catch (IOException e) {
throw new RuntimeException("在 " + parent + " 下创建临时目录失败", e);
}
}
}
@@ -0,0 +1,30 @@
package top.gtb520.java.gnu.java.utils.library;
public class version {
// 项目的版本信息,不代表实时运行环境
// 与 pom.xml 保持一致;构建 JAR 时优先读取 MANIFEST 的 Implementation-Version
public static final String VERSION = "1.7";
public static final String JAVA_VERSION = "25";
public static final String OpenJDK_VERSION = "25.0.0.0.1";
public static final String OpenJDK_Runtime_Environment = "25.0.0.0.1+36-GA";
public static final String OpenJDK_64Bit_Server_VM = "25.0.0.0.1+36-GA";
public String GetVersion() {
// 打包为 JAR 时读取 MANIFEST 中的 Implementation-Version,类路径运行(如测试)时回退到常量
Package pkg = version.class.getPackage();
String implementationVersion = (pkg == null) ? null : pkg.getImplementationVersion();
return (implementationVersion == null || implementationVersion.isEmpty()) ? VERSION : implementationVersion;
}
public String GetJavaVersion() {
return JAVA_VERSION;
}
public String GetOpenJDKVersion() {
return OpenJDK_VERSION;
}
public String GetOpenJDKRuntimeEnvironment() {
return OpenJDK_Runtime_Environment;
}
public String GetOpenJDK64BitServerVM() {
return OpenJDK_64Bit_Server_VM;
}
}
-16
View File
@@ -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());
}
}
-281
View File
@@ -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) {
}
}
}
-21
View File
@@ -1,21 +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();
}
static void ColorHexPrintlnTest() {
main.echo.ColorHexPrintln("ColorHexPrintlnTest_RED", "31");
main.echo.ColorHexPrintln("自定义橙色", "38;5;208");
main.echo.ColorHexPrintln("十六进制粉色", "#FFC0CB");
}
}
-32
View File
@@ -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 测试完成 ==========");
}
}
@@ -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));
}
}
@@ -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()));
}
}
@@ -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]);
}
}
}
@@ -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");
}
}
@@ -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));
}
}
@@ -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();
}
}
@@ -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++;
}
}
}
@@ -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);
}
}
@@ -0,0 +1,62 @@
package top.gtb520.java.gnu.java.utils.library.utils.runtime;
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 {
@Test
void getTempDirNotNull() {
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));
}
}
}
}