diff --git a/server/pom.xml b/server/pom.xml index 0ab813349..574ac04e5 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -154,6 +154,15 @@ sevenzipjbinding-all-platforms ${sevenzipjbinding.version} + + + net.sf.sevenzipjbinding + sevenzipjbinding-linux-arm64 + ${sevenzipjbinding.version} + com.github.junrar junrar diff --git a/server/src/main/java/cn/keking/service/CompressFileReader.java b/server/src/main/java/cn/keking/service/CompressFileReader.java index c98ac72e7..112ff08b3 100644 --- a/server/src/main/java/cn/keking/service/CompressFileReader.java +++ b/server/src/main/java/cn/keking/service/CompressFileReader.java @@ -4,6 +4,7 @@ import cn.keking.model.FileAttribute; import cn.keking.model.FileType; import cn.keking.utils.RarUtils; +import cn.keking.utils.SevenZipNativePlatform; import cn.keking.web.filter.BaseUrlFilter; import net.sf.sevenzipjbinding.ExtractOperationResult; import net.sf.sevenzipjbinding.IInArchive; @@ -34,6 +35,11 @@ */ @Component public class CompressFileReader { + static { + // Linux arm64 上需要显式指定 SevenZipJBinding 的平台名,其它平台不做任何处理 + SevenZipNativePlatform.init(); + } + private final FileHandlerService fileHandlerService; public CompressFileReader(FileHandlerService fileHandlerService) { diff --git a/server/src/main/java/cn/keking/utils/SevenZipNativePlatform.java b/server/src/main/java/cn/keking/utils/SevenZipNativePlatform.java new file mode 100644 index 000000000..e9b4a1921 --- /dev/null +++ b/server/src/main/java/cn/keking/utils/SevenZipNativePlatform.java @@ -0,0 +1,61 @@ +package cn.keking.utils; + +import net.sf.sevenzipjbinding.SevenZip; +import net.sf.sevenzipjbinding.SevenZipNativeInitializationException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 在 Linux arm64(aarch64)上为 SevenZipJBinding 指定原生库所在的平台名。 + * + * SevenZipJBinding 自动选择平台时,用 os.name 的第一个单词 + "-" + os.arch 作为平台名, + * 在 aarch64 的 Linux 上得到的是 Linux-aarch64,而 arm64 原生库所在的目录是 Linux-arm64, + * 两者不一致,即使原生库已在类路径上也会提示 Can't find suited platform,因此需要显式指定。 + * 其它平台不做任何处理,仍由 SevenZipJBinding 自动选择。 + */ +public final class SevenZipNativePlatform { + + private static final Logger logger = LoggerFactory.getLogger(SevenZipNativePlatform.class); + + /** + * sevenzipjbinding-linux-arm64 中原生库所在的目录名,也是初始化时使用的平台名 + */ + static final String LINUX_ARM64 = "Linux-arm64"; + + private SevenZipNativePlatform() { + } + + /** + * 获取需要显式指定的平台名 + * + * @param osName 系统属性 os.name + * @param osArch 系统属性 os.arch + * @return 平台名,返回 null 表示不指定,由 SevenZipJBinding 自动选择 + */ + static String explicitPlatform(String osName, String osArch) { + if (osName == null || osArch == null) { + return null; + } + // 与 SevenZipJBinding 一致,只取 os.name 的第一个单词 + if ("Linux".equals(osName.split(" ")[0]) && "aarch64".equals(osArch)) { + return LINUX_ARM64; + } + return null; + } + + /** + * 在首次使用 SevenZip 之前调用。初始化失败只记录日志,不影响应用启动 + */ + public static void init() { + String platform = explicitPlatform(System.getProperty("os.name"), System.getProperty("os.arch")); + if (platform == null) { + return; + } + try { + SevenZip.initSevenZipFromPlatformJAR(platform); + logger.info("SevenZipJBinding 原生库加载成功,平台: {}", platform); + } catch (SevenZipNativeInitializationException | RuntimeException e) { + logger.error("SevenZipJBinding 原生库加载失败,平台: {},压缩文件将无法预览", platform, e); + } + } +} diff --git a/server/src/test/java/cn/keking/utils/SevenZipNativePlatformTests.java b/server/src/test/java/cn/keking/utils/SevenZipNativePlatformTests.java new file mode 100644 index 000000000..ac10605c5 --- /dev/null +++ b/server/src/test/java/cn/keking/utils/SevenZipNativePlatformTests.java @@ -0,0 +1,101 @@ +package cn.keking.utils; + +import net.sf.sevenzipjbinding.SevenZip; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.Properties; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Linux arm64(aarch64)压缩包预览回归测试。 + * + * 背景:sevenzipjbinding-all-platforms 不包含 Linux arm64 的原生库,并且 SevenZipJBinding 按 + * os.name + "-" + os.arch 拼出的平台名是 Linux-aarch64,与原生库所在目录 Linux-arm64 不一致, + * 导致 aarch64 的 Linux 上所有压缩包都无法预览。 + * + * 运行测试的机器通常不是 arm64,无法真正加载该原生库,这里校验加载之前的各个环节: + * 原生库在类路径上且是 AArch64 的 ELF 文件;Linux + aarch64 时指定的平台名与原生库所在目录一致; + * 其它平台不指定平台名,仍由 SevenZipJBinding 自动选择。 + */ +class SevenZipNativePlatformTests { + + private static final int ELF_MACHINE_X86_64 = 62; + private static final int ELF_MACHINE_AARCH64 = 183; + + @Test + void shouldShipLinuxArm64NativeLibrary() throws Exception { + assertEquals(ELF_MACHINE_AARCH64, elfMachineOfNativeLibrary("Linux-arm64")); + } + + @Test + void shouldSelectLinuxArm64PlatformOnLinuxAarch64() throws Exception { + String platform = SevenZipNativePlatform.explicitPlatform("Linux", "aarch64"); + + assertEquals("Linux-arm64", platform); + // 指定的平台名必须能定位到 AArch64 的原生库 + assertEquals(ELF_MACHINE_AARCH64, elfMachineOfNativeLibrary(platform)); + } + + @Test + void shouldLeaveOtherPlatformsToAutomaticSelection() { + assertNull(SevenZipNativePlatform.explicitPlatform("Linux", "amd64")); + assertNull(SevenZipNativePlatform.explicitPlatform("Linux", "i386")); + assertNull(SevenZipNativePlatform.explicitPlatform("Linux", "arm")); + assertNull(SevenZipNativePlatform.explicitPlatform("Windows 11", "amd64")); + assertNull(SevenZipNativePlatform.explicitPlatform("Windows Server 2022", "aarch64")); + assertNull(SevenZipNativePlatform.explicitPlatform("Mac OS X", "aarch64")); + assertNull(SevenZipNativePlatform.explicitPlatform("Mac OS X", "x86_64")); + assertNull(SevenZipNativePlatform.explicitPlatform(null, null)); + } + + /** + * 对照:原有平台仍在 SevenZipJBinding 读取到的平台列表中,原生库也仍能取到。 + * 如果 arm64 的 jar 在类路径上排到 all-platforms 之前,平台列表会只剩 Linux-arm64 一项, + * 所有平台都会去加载 arm64 的原生库。 + */ + @Test + void shouldKeepExistingPlatformsAvailable() throws Exception { + List platforms = SevenZip.getPlatformList(); + + assertTrue(platforms.contains("Windows-amd64"), () -> "listed platforms: " + platforms); + assertTrue(platforms.contains("Linux-amd64"), () -> "listed platforms: " + platforms); + assertNotNull(nativeLibraryHeader("Windows-amd64")); + assertEquals(ELF_MACHINE_X86_64, elfMachineOfNativeLibrary("Linux-amd64")); + } + + private static int elfMachineOfNativeLibrary(String platform) throws IOException { + byte[] header = nativeLibraryHeader(platform); + assertTrue(header[0] == 0x7f && header[1] == 'E' && header[2] == 'L' && header[3] == 'F', + () -> platform + " native library is not an ELF file"); + return (header[18] & 0xff) | ((header[19] & 0xff) << 8); + } + + /** + * 与 SevenZipJBinding 定位原生库的方式一致:先读取 /平台名/sevenzipjbinding-lib.properties, + * 再按其中的 lib.1.name 读取文件。 + */ + private static byte[] nativeLibraryHeader(String platform) throws IOException { + assertNotNull(platform, "no platform name"); + String directory = "/" + platform + "/"; + Properties properties = new Properties(); + try (InputStream inputStream = SevenZip.class.getResourceAsStream(directory + "sevenzipjbinding-lib.properties")) { + assertNotNull(inputStream, () -> directory + "sevenzipjbinding-lib.properties is not on the classpath"); + properties.load(inputStream); + } + String libraryName = properties.getProperty("lib.1.name"); + assertNotNull(libraryName, () -> directory + "sevenzipjbinding-lib.properties has no lib.1.name"); + try (InputStream inputStream = SevenZip.class.getResourceAsStream(directory + libraryName)) { + assertNotNull(inputStream, () -> directory + libraryName + " is not on the classpath"); + byte[] header = inputStream.readNBytes(20); + assertEquals(20, header.length, () -> directory + libraryName + " is shorter than an ELF header"); + return header; + } + } +}