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: 10 additions & 0 deletions .github/codeql-config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,13 @@ query-filters:
id: cs/path-combine
paths:
- src/DemaConsulting.FileAssert/PathHelpers.cs
# Exclude useless-assignment warnings in generated code under obj/ directories
- exclude:
id: cs/useless-assignment-to-local
paths:
- "**/obj/**"
# Exclude missed-ternary-operator warnings in generated code under obj/ directories
- exclude:
id: cs/missed-ternary-operator
paths:
- "**/obj/**"
59 changes: 35 additions & 24 deletions src/DemaConsulting.FileAssert/Modeling/FileAssertFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
/// <param name="yamlAssert">The YAML assert unit, or null when no yaml: block is declared.</param>
/// <param name="jsonAssert">The JSON assert unit, or null when no json: block is declared.</param>
/// <param name="zipAssert">The zip assert unit, or null when no zip: block is declared.</param>
private FileAssertFile(

Check warning on line 49 in src/DemaConsulting.FileAssert/Modeling/FileAssertFile.cs

View workflow job for this annotation

GitHub Actions / Build / Build macos-latest

Constructor has 13 parameters, which is greater than the 7 authorized.

Check warning on line 49 in src/DemaConsulting.FileAssert/Modeling/FileAssertFile.cs

View workflow job for this annotation

GitHub Actions / Build / Build macos-latest

Constructor has 13 parameters, which is greater than the 7 authorized.

Check warning on line 49 in src/DemaConsulting.FileAssert/Modeling/FileAssertFile.cs

View workflow job for this annotation

GitHub Actions / Build / Build macos-latest

Constructor has 13 parameters, which is greater than the 7 authorized.

Check warning on line 49 in src/DemaConsulting.FileAssert/Modeling/FileAssertFile.cs

View workflow job for this annotation

GitHub Actions / Build / Build ubuntu-latest

Constructor has 13 parameters, which is greater than the 7 authorized.

Check warning on line 49 in src/DemaConsulting.FileAssert/Modeling/FileAssertFile.cs

View workflow job for this annotation

GitHub Actions / Build / Build ubuntu-latest

Constructor has 13 parameters, which is greater than the 7 authorized.

Check warning on line 49 in src/DemaConsulting.FileAssert/Modeling/FileAssertFile.cs

View workflow job for this annotation

GitHub Actions / Build / Build ubuntu-latest

Constructor has 13 parameters, which is greater than the 7 authorized.

Check warning on line 49 in src/DemaConsulting.FileAssert/Modeling/FileAssertFile.cs

View workflow job for this annotation

GitHub Actions / Build / Build windows-latest

Constructor has 13 parameters, which is greater than the 7 authorized.

Check warning on line 49 in src/DemaConsulting.FileAssert/Modeling/FileAssertFile.cs

View workflow job for this annotation

GitHub Actions / Build / Build windows-latest

Constructor has 13 parameters, which is greater than the 7 authorized.

Check warning on line 49 in src/DemaConsulting.FileAssert/Modeling/FileAssertFile.cs

View workflow job for this annotation

GitHub Actions / Build / Build windows-latest

Constructor has 13 parameters, which is greater than the 7 authorized.
string pattern,
int? min,
int? max,
Expand Down Expand Up @@ -239,34 +239,45 @@
{
foreach (var entryPath in files)
{
// Enforce size constraints when specified
if (MinSize.HasValue || MaxSize.HasValue)
{
var size = container.GetEntrySize(entryPath);
var displayPath = container.GetDisplayPath(entryPath);
RunEntryChecks(context, container, entryPath);
}
}
}

if (MinSize.HasValue && size < MinSize.Value)
{
context.WriteError(
$"File '{displayPath}' is {size} byte(s), which is less than the minimum {MinSize.Value} bytes");
}
/// <summary>
/// Runs size and file-type assertions for a single matched entry, reporting violations.
/// </summary>
/// <param name="context">The context used for reporting errors.</param>
/// <param name="container">The container that holds the entry.</param>
/// <param name="entryPath">The relative path of the entry within the container.</param>
private void RunEntryChecks(IContext context, IFileContainer container, string entryPath)
{
// Enforce size constraints when specified
if (MinSize.HasValue || MaxSize.HasValue)
{
var size = container.GetEntrySize(entryPath);
var displayPath = container.GetDisplayPath(entryPath);

if (MaxSize.HasValue && size > MaxSize.Value)
{
context.WriteError(
$"File '{displayPath}' is {size} byte(s), which exceeds the maximum {MaxSize.Value} bytes");
}
}
if (MinSize.HasValue && size < MinSize.Value)
{
context.WriteError(
$"File '{displayPath}' is {size} byte(s), which is less than the minimum {MinSize.Value} bytes");
}

// Delegate to each file-type assert unit when declared
TextAssert?.Run(context, container, entryPath);
PdfAssert?.Run(context, container, entryPath);
XmlAssert?.Run(context, container, entryPath);
HtmlAssert?.Run(context, container, entryPath);
YamlAssert?.Run(context, container, entryPath);
JsonAssert?.Run(context, container, entryPath);
ZipAssert?.Run(context, container, entryPath);
if (MaxSize.HasValue && size > MaxSize.Value)
{
context.WriteError(
$"File '{displayPath}' is {size} byte(s), which exceeds the maximum {MaxSize.Value} bytes");
}
}

// Delegate to each file-type assert unit when declared
TextAssert?.Run(context, container, entryPath);
PdfAssert?.Run(context, container, entryPath);
XmlAssert?.Run(context, container, entryPath);
HtmlAssert?.Run(context, container, entryPath);
YamlAssert?.Run(context, container, entryPath);
JsonAssert?.Run(context, container, entryPath);
ZipAssert?.Run(context, container, entryPath);
}
}
90 changes: 59 additions & 31 deletions src/DemaConsulting.FileAssert/Modeling/FileAssertPdfAssert.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
using DemaConsulting.FileAssert.Configuration;
using DemaConsulting.FileAssert.Utilities;
using UglyToad.PdfPig;
using UglyToad.PdfPig.Content;

