From ac41396eed12e437ac5e1665ddea0c32f3c5a940 Mon Sep 17 00:00:00 2001 From: ArisOIKON Date: Wed, 1 Apr 2026 23:36:44 +0300 Subject: [PATCH 1/6] fixed dependency issue on FastExcelReaderTest --- .vscode/settings.json | 3 +++ fastexcel-reader/src/main/java/module-info.java | 1 + pom.xml | 5 +++++ 3 files changed, 9 insertions(+) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..c5f3f6b9 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "java.configuration.updateBuildConfiguration": "interactive" +} \ No newline at end of file diff --git a/fastexcel-reader/src/main/java/module-info.java b/fastexcel-reader/src/main/java/module-info.java index 4497651d..ab3d2d3e 100644 --- a/fastexcel-reader/src/main/java/module-info.java +++ b/fastexcel-reader/src/main/java/module-info.java @@ -1,5 +1,6 @@ module org.dhatim.fastexcel.reader { requires java.xml; + requires java.logging; requires org.apache.commons.compress; requires com.fasterxml.aalto; diff --git a/pom.xml b/pom.xml index 173462d0..74b045e9 100644 --- a/pom.xml +++ b/pom.xml @@ -193,6 +193,11 @@ maven-surefire-plugin false + + @{argLine} + --add-opens java.base/java.util.logging=ALL-UNNAMED + --add-opens java.logging/java.util.logging=ALL-UNNAMED + From 3baef9e18dccce9f4da8cd794a726aad34cc3bf6 Mon Sep 17 00:00:00 2001 From: ArisOIKON Date: Wed, 27 May 2026 21:27:50 +0300 Subject: [PATCH 2/6] removed .vscode file --- .vscode/settings.json | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index c5f3f6b9..00000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "java.configuration.updateBuildConfiguration": "interactive" -} \ No newline at end of file From 8ac6346d9afcffbbc1c5e76c23ab2862df4e26a8 Mon Sep 17 00:00:00 2001 From: ArisOIKON Date: Thu, 28 May 2026 01:49:25 +0300 Subject: [PATCH 3/6] Implemented the "view password" feature, while adding e2e test coverage --- .../dhatim/fastexcel/ViewPasswordE2ETest.java | 124 ++++++++++++++++++ .../fastexcel/WorkbookProtectionTest.java | 92 +++++++++++++ .../java/org/dhatim/fastexcel/Workbook.java | 61 +++++++-- .../java/org/dhatim/fastexcel/Worksheet.java | 12 ++ 4 files changed, 280 insertions(+), 9 deletions(-) create mode 100644 e2e/src/test/java/org/dhatim/fastexcel/ViewPasswordE2ETest.java create mode 100644 e2e/src/test/java/org/dhatim/fastexcel/WorkbookProtectionTest.java diff --git a/e2e/src/test/java/org/dhatim/fastexcel/ViewPasswordE2ETest.java b/e2e/src/test/java/org/dhatim/fastexcel/ViewPasswordE2ETest.java new file mode 100644 index 00000000..d05dbd79 --- /dev/null +++ b/e2e/src/test/java/org/dhatim/fastexcel/ViewPasswordE2ETest.java @@ -0,0 +1,124 @@ +package org.dhatim.fastexcel; + +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.dhatim.fastexcel.reader.ReadableWorkbook; +import org.dhatim.fastexcel.reader.Row; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ViewPasswordE2ETest { + + @Test + void testSheetIsHiddenAfterProtectWithViewPassword() throws IOException { + ByteArrayOutputStream os = new ByteArrayOutputStream(); + try (Workbook wb = new Workbook(os, "Test", "1.0")) { + Worksheet ws = wb.newWorksheet("SecretSheet"); + ws.value(0, 0, "Sensitive Data"); + ws.value(1, 0, "More Sensitive Data"); + ws.protectWithViewPassword("viewPassword"); + } + + byte[] bytes = os.toByteArray(); + + // Verify sheet is hidden via Apache POI + try (XSSFWorkbook poiWb = new XSSFWorkbook(new ByteArrayInputStream(bytes))) { + assertTrue(poiWb.isStructureLocked()); + } + } + + @Test + void testWorkbookStructureIsLockedAfterProtectWithViewPassword() throws IOException { + ByteArrayOutputStream os = new ByteArrayOutputStream(); + try (Workbook wb = new Workbook(os, "Test", "1.0")) { + Worksheet ws = wb.newWorksheet("SecretSheet"); + ws.value(0, 0, "Sensitive Data"); + ws.protectWithViewPassword("viewPassword"); + } + + byte[] bytes = os.toByteArray(); + + // Verify workbook structure is locked via Apache POI + try (XSSFWorkbook poiWb = new XSSFWorkbook(new ByteArrayInputStream(bytes))) { + assertThat(poiWb.isStructureLocked()).isTrue(); + } + } + + @Test + void testDataIsPreservedAfterProtectWithViewPassword() throws IOException { + ByteArrayOutputStream os = new ByteArrayOutputStream(); + try (Workbook wb = new Workbook(os, "Test", "1.0")) { + Worksheet ws = wb.newWorksheet("SecretSheet"); + ws.value(0, 0, "Sensitive Data"); + ws.value(1, 0, "More Sensitive Data"); + ws.protectWithViewPassword("viewPassword"); + } + + byte[] bytes = os.toByteArray(); + + // Verify data is still readable via fastexcel reader + try (ReadableWorkbook rwb = new ReadableWorkbook(new ByteArrayInputStream(bytes))) { + try (Stream rows = rwb.getFirstSheet().openStream()) { + List values = rows + .map(r -> r.getCellAsString(0).orElse("")) + .collect(Collectors.toList()); + assertThat(values).containsExactly("Sensitive Data", "More Sensitive Data"); + } + } + } + + @Test + void testOnlyProtectedSheetIsHidden() throws IOException { + ByteArrayOutputStream os = new ByteArrayOutputStream(); + try (Workbook wb = new Workbook(os, "Test", "1.0")) { + Worksheet secretSheet = wb.newWorksheet("SecretSheet"); + secretSheet.value(0, 0, "Sensitive Data"); + secretSheet.protectWithViewPassword("viewPassword"); + + // Add a second visible sheet + Worksheet publicSheet = wb.newWorksheet("PublicSheet"); + publicSheet.value(0, 0, "Public Data"); + } + + byte[] bytes = os.toByteArray(); + + try (XSSFWorkbook poiWb = new XSSFWorkbook(new ByteArrayInputStream(bytes))) { + // First sheet (SecretSheet) should be hidden + assertThat(poiWb.isSheetHidden(0)).isTrue(); + // Second sheet (PublicSheet) should be visible + assertThat(poiWb.isSheetHidden(1)).isFalse(); + } + } + + @Test + void testProtectWithViewPasswordAndEditPassword() throws IOException { + ByteArrayOutputStream os = new ByteArrayOutputStream(); + try (Workbook wb = new Workbook(os, "Test", "1.0")) { + Worksheet ws = wb.newWorksheet("SecretSheet"); + ws.value(0, 0, "Sensitive Data"); + // Protect viewing + ws.protectWithViewPassword("viewPassword"); + // Also protect editing + ws.protect("editPassword"); + } + + byte[] bytes = os.toByteArray(); + + try (XSSFWorkbook poiWb = new XSSFWorkbook(new ByteArrayInputStream(bytes))) { + // Sheet should be hidden + assertThat(poiWb.isSheetHidden(0)).isTrue(); + // Workbook structure should be locked + assertThat(poiWb.isStructureLocked()).isTrue(); + // Sheet should be protected + assertThat(poiWb.getSheetAt(0).getProtect()).isTrue(); + } + } +} \ No newline at end of file diff --git a/e2e/src/test/java/org/dhatim/fastexcel/WorkbookProtectionTest.java b/e2e/src/test/java/org/dhatim/fastexcel/WorkbookProtectionTest.java new file mode 100644 index 00000000..1cdf0c47 --- /dev/null +++ b/e2e/src/test/java/org/dhatim/fastexcel/WorkbookProtectionTest.java @@ -0,0 +1,92 @@ +package org.dhatim.fastexcel; + +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; + +import java.io.*; + +import static org.junit.jupiter.api.Assertions.*; + +public class WorkbookProtectionTest { + + private static final File testFile = new File("target/workbookProtectionTest.xlsx"); + + private static final String testPassword = "myPassword"; + + private static final String testContent = "Hello fastexcel"; + + // ── Write helpers ──────────────────────────────────────────────────────── + + void fastexcelWriteWithStructureProtection() throws IOException { + try (FileOutputStream fos = new FileOutputStream(testFile); + Workbook wb = new Workbook(fos, "Test", "1.0")) { + Worksheet ws = wb.newWorksheet("Sheet1"); + ws.value(0, 0, testContent); + wb.protectStructure(testPassword); + } + } + + void fastexcelWriteWithoutStructureProtection() throws IOException { + try (FileOutputStream fos = new FileOutputStream(testFile); + Workbook wb = new Workbook(fos, "Test", "1.0")) { + Worksheet ws = wb.newWorksheet("Sheet1"); + ws.value(0, 0, testContent); + } + } + + void fastexcelWriteWithNullPassword() throws IOException { + try (FileOutputStream fos = new FileOutputStream(testFile); + Workbook wb = new Workbook(fos, "Test", "1.0")) { + Worksheet ws = wb.newWorksheet("Sheet1"); + ws.value(0, 0, testContent); + wb.protectStructure(testPassword); // set password + wb.protectStructure(null); // then remove it + } + } + + // ── Read helpers ───────────────────────────────────────────────────────── + + void poiVerifyStructureIsLocked() throws IOException { + try (FileInputStream fis = new FileInputStream(testFile); + XSSFWorkbook poiWb = new XSSFWorkbook(fis)) { + assertTrue(poiWb.isStructureLocked(), + "Workbook structure should be locked"); + } + } + + void poiVerifyStructureIsNotLocked() throws IOException { + try (FileInputStream fis = new FileInputStream(testFile); + XSSFWorkbook poiWb = new XSSFWorkbook(fis)) { + assertFalse(poiWb.isStructureLocked(), + "Workbook structure should not be locked"); + } + } + + // ── Cleanup ────────────────────────────────────────────────────────────── + + @AfterAll + static void cleanup() { + testFile.delete(); + } + + // ── Tests ──────────────────────────────────────────────────────────────── + + @Test + void fastexcelWrite_poiVerifyStructureLocked() throws Exception { + fastexcelWriteWithStructureProtection(); + poiVerifyStructureIsLocked(); + } + + @Test + void fastexcelWrite_poiVerifyStructureNotLocked() throws Exception { + fastexcelWriteWithoutStructureProtection(); + poiVerifyStructureIsNotLocked(); + } + + @Test + void fastexcelWrite_nullPassword_poiVerifyStructureNotLocked() throws Exception { + fastexcelWriteWithNullPassword(); + poiVerifyStructureIsNotLocked(); + } +} \ No newline at end of file diff --git a/fastexcel-writer/src/main/java/org/dhatim/fastexcel/Workbook.java b/fastexcel-writer/src/main/java/org/dhatim/fastexcel/Workbook.java index b3c7cb34..d79f39d7 100644 --- a/fastexcel-writer/src/main/java/org/dhatim/fastexcel/Workbook.java +++ b/fastexcel-writer/src/main/java/org/dhatim/fastexcel/Workbook.java @@ -38,6 +38,7 @@ public class Workbook implements Closeable { private int activeTab = 0; private boolean finished = false; + private String workbookPasswordHash; private final String applicationName; private final String applicationVersion; private final List worksheets = new ArrayList<>(); @@ -92,6 +93,41 @@ public void setCompressionLevel(int level) { public void setActiveTab(int tabIndex) { this.activeTab = tabIndex; } + /** + * Protects the workbook structure with a password. + * Prevents users from unhiding, adding, moving, or deleting sheets. + * (Note that this is not very secure and only meant for discouraging changes. Same amount of + * protection as the edit password for worksheets.) + * @param password The password to use. + */ + public void protectStructure(String password) { + this.workbookPasswordHash = password != null ? hashPassword(password) : null; + } + + /** + * Hash the password using the same algorithm as worksheet protection. + * @param password The password to hash. + * @return The password hash as a hex string. + */ + private static String hashPassword(String password) { + byte[] passwordCharacters = password.getBytes(); + int hash = 0; + if (passwordCharacters.length > 0) { + int charIndex = passwordCharacters.length; + while (charIndex-- > 0) { + hash = ((hash >> 14) & 0x01) | ((hash << 1) & 0x7fff); + hash ^= passwordCharacters[charIndex]; + } + hash = ((hash >> 14) & 0x01) | ((hash << 1) & 0x7fff); + hash ^= passwordCharacters.length; + hash ^= (0x8000 | ('N' << 8) | 'K'); + } + return Integer.toHexString(hash & 0xffff); + } + + + + public void setGlobalDefaultFont(String fontName, double fontSize) { this.setGlobalDefaultFont(Font.build(null, null, null, fontName, BigDecimal.valueOf(fontSize), null, null)); @@ -313,15 +349,22 @@ private Set collectUsedImageTypes() { */ private void writeWorkbookFile() throws IOException { writeFile("xl/workbook.xml", w -> { - w.append("" + - "" + - "" + - "" + - "" + - "" + - ""); + w.append("" + + "" + + ""); + + if (workbookPasswordHash != null) { + w.append(""); + } + + w.append("" + + "" + + "" + + ""); for (Worksheet ws : worksheets) { writeWorkbookSheet(w, ws); diff --git a/fastexcel-writer/src/main/java/org/dhatim/fastexcel/Worksheet.java b/fastexcel-writer/src/main/java/org/dhatim/fastexcel/Worksheet.java index 108bef22..bad3a6a2 100644 --- a/fastexcel-writer/src/main/java/org/dhatim/fastexcel/Worksheet.java +++ b/fastexcel-writer/src/main/java/org/dhatim/fastexcel/Worksheet.java @@ -537,6 +537,18 @@ public void protect(String password, Set options) { this.sheetProtectionOptions = options; this.passwordHash = hashPassword(password); } + /** + * Protects the sheet from viewing by hiding it and locking + * the workbook structure with a password. + * Unauthorized users will not be able to unhide the sheet + * without the correct password. + * (Note that this is not very secure and only meant for discouraging changes.) + * @param password The password required to unhide the sheet. + */ + public void protectWithViewPassword(String password) { + this.setVisibilityState(VisibilityState.HIDDEN); + this.workbook.protectStructure(password); + } /** * Applies autofilter specifically to the given cell range From 39f6ad0ae40ec308c1ee1d590b7131a677f003a3 Mon Sep 17 00:00:00 2001 From: ArisOIKON Date: Mon, 8 Jun 2026 03:14:13 +0300 Subject: [PATCH 4/6] Updating README file Added instructions for protecting a worksheet from viewing in the README. --- README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/README.md b/README.md index 38775597..45634ba0 100644 --- a/README.md +++ b/README.md @@ -402,3 +402,21 @@ protected excel documents, but it can be realized by combining `poi` and `poi-oo This test class is a reference implementation : [EncryptionTest](./e2e/src/test/java/org/dhatim/fastexcel/EncryptionTest.java) +### Protect a worksheet from viewing + +A worksheet can be hidden and the workbook structure can be protected with a password using `protectWithViewPassword`. + +```java +try (OutputStream os = new FileOutputStream("protected.xlsx"); + Workbook wb = new Workbook(os, "Application", "1.0")) { + + Worksheet ws = wb.newWorksheet("SecretSheet"); + ws.value(0, 0, "Sensitive Data"); + + ws.protectWithViewPassword("viewPassword"); +} +``` + +This hides the worksheet and protects the workbook structure, so users cannot unhide, move, rename, or delete sheets without the workbook structure password. + +Note: this is different from `protect(...)`, which protects a worksheet from editing. `protectWithViewPassword(...)` is intended to restrict viewing by hiding the worksheet and protecting the workbook structure. From 8acd26e86667d9886091785b68bd322de8d27db9 Mon Sep 17 00:00:00 2001 From: ArisOIKON Date: Mon, 8 Jun 2026 03:23:28 +0300 Subject: [PATCH 5/6] removed duplicate in reader module-info --- fastexcel-reader/src/main/java/module-info.java | 1 - 1 file changed, 1 deletion(-) diff --git a/fastexcel-reader/src/main/java/module-info.java b/fastexcel-reader/src/main/java/module-info.java index bfd13c18..7ecee45d 100644 --- a/fastexcel-reader/src/main/java/module-info.java +++ b/fastexcel-reader/src/main/java/module-info.java @@ -3,6 +3,5 @@ requires java.logging; requires org.apache.commons.compress; requires com.fasterxml.aalto; - requires java.logging; exports org.dhatim.fastexcel.reader; } \ No newline at end of file From a939ff59a24ebca2fc1884aeb34119f67981777f Mon Sep 17 00:00:00 2001 From: ArisOIKON Date: Mon, 8 Jun 2026 04:10:33 +0300 Subject: [PATCH 6/6] tests for coverage --- .../java/org/dhatim/fastexcel/WriterTest.java | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/fastexcel-writer/src/test/java/org/dhatim/fastexcel/WriterTest.java b/fastexcel-writer/src/test/java/org/dhatim/fastexcel/WriterTest.java index 7c669430..0f0035f8 100644 --- a/fastexcel-writer/src/test/java/org/dhatim/fastexcel/WriterTest.java +++ b/fastexcel-writer/src/test/java/org/dhatim/fastexcel/WriterTest.java @@ -15,9 +15,13 @@ */ package org.dhatim.fastexcel; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.junit.jupiter.api.Test; +import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.IOException; + import static org.assertj.core.api.Assertions.assertThat; class WriterTest { @@ -42,5 +46,56 @@ void testEscapingInvalidCharacters() throws Exception { String s = baos.toString("UTF-8"); assertThat(s).isEqualTo("some characters are ignored: or "); } + @Test + void protectStructureLocksWorkbookStructure() throws IOException { + ByteArrayOutputStream os = new ByteArrayOutputStream(); + + try (Workbook wb = new Workbook(os, "Test", "1.0")) { + Worksheet ws = wb.newWorksheet("Sheet1"); + ws.value(0, 0, "Hello"); + + wb.protectStructure("myPassword"); + } + + try (XSSFWorkbook poiWb = new XSSFWorkbook( + new ByteArrayInputStream(os.toByteArray()))) { + assertThat(poiWb.isStructureLocked()).isTrue(); + } + } + @Test + void protectStructureWithNullPasswordDoesNotLockWorkbookStructure() throws IOException { + ByteArrayOutputStream os = new ByteArrayOutputStream(); + + try (Workbook wb = new Workbook(os, "Test", "1.0")) { + Worksheet ws = wb.newWorksheet("Sheet1"); + ws.value(0, 0, "Hello"); + + wb.protectStructure(null); + } + + try (XSSFWorkbook poiWb = new XSSFWorkbook( + new ByteArrayInputStream(os.toByteArray()))) { + assertThat(poiWb.isStructureLocked()).isFalse(); + } + } + @Test + void protectWithViewPasswordOnlyHidesTargetSheet() throws IOException { + ByteArrayOutputStream os = new ByteArrayOutputStream(); + + try (Workbook wb = new Workbook(os, "Test", "1.0")) { + Worksheet secretSheet = wb.newWorksheet("SecretSheet"); + secretSheet.value(0, 0, "Sensitive Data"); + secretSheet.protectWithViewPassword("viewPassword"); + + Worksheet publicSheet = wb.newWorksheet("PublicSheet"); + publicSheet.value(0, 0, "Public Data"); + } + + try (XSSFWorkbook poiWb = new XSSFWorkbook( + new ByteArrayInputStream(os.toByteArray()))) { + assertThat(poiWb.isSheetHidden(0)).isTrue(); + assertThat(poiWb.isSheetHidden(1)).isFalse(); + } + } }