fix: 拦截非标准 XLSX 文件

- 在 POI 解析前识别旧版 XLS 与异常容器

- 返回可操作提示并补充回归测试
This commit is contained in:
2026-09-02 19:15:00 +08:00
parent 93e0ff2204
commit 4bc68ec7f7
2 changed files with 87 additions and 0 deletions

View File

@@ -14,6 +14,7 @@ import com.easyagents.document.core.entity.ParseRequest;
import com.easyagents.document.core.entity.ParseResponse;
import com.easyagents.document.core.entity.ParseResult;
import com.easyagents.document.core.entity.XlsxParseRequest;
import com.easyagents.document.core.exception.DocumentParseException;
import com.easyagents.document.core.support.AbstractAsyncDocumentParseService;
import com.easyagents.document.xlsx.XlsxDocumentProvider;
import com.easyagents.document.xlsx.model.XlsxCellArtifact;
@@ -52,6 +53,10 @@ import java.util.concurrent.Executors;
public class MineruXlsxDocumentParseService extends AbstractAsyncDocumentParseService<XlsxParseRequest> implements XlsxDocumentProvider {
public static final String PROVIDER_NAME = "mineru";
private static final byte[] OLE2_SIGNATURE = new byte[] {
(byte) 0xd0, (byte) 0xcf, 0x11, (byte) 0xe0,
(byte) 0xa1, (byte) 0xb1, 0x1a, (byte) 0xe1
};
private final MineruProperties properties;
private final MineruClient client;
@@ -145,6 +150,9 @@ public class MineruXlsxDocumentParseService extends AbstractAsyncDocumentParseSe
@Override
protected ParseResponse doParse(XlsxParseRequest request, DocumentAsyncTaskUpdater updater) {
for (ParseFile file : request.getFiles()) {
validateXlsxContent(file);
}
ParseResponse response = new ParseResponse();
List<ParseResult> results = new ArrayList<ParseResult>();
String backend = null;
@@ -211,6 +219,50 @@ public class MineruXlsxDocumentParseService extends AbstractAsyncDocumentParseSe
return aggregate;
}
/**
* 在 POI 打开工作簿前校验 XLSX 容器签名,避免向用户暴露底层格式异常。
*
* @param file 待解析文件
*/
private void validateXlsxContent(ParseFile file) {
byte[] content = file == null ? null : file.getContent();
if (hasZipSignature(content)) {
return;
}
String fileName = file == null || !StringUtil.hasText(file.getFileName())
? "当前文件"
: "文件“" + file.getFileName() + "";
String reason = startsWith(content, OLE2_SIGNATURE)
? "可能是旧版 XLS 或已加密文件"
: "文件内容与 .xlsx 扩展名不一致或文件已损坏";
throw new DocumentParseException(
fileName + "不是标准 XLSX" + reason
+ "。请解除保护后用 Excel/WPS 另存为 XLSX修改文件后缀无效"
);
}
private boolean hasZipSignature(byte[] content) {
return content != null
&& content.length >= 4
&& content[0] == 'P'
&& content[1] == 'K'
&& ((content[2] == 3 && content[3] == 4)
|| (content[2] == 5 && content[3] == 6)
|| (content[2] == 7 && content[3] == 8));
}
private boolean startsWith(byte[] content, byte[] signature) {
if (content == null || content.length < signature.length) {
return false;
}
for (int index = 0; index < signature.length; index++) {
if (content[index] != signature[index]) {
return false;
}
}
return true;
}
private SheetExtraction extractSheet(XSSFSheet sheet,
int sheetIndex,
DataFormatter formatter,

View File

@@ -16,6 +16,7 @@ import com.easyagents.document.core.entity.ParseTaskStatus;
import com.easyagents.document.core.entity.XlsxParseRequest;
import com.easyagents.document.core.exception.DocumentParseException;
import com.easyagents.document.xlsx.model.XlsxParseArtifact;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.ClientAnchor;
import org.apache.poi.xssf.usermodel.XSSFDrawing;
import org.apache.poi.xssf.usermodel.XSSFSheet;
@@ -119,6 +120,31 @@ public class MineruXlsxDocumentParseServiceTest {
Assert.assertEquals("image/jpeg", result.getImages().get(0).getMimeType());
}
@Test
public void shouldRejectLegacyXlsContentWithActionableMessage() throws Exception {
RecordingClient client = new RecordingClient(defaultProperties());
MineruMapper mapper = new MineruMapper(defaultProperties());
MineruXlsxDocumentParseService service = new MineruXlsxDocumentParseService(
defaultProperties(),
client,
mapper,
new DocumentAsyncTaskManager(new InMemoryDocumentAsyncTaskRepository(), directExecutor())
);
XlsxParseRequest request = new XlsxParseRequest();
request.addFile(ParseFile.of("legacy.xlsx", buildLegacyWorkbookBytes()));
DocumentParseException error = Assert.assertThrows(
DocumentParseException.class,
() -> service.parse(request)
);
Assert.assertEquals(
"文件“legacy.xlsx”不是标准 XLSX可能是旧版 XLS 或已加密文件。"
+ "请解除保护后用 Excel/WPS 另存为 XLSX修改文件后缀无效",
error.getMessage()
);
}
@Test
public void shouldAppendImageReferenceForImageOnlySheet() throws Exception {
RecordingClient client = new RecordingClient(defaultProperties());
@@ -255,6 +281,15 @@ public class MineruXlsxDocumentParseServiceTest {
return writeWorkbook(workbook);
}
private byte[] buildLegacyWorkbookBytes() throws Exception {
try (HSSFWorkbook workbook = new HSSFWorkbook();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
workbook.createSheet("Sheet1").createRow(0).createCell(0).setCellValue("旧版表格");
workbook.write(outputStream);
return outputStream.toByteArray();
}
}
private void addPicture(XSSFWorkbook workbook,
XSSFSheet sheet,
int rowIndex,