From 652221d03100933cba34f43b6002e0848b1b2ac8 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 25 May 2026 11:14:17 +0000 Subject: [PATCH 1/5] Ship XML documentation file in NuGet package Enable GenerateDocumentationFile so the heavily-documented public API provides IntelliSense to consumers of the package. https://claude.ai/code/session_01AikHWpKd7zQCfPDJcE2ZkX --- PasswordGenerator/PasswordGenerator.csproj | 3 +++ 1 file changed, 3 insertions(+) diff --git a/PasswordGenerator/PasswordGenerator.csproj b/PasswordGenerator/PasswordGenerator.csproj index ae8ce7a..a4c08df 100644 --- a/PasswordGenerator/PasswordGenerator.csproj +++ b/PasswordGenerator/PasswordGenerator.csproj @@ -5,6 +5,9 @@ enable latest + + true + 3.0.0 3.0.0.0 From 1ea24ef94adc8e66d6438a3cd77a7697be3932a8 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 25 May 2026 13:42:52 +0000 Subject: [PATCH 2/5] Use ValueTask and cancelled-task semantics for async APIs Switch NextAsync/GenerateAsync to ValueTask so the synchronous completion path allocates no Task, and surface cancellation as a cancelled task (ValueTask.FromCanceled) instead of throwing synchronously, so the result composes correctly when not awaited immediately. Argument validation still throws synchronously, matching BCL conventions. https://claude.ai/code/session_01AikHWpKd7zQCfPDJcE2ZkX --- PasswordGenerator.Tests/Phase3Tests.cs | 24 ++++++++++++++++++++++++ PasswordGenerator/IPasswordGenerator.cs | 12 +++++++----- PasswordGenerator/PassphraseGenerator.cs | 19 ++++++++++++------- PasswordGenerator/Password.cs | 19 ++++++++++++------- docs/api-surface.md | 2 +- 5 files changed, 56 insertions(+), 20 deletions(-) diff --git a/PasswordGenerator.Tests/Phase3Tests.cs b/PasswordGenerator.Tests/Phase3Tests.cs index 095e3a0..2758096 100644 --- a/PasswordGenerator.Tests/Phase3Tests.cs +++ b/PasswordGenerator.Tests/Phase3Tests.cs @@ -29,6 +29,30 @@ public void NextAsync_WithAlreadyCancelledToken_Throws() Throws.InstanceOf()); } + [Test] + public void NextAsync_WithCancelledToken_SurfacesCancellationThroughTask() + { + var pwd = new Password(20); + var cts = new CancellationTokenSource(); + cts.Cancel(); + + // Cancellation must come back through the returned task, not as a synchronous throw, + // so the result composes correctly when not awaited immediately. + var task = pwd.NextAsync(cts.Token); + Assert.That(task.IsCanceled, Is.True); + } + + [Test] + public void GenerateAsync_WithCancelledToken_SurfacesCancellationThroughTask() + { + var pwd = new Password(16); + var cts = new CancellationTokenSource(); + cts.Cancel(); + + var task = pwd.GenerateAsync(10, cts.Token); + Assert.That(task.IsCanceled, Is.True); + } + [Test] public void Generate_ReturnsRequestedCount() { diff --git a/PasswordGenerator/IPasswordGenerator.cs b/PasswordGenerator/IPasswordGenerator.cs index b842dc4..e3651c9 100644 --- a/PasswordGenerator/IPasswordGenerator.cs +++ b/PasswordGenerator/IPasswordGenerator.cs @@ -17,10 +17,12 @@ public interface IPasswordGenerator bool TryNext(out string? password); /// - /// Generates a single password. Generation is CPU-bound; this overload exists for ergonomics - /// and to honour cancellation, not to offload work to another thread. + /// Generates a single password. Generation is CPU-bound and completes synchronously; this overload + /// exists for ergonomics and to honour cancellation, not to offload work to another thread. A + /// is used because the result is always available synchronously. + /// If is already cancelled, the returned task is cancelled. /// - Task NextAsync(CancellationToken cancellationToken = default); + ValueTask NextAsync(CancellationToken cancellationToken = default); /// Generates the default number of passwords (configurable; one unless overridden). IReadOnlyList Generate(); @@ -29,9 +31,9 @@ public interface IPasswordGenerator IReadOnlyList Generate(int count); /// Generates the default number of passwords, observing . - Task> GenerateAsync(CancellationToken cancellationToken = default); + ValueTask> GenerateAsync(CancellationToken cancellationToken = default); /// Generates passwords, observing . - Task> GenerateAsync(int count, CancellationToken cancellationToken = default); + ValueTask> GenerateAsync(int count, CancellationToken cancellationToken = default); } } diff --git a/PasswordGenerator/PassphraseGenerator.cs b/PasswordGenerator/PassphraseGenerator.cs index e4ea7b7..4f1a920 100644 --- a/PasswordGenerator/PassphraseGenerator.cs +++ b/PasswordGenerator/PassphraseGenerator.cs @@ -73,10 +73,11 @@ public bool TryNext(out string? password) return true; } - public Task NextAsync(CancellationToken cancellationToken = default) + public ValueTask NextAsync(CancellationToken cancellationToken = default) { - cancellationToken.ThrowIfCancellationRequested(); - return Task.FromResult(Next()); + return cancellationToken.IsCancellationRequested + ? ValueTask.FromCanceled(cancellationToken) + : new ValueTask(Next()); } public IReadOnlyList Generate() @@ -96,24 +97,28 @@ public IReadOnlyList Generate(int count) return passphrases; } - public Task> GenerateAsync(CancellationToken cancellationToken = default) + public ValueTask> GenerateAsync(CancellationToken cancellationToken = default) { return GenerateAsync(DefaultBatchCount, cancellationToken); } - public Task> GenerateAsync(int count, CancellationToken cancellationToken = default) + public ValueTask> GenerateAsync(int count, CancellationToken cancellationToken = default) { if (count < 0) throw new ArgumentOutOfRangeException(nameof(count), "count cannot be negative."); + if (cancellationToken.IsCancellationRequested) + return ValueTask.FromCanceled>(cancellationToken); + var passphrases = new List(count); for (var i = 0; i < count; i++) { - cancellationToken.ThrowIfCancellationRequested(); + if (cancellationToken.IsCancellationRequested) + return ValueTask.FromCanceled>(cancellationToken); passphrases.Add(Next()); } - return Task.FromResult>(passphrases); + return new ValueTask>(passphrases); } /// Estimates passphrase entropy in bits from the word-list size, word count, and trailing number. diff --git a/PasswordGenerator/Password.cs b/PasswordGenerator/Password.cs index 4e44763..a9d5b32 100644 --- a/PasswordGenerator/Password.cs +++ b/PasswordGenerator/Password.cs @@ -187,10 +187,11 @@ public IEnumerable NextGroup(int numberOfPasswordsToGenerate) return Generate(numberOfPasswordsToGenerate); } - public Task NextAsync(CancellationToken cancellationToken = default) + public ValueTask NextAsync(CancellationToken cancellationToken = default) { - cancellationToken.ThrowIfCancellationRequested(); - return Task.FromResult(Next()); + return cancellationToken.IsCancellationRequested + ? ValueTask.FromCanceled(cancellationToken) + : new ValueTask(Next()); } public IReadOnlyList Generate() @@ -210,24 +211,28 @@ public IReadOnlyList Generate(int count) return passwords; } - public Task> GenerateAsync(CancellationToken cancellationToken = default) + public ValueTask> GenerateAsync(CancellationToken cancellationToken = default) { return GenerateAsync(DefaultBatchCount, cancellationToken); } - public Task> GenerateAsync(int count, CancellationToken cancellationToken = default) + public ValueTask> GenerateAsync(int count, CancellationToken cancellationToken = default) { if (count < 0) throw new ArgumentOutOfRangeException(nameof(count), "count cannot be negative."); + if (cancellationToken.IsCancellationRequested) + return ValueTask.FromCanceled>(cancellationToken); + var passwords = new List(count); for (var i = 0; i < count; i++) { - cancellationToken.ThrowIfCancellationRequested(); + if (cancellationToken.IsCancellationRequested) + return ValueTask.FromCanceled>(cancellationToken); passwords.Add(Next()); } - return Task.FromResult>(passwords); + return new ValueTask>(passwords); } private static readonly CharacterClass[] OrderedClasses = diff --git a/docs/api-surface.md b/docs/api-surface.md index 61723f4..7e3fffc 100644 --- a/docs/api-surface.md +++ b/docs/api-surface.md @@ -28,7 +28,7 @@ flowchart TD subgraph Gen["Generation (IPasswordGenerator)"] g1["Next() : string (throws on bad config)"] g2["TryNext(out string) : bool"] - g3["NextAsync(ct) : Task~string~"] + g3["NextAsync(ct) : ValueTask~string~"] g4["Generate() / Generate(count)"] g5["GenerateAsync() / GenerateAsync(count, ct)"] end From 9176697038a1cf4206a036c3312f9dcbe89d1919 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 25 May 2026 14:41:08 +0000 Subject: [PATCH 3/5] Fix benchmark build against ValueTask async APIs and silence CS1591 The async APIs now return ValueTask, so the AsyncBenchmarks methods must return ValueTask too (the Task return types no longer compile on either target framework). Add CS1591 to NoWarn so enabling the XML doc file does not warn on intentionally-undocumented public members. https://claude.ai/code/session_01WVsnjDXjACmcLWJLGNMKVL --- PasswordGenerator.Benchmarks/Benchmarks/AsyncBenchmarks.cs | 4 ++-- PasswordGenerator/PasswordGenerator.csproj | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/PasswordGenerator.Benchmarks/Benchmarks/AsyncBenchmarks.cs b/PasswordGenerator.Benchmarks/Benchmarks/AsyncBenchmarks.cs index a0ab1ef..05b597d 100644 --- a/PasswordGenerator.Benchmarks/Benchmarks/AsyncBenchmarks.cs +++ b/PasswordGenerator.Benchmarks/Benchmarks/AsyncBenchmarks.cs @@ -26,13 +26,13 @@ public void Cleanup() } [Benchmark] - public Task NextAsync() + public ValueTask NextAsync() { return _password.NextAsync(); } [Benchmark] - public Task> GenerateAsync() + public ValueTask> GenerateAsync() { return _password.GenerateAsync(Count); } diff --git a/PasswordGenerator/PasswordGenerator.csproj b/PasswordGenerator/PasswordGenerator.csproj index a4c08df..4c607a6 100644 --- a/PasswordGenerator/PasswordGenerator.csproj +++ b/PasswordGenerator/PasswordGenerator.csproj @@ -7,6 +7,8 @@ true + + $(NoWarn);CS1591 3.0.0 From 76fe0e6bda95ba42aa79a07fe5196cf344357a86 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 25 May 2026 14:46:21 +0000 Subject: [PATCH 4/5] Document the public API instead of suppressing CS1591 With the XML doc file enabled, add XML comments to every public type and member so the shipped documentation is complete and CS1591 no longer warns. Interface implementations use ; the NoWarn CS1591 suppression added earlier is removed. https://claude.ai/code/session_01WVsnjDXjACmcLWJLGNMKVL --- PasswordGenerator/CharacterClass.cs | 7 +++ PasswordGenerator/CryptoRandomSource.cs | 9 ++++ PasswordGenerator/IPassword.cs | 35 +++++++++++++++ PasswordGenerator/IPasswordSettings.cs | 34 ++++++++++++++ PasswordGenerator/PassphraseGenerator.cs | 25 +++++++++++ PasswordGenerator/Password.cs | 41 +++++++++++++++++ PasswordGenerator/PasswordGenerator.csproj | 2 - PasswordGenerator/PasswordOptions.cs | 8 ++++ PasswordGenerator/PasswordSettings.cs | 52 ++++++++++++++++++++++ PasswordGenerator/PoolEntropyEstimator.cs | 9 ++++ 10 files changed, 220 insertions(+), 2 deletions(-) diff --git a/PasswordGenerator/CharacterClass.cs b/PasswordGenerator/CharacterClass.cs index ea525c7..e5947f3 100644 --- a/PasswordGenerator/CharacterClass.cs +++ b/PasswordGenerator/CharacterClass.cs @@ -6,9 +6,16 @@ namespace PasswordGenerator /// public enum CharacterClass { + /// Lowercase letters (a–z). Lowercase, + + /// Uppercase letters (A–Z). Uppercase, + + /// Digits (0–9). Numeric, + + /// Special / symbol characters. Special } } diff --git a/PasswordGenerator/CryptoRandomSource.cs b/PasswordGenerator/CryptoRandomSource.cs index 93a220e..d3f7ace 100644 --- a/PasswordGenerator/CryptoRandomSource.cs +++ b/PasswordGenerator/CryptoRandomSource.cs @@ -10,6 +10,15 @@ namespace PasswordGenerator /// public sealed class CryptoRandomSource : IRandomSource { + /// + /// Returns a uniformly distributed, non-negative random integer that is less than + /// . + /// + /// The exclusive upper bound; must be positive. + /// A random integer in the range [0, maxExclusive). + /// + /// is zero or negative. + /// public int NextInt(int maxExclusive) { if (maxExclusive <= 0) diff --git a/PasswordGenerator/IPassword.cs b/PasswordGenerator/IPassword.cs index 70fe6ee..a778230 100644 --- a/PasswordGenerator/IPassword.cs +++ b/PasswordGenerator/IPassword.cs @@ -2,12 +2,31 @@ namespace PasswordGenerator { + /// + /// Fluent builder for configuring and generating passwords. Each configuration method returns the + /// same instance so calls can be chained. + /// public interface IPassword { + /// Includes lowercase letters in the pool. + /// The same builder, for chaining. IPassword IncludeLowercase(); + + /// Includes uppercase letters in the pool. + /// The same builder, for chaining. IPassword IncludeUppercase(); + + /// Includes digits in the pool. + /// The same builder, for chaining. IPassword IncludeNumeric(); + + /// Includes the default special characters in the pool. + /// The same builder, for chaining. IPassword IncludeSpecial(); + + /// Includes the given special characters in the pool. + /// The special characters to add to the pool. + /// The same builder, for chaining. IPassword IncludeSpecial(string specialCharactersToInclude); /// Replaces the pool with an explicit set of characters (no forced composition). @@ -22,9 +41,25 @@ public interface IPassword /// Requires at least characters from the given class. IPassword RequireAtLeast(CharacterClass characterClass, int count); + /// Sets the required password length. + /// The number of characters the generated password should contain. + /// The same builder, for chaining. IPassword LengthRequired(int passwordLength); + + /// Generates a single password using the current settings. + /// The generated password. string Next(); + + /// Attempts to generate a single password without throwing on invalid settings. + /// + /// When this method returns , the generated password; otherwise . + /// + /// if a password was generated; otherwise . bool TryNext(out string? password); + + /// Generates a sequence of passwords using the current settings. + /// How many passwords to generate. + /// The generated passwords. IEnumerable NextGroup(int numberOfPasswordsToGenerate); } } \ No newline at end of file diff --git a/PasswordGenerator/IPasswordSettings.cs b/PasswordGenerator/IPasswordSettings.cs index 8dbebd3..ddd2b97 100644 --- a/PasswordGenerator/IPasswordSettings.cs +++ b/PasswordGenerator/IPasswordSettings.cs @@ -7,11 +7,22 @@ namespace PasswordGenerator /// public interface IPasswordSettings { + /// Whether lowercase letters are included in the pool. bool IncludeLowercase { get; } + + /// Whether uppercase letters are included in the pool. bool IncludeUppercase { get; } + + /// Whether digits are included in the pool. bool IncludeNumeric { get; } + + /// Whether special characters are included in the pool. bool IncludeSpecial { get; } + + /// The number of characters the generated password should contain. int PasswordLength { get; set; } + + /// The full set of characters the password is drawn from, after applying all settings. string CharacterSet { get; } /// True when a custom pool (e.g. ) replaces the per-class sets. @@ -28,13 +39,35 @@ public interface IPasswordSettings /// guarantee at least one character from each required class is present in the output. /// IReadOnlyList CharacterGroups { get; } + + /// The maximum number of attempts allowed when generating a valid password. int MaximumAttempts { get; } + + /// The smallest allowed password length. int MinimumLength { get; } + + /// The largest allowed password length. int MaximumLength { get; } + + /// Enables lowercase letters in the pool. + /// The same settings, for chaining. IPasswordSettings AddLowercase(); + + /// Enables uppercase letters in the pool. + /// The same settings, for chaining. IPasswordSettings AddUppercase(); + + /// Enables digits in the pool. + /// The same settings, for chaining. IPasswordSettings AddNumeric(); + + /// Enables the default special characters in the pool. + /// The same settings, for chaining. IPasswordSettings AddSpecial(); + + /// Enables the given special characters in the pool. + /// The special characters to add to the pool. + /// The same settings, for chaining. IPasswordSettings AddSpecial(string specialCharactersToAdd); /// Replaces the entire pool with an explicit set of characters (no forced composition). @@ -49,6 +82,7 @@ public interface IPasswordSettings /// Requires at least characters from the given class, enabling it if needed. IPasswordSettings RequireAtLeast(CharacterClass characterClass, int count); + /// The special characters used when special characters are included. string SpecialCharacters { get; set; } } } \ No newline at end of file diff --git a/PasswordGenerator/PassphraseGenerator.cs b/PasswordGenerator/PassphraseGenerator.cs index 4f1a920..00549e1 100644 --- a/PasswordGenerator/PassphraseGenerator.cs +++ b/PasswordGenerator/PassphraseGenerator.cs @@ -16,6 +16,16 @@ public class PassphraseGenerator : IPasswordGenerator, IDisposable private readonly IRandomSource _random; private readonly bool _ownsRandom; + /// Creates a passphrase generator. + /// The number of words in each passphrase; must be at least one. + /// The character placed between words (and before the trailing number). + /// Whether to capitalize the first letter of each word. + /// Whether to append a random two-digit number. + /// + /// An optional random source. When supplied, the caller owns it; otherwise a + /// is created and owned by this instance. + /// + /// is less than one. public PassphraseGenerator(int wordCount = 4, char separator = '-', bool capitalize = false, bool includeNumber = true, IRandomSource? randomSource = null) { @@ -30,14 +40,22 @@ public PassphraseGenerator(int wordCount = 4, char separator = '-', bool capital _ownsRandom = randomSource == null; } + /// The number of words in each passphrase. public int WordCount { get; } + + /// The character placed between words. public char Separator { get; } + + /// Whether the first letter of each word is capitalized. public bool Capitalize { get; } + + /// Whether a random two-digit number is appended. public bool IncludeNumber { get; } /// The number of passphrases produced by the parameterless overload. public int DefaultBatchCount { get; set; } = 1; + /// public string Next() { var sb = new StringBuilder(); @@ -67,12 +85,14 @@ public string Next() return sb.ToString(); } + /// public bool TryNext(out string? password) { password = Next(); return true; } + /// public ValueTask NextAsync(CancellationToken cancellationToken = default) { return cancellationToken.IsCancellationRequested @@ -80,11 +100,13 @@ public ValueTask NextAsync(CancellationToken cancellationToken = default : new ValueTask(Next()); } + /// public IReadOnlyList Generate() { return Generate(DefaultBatchCount); } + /// public IReadOnlyList Generate(int count) { if (count < 0) @@ -97,11 +119,13 @@ public IReadOnlyList Generate(int count) return passphrases; } + /// public ValueTask> GenerateAsync(CancellationToken cancellationToken = default) { return GenerateAsync(DefaultBatchCount, cancellationToken); } + /// public ValueTask> GenerateAsync(int count, CancellationToken cancellationToken = default) { if (count < 0) @@ -129,6 +153,7 @@ public double EstimateEntropyBits() return bits; } + /// Disposes the random source when this instance owns it (i.e. it was not supplied by the caller). public void Dispose() { if (_ownsRandom && _random is IDisposable disposable) diff --git a/PasswordGenerator/Password.cs b/PasswordGenerator/Password.cs index a9d5b32..8cc48f5 100644 --- a/PasswordGenerator/Password.cs +++ b/PasswordGenerator/Password.cs @@ -20,6 +20,7 @@ public class Password : IPassword, IPasswordGenerator, IDisposable private readonly IRandomSource _random; private readonly bool _ownsRandom; + /// Creates a generator with the default settings (all character classes, length 16). public Password() { Settings = new PasswordSettings(DefaultIncludeLowercase, DefaultIncludeUppercase, @@ -29,6 +30,8 @@ public Password() _ownsRandom = true; } + /// Creates a generator from the supplied settings. + /// The settings to use. public Password(IPasswordSettings settings) { Settings = settings; @@ -36,6 +39,8 @@ public Password(IPasswordSettings settings) _ownsRandom = true; } + /// Creates a generator with the default character classes and the given length. + /// The required password length. public Password(int passwordLength) { Settings = new PasswordSettings(DefaultIncludeLowercase, DefaultIncludeUppercase, @@ -44,6 +49,11 @@ public Password(int passwordLength) _ownsRandom = true; } + /// Creates a generator with the given character classes enabled and the default length. + /// Whether to include lowercase letters. + /// Whether to include uppercase letters. + /// Whether to include digits. + /// Whether to include special characters. public Password(bool includeLowercase, bool includeUppercase, bool includeNumeric, bool includeSpecial) { Settings = new PasswordSettings(includeLowercase, includeUppercase, includeNumeric, @@ -52,6 +62,12 @@ public Password(bool includeLowercase, bool includeUppercase, bool includeNumeri _ownsRandom = true; } + /// Creates a generator with the given character classes enabled and a specific length. + /// Whether to include lowercase letters. + /// Whether to include uppercase letters. + /// Whether to include digits. + /// Whether to include special characters. + /// The required password length. public Password(bool includeLowercase, bool includeUppercase, bool includeNumeric, bool includeSpecial, int passwordLength) { @@ -61,6 +77,13 @@ public Password(bool includeLowercase, bool includeUppercase, bool includeNumeri _ownsRandom = true; } + /// Creates a generator with the given character classes, length, and attempt limit. + /// Whether to include lowercase letters. + /// Whether to include uppercase letters. + /// Whether to include digits. + /// Whether to include special characters. + /// The required password length. + /// The maximum number of generation attempts. public Password(bool includeLowercase, bool includeUppercase, bool includeNumeric, bool includeSpecial, int passwordLength, int maximumAttempts) { @@ -81,62 +104,73 @@ public Password(IPasswordSettings settings, IRandomSource randomSource) _ownsRandom = false; } + /// The settings that drive generation. Replaced in place by the fluent configuration methods. public IPasswordSettings Settings { get; set; } + /// public IPassword IncludeLowercase() { Settings = Settings.AddLowercase(); return this; } + /// public IPassword IncludeUppercase() { Settings = Settings.AddUppercase(); return this; } + /// public IPassword IncludeNumeric() { Settings = Settings.AddNumeric(); return this; } + /// public IPassword IncludeSpecial() { Settings = Settings.AddSpecial(); return this; } + /// public IPassword IncludeSpecial(string specialCharactersToInclude) { Settings = Settings.AddSpecial(specialCharactersToInclude); return this; } + /// public IPassword WithCharacters(string characters) { Settings = Settings.UseCharacters(characters); return this; } + /// public IPassword WithAllAscii() { Settings = Settings.UseAllAscii(); return this; } + /// public IPassword ExcludeAmbiguous() { Settings = Settings.ExcludeAmbiguousCharacters(); return this; } + /// public IPassword RequireAtLeast(CharacterClass characterClass, int count) { Settings = Settings.RequireAtLeast(characterClass, count); return this; } + /// public IPassword LengthRequired(int passwordLength) { Settings.PasswordLength = passwordLength; @@ -182,11 +216,13 @@ public bool TryNext(out string? password) return true; } + /// public IEnumerable NextGroup(int numberOfPasswordsToGenerate) { return Generate(numberOfPasswordsToGenerate); } + /// public ValueTask NextAsync(CancellationToken cancellationToken = default) { return cancellationToken.IsCancellationRequested @@ -194,11 +230,13 @@ public ValueTask NextAsync(CancellationToken cancellationToken = default : new ValueTask(Next()); } + /// public IReadOnlyList Generate() { return Generate(DefaultBatchCount); } + /// public IReadOnlyList Generate(int count) { if (count < 0) @@ -211,11 +249,13 @@ public IReadOnlyList Generate(int count) return passwords; } + /// public ValueTask> GenerateAsync(CancellationToken cancellationToken = default) { return GenerateAsync(DefaultBatchCount, cancellationToken); } + /// public ValueTask> GenerateAsync(int count, CancellationToken cancellationToken = default) { if (count < 0) @@ -374,6 +414,7 @@ private static bool LengthIsValid(int passwordLength, int minLength, int maxLeng return passwordLength >= minLength && passwordLength <= maxLength; } + /// Disposes the random source when this instance owns it (i.e. it was not supplied by the caller). public void Dispose() { if (_ownsRandom && _random is IDisposable disposable) diff --git a/PasswordGenerator/PasswordGenerator.csproj b/PasswordGenerator/PasswordGenerator.csproj index 4c607a6..a4c08df 100644 --- a/PasswordGenerator/PasswordGenerator.csproj +++ b/PasswordGenerator/PasswordGenerator.csproj @@ -7,8 +7,6 @@ true - - $(NoWarn);CS1591 3.0.0 diff --git a/PasswordGenerator/PasswordOptions.cs b/PasswordGenerator/PasswordOptions.cs index b9eb066..a025bbe 100644 --- a/PasswordGenerator/PasswordOptions.cs +++ b/PasswordGenerator/PasswordOptions.cs @@ -6,14 +6,22 @@ namespace PasswordGenerator /// public class PasswordOptions { + /// Whether lowercase letters are included in the pool. Defaults to . public bool IncludeLowercase { get; set; } = true; + + /// Whether uppercase letters are included in the pool. Defaults to . public bool IncludeUppercase { get; set; } = true; + + /// Whether digits are included in the pool. Defaults to . public bool IncludeNumeric { get; set; } = true; + + /// Whether special characters are included in the pool. Defaults to . public bool IncludeSpecial { get; set; } = true; /// Custom special characters. When null/empty the library default special set is used. public string? SpecialCharacters { get; set; } + /// The password length. Defaults to 16. public int Length { get; set; } = 16; /// Removes look-alike characters from the pool when true. diff --git a/PasswordGenerator/PasswordSettings.cs b/PasswordGenerator/PasswordSettings.cs index 26b6431..5f9c947 100644 --- a/PasswordGenerator/PasswordSettings.cs +++ b/PasswordGenerator/PasswordSettings.cs @@ -9,14 +9,33 @@ namespace PasswordGenerator /// public class PasswordSettings : IPasswordSettings { + /// The lowercase letters (a–z) used when lowercase is enabled. public const string LowercaseCharacters = "abcdefghijklmnopqrstuvwxyz"; + + /// The uppercase letters (A–Z) used when uppercase is enabled. public const string UppercaseCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + + /// The digits (0–9) used when numeric is enabled. public const string NumericCharacters = "0123456789"; + private const string DefaultSpecialCharacters = @"!#$%&*@\"; private const int DefaultMinPasswordLength = 4; private const int DefaultMaxPasswordLength = 256; + + /// public string SpecialCharacters { get; set; } + /// Creates a settings instance. + /// Whether to include lowercase letters. + /// Whether to include uppercase letters. + /// Whether to include digits. + /// Whether to include special characters. + /// The required password length. + /// The maximum number of generation attempts. + /// + /// Whether these are the library defaults; when , the first fluent + /// configuration call clears the default pool before applying changes. + /// public PasswordSettings(bool includeLowercase, bool includeUppercase, bool includeNumeric, bool includeSpecial, int passwordLength, int maximumAttempts, bool usingDefaults) { @@ -36,19 +55,43 @@ public PasswordSettings(bool includeLowercase, bool includeUppercase, bool inclu private bool UsingDefaults { get; set; } private readonly Dictionary _minimumCounts = new Dictionary(); + /// public bool IncludeLowercase { get; private set; } + + /// public bool IncludeUppercase { get; private set; } + + /// public bool IncludeNumeric { get; private set; } + + /// public bool IncludeSpecial { get; private set; } + + /// public int PasswordLength { get; set; } + + /// public string CharacterSet { get; private set; } + + /// public bool IsCustomPool { get; private set; } + + /// public bool ExcludeAmbiguous { get; private set; } + + /// public IReadOnlyDictionary MinimumCounts => _minimumCounts; + + /// public int MaximumAttempts { get; } + + /// public int MinimumLength { get; } + + /// public int MaximumLength { get; } + /// public IReadOnlyList CharacterGroups { get @@ -62,6 +105,7 @@ public IReadOnlyList CharacterGroups } } + /// public IPasswordSettings AddLowercase() { StopUsingDefaults(); @@ -70,6 +114,7 @@ public IPasswordSettings AddLowercase() return this; } + /// public IPasswordSettings AddUppercase() { StopUsingDefaults(); @@ -78,6 +123,7 @@ public IPasswordSettings AddUppercase() return this; } + /// public IPasswordSettings AddNumeric() { StopUsingDefaults(); @@ -86,6 +132,7 @@ public IPasswordSettings AddNumeric() return this; } + /// public IPasswordSettings AddSpecial() { StopUsingDefaults(); @@ -95,6 +142,7 @@ public IPasswordSettings AddSpecial() return this; } + /// public IPasswordSettings AddSpecial(string specialCharactersToAdd) { StopUsingDefaults(); @@ -104,6 +152,7 @@ public IPasswordSettings AddSpecial(string specialCharactersToAdd) return this; } + /// public IPasswordSettings UseCharacters(string characters) { if (characters == null) throw new ArgumentNullException(nameof(characters)); @@ -119,17 +168,20 @@ public IPasswordSettings UseCharacters(string characters) return this; } + /// public IPasswordSettings UseAllAscii() { return UseCharacters(CharacterFilter.AllPrintableAscii); } + /// public IPasswordSettings ExcludeAmbiguousCharacters() { ExcludeAmbiguous = true; return this; } + /// public IPasswordSettings RequireAtLeast(CharacterClass characterClass, int count) { if (count < 0) throw new ArgumentOutOfRangeException(nameof(count), "count cannot be negative."); diff --git a/PasswordGenerator/PoolEntropyEstimator.cs b/PasswordGenerator/PoolEntropyEstimator.cs index 0bd8753..b8a2308 100644 --- a/PasswordGenerator/PoolEntropyEstimator.cs +++ b/PasswordGenerator/PoolEntropyEstimator.cs @@ -7,6 +7,15 @@ namespace PasswordGenerator /// public class PoolEntropyEstimator : IEntropyEstimator { + /// + /// Estimates the entropy, in bits, of passwords produced with the given + /// as length × log2(effective pool size). + /// + /// The settings describing the character pool and length. + /// + /// The estimated entropy in bits, or 0 when the pool is empty or the length is not positive. + /// + /// is . public double EstimateBits(IPasswordSettings settings) { if (settings == null) throw new ArgumentNullException(nameof(settings)); From 38e2080eb178b52d94740dc6544e89671e78b045 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 25 May 2026 14:57:59 +0000 Subject: [PATCH 5/5] Bump GitHub Actions to Node 24 runtimes The v4 majors of checkout, setup-dotnet, and upload-artifact run on the deprecated Node.js 20 runner. Bump to checkout@v6, setup-dotnet@v5, and upload-artifact@v7, which run on Node.js 24. Inputs used here are unchanged across these majors. https://claude.ai/code/session_01WVsnjDXjACmcLWJLGNMKVL --- .github/workflows/benchmarks.yml | 6 +++--- .github/workflows/ci.yml | 8 ++++---- .github/workflows/release.yml | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 527e5b2..256759e 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -14,10 +14,10 @@ jobs: benchmark: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup .NET - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v5 with: dotnet-version: | 8.0.x @@ -58,7 +58,7 @@ jobs: - name: Upload raw artifacts if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: benchmark-results-${{ github.run_number }} path: PasswordGenerator.Benchmarks/BenchmarkDotNet.Artifacts/results/ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0e291c2..d04e877 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,12 +13,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 # full history so SourceLink can resolve the commit - name: Setup .NET - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v5 with: dotnet-version: | 8.0.x @@ -39,7 +39,7 @@ jobs: - name: Upload test results if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: test-results path: ./test-results/*.trx @@ -48,7 +48,7 @@ jobs: run: dotnet pack PasswordGenerator/PasswordGenerator.csproj -c Release --no-build -o artifacts - name: Upload package - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: nuget path: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e392bd6..07e75c6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,12 +12,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 # full history so SourceLink can resolve the commit - name: Setup .NET - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v5 with: dotnet-version: | 8.0.x