Initial commit of the ClosedXML library

Establishes the foundational structure, core Excel functionality, and tooling.

- Implements base components for cells, ranges, columns, and rows.
- Integrates a custom calculation engine with a wide range of Excel functions.
- Adds comprehensive support for styling, conditional formatting, data validation, comments, pictures, and charts.
- Configures build setup, project metadata (NuGet), and developer guidelines.
- Includes editor and Git attributes for consistent code style and line endings.
This commit is contained in:
Thom Lamb
2026-06-23 11:02:53 -05:00
parent 28bc05cf54
commit 2cd6df6481
1092 changed files with 97270 additions and 408 deletions
@@ -0,0 +1,30 @@
using ClosedXML.Utils;
using NUnit.Framework;
using static ClosedXML.Excel.XLProtectionAlgorithm;
namespace ClosedXML_Tests.Excel.Protection
{
public class HashAlgorithmTests
{
[Test]
public void TestEmptyPassword()
{
Assert.IsEmpty(CryptographicAlgorithms.GetPasswordHash(Algorithm.SHA512, string.Empty));
Assert.IsEmpty(CryptographicAlgorithms.GetPasswordHash(Algorithm.SimpleHash, string.Empty));
}
[Test]
public void TestSHA512()
{
var hash = CryptographicAlgorithms.GetPasswordHash(Algorithm.SHA512, "12345", "aVvPw1DNH3evPqRAd/y3UQ==", 100000);
Assert.AreEqual("E+qAhyIg/HM0dUrPaENfimFOZp7wlOkJsf/sdG+AGHOA9grOv7VLb1ik2vuYohljI9G36e0ea9wnixCK0MMuyQ==", hash);
}
[Test]
public void TestSimple()
{
var hash = CryptographicAlgorithms.GetPasswordHash(Algorithm.SimpleHash, "12345");
Assert.AreEqual("CA9C", hash);
}
}
}
@@ -0,0 +1,193 @@
// Keep this file CodeMaid organised and cleaned
using ClosedXML.Excel;
using NUnit.Framework;
using System;
using System.IO;
using System.Linq;
using static ClosedXML.Excel.XLProtectionAlgorithm;
namespace ClosedXML_Tests.Excel.Worksheets
{
[TestFixture]
public class XLSheetProtectionTests
{
[Test]
public void AllowEverything()
{
using (var wb = new XLWorkbook())
{
var ws = wb.AddWorksheet("Sheet1");
ws.Protect().AllowedElements = XLSheetProtectionElements.Everything;
foreach (var element in Enum.GetValues(typeof(XLSheetProtectionElements)).Cast<XLSheetProtectionElements>())
Assert.IsTrue(ws.Protection.AllowedElements.HasFlag(element), element.ToString());
}
using (var wb = new XLWorkbook())
{
var ws = wb.AddWorksheet("Sheet1");
ws.Protect().AllowElement(XLSheetProtectionElements.Everything);
foreach (var element in Enum.GetValues(typeof(XLSheetProtectionElements)).Cast<XLSheetProtectionElements>())
Assert.IsTrue(ws.Protection.AllowedElements.HasFlag(element), element.ToString());
}
using (var wb = new XLWorkbook())
{
var ws = wb.AddWorksheet("Sheet1");
ws.Protect().AllowEverything();
foreach (var element in Enum.GetValues(typeof(XLSheetProtectionElements)).Cast<XLSheetProtectionElements>())
Assert.IsTrue(ws.Protection.AllowedElements.HasFlag(element), element.ToString());
}
}
[Test]
public void AllowNothing()
{
using (var wb = new XLWorkbook())
{
var ws = wb.AddWorksheet("Sheet1");
ws.Protect().AllowedElements = XLSheetProtectionElements.None;
foreach (var element in Enum.GetValues(typeof(XLSheetProtectionElements))
.Cast<XLSheetProtectionElements>()
.Where(e => e != XLSheetProtectionElements.None))
Assert.IsFalse(ws.Protection.AllowedElements.HasFlag(element), element.ToString());
}
using (var wb = new XLWorkbook())
{
var ws = wb.AddWorksheet("Sheet1");
ws.Protect().AllowNone();
foreach (var element in Enum.GetValues(typeof(XLSheetProtectionElements))
.Cast<XLSheetProtectionElements>()
.Where(e => e != XLSheetProtectionElements.None))
Assert.IsFalse(ws.Protection.AllowedElements.HasFlag(element), element.ToString());
}
}
[Test]
public void ChangeHashingAlgorithm()
{
using (var ms = new MemoryStream())
{
using (var wb = new XLWorkbook())
{
var ws = wb.AddWorksheet();
ws.Protect("123", Algorithm.SimpleHash);
wb.SaveAs(ms);
}
ms.Seek(0, SeekOrigin.Begin);
using (var wb = new XLWorkbook(ms))
{
var ws = wb.Worksheets.First();
Assert.IsTrue(ws.Protection.IsProtected);
Assert.AreEqual(Algorithm.SimpleHash, ws.Protection.Algorithm);
ws.Unprotect("123");
ws.Protect("123", Algorithm.SHA512);
wb.Save();
}
ms.Seek(0, SeekOrigin.Begin);
using (var wb = new XLWorkbook(ms))
{
var ws = wb.Worksheets.First();
Assert.IsTrue(ws.Protection.IsProtected);
Assert.AreEqual(Algorithm.SHA512, ws.Protection.Algorithm);
Assert.DoesNotThrow(() => ws.Unprotect("123"));
}
}
}
[Test]
public void CopyProtectionFromAnotherSheet()
{
using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"Examples\Misc\SheetProtection.xlsx")))
using (var wb = new XLWorkbook(stream))
{
var ws1 = wb.Worksheet("Protected Password = 123");
var p1 = ws1.Protection.CastTo<XLSheetProtection>();
Assert.IsTrue(p1.IsProtected);
var ws2 = ws1.CopyTo("New worksheet");
Assert.IsFalse(ws2.Protection.IsProtected);
var p2 = ws2.Protection.CopyFrom(p1).CastTo<XLSheetProtection>();
Assert.IsTrue(p2.IsProtected);
Assert.IsTrue(p2.IsPasswordProtected);
Assert.AreEqual(p1.Algorithm, p2.Algorithm);
Assert.AreEqual(p1.PasswordHash, p2.PasswordHash);
Assert.AreEqual(p1.Base64EncodedSalt, p2.Base64EncodedSalt);
Assert.AreEqual(p1.SpinCount, p2.SpinCount);
Assert.IsTrue(p2.AllowedElements.HasFlag(XLSheetProtectionElements.InsertColumns));
Assert.IsTrue(p2.AllowedElements.HasFlag(XLSheetProtectionElements.InsertRows));
Assert.IsFalse(p2.AllowedElements.HasFlag(XLSheetProtectionElements.InsertHyperlinks));
Assert.Throws<InvalidOperationException>(() => ws2.Unprotect());
ws2.Unprotect("123");
}
}
[Test]
public void SetWorksheetProtectionCloning()
{
var ws1 = new XLWorkbook().AddWorksheet();
var ws2 = new XLWorkbook().AddWorksheet();
ws1.Protect("123")
.AllowElement(XLSheetProtectionElements.FormatEverything)
.DisallowElement(XLSheetProtectionElements.FormatCells);
Assert.AreEqual(XLSheetProtectionElements.FormatColumns | XLSheetProtectionElements.FormatRows | XLSheetProtectionElements.SelectEverything, ws1.Protection.AllowedElements);
ws2.Protection = ws1.Protection;
Assert.IsFalse(ReferenceEquals(ws1.Protection, ws2.Protection));
Assert.IsTrue(ws2.Protection.IsProtected);
Assert.AreEqual(XLSheetProtectionElements.FormatColumns | XLSheetProtectionElements.FormatRows | XLSheetProtectionElements.SelectEverything, ws2.Protection.AllowedElements);
Assert.AreEqual((ws1.Protection as XLSheetProtection).PasswordHash, (ws2.Protection as XLSheetProtection).PasswordHash);
}
[Test]
public void TestUnprotectWorksheetWithNoPassword()
{
using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"TryToLoad\SHA512PasswordProtection.xlsx")))
using (var wb = new XLWorkbook(stream))
{
var ws = wb.Worksheet("Sheet1");
Assert.IsTrue(ws.Protection.IsProtected);
ws.Unprotect();
Assert.IsFalse(ws.Protection.IsProtected);
}
}
[Test]
public void TestWorksheetWithSHA512Protection()
{
using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"TryToLoad\SHA512PasswordProtection.xlsx")))
using (var wb = new XLWorkbook(stream))
{
var ws = wb.Worksheet("Sheet2");
Assert.IsTrue(ws.Protection.IsProtected);
// Password required
Assert.Throws<InvalidOperationException>(() => ws.Unprotect());
Assert.AreEqual(Algorithm.SHA512, ws.Protection.Algorithm);
ws.Unprotect("abc");
Assert.IsFalse(ws.Protection.IsProtected);
}
}
}
}
@@ -0,0 +1,329 @@
// Keep this file CodeMaid organised and cleaned
using ClosedXML.Excel;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using static ClosedXML.Excel.XLProtectionAlgorithm;
namespace ClosedXML_Tests
{
public class XLWorkbookProtectionTests
{
[Test]
public void CanChangeProtectionAlgorithm()
{
using (var ms = new MemoryStream())
{
using (var stream = GetProtectedWorkbookStreamWithPassword())
using (var wb = new XLWorkbook(stream))
{
Assert.AreEqual(Algorithm.SHA512, wb.Protection.Algorithm);
wb.Unprotect("12345");
wb.Protect("12345", Algorithm.SimpleHash);
wb.SaveAs(ms);
}
ms.Seek(0, SeekOrigin.Begin);
using (var wb = new XLWorkbook(ms))
{
Assert.IsTrue(wb.IsPasswordProtected);
Assert.AreEqual(Algorithm.SimpleHash, wb.Protection.Algorithm);
}
}
}
[Test]
public void CanChangeToPasswordProtected()
{
using (var ms = new MemoryStream())
{
using (var stream = GetProtectedWorkbookStreamWithoutPassword())
using (var wb = new XLWorkbook(stream))
{
wb.Unprotect();
wb.Protection.Protect("12345");
Assert.IsTrue(wb.Protection.IsPasswordProtected);
wb.SaveAs(ms);
}
ms.Seek(0, SeekOrigin.Begin);
using (var wb = new XLWorkbook(ms))
{
Assert.IsTrue(wb.Protection.IsPasswordProtected);
Assert.AreEqual(Algorithm.SimpleHash, wb.Protection.Algorithm);
Assert.AreNotEqual("", wb.Protection.PasswordHash);
}
}
}
[Test]
public void CanChangeToProtectedWithoutPassword()
{
using (var ms = new MemoryStream())
{
using (var stream = GetProtectedWorkbookStreamWithPassword())
using (var wb = new XLWorkbook(stream))
{
wb.Unprotect("12345");
wb.Protection.Protect();
Assert.IsFalse(wb.Protection.IsPasswordProtected);
Assert.IsTrue(wb.Protection.IsProtected);
wb.SaveAs(ms);
}
ms.Seek(0, SeekOrigin.Begin);
using (var wb = new XLWorkbook(ms))
{
Assert.IsFalse(wb.Protection.IsPasswordProtected);
Assert.IsTrue(wb.Protection.IsProtected);
Assert.AreEqual(Algorithm.SimpleHash, wb.Protection.Algorithm);
Assert.AreEqual("", wb.Protection.PasswordHash);
}
}
}
[Test]
public void CannotUnprotectIfNoPassword()
{
using (var stream = GetProtectedWorkbookStreamWithoutPassword())
using (var wb = new XLWorkbook(stream))
{
var ex = Assert.Throws<ArgumentException>(() => wb.Unprotect("dummy password"));
Assert.AreEqual("Invalid password", ex.Message);
}
}
[Test]
public void CannotUnprotectWithoutPassword()
{
using (var stream = GetProtectedWorkbookStreamWithPassword())
using (var wb = new XLWorkbook(stream))
{
var ex = Assert.Throws<InvalidOperationException>(() => wb.Unprotect());
Assert.AreEqual("The workbook structure is password protected", ex.Message);
}
}
[Test]
[Theory]
public void CanProtectWithPassword(Algorithm algorithm)
{
using (var ms = new MemoryStream())
{
using (var wb = new XLWorkbook())
{
wb.AddWorksheet();
Assert.IsFalse(wb.Protection.IsProtected);
wb.Protection.Protect("12345", algorithm);
wb.Protection.AllowNone();
Assert.IsFalse(wb.Protection.AllowedElements.HasFlag(XLWorkbookProtectionElements.Structure));
Assert.IsFalse(wb.Protection.AllowedElements.HasFlag(XLWorkbookProtectionElements.Windows));
wb.SaveAs(ms);
}
ms.Seek(0, SeekOrigin.Begin);
using (var wb = new XLWorkbook(ms))
{
Assert.IsTrue(wb.Protection.IsPasswordProtected);
Assert.IsTrue(wb.Protection.IsProtected);
Assert.AreEqual(algorithm, wb.Protection.Algorithm);
Assert.AreNotEqual("", wb.Protection.PasswordHash);
Assert.IsFalse(wb.Protection.AllowedElements.HasFlag(XLWorkbookProtectionElements.Structure));
Assert.IsFalse(wb.Protection.AllowedElements.HasFlag(XLWorkbookProtectionElements.Windows));
var ex = Assert.Throws<ArgumentException>(() => wb.Unprotect("dummy password"));
Assert.AreEqual("Invalid password", ex.Message);
wb.Protection.Unprotect("12345");
wb.Save();
}
}
}
[Test]
public void CanUnprotectWithoutPassword()
{
using (var ms = new MemoryStream())
{
using (var stream = GetProtectedWorkbookStreamWithoutPassword())
using (var wb = new XLWorkbook(stream))
{
// Unprotect without password
wb.Unprotect();
Assert.IsFalse(wb.Protection.IsProtected);
wb.SaveAs(ms);
}
ms.Seek(0, SeekOrigin.Begin);
using (var wb = new XLWorkbook(ms))
{
Assert.IsFalse(wb.Protection.IsProtected);
}
}
}
[Test]
public void CanUnprotectWithPassword()
{
using (var ms = new MemoryStream())
{
using (var stream = GetProtectedWorkbookStreamWithPassword())
using (var wb = new XLWorkbook(stream))
{
// Unprotect with password
wb.Unprotect("12345");
Assert.IsFalse(wb.Protection.IsProtected);
wb.SaveAs(ms);
}
ms.Seek(0, SeekOrigin.Begin);
using (var wb = new XLWorkbook(ms))
{
Assert.IsFalse(wb.Protection.IsProtected);
}
}
}
[Test]
public void CopyProtectionFromAnotherWorkbook()
{
using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"Examples\Misc\WorkbookProtection.xlsx")))
using (var wb1 = new XLWorkbook(stream))
using (var wb2 = new XLWorkbook())
{
wb2.AddWorksheet();
var p1 = wb1.Protection.CastTo<XLWorkbookProtection>();
Assert.IsTrue(p1.IsProtected);
Assert.IsFalse(wb2.Protection.IsProtected);
var p2 = wb2.Protection.CopyFrom(wb1.Protection).CastTo<XLWorkbookProtection>();
Assert.IsTrue(p2.IsProtected);
Assert.IsTrue(p2.IsPasswordProtected);
Assert.AreEqual(p1.Algorithm, p2.Algorithm);
Assert.AreEqual(p1.PasswordHash, p2.PasswordHash);
Assert.AreEqual(p1.Base64EncodedSalt, p2.Base64EncodedSalt);
Assert.AreEqual(p1.SpinCount, p2.SpinCount);
Assert.IsTrue(p2.AllowedElements.HasFlag(XLWorkbookProtectionElements.Windows));
Assert.IsFalse(p2.AllowedElements.HasFlag(XLWorkbookProtectionElements.Structure));
Assert.Throws<InvalidOperationException>(() => wb2.Unprotect());
wb2.Unprotect("Abc@123");
}
}
[Test]
public void IXLProtectableTests()
{
using var wb = new XLWorkbook();
Enumerable.Range(1, 5).ForEach(i => wb.AddWorksheet());
var list = new List<IXLProtectable>() { wb };
list.AddRange(wb.Worksheets);
list.ForEach(el => el.Protect());
list.ForEach(el => Assert.IsTrue(el.IsProtected));
list.ForEach(el => Assert.IsFalse(el.IsPasswordProtected));
list.ForEach(el => el.Unprotect());
list.ForEach(el => Assert.IsFalse(el.IsProtected));
list.ForEach(el => Assert.IsFalse(el.IsPasswordProtected));
list.ForEach(el => el.Protect("password"));
list.ForEach(el => Assert.IsTrue(el.IsProtected));
list.ForEach(el => Assert.IsTrue(el.IsPasswordProtected));
list.ForEach(el => el.Unprotect("password"));
list.ForEach(el => Assert.IsFalse(el.IsProtected));
list.ForEach(el => Assert.IsFalse(el.IsPasswordProtected));
}
[Test]
public void LoadProtectionWithoutPasswordFromFile()
{
using (var stream = GetProtectedWorkbookStreamWithoutPassword())
using (var wb = new XLWorkbook(stream))
{
Assert.IsFalse(wb.Protection.IsPasswordProtected);
Assert.IsTrue(wb.Protection.IsProtected);
Assert.AreEqual("", wb.Protection.PasswordHash);
Assert.IsTrue(wb.Protection.AllowedElements.HasFlag(XLWorkbookProtectionElements.Windows));
Assert.IsFalse(wb.Protection.AllowedElements.HasFlag(XLWorkbookProtectionElements.Structure));
}
}
[Test]
public void LoadProtectionWithPasswordFromFile()
{
using (var stream = GetProtectedWorkbookStreamWithPassword())
using (var wb = new XLWorkbook(stream))
{
Assert.IsTrue(wb.Protection.IsPasswordProtected);
Assert.AreNotEqual("", wb.Protection.PasswordHash);
Assert.IsTrue(wb.Protection.AllowedElements.HasFlag(XLWorkbookProtectionElements.Windows));
Assert.IsFalse(wb.Protection.AllowedElements.HasFlag(XLWorkbookProtectionElements.Structure));
}
}
[Test]
public void SetWorkbookProtectionCloning()
{
var wb1 = new XLWorkbook();
var wb2 = new XLWorkbook();
wb1.AddWorksheet();
wb2.AddWorksheet();
wb1.Protect("123", Algorithm.SHA512)
.AllowElement(XLWorkbookProtectionElements.Windows)
.DisallowElement(XLWorkbookProtectionElements.Structure);
Assert.IsTrue(wb1.Protection.IsProtected);
Assert.AreEqual(XLWorkbookProtectionElements.Windows, wb1.Protection.AllowedElements);
wb2.Protection = wb1.Protection;
Assert.IsFalse(ReferenceEquals(wb1.Protection, wb2.Protection));
Assert.IsTrue(wb2.Protection.IsProtected);
Assert.AreEqual(XLWorkbookProtectionElements.Windows, wb2.Protection.AllowedElements);
Assert.AreEqual(wb1.Protection.PasswordHash, wb2.Protection.PasswordHash);
}
private Stream GetProtectedWorkbookStreamWithoutPassword() => TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"Other\Protection\protectstructurewithoutpassword.xlsx"));
private Stream GetProtectedWorkbookStreamWithPassword() => TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"Other\Protection\protectstructurewithpassword.xlsx"));
}
}