Skip to content

Larger major change updates - #75

Open
rheone wants to merge 70 commits into
sandialabs:mainfrom
rheone:pilot/batch-1
Open

Larger major change updates#75
rheone wants to merge 70 commits into
sandialabs:mainfrom
rheone:pilot/batch-1

Conversation

@rheone

@rheone rheone commented Jul 11, 2026

Copy link
Copy Markdown
Member

Work

Added

  • .NET 10 (net10.0) target framework
  • IIPAddressRange.ToIPAddresses() -- safe enumeration method capped at MaxEnumerationExponent
  • MaxEnumerationExponent property on all range types (default 12 = 4096 addresses)
  • SubnetUtilities.FewestConsecutiveSubnetsFor overload with maxEnumerationExponent parameter
  • Regex source generators for improved parsing performance
  • Partial class split for maintainability (Subnet, IPAddressRange, AbstractIPAddressRange, BigEndianBitWrapper)

Changed

  • SubnetUtilities.PrivateIPAddressRangesList and LinkLocalIPAddressRangesList are now readonly (lazily initialized)
  • AbstractIPAddressRange.Overlaps -- symmetry restored (both directions now return true)
  • IPAddressRange.TryExcludeAll -- boundary guards at family min/max (no longer throws)
  • Subnet parsing and IPAddressUtilities parsing use [GeneratedRegex] where available
  • Package dependencies updated and centralized via Directory.Packages.props
  • Deterministic builds enabled
  • Language version set to C# 14
  • XML documentation improved throughout all public APIs with RFC references
  • Exception types corrected (InvalidOperationException to ArgumentException where appropriate)

Removed

  • MacAddress type (was [Obsolete] in v3.x) - replacement coming soon-ish in a new package
  • DefaultIPAddressRangeComparer (use DefaultIIPAddressRangeComparer)
  • Drops Gulliver dependency

Fixed

  • AbstractIPAddressRange.Overlaps asymmetry (inner.Overlaps(outer) returned false)
  • ContainsAnyPrivateAddresses/ContainsAllPublicAddresses endpoint heuristic producing incorrect results for ranges spanning private subnets
  • IPAddressRange.TryExcludeAll throwing InvalidOperationException at family max address
  • IComparable consistency across range types

Deprecated (will be removed in future major version)

  • GetEnumerator() on any range type -- use ToIPAddresses() instead
  • IIPAddressRange implementing IEnumerable<IPAddress> -- use ToIPAddresses() explicitly
  • Subnet.TryIPv6FromPartial(string, out IEnumerable<Subnet>) -- to be replaced / improved by more explicit methods

Testing

  • Comprehensive test suite expansion across all range types, comparers, converters, math, and utilities
  • Subnet tests split into partial files: factory, IComparable, IEquatable, ISerializable, operators
  • IPAddressRange tests split into partial files: factory, IComparable, IEquatable, ISerializable, operators
  • AbstractIPAddressRange tests split into: IEnumerable, IFormattable, IIPAddressRange
  • Smoke test project (smoketests/) validating the packed NuGet package on real runtimes

Infrastructure

  • Benchmark project (src/Arcus.Benchmarks/) with BenchmarkDotNet for all major types
  • CI/CD pipeline rework: build, CodeQL, dependency review, OpenSSF Scorecard, and smoke-test workflows
  • .slnx solution format (replaces legacy .sln)
  • CONTRIBUTING.md with build, test, and PR guidance
  • EditorConfig consolidated to repository root

rheone and others added 30 commits May 22, 2026 21:38
## Breaking Changes

### SubnetUtilities static fields are now readonly
`SubnetUtilities.PrivateIPAddressRangesList` and `LinkLocalIPAddressRangesList`
are now declared `readonly`. Any code that reassigned these field references
(e.g. `SubnetUtilities.PrivateIPAddressRangesList = myList`) will no longer
compile. The `IReadOnlyList<Subnet>` type already prevented content mutation;
`readonly` now also prevents reference replacement.

## Bug Fixes

### AbstractIPAddressRange.Overlaps — symmetry restored
`Overlaps(IIPAddressRange)` previously returned `false` when the argument range
was wholly contained inside `this`. Added `addressRange.Contains(this)` to the
check so the method is symmetric: `A.Overlaps(B) == B.Overlaps(A)` in all cases.
Bidirectional and Subnet-typed symmetry regression tests added.