namespace DemaConsulting.FileAssert.Modeling;

Expand Down Expand Up @@ -275,43 +276,70 @@

using (document)
{
// Apply metadata assertions to the document information
foreach (var rule in _metadata)
RunDocumentAssertions(context, displayPath, document);
}
}

/// <summary>
/// Applies all configured metadata, page-count, and text assertions to an already-opened
/// PDF document, reporting violations via <paramref name="context"/>.
/// </summary>
/// <param name="context">The context used for reporting errors.</param>
/// <param name="displayPath">The display path of the file, used in error messages.</param>
/// <param name="document">The opened PDF document to assert against.</param>
private void RunDocumentAssertions(IContext context, string displayPath, PdfDocument document)
{
// Apply metadata assertions to the document information
foreach (var rule in _metadata)
{
var value = GetMetadataField(document, rule.Field);
rule.Apply(context, displayPath, value);
}

// Skip page and text checks when no such constraints are configured
if (_pages == null && _text.Count == 0)
{
return;
}

// When text rules are present, materialize pages once for both count and content.
// When only a page-count constraint is configured, enumerate without allocating Page objects.
if (_text.Count > 0)
{
var pageList = document.GetPages().ToList();
_pages?.Apply(context, displayPath, pageList.Count);
var content = BuildPageText(pageList);
foreach (var rule in _text)
{
var value = GetMetadataField(document, rule.Field);
rule.Apply(context, displayPath, value);
rule.Apply(context, displayPath, content);
}
}
else
{
_pages?.Apply(context, displayPath, document.GetPages().Count());

Check warning on line 319 in src/DemaConsulting.FileAssert/Modeling/FileAssertPdfAssert.cs

View workflow job for this annotation

GitHub Actions / Build / Build macos-latest

Remove this unnecessary check for null.

Check warning on line 319 in src/DemaConsulting.FileAssert/Modeling/FileAssertPdfAssert.cs

View workflow job for this annotation

GitHub Actions / Build / Build macos-latest

Remove this unnecessary check for null.

Check warning on line 319 in src/DemaConsulting.FileAssert/Modeling/FileAssertPdfAssert.cs

View workflow job for this annotation

GitHub Actions / Build / Build macos-latest

Remove this unnecessary check for null.

Check warning on line 319 in src/DemaConsulting.FileAssert/Modeling/FileAssertPdfAssert.cs

View workflow job for this annotation

GitHub Actions / Build / Build ubuntu-latest

Remove this unnecessary check for null.

Check warning on line 319 in src/DemaConsulting.FileAssert/Modeling/FileAssertPdfAssert.cs

View workflow job for this annotation

GitHub Actions / Build / Build ubuntu-latest

Remove this unnecessary check for null.

Check warning on line 319 in src/DemaConsulting.FileAssert/Modeling/FileAssertPdfAssert.cs

View workflow job for this annotation

GitHub Actions / Build / Build ubuntu-latest

Remove this unnecessary check for null.

Check warning on line 319 in src/DemaConsulting.FileAssert/Modeling/FileAssertPdfAssert.cs

View workflow job for this annotation

GitHub Actions / Build / Build windows-latest

Remove this unnecessary check for null.

Check warning on line 319 in src/DemaConsulting.FileAssert/Modeling/FileAssertPdfAssert.cs

View workflow job for this annotation

GitHub Actions / Build / Build windows-latest

Remove this unnecessary check for null.

Check warning on line 319 in src/DemaConsulting.FileAssert/Modeling/FileAssertPdfAssert.cs

View workflow job for this annotation

GitHub Actions / Build / Build windows-latest

Remove this unnecessary check for null.
}
}

