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;
+ }
+ }
+}