### AbstractIPAddressRange — ContainsAnyPrivateAddresses / ContainsAllPublicAddresses
All four `ContainsAny/AllPrivate/PublicAddresses` methods previously used an
endpoint heuristic that produced wrong results when both endpoints of a range
were public but the interior spanned a private subnet (e.g. 11.0.0.0-173.0.0.0
spans 172.16.0.0/12). All four methods now independently use range-overlap
detection against `PrivateIPAddressRangesList`. Regression tests for the
spanning-block case and cross-method inverse-assertion tests added.

### IPAddressRange.TryExcludeAll — boundary guards at family min/max
All four unguarded `Increment`/`Increment(-1)` call sites are now guarded with
`IsAtMax`/`IsAtMin`. When an exclusion ends at the family maximum (e.g.
255.255.255.255) the method returns `(true, leading segment)` or `(true, [])`
instead of throwing `InvalidOperationException`. When an exclusion starts at the
family minimum the trailing-segment path is guarded symmetrically. Boundary
behavior is documented in XML remarks. Regression tests for IPv4 and IPv6
family-max/min exclusion cases added.

### MacAddress.IsUnusable — implementation corrected
`IsUnusable` previously used `Any(b != 0)` and returned `true` for almost every
real MAC address — the logical inverse of the documented contract. Fixed to
`All(b == 0)`: returns `true` only when all three OUI bytes are 0x00. All four
existing test cases have their expected values corrected with comments noting the
intentional behavioral fix.

## Additional Coverage
- `Overlaps(Subnet)` typed override: explicit bidirectional symmetry test
- `Overlaps(IIPAddressRange)` called from a Subnet context: symmetry test
- `TryExcludeAll`: IPv4 and IPv6 family-max and family-min boundary tests
- Private/public spanning-block regression + ContainsAnyPrivate <->
  ContainsAllPublic inverse assertions
Addressing targeting issues with specific lang version change
Adds RFC notes
Cleans up ToBase85String
Dropped (most) obsolete items; including MAC Address type
Cleaned up ToDottedQuadString method
Using modern collection conventions
fixing bad exceptions - adding tests too
using matching and switch where possible
Touches serialization
Addresses warnings
Adds smoke test for verifying other targeted consumers
H2: Unify GetHashCode at AbstractIPAddressRange (Head+Tail hash)
H3: Add enumeration warning remarks to IIPAddressRange
H4: Fix Deconstruct doc (non IPv6 -> IPv6)
M9: Fix XML doc grammar in IIPAddressRange/AbstractIPAddressRange
M15: Simplify ContainsAnyPublicAddresses implementation + add examples
L1: Simplify Overlaps, fix Touches Equals call
L6: Add typed Contains/Overlaps overloads to IPAddressRange
L10: Fix typos (Ovelap->Overlap, metods->methods, Goldilocks comment)
L11: Remove bare TODO (superseded by H3)
L12: Make AddressTuple private protected
I1: Fix IEnumerable buffer-reuse comment
Add enumeration safety cap via MaxEnumerationExponent; refactor TryIPv6FromPartial

Add MaxEnumerationExponent (default 12, cap 4096 addresses) to
AbstractIPAddressRange, propagated through Subnet constructors,
SubnetUtilities.FewestConsecutiveSubnetsFor, and TryIPv4FromPartial/
TryIPv6FromPartial. Enumeration now throws InvalidOperationException
when the limit is exceeded, preventing accidental iteration over
enormous ranges.

Introduce ToIPAddresses() as the primary enumeration method on
IIPAddressRange; the inherited GetEnumerator() is marked [Obsolete]
and delegates to it.

Decompose TryIPv6FromPartial into 7 focused private helpers
(NormalizeIPv6PartialInput, IsPotentiallyValidIPv6Partial,
ContainsOnlyHexChars, TryParseExactCidr, TryParseBareHextet,
TryParseAmbiguousHextets, ExpandHextetPermutations) with thorough
XML documentation. Reverse permutation output order so most-specific
subnets appear first.

Add serialization versioning to Subnet (version 2, stores
MaxEnumerationExponent) with backward-compatible fallback for
previously serialized instances.

Add Subnet.GetHashCode() and AddressTuple.GetHashCode() (moved from
base class). Apply expression-bodied members and switch expressions
throughout.
Developer added 29 commits June 28, 2026 13:14
…CodeQL noise

- Centralize all NuGet package versions in `src/Directory.Packages.props` and
  strip `Version` attributes from individual `.csproj` files (Arcus,
  Arcus.Tests, Arcus.Benchmarks). Regenerate lock files to v2 format with
  CPM-aware dependency resolution and win-x86 runtime sections.

- Add 7 new test methods across BigEndianBitWrapper (TryAdd with
  long.MinValue delta, DebuggerDisplay for IPv4/IPv6), IPAddressRange
  (self-reference and unrelated-type Equals), and SubnetUtilities
  (range at IPv4 max, null mid-sequence).

