Skip to content
Merged
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
10 changes: 7 additions & 3 deletions src/MiniExcel.Core/Helpers/CellReferenceConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ namespace MiniExcelLib.Core.Helpers;
public static class CellReferenceConverter
{
private const int GeneralColumnIndex = 255;
private const int MaxColumnIndex = 16383;
public const int MaxColumnNumber = 16_384;
public const int MaxRowNumber = 1_048_576;

private static readonly ConcurrentDictionary<int, string> IntMappingToAlphabet = new();
private static readonly ConcurrentDictionary<string, int> AlphabetMappingToInt = new();
Expand Down Expand Up @@ -35,7 +36,7 @@ private static void EnsureMappingsUpTo(int columnIndex)
if (columnIndex < IntMappingToAlphabet.Count)
return;

if (columnIndex > MaxColumnIndex)
if (columnIndex > MaxColumnNumber - 1)
throw new InvalidDataException($"Column index {columnIndex} exceeds Excel's maximum valid index.");

for (int i = IntMappingToAlphabet.Count; i <= columnIndex; i++)
Expand Down Expand Up @@ -92,6 +93,8 @@ public static bool TryParseCellReference(string? value, out int column, out int
{
position++;
column = column * 26 + c - offset;
if (column > MaxColumnNumber)
return false;
Comment thread
michelebastione marked this conversation as resolved.
continue;
}

Expand All @@ -112,6 +115,7 @@ public static bool TryParseCellReference(string? value, out int column, out int
if (position == 0)
return false;

return int.TryParse(value[position..], NumberStyles.None, CultureInfo.InvariantCulture, out row) && row > 0;
return int.TryParse(value[position..], NumberStyles.None, CultureInfo.InvariantCulture, out row)
&& row is > 0 and <= MaxRowNumber;
}
}
6 changes: 6 additions & 0 deletions src/MiniExcel.OpenXml/OpenXmlConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ public class OpenXmlConfiguration : MiniExcelBaseConfiguration
public bool WriteEmptyStringAsNull { get; set; } = false;
public bool TrimColumnNames { get; set; } = true;
public bool IgnoreEmptyRows { get; set; } = false;

/// <summary>
/// Maximum number of cells that may be synthesized for implicit empty rows during a query.
/// Each synthesized row counts as at least one cell. Set to <see langword="null" /> to disable the limit.
/// </summary>
public long? MaxSynthesizedCells { get; set; } = 100_000;

public StringStorageMode StringStorageMode { get; set; } = StringStorageMode.Inline;
public bool EnableSharedStringCache { get; set; } = true;
Expand Down
63 changes: 46 additions & 17 deletions src/MiniExcel.OpenXml/Reader/OpenXmlReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -126,34 +126,32 @@ internal static async Task<OpenXmlReader> CreateAsync(Stream stream, IMiniExcelC
{
cancellationToken.ThrowIfCancellationRequested();

if (startRowIndex <= 0)
throw new ArgumentOutOfRangeException(nameof(startRowIndex), "Start row index is 1-based and must be greater than 0.");
if (startColumnIndex <= 0)
throw new ArgumentOutOfRangeException(nameof(startColumnIndex), "Start column index is 1-based and must be greater than 0.");
if (startRowIndex is <= 0 or > CellReferenceConverter.MaxRowNumber)
throw new ArgumentOutOfRangeException(nameof(startRowIndex), $"Start row index must be between 1 and {CellReferenceConverter.MaxRowNumber}.");

if (startColumnIndex is <= 0 or > CellReferenceConverter.MaxColumnNumber)
throw new ArgumentOutOfRangeException(nameof(startColumnIndex), $"Start column index must be between 1 and {CellReferenceConverter.MaxColumnNumber}.");

// convert to 0-based
startColumnIndex--;
startRowIndex--;

if (endRowIndex.HasValue)
{
if (endRowIndex.Value <= 0)
throw new ArgumentOutOfRangeException(nameof(endRowIndex), "End row index is 1-based and must be greater than 0.");
if (endRowIndex.Value is <= 0 or > CellReferenceConverter.MaxRowNumber)
throw new ArgumentOutOfRangeException(nameof(endRowIndex), $"End row index must be between 1 and {CellReferenceConverter.MaxRowNumber}.");

// convert to 0-based
endRowIndex--;
}

if (endColumnIndex.HasValue)
{
if (endColumnIndex.Value > 0)
{
// convert to 0-based
endColumnIndex--;
}
else
{
throw new ArgumentOutOfRangeException(nameof(endColumnIndex), "End column index is 1-based and must be greater than 0.");
}
if (endColumnIndex.Value is <= 0 or > CellReferenceConverter.MaxColumnNumber)
throw new ArgumentOutOfRangeException(nameof(endColumnIndex), $"End column index must be between 1 and {CellReferenceConverter.MaxColumnNumber}.");

// convert to 0-based
endColumnIndex--;
}

return InternalQueryRangeAsync(hasHeaderRow, sheetName, startRowIndex, startColumnIndex, endRowIndex, endColumnIndex, cancellationToken);
Expand Down Expand Up @@ -222,16 +220,29 @@ internal static async Task<OpenXmlReader> CreateAsync(Stream stream, IMiniExcelC

int rowIndex = -1;
bool isFirstRow = true;
long synthesizedCellCount = 0;
var headRows = new Dictionary<int, string>();
while (!reader.EOF)
{
if (reader.IsStartElement("row", Ns))
{
var nextRowIndex = rowIndex + 1;
if (int.TryParse(reader.GetAttribute("r"), out int arValue))
rowIndex = arValue - 1; // The row attribute is 1-based
if (reader.GetAttribute("r") is { } rowAttribute)
{
if (!int.TryParse(rowAttribute, NumberStyles.None, CultureInfo.InvariantCulture, out var rowNumber) ||
rowNumber is < 1 or > CellReferenceConverter.MaxRowNumber)
{
throw new InvalidDataException($"Row index '{rowAttribute}' is outside Excel's valid range.");
}

rowIndex = rowNumber - 1; // The row attribute is 1-based
}
else
{
rowIndex++;
if (rowIndex >= CellReferenceConverter.MaxRowNumber)
throw new InvalidDataException("The worksheet contains more rows than Excel supports.");
}

if (rowIndex < startRowIndex)
{
Expand All @@ -247,6 +258,21 @@ internal static async Task<OpenXmlReader> CreateAsync(Stream stream, IMiniExcelC
break;
}

if (_config is { IgnoreEmptyRows: false, MaxSynthesizedCells: { } maxSynthesizedCells })
{
var expectedRowIndex = isFirstRow ? startRowIndex : nextRowIndex;
var emptyRowCount = Math.Max(0, rowIndex - expectedRowIndex);
var columnCount = hasHeaderRow
? Math.Max(1, headRows.Count)
: Math.Max(1, maxColumnIndex - startColumnIndex + 1);

var selfClosingRowCount = reader.IsEmptyElement ? 1 : 0;
synthesizedCellCount += (long)(emptyRowCount + selfClosingRowCount) * columnCount;

if (synthesizedCellCount > maxSynthesizedCells)
throw new InvalidDataException($"The worksheet exceeds the configured limit of {maxSynthesizedCells} synthesized empty cells.");
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

var query = QueryRowAsync(reader, isFirstRow, startRowIndex, nextRowIndex, rowIndex,
startColumnIndex, endColumnIndex, maxColumnIndex, withoutCr, hasHeaderRow, headRows,
mergeCells, cancellationToken);
Expand Down Expand Up @@ -625,6 +651,9 @@ protected async Task<CellAndColumn> ReadCellAndSetColumnIndexAsync(XmlReader rea
}
else
{
if (!string.IsNullOrEmpty(aR) && referenceColumn > CellReferenceConverter.MaxColumnNumber)
throw new InvalidDataException($"Cell reference '{aR}' is invalid.");

newColumnIndex = columnIndex;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,25 @@ static MiniExcelGithubIssuesAsyncTests()
ExcelPackage.LicenseContext = LicenseContext.NonCommercial;
}

[Fact]
public async Task QueryAsyncRejectsRowIndexOutsideWorksheetLimits()
{
using var path = AutoDeletingPath.Create();
await _excelExporter.ExportAsync(path.FilePath, new[] { new { Value = "control" } });
SheetHelper.ReplaceFirstSheetXml(path.FilePath, """
<?xml version="1.0" encoding="utf-8"?>
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
<dimension ref="A1" />
<sheetData>
<row r="2000000000"><c r="A2000000000" t="str"><v>x</v></c></row>
</sheetData>
</worksheet>
""");

await Assert.ThrowsAsync<InvalidDataException>(async () =>
await _excelImporter.QueryAsync(path.FilePath).FirstAsync());
}

[Fact]
public async Task EmptyDataReaderIssue()
{
Expand Down
91 changes: 91 additions & 0 deletions tests/MiniExcel.OpenXml.Tests/Issues/MiniExcelGithubIssuesTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Text.RegularExpressions;
using MiniExcelLib.Core.Enums;
using MiniExcelLib.Core.Exceptions;
using MiniExcelLib.Core.Helpers;
using MiniExcelLib.OpenXml.Picture;
using MiniExcelLib.OpenXml.Tests.Utils;
using MiniExcelLib.Tests.Common.Utils;
Expand All @@ -24,6 +25,96 @@ static MiniExcelGithubIssuesTests()

private static bool IsDateFormatString(string formatCode) => DateTimeHelper.IsDateTimeFormat(formatCode);

[Fact]
public void CellReferenceConverterEnforcesWorksheetLimits()
{
Assert.True(CellReferenceConverter.TryParseCellReference("XFD1048576", out var column, out var row));
Assert.Equal(CellReferenceConverter.MaxColumnNumber, column);
Assert.Equal(CellReferenceConverter.MaxRowNumber, row);
Assert.False(CellReferenceConverter.TryParseCellReference("XFE1", out _, out _));
Assert.False(CellReferenceConverter.TryParseCellReference("A1048577", out _, out _));
}

[Fact]
public void QueryRangeAcceptsMaximumWorksheetCoordinates()
{
using var path = AutoDeletingPath.Create();
_excelExporter.Export(path.FilePath, new[] { new { Value = "control" } });
SheetHelper.ReplaceFirstSheetXml(path.FilePath, """
<?xml version="1.0" encoding="utf-8"?>
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
<dimension ref="XFD1048576" />
<sheetData>
<row r="1048576"><c r="XFD1048576" t="str"><v>x</v></c></row>
</sheetData>
</worksheet>
""");

var row = _excelImporter.QueryRange(
path.FilePath,
startRowIndex: CellReferenceConverter.MaxRowNumber,
startColumnIndex: CellReferenceConverter.MaxColumnNumber,
endRowIndex: CellReferenceConverter.MaxRowNumber,
endColumnIndex: CellReferenceConverter.MaxColumnNumber).Single();

Assert.Equal("x", row.XFD);
}

[Fact]
public void QueryRejectsWorksheetExceedingSynthesizedCellLimit()
{
using var path = AutoDeletingPath.Create();
_excelExporter.Export(path.FilePath, new[] { new { Value = "control" } });
SheetHelper.ReplaceFirstSheetXml(path.FilePath, """
<?xml version="1.0" encoding="utf-8"?>
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
<dimension ref="A1:XFD1048576" />
<sheetData>
<row r="1048576"><c r="XFD1048576" t="str"><v>x</v></c></row>
</sheetData>
</worksheet>
""");

var exception = Assert.Throws<InvalidDataException>(() => _excelImporter.Query(path.FilePath).First());
Assert.Contains("synthesized empty cells", exception.Message);
}

[Theory]
[InlineData("1048577")]
[InlineData("2000000000")]
public void QueryRejectsRowIndexOutsideWorksheetLimits(string rowNumber)
{
using var path = AutoDeletingPath.Create();
_excelExporter.Export(path.FilePath, new[] { new { Value = "control" } });
SheetHelper.ReplaceFirstSheetXml(path.FilePath, $"""
<?xml version="1.0" encoding="utf-8"?>
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
<dimension ref="A1" />
<sheetData>
<row r="{rowNumber}"><c r="A{rowNumber}" t="str"><v>x</v></c></row>
</sheetData>
</worksheet>
""");

Assert.Throws<InvalidDataException>(() => _excelImporter.Query(path.FilePath).First());
}

[Fact]
public void QueryRejectsDimensionOutsideWorksheetLimits()
{
using var path = AutoDeletingPath.Create();
_excelExporter.Export(path.FilePath, new[] { new { Value = "control" } });
SheetHelper.ReplaceFirstSheetXml(path.FilePath, """
<?xml version="1.0" encoding="utf-8"?>
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
<dimension ref="A1:XFE1" />
<sheetData><row r="1"><c r="A1" t="str"><v>x</v></c></row></sheetData>
</worksheet>
""");

Assert.Throws<InvalidDataException>(() => _excelImporter.Query(path.FilePath).First());
}

[Fact]
public void TestIssue_DataReaderSupportDimension()
{
Expand Down
15 changes: 15 additions & 0 deletions tests/MiniExcel.OpenXml.Tests/Utils/SheetHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,21 @@ internal static string GetZipFileContent(string zipPath, string filePath)
return doc.ToString();
}

internal static void ReplaceFirstSheetXml(string path, string worksheetXml)
{
using var stream = File.Open(path, FileMode.Open, FileAccess.ReadWrite);
using var archive = new ZipArchive(stream, ZipArchiveMode.Update, false, Encoding.UTF8);
var sheet = archive.Entries.Single(w =>
w.FullName.StartsWith("xl/worksheets/sheet1", StringComparison.OrdinalIgnoreCase) ||
w.FullName.StartsWith("/xl/worksheets/sheet1", StringComparison.OrdinalIgnoreCase));
var entryName = sheet.FullName;
sheet.Delete();

var replacement = archive.CreateEntry(entryName);
using var writer = new StreamWriter(replacement.Open(), new UTF8Encoding(false));
writer.Write(worksheetXml);
}

internal static string? GetFirstSheetDimensionRefValue(string path)
{
var ns = new XmlNamespaceManager(new NameTable());
Expand Down
Loading