// Apply page count constraints and collect pages only when needed
if (_pages != null || _text.Count > 0)
/// <summary>
/// Concatenates the text from all pages into a single string, separating pages with newlines
/// so that text rules do not see words from adjacent pages merged together.
/// </summary>
/// <param name="pages">The ordered list of pages from the PDF document.</param>
/// <returns>A single string containing all page text joined with newline separators.</returns>
private static string BuildPageText(IReadOnlyList<Page> pages)
{
var sb = new StringBuilder();
for (var i = 0; i < pages.Count; i++)
{
if (i > 0)
{
var pageList = document.GetPages().ToList();
_pages?.Apply(context, displayPath, pageList.Count);

// Apply text rules to the extracted body text when rules are defined
if (_text.Count > 0)
{
var sb = new StringBuilder();
for (var i = 0; i < pageList.Count; i++)
{
if (i > 0)
{
// Separate page text with newlines so that text rules don't
// see two adjacent words from different pages glued together.
sb.Append('\n');
}

sb.Append(pageList[i].Text);
}

var content = sb.ToString();
foreach (var rule in _text)
{
rule.Apply(context, displayPath, content);
}
}
sb.Append('\n');
}

sb.Append(pages[i].Text);
}

return sb.ToString();
}

/// <summary>
Expand Down
25 changes: 12 additions & 13 deletions test/DemaConsulting.FileAssert.Tests/Cli/CliTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,21 +40,20 @@ public void Cli_CreateContext_ParsesSilentValidateAndLogFlags()
var logPath = tempDir.GetFilePath("out.log");

// Act - create a context with the silent, validate, and log flags
using (var context = Context.Create(
using var context = Context.Create(
[
"--silent",
"--validate",
"--log", logPath
]))
{
// Assert - all flags are reflected in the context properties
Assert.True(context.Silent);
Assert.True(context.Validate);
Assert.False(context.Version);
Assert.False(context.Help);
Assert.Equal(".fileassert.yaml", context.ConfigFile);
Assert.Equal(0, context.ExitCode);
}
]);

// Assert - all flags are reflected in the context properties
Assert.True(context.Silent);
Assert.True(context.Validate);
Assert.False(context.Version);
Assert.False(context.Help);
Assert.Equal(".fileassert.yaml", context.ConfigFile);
Assert.Equal(0, context.ExitCode);

}