- Add 11 inline CodeQL suppression comments for intentional test
  patterns: self-comparison, unrelated-type Equals, null extension
  invocation, upcast for overload resolution, and loop-local StringBuilder.

- Add `src/.runsettings` with xUnit CPU-parallelization and XPlat
  Code Coverage configuration (excluding test fw and benchmark assemblies).

- Update README badges: replace GitHub Release/Tag/old-license entries
  with NuGet, OpenSSF Scorecard, refreshed build, and underscore-separated
  targets badge for Shields.io compatibility.

- Add `allow-unknown-licenses: true` in dependency-review workflow with
  explanatory comments for false-positive license warnings.
…ssemblies, and migrate suppression comments

- Add `paths-ignore` to CodeQL config to exclude test and benchmark
  projects, reducing noise from intentional test patterns
- Add `Microsoft.NETFramework.ReferenceAssemblies` to central package
  management in `Directory.Packages.props` and reference it
  conditionally for `net48` targets in both `.csproj` files
- Prune `win-x86` runtime-specific sections from `packages.lock.json`
  that are no longer needed after CPM adoption
- Migrate suppression comments from `// CodeQL [...]` syntax to
  `// lgtm[...]` across 6 test files for consistent tooling
- X1: Add <Nullable>annotations</Nullable> to Arcus.csproj
- X3: Update Directory.Build.props metadata (Sandia/Apache-2.0/sandialabs)
- X4: Remove explicit LangVersion from Arcus.csproj (inherits from Directory.Build.props)
- X7: Replace #if NETSTANDARD2_0 with #if !NET7_0_OR_GREATER for GeneratedRegex
- X8: Add XML doc comment to InternalsVisibleTo items
- .gitignore: Add /AGENTS.md and /CLAUDE.md
- B1: Replace Debug.Assert with ArgumentException in ToBytes(Span<byte>)
- B2: Guard MaxValueForWidth/MaxHiLoForWidth for ByteWidth=0
- B3: ByteWidth validation in & and |
- B4: Mask & and | results to ByteWidth (consistent with ~)
- B5: Implement ^ (XOR) operator with ByteWidth validation + masking
- B6: ByteWidth validation in Subtract
- X9: Document unchecked arithmetic dependency in class XML doc
- Add Debug.Assert at entry of Subtract, CompareTo, operators
- Add 13 new tests in Safety.cs and Operators.cs
- A2: Clamp MaxEnumerationExponent to address family bit width
  (32 for IPv4, 128 for IPv6) to prevent invalid cap values
- A3: Add null guards to AddressTuple.Equals and GetHashCode
  to prevent NRE on default(struct) instances
- A4: Add recursion warning XML doc to Overlaps method
- A5: Use long counter in EnumerateCore when maxCount fits in long,
  avoiding BigInteger heap allocation per yielded address
- A6: Cache BigInteger.One << MaxEnumerationExponent in _maxCount field
  to avoid recomputation on each ToIPAddresses call
- A7: Remove dead IPAddressMath.Min clamping in EnumerateCore
  (Tail can never exceed address family max)
- A10: Document private subnet containment assumption on
  ContainsAnyPublicAddresses
S1 - Parse whitespace: Trim() in Parse/TryParse
S2 - Contains delegation: delegate to base.Contains
S5 - Negative prefix: reject negative routing prefixes
S6 - Typo fix: 'an' -> 'a' in exception message
S7 - Operators: fix double-evaluation in <=/>= (now !(>)/!(<))
S8 - Overlaps doc: add CIDR containment property doc
S9 - IPv6 netmask skip: bypass NormalizeAndCreateNetMask for IPv6
S10 - Single-IP exponent: add Subnet.FromIPAddress factory
S11 - Usables doc: (documentation-only, no code change)

14 new tests, 9398/9398 passing on net8.0/net10.0, 0 warnings
- GetObjectData writes NetworkPrefixAddress (Head) instead of BroadcastAddress (Tail)
- DeserializeSubnet validates SerVersion == 3 (throws SerializationException otherwise)
- MaxEnumerationExponent read directly (no try/catch fallback)
- Add SerializationFormatVersion constant (3)
- Add test-only DeserializeFromInfo internal helper
- Add #if NET48 tests for field name, version, round-trip

S3 (MEDIUM). X2 (TFM gate expansion) deferred - net48-only on Linux,
BinaryFormatter deprecated on net8.0+.
- Add Equals(object) + GetHashCode() to AbstractIPAddressRange (Head+Tail)
- Subnet.Equals(Subnet) uses Head+Tail; GetHashCode delegates to base
- IPAddressRange.GetHashCode delegates to base
- Remove DefaultIIPAddressRangeComparer dependency from Subnet equality
- Add unification test (Subnet vs IPAddressRange hash match)

