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
86 changes: 86 additions & 0 deletions ImageDescriber.Test/SearchTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Copyright (c) 2023-2026 ktsu-dev contributors

namespace ktsu.ImageDescriber.Tests;

using ktsu.ImageDescriber.Verbs;
using ktsu.Semantics.Paths;
using ktsu.Semantics.Strings;

[TestClass]
public class SearchTests
{
private PersistentState? originalSettings;

[TestInitialize]
public void Initialize() => originalSettings = Program.Settings;

[TestCleanup]
public void Cleanup() => Program.Settings = originalSettings!;

[TestMethod]
public void SearchPrintsEntryWhoseHashIsShorterThanTwelveCharacters()
{
Program.Settings = new PersistentState();
Program.Settings.Descriptions["abc123"] = new ImageDescription
{
Hash = "abc123",
Description = "a dog on a beach",
SuggestedFileName = "dog.jpg".As<FileName>(),
};

string output = CaptureConsole(() => new Search().Run(new Search { Query = "dog" }));

StringAssert.Contains(output, "Hash: abc123...");

Check warning on line 33 in ImageDescriber.Test/SearchTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.Contains' instead of 'StringAssert.Contains'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_ImageDescriber&issues=AaDcn7eyosULGQsgismx&open=AaDcn7eyosULGQsgismx&pullRequest=153
StringAssert.Contains(output, "Description: a dog on a beach");

Check warning on line 34 in ImageDescriber.Test/SearchTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.Contains' instead of 'StringAssert.Contains'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_ImageDescriber&issues=AaDcn7eyosULGQsgismy&open=AaDcn7eyosULGQsgismy&pullRequest=153
}

[TestMethod]
public void MergeEntriesSkipsEntriesWhoseHashIsNotSha256Hex()
{
Program.Settings = new PersistentState();
string validHash = new('a', ImageHasher.HashLength);
List<ImageDescription> entries =
[
new() { Hash = "abc123" },
new() { Hash = new string('z', ImageHasher.HashLength) },
new() { Hash = validHash },
];

(int NewCount, int UpdatedCount, int SkippedCount) result = default;
CaptureConsole(() => result = Import.MergeEntries(entries));

Assert.AreEqual(1, result.NewCount);
Assert.AreEqual(2, result.SkippedCount);
string[] expectedKeys = [validHash];
CollectionAssert.AreEquivalent(expectedKeys, Program.Settings.Descriptions.Keys.ToArray());

Check warning on line 55 in ImageDescriber.Test/SearchTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.AreSequenceEqual' instead of 'CollectionAssert.AreEquivalent'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_ImageDescriber&issues=AaDcn7eyosULGQsgismz&open=AaDcn7eyosULGQsgismz&pullRequest=153
}

[TestMethod]
public void IsValidHashAcceptsOnlySha256HexStrings()
{
Assert.IsTrue(ImageHasher.IsValidHash(new string('0', 64)));
Assert.IsTrue(ImageHasher.IsValidHash(string.Concat(Enumerable.Repeat("0123456789abcdefABCDEF", 3))[..64]));
Assert.IsFalse(ImageHasher.IsValidHash(null));
Assert.IsFalse(ImageHasher.IsValidHash(string.Empty));
Assert.IsFalse(ImageHasher.IsValidHash("abc123"));
Assert.IsFalse(ImageHasher.IsValidHash(new string('0', 65)));
Assert.IsFalse(ImageHasher.IsValidHash(new string('g', 64)));
}

private static string CaptureConsole(Action action)
{
TextWriter original = Console.Out;
using StringWriter writer = new();
Console.SetOut(writer);
try
{
action();
}
finally
{
Console.SetOut(original);
}

return writer.ToString();
}
}
8 changes: 8 additions & 0 deletions ImageDescriber/ImageHasher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ namespace ktsu.ImageDescriber;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;

using ktsu.Semantics.Paths;
Expand All @@ -31,6 +32,13 @@ internal static Dictionary<AbsoluteFilePath, string> HashFiles(IReadOnlyList<Abs
return new Dictionary<AbsoluteFilePath, string>(results);
}

internal const int HashLength = 64;

internal static bool IsValidHash(string? hash) =>
hash is { Length: HashLength } && hash.All(char.IsAsciiHexDigit);

internal static string ShortHash(string hash) => hash[..Math.Min(12, hash.Length)];

internal static string ComputeHash(AbsoluteFilePath filePath)
{
using FileStream stream = File.OpenRead(filePath.WeakString);
Expand Down
11 changes: 10 additions & 1 deletion ImageDescriber/Verbs/Import.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ internal override void Run(Import options)
}
}

private static (int NewCount, int UpdatedCount, int SkippedCount) MergeEntries(List<ImageDescription> entries)
internal static (int NewCount, int UpdatedCount, int SkippedCount) MergeEntries(List<ImageDescription> entries)
{
int newCount = 0;
int updatedCount = 0;
Expand All @@ -99,6 +99,15 @@ private static (int NewCount, int UpdatedCount, int SkippedCount) MergeEntries(L
continue;
}

// Every stored hash is a SHA-256 hex string, and Search and Scan both slice it, so a
// malformed one would crash them on every later run rather than just this import.
if (!ImageHasher.IsValidHash(entry.Hash))
{
Console.WriteLine($" Skipping entry with invalid hash \"{entry.Hash}\" ({entry.SuggestedFileName}): expected {ImageHasher.HashLength} hex characters.");
skippedCount++;
continue;
}

if (Program.Settings.Descriptions.TryGetValue(entry.Hash, out ImageDescription? existing))
{
if (MergeKnownPaths(entry, existing))
Expand Down
2 changes: 1 addition & 1 deletion ImageDescriber/Verbs/Search.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ internal override void Run(Search options)
foreach (ImageDescription desc in matches)
{
Console.WriteLine($" Suggested: {desc.SuggestedFileName}");
Console.WriteLine($" Hash: {desc.Hash[..12]}...");
Console.WriteLine($" Hash: {ImageHasher.ShortHash(desc.Hash)}...");
Console.WriteLine($" Date: {desc.DescribedAt:yyyy-MM-dd HH:mm:ss} UTC");
Console.WriteLine($" Paths ({desc.KnownPaths.Count}):");
foreach (AbsoluteFilePath path in desc.KnownPaths)
Expand Down
Loading