diff --git a/AGENTS.md b/AGENTS.md index 54ddd51..5d50f89 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,7 +17,6 @@ - Every new public API member must have XML documentation matching the quality of the surrounding API. - New protected members that define engine contracts or are intended for derived engine implementations must also have XML documentation. - The documentation requirements do not apply to the `EPPlus45-FixCalcsEdition.MultiTarget` project or to `TestAndDemoExcelOps`; do not make documentation-only changes there. -- The HTML export API classes are currently excluded from documentation cleanup because they are tracked for a separate follow-up ticket. - Historical documentation baselines should stay at zero. If a member is intentionally out of scope, add a narrow checker exclusion instead of raising a global allowance. - Public enums and their values must be documented. - Keep `` text short and focused, for example `Insert one or more columns.`. @@ -43,6 +42,14 @@ - Static test workbooks belong in the appropriate `test_data` directories. - When repository copy/clone scripts generate or synchronize shared source or test files, include the resulting copied files in the same change. +## Release Process + +- Create releases only from the repository's primary integration branch, currently `main` or `master`. +- Do not create a release from a feature branch, ticket branch, Codex branch, or any other branch that has not been merged into the primary integration branch. +- Before creating a release, ensure the pull request has been created, reviewed as required, merged into the primary integration branch, and the build-and-test workflow for that branch has completed successfully. +- If the workflow is configured to run on pull requests and on pushes to the primary integration branch, wait for the relevant successful run after merge before creating the release. +- If a release is requested before these prerequisites are met, create or update the pull request first and explicitly tell the user that the release must wait for the successful build-and-test pipeline on the primary integration branch. + ## File Encoding and Line Endings - Save text files as UTF-8 with BOM and CRLF line endings, matching `.editorconfig`. diff --git a/ExcelOps-EpplusFreeFixCalcsEdition/EpplusFreeExcelDataOperations.SharedCode.vb b/ExcelOps-EpplusFreeFixCalcsEdition/EpplusFreeExcelDataOperations.SharedCode.vb index dc3bc12..ddf71ad 100644 --- a/ExcelOps-EpplusFreeFixCalcsEdition/EpplusFreeExcelDataOperations.SharedCode.vb +++ b/ExcelOps-EpplusFreeFixCalcsEdition/EpplusFreeExcelDataOperations.SharedCode.vb @@ -407,15 +407,34 @@ Namespace ExcelOps ''' Protected Overrides Sub LoadWorkbook(file As System.IO.FileInfo) - If Me.PasswordForOpening <> Nothing Then - Me._WorkbookPackage = New CompuMaster.Epplus4.ExcelPackage(file, Me.PasswordForOpening) - Else - Me._WorkbookPackage = New CompuMaster.Epplus4.ExcelPackage(file) + If IsUnsupportedBinaryXlsFile(file) Then + Throw New BinaryXlsFileNotSupportedException(file) End If - Me._WorkbookPackage.Compatibility.IsWorksheets1Based = False + If IsUnsupportedBinaryXlsbFile(file) Then + Throw New BinaryXlsbFileNotSupportedException(file) + End If + Try + If Me.PasswordForOpening <> Nothing Then + Me._WorkbookPackage = New CompuMaster.Epplus4.ExcelPackage(file, Me.PasswordForOpening) + Else + Me._WorkbookPackage = New CompuMaster.Epplus4.ExcelPackage(file) + End If + Catch ex As Exception When IsUnsupportedBinaryXlsFile(file) + Throw New BinaryXlsFileNotSupportedException(file, ex) + Catch ex As Exception When IsPasswordProtectedFilePasswordMismatch(file) + Throw New FilePasswordProtectedMismatchException(file, ex) + End Try + If String.Equals(file.Extension, ".xlsb", StringComparison.OrdinalIgnoreCase) Then + Throw New BinaryXlsbFileNotSupportedException(file) + End If + Try + Me._WorkbookPackage.Compatibility.IsWorksheets1Based = False - 'set workbook FullCalcOnLoad always to False since it's already triggered using property of Me.AutoCalculationOnLoad - Me.Workbook.FullCalcOnLoad = Me.AutoCalculationOnLoad 'unknown if executed after loading already completed or if it's a workbook setting with effect on opening as user in MS Excel, too + 'set workbook FullCalcOnLoad always to False since it's already triggered using property of Me.AutoCalculationOnLoad + Me.Workbook.FullCalcOnLoad = Me.AutoCalculationOnLoad 'unknown if executed after loading already completed or if it's a workbook setting with effect on opening as user in MS Excel, too + Catch ex As System.Xml.XmlException + Throw New FileCorruptedOrInvalidFileFormatException(file, ex) + End Try End Sub ''' @@ -428,17 +447,131 @@ Namespace ExcelOps ''' Protected Overrides Sub LoadWorkbook(data As System.IO.Stream) - If Me.PasswordForOpening <> Nothing Then - Me._WorkbookPackage = New CompuMaster.Epplus4.ExcelPackage(data, Me.PasswordForOpening) - Else - Me._WorkbookPackage = New CompuMaster.Epplus4.ExcelPackage(data) + If IsUnsupportedBinaryXlsData(data) Then + Throw New BinaryXlsFileNotSupportedException(CType(Nothing, String)) End If - Me._WorkbookPackage.Compatibility.IsWorksheets1Based = False + If IsUnsupportedBinaryXlsbData(data) Then + Throw New BinaryXlsbFileNotSupportedException(CType(Nothing, String)) + End If + Try + If Me.PasswordForOpening <> Nothing Then + Me._WorkbookPackage = New CompuMaster.Epplus4.ExcelPackage(data, Me.PasswordForOpening) + Else + Me._WorkbookPackage = New CompuMaster.Epplus4.ExcelPackage(data) + End If + Catch ex As Exception When IsUnsupportedBinaryXlsData(data) + Throw New BinaryXlsFileNotSupportedException(CType(Nothing, String), ex) + Catch ex As Exception When IsPasswordProtectedDataPasswordMismatch(data) + Throw New FilePasswordProtectedMismatchException(CType(Nothing, String), ex) + End Try + Try + Me._WorkbookPackage.Compatibility.IsWorksheets1Based = False - 'set workbook FullCalcOnLoad always to False since it's already triggered using property of Me.AutoCalculationOnLoad - Me.Workbook.FullCalcOnLoad = Me.AutoCalculationOnLoad 'unknown if executed after loading already completed or if it's a workbook setting with effect on opening as user in MS Excel, too + 'set workbook FullCalcOnLoad always to False since it's already triggered using property of Me.AutoCalculationOnLoad + Me.Workbook.FullCalcOnLoad = Me.AutoCalculationOnLoad 'unknown if executed after loading already completed or if it's a workbook setting with effect on opening as user in MS Excel, too + Catch ex As System.Xml.XmlException + Throw New FileCorruptedOrInvalidFileFormatException(CType(Nothing, String), ex) + End Try End Sub + Private Shared Function IsUnsupportedBinaryXlsFile(file As FileInfo) As Boolean + Using stream As FileStream = file.OpenRead() + Return IsUnsupportedBinaryXlsData(stream) + End Using + End Function + + Private Shared Function IsUnsupportedBinaryXlsbFile(file As FileInfo) As Boolean + Using stream As FileStream = file.OpenRead() + Return IsUnsupportedBinaryXlsbData(stream) + End Using + End Function + + Private Shared Function IsPasswordProtectedFilePasswordMismatch(file As FileInfo) As Boolean + Using stream As FileStream = file.OpenRead() + Return IsPasswordProtectedDataPasswordMismatch(stream) + End Using + End Function + + Private Shared Function IsUnsupportedBinaryXlsData(stream As Stream) As Boolean + Return IsOleCompoundDocument(stream) AndAlso Not ContainsOleDirectoryName(stream, "EncryptedPackage") + End Function + + Private Shared Function IsUnsupportedBinaryXlsbData(stream As Stream) As Boolean + Return ContainsZipEntryName(stream, "xl/workbook.bin") + End Function + + Private Shared Function IsPasswordProtectedDataPasswordMismatch(stream As Stream) As Boolean + Return IsOleCompoundDocument(stream) AndAlso ContainsOleDirectoryName(stream, "EncryptedPackage") + End Function + + Private Shared Function IsOleCompoundDocument(stream As Stream) As Boolean + Dim header = ReadHeader(stream, 8) + Dim oleHeader As Byte() = {&HD0, &HCF, &H11, &HE0, &HA1, &HB1, &H1A, &HE1} + Return ContainsBytes(header, oleHeader) + End Function + + Private Shared Function ContainsOleDirectoryName(stream As Stream, name As String) As Boolean + Dim originalPosition As Long? = Nothing + If stream.CanSeek Then originalPosition = stream.Position + Try + If stream.CanSeek Then stream.Position = 0 + Using copy As New MemoryStream() + stream.CopyTo(copy) + Dim data As Byte() = copy.ToArray() + Dim asciiNeedle As Byte() = Encoding.ASCII.GetBytes(name) + Dim unicodeNeedle As Byte() = Encoding.Unicode.GetBytes(name) + Return ContainsBytes(data, asciiNeedle) OrElse ContainsBytes(data, unicodeNeedle) + End Using + Finally + If originalPosition.HasValue Then stream.Position = originalPosition.Value + End Try + End Function + + Private Shared Function ContainsZipEntryName(stream As Stream, name As String) As Boolean + Dim originalPosition As Long? = Nothing + If stream.CanSeek Then originalPosition = stream.Position + Try + If stream.CanSeek Then stream.Position = 0 + Using copy As New MemoryStream() + stream.CopyTo(copy) + Dim data As Byte() = copy.ToArray() + Dim asciiNeedle As Byte() = Encoding.ASCII.GetBytes(name) + Return ContainsBytes(data, asciiNeedle) + End Using + Finally + If originalPosition.HasValue Then stream.Position = originalPosition.Value + End Try + End Function + + Private Shared Function ReadHeader(stream As Stream, count As Integer) As Byte() + Dim originalPosition As Long? = Nothing + If stream.CanSeek Then originalPosition = stream.Position + Try + If stream.CanSeek Then stream.Position = 0 + Dim buffer(count - 1) As Byte + Dim read As Integer = stream.Read(buffer, 0, buffer.Length) + If read <> buffer.Length Then Array.Resize(buffer, read) + Return buffer + Finally + If originalPosition.HasValue Then stream.Position = originalPosition.Value + End Try + End Function + + Private Shared Function ContainsBytes(data As Byte(), pattern As Byte()) As Boolean + If pattern.Length = 0 OrElse data.Length < pattern.Length Then Return False + For index As Integer = 0 To data.Length - pattern.Length + Dim found As Boolean = True + For patternIndex As Integer = 0 To pattern.Length - 1 + If data(index + patternIndex) <> pattern(patternIndex) Then + found = False + Exit For + End If + Next + If found Then Return True + Next + Return False + End Function + ''' Public Overrides Function LookupLastCell(sheetName As String) As ExcelOps.ExcelCell If sheetName = Nothing Then Throw New ArgumentNullException(NameOf(sheetName)) diff --git a/ExcelOps-EpplusPolyform/EpplusPolyformExcelDataOperations.SharedCode.vb b/ExcelOps-EpplusPolyform/EpplusPolyformExcelDataOperations.SharedCode.vb index d0291c1..e1bb097 100644 --- a/ExcelOps-EpplusPolyform/EpplusPolyformExcelDataOperations.SharedCode.vb +++ b/ExcelOps-EpplusPolyform/EpplusPolyformExcelDataOperations.SharedCode.vb @@ -407,15 +407,34 @@ Namespace ExcelOps ''' Protected Overrides Sub LoadWorkbook(file As System.IO.FileInfo) - If Me.PasswordForOpening <> Nothing Then - Me._WorkbookPackage = New OfficeOpenXml.ExcelPackage(file, Me.PasswordForOpening) - Else - Me._WorkbookPackage = New OfficeOpenXml.ExcelPackage(file) + If IsUnsupportedBinaryXlsFile(file) Then + Throw New BinaryXlsFileNotSupportedException(file) End If - Me._WorkbookPackage.Compatibility.IsWorksheets1Based = False + If IsUnsupportedBinaryXlsbFile(file) Then + Throw New BinaryXlsbFileNotSupportedException(file) + End If + Try + If Me.PasswordForOpening <> Nothing Then + Me._WorkbookPackage = New OfficeOpenXml.ExcelPackage(file, Me.PasswordForOpening) + Else + Me._WorkbookPackage = New OfficeOpenXml.ExcelPackage(file) + End If + Catch ex As Exception When IsUnsupportedBinaryXlsFile(file) + Throw New BinaryXlsFileNotSupportedException(file, ex) + Catch ex As Exception When IsPasswordProtectedFilePasswordMismatch(file) + Throw New FilePasswordProtectedMismatchException(file, ex) + End Try + If String.Equals(file.Extension, ".xlsb", StringComparison.OrdinalIgnoreCase) Then + Throw New BinaryXlsbFileNotSupportedException(file) + End If + Try + Me._WorkbookPackage.Compatibility.IsWorksheets1Based = False - 'set workbook FullCalcOnLoad always to False since it's already triggered using property of Me.AutoCalculationOnLoad - Me.Workbook.FullCalcOnLoad = Me.AutoCalculationOnLoad 'unknown if executed after loading already completed or if it's a workbook setting with effect on opening as user in MS Excel, too + 'set workbook FullCalcOnLoad always to False since it's already triggered using property of Me.AutoCalculationOnLoad + Me.Workbook.FullCalcOnLoad = Me.AutoCalculationOnLoad 'unknown if executed after loading already completed or if it's a workbook setting with effect on opening as user in MS Excel, too + Catch ex As System.Xml.XmlException + Throw New FileCorruptedOrInvalidFileFormatException(file, ex) + End Try End Sub ''' @@ -428,17 +447,131 @@ Namespace ExcelOps ''' Protected Overrides Sub LoadWorkbook(data As System.IO.Stream) - If Me.PasswordForOpening <> Nothing Then - Me._WorkbookPackage = New OfficeOpenXml.ExcelPackage(data, Me.PasswordForOpening) - Else - Me._WorkbookPackage = New OfficeOpenXml.ExcelPackage(data) + If IsUnsupportedBinaryXlsData(data) Then + Throw New BinaryXlsFileNotSupportedException(CType(Nothing, String)) End If - Me._WorkbookPackage.Compatibility.IsWorksheets1Based = False + If IsUnsupportedBinaryXlsbData(data) Then + Throw New BinaryXlsbFileNotSupportedException(CType(Nothing, String)) + End If + Try + If Me.PasswordForOpening <> Nothing Then + Me._WorkbookPackage = New OfficeOpenXml.ExcelPackage(data, Me.PasswordForOpening) + Else + Me._WorkbookPackage = New OfficeOpenXml.ExcelPackage(data) + End If + Catch ex As Exception When IsUnsupportedBinaryXlsData(data) + Throw New BinaryXlsFileNotSupportedException(CType(Nothing, String), ex) + Catch ex As Exception When IsPasswordProtectedDataPasswordMismatch(data) + Throw New FilePasswordProtectedMismatchException(CType(Nothing, String), ex) + End Try + Try + Me._WorkbookPackage.Compatibility.IsWorksheets1Based = False - 'set workbook FullCalcOnLoad always to False since it's already triggered using property of Me.AutoCalculationOnLoad - Me.Workbook.FullCalcOnLoad = Me.AutoCalculationOnLoad 'unknown if executed after loading already completed or if it's a workbook setting with effect on opening as user in MS Excel, too + 'set workbook FullCalcOnLoad always to False since it's already triggered using property of Me.AutoCalculationOnLoad + Me.Workbook.FullCalcOnLoad = Me.AutoCalculationOnLoad 'unknown if executed after loading already completed or if it's a workbook setting with effect on opening as user in MS Excel, too + Catch ex As System.Xml.XmlException + Throw New FileCorruptedOrInvalidFileFormatException(CType(Nothing, String), ex) + End Try End Sub + Private Shared Function IsUnsupportedBinaryXlsFile(file As FileInfo) As Boolean + Using stream As FileStream = file.OpenRead() + Return IsUnsupportedBinaryXlsData(stream) + End Using + End Function + + Private Shared Function IsUnsupportedBinaryXlsbFile(file As FileInfo) As Boolean + Using stream As FileStream = file.OpenRead() + Return IsUnsupportedBinaryXlsbData(stream) + End Using + End Function + + Private Shared Function IsPasswordProtectedFilePasswordMismatch(file As FileInfo) As Boolean + Using stream As FileStream = file.OpenRead() + Return IsPasswordProtectedDataPasswordMismatch(stream) + End Using + End Function + + Private Shared Function IsUnsupportedBinaryXlsData(stream As Stream) As Boolean + Return IsOleCompoundDocument(stream) AndAlso Not ContainsOleDirectoryName(stream, "EncryptedPackage") + End Function + + Private Shared Function IsUnsupportedBinaryXlsbData(stream As Stream) As Boolean + Return ContainsZipEntryName(stream, "xl/workbook.bin") + End Function + + Private Shared Function IsPasswordProtectedDataPasswordMismatch(stream As Stream) As Boolean + Return IsOleCompoundDocument(stream) AndAlso ContainsOleDirectoryName(stream, "EncryptedPackage") + End Function + + Private Shared Function IsOleCompoundDocument(stream As Stream) As Boolean + Dim header = ReadHeader(stream, 8) + Dim oleHeader As Byte() = {&HD0, &HCF, &H11, &HE0, &HA1, &HB1, &H1A, &HE1} + Return ContainsBytes(header, oleHeader) + End Function + + Private Shared Function ContainsOleDirectoryName(stream As Stream, name As String) As Boolean + Dim originalPosition As Long? = Nothing + If stream.CanSeek Then originalPosition = stream.Position + Try + If stream.CanSeek Then stream.Position = 0 + Using copy As New MemoryStream() + stream.CopyTo(copy) + Dim data As Byte() = copy.ToArray() + Dim asciiNeedle As Byte() = Encoding.ASCII.GetBytes(name) + Dim unicodeNeedle As Byte() = Encoding.Unicode.GetBytes(name) + Return ContainsBytes(data, asciiNeedle) OrElse ContainsBytes(data, unicodeNeedle) + End Using + Finally + If originalPosition.HasValue Then stream.Position = originalPosition.Value + End Try + End Function + + Private Shared Function ContainsZipEntryName(stream As Stream, name As String) As Boolean + Dim originalPosition As Long? = Nothing + If stream.CanSeek Then originalPosition = stream.Position + Try + If stream.CanSeek Then stream.Position = 0 + Using copy As New MemoryStream() + stream.CopyTo(copy) + Dim data As Byte() = copy.ToArray() + Dim asciiNeedle As Byte() = Encoding.ASCII.GetBytes(name) + Return ContainsBytes(data, asciiNeedle) + End Using + Finally + If originalPosition.HasValue Then stream.Position = originalPosition.Value + End Try + End Function + + Private Shared Function ReadHeader(stream As Stream, count As Integer) As Byte() + Dim originalPosition As Long? = Nothing + If stream.CanSeek Then originalPosition = stream.Position + Try + If stream.CanSeek Then stream.Position = 0 + Dim buffer(count - 1) As Byte + Dim read As Integer = stream.Read(buffer, 0, buffer.Length) + If read <> buffer.Length Then Array.Resize(buffer, read) + Return buffer + Finally + If originalPosition.HasValue Then stream.Position = originalPosition.Value + End Try + End Function + + Private Shared Function ContainsBytes(data As Byte(), pattern As Byte()) As Boolean + If pattern.Length = 0 OrElse data.Length < pattern.Length Then Return False + For index As Integer = 0 To data.Length - pattern.Length + Dim found As Boolean = True + For patternIndex As Integer = 0 To pattern.Length - 1 + If data(index + patternIndex) <> pattern(patternIndex) Then + found = False + Exit For + End If + Next + If found Then Return True + Next + Return False + End Function + ''' Public Overrides Function LookupLastCell(sheetName As String) As ExcelOps.ExcelCell If sheetName = Nothing Then Throw New ArgumentNullException(NameOf(sheetName)) diff --git a/ExcelOps-FreeSpireXls/FreeSpireXlsDataOperations.SharedCode.vb b/ExcelOps-FreeSpireXls/FreeSpireXlsDataOperations.SharedCode.vb index 3812bde..a6a10db 100644 --- a/ExcelOps-FreeSpireXls/FreeSpireXlsDataOperations.SharedCode.vb +++ b/ExcelOps-FreeSpireXls/FreeSpireXlsDataOperations.SharedCode.vb @@ -339,7 +339,11 @@ Namespace ExcelOps Protected Overrides Sub LoadWorkbook(file As System.IO.FileInfo) Me._Workbook = New Spire.Xls.Workbook Me.Workbook.OpenPassword = Me.PasswordForOpening - Me.Workbook.LoadFromFile(file.FullName) + Try + Me.Workbook.LoadFromFile(file.FullName) + Catch ex As Exception When IsPasswordProtectedFilePasswordMismatch(ex) + Throw New FilePasswordProtectedMismatchException(file, ex) + End Try End Sub ''' @@ -354,9 +358,19 @@ Namespace ExcelOps Protected Overrides Sub LoadWorkbook(data As IO.Stream) Me._Workbook = New Spire.Xls.Workbook Me.Workbook.OpenPassword = Me.PasswordForOpening - Me.Workbook.LoadFromStream(data) + Try + Me.Workbook.LoadFromStream(data) + Catch ex As Exception When IsPasswordProtectedFilePasswordMismatch(ex) + Throw New FilePasswordProtectedMismatchException(CType(Nothing, String), ex) + End Try End Sub + Private Shared Function IsPasswordProtectedFilePasswordMismatch(ex As Exception) As Boolean + Return ex IsNot Nothing AndAlso ex.Message IsNot Nothing AndAlso + (ex.Message.IndexOf("Invalid password", StringComparison.OrdinalIgnoreCase) >= 0 OrElse + ex.Message.IndexOf("provide password", StringComparison.OrdinalIgnoreCase) >= 0) + End Function + ''' Public Overrides Function LookupLastCell(sheetName As String) As ExcelOps.ExcelCell If sheetName = Nothing Then Throw New ArgumentNullException(NameOf(sheetName)) diff --git a/ExcelOps-MicrosoftExcel/ExcelOpsLowLevel/MsExcelDataOperations.vb b/ExcelOps-MicrosoftExcel/ExcelOpsLowLevel/MsExcelDataOperations.vb index ddd85a3..b9d1c43 100644 --- a/ExcelOps-MicrosoftExcel/ExcelOpsLowLevel/MsExcelDataOperations.vb +++ b/ExcelOps-MicrosoftExcel/ExcelOpsLowLevel/MsExcelDataOperations.vb @@ -466,7 +466,11 @@ Namespace Global.CompuMaster.Excel.ExcelOps End If Catch ex As System.Runtime.InteropServices.COMException If ex.ErrorCode = &H800A03EC Then - Throw New FileCorruptedOrInvalidFileFormatException(file, ex) + If IsPasswordProtectedFilePasswordMismatch(file) OrElse IsPasswordProtectedFilePasswordMismatch(ex) Then + Throw New FilePasswordProtectedMismatchException(file, ex) + Else + Throw New FileCorruptedOrInvalidFileFormatException(file, ex) + End If Else Throw End If @@ -490,6 +494,69 @@ Namespace Global.CompuMaster.Excel.ExcelOps Throw New NotSupportedException() End Sub + Private Shared Function IsPasswordProtectedFilePasswordMismatch(file As System.IO.FileInfo) As Boolean + Using stream As System.IO.FileStream = file.OpenRead() + Return IsOleCompoundDocument(stream) AndAlso ContainsOleDirectoryName(stream, "EncryptedPackage") + End Using + End Function + + Private Shared Function IsPasswordProtectedFilePasswordMismatch(ex As Exception) As Boolean + If ex Is Nothing OrElse ex.Message Is Nothing Then Return False + Return ex.Message.IndexOf("password", StringComparison.OrdinalIgnoreCase) >= 0 OrElse ex.Message.IndexOf("Kennwort", StringComparison.OrdinalIgnoreCase) >= 0 + End Function + + Private Shared Function IsOleCompoundDocument(stream As System.IO.Stream) As Boolean + Dim header = ReadHeader(stream, 8) + Dim oleHeader As Byte() = {&HD0, &HCF, &H11, &HE0, &HA1, &HB1, &H1A, &HE1} + Return ContainsBytes(header, oleHeader) + End Function + + Private Shared Function ContainsOleDirectoryName(stream As System.IO.Stream, name As String) As Boolean + Dim originalPosition As Long? = Nothing + If stream.CanSeek Then originalPosition = stream.Position + Try + If stream.CanSeek Then stream.Position = 0 + Using copy As New System.IO.MemoryStream() + stream.CopyTo(copy) + Dim data As Byte() = copy.ToArray() + Dim asciiNeedle As Byte() = Encoding.ASCII.GetBytes(name) + Dim unicodeNeedle As Byte() = Encoding.Unicode.GetBytes(name) + Return ContainsBytes(data, asciiNeedle) OrElse ContainsBytes(data, unicodeNeedle) + End Using + Finally + If originalPosition.HasValue Then stream.Position = originalPosition.Value + End Try + End Function + + Private Shared Function ReadHeader(stream As System.IO.Stream, count As Integer) As Byte() + Dim originalPosition As Long? = Nothing + If stream.CanSeek Then originalPosition = stream.Position + Try + If stream.CanSeek Then stream.Position = 0 + Dim buffer(count - 1) As Byte + Dim read As Integer = stream.Read(buffer, 0, buffer.Length) + If read <> buffer.Length Then Array.Resize(buffer, read) + Return buffer + Finally + If originalPosition.HasValue Then stream.Position = originalPosition.Value + End Try + End Function + + Private Shared Function ContainsBytes(data As Byte(), pattern As Byte()) As Boolean + If pattern.Length = 0 OrElse data.Length < pattern.Length Then Return False + For index As Integer = 0 To data.Length - pattern.Length + Dim found As Boolean = True + For patternIndex As Integer = 0 To pattern.Length - 1 + If data(index + patternIndex) <> pattern(patternIndex) Then + found = False + Exit For + End If + Next + If found Then Return True + Next + Return False + End Function + ''' Public Overrides Sub CleanupRangeNames() 'do nothing - just needs to be done once, see Epplus implementation diff --git a/ExcelOps-SpireXls/SpireXlsDataOperations.SharedCode.vb b/ExcelOps-SpireXls/SpireXlsDataOperations.SharedCode.vb index 17cbb2f..dc28806 100644 --- a/ExcelOps-SpireXls/SpireXlsDataOperations.SharedCode.vb +++ b/ExcelOps-SpireXls/SpireXlsDataOperations.SharedCode.vb @@ -339,7 +339,11 @@ Namespace ExcelOps Protected Overrides Sub LoadWorkbook(file As System.IO.FileInfo) Me._Workbook = New Spire.Xls.Workbook Me.Workbook.OpenPassword = Me.PasswordForOpening - Me.Workbook.LoadFromFile(file.FullName) + Try + Me.Workbook.LoadFromFile(file.FullName) + Catch ex As Exception When IsPasswordProtectedFilePasswordMismatch(ex) + Throw New FilePasswordProtectedMismatchException(file, ex) + End Try End Sub ''' @@ -354,9 +358,19 @@ Namespace ExcelOps Protected Overrides Sub LoadWorkbook(data As IO.Stream) Me._Workbook = New Spire.Xls.Workbook Me.Workbook.OpenPassword = Me.PasswordForOpening - Me.Workbook.LoadFromStream(data) + Try + Me.Workbook.LoadFromStream(data) + Catch ex As Exception When IsPasswordProtectedFilePasswordMismatch(ex) + Throw New FilePasswordProtectedMismatchException(CType(Nothing, String), ex) + End Try End Sub + Private Shared Function IsPasswordProtectedFilePasswordMismatch(ex As Exception) As Boolean + Return ex IsNot Nothing AndAlso ex.Message IsNot Nothing AndAlso + (ex.Message.IndexOf("Invalid password", StringComparison.OrdinalIgnoreCase) >= 0 OrElse + ex.Message.IndexOf("provide password", StringComparison.OrdinalIgnoreCase) >= 0) + End Function + ''' Public Overrides Function LookupLastCell(sheetName As String) As ExcelOps.ExcelCell If sheetName = Nothing Then Throw New ArgumentNullException(NameOf(sheetName)) diff --git a/ExcelOps/ExcelOpsLowLevel/ExcelDataOperationsBase.vb b/ExcelOps/ExcelOpsLowLevel/ExcelDataOperationsBase.vb index 9833f2e..32ce71b 100644 --- a/ExcelOps/ExcelOpsLowLevel/ExcelDataOperationsBase.vb +++ b/ExcelOps/ExcelOpsLowLevel/ExcelDataOperationsBase.vb @@ -2320,6 +2320,9 @@ Namespace ExcelOps End Select End Sub + ''' + ''' Defines which parts of an HTML document are emitted for worksheet export. + ''' Public Enum HtmlDocumentExportParts As Byte ''' ''' Creates a full HTML document with header tags, style tags, etc. diff --git a/ExcelOps/ExcelOpsLowLevel/HtmlSheetExportOptions.vb b/ExcelOps/ExcelOpsLowLevel/HtmlSheetExportOptions.vb index ec01f7a..ce07aac 100644 --- a/ExcelOps/ExcelOpsLowLevel/HtmlSheetExportOptions.vb +++ b/ExcelOps/ExcelOpsLowLevel/HtmlSheetExportOptions.vb @@ -4,98 +4,136 @@ Imports System.Text Namespace ExcelOps + ''' + ''' Defines options for exporting a worksheet to HTML. + ''' Public Class HtmlSheetExportOptions + ''' + ''' Creates a new HTML worksheet export options instance. + ''' Public Sub New() End Sub + ''' + ''' Defines how a worksheet title is rendered before exported worksheet data. + ''' Public Enum SheetTitleStyles As Integer + ''' + ''' Does not render a worksheet title. + ''' None = 0 + ''' + ''' Renders the worksheet title as an H1 element. + ''' H1 = 1 + ''' + ''' Renders the worksheet title as an H2 element. + ''' H2 = 2 + ''' + ''' Renders the worksheet title as an H3 element. + ''' H3 = 3 + ''' + ''' Renders the worksheet title as an H4 element. + ''' H4 = 4 + ''' + ''' Renders the worksheet title as an H5 element. + ''' H5 = 5 + ''' + ''' Renders the worksheet title as an H6 element. + ''' H6 = 6 + ''' + ''' Renders the worksheet title as a paragraph element. + ''' P = 10 End Enum ''' - ''' Add a header before the sheet data + ''' Gets or sets how the worksheet name is rendered before worksheet data. ''' - ''' Public Property ExportSheetNameAsTitle As SheetTitleStyles ''' - ''' Cells of these row indexes will be converted to TH cells instead of TD cells + ''' Gets or sets row indexes whose cells are rendered as TH elements instead of TD elements. ''' - ''' Public Property ConsiderRowIndexesAsTableHeader As List(Of Integer) ''' - ''' The class name for the table in HTML + ''' Gets or sets the CSS class name used for generated worksheet tables. ''' - ''' Public Property TableCssClassName As String = "xlTable" #Disable Warning CA1805 ' Keine unnötige Initialisierung + ''' + ''' Gets or sets the zero-based index of the first worksheet row to export. + ''' Public Property FirstRowIndex As Integer = 0 + ''' + ''' Gets or sets the zero-based index of the first worksheet column to export. + ''' Public Property FirstColumnIndex As Integer = 0 + ''' + ''' Gets or sets the zero-based index of the last worksheet row to export. + ''' Public Property LastRowIndex As Integer? + ''' + ''' Gets or sets the zero-based index of the last worksheet column to export. + ''' Public Property LastColumnIndex As Integer? #Enable Warning CA1805 ' Keine unnötige Initialisierung + ''' + ''' Gets or sets the HTML emitted when a worksheet has no exportable content. + ''' Public Property HtmlForEmptySheet As String ''' - ''' HTML code on top of everything (including html and head tags) + ''' Gets or sets the HTML emitted before generated worksheet content, including HTML and HEAD tags. ''' - ''' Public Property HtmlDocumentHeader As String ''' - ''' HTML code on top of exported sheets (usually </head><body>) + ''' Gets or sets the HTML emitted between the document header and exported worksheets. ''' - ''' Public Property HtmlDocumentHeaderEndAndBeginOfBody As String ''' - ''' HTML code on bottom of everything (usually </body></html>) + ''' Gets or sets the HTML emitted after generated worksheet content. ''' - ''' Public Property HtmlDocumentEnd As String ''' - ''' Typically html+head tags + ''' Gets the default HTML document header. ''' - ''' Protected Friend ReadOnly Property DefaultHtmlDocumentHeader As String = "" & ControlChars.CrLf & "" & ControlChars.CrLf & "" ''' - ''' Typically /head+body tags + ''' Gets the default HTML fragment that closes HEAD and opens BODY. ''' - ''' Protected Friend ReadOnly Property DefaultHtmlDocumentHeaderEndAndBeginOfBody As String = "" ''' - ''' Typically /body+/html tags + ''' Gets the default HTML document ending. ''' - ''' Protected Friend ReadOnly Property DefaultHtmlDocumentEnd As String = "" ''' - ''' Typically some HTML indicating there was no content in Excel worksheet + ''' Gets the default HTML emitted for an empty worksheet. ''' - ''' Protected Friend ReadOnly Property DefaultHtmlForEmptySheet As String = "-/-" ''' - ''' Typically some HTML indicating there was no content in Excel worksheet + ''' Gets the effective HTML emitted for an empty worksheet. ''' - ''' + ''' Configured empty-sheet HTML, or the default empty-sheet HTML when no value is configured. Public Overridable Function EffectiveHtmlForEmptySheet() As String If HtmlForEmptySheet Is Nothing Then Return DefaultHtmlForEmptySheet @@ -105,9 +143,9 @@ Namespace ExcelOps End Function ''' - ''' Typically /head+body tags + ''' Gets the effective HTML fragment that closes HEAD and opens BODY. ''' - ''' + ''' Configured header/body transition HTML, or the default transition HTML when no value is configured. Public Overridable Function EffectiveHtmlDocumentHeaderEndAndBeginOfBody() As String If HtmlDocumentHeaderEndAndBeginOfBody Is Nothing Then Return DefaultHtmlDocumentHeaderEndAndBeginOfBody @@ -117,9 +155,9 @@ Namespace ExcelOps End Function ''' - ''' Typically html+head+styles+body tags + ''' Gets the effective HTML document header including styles and BODY start. ''' - ''' + ''' HTML emitted before worksheet content. Public Overridable Function EffectiveHtmlDocumentHeaderAndBody() As String Dim sb As New System.Text.StringBuilder(1024) sb.AppendLine(Me.EffectiveHtmlDocumentHeader) @@ -129,9 +167,9 @@ Namespace ExcelOps End Function ''' - ''' Typically html+head tags + ''' Gets the effective HTML document header. ''' - ''' + ''' Configured document header HTML, or the default document header HTML when no value is configured. Public Overridable Function EffectiveHtmlDocumentHeader() As String If HtmlDocumentHeader Is Nothing Then Return DefaultHtmlDocumentHeader @@ -141,9 +179,9 @@ Namespace ExcelOps End Function ''' - ''' Typically /body+/html tags + ''' Gets the effective HTML document ending. ''' - ''' + ''' Configured document end HTML, or the default document end HTML when no value is configured. Public Overridable Function EffectiveHtmlDocumentEnd() As String If HtmlDocumentEnd Is Nothing Then Return DefaultHtmlDocumentEnd @@ -153,9 +191,9 @@ Namespace ExcelOps End Function ''' - ''' Style HTML for the table (<style>...</style>) + ''' Gets the effective STYLE element for generated worksheet tables. ''' - ''' + ''' HTML STYLE element for generated worksheet tables. Public Overridable Function EffectiveTableCssClassStyleHtml() As String Dim sb As New System.Text.StringBuilder(1024) sb.AppendLine("