Breaking: Subnet.GetHashCode changes (Head+RoutingPrefix -> Head+Tail).
R1 (MEDIUM).
- C3: remove dead null-coalescing in DefaultIPAddressComparer ctor
- C4: NetmaskToCidrRoutePrefix throws ArgumentException (was InvalidOperationException)
- C5: ParseFromHexString handles all-zero input (preserve a single 0)
- C6: document Increment delta is 64-bit (no BigInteger overload)
- C7: IsGreaterThanOrEqualTo/IsLessThanOrEqualTo consistent null handling (single-null false; both-null true via ReferenceEquals)
- C8: Debug.Assert non-null Head in DefaultIIPAddressRangeComparer
- C9: PrivateIPAddressRangesList/LinkLocalIPAddressRangesList lazily initialized (Lazy-backed properties)
- C10: ValidAddressFamilies uses array (was List.AsReadOnly)
- C11: ToDottedQuadString graceful fallback on TryWriteBytes short write

12 new tests; 9414/9414 passing on net8.0/net10.0; 0 warnings.
C4 breaking: exception type changes for invalid netmask (LOW).
- C2: FewestConsecutiveSubnetsFor FilledSubnets converted from recursive Concat
  chain to yield-return iterator (removes O(N) Concat wrapper depth, O(N^2)
  enumeration pressure). 2 sentinel tests guard count + contiguous coverage.
- S4: NormalizeAndCreateNetMask(IPAddress, IPAddress) reuses fetched head bytes;
  avoids redundant GetAddressBytes allocation on the IPv4 hot path.
- A9: document Length is computed once and cached (audit-only).
- B8: document ToBytes() allocation and recommend Span overload on net8+ (audit-only).

C1 (Span-based BigEndianBitWrapper.FromBytes + TryWriteBytes in comparer/math)
deferred - meaningful API addition with cross-TFM considerations; tracked separately.
9416/9416 passing on net8.0/net10.0; 0 warnings.
- A1: document why AbstractIPAddressRange is abstract (enforces concrete subtype)
- S11: document /31 usable hosts follows RFC 950 (RFC 3021 note)
- B7: document FromBytes(byte[], int) empty array -> all-zero wrapper
- B9: document non-generic IComparable intentionally not implemented (internal type)
- A8: document [Obsolete] design decision on GetEnumerator (prefer ToIPAddresses())
- X5: generated regex types in Arcus.xml accepted (source-generator limitation)
- X6: net9/net10 API audit documented (SearchValues, OrderedDictionary) - no code change

Doc-only batch. 9416/9416 passing on net8.0/net10.0; 0 warnings.
- Gate `[SkipLocalsInit]` on `NET9_0_OR_GREATER` (not NET8_0) to match
  actual runtime support in `BigEndianBitWrapper`
- Add null-check, address-family, and prefix-range validation in
  `Subnet.DeserializeSubnet`; suppress S5766 downstream
- Change `ValidAddressFamilies` from `IReadOnlyCollection` to
  `AddressFamily[]` for collection-expression syntax
- Suppress S1313 on intentional private/link-local IP literals
- Bump SonarAnalyzer 10.27 → 10.28
- Pin `LangVersion` to `14.0` (was `latest`)
- Convert `FewestConsecutiveSubnetsFor` theory data from `object[]`
  to `TheoryDataRow`; extract inline arrays into named static fields;
  replace `[^1]` with `.Count - 1` for netstandard2.0 compat
- Fix `Count` → `Length` assertion on array in IPAddressUtilitiesTests
removed AsyncFixer (there is no async code)
The separate coverage-report job could not post PR comments from fork PRs
because pull_request events from forks don't receive a write token.
Moving the coverage report generation and comment posting into the build
job (which already runs on ubuntu-latest with correct permissions) avoids
the cross-workflow artifact and token issues entirely.
The coverage comment failed on fork PRs because pull_request events
from forks only get a read-only GITHUB_TOKEN. Inlining the comment
into the build job didn't help since it still runs in the pull_request
context.

Fix by splitting into a pull_request_target workflow that runs in the
base repo context with full write permissions. The new workflow:
- Finds the build run for the PR's head SHA via the API
- Downloads the coverage artifact from that run
- Generates the report and posts the sticky comment

The build.yml is restored to its original form (without coverage
comment steps or the old coverage-report job).
@rheone rheone changed the title Pilot/batch 1 Larger major change updates Jul 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant