Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
import org.apache.fesod.sheet.util.NumberUtils;
import org.apache.fesod.sheet.util.WorkBookUtil;
import org.apache.fesod.sheet.util.WriteHandlerUtils;
import org.apache.fesod.sheet.util.XlsxEscapeUtils;
import org.apache.fesod.sheet.write.handler.context.CellWriteHandlerContext;
import org.apache.fesod.sheet.write.handler.context.RowWriteHandlerContext;
import org.apache.fesod.sheet.write.handler.context.SheetWriteHandlerContext;
Expand Down Expand Up @@ -379,10 +380,19 @@ private void addOneRowOfHeadDataToExcel(

WriteCellData<String> writeCellData =
new WriteCellData<>(head.getHeadNameList().get(relativeRowIndex));
cell.setCellValue(writeCellData.getStringValue());
// Head strings are stored outside the converter flow, so the default xlsx hex escaping that
// data cells receive through EscapeHexCellWriteHandler is applied here directly. Emitted as
// static escaping instead of a handler event to keep the documented CellWriteHandler call
// matrix unchanged.
String headString = writeCellData.getStringValue();
if (writeWorkbookHolder().getExcelType() == ExcelTypeEnum.XLSX) {
headString = XlsxEscapeUtils.escape(headString);
writeCellData.setStringValue(headString);
}
cellWriteHandlerContext.setCellDataList(ListUtils.newArrayList(writeCellData));
cellWriteHandlerContext.setFirstCellData(writeCellData);

cell.setCellValue(writeCellData.getStringValue());
WriteHandlerUtils.afterCellDispose(cellWriteHandlerContext);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,86 @@ public class XlsxEscapeUtils {

private XlsxEscapeUtils() {}

// ASCII hex digits only. Not Character.digit(c, 16), which also accepts non-ASCII
// digits such as U+0663 that OOXML never uses.
private static final boolean[] HEX_TABLE = new boolean[128];

private static final String ESCAPE_PREFIX = "_x";
private static final String ESCAPED_PREFIX = "_x005F_x";
private static final int PREFIX_LENGTH = ESCAPE_PREFIX.length();
private static final int HEX_DIGIT_COUNT = 4;

static {
for (char c = '0'; c <= '9'; c++) HEX_TABLE[c] = true;
for (char c = 'A'; c <= 'F'; c++) HEX_TABLE[c] = true;
for (char c = 'a'; c <= 'f'; c++) HEX_TABLE[c] = true;
}

/**
* Replaces every {@code _xHHHH_} sequence with {@code _x005F_xHHHH_} so a reader decodes the literal back
* instead of the character the sequence names. The write half of {@link #utfDecode(String)}.
*
* @param originalString the string to escape
* @return the escaped string, or the input unchanged when it holds no escape sequence
*/
public static String escape(String originalString) {
if (originalString == null || !originalString.contains(ESCAPE_PREFIX)) {
return originalString;
}
int length = originalString.length();

// Lazily allocated: stays null (no allocation) when no valid pattern is found
StringBuilder result = null;
int lastEnd = 0;
int searchStart = 0;
int patternIndex;
while ((patternIndex = originalString.indexOf(ESCAPE_PREFIX, searchStart)) != -1) {
int hexStart = patternIndex + PREFIX_LENGTH;
int suffixIndex = hexStart + HEX_DIGIT_COUNT;
int patternEnd = suffixIndex + 1;
// Too few characters left for a full pattern, and any later match has even fewer
if (patternEnd > length) {
break;
}

if (originalString.charAt(suffixIndex) == '_' && isHexDigits(originalString, hexStart)) {
if (result == null) {
result = new StringBuilder(length + 64);
}
// Append content since the previous match, then the escaped pattern
result.append(originalString, lastEnd, patternIndex);
result.append(ESCAPED_PREFIX);
result.append(originalString, hexStart, suffixIndex);
result.append('_');
lastEnd = patternEnd;
searchStart = patternEnd;
} else {
searchStart = hexStart;
}
}

// No valid patterns found
if (result == null) {
return originalString;
}

result.append(originalString, lastEnd, length);
return result.toString();
}

/**
* Checks whether the four characters starting at {@code startIndex} are all ASCII hex digits.
*/
private static boolean isHexDigits(String str, int startIndex) {
for (int i = 0; i < HEX_DIGIT_COUNT; i++) {
char c = str.charAt(startIndex + i);
if (c >= 128 || !HEX_TABLE[c]) {
return false;
}
}
return true;
}

/**
* from poi XSSFRichTextString
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,13 @@
import org.apache.fesod.sheet.exception.ExcelGenerateException;
import org.apache.fesod.sheet.metadata.data.WriteCellData;
import org.apache.fesod.sheet.metadata.property.ExcelContentProperty;
import org.apache.fesod.sheet.support.ExcelTypeEnum;
import org.apache.fesod.sheet.util.BeanMapUtils;
import org.apache.fesod.sheet.util.ClassUtils;
import org.apache.fesod.sheet.util.FieldUtils;
import org.apache.fesod.sheet.util.PoiUtils;
import org.apache.fesod.sheet.util.WriteHandlerUtils;
import org.apache.fesod.sheet.util.XlsxEscapeUtils;
import org.apache.fesod.sheet.write.handler.context.CellWriteHandlerContext;
import org.apache.fesod.sheet.write.handler.context.RowWriteHandlerContext;
import org.apache.fesod.sheet.write.metadata.fill.AnalysisCell;
Expand Down Expand Up @@ -317,7 +319,13 @@ private void doFill(
}
}
cellValueBuild.append(analysisCell.getPrepareDataList().get(index));
cell.setCellValue(cellValueBuild.toString());
String joined = cellValueBuild.toString();
if (writeContext.writeWorkbookHolder().getExcelType() == ExcelTypeEnum.XLSX) {
// The variable values above never passed through afterCellDataConverted, so the
// joined literal still needs the default xlsx hex escaping before it is stored.
joined = XlsxEscapeUtils.escape(joined);
}
cell.setCellValue(joined);
cellWriteHandlerContext.setCellDataList(cellDataList);
if (CollectionUtils.isNotEmpty(cellDataList)) {
cellWriteHandlerContext.setFirstCellData(cellDataList.get(0));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import org.apache.fesod.sheet.event.NotRepeatExecutor;
import org.apache.fesod.sheet.metadata.Head;
import org.apache.fesod.sheet.metadata.data.WriteCellData;
import org.apache.fesod.sheet.util.XlsxEscapeUtils;
import org.apache.fesod.sheet.write.handler.CellWriteHandler;
import org.apache.fesod.sheet.write.metadata.holder.WriteSheetHolder;
import org.apache.fesod.sheet.write.metadata.holder.WriteTableHolder;
Expand Down Expand Up @@ -51,21 +52,6 @@
*/
public class EscapeHexCellWriteHandler implements CellWriteHandler, NotRepeatExecutor {

// ASCII hex digits only. Not Character.digit(c, 16), which also accepts non-ASCII
// digits such as U+0663 that OOXML never uses.
private static final boolean[] HEX_TABLE = new boolean[128];

static {
for (char c = '0'; c <= '9'; c++) HEX_TABLE[c] = true;
for (char c = 'A'; c <= 'F'; c++) HEX_TABLE[c] = true;
for (char c = 'a'; c <= 'f'; c++) HEX_TABLE[c] = true;
}

private static final String PREFIX = "_x";
private static final String ESCAPED_PREFIX = "_x005F" + PREFIX;
private static final int PREFIX_LENGTH = PREFIX.length();
private static final int HEX_DIGIT_COUNT = 4;

/**
* Returns the fully qualified class name as the unique identity of this handler.
* <p>
Expand All @@ -90,67 +76,9 @@ public void afterCellDataConverted(
if (cellData != null && cell instanceof SXSSFCell && CellDataTypeEnum.STRING.equals(cellData.getType())) {
String originalString = cellData.getStringValue();
if (originalString != null) {
String escapedString = escapeHex(originalString);
String escapedString = XlsxEscapeUtils.escape(originalString);
cellData.setStringValue(escapedString);
}
}
}

/**
* Replaces every _xHHHH_ sequence with _x005F_xHHHH_ to prevent POI from decoding them.
*/
private String escapeHex(String originalString) {
int length = originalString.length();

// Lazily allocated: stays null (no allocation) when no valid pattern is found
StringBuilder result = null;
int lastEnd = 0;
int searchStart = 0;
int patternIndex;
while ((patternIndex = originalString.indexOf(PREFIX, searchStart)) != -1) {
int hexStart = patternIndex + PREFIX_LENGTH;
int suffixIndex = hexStart + HEX_DIGIT_COUNT;
int patternEnd = suffixIndex + 1;
// Too few characters left for a full pattern, and any later match has even fewer
if (patternEnd > length) {
break;
}

if (originalString.charAt(suffixIndex) == '_' && isHexDigits(originalString, hexStart)) {
if (result == null) {
result = new StringBuilder(length + 64);
}
// Append content since the previous match, then the escaped pattern
result.append(originalString, lastEnd, patternIndex);
result.append(ESCAPED_PREFIX);
result.append(originalString, hexStart, suffixIndex);
result.append('_');
lastEnd = patternEnd;
searchStart = patternEnd;
} else {
searchStart = hexStart;
}
}

// No valid patterns found
if (result == null) {
return originalString;
}

result.append(originalString, lastEnd, length);
return result.toString();
}

/**
* Checks whether the four characters starting at {@code startIndex} are all ASCII hex digits.
*/
private static boolean isHexDigits(String str, int startIndex) {
for (int i = 0; i < HEX_DIGIT_COUNT; i++) {
char c = str.charAt(startIndex + i);
if (c >= 128 || !HEX_TABLE[c]) {
return false;
}
}
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package org.apache.fesod.sheet.write.handler;

import java.io.File;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.fesod.sheet.FesodSheet;
import org.apache.fesod.sheet.support.ExcelTypeEnum;
import org.apache.fesod.sheet.testkit.Tags;
import org.apache.fesod.sheet.testkit.base.AbstractExcelTest;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.WorkbookFactory;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;

/**
* The default XLSX write handler set includes {@code EscapeHexCellWriteHandler}, so literal {@code _xHHHH_}
* sequences survive a round trip wherever text reaches a cell: data, head and multi-variable fill cells alike.
* POI's reader decodes {@code _xHHHH_} to the character it names, so an unescaped literal comes back corrupted.
*/
@Tag(Tags.ROUND_TRIP)
class EscapeHexHeadAndFillWriteTest extends AbstractExcelTest {

@Test
void headCellRoundTripsLiteralEscapeSequences() throws Exception {
File file = createTempFile("escape-hex-head", org.apache.fesod.sheet.testkit.enums.ExcelFormat.XLSX);

List<List<String>> head = new ArrayList<>();
head.add(Collections.singletonList("_x0041_ col"));
List<List<Object>> data = new ArrayList<>();
data.add(Collections.singletonList("v"));
FesodSheet.write(file).excelType(ExcelTypeEnum.XLSX).head(head).sheet().doWrite(data);

Assertions.assertEquals("_x0041_ col", readCellValue(file, 0, 0));
}

@Test
void fillMultiVariableCellRoundTripsLiteralEscapeSequences() throws Exception {
File template = createTemplateWithPlaceholder();
File file = createTempFile("escape-hex-fill", org.apache.fesod.sheet.testkit.enums.ExcelFormat.XLSX);

Map<String, Object> map = new HashMap<>();
map.put("a", "_x0041_ x");
FesodSheet.write(file)
.excelType(ExcelTypeEnum.XLSX)
.withTemplate(template)
.sheet()
.doFill(map);

Assertions.assertEquals("p _x0041_ x s", readCellValue(file, 0, 0));
}

private File createTemplateWithPlaceholder() throws Exception {
File template =
createTempFile("escape-hex-fill-template", org.apache.fesod.sheet.testkit.enums.ExcelFormat.XLSX);
try (Workbook workbook = new XSSFWorkbook()) {
workbook.createSheet("S").createRow(0).createCell(0).setCellValue("p {a} s");
try (FileOutputStream out = new FileOutputStream(template)) {
workbook.write(out);
}
}
return template;
}

private String readCellValue(File file, int rowIdx, int colIdx) throws Exception {
try (Workbook workbook = WorkbookFactory.create(file)) {
return workbook.getSheetAt(0).getRow(rowIdx).getCell(colIdx).getStringCellValue();
}
}
}
Loading