Expand Down Expand Up @@ -137,8 +136,8 @@ public void Cli_OutputPipeline_WithoutSilentFlag_WritesMessagesToConsole()
// Arrange
var originalOut = Console.Out;
var originalError = Console.Error;
var outWriter = new System.IO.StringWriter();
var errorWriter = new System.IO.StringWriter();
using var outWriter = new System.IO.StringWriter();
using var errorWriter = new System.IO.StringWriter();

try
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

using System.Collections.ObjectModel;
using DemaConsulting.FileAssert.Cli;
using DemaConsulting.FileAssert.Configuration;
using DemaConsulting.FileAssert.Modeling;
Expand Down Expand Up @@ -471,7 +472,7 @@ private sealed class CapturingContext : IContext
private readonly List<string> _errors = [];

/// <summary>Gets all error messages captured since this context was created.</summary>
public IReadOnlyList<string> Errors => _errors.AsReadOnly();
public ReadOnlyCollection<string> Errors => _errors.AsReadOnly();

/// <inheritdoc/>
public void WriteLine(string message) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

using System.Collections.ObjectModel;
using DemaConsulting.FileAssert.Cli;
using DemaConsulting.FileAssert.Configuration;
using DemaConsulting.FileAssert.Modeling;
Expand Down Expand Up @@ -400,7 +401,7 @@ private sealed class CapturingContext : IContext
private readonly List<string> _errors = [];

/// <summary>Gets all error messages captured since this context was created.</summary>
public IReadOnlyList<string> Errors => _errors.AsReadOnly();
public ReadOnlyCollection<string> Errors => _errors.AsReadOnly();

/// <inheritdoc/>
public void WriteLine(string message) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

using System.Collections.ObjectModel;
using DemaConsulting.FileAssert.Cli;
using DemaConsulting.FileAssert.Configuration;
using DemaConsulting.FileAssert.Modeling;
Expand Down Expand Up @@ -454,7 +455,7 @@ private sealed class CapturingContext : IContext
private readonly List<string> _errors = [];

/// <summary>Gets all error messages captured since this context was created.</summary>
public IReadOnlyList<string> Errors => _errors.AsReadOnly();
public ReadOnlyCollection<string> Errors => _errors.AsReadOnly();

/// <inheritdoc/>
public void WriteLine(string message) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

using System.Collections.ObjectModel;
using System.IO.Compression;
using DemaConsulting.FileAssert.Cli;
using DemaConsulting.FileAssert.Configuration;
Expand Down Expand Up @@ -83,8 +84,7 @@ private static void CreateZipFile(string path, IEnumerable<string> entries)
using var archive = ZipFile.Open(path, ZipArchiveMode.Create);
foreach (var entry in entries)
{
var archiveEntry = archive.CreateEntry(entry);
using var stream = archiveEntry.Open();
using var stream = archive.CreateEntry(entry).Open();

// Write a single placeholder byte so the entry is not an empty-stream edge case
stream.WriteByte(0x00);
Expand Down Expand Up @@ -184,7 +184,7 @@ private sealed class CapturingContext : IContext
private readonly List<string> _errors = [];

/// <summary>Gets all error messages captured since this context was created.</summary>
public IReadOnlyList<string> Errors => _errors.AsReadOnly();
public ReadOnlyCollection<string> Errors => _errors.AsReadOnly();

/// <inheritdoc/>
public void WriteLine(string message) { }
Expand Down
2 changes: 1 addition & 1 deletion test/DemaConsulting.FileAssert.Tests/ProgramTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ public void Program_Run_ExplicitConfigMissing_WritesError()
Program.Run(context);

// Assert
var combined = outWriter.ToString() + errWriter.ToString();
var combined = $"{outWriter}{errWriter}";
Assert.Contains("Configuration file not found", combined);
Assert.Equal(1, context.ExitCode);
}
Expand Down
Loading