Skip to content

Commit fd8ce75

Browse files
Bound synthesized rows in v1 OpenXml queries (#1017)
* fix: bound synthesized rows in OpenXml queries * Copied adjustments from #1016 - Added check to `ReadCellAndSetColumnIndexAsync` to throw an exception when the column number exceeds the Excel column limit - Included empty row tags in the calculation of synthesized cells count --------- Co-authored-by: Michele Bastione <michele.bastione@gmail.com>
1 parent 521d848 commit fd8ce75

7 files changed

Lines changed: 172 additions & 11 deletions

File tree

‎src/MiniExcel/OpenXml/ExcelOpenXmlSheetReader.cs‎

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -84,32 +84,32 @@ public IEnumerable<IDictionary<string, object>> QueryRange(bool useHeaderRow, st
8484

8585
public IEnumerable<IDictionary<string, object>> QueryRange(bool useHeaderRow, string sheetName, int startRowIndex, int startColumnIndex, int? endRowIndex, int? endColumnIndex)
8686
{
87-
if (startRowIndex <= 0)
87+
if (startRowIndex is <= 0 or > ReferenceHelper.MaxRowNumber)
8888
{
89-
throw new ArgumentOutOfRangeException(nameof(startRowIndex), "Start row index is 1-based and must be greater than 0.");
89+
throw new ArgumentOutOfRangeException(nameof(startRowIndex), $"Start row index must be between 1 and {ReferenceHelper.MaxRowNumber}.");
9090
}
91-
if (startColumnIndex <= 0)
91+
if (startColumnIndex is <= 0 or > ReferenceHelper.MaxColumnNumber)
9292
{
93-
throw new ArgumentOutOfRangeException(nameof(startColumnIndex), "Start column index is 1-based and must be greater than 0.");
93+
throw new ArgumentOutOfRangeException(nameof(startColumnIndex), $"Start column index must be between 1 and {ReferenceHelper.MaxColumnNumber}.");
9494
}
9595
// convert to 0-based
9696
startColumnIndex--;
9797
startRowIndex--;
9898

9999
if (endRowIndex.HasValue)
100100
{
101-
if (endRowIndex.Value <= 0)
101+
if (endRowIndex.Value is <= 0 or > ReferenceHelper.MaxRowNumber)
102102
{
103-
throw new ArgumentOutOfRangeException(nameof(endRowIndex), "End row index is 1-based and must be greater than 0.");
103+
throw new ArgumentOutOfRangeException(nameof(endRowIndex), $"End row index must be between 1 and {ReferenceHelper.MaxRowNumber}.");
104104
}
105105
// convert to 0-based
106106
endRowIndex--;
107107
}
108108
if (endColumnIndex.HasValue)
109109
{
110-
if (endColumnIndex.Value <= 0)
110+
if (endColumnIndex.Value is <= 0 or > ReferenceHelper.MaxColumnNumber)
111111
{
112-
throw new ArgumentOutOfRangeException(nameof(endColumnIndex), "End column index is 1-based and must be greater than 0.");
112+
throw new ArgumentOutOfRangeException(nameof(endColumnIndex), $"End column index must be between 1 and {ReferenceHelper.MaxColumnNumber}.");
113113
}
114114
// convert to 0-based
115115
endColumnIndex--;
@@ -164,15 +164,29 @@ internal IEnumerable<IDictionary<string, object>> InternalQueryRange(bool useHea
164164
var headRows = new Dictionary<int, string>();
165165
int rowIndex = -1;
166166
bool isFirstRow = true;
167+
long synthesizedCellCount = 0;
167168
while (!reader.EOF)
168169
{
169170
if (XmlReaderHelper.IsStartElement(reader, "row", _ns))
170171
{
171172
var nextRowIndex = rowIndex + 1;
172-
if (int.TryParse(reader.GetAttribute("r"), out int arValue))
173-
rowIndex = arValue - 1; // The row attribute is 1-based
173+
var rowAttribute = reader.GetAttribute("r");
174+
if (rowAttribute != null)
175+
{
176+
if (!int.TryParse(rowAttribute, NumberStyles.None, CultureInfo.InvariantCulture, out var rowNumber)
177+
|| rowNumber is < 1 or > ReferenceHelper.MaxRowNumber)
178+
{
179+
throw new InvalidDataException($"Row index '{rowAttribute}' is outside Excel's valid range.");
180+
}
181+
182+
rowIndex = rowNumber - 1; // The row attribute is 1-based
183+
}
174184
else
185+
{
175186
rowIndex++;
187+
if (rowIndex >= ReferenceHelper.MaxRowNumber)
188+
throw new InvalidDataException("The worksheet contains more rows than Excel supports.");
189+
}
176190

177191
if (rowIndex < startRowIndex)
178192
{
@@ -188,6 +202,24 @@ internal IEnumerable<IDictionary<string, object>> InternalQueryRange(bool useHea
188202
break;
189203
}
190204

205+
if (!_config.IgnoreEmptyRows && _config.MaxSynthesizedCells.HasValue)
206+
{
207+
var expectedRowIndex = isFirstRow ? startRowIndex : nextRowIndex;
208+
var emptyRowCount = Math.Max(0, rowIndex - expectedRowIndex);
209+
var columnCount = useHeaderRow
210+
? Math.Max(1, headRows.Count)
211+
: Math.Max(1, maxColumnIndex - startColumnIndex + 1);
212+
213+
var selfClosingRowCount = reader.IsEmptyElement ? 1 : 0;
214+
synthesizedCellCount += (long)(emptyRowCount + selfClosingRowCount) * columnCount;
215+
216+
if (synthesizedCellCount > _config.MaxSynthesizedCells.Value)
217+
{
218+
throw new InvalidDataException(
219+
$"The worksheet exceeds the configured limit of {_config.MaxSynthesizedCells.Value} synthesized empty cells.");
220+
}
221+
}
222+
191223
foreach (var row in QueryRow(reader, isFirstRow, startRowIndex, nextRowIndex, rowIndex, startColumnIndex, endColumnIndex, maxColumnIndex, withoutCR, useHeaderRow, headRows, _mergeCells))
192224
{
193225
if (isFirstRow)
@@ -643,6 +675,8 @@ private CellAndColumn ReadCellAndSetColumnIndex(XmlReader reader, int columnInde
643675
//TODO:need to check only need nextColumnIndex or columnIndex
644676
else if (ReferenceHelper.ParseReference(aR, out int referenceColumn, out _))
645677
newColumnIndex = referenceColumn - 1; // ParseReference is 1-based
678+
else if (!string.IsNullOrEmpty(aR) && referenceColumn > ReferenceHelper.MaxColumnNumber)
679+
throw new InvalidDataException($"Cell reference '{aR}' is invalid.");
646680
else
647681
newColumnIndex = columnIndex;
648682

‎src/MiniExcel/OpenXml/OpenXmlConfiguration.cs‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ public class OpenXmlConfiguration : Configuration
1717
public bool WriteEmptyStringAsNull { get; set; } = false;
1818
public bool TrimColumnNames { get; set; } = true;
1919
public bool IgnoreEmptyRows { get; set; } = false;
20+
21+
/// <summary>
22+
/// Maximum number of cells that may be synthesized for implicit empty rows during a query.
23+
/// Each synthesized row counts as at least one cell. Set to <see langword="null" /> to disable the limit.
24+
/// </summary>
25+
public long? MaxSynthesizedCells { get; set; } = 100_000;
2026
public bool EnableSharedStringCache { get; set; } = true;
2127
public long SharedStringCacheSize { get; set; } = 5 * 1024 * 1024;
2228

‎src/MiniExcel/Utils/ReferenceHelper.cs‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
internal static class ReferenceHelper
44
{
5+
public const int MaxColumnNumber = 16_384;
6+
public const int MaxRowNumber = 1_048_576;
7+
58
public static string GetCellNumber(string cell)
69
{
710
return cell
@@ -70,6 +73,8 @@ public static bool ParseReference(string value, out int column, out int row)
7073
position++;
7174
column *= 26;
7275
column += c - offset;
76+
if (column > MaxColumnNumber)
77+
return false;
7378
continue;
7479
}
7580

@@ -105,6 +110,6 @@ public static bool ParseReference(string value, out int column, out int row)
105110
row = row * 10 + digit;
106111
}
107112

108-
return row > 0;
113+
return row is > 0 and <= MaxRowNumber;
109114
}
110115
}

‎tests/MiniExcelTests/MiniExcelIssueAsyncTests.cs‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,25 @@ public class MiniExcelIssueAsyncTests(ITestOutputHelper output)
1818
{
1919
private readonly ITestOutputHelper _output = output;
2020

21+
[Fact]
22+
public async Task QueryAsyncRejectsRowIndexOutsideWorksheetLimits()
23+
{
24+
using var path = AutoDeletingPath.Create();
25+
MiniExcel.SaveAs(path.ToString(), new[] { new { Value = "control" } });
26+
Helpers.ReplaceFirstSheetXml(path.ToString(), """
27+
<?xml version="1.0" encoding="utf-8"?>
28+
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
29+
<dimension ref="A1" />
30+
<sheetData>
31+
<row r="2000000000"><c r="A2000000000" t="str"><v>x</v></c></row>
32+
</sheetData>
33+
</worksheet>
34+
""");
35+
36+
var rows = await MiniExcel.QueryAsync(path.ToString());
37+
Assert.Throws<InvalidDataException>(() => rows.First());
38+
}
39+
2140
[Fact]
2241
public async Task Issue997()
2342
{

‎tests/MiniExcelTests/MiniExcelIssueTests.cs‎

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,86 @@ public class MiniExcelIssueTests(ITestOutputHelper output)
2929
{
3030
private readonly ITestOutputHelper _output = output;
3131

32+
[Theory]
33+
[InlineData("1048577")]
34+
[InlineData("2000000000")]
35+
public void QueryRejectsRowIndexOutsideWorksheetLimits(string rowNumber)
36+
{
37+
using var path = AutoDeletingPath.Create();
38+
MiniExcel.SaveAs(path.ToString(), new[] { new { Value = "control" } });
39+
Helpers.ReplaceFirstSheetXml(path.ToString(), $"""
40+
<?xml version="1.0" encoding="utf-8"?>
41+
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
42+
<dimension ref="A1" />
43+
<sheetData>
44+
<row r="{rowNumber}"><c r="A{rowNumber}" t="str"><v>x</v></c></row>
45+
</sheetData>
46+
</worksheet>
47+
""");
48+
49+
Assert.Throws<InvalidDataException>(() => MiniExcel.Query(path.ToString()).First());
50+
}
51+
52+
[Fact]
53+
public void QueryRejectsDimensionOutsideWorksheetLimits()
54+
{
55+
using var path = AutoDeletingPath.Create();
56+
MiniExcel.SaveAs(path.ToString(), new[] { new { Value = "control" } });
57+
Helpers.ReplaceFirstSheetXml(path.ToString(), """
58+
<?xml version="1.0" encoding="utf-8"?>
59+
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
60+
<dimension ref="A1:XFE1" />
61+
<sheetData><row r="1"><c r="A1" t="str"><v>x</v></c></row></sheetData>
62+
</worksheet>
63+
""");
64+
65+
Assert.Throws<InvalidDataException>(() => MiniExcel.Query(path.ToString()).First());
66+
}
67+
68+
[Fact]
69+
public void QueryRangeAcceptsMaximumWorksheetCoordinates()
70+
{
71+
using var path = AutoDeletingPath.Create();
72+
MiniExcel.SaveAs(path.ToString(), new[] { new { Value = "control" } });
73+
Helpers.ReplaceFirstSheetXml(path.ToString(), """
74+
<?xml version="1.0" encoding="utf-8"?>
75+
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
76+
<dimension ref="XFD1048576" />
77+
<sheetData>
78+
<row r="1048576"><c r="XFD1048576" t="str"><v>x</v></c></row>
79+
</sheetData>
80+
</worksheet>
81+
""");
82+
83+
var row = MiniExcel.QueryRange(
84+
path.ToString(),
85+
startRowIndex: ReferenceHelper.MaxRowNumber,
86+
startColumnIndex: ReferenceHelper.MaxColumnNumber,
87+
endRowIndex: ReferenceHelper.MaxRowNumber,
88+
endColumnIndex: ReferenceHelper.MaxColumnNumber).Single();
89+
90+
Assert.Equal("x", row.XFD);
91+
}
92+
93+
[Fact]
94+
public void QueryRejectsWorksheetExceedingSynthesizedCellLimit()
95+
{
96+
using var path = AutoDeletingPath.Create();
97+
MiniExcel.SaveAs(path.ToString(), new[] { new { Value = "control" } });
98+
Helpers.ReplaceFirstSheetXml(path.ToString(), """
99+
<?xml version="1.0" encoding="utf-8"?>
100+
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
101+
<dimension ref="A1:XFD1048576" />
102+
<sheetData>
103+
<row r="1048576"><c r="XFD1048576" t="str"><v>x</v></c></row>
104+
</sheetData>
105+
</worksheet>
106+
""");
107+
108+
var exception = Assert.Throws<InvalidDataException>(() => MiniExcel.Query(path.ToString()).First());
109+
Assert.Contains("synthesized empty cells", exception.Message);
110+
}
111+
32112
/// <summary>
33113
/// https://github.com/mini-software/MiniExcel/issues/549
34114
/// </summary>

‎tests/MiniExcelTests/MiniExcelOpenXmlTests.cs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,8 @@ public void ParseValidCellReference(string reference, int expectedColumn, int ex
166166
[InlineData("A0")]
167167
[InlineData("A1x")]
168168
[InlineData("A2147483648")]
169+
[InlineData("A1048577")]
170+
[InlineData("XFE1")]
169171
public void RejectInvalidCellReference(string reference)
170172
{
171173
Assert.False(MiniExcelLibs.Utils.ReferenceHelper.ParseReference(reference, out _, out _));

‎tests/MiniExcelTests/Utils/Helpers.cs‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,21 @@ internal static string GetZipFileContent(string zipPath, string filePath)
7676
return doc.ToString();
7777
}
7878

79+
internal static void ReplaceFirstSheetXml(string path, string worksheetXml)
80+
{
81+
using var stream = File.Open(path, FileMode.Open, FileAccess.ReadWrite);
82+
using var archive = new ZipArchive(stream, ZipArchiveMode.Update, false, Encoding.UTF8);
83+
var sheet = archive.Entries.Single(w =>
84+
w.FullName.StartsWith("xl/worksheets/sheet1", StringComparison.OrdinalIgnoreCase) ||
85+
w.FullName.StartsWith("/xl/worksheets/sheet1", StringComparison.OrdinalIgnoreCase));
86+
var entryName = sheet.FullName;
87+
sheet.Delete();
88+
89+
var replacement = archive.CreateEntry(entryName);
90+
using var writer = new StreamWriter(replacement.Open(), new UTF8Encoding(false));
91+
writer.Write(worksheetXml);
92+
}
93+
7994
internal static string? GetFirstSheetDimensionRefValue(string path)
8095
{
8196
var ns = new XmlNamespaceManager(new NameTable());

0 commit comments

Comments
 (0)