GZipFile - Support Header / Footer / WriteBom#6129
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughConsolidated "new/empty file" detection into Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
Adjusts FileTarget stream-initialization logic to avoid calling Stream.Length for GZipStream (which throws NotSupportedException), enabling GZipFileTarget to support Header, Footer, and WriteBom scenarios and adding test coverage for the behavior.
Changes:
- Updated
FileTarget.OpenNewFileStreamto treat files as “empty” on initial open whenArchiveOldFileOnStartupis enabled, avoidingStream.Lengthin those cases. - Updated header-writing condition to also short-circuit when
ArchiveOldFileOnStartupis enabled on initial open. - Added a new xUnit test verifying gzip output includes Header, message, and Footer with BOM enabled.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
src/NLog/Targets/FileTarget.cs |
Avoids Stream.Length access for initial-open + archive-on-startup scenarios (intended to prevent GZipStream.Length exceptions during open). |
tests/NLog.Targets.GZipFile.Tests/GZipFileTests.cs |
Adds a test validating gzip output when using Header/Footer and BOM. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
🧹 Nitpick comments (2)
tests/NLog.Targets.GZipFile.Tests/GZipFileTests.cs (1)
43-46: Global static setting may affect test isolation.
LogManager.ThrowExceptionsis a global static property. Setting it in the constructor affects all tests in this class, but may also bleed into other test classes if tests run in parallel within the same process. Consider resetting it in aDisposemethod (implementingIDisposable) or usingIClassFixtureto manage the setting's lifecycle.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/NLog.Targets.GZipFile.Tests/GZipFileTests.cs` around lines 43 - 46, The constructor for GZipFileTests sets the global static LogManager.ThrowExceptions which can leak across tests; modify GZipFileTests to capture the previous value, implement IDisposable (or use an IClassFixture) and in Dispose restore LogManager.ThrowExceptions to the saved previousValue so the global state is reset after the test class finishes; reference the GZipFileTests constructor, LogManager.ThrowExceptions and Dispose/IDisposable (or IClassFixture) when making the change.src/NLog/Targets/FileTarget.cs (1)
1313-1313: Consider extracting conditions to reduce cognitive complexity.This line now has 5 conditional operators, exceeding SonarCloud's maximum of 4. While functionally correct, extracting to named locals would improve readability and satisfy the static analysis rule.
♻️ Proposed refactor to reduce complexity
if (Header != null) { - bool writeHeader = (initialFileOpen && WriteHeaderWhenInitialFileNotEmpty) || ReplaceFileContentsOnEachWrite || (initialFileOpen && ArchiveOldFileOnStartup) || (fileWasEmpty ?? fileStream.Length == 0); + bool treatAsNewFile = ReplaceFileContentsOnEachWrite || (initialFileOpen && ArchiveOldFileOnStartup); + bool forceHeaderOnStart = initialFileOpen && WriteHeaderWhenInitialFileNotEmpty; + bool writeHeader = forceHeaderOnStart || treatAsNewFile || (fileWasEmpty ?? fileStream.Length == 0); if (writeHeader)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/NLog/Targets/FileTarget.cs` at line 1313, The complex boolean for writeHeader should be split into named local booleans to reduce cognitive complexity: compute separate locals such as bool isInitialOpen = initialFileOpen, bool initialNotEmptyHeader = isInitialOpen && WriteHeaderWhenInitialFileNotEmpty, bool replaceOnEachWrite = ReplaceFileContentsOnEachWrite, bool archiveOnStartup = isInitialOpen && ArchiveOldFileOnStartup, and bool isFileEmpty = fileWasEmpty ?? fileStream.Length == 0, then set writeHeader = initialNotEmptyHeader || replaceOnEachWrite || archiveOnStartup || isFileEmpty; update the assignment site (writeHeader and the referenced symbols initialFileOpen, WriteHeaderWhenInitialFileNotEmpty, ReplaceFileContentsOnEachWrite, ArchiveOldFileOnStartup, fileWasEmpty, fileStream.Length) accordingly to improve readability and satisfy SonarCloud.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src/NLog/Targets/FileTarget.cs`:
- Line 1313: The complex boolean for writeHeader should be split into named
local booleans to reduce cognitive complexity: compute separate locals such as
bool isInitialOpen = initialFileOpen, bool initialNotEmptyHeader = isInitialOpen
&& WriteHeaderWhenInitialFileNotEmpty, bool replaceOnEachWrite =
ReplaceFileContentsOnEachWrite, bool archiveOnStartup = isInitialOpen &&
ArchiveOldFileOnStartup, and bool isFileEmpty = fileWasEmpty ??
fileStream.Length == 0, then set writeHeader = initialNotEmptyHeader ||
replaceOnEachWrite || archiveOnStartup || isFileEmpty; update the assignment
site (writeHeader and the referenced symbols initialFileOpen,
WriteHeaderWhenInitialFileNotEmpty, ReplaceFileContentsOnEachWrite,
ArchiveOldFileOnStartup, fileWasEmpty, fileStream.Length) accordingly to improve
readability and satisfy SonarCloud.
In `@tests/NLog.Targets.GZipFile.Tests/GZipFileTests.cs`:
- Around line 43-46: The constructor for GZipFileTests sets the global static
LogManager.ThrowExceptions which can leak across tests; modify GZipFileTests to
capture the previous value, implement IDisposable (or use an IClassFixture) and
in Dispose restore LogManager.ThrowExceptions to the saved previousValue so the
global state is reset after the test class finishes; reference the GZipFileTests
constructor, LogManager.ThrowExceptions and Dispose/IDisposable (or
IClassFixture) when making the change.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: e2b1205d-6a9d-4f67-8921-a6b8d42621f5
📒 Files selected for processing (2)
src/NLog/Targets/FileTarget.cstests/NLog.Targets.GZipFile.Tests/GZipFileTests.cs
30fd39d to
e9422fc
Compare
There was a problem hiding this comment.
🧹 Nitpick comments (2)
tests/NLog.Targets.GZipFile.Tests/GZipFileTests.cs (1)
120-128: Make regression preconditions explicit in target setup.To avoid this test silently drifting with future default changes, set the key preconditions explicitly (especially the startup/archive/open mode that exercises this path).
🧪 Suggested test setup hardening
cfg.ForLogger().WriteTo(new GZipFileTarget() { FileName = logFileName, Layout = "${message}", LineEnding = LineEndingMode.LF, + KeepFileOpen = true, + ArchiveOldFileOnStartup = true, + CompressionLevel = CompressionLevel.Optimal, Header = "Hello World", Footer = "Goodbye World", WriteBom = true, });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/NLog.Targets.GZipFile.Tests/GZipFileTests.cs` around lines 120 - 128, The test relies on specific target startup/archive/open behavior but doesn't lock those preconditions; update the GZipFileTarget setup (the new GZipFileTarget instance used in the test where FileName, Layout, LineEnding, Header, Footer, WriteBom are set) to explicitly set the startup/archive/open mode properties so the exercised path can't change with defaults—for example set StartMode (or equivalent startup property), ArchiveMode (or archive rotation property), and file-open behavior properties such as KeepFileOpen and ConcurrentWrite (or OpenFileShare/OpenFileCache settings) to the values required by the test; ensure those properties are set on the same GZipFileTarget instance so the test remains deterministic.src/NLog/Targets/FileTarget.cs (1)
1331-1338: HardenIsNewFileEmptyagainst non-seekable streams.Line 1335's exact-type matching with
GetType()is brittle for wrapped streams. More critically, line 1337's unconditional access tofileStream.Lengthcan throwNotSupportedExceptionfor non-seekable streams (e.g.,GZipStreamfromGZipFileTarget) wheninitialFileOpen=falseduring file reopens, since the type guard at line 1335 only applies wheninitialFileOpen=true. Use capability checks (CanSeek) instead of runtime type matching.♻️ Proposed refactor
private bool IsNewFileEmpty(bool initialFileOpen, Stream fileStream) { if (ReplaceFileContentsOnEachWrite) return true; - if (initialFileOpen && ArchiveOldFileOnStartup && !typeof(FileStream).Equals(fileStream.GetType())) + if (initialFileOpen && ArchiveOldFileOnStartup && !fileStream.CanSeek) return true; + if (!fileStream.CanSeek) + return false; return fileStream.Length == 0; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/NLog/Targets/FileTarget.cs` around lines 1331 - 1338, The IsNewFileEmpty method currently uses exact type comparison and unguarded access to fileStream.Length which can throw for non-seekable or wrapped streams; update IsNewFileEmpty to return true when ReplaceFileContentsOnEachWrite is set, and when initialFileOpen is true and ArchiveOldFileOnStartup is set but the provided stream cannot seek (use fileStream.CanSeek instead of typeof(FileStream).Equals(fileStream.GetType())), and before reading Length ensure fileStream.CanSeek is true (otherwise treat as non-empty/return true as appropriate for your logic) to avoid NotSupportedException on streams like GZipStream.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src/NLog/Targets/FileTarget.cs`:
- Around line 1331-1338: The IsNewFileEmpty method currently uses exact type
comparison and unguarded access to fileStream.Length which can throw for
non-seekable or wrapped streams; update IsNewFileEmpty to return true when
ReplaceFileContentsOnEachWrite is set, and when initialFileOpen is true and
ArchiveOldFileOnStartup is set but the provided stream cannot seek (use
fileStream.CanSeek instead of typeof(FileStream).Equals(fileStream.GetType())),
and before reading Length ensure fileStream.CanSeek is true (otherwise treat as
non-empty/return true as appropriate for your logic) to avoid
NotSupportedException on streams like GZipStream.
In `@tests/NLog.Targets.GZipFile.Tests/GZipFileTests.cs`:
- Around line 120-128: The test relies on specific target startup/archive/open
behavior but doesn't lock those preconditions; update the GZipFileTarget setup
(the new GZipFileTarget instance used in the test where FileName, Layout,
LineEnding, Header, Footer, WriteBom are set) to explicitly set the
startup/archive/open mode properties so the exercised path can't change with
defaults—for example set StartMode (or equivalent startup property), ArchiveMode
(or archive rotation property), and file-open behavior properties such as
KeepFileOpen and ConcurrentWrite (or OpenFileShare/OpenFileCache settings) to
the values required by the test; ensure those properties are set on the same
GZipFileTarget instance so the test remains deterministic.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 4b55ca34-461e-4568-9ae7-a7ba5e2123c2
📒 Files selected for processing (2)
src/NLog/Targets/FileTarget.cstests/NLog.Targets.GZipFile.Tests/GZipFileTests.cs
a545d0e to
336d209
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/NLog/Targets/FileTarget.cs`:
- Around line 1331-1337: The IsNewFileEmpty method currently accesses
fileStream.Length without verifying seekability; update IsNewFileEmpty to first
check fileStream.CanSeek and handle non-seekable streams (e.g., if
(!fileStream.CanSeek) return true) before reading Length so you don't throw
NotSupportedException for streams like GZipStream; keep the existing
special-case logic (ReplaceFileContentsOnEachWrite, initialFileOpen &&
ArchiveOldFileOnStartup) and then only use fileStream.Length when
fileStream.CanSeek is true.
In `@tests/NLog.Targets.GZipFile.Tests/GZipFileTests.cs`:
- Around line 43-46: The constructor GZipFileTests currently mutates global
state by setting LogManager.ThrowExceptions = true and never restores it; modify
the GZipFileTests class to implement IDisposable, add a private bool field (e.g.
originalThrowExceptions) to capture the prior LogManager.ThrowExceptions value
in the constructor, and implement Dispose() to restore
LogManager.ThrowExceptions back to originalThrowExceptions after each test;
ensure the constructor still sets ThrowExceptions = true and Dispose restores
the saved value.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: fe9d954d-b224-4d57-aa66-d7822c7cf7df
📒 Files selected for processing (2)
src/NLog/Targets/FileTarget.cstests/NLog.Targets.GZipFile.Tests/GZipFileTests.cs
51730ef to
3929f86
Compare
3929f86 to
65e15c7
Compare
65e15c7 to
3e766ef
Compare
There was a problem hiding this comment.
🧹 Nitpick comments (2)
src/NLog/Targets/FileTarget.cs (1)
1331-1338: Consider a more defensive check for non-seekable streams.The current logic only avoids
fileStream.Lengthwhen bothArchiveOldFileOnStartupis true AND the stream is non-seekable. If a future subclass overridesCreateFileStreamto return a non-seekable stream without settingArchiveOldFileOnStartup=true, this would throwNotSupportedException.The implementation works correctly for
GZipFileTargetsince it falls back to a regularFileStreamwhenArchiveOldFileOnStartup=false. However, a more defensive approach would checkCanSeekfirst:🛡️ Optional defensive refactor
private bool IsNewFileEmpty(bool initialFileOpen, Stream fileStream) { if (ReplaceFileContentsOnEachWrite) return true; - if (ArchiveOldFileOnStartup && !fileStream.CanSeek) - return initialFileOpen; // NLog.Targets.GZipFile expects newly opened files to be empty + if (!fileStream.CanSeek) + return initialFileOpen && ArchiveOldFileOnStartup; // NLog.Targets.GZipFile expects newly opened files to be empty return fileStream.Length == 0; }This ensures
fileStream.Lengthis never accessed on non-seekable streams, returningfalse(non-empty) as a safe fallback when conditions aren't met.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/NLog/Targets/FileTarget.cs` around lines 1331 - 1338, The IsNewFileEmpty method must avoid accessing fileStream.Length on non-seekable streams; update the logic in IsNewFileEmpty to first check ReplaceFileContentsOnEachWrite (return true), then check fileStream.CanSeek and if false return ArchiveOldFileOnStartup ? initialFileOpen : false, and only if the stream is seekable evaluate ArchiveOldFileOnStartup or fileStream.Length == 0; this ensures fileStream.Length is never called for non-seekable streams (refer to method IsNewFileEmpty and flags ReplaceFileContentsOnEachWrite and ArchiveOldFileOnStartup).tests/NLog.Targets.AtomicFile.Tests/AtomFileTests.cs (1)
42-45: Global state mutation may affect test isolation.Setting
LogManager.ThrowExceptions = truein the constructor modifies global state, which could interfere with parallel test execution. Since the test creates its ownLogFactoryinstance (line 55), consider settingThrowExceptionson that specific factory instead:♻️ Preferred: Configure the specific LogFactory instance
public AtomFileTests() { - LogManager.ThrowExceptions = true; } [Fact] public void SimpleAtomicFileStream_HeaderFooterBom() { // ... try { - var logFactory = new LogFactory().Setup().LoadConfiguration(cfg => + var logFactory = new LogFactory(); + logFactory.ThrowExceptions = true; + logFactory.Setup().LoadConfiguration(cfg =>Alternatively, if
LogManager.ThrowExceptionsis intentional for all tests, consider implementingIDisposableto restore the original value after tests complete.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/NLog.Targets.AtomicFile.Tests/AtomFileTests.cs` around lines 42 - 45, The test constructor currently sets the global LogManager.ThrowExceptions = true which mutates global state; instead, remove that global assignment in AtomFileTests() and set the ThrowExceptions flag on the specific LogFactory instance you create for the tests (the LogFactory created in the test setup) by assigning factory.ThrowExceptions = true right after constructing the LogFactory; if the global change was intentional for all tests, implement IDisposable on AtomFileTests to save the original LogManager.ThrowExceptions value in the constructor and restore it in Dispose() to avoid leaking global state across tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src/NLog/Targets/FileTarget.cs`:
- Around line 1331-1338: The IsNewFileEmpty method must avoid accessing
fileStream.Length on non-seekable streams; update the logic in IsNewFileEmpty to
first check ReplaceFileContentsOnEachWrite (return true), then check
fileStream.CanSeek and if false return ArchiveOldFileOnStartup ? initialFileOpen
: false, and only if the stream is seekable evaluate ArchiveOldFileOnStartup or
fileStream.Length == 0; this ensures fileStream.Length is never called for
non-seekable streams (refer to method IsNewFileEmpty and flags
ReplaceFileContentsOnEachWrite and ArchiveOldFileOnStartup).
In `@tests/NLog.Targets.AtomicFile.Tests/AtomFileTests.cs`:
- Around line 42-45: The test constructor currently sets the global
LogManager.ThrowExceptions = true which mutates global state; instead, remove
that global assignment in AtomFileTests() and set the ThrowExceptions flag on
the specific LogFactory instance you create for the tests (the LogFactory
created in the test setup) by assigning factory.ThrowExceptions = true right
after constructing the LogFactory; if the global change was intentional for all
tests, implement IDisposable on AtomFileTests to save the original
LogManager.ThrowExceptions value in the constructor and restore it in Dispose()
to avoid leaking global state across tests.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 5842a025-1254-4b7a-b71e-5bca4ba095a4
📒 Files selected for processing (3)
src/NLog/Targets/FileTarget.cstests/NLog.Targets.AtomicFile.Tests/AtomFileTests.cstests/NLog.Targets.GZipFile.Tests/GZipFileTests.cs
🚧 Files skipped from review as they are similar to previous changes (1)
- tests/NLog.Targets.GZipFile.Tests/GZipFileTests.cs
|
|



Trying to resolve #6128