diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 35d5507..6e5e97a 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,21 +3,21 @@ "isRoot": true, "tools": { "csharpier": { - "version": "1.0.1", + "version": "1.2.6", "commands": [ "csharpier" ], "rollForward": false }, "dotnet-outdated-tool": { - "version": "4.6.8", + "version": "4.7.1", "commands": [ "dotnet-outdated" ], "rollForward": false }, "husky": { - "version": "0.7.2", + "version": "0.9.1", "commands": [ "husky" ], diff --git a/src/.csharpierrc.json b/.csharpierrc.json similarity index 100% rename from src/.csharpierrc.json rename to .csharpierrc.json diff --git a/.devcontainer/.gitignore b/.devcontainer/.gitignore new file mode 100644 index 0000000..aaf0c97 --- /dev/null +++ b/.devcontainer/.gitignore @@ -0,0 +1,10 @@ +# Ignore local-only devcontainer overrides. +devcontainer.local.json +docker-compose.local.yml + +# Ignore temporary shell artifacts. +*.tmp + +# logs +*.post-create.log +*.post-start.log diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..79ae79e --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,51 @@ +# Base .NET development image. +FROM mcr.microsoft.com/devcontainers/dotnet:10.0 + +# Install common developer utilities. +RUN apt-get update && apt-get install -y \ + bat \ + curl \ + fd-find \ + fzf \ + git \ + git-lfs \ + jq \ + less \ + ripgrep \ + tree \ + unzip \ + vim \ + wget \ + zip \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +# Create a symbolic link for fd-find to be accessible as 'fd'. +RUN ln -s "$(which fdfind)" /usr/local/bin/fd + +# Enable Git LFS support. +RUN git lfs install + +# Configure PowerShell profile directory. +RUN mkdir -p /home/vscode/.config/powershell + +# Copy custom PowerShell profile into the container. +COPY .devcontainer/dotfiles/powershell-profile.ps1 /home/vscode/.config/powershell/Microsoft.PowerShell_profile.ps1 + +# Ensure NuGet config directory exists and is owned by vscode. +RUN mkdir -p /home/vscode/.nuget/NuGet \ + && chown -R vscode:vscode /home/vscode/.nuget + +# Pre-create volume mount points with correct ownership so post-create.sh +# can write into them without requiring chown at runtime. +RUN mkdir -p \ + /home/vscode/.claude \ + /home/vscode/.claude-runtime/plugins \ + /home/vscode/.claude-runtime/marketplaces \ + /home/vscode/.claude-runtime/cache \ + && chown -R vscode:vscode \ + /home/vscode/.claude \ + /home/vscode/.claude-runtime + +# Ensure vscode user owns their home directory. +RUN chown -R vscode:vscode /home/vscode diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..886801e --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,156 @@ +{ + // Display name shown in VS Code. + "name": "${localWorkspaceFolderBasename}", + // Use Docker Compose instead of directly building/running containers. + "dockerComposeFile": [ + "docker-compose.yml", + "docker-compose.local.yml" + ], + // Main service name from docker-compose.yml. + "service": "devcontainer", + // Folder inside the container where the workspace is mounted. + "workspaceFolder": "/workspace", + // Features installed into the container. + "features": { + // Enhanced git diff pager with syntax highlighting and side-by-side diffs. + "ghcr.io/devcontainer-community/devcontainer-features/delta:1": {}, + // Terminal UI for git operations (commits, branches, staging, rebasing, etc.). + "ghcr.io/devcontainer-community/devcontainer-features/lazygit:1": {}, + // Installs the Prettier formatter for JavaScript, TypeScript, JSON, Markdown, YAML, etc. + "ghcr.io/devcontainers-community/npm-features/prettier:1": {}, + // Installs the Claude Code CLI / agent tooling inside the container. + "ghcr.io/devcontainers-extra/features/claude-code:2": {}, + // GitHub Copilot CLI integration for shell assistance and AI-powered command suggestions. + "ghcr.io/devcontainers/features/copilot-cli:1": {}, + // Installs Node.js runtime, npm, and related tooling. + "ghcr.io/devcontainers/features/node:2": {}, + // Installs PowerShell for cross-platform scripting and shell usage. + "ghcr.io/devcontainers/features/powershell:2": {}, + // Installs Python plus common tooling such as pip. + "ghcr.io/devcontainers/features/python:1": {}, + // Installs OpenCode AI coding agent tooling. + "ghcr.io/jsburckhardt/devcontainer-features/opencode:1": {}, + // Smarter directory navigation with usage-based jumping (`z`, `zi`, etc.). + "ghcr.io/jsburckhardt/devcontainer-features/zoxide:1": {}, + // Installs CSharpier code formatter for consistent C# formatting. + "ghcr.io/nikiforovall/devcontainer-features/dotnet-csharpier:1": {}, + // Installs Zsh shell and configures it for use in the container. + "ghcr.io/nils-geistmann/devcontainers-features/zsh:0": {}, + // Installs Oh My Posh prompt theming engine for customizable shell prompts. + "ghcr.io/rosstaco/devcontainer-features/ohmyposh:1": {}, + // dotnet SDKs for C# development + "ghcr.io/devcontainers/features/dotnet:2": {}, + // GitHub CLI for interacting with GitHub repositories, issues, pull requests, and more directly from the terminal. + "ghcr.io/devcontainers/features/github-cli:1": {}, + // Common utilities like curl, wget, git, and more for general development and scripting tasks. + "ghcr.io/devcontainers/features/common-utils:2": {}, + // Mono runtime for running .NET Framework applications and tools that require Mono. + "ghcr.io/sliekens/devcontainer-features/mono:1": {} + }, + // Environment variables inside the container. + "containerEnv": { + "CLAUDE_RUNTIME_DIR": "/home/vscode/.claude-runtime", + "DOTNET_CLI_TELEMETRY_OPTOUT": "1", + "DOTNET_NOLOGO": "1", + "ENABLE_LSP_TOOL": "1", + "GIT_CONFIG_GLOBAL": "/home/vscode/.config/git/config", + "NUGET_XMLDOC_MODE": "skip" + }, + // VS Code customizations. + "customizations": { + "vscode": { + "extensions": [ + // Anthropic Claude Code integration and agent tooling. + "anthropic.claude-code", + // CSharpier formatter integration for consistent C# formatting. + "csharpier.csharpier-vscode", + // Markdown linting and style enforcement. + "davidanson.vscode-markdownlint", + // Advanced Git visualization, blame, history, and repository insights. + "eamodio.gitlens", + // EditorConfig support for consistent cross-editor formatting rules. + "editorconfig.editorconfig", + // Prettier formatter integration for web and config files. + "esbenp.prettier-vscode", + // GitHub Copilot conversational AI assistant. + "github.copilot-chat", + // GitHub Actions workflow syntax support and validation. + "github.vscode-github-actions", + // Highlights and aggregates TODO, FIXME, HACK, NOTE comments. + "gruntfuggly.todo-tree", + // REST API testing directly inside VS Code using .http files. + "humao.rest-client", + // Docker and container tooling integration. + "ms-azuretools.vscode-docker", + // Primary .NET development toolkit and solution management. + "ms-dotnettools.csdevkit", + // C# language support, debugging, and IntelliSense. + "ms-dotnettools.csharp", + // Python language support and tooling. + "ms-python.python", + // Python language server with rich type analysis and IntelliSense. + "ms-python.vscode-pylance", + // PowerShell language support and integrated terminal tooling. + "ms-vscode.powershell", + // Alternating indentation colors for improved readability. + "oderwat.indent-rainbow", + // OpenCode AI agent integration. + "sst-dev.opencode", + // Spell checking for code comments, markdown, and documentation. + "streetsidesoftware.code-spell-checker", + // Quickly sort selected lines alphabetically. + "tyriar.sort-lines", + // Markdown authoring enhancements including shortcuts and TOC generation. + "yzhang.markdown-all-in-one" + ], + "settings": { + // Preserve whitespace-only changes in diff view. + "diffEditor.ignoreTrimWhitespace": false, + // Suggest symbols from namespaces not yet imported. + "dotnet.completion.showCompletionItemsFromUnimportedNamespaces": true, + // Display bracket pair guide lines for nested scopes. + "editor.guides.bracketPairs": true, + // Enable inline AI/code completion suggestions. + "editor.inlineSuggest.enabled": true, + // Automatically format supported files on save. + "editor.formatOnSave": true, + // Ensure files end with a trailing newline. + "files.insertFinalNewline": true, + // Remove trailing whitespace during save operations. + "files.trimTrailingWhitespace": true, + // Disable automatic file encoding detection for consistency. + "files.autoGuessEncoding": false, + // UTF-8 without BOM is correct for a Linux container environment. + "files.encoding": "utf8", + // Use LF line endings for cross-platform consistency. + "files.eol": "\n", + // Automatically fetch remote git changes periodically. + "git.autofetch": true, + // "terminal.integrated.defaultProfile.linux": "bash" // default to bash + // "terminal.integrated.defaultProfile.linux": "zsh" // default to zsh + // Use PowerShell as the default integrated terminal shell. + "terminal.integrated.defaultProfile.linux": "pwsh" + } + } + }, + // One-time setup after container creation. + //"postCreateCommand": "bash .devcontainer/post-create.sh > /tmp/post-create.log 2>&1 || (cat /tmp/post-create.log && exit 1)", + // "postCreateCommand": "bash .devcontainer/post-create.sh 2>&1 | tee /workspace/.devcontainer/post-create.log; exit ${PIPESTATUS[0]}", // Post Create debug; renders log + "postCreateCommand": "bash .devcontainer/post-create.sh", + // Runs whenever the container starts. + "postStartCommand": "bash .devcontainer/post-start.sh", + // Use non-root user. + "remoteUser": "vscode", + // Sync UID/GID when supported. + "updateRemoteUserUID": true, + // Mounts: + // - claude-data: named Docker volume for all Claude state (auth, plugins, marketplace cache, history). + // Keeps everything writable inside the container without host-path dependencies. + // - claude-runtime: separate named volume for volatile plugin/LSP runtime state. + // Kept separate so a claude-data wipe doesn't orphan running processes. + // - nuget-cache: persists NuGet packages across rebuilds (also declared in docker-compose.yml). + "mounts": [ + "source=claude-data,target=/home/vscode/.claude,type=volume", + "source=claude-runtime,target=/home/vscode/.claude-runtime,type=volume" + ] +} diff --git a/.devcontainer/docker-compose.local.example.yml b/.devcontainer/docker-compose.local.example.yml new file mode 100644 index 0000000..4cc07f3 --- /dev/null +++ b/.devcontainer/docker-compose.local.example.yml @@ -0,0 +1,24 @@ +# Example local overrides for the devcontainer. +# Copy this file to docker-compose.local.yml and fill in the values below, +# or export the variables in your shell / .env file. +# +# docker-compose.local.yml is git-ignored — your personal paths never get committed. + +services: + devcontainer: + volumes: + # Persist Claude authentication tokens from the host so you don't need to + # re-authenticate after every container rebuild. + - ${CLAUDE_DIR}/auth:/home/vscode/.claude/auth + + # Share personal Claude skills/prompts from the host into the container. + - ${CLAUDE_DIR}/skills:/home/vscode/.claude/skills:ro + + # SSH keys for git access (read-only). + - ${SSH_DIR}:/home/vscode/.ssh:ro + + # Host git configuration. + # Configure on the host with: + # git config --global user.name "Your Name" + # git config --global user.email "you@example.com" + - ${GITCONFIG_PATH}:/home/vscode/.gitconfig:ro diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml new file mode 100644 index 0000000..e4cca4d --- /dev/null +++ b/.devcontainer/docker-compose.yml @@ -0,0 +1,26 @@ +services: + devcontainer: + build: + context: .. + dockerfile: .devcontainer/Dockerfile + + volumes: + # Persist NuGet packages between rebuilds. + - nuget-cache:/home/vscode/.nuget/packages + + # Mount the repository into the container. + - ..:/workspace:cached + + working_dir: /workspace + + init: true + stdin_open: true + tty: true + + environment: + DOTNET_NOLOGO: "1" + DOTNET_CLI_TELEMETRY_OPTOUT: "1" + NUGET_XMLDOC_MODE: "skip" + +volumes: + nuget-cache: \ No newline at end of file diff --git a/.devcontainer/dotfiles/.editorconfig b/.devcontainer/dotfiles/.editorconfig new file mode 100644 index 0000000..4249691 --- /dev/null +++ b/.devcontainer/dotfiles/.editorconfig @@ -0,0 +1,40 @@ +# Top-most EditorConfig file. +root = true + +# ----------------------------------------------------------------------------- +# Global defaults +# ----------------------------------------------------------------------------- +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +# ----------------------------------------------------------------------------- +# C# +# ----------------------------------------------------------------------------- +[*.cs] +indent_style = space +indent_size = 4 + +# ----------------------------------------------------------------------------- +# JSON / YAML / Markdown +# ----------------------------------------------------------------------------- +[*.{json,jsonc,yml,yaml,md}] +indent_style = space +indent_size = 2 + +# ----------------------------------------------------------------------------- +# PowerShell +# ----------------------------------------------------------------------------- +[*.ps1] +indent_style = space +indent_size = 4 + +# ----------------------------------------------------------------------------- +# Markdown +# Preserve trailing whitespace since it has semantic meaning +# (hard line breaks in CommonMark/GitHub Flavored Markdown). +# ----------------------------------------------------------------------------- +[*.md] +trim_trailing_whitespace = false \ No newline at end of file diff --git a/.devcontainer/dotfiles/opencode.jsonc b/.devcontainer/dotfiles/opencode.jsonc new file mode 100644 index 0000000..653d493 --- /dev/null +++ b/.devcontainer/dotfiles/opencode.jsonc @@ -0,0 +1,4 @@ +{ + "$schema": "https://opencode.ai/config.json", + "lsp": true +} diff --git a/.devcontainer/dotfiles/powershell-profile.ps1 b/.devcontainer/dotfiles/powershell-profile.ps1 new file mode 100644 index 0000000..590d526 --- /dev/null +++ b/.devcontainer/dotfiles/powershell-profile.ps1 @@ -0,0 +1,14 @@ +# PowerShell profile loaded inside the container. + +# Configure Oh My Posh +oh-my-posh init pwsh | Invoke-Expression + +# Better directory listings. +Set-Alias ll Get-ChildItem + +# Common git shortcuts. +function gs { git status } +function gl { git log --oneline --graph --decorate -20 } + +# Improve terminal experience. +$PSStyle.FileInfo.Directory = "`e[36m" diff --git a/.devcontainer/dotfiles/pre-commit-config.yaml b/.devcontainer/dotfiles/pre-commit-config.yaml new file mode 100644 index 0000000..71a3fc7 --- /dev/null +++ b/.devcontainer/dotfiles/pre-commit-config.yaml @@ -0,0 +1,38 @@ +# Pre-commit hook configuration. +# Install with: pre-commit install +# Run on all files: pre-commit run --all-files +# Update hook versions: pre-commit autoupdate +# +# See https://pre-commit.com for more information. + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + args: [--maxkb=512] + + - repo: https://github.com/codespell-project/codespell + rev: v2.4.1 + hooks: + - id: codespell + args: [--ignore-words-list=fo] + types_or: [csharp, python, javascript, ts, markdown, shell, yaml, json] + + - repo: https://github.com/igorshubovych/markdownlint-cli + rev: v0.44.0 + hooks: + - id: markdownlint + args: [--disable, MD013] + + - repo: local + hooks: + - id: csharpier + name: CSharpier + entry: dotnet csharpier + language: system + types: [csharp] + pass_filenames: false \ No newline at end of file diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh new file mode 100644 index 0000000..6dd8027 --- /dev/null +++ b/.devcontainer/post-create.sh @@ -0,0 +1,208 @@ +#!/usr/bin/env bash + +set -e + +echo "Running post-create setup..." + +USER_HOME="/home/vscode" + +export GIT_CONFIG_GLOBAL=/home/vscode/.config/git/config +mkdir -p "$(dirname "$GIT_CONFIG_GLOBAL")" + +# --------------------------------------------------------------------------- +# Zsh / Oh My Zsh +# --------------------------------------------------------------------------- + +# Install Oh My Zsh (unattended) as the vscode user. +if [ ! -d "$USER_HOME/.oh-my-zsh" ]; then + sudo -u vscode sh -c \ + "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended +fi + +# Set zsh as default shell. +sudo chsh -s /usr/bin/zsh vscode || true + +# Write files as root then chown, since sudoers does not permit `sudo -u vscode tee`. +# Configure Oh My Posh for zsh. +cat > "$USER_HOME/.oh-my-posh.zsh" <<'EOF' +eval "$(oh-my-posh init zsh)" +EOF +chown vscode:vscode "$USER_HOME/.oh-my-posh.zsh" + +# Append to .zshrc only once. +if ! grep -q "oh-my-posh.zsh" "$USER_HOME/.zshrc" 2>/dev/null; then + cat >> "$USER_HOME/.zshrc" <<'EOF' + +source ~/.oh-my-posh.zsh + +# CSharpier installed to /root/.dotnet/tools (not on vscode user's PATH by default) +export PATH="$PATH:/root/.dotnet/tools" + +# CLI completions (silent if completion files not yet available). +if [ -f /usr/share/zsh/site-functions/_dotnet ]; then + source /usr/share/zsh/site-functions/_dotnet +fi + +complete -o nospace -C /usr/bin/git git 2>/dev/null || true + +# Aliases +alias ll="ls -alF" +alias gs="git status" + +EOF + chown vscode:vscode "$USER_HOME/.zshrc" +fi + +# --------------------------------------------------------------------------- +# dotnet global tools +# --------------------------------------------------------------------------- + +# Ensure global dotnet tools are on PATH for the remainder of this script. +export PATH="$PATH:/home/vscode/.dotnet/tools" + +echo "Installing dotnet global tools..." + +# Under `set -e` this aborts the entire script on any re-run (e.g. rebuild). +# This helper installs if absent, updates if present, and never fails the script. +dotnet_tool_ensure() { + local tool="$1" + if dotnet tool list -g | grep -q "^${tool}\b"; then + dotnet tool update -g "$tool" 2>&1 || true + else + dotnet tool install -g "$tool" 2>&1 || true + fi +} + +dotnet_tool_ensure dotnet-ef +dotnet_tool_ensure dotnet-format +dotnet_tool_ensure dotnet-monitor +dotnet_tool_ensure dotnet-outdated-tool +dotnet_tool_ensure dotnet-script +dotnet_tool_ensure dotnet-trace +dotnet_tool_ensure dotnet-counters +dotnet_tool_ensure dotnet-dump + +# --------------------------------------------------------------------------- +# Language servers +# --------------------------------------------------------------------------- + +echo "Installing language servers..." + +# Python language server. +if ! command -v pyright-langserver >/dev/null 2>&1; then + npm install -g pyright || true +fi + +# --------------------------------------------------------------------------- +# Claude Code environment +# --------------------------------------------------------------------------- + +echo "Configuring Claude Code environment..." + +# postCreateCommand runs as the remoteUser (vscode), not root. Named volumes may +# be mounted with root ownership if they pre-existed before the Dockerfile fix. +# Use sudo (runs as root) to create and fix ownership of the volume directories, +# then subsequent operations run as vscode without issue. +sudo mkdir -p \ + /home/vscode/.claude-runtime/plugins \ + /home/vscode/.claude-runtime/marketplaces \ + /home/vscode/.claude-runtime/cache \ + /home/vscode/.claude/plugins \ + /home/vscode/.claude/marketplaces \ + /home/vscode/.claude/cache + +sudo chown -R vscode:vscode \ + /home/vscode/.claude-runtime \ + /home/vscode/.claude/plugins \ + /home/vscode/.claude/marketplaces \ + /home/vscode/.claude/cache + +# Redirect volatile state into the runtime volume. +rm -rf /home/vscode/.claude/plugins +ln -sfn /home/vscode/.claude-runtime/plugins /home/vscode/.claude/plugins +rm -rf /home/vscode/.claude/marketplaces +ln -sfn /home/vscode/.claude-runtime/marketplaces /home/vscode/.claude/marketplaces +rm -rf /home/vscode/.claude/cache +ln -sfn /home/vscode/.claude-runtime/cache /home/vscode/.claude/cache + +# Copy workspace skills into the volume (no-clobber so user edits are preserved). +if [ -d "/workspace/.claude-skills" ]; then + mkdir -p /home/vscode/.claude/skills + cp -rn /workspace/.claude-skills/. /home/vscode/.claude/skills/ +fi + +# --------------------------------------------------------------------------- +# Configuring Claude Marketplace and adding Code Plugins +# --------------------------------------------------------------------------- + +echo "Installing Claude Code plugins..." + +if command -v claude > /dev/null 2>&1; then + # Register the official Anthropic marketplace if not already present. + claude plugin marketplace add anthropics/claude-plugins-official 2>/dev/null || true + + # Now update and install. + claude plugin marketplace update claude-plugins-official || true + claude plugin install csharp-lsp@claude-plugins-official || true + claude plugin install pyright-lsp@claude-plugins-official || true +else + echo "claude CLI not found — skipping plugin installs." +fi + +# --------------------------------------------------------------------------- +# OpenCode config +# --------------------------------------------------------------------------- + +mkdir -p /home/vscode/.config/opencode +cp .devcontainer/dotfiles/opencode.jsonc /home/vscode/.config/opencode/opencode.jsonc +chown vscode:vscode /home/vscode/.config/opencode/opencode.jsonc + +# --------------------------------------------------------------------------- +# Pre-commit hooks +# --------------------------------------------------------------------------- + +if command -v pip > /dev/null 2>&1; then + pip install --quiet pre-commit codespell 2>/dev/null || true +fi + +# Install the pre-commit config to the workspace root. +if [ -f .devcontainer/dotfiles/pre-commit-config.yaml ] && [ ! -f /workspace/.pre-commit-config.yaml ]; then + cp .devcontainer/dotfiles/pre-commit-config.yaml /workspace/.pre-commit-config.yaml + chown vscode:vscode /workspace/.pre-commit-config.yaml + echo "Pre-commit config deployed to /workspace/.pre-commit-config.yaml" + echo " Activate with: cd /workspace && pre-commit install" +fi + +# --------------------------------------------------------------------------- +# Solution restore +# --------------------------------------------------------------------------- + +sln_count=$(find . -name "*.sln" -maxdepth 2 | wc -l) +if [ "$sln_count" -eq 0 ]; then + echo "No .sln files found — skipping NuGet restore." + echo " (Run 'dotnet restore' manually or use /init to scaffold a project.)" +else + find . -name "*.sln" -maxdepth 2 -print0 | while IFS= read -r -d '' sln; do + echo "Restoring: $sln" + dotnet restore "$sln" || echo "WARNING: restore failed for $sln — continuing." + done +fi + +# --------------------------------------------------------------------------- +# PowerShell profile +# --------------------------------------------------------------------------- + +PWSH_PROFILE_DIR="/home/vscode/.config/powershell" +PWSH_PROFILE="$PWSH_PROFILE_DIR/Microsoft.PowerShell_profile.ps1" + +mkdir -p "$PWSH_PROFILE_DIR" + +# Write as root then chown, since sudoers does not permit `sudo -u vscode tee`. +if ! grep -q "oh-my-posh" "$PWSH_PROFILE" 2>/dev/null; then + cat >> "$PWSH_PROFILE" <<'EOF' +oh-my-posh init pwsh | Invoke-Expression +EOF + chown vscode:vscode "$PWSH_PROFILE" +fi + +echo "Post-create complete." diff --git a/.devcontainer/post-start.sh b/.devcontainer/post-start.sh new file mode 100644 index 0000000..274fd0e --- /dev/null +++ b/.devcontainer/post-start.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +set -e + +# Ensure workspace is trusted by git. +git config --global --add safe.directory "$(pwd)" + +# Standardize line ending handling. +git config --global core.autocrlf false + +# Avoid detached head warnings in some container scenarios. +git config --global advice.detachedHead false \ No newline at end of file diff --git a/.editorconfig b/.editorconfig index 34176d3..9697655 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,4 +1,4 @@ -# Root .editorconfig as of 2025-02-03 +# Root .editorconfig as of 2026-06-07 root = true @@ -35,3 +35,599 @@ end_of_line = crlf # XML [*.xml] indent_size = 2 + +# === .NET/C# coding conventions (merged from src/.editorconfig) === +# Visual Studio Solutions +[*.sln] +indent_style = tab + +# Visual Studio Project Files +[*.csproj] +indent_size = 2 + +# C# files +[{src,smoketests}/**/*.cs] + +#### Core EditorConfig Options #### + +# Indentation and spacing +indent_size = 4 + +# New line preferences +end_of_line = crlf +insert_final_newline = true + +#### .NET Coding Conventions #### + +# Organize usings +dotnet_separate_import_directive_groups = false +dotnet_sort_system_directives_first = true +file_header_template = unset + +# this. and Me. preferences +dotnet_style_qualification_for_event = false:silent +dotnet_style_qualification_for_field = false:silent +dotnet_style_qualification_for_method = false:silent +dotnet_style_qualification_for_property = false:silent + +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true:silent +dotnet_style_predefined_type_for_member_access = true:silent + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent + +# Expression-level preferences +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_object_initializer = true:suggestion +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_prefer_auto_properties = true:suggestion +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion +dotnet_style_prefer_conditional_expression_over_return = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_simplified_interpolation = true:suggestion + +# Field preferences +dotnet_style_readonly_field = true:warning + +# Parameter preferences +dotnet_code_quality_unused_parameters = all:suggestion + +# Suppression preferences +dotnet_remove_unnecessary_suppression_exclusions = None + +#### C# Coding Conventions #### +# var preferences +csharp_style_var_elsewhere = true:suggestion +csharp_style_var_for_built_in_types = true:warning +csharp_style_var_when_type_is_apparent = true:warning + + +# Expression-bodied members +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_lambdas = true:suggestion +csharp_style_expression_bodied_local_functions = false:silent +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent + +# Pattern matching preferences +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_prefer_not_pattern = true:suggestion +csharp_style_prefer_pattern_matching = true:suggestion +csharp_style_prefer_switch_expression = true:suggestion + +# Null-checking preferences +csharp_style_conditional_delegate_call = true:suggestion + +# Modifier preferences +csharp_prefer_static_local_function = true:warning +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:silent + +# Code-block preferences +csharp_prefer_braces = true:warning +csharp_prefer_simple_using_statement = true:suggestion + +# Expression-level preferences +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_prefer_index_operator = true:suggestion +csharp_style_prefer_range_operator = true:suggestion +csharp_style_throw_expression = true:suggestion +csharp_style_unused_value_assignment_preference = discard_variable:suggestion +csharp_style_unused_value_expression_statement_preference = discard_variable:silent + +# 'using' directive preferences +csharp_using_directive_placement = outside_namespace:silent + +#### C# Formatting Rules #### + +# New line preferences +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = all +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_labels = no_change +csharp_indent_switch_labels = true + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Wrapping preferences +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true +csharp_style_namespace_declarations = block_scoped:silent +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_prefer_top_level_statements = true:silent +csharp_style_prefer_null_check_over_type_check = true:suggestion +csharp_style_prefer_local_over_anonymous_function = true:suggestion +csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion +csharp_style_prefer_tuple_swap = true:suggestion +csharp_style_prefer_utf8_string_literals = true:suggestion +csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:silent +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent +csharp_style_prefer_extended_property_pattern = true:suggestion + +#### Naming styles #### +# Naming rules +dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.symbols = types_and_namespaces +dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.interfaces_should_be_ipascalcase.severity = suggestion +dotnet_naming_rule.interfaces_should_be_ipascalcase.symbols = interfaces +dotnet_naming_rule.interfaces_should_be_ipascalcase.style = ipascalcase + +dotnet_naming_rule.type_parameters_should_be_tpascalcase.severity = suggestion +dotnet_naming_rule.type_parameters_should_be_tpascalcase.symbols = type_parameters +dotnet_naming_rule.type_parameters_should_be_tpascalcase.style = tpascalcase + +dotnet_naming_rule.methods_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.methods_should_be_pascalcase.symbols = methods +dotnet_naming_rule.methods_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.properties_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.properties_should_be_pascalcase.symbols = properties +dotnet_naming_rule.properties_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.events_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.events_should_be_pascalcase.symbols = events +dotnet_naming_rule.events_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.local_variables_should_be_camelcase.severity = suggestion +dotnet_naming_rule.local_variables_should_be_camelcase.symbols = local_variables +dotnet_naming_rule.local_variables_should_be_camelcase.style = camelcase + +dotnet_naming_rule.local_constants_should_be_camelcase.severity = suggestion +dotnet_naming_rule.local_constants_should_be_camelcase.symbols = local_constants +dotnet_naming_rule.local_constants_should_be_camelcase.style = camelcase + +dotnet_naming_rule.parameters_should_be_camelcase.severity = suggestion +dotnet_naming_rule.parameters_should_be_camelcase.symbols = parameters +dotnet_naming_rule.parameters_should_be_camelcase.style = camelcase + +dotnet_naming_rule.public_fields_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.public_fields_should_be_pascalcase.symbols = public_fields +dotnet_naming_rule.public_fields_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.private_fields_should_be__camelcase.severity = suggestion +dotnet_naming_rule.private_fields_should_be__camelcase.symbols = private_fields +dotnet_naming_rule.private_fields_should_be__camelcase.style = _camelcase + +dotnet_naming_rule.private_static_fields_should_be_s_camelcase.severity = suggestion +dotnet_naming_rule.private_static_fields_should_be_s_camelcase.symbols = private_static_fields +dotnet_naming_rule.private_static_fields_should_be_s_camelcase.style = _camelcase + +dotnet_naming_rule.public_constant_fields_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.public_constant_fields_should_be_pascalcase.symbols = public_constant_fields +dotnet_naming_rule.public_constant_fields_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.private_constant_fields_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.private_constant_fields_should_be_pascalcase.symbols = private_constant_fields +dotnet_naming_rule.private_constant_fields_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.symbols = public_static_readonly_fields +dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.symbols = private_static_readonly_fields +dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.enums_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.enums_should_be_pascalcase.symbols = enums +dotnet_naming_rule.enums_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.local_functions_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.local_functions_should_be_pascalcase.symbols = local_functions +dotnet_naming_rule.local_functions_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.non_field_members_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascalcase.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascalcase.style = pascalcase + +# Symbol specifications + +dotnet_naming_symbols.interfaces.applicable_kinds = interface +dotnet_naming_symbols.interfaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interfaces.required_modifiers = + +dotnet_naming_symbols.enums.applicable_kinds = enum +dotnet_naming_symbols.enums.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.enums.required_modifiers = + +dotnet_naming_symbols.events.applicable_kinds = event +dotnet_naming_symbols.events.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.events.required_modifiers = + +dotnet_naming_symbols.methods.applicable_kinds = method +dotnet_naming_symbols.methods.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.methods.required_modifiers = + +dotnet_naming_symbols.properties.applicable_kinds = property +dotnet_naming_symbols.properties.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.properties.required_modifiers = + +dotnet_naming_symbols.public_fields.applicable_kinds = field +dotnet_naming_symbols.public_fields.applicable_accessibilities = public, internal +dotnet_naming_symbols.public_fields.required_modifiers = + +dotnet_naming_symbols.private_fields.applicable_kinds = field +dotnet_naming_symbols.private_fields.applicable_accessibilities = private, protected, protected_internal, private_protected +dotnet_naming_symbols.private_fields.required_modifiers = + +dotnet_naming_symbols.private_static_fields.applicable_kinds = field +dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private, protected, protected_internal, private_protected +dotnet_naming_symbols.private_static_fields.required_modifiers = static + +dotnet_naming_symbols.types_and_namespaces.applicable_kinds = namespace, class, struct, interface, enum +dotnet_naming_symbols.types_and_namespaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types_and_namespaces.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +dotnet_naming_symbols.type_parameters.applicable_kinds = namespace +dotnet_naming_symbols.type_parameters.applicable_accessibilities = * +dotnet_naming_symbols.type_parameters.required_modifiers = + +dotnet_naming_symbols.private_constant_fields.applicable_kinds = field +dotnet_naming_symbols.private_constant_fields.applicable_accessibilities = private, protected, protected_internal, private_protected +dotnet_naming_symbols.private_constant_fields.required_modifiers = const + +dotnet_naming_symbols.local_variables.applicable_kinds = local +dotnet_naming_symbols.local_variables.applicable_accessibilities = local +dotnet_naming_symbols.local_variables.required_modifiers = + +dotnet_naming_symbols.local_constants.applicable_kinds = local +dotnet_naming_symbols.local_constants.applicable_accessibilities = local +dotnet_naming_symbols.local_constants.required_modifiers = const + +dotnet_naming_symbols.parameters.applicable_kinds = parameter +dotnet_naming_symbols.parameters.applicable_accessibilities = * +dotnet_naming_symbols.parameters.required_modifiers = + +dotnet_naming_symbols.public_constant_fields.applicable_kinds = field +dotnet_naming_symbols.public_constant_fields.applicable_accessibilities = public, internal +dotnet_naming_symbols.public_constant_fields.required_modifiers = const + +dotnet_naming_symbols.public_static_readonly_fields.applicable_kinds = field +dotnet_naming_symbols.public_static_readonly_fields.applicable_accessibilities = public, internal +dotnet_naming_symbols.public_static_readonly_fields.required_modifiers = readonly, static + +dotnet_naming_symbols.private_static_readonly_fields.applicable_kinds = field +dotnet_naming_symbols.private_static_readonly_fields.applicable_accessibilities = private, protected, protected_internal, private_protected +dotnet_naming_symbols.private_static_readonly_fields.required_modifiers = readonly, static + +dotnet_naming_symbols.local_functions.applicable_kinds = local_function +dotnet_naming_symbols.local_functions.applicable_accessibilities = * +dotnet_naming_symbols.local_functions.required_modifiers = + +# Naming styles + +dotnet_naming_style.pascalcase.required_prefix = +dotnet_naming_style.pascalcase.required_suffix = +dotnet_naming_style.pascalcase.word_separator = +dotnet_naming_style.pascalcase.capitalization = pascal_case + +dotnet_naming_style.ipascalcase.required_prefix = I +dotnet_naming_style.ipascalcase.required_suffix = +dotnet_naming_style.ipascalcase.word_separator = +dotnet_naming_style.ipascalcase.capitalization = pascal_case + +dotnet_naming_style.tpascalcase.required_prefix = T +dotnet_naming_style.tpascalcase.required_suffix = +dotnet_naming_style.tpascalcase.word_separator = +dotnet_naming_style.tpascalcase.capitalization = pascal_case + +dotnet_naming_style._camelcase.required_prefix = _ +dotnet_naming_style._camelcase.required_suffix = +dotnet_naming_style._camelcase.word_separator = +dotnet_naming_style._camelcase.capitalization = camel_case + +dotnet_naming_style.camelcase.required_prefix = +dotnet_naming_style.camelcase.required_suffix = +dotnet_naming_style.camelcase.word_separator = +dotnet_naming_style.camelcase.capitalization = camel_case + +dotnet_naming_style.s_camelcase.required_prefix = s_ +dotnet_naming_style.s_camelcase.required_suffix = +dotnet_naming_style.s_camelcase.word_separator = +dotnet_naming_style.s_camelcase.capitalization = camel_case +dotnet_style_namespace_match_folder = true:suggestion +dotnet_style_allow_multiple_blank_lines_experimental = true:suggestion +dotnet_style_allow_statement_immediately_after_block_experimental = true:silent + +# AnalyzerId= AsyncUsageAnalyzers RuleNamespace= AsyncUsageAnalyzers +dotnet_diagnostic.UseConfigureAwait.severity = Warning + +# AnalyzerId= DocumentationAnalyzers RuleNamespace= DocumentationAnalyzers +dotnet_diagnostic.DOC100.severity = Warning +dotnet_diagnostic.DOC101.severity = Warning +dotnet_diagnostic.DOC102.severity = Warning +dotnet_diagnostic.DOC103.severity = Warning +dotnet_diagnostic.DOC104.severity = Warning +dotnet_diagnostic.DOC105.severity = Warning +dotnet_diagnostic.DOC106.severity = Warning +dotnet_diagnostic.DOC107.severity = Warning +dotnet_diagnostic.DOC108.severity = Warning +dotnet_diagnostic.DOC200.severity = Warning +dotnet_diagnostic.DOC201.severity = Warning +dotnet_diagnostic.DOC202.severity = Warning +dotnet_diagnostic.DOC203.severity = Warning +dotnet_diagnostic.DOC204.severity = Warning + +# AnalyzerId= Microsoft.Analyzers.ManagedCodeAnalysis RuleNamespace= Microsoft.Rules.Managed +dotnet_diagnostic.CA1001.severity = Warning +dotnet_diagnostic.CA1009.severity = Warning +dotnet_diagnostic.CA1016.severity = Warning +dotnet_diagnostic.CA1033.severity = Warning +dotnet_diagnostic.CA1049.severity = Warning +dotnet_diagnostic.CA1056.severity = Warning +dotnet_diagnostic.CA1060.severity = Warning +dotnet_diagnostic.CA1061.severity = Warning +dotnet_diagnostic.CA1063.severity = Warning +dotnet_diagnostic.CA1065.severity = Warning +dotnet_diagnostic.CA1301.severity = Warning +dotnet_diagnostic.CA1400.severity = Warning +dotnet_diagnostic.CA1401.severity = Warning +dotnet_diagnostic.CA1403.severity = Warning +dotnet_diagnostic.CA1404.severity = Warning +dotnet_diagnostic.CA1405.severity = Warning +dotnet_diagnostic.CA1410.severity = Warning +dotnet_diagnostic.CA1415.severity = Warning +dotnet_diagnostic.CA1821.severity = Warning +dotnet_diagnostic.CA1825.severity = Warning +dotnet_diagnostic.CA1900.severity = Warning +dotnet_diagnostic.CA1901.severity = Warning +dotnet_diagnostic.CA2000.severity = Warning +dotnet_diagnostic.CA2002.severity = Warning +dotnet_diagnostic.CA2100.severity = Warning +dotnet_diagnostic.CA2101.severity = Warning +dotnet_diagnostic.CA2108.severity = Warning +dotnet_diagnostic.CA2111.severity = Warning +dotnet_diagnostic.CA2112.severity = Warning +dotnet_diagnostic.CA2114.severity = Warning +dotnet_diagnostic.CA2116.severity = Warning +dotnet_diagnostic.CA2117.severity = Warning +dotnet_diagnostic.CA2122.severity = Warning +dotnet_diagnostic.CA2123.severity = Warning +dotnet_diagnostic.CA2124.severity = Warning +dotnet_diagnostic.CA2126.severity = Warning +dotnet_diagnostic.CA2131.severity = Warning +dotnet_diagnostic.CA2132.severity = Warning +dotnet_diagnostic.CA2133.severity = Warning +dotnet_diagnostic.CA2134.severity = Warning +dotnet_diagnostic.CA2137.severity = Warning +dotnet_diagnostic.CA2138.severity = Warning +dotnet_diagnostic.CA2140.severity = Warning +dotnet_diagnostic.CA2141.severity = Warning +dotnet_diagnostic.CA2146.severity = Warning +dotnet_diagnostic.CA2147.severity = Warning +dotnet_diagnostic.CA2149.severity = Warning +dotnet_diagnostic.CA2200.severity = Warning +dotnet_diagnostic.CA2202.severity = Warning +dotnet_diagnostic.CA2207.severity = Warning +dotnet_diagnostic.CA2212.severity = Warning +dotnet_diagnostic.CA2213.severity = Warning +dotnet_diagnostic.CA2214.severity = Warning +dotnet_diagnostic.CA2215.severity = Warning +dotnet_diagnostic.CA2216.severity = Warning +dotnet_diagnostic.CA2220.severity = Warning +dotnet_diagnostic.CA2229.severity = Warning +dotnet_diagnostic.CA2231.severity = Warning +dotnet_diagnostic.CA2232.severity = Warning +dotnet_diagnostic.CA2235.severity = Warning +dotnet_diagnostic.CA2236.severity = Warning +dotnet_diagnostic.CA2237.severity = Warning +dotnet_diagnostic.CA2238.severity = Warning +dotnet_diagnostic.CA2240.severity = Warning +dotnet_diagnostic.CA2241.severity = Warning +dotnet_diagnostic.CA2242.severity = Warning +dotnet_diagnostic.CA2253.severity = Warning +dotnet_diagnostic.CA2254.severity = Warning + +# AnalyzerId= Microsoft.CodeAnalysis.CSharp RuleNamespace= Microsoft.CodeAnalysis.CSharp +dotnet_diagnostic.CS8019.severity = Warning + +# AnalyzerId= Microsoft.CodeAnalysis.CSharp.Analyzers RuleNamespace= Microsoft.CodeAnalysis.CSharp.Analyzers +dotnet_diagnostic.RS1022.severity = Warning + +# AnalyzerId= Microsoft.CodeAnalysis.CSharp.CodeStyle RuleNamespace= Microsoft.CodeAnalysis.CSharp.CodeStyle +dotnet_diagnostic.IDE0055.severity = None +dotnet_diagnostic.IDE0079.severity = Warning +dotnet_diagnostic.IDE1006.severity = Warning + +# AnalyzerId= Microsoft.CodeAnalysis.CSharp.Features RuleNamespace= Microsoft.CodeAnalysis.CSharp.Features +dotnet_diagnostic.IDE0003.severity = None +dotnet_diagnostic.IDE0007.severity = Warning +dotnet_diagnostic.IDE0009.severity = None + +# AnalyzerId= StyleCop.Analyzers RuleNamespace= StyleCop.Analyzers +dotnet_diagnostic.SA1000.severity = None +dotnet_diagnostic.SA1010.severity = None +dotnet_diagnostic.SA1003.severity = None +dotnet_diagnostic.SA1005.severity = None +dotnet_diagnostic.SA1008.severity = None +dotnet_diagnostic.SA1009.severity = None +dotnet_diagnostic.SA1012.severity = None +dotnet_diagnostic.SA1013.severity = None +dotnet_diagnostic.SA1025.severity = None +dotnet_diagnostic.SA1101.severity = None +dotnet_diagnostic.SA1108.severity = None +dotnet_diagnostic.SA1111.severity = None +dotnet_diagnostic.SA1116.severity = None +dotnet_diagnostic.SA1118.severity = None +dotnet_diagnostic.SA1123.severity = None +dotnet_diagnostic.SA1124.severity = None +dotnet_diagnostic.SA1127.severity = None +dotnet_diagnostic.SA1128.severity = None +dotnet_diagnostic.SA1137.severity = None +dotnet_diagnostic.SA1201.severity = None +dotnet_diagnostic.SA1202.severity = None +dotnet_diagnostic.SA1203.severity = None +dotnet_diagnostic.SA1204.severity = None +dotnet_diagnostic.SA1309.severity = None +dotnet_diagnostic.SA1412.severity = Error +dotnet_diagnostic.SA1413.severity = None +dotnet_diagnostic.SA1500.severity = None +dotnet_diagnostic.SA1501.severity = None +dotnet_diagnostic.SA1502.severity = None +dotnet_diagnostic.SA1504.severity = None +dotnet_diagnostic.SA1512.severity = None +dotnet_diagnostic.SA1513.severity = None +dotnet_diagnostic.SA1515.severity = None +dotnet_diagnostic.SA1516.severity = None + +# Documentation Rules (SA1600-) https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/DocumentationRules.md +dotnet_diagnostic.SA1600.severity = Warning # A C# code element is missing a documentation header. +dotnet_diagnostic.SA1601.severity = Warning # A C# partial element is missing a documentation header. +dotnet_diagnostic.SA1602.severity = Warning # An item within a C# enumeration is missing an Xml documentation header. +dotnet_diagnostic.SA1603.severity = Warning # The Xml within a C# element's document header is badly formed. +dotnet_diagnostic.SA1604.severity = Warning # The Xml header documentation for a C# element is missing a tag. +dotnet_diagnostic.SA1605.severity = Warning # The or tag within the documentation header for a C# code element is missing or empty. +dotnet_diagnostic.SA1606.severity = Warning # The tag within the documentation header for a C# code element is empty. +dotnet_diagnostic.SA1607.severity = Warning # The or tag within the documentation header for a C# code element is empty. +dotnet_diagnostic.SA1608.severity = Warning # The tag within an element's Xml header documentation contains the default text generated by Visual Studio during the creation of the element. +dotnet_diagnostic.SA1609.severity = Warning # The Xml header documentation for a C# property does not contain a tag. +dotnet_diagnostic.SA1610.severity = Warning # The Xml header documentation for a C# property contains an empty tag. +dotnet_diagnostic.SA1611.severity = Warning # A C# method, constructor, delegate or indexer element is missing documentation for one or more of its parameters. +dotnet_diagnostic.SA1612.severity = Warning # The documentation describing the parameters to a C# method, constructor, delegate or indexer element does not match the actual parameters on the element. +dotnet_diagnostic.SA1613.severity = Warning # A tag within a C# element's documentation header is missing a name attribute containing the name of the parameter. +dotnet_diagnostic.SA1614.severity = Warning # A tag within a C# element's documentation header is empty. +dotnet_diagnostic.SA1615.severity = Warning # A C# element is missing documentation for its return value. +dotnet_diagnostic.SA1616.severity = Warning # The tag within a C# element's documentation header is empty. +dotnet_diagnostic.SA1617.severity = Warning # A C# code element does not contain a return value, or returns void, but the documentation header for the element contains a tag. +dotnet_diagnostic.SA1618.severity = Warning # A generic C# element is missing documentation for one or more of its generic type parameters. +dotnet_diagnostic.SA1619.severity = Warning # A generic, partial C# element is missing documentation for one or more of its generic type parameters, and the documentation for the element contains a tag. +dotnet_diagnostic.SA1620.severity = Warning # The tags within the Xml header documentation for a generic C# element do not match the generic type parameters on the element. +dotnet_diagnostic.SA1621.severity = Warning # A tag within the Xml header documentation for a generic C# element is missing a name attribute, or contains an empty name attribute. +dotnet_diagnostic.SA1622.severity = Warning # A tag within the Xml header documentation for a generic C# element is empty. +dotnet_diagnostic.SA1623.severity = Warning # The documentation text within a C# property's tag does not match the accessors within the property. +dotnet_diagnostic.SA1624.severity = Warning # The documentation text within a C# property's tag takes into account all of the accessors within the property, but one of the accessors has limited access. +dotnet_diagnostic.SA1625.severity = Warning # The Xml documentation for a C# element contains two or more identical entries, indicating that the documentation has been copied and pasted. +dotnet_diagnostic.SA1626.severity = Warning # The C# code contains a single-line comment which begins with three forward slashes in a row. +dotnet_diagnostic.SA1627.severity = Warning # The Xml header documentation for a C# code element contains an empty tag. +dotnet_diagnostic.SA1628.severity = Warning # A section of the Xml header documentation for a C# element does not begin with a capital letter. +dotnet_diagnostic.SA1629.severity = None # A section of the Xml header documentation for a C# element does not end with a period (also known as a full stop). +dotnet_diagnostic.SA1630.severity = Warning # A section of the Xml header documentation for a C# element does not contain any whitespace between words. +dotnet_diagnostic.SA1631.severity = Warning # A section of the Xml header documentation for a C# element does not contain enough alphabetic characters. +dotnet_diagnostic.SA1632.severity = None # From StyleCop 4.5 this rule is disabled by default. +dotnet_diagnostic.SA1633.severity = None # A C# code file is missing a standard file header. +dotnet_diagnostic.SA1634.severity = Warning # The file header at the top of a C# code file is missing a copyright tag. +dotnet_diagnostic.SA1635.severity = Warning # The file header at the top of a C# code file is missing copyright text. +dotnet_diagnostic.SA1636.severity = Warning # The file header at the top of a C# code file does not contain the appropriate copyright text. +dotnet_diagnostic.SA1637.severity = Warning # The file header at the top of a C# code file is missing the file name. +dotnet_diagnostic.SA1638.severity = Warning # The file tag within the file header at the top of a C# code file does not contain the name of the file. +dotnet_diagnostic.SA1639.severity = Warning # The file header at the top of a C# code file does not contain a filled-in summary tag. +dotnet_diagnostic.SA1640.severity = Warning # The file header at the top of a C# code file does not contain company name text. +dotnet_diagnostic.SA1641.severity = Warning # The file header at the top of a C# code file does not contain the appropriate company name text. +dotnet_diagnostic.SA1642.severity = Warning # The XML documentation header for a C# constructor does not contain the appropriate summary text. +dotnet_diagnostic.SA1643.severity = Warning # The Xml documentation header for a C# finalizer does not contain the appropriate summary text. +dotnet_diagnostic.SA1644.severity = Warning # A section within the Xml documentation header for a C# element contains blank lines. +dotnet_diagnostic.SA1645.severity = Warning # An included Xml documentation file does not exist. +dotnet_diagnostic.SA1646.severity = Warning # An included Xml documentation link contains an invalid path. +dotnet_diagnostic.SA1647.severity = Warning # An include tag within an Xml documentation header does not contain valid file and path attribute. +dotnet_diagnostic.SA1648.severity = Warning # has been used on an element that doesn't inherit from a base class or implement an interface. +dotnet_diagnostic.SA1649.severity = Warning # The file name of a C# code file does not match the first type declared in the file. +dotnet_diagnostic.SA1650.severity = Warning # The element documentation for the element contains one or more spelling mistakes or unrecognized words. +dotnet_diagnostic.SA1651.severity = Warning # The documentation for the element contains one or more elements. +dotnet_diagnostic.SA1652.severity = None # This rule was moved to SA0001 + +# Sonar +dotnet_diagnostic.S107.severity = Suggestion # Methods should not have too many parameters +dotnet_diagnostic.S1210.severity = Suggestion # "Equals" and the comparison operators should be overridden when implementing "IComparable" +dotnet_diagnostic.S2436.severity = Suggestion # Types and methods should not have too many generic parameters +dotnet_diagnostic.S4457.severity = Warning # Parameter validation in "async"/"await" methods should be wrapped + +# C# Autogenerated code +[{**/obj/**,**/bin/**}] +# AnalyzerId= Microsoft.CodeAnalysis.CSharp RuleNamespace= Microsoft.CodeAnalysis.CSharp +dotnet_diagnostic.CS8019.severity = none + +# C# Tests files in Tests projects +[{**Tests/**Tests.cs,**Tests.**/**Tests.cs}] +# Reset StyleCop documentation comments +dotnet_diagnostic.SA1600.severity = None # A C# code element is missing a documentation header. +dotnet_diagnostic.SA1601.severity = None # A C# partial element is missing a documentation header. +dotnet_diagnostic.SA1602.severity = None # An item within a C# enumeration is missing an Xml documentation header. + +# Sonar +dotnet_diagnostic.S4114.severity = Suggestion # Methods should not have identical implementations + +# Microsoft.VisualStudio.Threading +dotnet_diagnostic.VSTHRD200.severity = None # Use Async suffix for async methods + +# Misc +dotnet_diagnostic.CS1591.severity = None # Missing XML comment for publicly visible type or member 'Type_or_Member' +dotnet_diagnostic.CA1707.severity = None # Identifiers should not contain underscores +dotnet_diagnostic.RCS1192.severity = Warning # RCS1192: Unnecessary usage of verbatim string literal. diff --git a/.gitattributes b/.gitattributes index d6ad5f9..20c6716 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,26 +1,57 @@ +############################################################################### +# Git Attributes +# +# Normalize text files to LF in the repository. Git will handle conversion +# during checkout as needed, while .editorconfig controls the editor behavior. ############################################################################### -# Set default behavior to automatically normalize line endings. + +* text=auto eol=lf + ############################################################################### -* text=auto +# Language-specific diff drivers +############################################################################### + +*.cs diff=csharp ############################################################################### -# Set default behavior for command prompt diff. +# Merge behavior # -# This is need for earlier builds of msysgit that does not have it on by -# default for csharp files. -# Note: This is only used by command line +# Prevent Git from inserting conflict markers into Visual Studio solution and +# project files. These files require manual conflict resolution. +############################################################################### + +*.sln merge=binary +*.csproj merge=binary + +############################################################################### +# Linux / Cross-platform files ############################################################################### -*.cs diff=csharp + +*.bash text eol=lf +*.editorconfig text eol=lf +*.sh text eol=lf + +*.json text eol=lf +*.jsonc text eol=lf +*.md text eol=lf +*.xml text eol=lf +*.yaml text eol=lf +*.yml text eol=lf + +Dockerfile text eol=lf ############################################################################### -# Set the merge driver for project and solution files +# Windows-native files +############################################################################### + +*.bat text eol=crlf +*.cmd text eol=crlf + +############################################################################### +# PowerShell # -# Merging from the command prompt will add diff markers to the files if there -# are conflicts (Merging from VS is not affected by the settings below, in VS -# the diff markers are never inserted). Diff markers may cause the following -# file extensions to fail to load in VS. An alternative would be to treat -# these files as binary and thus will always conflict and require user -# intervention with every merge. To do so, just uncomment the entries below -############################################################################### -*.sln merge=binary -*.csproj merge=binary \ No newline at end of file +# PowerShell fully supports LF, making it the preferred choice for +# cross-platform repositories and devcontainers. +############################################################################### + +*.ps1 text eol=lf diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..5f28270 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.github/codeql/codeql-config.yml b/.github/codeql/codeql-config.yml new file mode 100644 index 0000000..9218266 --- /dev/null +++ b/.github/codeql/codeql-config.yml @@ -0,0 +1,12 @@ +name: "Arcus CodeQL Configuration" + +query-filters: + - exclude: + problem.severity: + - warning + - recommendation + - note + +paths-ignore: + - src/Arcus.Tests + - src/Arcus.Benchmarks diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1f617d9..99b3fb1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,9 +13,17 @@ on: branches: ["main"] workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} + cancel-in-progress: true + jobs: build: - runs-on: windows-latest + strategy: + fail-fast: false + matrix: + os: [windows-latest, ubuntu-latest] + runs-on: ${{ matrix.os }} env: HUSKY: 0 # don't run husky.net hooks @@ -26,22 +34,59 @@ jobs: - name: Setup .NET SDKs uses: actions/setup-dotnet@v5 with: - # .net 4.8.x is included by default in windows-latest dotnet-version: | 8.0.x 9.0.x 10.0.x + - name: NuGet cache + uses: actions/cache@v4 + with: + path: ~/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} + restore-keys: | + ${{ runner.os }}-nuget- + - name: Restore .NET tools run: dotnet tool restore - working-directory: . - name: Restore dependencies run: dotnet restore - working-directory: ./src - name: Build - run: dotnet build ./src --no-restore -p:VersionFromCI="0.0.0-cibuild" + run: dotnet build --no-restore -p:VersionFromCI="0.0.0-cibuild" + + - name: Test (Ubuntu — net8.0) + if: matrix.os == 'ubuntu-latest' + run: dotnet test --no-build --verbosity normal --logger trx --framework net8.0 + + - name: Test (Ubuntu — net9.0) + if: matrix.os == 'ubuntu-latest' + run: dotnet test --no-build --verbosity normal --logger trx --framework net9.0 + + - name: Test with coverage (Ubuntu — net10.0) + if: matrix.os == 'ubuntu-latest' + run: | + dotnet tool install --global dotnet-coverage + dotnet-coverage collect "dotnet test --no-build --verbosity normal --logger trx --framework net10.0" -f cobertura -o coverage/cobertura.xml + + - name: Test (Windows — all TFMs including net48) + if: matrix.os == 'windows-latest' + run: dotnet test --no-build --verbosity normal --logger trx + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results-${{ runner.os }} + path: '**/TestResults/*.trx' + retention-days: 30 + + - name: Upload coverage results + if: matrix.os == 'ubuntu-latest' + uses: actions/upload-artifact@v4 + with: + name: coverage-${{ github.sha }} + path: coverage/ + retention-days: 30 - - name: Test - run: dotnet test ./src --no-build --verbosity normal diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 709fa89..acb9735 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,41 +1,20 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# -name: "CodeQL Advanced" +name: "CodeQL" on: push: - branches: [ "master" ] + branches: ["main"] pull_request: - branches: [ "master" ] + branches: ["main"] schedule: - cron: '31 15 * * 2' jobs: analyze: name: Analyze (${{ matrix.language }}) - # Runner size impacts CodeQL analysis time. To learn more, please see: - # - https://gh.io/recommended-hardware-resources-for-running-codeql - # - https://gh.io/supported-runners-and-hardware-resources - # - https://gh.io/using-larger-runners (GitHub.com only) - # Consider using larger runners or machines with greater resources for possible analysis time improvements. runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} permissions: - # required for all workflows security-events: write - - # required to fetch internal or private CodeQL packs packages: read - - # only required for workflows in private repositories actions: read contents: read @@ -44,47 +23,24 @@ jobs: matrix: include: - language: csharp - build-mode: none - # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' - # Use `c-cpp` to analyze code written in C, C++ or both - # Use 'java-kotlin' to analyze code written in Java, Kotlin or both - # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both - # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, - # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. - # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how - # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages + build-mode: autobuild + steps: - name: Checkout repository uses: actions/checkout@v6 - # Initializes the CodeQL tools for scanning. + - name: Setup .NET SDK + uses: actions/setup-dotnet@v5 + with: + dotnet-version: 10.0.x + - name: Initialize CodeQL uses: github/codeql-action/init@v4 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - - # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs - # queries: security-extended,security-and-quality - - # If the analyze step fails for one of the languages you are analyzing with - # "We were unable to automatically build your code", modify the matrix above - # to set the build mode to "manual" for that language. Then modify this step - # to build your code. - # ℹ️ Command-line programs to run using the OS shell. - # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - - if: matrix.build-mode == 'manual' - shell: bash - run: | - echo 'If you are using a "manual" build mode for one or more of the' \ - 'languages you are analyzing, replace this with the commands to build' \ - 'your code, for example:' - echo ' make bootstrap' - echo ' make release' - exit 1 + queries: security-extended,security-and-quality + config-file: ./.github/codeql/codeql-config.yml - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v4 diff --git a/.github/workflows/coverage-report.yml b/.github/workflows/coverage-report.yml new file mode 100644 index 0000000..1597088 --- /dev/null +++ b/.github/workflows/coverage-report.yml @@ -0,0 +1,59 @@ +# Coverage Report for Pull Requests +# +# Triggered by pull_request_target so fork PRs get a write token. +# Must exist on the default branch (main) to trigger. + +name: Coverage Report + +on: + pull_request_target: + branches: ["main"] + +permissions: + contents: read + pull-requests: write + +jobs: + coverage-report: + runs-on: ubuntu-latest + steps: + - name: Find build run + uses: actions/github-script@v7 + id: find-run + with: + script: | + const { data: runs } = await github.rest.actions.listWorkflowRunsForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow: 'build.yml', + head_sha: context.payload.pull_request.head.sha, + per_page: 1 + }); + if (runs.workflow_runs.length === 0) { + core.setFailed('No build workflow run found for this SHA'); + return; + } + core.setOutput('run-id', runs.workflow_runs[0].id); + core.setOutput('head-sha', runs.workflow_runs[0].head_sha); + + - name: Download coverage artifact + uses: actions/download-artifact@v4 + with: + name: coverage-${{ steps.find-run.outputs.head-sha }} + path: coverage/ + merge-multiple: true + run-id: ${{ steps.find-run.outputs.run-id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Generate coverage report + uses: danielpalme/ReportGenerator-GitHub-Action@v5 + with: + reports: coverage/*.xml + targetdir: coveragereport + reporttypes: MarkdownSummary + + - name: Post coverage summary + uses: marocchino/sticky-pull-request-comment@v2 + with: + header: Code Coverage + path: coveragereport/Summary.md diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 0000000..ce9f50f --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,28 @@ +name: Dependency Review + +on: + pull_request: + branches: ["main"] + +permissions: + contents: read + pull-requests: write + +jobs: + dependency-review: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Review dependencies + uses: actions/dependency-review-action@v4 + continue-on-error: true + with: + fail-on-severity: high + comment-summary-in-pr: true + # Suppress "unknown license" warnings — they are false positives: + # 1) GitHub Actions don't carry SPDX metadata the tool can read + # 2) Some preview NuGet packages lack modern in nuspec + # 3) The Arcus smoke test references a placeholder version not on NuGet.org + allow-unknown-licenses: true diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 7de719f..deec6dc 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -3,7 +3,7 @@ name: Publish .NET Package permissions: - contents: read + contents: write packages: write pull-requests: write @@ -28,30 +28,62 @@ jobs: with: dotnet-version: 10.0.x - - name: Extract version - id: extract_version - run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV - - name: Restore .NET tools run: dotnet tool restore - working-directory: . - name: Restore dependencies run: dotnet restore - working-directory: ./src - name: Build - run: dotnet build ./src --no-restore --configuration Release /p:VersionFromCI=${{ env.VERSION }} + run: dotnet build --no-restore --configuration Release /p:VersionFromCI=${{ github.ref_name }} - name: Pack - run: dotnet pack ./src/Arcus/Arcus.csproj --configuration Release --no-build --output ./packages /p:VersionFromCI=${{ env.VERSION }} + run: dotnet pack src/Arcus/Arcus.csproj --configuration Release --no-build --output packages /p:VersionFromCI=${{ github.ref_name }} + + - name: Generate CycloneDX SBOM + run: | + dotnet tool install --global CycloneDX --version 3.* + dotnet-cyclonedx packages/ -o packages/ --output-format json + echo "SBOM generated:" + ls packages/*.json + + - name: Upload NuGet packages and SBOM + uses: actions/upload-artifact@v4 + with: + name: nuget-packages + path: | + packages/*.nupkg + packages/*.json + packages/*.xml + retention-days: 90 - name: Publish to GitHub Packages env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: dotnet nuget push ./packages/*.nupkg --source "https://nuget.pkg.github.com/sandialabs/index.json" --api-key $GITHUB_TOKEN --skip-duplicate + run: dotnet nuget push ./packages/*.nupkg --source "https://nuget.pkg.github.com/${{ github.repository }}/index.json" --api-key $GITHUB_TOKEN --skip-duplicate - name: Publish to nuget.org env: NUGET_ORG_APIKEY: ${{ secrets.NUGET_ORG_APIKEY }} run: dotnet nuget push ./packages/*.nupkg --source "https://api.nuget.org/v3/index.json" --api-key $NUGET_ORG_APIKEY --skip-duplicate + + - name: Generate release body + run: | + VERSION="${{ github.ref_name }}" + TAG="${{ github.ref_name }}" + # Extract section from CHANGELOG.md for this version, or fall back to commit log + if grep -q "## \[$VERSION\]" CHANGELOG.md 2>/dev/null; then + awk "/^## \[$VERSION\]/,/^## \[/{ if (!/^## \[/) print }" CHANGELOG.md | head -n -1 > release_body.md + else + echo "See [CHANGELOG.md](https://github.com/${{ github.repository }}/blob/main/CHANGELOG.md) for release notes." > release_body.md + fi + + - name: Create GitHub Release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release create "${{ github.ref_name }}" \ + --repo "${{ github.repository }}" \ + --title "v${{ github.ref_name }}" \ + --notes-file release_body.md \ + ./packages/*.nupkg diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml new file mode 100644 index 0000000..5c433d4 --- /dev/null +++ b/.github/workflows/scorecard.yml @@ -0,0 +1,44 @@ +name: OpenSSF Scorecard + +on: + push: + branches: ["main"] + schedule: + - cron: "0 10 * * 1" + +permissions: + contents: read + +jobs: + analysis: + runs-on: ubuntu-latest + permissions: + security-events: write + id-token: write + actions: read + contents: read + + steps: + - name: Checkout code + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Run analysis + uses: ossf/scorecard-action@v2 + with: + results_file: results.sarif + results_format: sarif + publish_results: true + + - name: Upload SARIF results + uses: actions/upload-artifact@v4 + with: + name: scorecard-results + path: results.sarif + retention-days: 30 + + - name: Upload to code-scanning dashboard + uses: github/codeql-action/upload-sarif@v4 + with: + sarif_file: results.sarif diff --git a/.github/workflows/smoke-test.yml b/.github/workflows/smoke-test.yml new file mode 100644 index 0000000..5fe8ebf --- /dev/null +++ b/.github/workflows/smoke-test.yml @@ -0,0 +1,77 @@ +name: Smoke Test + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + smoke-test: + name: ${{ matrix.os }} / ${{ matrix.framework }} + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + include: + - os: windows-latest + framework: net48 + - os: ubuntu-latest + framework: net8.0 + - os: ubuntu-latest + framework: net9.0 + - os: ubuntu-latest + framework: net10.0 + + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-dotnet@v5 + with: + dotnet-version: | + 8.0.x + 9.0.x + 10.0.x + + - name: NuGet cache + uses: actions/cache@v4 + with: + path: ~/.nuget/packages + key: ${{ runner.os }}-nuget-smoke-${{ hashFiles('**/packages.lock.json') }} + restore-keys: | + ${{ runner.os }}-nuget-smoke- + ${{ runner.os }}-nuget- + + - name: Pack Arcus into local feed + shell: bash + run: | + mkdir -p smoketests/feed + dotnet pack src/Arcus/Arcus.csproj \ + --configuration Release \ + --output smoketests/feed \ + -p:VersionFromCI=99.0.0-smoke \ + --verbosity quiet + + - name: Restore SmokeTests consumer + run: dotnet restore smoketests/SmokeTests/SmokeTests.csproj -p:ArcusVersion=99.0.0-smoke --verbosity quiet + + - name: Build SmokeTests consumer + run: dotnet build smoketests/SmokeTests/SmokeTests.csproj --no-restore --configuration Release -p:ArcusVersion=99.0.0-smoke --verbosity quiet + + - name: Run smoke tests (${{ matrix.framework }}) + run: > + dotnet run + --project smoketests/SmokeTests/SmokeTests.csproj + --framework ${{ matrix.framework }} + --configuration Release + --no-build + -p:ArcusVersion=99.0.0-smoke diff --git a/.gitignore b/.gitignore index 7e2820d..0f87fb4 100644 --- a/.gitignore +++ b/.gitignore @@ -202,3 +202,35 @@ ehthumbs_vista.db ### Read the Docs ### docs/_build + +# Smoke test local NuGet feed (created by run-smoke-tests scripts) +smoketests/feed/ + +# Benchmark results (commit manually with git add -f to record a progression snapshot) +src/results/ + +# Planning and working documents (root-level only) +/PRD*.md +/RELEASE-*.md +/future.todo.md +/skills-lock.json +.claude/ +.opencode/ +.agents/ +devcontainer-lock.json +TestResults/ + +# Git worktrees +.worktrees/ + +# Agent instruction files (root-level only, prevent accidental staging) +/AGENTS.md +/CLAUDE.md + +# Generated XML doc (in-tree artifact) +src/Arcus/Arcus.xml + +# Temporary files +*.bak +*.swp +*.swo diff --git a/.husky/task-runner.json b/.husky/task-runner.json index 9603920..aa4b730 100644 --- a/.husky/task-runner.json +++ b/.husky/task-runner.json @@ -4,22 +4,22 @@ "name": "style", "group": "pre-commit", "command": "dotnet", - "args": ["format", "src", "style", "--no-restore", "--include", "${staged}"], - "include": ["src/**/*.cs"] + "args": ["format", "style", "--no-restore", "--include", "${staged}"], + "include": ["src/**/*.cs", "smoketests/**/*.cs"] }, { "name": "analyzers", "group": "pre-commit", "command": "dotnet", - "args": ["format", "src", "analyzers", "--no-restore", "--include", "${staged}"], - "include": ["src/**/*.cs"] + "args": ["format", "analyzers", "--no-restore", "--include", "${staged}"], + "include": ["src/**/*.cs", "smoketests/**/*.cs"] }, { "name": "csharpier", "group": "pre-commit", "command": "dotnet", - "args": ["csharpier", "format", "--config-path", ".\\src\\.csharpierrc.json", "${staged}"], - "include": ["src/**/*.cs"] + "args": ["csharpier", "format", "--config-path", "./.csharpierrc.json", "${staged}"], + "include": ["src/**/*.cs", "smoketests/**/*.cs"] } ] } diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..71a3fc7 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,38 @@ +# Pre-commit hook configuration. +# Install with: pre-commit install +# Run on all files: pre-commit run --all-files +# Update hook versions: pre-commit autoupdate +# +# See https://pre-commit.com for more information. + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + args: [--maxkb=512] + + - repo: https://github.com/codespell-project/codespell + rev: v2.4.1 + hooks: + - id: codespell + args: [--ignore-words-list=fo] + types_or: [csharp, python, javascript, ts, markdown, shell, yaml, json] + + - repo: https://github.com/igorshubovych/markdownlint-cli + rev: v0.44.0 + hooks: + - id: markdownlint + args: [--disable, MD013] + + - repo: local + hooks: + - id: csharpier + name: CSharpier + entry: dotnet csharpier + language: system + types: [csharp] + pass_filenames: false \ No newline at end of file diff --git a/Arcus.slnx b/Arcus.slnx new file mode 100644 index 0000000..58ccc19 --- /dev/null +++ b/Arcus.slnx @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..a39616f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,66 @@ +# Changelog + +All notable changes to Arcus are documented here. +This project adheres to [Semantic Versioning](https://semver.org/). + +## [5.0.0] - TBD + +### 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](https://github.com/sandialabs/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` -- use `ToIPAddresses()` explicitly +- `Subnet.TryIPv6FromPartial(string, out IEnumerable)` -- 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 + +## [4.0.0] - 2026-01-15 + +## [3.0.0] - 2025-06-15 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..a6585b5 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,36 @@ +# Contributing to Arcus + +## Prerequisites + +- .NET SDK (see `src/global.json` for the pinned version) +- (optional) the dev container in `.devcontainer/` if you'd rather not install the SDK locally + +## Build & test + +```bash +cd src +dotnet restore +dotnet build +dotnet test +``` + +## Project layout + +This repo follows a standard layout: `src/` for the library and its unit +tests, `smoketests/` for package-consumption tests, `docs/` for design and +published documentation. See `docs/design/adr/` for the reasoning behind +non-obvious structural choices. + +## Pull requests + +1. Fork and branch from `main`. +2. Keep changes focused -- one logical change per PR. +3. Add or update tests for behavior changes. +4. Run `dotnet format` before pushing (or let the pre-commit hook do it). +5. Fill out the PR template; link any related issue. + +## Code style + +Formatting and analyzer rules are enforced via `.editorconfig` and +`Directory.Build.props`. CI will fail the build on analyzer warnings treated +as errors -- run `dotnet build` locally to catch these before pushing. diff --git a/HUMANS.md b/HUMANS.md index bcde8c3..9415d5d 100644 --- a/HUMANS.md +++ b/HUMANS.md @@ -1,4 +1,4 @@ -# Arcus +# Arcus ```asciiart /\ @@ -8,13 +8,14 @@ /_/ \_\_| \___|\__,_|___/ ``` -Let us not forget that software is made for humans (for computers) by humans (on computers). Arcus is no different. Thank you all that have helped, be it by spreading the work of this library, sharing the viral ideas of opensource, provided comments, creating pull requests or even just adding to our usage count. +Let us not forget that software is made for humans (for computers) by humans (on computers). Arcus is no different. Thank you all that have helped, be it by spreading the word of this library, sharing the viral ideas of opensource, provided comments, creating pull requests, or even just adding to our usage count. ## Some of the humans involved include - **Robert H. Engelhardt** - *Primary Developer, Source of Ideas Good and Bad* - [rheone](https://github.com/rheone) - **Andrew Steele** - *Review and Suggestions* - [ahsteele](https://github.com/ahsteele) - **Nick Bachicha** - *Git Wrangler and DevOps Extraordinaire* - [nicksterx](https://github.com/nicksterx) +- **Drew Antonich** - *Review and Suggestions* [drewantonich](https://github.com/drewantonich) If you'd like to make a significant contribution, we'd appreciate your support and would be happy to add you to our list of involved humans. If you're not human, we'll find a nice heading for you too. diff --git a/README.md b/README.md index aba0485..dedaaad 100644 --- a/README.md +++ b/README.md @@ -1,163 +1,399 @@ -# ![Arcus](src/Arcus/icon.png) Arcus +# ![Arcus](src/Arcus/icon.png) Arcus -![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/sandialabs/Arcus/build.yml?branch=main) -[![nuget Version](https://img.shields.io/nuget/v/Arcus)](https://www.nuget.org/packages/Arcus) -[![GitHub Release](https://img.shields.io/github/v/release/sandialabs/Arcus)](https://github.com/sandialabs/Arcus/releases) -[![GitHub Tag](https://img.shields.io/github/v/tag/sandialabs/Arcus)](https://github.com/sandialabs/Arcus/tags) -![Targets](https://img.shields.io/badge/.NET%20Standard%202.0%20|%20.NET%208.0%20|%20.NET%209.0|%20.NET%2010.0-blue) -[![Apache 2.0 License](https://img.shields.io/github/license/sandialabs/Arcus?logo=apache)](https://github.com/sandialabs/Arcus/blob/main/LICENSE) +[![Build](https://img.shields.io/github/actions/workflow/status/sandialabs/Arcus/build.yml?branch=main&logo=github)](https://github.com/sandialabs/Arcus/actions/workflows/build.yml) +[![NuGet](https://img.shields.io/nuget/v/Arcus?logo=nuget)](https://www.nuget.org/packages/Arcus) +![Targets](https://img.shields.io/badge/.NET_Standard_2.0_%7C_.NET_8.0_%7C_.NET_9.0_%7C_.NET_10.0-blue) +[![License](https://img.shields.io/github/license/sandialabs/Arcus?logo=apache)](https://github.com/sandialabs/Arcus/blob/main/LICENSE) ## About the Project Arcus is a C# manipulation library for calculating, parsing, formatting, converting, and comparing both IPv4 and IPv6 addresses and subnets. It accounts for 128-bit numbers on 32-bit platforms. -## ❗Breaking Changes in Version 3+ +## ❗Breaking Changes in v5.0.0 -### IP Address Parsing based on .NET Targets +### Gulliver dependency removed -In .NET versions up to and including .NET 4.8 (which corresponds to .NET Standard 2.0), stricter parsing rules were enforced for `IPAddress` according to the IPv6 specification. Specifically, the presence of a terminal '%' character without a valid zone index is considered invalid in these versions. As a result, the input `abcd::%` fails to parse, leading to a null or failed address parsing depending on `Parse`/`TryParse`. This behavior represents a breaking change from Arcus's previous target of .NET Standard 1.3. and may provide confusion for .NET 4.8 / .NET Standard 2.0 versions. +The [Gulliver](https://github.com/sandialabs/gulliver) NuGet package is no longer a dependency. All byte-level manipulation previously delegated to Gulliver is now handled internally by the new `BigEndianBitWrapper` type. -In contrast, in newer versions of .NET, including .NET 8 and .NET 9, and .NET 10 the parsing rules have been relaxed. The trailing '%' character is now ignored during parsing, allowing for inputs that would have previously failed. +**Why:** Removing the external dependency simplifies the dependency graph for consumers, avoids version conflicts, and gives Arcus full control over its byte-level operations without relying on an outside library's API surface. -It is important to note that this scenario appears to be an extreme edge case, and developers should ensure that their applications handle `IPAddress` parsing appropriately across different target frameworks as expected. +**Migration:** If your project depended on Gulliver being available transitively through Arcus (e.g. you used `ByteArrayUtils`, `ShiftBitsLeft`, or the `byte[].ToString("HC"/"IBE"/"b")` format extensions without a direct reference to Gulliver), you must add a direct `PackageReference` to Gulliver or replace those usages with equivalent implementations. -If in doubt it is suggested that IP Address based user input should be sanitized to meet your development needs. +### .NET 10 target added -## Getting Started +Arcus now targets `net10.0` in addition to `netstandard2.0`, `net8.0`, and `net9.0`. The test project similarly targets `net10.0`. -The latest stable release of Arcus is [available on NuGet](https://www.nuget.org/packages/Arcus/). +**Why:** Stay current with the latest .NET releases and take advantage of new platform features and performance improvements. -The latest [Arcus documentation](https://arcus.readthedocs.io/en/latest/) may be found on [ReadTheDocs](https://arcus.readthedocs.io/en/latest/). +### Enumeration cap via `maxEnumerationExponent` and `DefaultMaxEnumerationExponent` -### Usage +Every range type now stores a `MaxEnumerationExponent` property (0–128). Enumeration via `ToIPAddresses()` (and `foreach` / `IEnumerable` for backwards compatibility) yields at most **2MaxEnumerationExponent** addresses. -At its heart Arcus is split amongst five separate interdependent units. _Types_, _Comparers_, _Converters_, _Math_, and _Utilities_. +- The **default** is `DefaultMaxEnumerationExponent` = 12 (212 = 4096 addresses). +- If a range contains more than 2MaxEnumerationExponent addresses, `InvalidOperationException` is thrown at the point the limit is exceeded. +- This prevents accidental enumeration of enormous address spaces (e.g., a `/8` IPv4 subnet has 16,777,216 addresses). -These units each work across Arcus's `IPAddressRange`, `Subnet` and .NET's `System.Net.IPAddress`. Arcus adds extra desired functionality where the standard C# libraries left off. +**Why:** Previously, `foreach` on an arbitrary range would silently enumerate every address — potentially trillions of iterations for large ranges. The cap forces developers to consciously opt into large enumeration by specifying a higher exponent. A value of `0` disables enumeration entirely; `128` allows enumeration up to the full IPv6 space (though practically impossible). -#### Types +**How to increase the cap:** Pass `maxEnumerationExponent` to constructors: -##### `Subnet` +```csharp +// Default cap: 4096 addresses +var subnet = new Subnet(IPAddress.Parse("10.0.0.0"), 8); -An IPv4 or IPv6 subnetwork representation - the work horse and original reason for the Arcus library. Outside the concept of the `Subnet` object, most everything else in Arcus is auxiliary and exists only in support of the `Subnet` object. That’s not to say that the remaining pieces of the Arcus library aren’t useful, on the contrary their utility can benefit a developer greatly. +// This will throw InvalidOperationException (> 4096 addresses) +foreach (var addr in subnet) { /* throws */ } -A `Subnet` may be instantiated in several ways: +// Raise cap to 24 (16,777,216 addresses — enough for /8) +var large = new Subnet(IPAddress.Parse("10.0.0.0"), 8, maxEnumerationExponent: 24); +foreach (var addr in large) { /* OK */ } +``` -The most common way to create a `Subnet` object is to construct it via a high and low `IPAddress` by calling the constructor `Subnet(IPAddress primary, IPAddress secondary)`. This constructs the smallest possible subnet that would contain both IP addresses. Typically the addresses specified are the Network and Broadcast addresses (lower and higher bounds of a subnet) but this is not necessary. Addresses _MUST_ be the same address family (either Internetwork or InternetworkV6). +The exponent parameter is available on all range constructors (`Subnet`, `IPAddressRange`, `AbstractIPAddressRange`), static factory methods (`Subnet.Parse`, `Subnet.FromNetMask`, `IPAddressRange.TryCollapseAll`, `IPAddressRange.TryExcludeAll`, `IPAddressRange.TryMerge`), and `SubnetUtilities.FewestConsecutiveSubnetsFor`. -It is also possible to create a `Subnet` from an `IPAddress` and an `integer` based _route prefix_. Eg: `Subnet(IPAddress ipAddress, int routingPrefix)`. +**Migration:** -Likewise it may be desired to statically parse a subnet string with `Subnet.Parse(string input)` or it’s safe equivalent of `bool Subnet.TryParse(string input, out Subnet subnet)` +| Old pattern | New pattern | +|---|---| +| `subnet.Enumerate()` | `foreach (var addr in subnet)` (uses constructor cap; default 4096) | +| `subnet.Enumerate(20)` | Construct with `maxEnumerationExponent: 20` and use `foreach` | +| unlimited `foreach` on large ranges | Throws `InvalidOperationException` by default; increase exponent or use explicit arithmetic operations | -For example, one could safely parse the `string` "192.168.1.0/16" via +### New `ToIPAddresses()` method -```c# -Subnet subnet; -var success = Subnet.TryParse("192.168.1.0/16", out subnet) +A new method `IEnumerable ToIPAddresses()` has been added to the `IIPAddressRange` interface and implemented on `AbstractIPAddressRange`. It returns an enumerable of the addresses in the range, capped by `MaxEnumerationExponent`. + +The existing `GetEnumerator()` is now `[Obsolete("Use ToIPAddresses() instead")]`. It still works (delegating to `ToIPAddresses()`), but produces a compile-time warning. + +```csharp +// Current (warning-free): +foreach (var addr in range.ToIPAddresses()) { ... } + +// Old style (produces obsolete warning in v5, will break in v6): +foreach (var addr in range) { ... } ``` -##### `IPAddressRange` +### Removed types and members -`IPAddressRange` is a basic implementation of `IIPAddressRange` it is used to represent an inclusive range of arbitrary IP Addresses of the same address family. Unlike `Subnet`, `IPAddressRange` is not restricted to a power of two length, nor a valid broadcast address head. +The following previously `[Obsolete]` types and members have been removed. -#### Comparers +| Removed | Namespace / Location | Migration | +| --- | --- | --- | +| `MacAddress` (entire type) | `Arcus` | No direct replacement in this library | +| `DefaultIPAddressRangeComparer` (entire type) | `Arcus.Comparers` | Use `DefaultIIPAddressRangeComparer` | + +### `SubnetUtilities` static fields are now `readonly` + +`SubnetUtilities.PrivateIPAddressRangesList` and `SubnetUtilities.LinkLocalIPAddressRangesList` are now declared `readonly`. Previously the field *reference* could be replaced by external code (e.g., `SubnetUtilities.PrivateIPAddressRangesList = myList`). That pattern will no longer compile. The `IReadOnlyList` type already prevented mutation of the list *contents*; `readonly` now also prevents replacement of the list itself. + +**Migration:** If you were replacing these fields to customize private-address detection, extract that logic into a separate variable and pass it explicitly to your own helper methods. + +### Behavior corrections (non-breaking for correct usage) + +The following bugs have been fixed. If your code was intentionally relying on the incorrect behavior, you will need to update it. + +| Type / Member | Previous (incorrect) | Fixed | +| --- | --- | --- | +| `AbstractIPAddressRange.Overlaps(IIPAddressRange)` | `B.Overlaps(A)` returned `false` when B was wholly inside A | Both directions now return `true` (symmetric) | +| `AbstractIPAddressRange.ContainsAnyPrivateAddresses()` | Checked only endpoints; missed ranges that span a private block with public endpoints | Uses range-overlap detection across `PrivateIPAddressRangesList` | +| `AbstractIPAddressRange.ContainsAllPublicAddresses()` | Checked only endpoints; returned `true` for ranges whose endpoints are public but whose interior spans a private block | Uses range-overlap detection | +| `IPAddressRange.TryExcludeAll` | Threw `InvalidOperationException` when an exclusion ended at the family maximum address | Returns `(true, leading segment)` or `(true, [])` as appropriate | + +### IP Address Parsing across .NET Targets + +In .NET versions up to and including .NET 4.8 (which corresponds to .NET Standard 2.0), stricter parsing rules are enforced for `IPAddress` according to the IPv6 specification. Specifically, the presence of a terminal '%' character without a valid zone index is considered invalid in these versions. As a result, the input `abcd::%` fails to parse, leading to a null or failed address parsing depending on `Parse`/`TryParse`. + +In newer versions of .NET, including .NET 8, .NET 9, and .NET 10, the parsing rules have been relaxed. The trailing '%' character is now ignored during parsing, allowing for inputs that would have previously failed. + +It is important to note that this scenario appears to be an extreme edge case. If in doubt, sanitize IP address user input to meet your development needs. + +### Obsolete members (compile-time warning in v5, will be removed in v6) + +The following members are marked `[Obsolete]` in v5.0.0. They continue to work but produce compile-time warnings. They will be **removed in v6.0.0**. + +| Obsolete member | Replacement | Notes | +|---|---|---| +| `IIPAddressRange.GetEnumerator()` (and `foreach` on any range type) | `ToIPAddresses()` | `GetEnumerator()` now delegates to `ToIPAddresses()`; direct `foreach` on range types is deprecated | +| `IIPAddressRange` implementing `IEnumerable` | None — the interface itself will be removed | Avoid relying on `IIPAddressRange` being enumerable; use `ToIPAddresses()` explicitly | +| `Subnet.TryIPv6FromPartial(string, out IEnumerable)` | To be replaced by more explicit methods in a future release | This method's behavior was highly specialized and often surprising to consumers | + +### Performance and modernization + +- **Regex source generators:** `Subnet` parsing and `IPAddressUtilities` hexadecimal/octal parsing now use C# `[GeneratedRegex]` source generators for improved startup and throughput on targets that support it. +- **Partial class split:** Large types (`Subnet`, `IPAddressRange`, `AbstractIPAddressRange`, `BigEndianBitWrapper`) have been split into partial files organized by interface implementation for better maintainability. +- **Internal `BigEndianBitWrapper`:** A new internal type provides big-endian byte-array arithmetic, replacing the Gulliver dependency. +- **`InternalsVisibleTo`:** Tests and benchmarks now have access to internal types for more thorough testing. + +### Anticipated future breaking change: `IEnumerable` removal from `IIPAddressRange` + +In a future major version, `IIPAddressRange` (and its inheritors `Subnet` and `IPAddressRange`) will **no longer implement `IEnumerable`**. The `GetEnumerator()` method will be removed entirely. + +**Why:** The `IEnumerable` interface makes it too easy to accidentally enumerate astronomically large address spaces (e.g., a full IPv4 `/0` has ~4 billion addresses). The enumeration cap via `MaxEnumerationExponent` mitigates this at runtime, but the interface contract itself encourages direct `foreach` usage that is semantically misleading — iterating over billions of items is rarely the intent. + +**Migration strategy:** Replace all `foreach (var addr in range)` patterns with `foreach (var addr in range.ToIPAddresses())` now. This produces no warnings in v5 and will be required in the future version. + +```csharp +// v5 (warning-free, will continue to work): +foreach (var addr in range.ToIPAddresses()) { ... } + +// v5 (produces obsolete warning, will break in v6): +foreach (var addr in range) { ... } +``` + +## Getting Started + +The latest stable release of Arcus is [available on NuGet](https://www.nuget.org/packages/Arcus/). -The _Comparers_ package contains useful Comparer objects for comparing properties of IP Addresses and IP Address composite objects. +The latest [Arcus documentation](https://arcus.readthedocs.io/en/latest/) may be found on [ReadTheDocs](https://arcus.readthedocs.io/en/latest/). + +### Usage -- `DefaultAddressFamilyComparer` - A comparer that compares address families. Most frequently `Internetwork` (IPv4) and `InternetworkV6` (IPv6) -- `DefaultIPAddressComparer` - A comparer for `IPAddress` objects -- `DefaultIPAddressRangeComparer` - A comparer for `IIPAddressRange`. Compares such that lower order ranges are less that higher order ranges accounting for size at matching range starts +Arcus provides types and utilities for working with IP addresses and subnets beyond what the .NET BCL offers. The library is organized around a few core concepts. -#### Converters +#### Quick Start -The _Converters_ package is a package of static utility classes for converting one type into another type. +```csharp +using Arcus; +using Arcus.Utilities; -##### `IPAddressConverters` +// Parse a subnet +var subnet = Subnet.Parse("192.168.1.0/24"); +Console.WriteLine($"Network: {subnet.Head}, Broadcast: {subnet.Tail}"); +Console.WriteLine($"Usable addresses: {subnet.UsableHostAddressCount}"); -Static utility class containing conversion methods for converting `IPAddress` objects into something else. +// Check containment +var testAddress = IPAddress.Parse("192.168.1.50"); +Console.WriteLine(subnet.Contains(testAddress)); // True -#### Math +// Enumerate (capped at 2^MaxEnumerationExponent = 4096 by default) +foreach (var addr in subnet.ToIPAddresses().Take(5)) +{ + Console.WriteLine(addr); +} -The _Math_ package is a package of static utility classes for doing computational mathematics on objects. +// IP math +var nextAddress = testAddress.Increment(); // 192.168.1.51 +var prevAddress = testAddress.Increment(-2); // 192.168.1.48 -##### `IPAddressMath` +// Comparison +testAddress.IsBetween(subnet.Head, subnet.Tail); // True +testAddress.IsGreaterThan(IPAddress.Parse("10.0.0.1")); // True -In some cases the C# `IPAddress` object doesn't go far enough with what you can do with it mathematically, this static utility class containing mathematical methods to fill in the gaps. +// Address family detection +testAddress.IsIPv4(); // True +testAddress.IsIPv6(); // False -###### Incrementing and Decrementing +// Work with ranges +var range = new IPAddressRange( + IPAddress.Parse("10.0.0.0"), + IPAddress.Parse("10.0.0.255")); +Console.WriteLine($"Range length: {range.Length}"); + +// Fewest subnets covering a range +var covering = SubnetUtilities.FewestConsecutiveSubnetsFor( + IPAddress.Parse("128.64.20.3"), + IPAddress.Parse("128.64.20.12")); +``` -Incrementing and Decrementing an `IPAddress` is easy. +#### Enumeration safety -Incrementing by one is a simple call to the extension method: +All range types enforce an enumeration cap via `MaxEnumerationExponent`. Use `ToIPAddresses()` to enumerate explicitly: -```c# -var address = IPAddress.Parse("192.168.1.1"); -var result = address.Increment(); // result is 192.168.1.2 +```csharp +// Safe enumeration (throws if range exceeds cap) +foreach (var addr in range.ToIPAddresses()) +{ + Process(addr); +} + +// Check if enumeration would succeed +if (range.Length <= (BigInteger.One << range.MaxEnumerationExponent)) +{ + foreach (var addr in range.ToIPAddresses()) { ... } +} ``` -Decrementing is just as simple: +#### `Subnet` + +An IPv4 or IPv6 subnetwork representation conforming to CIDR rules (power-of-two length, aligned to a prefix boundary). Implements `IIPAddressRange`, `IEquatable`, `IComparable`, `IFormattable`, `IEnumerable`, and `ISerializable`. + +**Construction:** + +```csharp +// From two addresses (smallest subnet containing both) +var subnet = new Subnet( + IPAddress.Parse("192.168.1.0"), + IPAddress.Parse("192.168.1.255")); -```c# -var address = IPAddress.Parse("192.168.1.1"); -var result = address.Increment(-2); // result is 192.168.0.0 +// From address and routing prefix +var subnet = new Subnet(IPAddress.Parse("192.168.1.1"), 24); // autocorrects to 192.168.1.0/24 + +// From a CIDR string +var subnet = Subnet.Parse("192.168.1.0/24"); +Subnet.TryParse("10.0.0.0/8", out var result); + +// From netmask +var subnet = Subnet.FromNetMask( + IPAddress.Parse("192.168.1.0"), + IPAddress.Parse("255.255.255.0")); + +// Single-address subnet +var single = new Subnet(IPAddress.Parse("192.168.1.1")); // 192.168.1.1/32 ``` -_Overflow_ and _Underflow_ conditions will result in an `InvalidOperationException`. +All constructors and factory methods accept an optional `maxEnumerationExponent` parameter to control enumeration limits. -###### Equality +**Properties:** `Head`, `Tail`, `RoutingPrefix`, `Netmask` (IPv4 only), `BroadcastAddress`, `NetworkPrefixAddress`, `UsableHostAddressCount`, `Length`, `MaxEnumerationExponent`. -Equality may also be tested via a host of equality extension methods: +**Set operations:** `Contains(Subnet)`, `Overlaps(Subnet)`, `Touches(IIPAddressRange)`. -- `bool IsEqualTo(this IPAddress alpha, IPAddress beta)` -- `bool IsGreaterThan(this IPAddress alpha, IPAddress beta)` -- `bool IsGreaterThanOrEqualTo(this IPAddress alpha, IPAddress beta)` -- `bool IsLessThan(this IPAddress alpha, IPAddress beta)` -- `bool IsLessThanOrEqualTo(this IPAddress alpha, IPAddress beta)` +**Public constants:** -#### Utilities +| Constant | Description | +|---|---| +| `Ipv4OctetPartialPattern` | Regex pattern matching partial IPv4 octet strings | +| `RoughSubnetStringPattern` | Regex pattern matching rough subnet string shape | -The _Utilities_ package contains static classes for miscellaneous operations on specific types. +#### `IPAddressRange` -##### `IPAddressUtilities` +An inclusive range of IP addresses of the same address family. Unlike `Subnet`, not restricted to CIDR boundaries — any `head` through `tail` inclusive. -Static utility class containing miscellaneous operations for `IPAddress` objects +```csharp +var range = new IPAddressRange( + IPAddress.Parse("10.0.0.5"), + IPAddress.Parse("10.0.0.42")); -###### Address Family Detection +// Static utilities +IPAddressRange.TryCollapseAll(ranges, out var collapsed); +IPAddressRange.TryExcludeAll(initial, exclusions, out var remaining); +IPAddressRange.TryMerge(left, right, out var merged); +``` -A couple of extension methods were created to quickly determine the address family of an IP Address. To determine if an address is IPv4 use `bool IsIPv4(this IPAddress ipAddress)`, likewise `bool IsIPv6(this IPAddress ipAddress)` can be used to test for IPv6. +All static utility methods accept an optional `maxEnumerationExponent` parameter. -###### Parsing +#### `IIPAddressRange` -It is possible to parse an `IPAddress` from a hexadecimal string into either an IPv4 of IPv6 address using the `IPAddress ParseFromHexString(string input, AddressFamily addressFamily)` method. Likewise it can be done safely with `bool TryParseFromHexString(string input, AddressFamily addressFamily, out IPAddress address)`. +The core interface implemented by both `Subnet` and `IPAddressRange`. Provides: -Similarly, conversion may be done from an octal string by using `bool TryParseIgnoreOctalInIPv4(string input, out IPAddress address)` or even a `BigInteger` by way of `bool TryParse(BigInteger input, AddressFamily addressFamily, out IPAddress address)`. +- `Head`, `Tail`, `Length`, `AddressFamily` +- `IsSingleIP`, `IsIPv4`, `IsIPv6` +- `Contains(IIPAddressRange)`, `Contains(IPAddress)` +- `Overlaps(IIPAddressRange)`, `Touches(IIPAddressRange)` +- `HeadOverlappedBy(IIPAddressRange)`, `TailOverlappedBy(IIPAddressRange)` +- `ContainsAnyPrivateAddresses()`, `ContainsAllPrivateAddresses()` +- `ContainsAnyPublicAddresses()`, `ContainsAllPublicAddresses()` +- `ToIPAddresses()` — enumerate addresses capped at `MaxEnumerationExponent` +- `MaxEnumerationExponent` — the exponent controlling the enumeration cap -##### `SubnetUtilities` +#### Comparers -Static utility class containing miscellaneous operations for `Subnet` objects that didn't make sense to put on the object itself. +| Comparer | Description | +|---|---| +| `DefaultAddressFamilyComparer` | Compares `AddressFamily` values (`InterNetwork` < `InterNetworkV6`) | +| `DefaultIPAddressComparer` | Compares `IPAddress` by family then unsigned big-endian value | +| `DefaultIIPAddressRangeComparer` | Compares ranges by head address then by length | + +#### `IPAddressMath` — Arithmetic and Comparison + +```csharp +// Increment and decrement +var next = address.Increment(); // +1 +var prev = address.Increment(-5); // -5 +address.TryIncrement(out var result, delta: 10); + +// Comparison +address.IsEqualTo(other); +address.IsGreaterThan(other); +address.IsLessThan(other); +address.IsBetween(low, high); + +// Bounds +address.IsAtMin(); // true for 0.0.0.0 or :: +address.IsAtMax(); // true for 255.255.255.255 or ffff:...:ffff +IPAddressMath.Min(a, b); +IPAddressMath.Max(a, b); +``` + +#### `IPAddressConverters` -Given two arbitrary IP Addresses of the same family it may be desired to calculate the fewest consecutive subnets that would hold the inclusive range between them. For example +```csharp +// CIDR conversion (IPv4 only) +netmask.NetmaskToCidrRoutePrefix(); // IPAddress → int -```c# -SubnetUtilities.FewestConsecutiveSubnetsFor(IPAddress.Parse("128.64.20.3"), IPAddress.Parse("128.64.20.12")) +// String formatting +address.ToDottedQuadString(); // IPv6 → mixed IPv4/IPv6 notation +address.ToHexString(); // Big-endian hex +address.ToNumericString(); // Unsigned integer string +address.ToUncompressedString(); // Fully expanded +address.ToBase85String(); // RFC 1924 (IPv6 only) ``` -would return an `Enumerable` containing the subnets `128.64.20.3/32`, `128.64.20.4/30`, `128.64.20.8/30`, `128.64.20.12/32`. +#### `IPAddressUtilities` + +```csharp +// Address family +address.IsIPv4(); +address.IsIPv6(); + +// Detection +address.IsIPv4MappedIPv6(); // RFC 4291 +netmask.IsValidNetMask(); // IPv4 netmask validation -### Developer Notes +// Special values +AddressFamily.InterNetwork.MaxIPAddress(); +AddressFamily.InterNetworkV6.MinIPAddress(); + +// Parsing +IPAddressUtilities.ParseFromHexString("C0A80101", AddressFamily.InterNetwork); +IPAddressUtilities.TryParseFromHexString(input, family, out address); +IPAddressUtilities.ParseIgnoreOctalInIPv4("010.000.001.001"); +IPAddressUtilities.TryParse(bigInteger, family, out address); +``` + +**Public constants:** + +| Constant | Description | +|---|---| +| `DottedQuadLeadingZerosPattern` | Regex pattern matching leading zeros in dotted-quad octets | +| `DottedQuadRegularExpressionPattern` | Regex pattern checking dotted-quad format | +| `HexLikePattern` | Regex pattern matching hexadecimal digit strings | +| `IPv4BitCount` | 32 | +| `IPv4ByteCount` | 4 | +| `IPv4OctetCount` | 4 | +| `IPv6BitCount` | 128 | +| `IPv6ByteCount` | 16 | +| `IPv6HextetCount` | 8 | + +#### `SubnetUtilities` + +```csharp +// Fewest consecutive subnets covering an address range +var subnets = SubnetUtilities.FewestConsecutiveSubnetsFor( + IPAddress.Parse("128.64.20.3"), + IPAddress.Parse("128.64.20.12")); + +// Largest / smallest subnet in a collection +SubnetUtilities.LargestSubnet(subnets); +SubnetUtilities.SmallestSubnet(subnets); + +// Private/public range detection (constants) +SubnetUtilities.PrivateIPAddressRangesList; +SubnetUtilities.LinkLocalIPAddressRangesList; +``` ## Built With This project was built with the aid of: -- [CSharpier](https://csharpier.com/) -- [dotnet-outdated](https://github.com/dotnet-outdated/dotnet-outdated) -- [Gulliver](https://github.com/sandialabs/gulliver) - A self created library that helped us keep our bits and bytes in order -- [Husky.Net](https://alirezanet.github.io/Husky.Net/) -- [Roslynator](https://josefpihrt.github.io/docs/roslynator/) -- [SonarAnalyzer](https://www.sonarsource.com/products/sonarlint/features/visual-studio/) -- [StyleCop.Analyzers](https://github.com/DotNetAnalyzers/StyleCopAnalyzers) -- [xUnit.net](https://xunit.net/) +- [BenchmarkDotNet](https://benchmarkdotnet.com/) - microbenchmarking +- [CSharpier](https://csharpier.com/) - code formatting +- [dotnet-outdated](https://github.com/dotnet-outdated/dotnet-outdated) - dependency updates +- [Husky.Net](https://alirezanet.github.io/Husky.Net/) - git hooks +- [Pre-commit](https://pre-commit.com/) - pre-commit hooks (trailing whitespace, codespell, markdownlint) +- [Roslynator](https://josefpihrt.github.io/docs/roslynator/) - Roslyn analyzers +- [SonarAnalyzer](https://www.sonarsource.com/products/sonarlint/features/visual-studio/) - code quality +- [StyleCop.Analyzers](https://github.com/DotNetAnalyzers/StyleCopAnalyzers) - style enforcement +- [xUnit.net](https://xunit.net/) - unit testing ### Versioning @@ -165,7 +401,9 @@ This project uses [Semantic Versioning](https://semver.org/) ### Targeting -The project targets [.NET Standard 2.0](https://learn.microsoft.com/en-us/dotnet/standard/net-standard?tabs=net-standard-2-0), [.NET 8](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8), [.NET 9](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-9/overview), and [.NET 10](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-10/overview). The test project similarly targets .NET 8, .NET 9, .NET 10, but targets [.NET Framework 4.8](https://dotnet.microsoft.com/en-us/download/dotnet-framework/net48) for the .NET Standard 2.0 tests. +The library targets [.NET Standard 2.0](https://learn.microsoft.com/en-us/dotnet/standard/net-standard?tabs=net-standard-2-0), [.NET 8](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8), [.NET 9](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-9/overview), and [.NET 10](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-10/overview). The test project targets `net48` (Windows only), `net8.0`, `net9.0`, and `net10.0`. The benchmark project targets `net48`, `net8.0`, `net9.0`, and `net10.0`. + +Language version: C# 14.0. ### Commit Hook @@ -189,7 +427,28 @@ These commands may be called independently, but order may matter. #### Testing -After making changes tests should be run that include all targets +After making changes, tests should be run that include all targets: + +```shell +cd src +dotnet test +``` + +Tests target `net48` (Windows only, for `netstandard2.0` consumers), `net8.0`, `net9.0`, and `net10.0`. On Ubuntu CI, tests run per-TFM sequentially; on Windows, all TFMs run in one `dotnet test` call. + +To run a single TFM or test class: + +```shell +dotnet test --framework net10.0 +dotnet test --filter "FullyQualifiedName~SubnetTests" +``` + +Smoke tests validate the packed NuGet package against real runtimes: + +```shell +cd smoketests +./run-smoke-tests.sh +``` ## Acknowledgments @@ -197,9 +456,10 @@ This project was built by the Production Tools Team at Sandia National Laborator Including, but not limited to: -- **Robert H. Engelhardt** - _Primary Developer, Source of Ideas Good and Bad_ - [rheone](https://github.com/rheone) -- **Andrew Steele** - _Review and Suggestions_ - [ahsteele](https://github.com/ahsteele) -- **Nick Bachicha** - _Git Wrangler and DevOps Extraordinaire_ - [nicksterx](https://github.com/nicksterx) +- **Robert H. Engelhardt** - *Primary Developer, Source of Ideas Good and Bad* - [rheone](https://github.com/rheone) +- **Andrew Steele** - *Review and Suggestions* - [ahsteele](https://github.com/ahsteele) +- **Nick Bachicha** - *Git Wrangler and DevOps Extraordinaire* - [nicksterx](https://github.com/nicksterx) +- **Drew Antonich** - *Review and Suggestions* [drewantonich](https://github.com/drewantonich) ## Copyright @@ -209,6 +469,6 @@ Including, but not limited to: > Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at > -> http://www.apache.org/licenses/LICENSE-2.0 +> > > Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/dictionary.dic b/dictionary.dic index 80a3f63..1458fbc 100644 --- a/dictionary.dic +++ b/dictionary.dic @@ -1,8 +1,11 @@ -cidr +cidr hextet hextets +NETCOREAPP +NETSTANDARD NTESS parseable Sandia stringified undelimited +Internetwork diff --git a/docs/AbstractIPAddressRange.rst b/docs/AbstractIPAddressRange.rst index a7b677e..b896eca 100644 --- a/docs/AbstractIPAddressRange.rst +++ b/docs/AbstractIPAddressRange.rst @@ -3,8 +3,24 @@ AbstractIPAddressRange ====================== +|version| v5.0.0 + The ``AbstractIPAddressRange`` is an abstract implementation of :ref:`IIPAddressRange`. It is extended by both :ref:`IPAddressRange`, and :ref:`Subnet`. +.. important:: + + **v5.0.0 Breaking changes in this class:** + + **MaxEnumerationExponent property:** Every range now stores a ``MaxEnumerationExponent`` property (0–128, default 12). Enumeration via ``ToIPAddresses()`` yields at most ``2^MaxEnumerationExponent`` addresses (4096 by default). If exceeded, ``InvalidOperationException`` is thrown. Pass a larger ``maxEnumerationExponent`` at construction time to enumerate larger ranges. + + **Obsolete — GetEnumerator():** ``GetEnumerator()`` is now ``[Obsolete("Use ToIPAddresses() instead")]``. It delegates to ``ToIPAddresses()`` but produces a compile-time warning and **will be removed in v6.0.0**. + + **Overlaps — symmetry fix:** ``Overlaps(IIPAddressRange)`` now returns ``true`` in both directions when one range is wholly contained inside the other. Previously, calling ``inner.Overlaps(outer)`` returned ``false`` when ``inner`` was wholly inside ``outer``. + + **ContainsAnyPrivateAddresses / ContainsAllPublicAddresses — range-overlap fix:** these methods previously used an endpoint heuristic that produced incorrect results when both endpoints of a range were public but the range's interior spanned a private subnet (e.g., ``11.0.0.0 – 173.0.0.0`` spans ``172.16.0.0/12``). All four ``ContainsAny/AllPrivate/PublicAddresses`` methods now use range-overlap detection against ``SubnetUtilities.PrivateIPAddressRangesList``. + + **TryExcludeAll — boundary guards:** when an exclusion ends at the family maximum address (e.g., ``255.255.255.255``) the method returns ``(true, leading segment)`` or ``(true, [])`` instead of throwing ``InvalidOperationException``. The trailing-segment path is guarded symmetrically for family-minimum exclusions. + Functionality Implementation ---------------------------- diff --git a/docs/Acknowledgements.rst b/docs/Acknowledgements.rst index aba1b53..3713e4f 100644 --- a/docs/Acknowledgements.rst +++ b/docs/Acknowledgements.rst @@ -14,8 +14,4 @@ Logo cropped from image `"Iris Carrying the Water of the River Styx to Olympus f The person who associated a work with this deed has dedicated the work to the public domain by waiving all of their rights to the work worldwide under copyright law, including all related and neighboring rights, to the extent allowed by law. You can copy, modify, distribute and perform the work, even for commercial purposes, all without asking permission. -Structure of a 48-bit MAC address -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -`Diagram showing the structure of a MAC-48 network address, explicitly showing the positions of the multicast/unicast bit and the OUI/local address type bit. `_ - - This file is licensed under the `Creative Commons `_ Attribution-Share Alike `2.5 Generic `_, 2.0 Generic and `1.0 Generic `_ license. +(The ``MacAddress`` type was removed in v5.0.0. This attribution is retained for historical reference.) diff --git a/docs/FAQ.rst b/docs/FAQ.rst index 5a316b7..e3f5f1c 100644 --- a/docs/FAQ.rst +++ b/docs/FAQ.rst @@ -17,7 +17,7 @@ That's roughly :math:`3.4\times10^{38}` addresses. 340 undecillion 282 decillion 366 nonillion 920 octillion 938 septillion 463 sextillion 463 quintillion 374 quadrillion 607 trillion 431 billion 768 million 211 thousand 456 to be exact. -Let's face it, arbitrary numbers much bigger than 7 are hard to conceptualize for some of us [#SevenIsHard]_. I personally get lost after three-ish. The awe inspiring scale of IPv6 is much bigger than 3, at least double, probably even over triple that. It is so big we had to jump through some hoops to make C# do the math necessary. This is why both the Arcus and `Gulliver `_ libraries now exist. +Let's face it, arbitrary numbers much bigger than 7 are hard to conceptualize for some of us [#SevenIsHard]_. I personally get lost after three-ish. The awe inspiring scale of IPv6 is much bigger than 3, at least double, probably even over triple that. It is so big we had to jump through some hoops to make C# do the math necessary. This is why Arcus exists. As a thought exercise let's try to visualize the mighty scale of IPv6. diff --git a/docs/Glossary.rst b/docs/Glossary.rst index 4aae60e..3de221c 100644 --- a/docs/Glossary.rst +++ b/docs/Glossary.rst @@ -1,8 +1,15 @@ Glossary ======== +|version| v5.0.0 + .. glossary:: + MaxEnumerationExponent + ``int`` property on all range types (introduced in v5.0.0). Controls the maximum enumeration size: ``ToIPAddresses()`` (and ``GetEnumerator()``, for backwards compatibility) yields at most ``2^MaxEnumerationExponent`` addresses. The default is 12 (4096 addresses). Values range from 0 (enumeration disabled) to 128 (practically unlimited). If a range exceeds this cap, ``InvalidOperationException`` is thrown. + + **Why:** Prevents accidental enumeration of enormous address spaces (e.g., a ``/8`` IPv4 subnet has 16 million addresses). + Arcus Arcus is the lesser known Roman equivalent of the Greek goddess Iris. She is the Olympian messenger god. You know, because IP Addresses and Subnets are all about sending messages. Rainbows are cool too. @@ -19,23 +26,21 @@ Glossary `see Endianness on Wikipedia `_ - Big-Endian - Big-Endian ordering, at times also referred to as *Network Byte Order*, is a left-to-right ordering of bytes where the left most bytes are most significant than right most. + Big-Endian + Big-Endian ordering, at times also referred to as *Network Byte Order*, is a left-to-right ordering of bytes where the left most bytes are most significant than right most. - For example, the decimal value of the unsigned integer ``6060842`` may be represented as ``0x5C7B2A`` in hexadecimal. This hexadecimal value is composed of the three bytes ``0x5C``, ``0x7B``, and ``0x28``. As such the value ``6060842`` may be represented in Big-Endian as a byte array of ``[0x5C, 0x7B, 0x2A]``. + For example, the decimal value of the unsigned integer ``6060842`` may be represented as ``0x5C7B2A`` in hexadecimal. This hexadecimal value is composed of the three bytes ``0x5C``, ``0x7B``, and ``0x28``. As such the value ``6060842`` may be represented in Big-Endian as a byte array of ``[0x5C, 0x7B, 0x2A]``. - `see Gulliver's What is Endianness `_ - - Gulliver - Gulliver is a C# utility package and library engineered for the manipulation of arbitrary sized byte arrays accounting for appropriate endianness and jagged byte length. It was developed by the same folks who created Arcus. - - `see Gulliver on GitHub `_ + `see Endianness on Wikipedia `_ IP Address Short for **Internet Protocol Address** it is a numeric representation that typically comes in two flavors IPv4 and IPv6. `see IP Address on Wikipedia `_ + ToIPAddresses + Method on :ref:`IIPAddressRange` (introduced in v5.0.0). Returns an ``IEnumerable`` yielding addresses from ``Head`` to ``Tail``, capped at ``2^MaxEnumerationExponent``. Replaces the deprecated ``GetEnumerator()``. This will be the **only** enumeration method in v6.0.0. + IPv4 IPv4 is an IP Address that follows version 4 of the Internet Protocol. It is a 32-bit number, four bytes, with :math:`2^{32}` distinct addresses. IPv4 Addresses are typically represented in a format referred to as *Dotted Quad* or *Quad-dotted* in which the four bytes making the address are delimited by a period (*.*) character in decimal big-endian order, such as ``192.168.1.0``. diff --git a/docs/IIPAddressRange.rst b/docs/IIPAddressRange.rst index 076ef33..f427517 100644 --- a/docs/IIPAddressRange.rst +++ b/docs/IIPAddressRange.rst @@ -3,10 +3,19 @@ IIPAddressRange =============== -Arcus defines the ``IIPAddressRange`` interface for representation of consecutive ``IPAddress`` objects. It implements both ``IFormattable`` and ``IEnumerable``. +|version| v5.0.0 +Arcus defines the ``IIPAddressRange`` interface for representation of consecutive ``IPAddress`` objects. It implements ``IFormattable``. -.. caution:: ``IIPAddressRange`` implements ``IEnumerable``, this means that you should pay particular attention when you may be iterating over large ranges. Such as the full set of IPv6 addresses, which will take a while. A long while. It isn't recommended. +.. warning:: + + **Will be removed in v6.0.0:** ``IIPAddressRange`` currently implements ``IEnumerable`` for backwards compatibility, but this will be **removed in v6.0.0**. + + **Why:** The ``IEnumerable`` interface makes it too easy to accidentally enumerate astronomically large address spaces. Even with the enumeration cap, the interface contract itself encourages direct ``foreach`` usage that is semantically misleading — iterating over billions of items is rarely the intent. + + **Migration:** Replace ``foreach (var addr in range)`` with ``foreach (var addr in range.ToIPAddresses())`` now. This produces no warnings in v5 and will be **required** in v6.0.0. + + .. seealso:: :ref:`ToIPAddresses` for the replacement enumeration method. .. hint:: When dealing with more than one ``IPAddress`` or multiple implementations of ``IIPAddressRange`` unless otherwise explicitly stated their ``AddressFamily``, or equivalent properties, **must** match. @@ -14,6 +23,10 @@ Arcus defines the ``IIPAddressRange`` interface for representation of consecutiv ``IIPAddressRange`` is implemented by :ref:`AbstractIPAddressRange`, :ref:`IPAddressRange`, and :ref:`Subnet`. +.. note:: + + **New in v5.0.0:** All range types now accept an optional ``maxEnumerationExponent`` parameter (default 12) that caps enumeration at ``2^maxEnumerationExponent`` addresses (4096 by default). This prevents accidental enumeration of enormous address spaces. See :ref:`ToIPAddresses` for details. + Functionality Promises ---------------------- @@ -28,8 +41,30 @@ Properties :``bool`` IsIPv6: Returns ``true`` if, and only if, the range is IPv6 :``bool`` IsSingleIP: Returns ``true`` if, and only if, the range is comprised of only a single ``IPAddress`` :``BigInteger`` Length: The number of ``IPAddress`` within the range +:``int`` MaxEnumerationExponent: The exponent controlling the enumeration cap (0–128). Enumeration via ``ToIPAddresses()`` yields at most ``2^MaxEnumerationExponent`` addresses. Default is 12 (4096 addresses). :``IPAddress`` Tail: The last ``IPAddress`` within the range +Enumeration via ToIPAddresses +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. _ToIPAddresses: + +**New in v5.0.0:** The ``ToIPAddresses()`` method returns an ``IEnumerable`` yielding addresses from ``Head`` to ``Tail``, capped at ``2^MaxEnumerationExponent``. This is the recommended way to enumerate range addresses in v5 and will be the **only** way in v6.0.0. + +.. code-block:: c# + + IEnumerable ToIPAddresses(); + +.. caution:: + + If the range contains more addresses than the cap allows, ``InvalidOperationException`` is thrown at the point the limit is exceeded. Increase ``maxEnumerationExponent`` at construction time to enumerate larger ranges. A value of ``0`` disables enumeration entirely; ``128`` allows enumeration up to the full IPv6 space. + +.. warning:: + + **Obsolete in v5.0.0, will be removed in v6.0.0:** The ``GetEnumerator()`` method is now ``[Obsolete("Use ToIPAddresses() instead")]``. It still works by delegating to ``ToIPAddresses()`` but produces a compile-time warning. + + Replace all uses of ``foreach (var addr in range)`` with ``foreach (var addr in range.ToIPAddresses())`` now to prepare for v6.0.0. + Set Based Operations ^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/IPAddress-Comparers.rst b/docs/IPAddress-Comparers.rst index 272edec..eadbee3 100644 --- a/docs/IPAddress-Comparers.rst +++ b/docs/IPAddress-Comparers.rst @@ -2,8 +2,14 @@ IPAddress Comparers =================== +|version| v5.0.0 + IP Addresses are just numbers. Numbers are comparable. Some are bigger, some are smaller, some are even equal. +.. warning:: + + **Removed in v5.0.0:** The ``DefaultIPAddressRangeComparer`` type has been removed. Use :ref:`DefaultIIPAddressRangeComparer` instead. This type was previously marked ``[Obsolete]`` and has been fully removed in v5.0.0. + .. _DefaultIPAddressComparer: DefaultIPAddressComparer diff --git a/docs/IPAddress-Converters.rst b/docs/IPAddress-Converters.rst index f302696..f6fe4f2 100644 --- a/docs/IPAddress-Converters.rst +++ b/docs/IPAddress-Converters.rst @@ -19,39 +19,28 @@ Netmask To Cidr Route Prefix public static int NetmaskToCidrRoutePrefix(this IPAddress netmask) -The following example generates a table of all route prefixes for the equivalent netmask ``IPAddress`` input. Note that this example uses Gulliver [#GulliverLib]_ in order to deal with byte manipulation. +The following example generates a table of all route prefixes for the equivalent netmask ``IPAddress`` input. .. code-block:: c# - :emphasize-lines: 17 :caption: NetmaskToCidrRoutePrefix Example :name: NetmaskToCidrRoutePrefix Example public void NetmaskToCidrRoutePrefix_Example() { - // equivalent byte value of 255.255.255.255 or 2^32 - var maxIPv4Bytes = Enumerable.Repeat((byte) 0xFF, 4) - .ToArray(); - - // build all valid net masks - var allNetMasks = Enumerable.Range(7, 10) - .Select(i => maxIPv4Bytes.ShiftBitsLeft(32 - i)) // use Gulliver to shift bits of byte array - .Select(b => new IPAddress(b)) - .ToArray(); - - var sb = new StringBuilder(); - - foreach (var netmask in allNetMasks) + var output = new StringBuilder(); + + foreach (var routePrefix in Enumerable.Range(0, 33)) { - var routePrefix = netmask.NetmaskToCidrRoutePrefix(); - _ = sb.Append(routePrefix) - .Append('\t') - .AppendFormat("{0,-15}", netmask) - .Append('\t') - .Append(netmask.GetAddressBytes() - .ToString("b")) // using Gulliver to print bytes as bits - .AppendLine(); + var subnet = new Subnet(IPAddress.Parse("0.0.0.0"), routePrefix); + var netmask = subnet.Netmask; + + _ = output.Append(routePrefix) + .Append('\t') + .AppendFormat("{0,-15}", netmask) + .AppendLine(); } - this.output.WriteLine(sb.ToString()); + + this.output.WriteLine(output.ToString()); } .. code-block:: none @@ -137,7 +126,7 @@ The example below shows the output generated by calling the ``ToDottedQuadString ToHexString ----------- -``ToHexString`` may be used to encode the ``IPAddress ipAddress`` as a Big-Endian [#GulliverLib]_ ordered string. It will keep all zero-valued most significant bytes. +``ToHexString`` may be used to encode the ``IPAddress ipAddress`` as a big-endian ordered hexadecimal string. It will keep all zero-valued most significant bytes. .. code-block:: c# @@ -374,5 +363,4 @@ The example below shows the output created by calling the ``ToBase85String`` ext .. rubric:: Footnotes -.. [#GulliverLib] Interested in byte manipulation? Is endianess your calling? You should check out `Gulliver `_, an awesome opensource C# library developed by a number of smart and attractive people that like playing with thier bits. .. [#RFC1924] `RFC 1924 `_ is an April Fools Day Joke, but we implemented it anyhow. The question is, did we realize it was a joke before we implemented it or not. Ah, programmer jokes. There are 10 types of developers out there, those that get the joke, and those that don't. diff --git a/docs/IPAddress-Math.rst b/docs/IPAddress-Math.rst index dd3bdc3..e57242c 100644 --- a/docs/IPAddress-Math.rst +++ b/docs/IPAddress-Math.rst @@ -1,7 +1,7 @@ IP Address Math =============== -Too frequently the existing implementation of the C# ``IPAddress`` object is too limited for anything beyond some of the most trivial interactions. Mathematical operations in fact are wholly absent, forcing developers to directly manipulate bytes [#GulliverLib]_, often requiring a great deal of manual implementation of non-existent byte math. Don't worry though, Arcus is here to fill in some of those gaps. +Too frequently the existing implementation of the C# ``IPAddress`` object is too limited for anything beyond some of the most trivial interactions. Mathematical operations in fact are wholly absent, forcing developers to directly manipulate bytes, often requiring a great deal of manual implementation of non-existent byte math. Don't worry though, Arcus is here to fill in some of those gaps. .. note:: Unless otherwise specified regarding the math of the ``IPAddress`` object treats it as an unsigned integer based on its bytes interpenetrated as 32-bit for IPv4 and 128-bit for IPv6 all in big-endian byte order. @@ -95,6 +95,4 @@ Determine Scale .. rubric:: Footnotes -.. [#GulliverLib] If you actually want to manipulate bytes take a gander at `Gulliver `_, an C# library developed by the same folks that wrote Arcus. They're kinda great. - .. [#OperatorExtensionMethods] A GitHub issue for `Extension function members `_ requesting a champion for some proposed changes regarding the future of extension methods. diff --git a/docs/IPAddress-Utilities.rst b/docs/IPAddress-Utilities.rst index c46e59c..c5ce29f 100644 --- a/docs/IPAddress-Utilities.rst +++ b/docs/IPAddress-Utilities.rst @@ -7,6 +7,9 @@ Useful Values ^^^^^^^^^^^^^ Included within are some handy-dandy constant values and static readonly properties: +:``string`` DottedQuadLeadingZerosPattern: Regex pattern matching leading zeros in each dotted-quad octet +:``string`` DottedQuadRegularExpressionPattern: Regex pattern checking dotted-quad format +:``string`` HexLikePattern: Regex pattern matching hexadecimal digit strings :``int`` IPv4BitCount: The number of bits in an IPv4 address (``32``) :``int`` IPv4ByteCount: The number of bytes in an IPv4 address (``4``) :``int`` IPv4OctetCount: The number of octets in an IPv4 address (``4``) diff --git a/docs/IPAddressRange-Comparers.rst b/docs/IPAddressRange-Comparers.rst index 6d7c9b5..9aba763 100644 --- a/docs/IPAddressRange-Comparers.rst +++ b/docs/IPAddressRange-Comparers.rst @@ -1,23 +1,29 @@ IP Address Range Comparers ========================== +|version| v5.0.0 + +.. warning:: + + **Removed in v5.0.0:** The ``DefaultIPAddressRangeComparer`` type has been removed. Use :ref:`DefaultIIPAddressRangeComparer` (documented below) instead. This type was previously marked ``[Obsolete]`` and has been fully removed in v5.0.0. + Unsurprisingly, sometimes it is necessary to compare an :ref:`IIPAddressRange` to another. For that an implementation of a ``Comparer`` is just what the code monkey ordered. -.. _DefaultIPAddressRangeComparer: +.. _DefaultIIPAddressRangeComparer: -DefaultIPAddressRangeComparer -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +DefaultIIPAddressRangeComparer +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -.. note:: the ``DefaultIPAddressRangeComparer`` will happily compare ``IIPAddressRange`` of differing address families. +.. note:: the ``DefaultIIPAddressRangeComparer`` will happily compare ``IIPAddressRange`` of differing address families. -The ``DefaultIPAddressRangeComparer`` is a ``Comparer`` that compares implementations of ``IIPAddressRange`` first by their ``IIPAddressRange.Head`` and then by their total length. +The ``DefaultIIPAddressRangeComparer`` is a ``Comparer`` that compares implementations of ``IIPAddressRange`` first by their ``IIPAddressRange.Head`` and then by their total length. By default the two ``IIPAddressRange.Head`` values are compared via the :ref:`DefaultIPAddressComparer`, but that may be overridden by providing your own ``IComparer`` to the appropriate constructor. .. code-block:: c# - public DefaultIPAddressRangeComparer() + public DefaultIIPAddressRangeComparer() .. code-block:: c# - public DefaultIPAddressRangeComparer(IComparer ipAddressComparer) + public DefaultIIPAddressRangeComparer(IComparer ipAddressComparer) diff --git a/docs/IPAddressRange.rst b/docs/IPAddressRange.rst index de9485c..85af5cf 100644 --- a/docs/IPAddressRange.rst +++ b/docs/IPAddressRange.rst @@ -3,9 +3,19 @@ IPAddress Range =============== +|version| v5.0.0 + ``IPAddressRange`` is a very basic implementation of an :ref:`AbstractIPAddressRange` used to represent an inclusive range of arbitrary IP Addresses of the same address family. It isn't restricted to a `CIDR `_ representation like a :ref:`Subnet` is, allowing for non-power of two range sizes. -The ``IPAddressRange`` class extends :ref:`AbstractIPAddressRange` and implements :ref:`IIPAddressRange`, ``IEquatable``, ``IComparable``, ``IFormattable``, ``IEnumerable``, and ``ISerializable``. +The ``IPAddressRange`` class extends :ref:`AbstractIPAddressRange` and implements :ref:`IIPAddressRange`, ``IEquatable``, ``IComparable``, ``IFormattable``, and ``ISerializable``. + +.. warning:: + + ``IEnumerable`` (and ``GetEnumerator()``) are **deprecated in v5.0.0** and will be **removed in v6.0.0**. Use :ref:`ToIPAddresses` instead. See :ref:`IIPAddressRange` for details. + +.. note:: + + Beginning in v5.0.0, **all constructors and factory methods** accept an optional ``maxEnumerationExponent`` parameter (default 12) that controls enumeration limits. See the constructor signatures below. Creation -------- @@ -19,7 +29,9 @@ Addresses *MUST* be the same address family (either ``InterNetwork`` or ``InterN .. code-block:: c# - public IPAddressRange(IPAddress head, IPAddress tail) + public IPAddressRange(IPAddress head, IPAddress tail, int maxEnumerationExponent = 12) + +The optional ``maxEnumerationExponent`` parameter controls the enumeration cap: ``ToIPAddresses()`` will yield at most ``2^maxEnumerationExponent`` addresses (default 4096). constructor ``IPAddress address`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -28,7 +40,7 @@ On the rare occasion it may be desirable to make a ``IPAddressRange`` comprised .. code-block:: c# - public IPAddressRange(IPAddress address) + public IPAddressRange(IPAddress address, int maxEnumerationExponent = 12) Static Functionality -------------------- @@ -44,7 +56,13 @@ The function call will return ``true`` if it could collapse two or more ranges. .. code-block:: c# - public static bool TryCollapseAll(IEnumerable ranges, out IEnumerable result) + public static bool TryCollapseAll(IEnumerable ranges, out IEnumerable result, int maxEnumerationExponent = 12) + +The optional ``maxEnumerationExponent`` parameter controls the enumeration cap for any ranges created during collapse. + +.. note:: + + **v5.0.0 behavior correction — TryExcludeAll boundary guards:** When an exclusion ends at the family maximum address (e.g., ``255.255.255.255`` for IPv4), the method returns ``(true, leading segment)`` or ``(true, [])`` instead of throwing ``InvalidOperationException``. The trailing-segment path is guarded symmetrically for family-minimum exclusions. The following example shows that the three touching ranges of ``192.168.1.0 - 192.168.1.5``, ``192.168.1.6 - 192.168.1.7``, and ``192.168.1.8 - 192.168.1.20`` were collapsed into the new ``IPAddressRange`` of ``192.168.1.0 - 192.168.1.20``. @@ -84,10 +102,15 @@ TryExcludeAll ``TryExcludeAll`` is a tricky beast, but if you're willing to take the time to tame it'll not only respect you, but it may also take care of you in very specific cases. The method takes a ``IPAddressRange initialRange`` and with that it attempts to systematically remove each of the sub ranges defined within ``IEnumerable excludedRanges``. On success, the operation returns ``true`` and will *out* an ``IEnumerable result`` which is comprised of a distinct remaining ranges after ``excludedRanges`` have been carved out. +.. note:: + + A return value of ``false`` signals an error condition (null input, address-family mismatch, or null elements in ``excludedRanges``), **not** an empty result set. A ``true`` return with an empty ``result`` means the exclusions cover the entire ``initialRange``. + + **Family-boundary behavior:** when an exclusion ends at the family maximum address (e.g., ``255.255.255.255`` for IPv4) no trailing segment can be produced; the method returns ``true`` with only the leading segment (which may itself be empty). When an exclusion starts at the family minimum address (e.g., ``0.0.0.0``) no leading segment can be produced; the method returns ``true`` with only the trailing segment. .. code-block:: c# - public static bool TryExcludeAll(IPAddressRange initialRange, IEnumerable excludedRanges, out IEnumerable result) + public static bool TryExcludeAll(IPAddressRange initialRange, IEnumerable excludedRanges, out IEnumerable result, int maxEnumerationExponent = 12) TryMerge ^^^^^^^^ @@ -96,4 +119,4 @@ TryMerge .. code-block:: c# - public static bool TryMerge(IPAddressRange left, IPAddressRange right, out IPAddressRange mergedRange) + public static bool TryMerge(IPAddressRange left, IPAddressRange right, out IPAddressRange mergedRange, int maxEnumerationExponent = 12) diff --git a/docs/MacAddress.rst b/docs/MacAddress.rst deleted file mode 100644 index 7d702df..0000000 --- a/docs/MacAddress.rst +++ /dev/null @@ -1,150 +0,0 @@ -.. _MacAddress: - -MacAddress -========== - -The ``MacAddress`` type represents a 48-bit MAC Address [#48-BitMAC]_ as per the IEEE EUI standard [#IEEE-Eui]_. It serves the purpose of a Networking Adjacent worker class, and as a handy way to represent, store, format, and compare MAC addresses. - -The ``MacAddress`` class implements ``IEquatable``, ``IComparable``, ``IComparable``, ``IFormattable``, and ``ISerializable``. - -.. note:: - - Unless otherwise stated recognized readable MAC Address formats include only the following formats: - - - IEEE 802 format for printing **EUI-48** and **MAC-48** addresses in six groups of two hexadecimal digits, separated by a dash (``-``). *E.g.* ``AA-BB-CC-DD-EE-FF`` - - Common Six groups of two hexadecimal digits separated by colons (``:``). *E.g.* ``AA:BB:CC:DD:EE:FF`` - - Six groups of two hexadecimal digits separated by a space character. *E.g.* ``AA BB CC DD EE FF`` - - 12 hexadecimal digits with no delimitation. *E.g.* ``AABBCCDDEEFF`` - - Cisco three groups of four hexadecimal digits separated by dots (``.``). *E.g.* ``AABB.CCDD.EEFF`` - - For the sake of parsing and reading these formats are case insensitive. - -.. figure:: img/MAC-48_Address.svg - :scale: 50 % - :align: center - :alt: MAC-48 Address.svg This file is licensed under the Creative Commons Attribution-Share Alike 2.5 Generic, 2.0 Generic and 1.0 Generic license. - :target: https://commons.wikimedia.org/wiki/File:MAC-48_Address.svg - - Structure of a MAC-48 Address - -Creation --------- - -Constructor -^^^^^^^^^^^ - -``IEnumerable`` -+++++++++++++++++++++ - -A new ``MacAddress`` may be constructed by providing an ``IEnumerable`` of six bytes to the constructor. - -.. code-block:: c# - - public MacAddress(IEnumerable bytes) - -Factory -^^^^^^^ - -Parse ``string`` -++++++++++++++++ - -A ``MacAddress`` may also be created via either the ``Parse`` or safe ``TryParse`` method. Not that these methods are strict in that they will only succeed with a MAC address in a known format. If you wish to more liberally parse a string into a ``MacAddress`` see the ``ParseAny`` and ``TryParseAny`` defined below. - -.. code-block:: c# - - public static MacAddress Parse(string input) - -.. code-block:: c# - - public static bool TryParse(string input, out MacAddress macAddress) - -ParseAny ``string`` -+++++++++++++++++++ - -``ParseAny`` and the safe ``TryParseAny`` allow the parsing of an arbitrary string that may be a Mac address into a ``MacAddress``. It looks for six hexadecimal digits within the string, joins them and interprets the result as consecutive big-endian hextets. If six, and only six, hexadecimal digits are not found the parse will fail. - -.. code-block:: c# - - public static MacAddress ParseAny(string input) - -.. code-block:: c# - - public static bool TryParseAny(string input, out MacAddress macAddress) - -Functionality -------------- - -Properties -^^^^^^^^^^ -:``bool`` IsDefault: returns ``true`` if, and only if, the MAC Address is the EUI-48 default [#EUI-48Default]_, meaning all bits of the MAC Address are set making it equivalent to ``FF:FF:FF:FF:FF:FF``. -:``bool`` IsGloballyUnique: returns ``true`` if, and only if, is globally unique (OUI [#Eui-Oui]_ enforced). -:``bool`` IsLocallyAdministered: returns ``true`` if, and only if, is locally administered. -:``bool`` IsMulticast: returns ``true`` if, and only if, the MAC Address is multicast. -:``bool`` IsUnicast: returns ``true`` if, and only if, the MAC Address is unicast. -:``bool`` IsUnusable: returns ``true`` if, and only if, the MAC Address is "unusable" [#EUI-Usable]_, meaning all OUI bits of the MAC Address are unset. - -:``MacAddress`` DefaultMacAddress: Provides a ``MacAddress`` that represents the default or ``null`` case MAC address. - -:``Regex`` AllFormatMacAddressRegularExpression: Returns a regular expression for matching accepted MAC Address formats. -:``Regex`` CommonFormatMacAddressRegularExpression: Returns a regular expression for matching the "common" six groups of two uppercase hexadecimal digits format. - -:``string`` AllFormatMacAddressPattern: Returns a regular expression pattern for matching accepted MAC Address formats. -:``string`` CommonFormatMacAddressPattern: Returns a regular expression pattern for matching the "common" six groups of two uppercase hexadecimal digits format. - -Methods -^^^^^^^ - -GetAddressBytes -+++++++++++++++ - -``GetAddressBytes`` returns a copy of the underlying big-endian bytes of the ``MacAddress``. This will always be six bytes in length. - -.. code-block:: c# - - public byte[] GetAddressBytes() - -GetOuiBytes -+++++++++++ - -``GetOuiBytes`` returns the *Organizationally Unique Identifier (OUI)* [#Eui-Oui]_ of the ``MacAddress``. - -.. code-block:: c# - - public byte[] GetOuiBytes() - -GetCidBytes -+++++++++++ - -``GetCidBytes`` returns the *Company ID (CID)* [#Eui-Cid]_ of the ``MacAddress``. - -.. code-block:: c# - - public byte[] GetCidBytes() - -IFormatable -^^^^^^^^^^^ - -``MacAddress`` offers a number or preexisting formats that are accessible via the standard ``ToString`` method provided by ``IFormattable`` interface. - -.. csv-table:: Subnet format values - :file: macaddress-formats.csv - :header-rows: 1 - -Operators -^^^^^^^^^ - -``MacAddress`` implements all the standard C# equality and comparison operators. The comparison operators treat the ``MacAddress`` bytes as an unsigned big-endian integer value. - -.. rubric:: Footnotes - -.. [#48-BitMAC] **48-Bit MAC** is a Media Access Control Address (MAC) following both the now deprecated *MAC-48* and the active *EUI-48* specifications. - -.. [#EUI-48Default] The recommended null or default value for **EUI-48** is ``FF-FF-FF-FF-FF-FF`` - -.. [#IEEE-Eui] `Guidelines for Use of Extended Unique Identifier (EUI), Organizationally Unique Identifier (OUI), and Company ID (CID) `_ - -.. [#Eui-Oui] *Organizationally Unique Identifier (OUI)* is the first 3-bytes (24-bits) of a MAC-48 MAC Address. - -.. [#Eui-Cid] *Company Id (Cid)* is the last 3-bytes (24-bits) of a MAC-48 MAC Address. - -.. [#EUI-Usable] Usable **EUI-48** values are based on a zeroed OUI. A all zero EUI value, such as ``00-00-00-00-00-00`` and ``00-00-00-FA-BC-21``, according to spec shall not be used as an identifier. diff --git a/docs/Subnet-Utilities.rst b/docs/Subnet-Utilities.rst index 9a1679e..5bb00de 100644 --- a/docs/Subnet-Utilities.rst +++ b/docs/Subnet-Utilities.rst @@ -1,16 +1,30 @@ Subnet Utilities ================ +|version| v5.0.0 + ``Arcus.Utilities.SubnetUtilities`` is a static utility class containing miscellaneous operations for :ref:`Subnet` and collections there of. It is a catchall for methods and functionally that didn't make sense on the ``Subnet`` class itself. +.. warning:: + + **Breaking change in v5.0.0:** ``PrivateIPAddressRangesList`` and ``LinkLocalIPAddressRangesList`` are now declared ``readonly``. Code that previously reassigned either field (e.g., ``SubnetUtilities.PrivateIPAddressRangesList = myList``) will no longer compile. The ``IReadOnlyList`` type already prevented mutation of the list contents; ``readonly`` now extends that guarantee to the field reference itself. + + **Why:** Making these fields ``readonly`` prevents external code from silently replacing the entire list, which would affect all callers depending on these lists (e.g., ``ContainsAnyPrivateAddresses``, ``IsPrivate``). This is a consistency and safety improvement. + find Fewest Consecutive Subnets ------------------------------- +.. note:: + + **v5.0.0:** This method now accepts an optional ``maxEnumerationExponent`` parameter (default 12) that controls the enumeration cap for subnets created during computation. + Given an inclusive range of IP Addresses defined by ``IPAddress left`` and ``IPAddress right`` get the fewest consecutive subnets that would contain all addresses within the range between and no other addresses. .. code-block:: c# - public static IEnumerable FewestConsecutiveSubnetsFor(IPAddress left, IPAddress right) + public static IEnumerable FewestConsecutiveSubnetsFor(IPAddress left, IPAddress right, int maxEnumerationExponent = 12) + +The optional ``maxEnumerationExponent`` parameter controls the enumeration cap for subnets created during computation. The following examples shows that the range defined by ``192.168.1.3`` - ``192.168.1.5`` fits in two consecutive subnets defined by ``192.168.1.4/31`` and ``192.168.1.3/32``. diff --git a/docs/Subnet.rst b/docs/Subnet.rst index f879ac6..61314e4 100644 --- a/docs/Subnet.rst +++ b/docs/Subnet.rst @@ -3,11 +3,21 @@ Subnet ====== -The ``Subnet`` type, flavored in both IPv4 or IPv6, is a representation of a subnetwork within Arcus. It is the workhorse and original reason for the Arcus library. Outside the concept of the ``Subnet`` object, most everything else in Arcus is auxiliary and exists only in support of making this one facet work. That’s not to say that the remaining pieces of the Arcus library aren’t useful, on the contrary their utility can benefit a developer greatly. But that said, once the dark and mysterious magic of the ``Subnet`` is understood the rest of Arcus should be easy to understand. +|version| v5.0.0 + +The ``Subnet`` type, flavored in both IPv4 or IPv6, is a representation of a subnetwork within Arcus. It is the workhorse and original reason for the Arcus library. Outside the concept of the ``Subnet`` object, most everything else in Arcus is auxiliary and exists only in support of making this one facet work. That's not to say that the remaining pieces of the Arcus library aren't useful, on the contrary their utility can benefit a developer greatly. But that said, once the dark and mysterious magic of the ``Subnet`` is understood the rest of Arcus should be easy to understand. Keep in mind that a ``Subnet`` is not an arbitrary range of addresses, for that you want an :ref:`IPAddressRange`, but rather conforms to a range of length :math:`2^n` starting a particular position, following the typical rules of `Classless Inter-Domain Routing `_. -The ``Subnet`` class extends :ref:`AbstractIPAddressRange` and implements :ref:`IIPAddressRange`, ``IEquatable``, ``IComparable``, ``IFormattable``, ``IEnumerable``, and ``ISerializable``. +The ``Subnet`` class extends :ref:`AbstractIPAddressRange` and implements :ref:`IIPAddressRange`, ``IEquatable``, ``IComparable``, ``IFormattable``, and ``ISerializable``. + +.. warning:: + + ``IEnumerable`` (and ``GetEnumerator()``) are **deprecated in v5.0.0** and will be **removed in v6.0.0**. Use :ref:`ToIPAddresses` instead. See :ref:`IIPAddressRange` for details. + +.. note:: + + Beginning in v5.0.0, **all constructors and factory methods** accept an optional ``maxEnumerationExponent`` parameter (default 12) that controls enumeration limits. See the constructor signatures below. .. note:: Be aware that ``Subnet`` does *not* extend :ref:`IPAddressRange` but does implement :ref:`IIPAddressRange`. @@ -27,7 +37,9 @@ Addresses *MUST* be the same address family (either ``InterNetwork`` or ``InterN .. code-block:: c# - public Subnet(IPAddress lowAddress, IPAddress highAddress) + public Subnet(IPAddress lowAddress, IPAddress highAddress, int maxEnumerationExponent = 12) + +The optional ``maxEnumerationExponent`` parameter controls the enumeration cap: ``foreach`` / ``ToIPAddresses()`` will yield at most ``2^maxEnumerationExponent`` addresses (default 4096). constructor ``IPAddress address``, ``int routingPrefix`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -36,7 +48,7 @@ It is also possible to create a ``Subnet`` from an ``IPAddress address`` and an .. code-block:: c# - public Subnet(IPAddress address, int routingPrefix) + public Subnet(IPAddress address, int routingPrefix, int maxEnumerationExponent = 12) The following example shows that the ``IPAddress`` and ``routingPrefix`` constructor taking an input of ``192.168.1.1`` and ``24`` creates a ``Subnet`` ``192.168.1.0/32``. Note that the ``Head`` is ``192.168.1.0`` and not ``192.168.1.1``, this is done as Arcus will autocorrect the input to a valid ``Subnet``. If this is not desired it is advised that you compare the ``Head`` to the input in order to validate expectations. @@ -105,11 +117,11 @@ The following factory methods may be used to create an IPv4 ``Subnet`` where as .. code-block:: c# - public static Subnet FromNetMask(IPAddress address, IPAddress netmask) + public static Subnet FromNetMask(IPAddress address, IPAddress netmask, int maxEnumerationExponent = 12) .. code-block:: c# - public static bool TryFromNetMask(IPAddress address, IPAddress netmask, out Subnet subnet) + public static bool TryFromNetMask(IPAddress address, IPAddress netmask, out Subnet subnet, int maxEnumerationExponent = 12) factory From Big-Endian Byte Arrays ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -121,11 +133,11 @@ The given ``byte`` arrays are interpreted as being in big-endian ordering are ar .. code-block:: c# - public static Subnet FromBytes(byte[] lowAddressBytes, byte[] highAddressBytes) + public static Subnet FromBytes(byte[] lowAddressBytes, byte[] highAddressBytes, int maxEnumerationExponent = 12) .. code-block:: c# - public static bool TryFromBytes(byte[] lowAddressBytes, byte[] highAddressBytes, out Subnet subnet) + public static bool TryFromBytes(byte[] lowAddressBytes, byte[] highAddressBytes, out Subnet subnet, int maxEnumerationExponent = 12) parse string ^^^^^^^^^^^^ @@ -136,11 +148,11 @@ If a representation of an IP Address ``string`` is provided the resulting ``Subn .. code-block:: c# - public static Subnet Parse(string subnetString) + public static Subnet Parse(string subnetString, int maxEnumerationExponent = 12) .. code-block:: c# - public static bool TryParse(string subnetString, out Subnet subnet) + public static bool TryParse(string subnetString, out Subnet subnet, int maxEnumerationExponent = 12) parse IPAddress string and RoutingPrefix int ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -149,11 +161,11 @@ It is also possible to build a ``Subnet`` from an ``String address`` and an ``i .. code-block:: c# - public static Subnet Parse(string addressString, int routingPrefix) + public static Subnet Parse(string addressString, int routingPrefix, int maxEnumerationExponent = 12) .. code-block:: c# - public static bool TryParse(string addressString, int routingPrefix, out Subnet subnet) + public static bool TryParse(string addressString, int routingPrefix, out Subnet subnet, int maxEnumerationExponent = 12) parse IPAddress strings ^^^^^^^^^^^^^^^^^^^^^^^ @@ -162,11 +174,11 @@ A rather common way to to build a ``Subnet`` is to provide a pair of ``string`` .. code-block:: c# - public static Subnet Parse(string lowAddressString, string highAddressString) + public static Subnet Parse(string lowAddressString, string highAddressString, int maxEnumerationExponent = 12) .. code-block:: c# - public static bool TryParse(string lowAddressString, string highAddressString, out Subnet subnet) + public static bool TryParse(string lowAddressString, string highAddressString, out Subnet subnet, int maxEnumerationExponent = 12) Functionality ------------- @@ -185,6 +197,10 @@ In addition to the properties defined in :ref:`IIPAddressRange` ``Subnet`` provi :``int`` RoutingPrefix: The routing prefix used to specify the subnet :``BigInteger`` UsableHostAddressCount: The number of usable addresses in the subnet ignoring both the Broadcast and Network addresses +Inherited from :ref:`IIPAddressRange`: + +:``int`` MaxEnumerationExponent: The exponent controlling enumeration cap (0–128). Enumeration via ``ToIPAddresses()`` yields at most ``2^MaxEnumerationExponent`` addresses. + Set Based Operations ^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/index.rst b/docs/index.rst index 678ff78..03a7640 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -18,25 +18,36 @@ Arcus :alt: Apache 2.0 License :target: https://github.com/sandialabs/Arcus/blob/main/LICENSE +|version| Arcus v5.0.0 + Arcus is a C# manipulation library for calculating, parsing, formatting, converting, and comparing both IPv4 and IPv6 addresses and subnets. It accounts for 128-bit numbers on 32-bit platforms. Arcus provides extension and helper methods for the pre-existing ``System.Net.IPAddress`` and other objects within that realm. It was created to fill in some of the gaps left by the absence of a representation of a :doc:`Subnet `. As more gaps were found, they were filled. Like all coding projects, Arcus is a work in progress. We rely on both our free time and our :doc:`community ` in order to provide the best solution we can given the constraints we must conform to. .. hint:: Chances are you're primarily here looking for the :doc:`Subnet ` object. -Arcus heavily relies upon one of our other libraries `Gulliver `_, if you're interested in byte manipulation it is worth checking out. +.. _release-notes: .. warning:: - Breaking Changes in Version 3+ + Arcus v5.0.0 introduces several **breaking changes**. See the :ref:`release-notes` for a full migration guide. + + Key changes: + - Enumeration cap (``MaxEnumerationExponent``) prevents accidental enumeration of large ranges + - ``ToIPAddresses()`` replaces ``GetEnumerator()`` (deprecated, will be removed in v6) + - ``MacAddress`` type and ``DefaultIPAddressRangeComparer`` removed + - Gulliver dependency removed + - ``SubnetUtilities.*List`` fields are now ``readonly`` + +.. warning:: - In .NET versions up to and including .NET 4.8 (which corresponds to .NET Standard 2.0), stricter parsing rules were enforced for `IPAddress` according to the IPv6 specification.Specifically, the presence of a terminal '%' character without a valid zone index is considered invalid in these versions. As a result, the input `abcd::%` fails to parse, leading to a null or failed address parsing depending on `Parse`/`TryParse`. This behavior represents a breaking change from Arcus's previous target of .NET Standard 1.3. and may provide confusion for .NET 4.8 / .NET Standard 2.0 versions. + IP Address Parsing differs across .NET Targets - In contrast, in newer versions of .NET, including .NET 8 and .NET 9, the parsing rules have been relaxed. The trailing '%' character is now ignored during parsing, allowing for inputs that would have previously failed. + In .NET versions up to and including .NET 4.8 (which corresponds to .NET Standard 2.0), stricter parsing rules are enforced for `IPAddress` according to the IPv6 specification. Specifically, the presence of a terminal '%' character without a valid zone index is considered invalid in these versions. As a result, the input `abcd::%` fails to parse, leading to a null or failed address parsing depending on `Parse`/`TryParse`. - It is important to note that this scenario appears to be an extreme edge case, and developers should ensure that their applications handle `IPAddress` parsing appropriately across different target frameworks as expected. + In newer versions of .NET, including .NET 8, .NET 9, and .NET 10, the parsing rules have been relaxed. The trailing '%' character is now ignored during parsing, allowing for inputs that would have previously failed. - If in doubt it is suggested that IP Address based user input should be sanitized to meet your development needs. + This appears to be an extreme edge case. If in doubt, sanitize IP address user input to meet your development needs. .. toctree:: :maxdepth: 1 @@ -59,12 +70,6 @@ Arcus heavily relies upon one of our other libraries `Gulliver /` folder layout in the `.nupkg` +- Wrong TFM asset being chosen by a consumer (e.g. `net9.0` falling back to `netstandard2.0` + when a dedicated `net9.0` asset should exist) +- BCL types referenced by a C# 14 language feature that are missing on an older runtime +- Missing or mismatched transitive dependencies declared in the `.nuspec` + +Smoke tests fix this by **packing the library first**, then referencing it as a NuGet package +from a separate consumer project — exactly as an end-user would. + +## Structure + +``` +smoketests/ +├── nuget.config # adds smoketests/feed/ as a local NuGet source +├── run-smoke-tests.sh # Linux / macOS (net8.0, net9.0, net10.0) +├── run-smoke-tests.ps1 # Windows (net48, net8.0, net9.0, net10.0) +└── SmokeTests/ + ├── SmokeTests.csproj # multi-target console app; PackageReference to Arcus + └── Program.cs # exercises the full public API surface +``` + +`smoketests/feed/` is git-ignored; the run scripts create it on demand. + +## Running locally + +### Linux / macOS + +```bash +# From the repo root: +./smoketests/run-smoke-tests.sh + +# With an explicit version: +./smoketests/run-smoke-tests.sh 4.0.0-preview +``` + +Covers **net8.0**, **net9.0**, **net10.0**. + +### Windows (PowerShell) + +```powershell +# From the repo root: +.\smoketests\run-smoke-tests.ps1 + +# With an explicit version: +.\smoketests\run-smoke-tests.ps1 -Version 4.0.0-preview +``` + +Covers **net48** (→ `netstandard2.0` asset), **net8.0**, **net9.0**, **net10.0**. + +> **net48 requires Windows.** The `netstandard2.0` asset is what any .NET Framework +> or legacy .NET Standard consumer receives; net48 is the proxy TFM that validates it. + +## What is tested + +`Program.cs` exercises the entire public API surface against a real runtime: + +| Area | Checks | +|---|---| +| `Subnet` (IPv4) | Parse, construct (address+prefix, two addresses), Netmask, Contains, Overlaps, Touches, Enumerate, ContainsAnyPrivateAddresses, ContainsAllPrivateAddresses, TryParse, Deconstruct | +| `Subnet` (IPv6) | Parse, construct, Contains, Overlaps | +| `IPAddressRange` | Construct, Contains, Overlaps | +| `SubnetUtilities` | FewestConsecutiveSubnetsFor, PrivateIPAddressRangesList, LargestSubnet, SmallestSubnet | +| `IPAddressMath` | Increment, TryIncrement, IsGreaterThan/LessThan/EqualTo, IsBetween, Max, Min, IsAtMin/Max | +| `IPAddressUtilities` | IsIPv4, IsIPv6, IsPrivate, IPv4Min/MaxAddress, IsValidNetMask, ParseFromHexString | +| `IPAddressConverters` | NetmaskToCidrRoutePrefix, ToHexString, ToNumericString | +| `Comparers` | DefaultIPAddressComparer, DefaultIIPAddressRangeComparer, DefaultAddressFamilyComparer | + +## CI + +The `smoke-test` job in `.github/workflows/build.yml` runs automatically on every PR and push +to `main`, after the main `build` job succeeds. It uses a matrix strategy: + +- **ubuntu-latest** → net8.0, net9.0, net10.0 +- **windows-latest** → net48 (netstandard2.0 asset validation) + +Exit code 1 from any framework fails the job. diff --git a/smoketests/SmokeTests/Program.cs b/smoketests/SmokeTests/Program.cs new file mode 100644 index 0000000..ba991e9 --- /dev/null +++ b/smoketests/SmokeTests/Program.cs @@ -0,0 +1,607 @@ +// Arcus NuGet package smoke tests. +// +// This program is a consumer of the *packed NuGet package* (not a project reference). +// Its only job is to prove that each target-framework asset (netstandard2.0 via net48, +// net8.0, net9.0, net10.0) loads correctly and that the public API behaves at runtime. +// +// Run via smoke-tests/run-smoke-tests.{sh,ps1} — those scripts pack the library first. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Numerics; +using Arcus; +using Arcus.Comparers; +using Arcus.Converters; +using Arcus.Math; +using Arcus.Utilities; + +var tfm = System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription; +Console.WriteLine($"=== Arcus Smoke Tests ({tfm}) ==="); +Console.WriteLine(); + +var passed = 0; +var failed = 0; + +void Check(string name, Action test) +{ + try + { + test(); + Console.WriteLine($" [PASS] {name}"); + passed++; + } + catch (Exception ex) + { + Console.WriteLine($" [FAIL] {name}: {ex.Message}"); + failed++; + } +} + +void Require(bool condition, string message) +{ + if (!condition) + throw new InvalidOperationException(message); +} + +void RequireEqual(T expected, T actual, string field) +{ + if (!Equals(expected, actual)) + throw new InvalidOperationException($"{field}: expected '{expected}', got '{actual}'"); +} + +// --------------------------------------------------------------------------- +// Subnet — IPv4 +// --------------------------------------------------------------------------- +Console.WriteLine("Subnet (IPv4)"); + +Check( + "Parse CIDR notation", + () => + { + var s = Subnet.Parse("192.168.0.0/24"); + RequireEqual( + IPAddress.Parse("192.168.0.0"), + s.NetworkPrefixAddress, + "NetworkPrefixAddress" + ); + RequireEqual(IPAddress.Parse("192.168.0.255"), s.BroadcastAddress, "BroadcastAddress"); + RequireEqual(24, s.RoutingPrefix, "RoutingPrefix"); + RequireEqual(new BigInteger(256), s.Length, "Length"); + RequireEqual(new BigInteger(254), s.UsableHostAddressCount, "UsableHostAddressCount"); + Require(s.IsIPv4, "IsIPv4"); + } +); + +Check( + "Construct from address + routing prefix", + () => + { + var s = new Subnet(IPAddress.Parse("10.0.0.0"), 8); + RequireEqual(IPAddress.Parse("10.0.0.0"), s.NetworkPrefixAddress, "NetworkPrefixAddress"); + RequireEqual(IPAddress.Parse("10.255.255.255"), s.BroadcastAddress, "BroadcastAddress"); + RequireEqual(8, s.RoutingPrefix, "RoutingPrefix"); + } +); + +Check( + "Construct from two addresses (smallest containing subnet)", + () => + { + var s = new Subnet(IPAddress.Parse("192.168.1.100"), IPAddress.Parse("192.168.1.200")); + Require(s.Contains(IPAddress.Parse("192.168.1.100")), "Contains low bound"); + Require(s.Contains(IPAddress.Parse("192.168.1.200")), "Contains high bound"); + } +); + +Check( + "Netmask", + () => + { + var s = Subnet.Parse("10.0.0.0/8"); + RequireEqual(IPAddress.Parse("255.0.0.0"), s.Netmask, "Netmask"); + } +); + +Check( + "Contains IPAddress", + () => + { + var s = Subnet.Parse("172.16.0.0/12"); + Require(s.Contains(IPAddress.Parse("172.20.0.1")), "inside"); + Require(!s.Contains(IPAddress.Parse("172.32.0.1")), "outside"); + } +); + +Check( + "Contains Subnet", + () => + { + var outer = Subnet.Parse("10.0.0.0/8"); + var inner = Subnet.Parse("10.1.0.0/16"); + Require(outer.Contains(inner), "inner ⊆ outer"); + Require(!inner.Contains(outer), "outer ⊄ inner"); + } +); + +Check( + "Overlaps", + () => + { + var a = Subnet.Parse("192.168.0.0/24"); + var b = Subnet.Parse("192.168.0.128/25"); + Require(a.Overlaps(b), "overlapping subnets"); + var c = Subnet.Parse("10.0.0.0/8"); + Require(!a.Overlaps(c), "disjoint subnets"); + } +); + +Check( + "Touches (adjacent subnets)", + () => + { + var a = Subnet.Parse("192.168.0.0/25"); + var b = Subnet.Parse("192.168.0.128/25"); + Require(a.Touches(b), "adjacent /25 subnets touch"); + } +); + +Check( + "Enumerate (small /30 = 4 addresses)", + () => + { + var s = Subnet.Parse("192.168.1.0/30"); + var addresses = s.ToList(); + RequireEqual(4, addresses.Count, "address count"); + RequireEqual(IPAddress.Parse("192.168.1.0"), addresses[0], "first address"); + RequireEqual(IPAddress.Parse("192.168.1.3"), addresses[3], "last address"); + } +); + +Check( + "ContainsAnyPrivateAddresses", + () => + { + Require(Subnet.Parse("10.1.2.0/24").ContainsAnyPrivateAddresses(), "10.x/24 is private"); + Require(!Subnet.Parse("8.8.8.0/24").ContainsAnyPrivateAddresses(), "8.8.8.0/24 is public"); + } +); + +Check( + "ContainsAllPrivateAddresses", + () => + { + Require(Subnet.Parse("192.168.5.0/24").ContainsAllPrivateAddresses(), "all private"); + } +); + +Check( + "TryParse success and failure", + () => + { + Require(Subnet.TryParse("192.168.100.0/22", out var s), "valid CIDR parses"); + Require(s != null, "result not null"); + RequireEqual(22, s!.RoutingPrefix, "RoutingPrefix"); + Require(!Subnet.TryParse("not-an-ip", out _), "garbage input returns false"); + } +); + +Check( + "Deconstruct (address, prefix)", + () => + { + var s = Subnet.Parse("10.2.3.0/24"); + s.Deconstruct(out var addr, out var prefix); + RequireEqual(IPAddress.Parse("10.2.3.0"), addr, "address"); + RequireEqual(24, prefix, "prefix"); + } +); + +// --------------------------------------------------------------------------- +// Subnet — IPv6 +// --------------------------------------------------------------------------- +Console.WriteLine("\nSubnet (IPv6)"); + +Check( + "Parse IPv6 CIDR", + () => + { + var s = Subnet.Parse("2001:db8::/32"); + RequireEqual(AddressFamily.InterNetworkV6, s.Head.AddressFamily, "AddressFamily"); + RequireEqual(32, s.RoutingPrefix, "RoutingPrefix"); + Require(s.IsIPv6, "IsIPv6"); + } +); + +Check( + "Construct IPv6 from address + prefix", + () => + { + var s = new Subnet(IPAddress.Parse("fd00::"), 8); + Require(s.Contains(IPAddress.Parse("fd00::1")), "Contains fd00::1"); + Require(s.IsIPv6, "IsIPv6"); + } +); + +Check( + "IPv6 Contains and Overlaps", + () => + { + var outer = Subnet.Parse("2001:db8::/32"); + var inner = Subnet.Parse("2001:db8:1::/48"); + Require(outer.Contains(inner), "inner ⊆ outer"); + Require(outer.Overlaps(inner), "overlaps"); + } +); + +// --------------------------------------------------------------------------- +// IPAddressRange +// --------------------------------------------------------------------------- +Console.WriteLine("\nIPAddressRange"); + +Check( + "Construct arbitrary range", + () => + { + var r = new IPAddressRange(IPAddress.Parse("10.0.0.5"), IPAddress.Parse("10.0.0.10")); + RequireEqual(IPAddress.Parse("10.0.0.5"), r.Head, "Head"); + RequireEqual(IPAddress.Parse("10.0.0.10"), r.Tail, "Tail"); + RequireEqual(new BigInteger(6), r.Length, "Length"); + } +); + +Check( + "Contains IPAddress", + () => + { + var r = new IPAddressRange(IPAddress.Parse("192.168.0.1"), IPAddress.Parse("192.168.0.50")); + Require(r.Contains(IPAddress.Parse("192.168.0.25")), "inside"); + Require(!r.Contains(IPAddress.Parse("192.168.0.51")), "outside"); + } +); + +Check( + "Overlaps with Subnet", + () => + { + var r = new IPAddressRange(IPAddress.Parse("10.0.0.0"), IPAddress.Parse("10.0.0.100")); + var s = Subnet.Parse("10.0.0.50/30"); + Require(r.Overlaps(s), "range overlaps subnet"); + } +); + +// --------------------------------------------------------------------------- +// SubnetUtilities +// --------------------------------------------------------------------------- +Console.WriteLine("\nSubnetUtilities"); + +Check( + "FewestConsecutiveSubnetsFor", + () => + { + var subnets = SubnetUtilities + .FewestConsecutiveSubnetsFor( + IPAddress.Parse("192.168.0.1"), + IPAddress.Parse("192.168.0.10") + ) + .ToList(); + Require(subnets.Count > 0, "at least one result subnet"); + Require(subnets.All(s => s.IsIPv4), "all results are IPv4"); + Require(subnets.First().Contains(IPAddress.Parse("192.168.0.1")), "covers left bound"); + Require(subnets.Last().Contains(IPAddress.Parse("192.168.0.10")), "covers right bound"); + } +); + +Check( + "PrivateIPAddressRangesList", + () => + { + Require(SubnetUtilities.PrivateIPAddressRangesList.Count > 0, "list not empty"); + Require( + SubnetUtilities.PrivateIPAddressRangesList.Any(s => + s.Contains(IPAddress.Parse("10.1.2.3")) + ), + "10.1.2.3 is in private list" + ); + Require( + SubnetUtilities.PrivateIPAddressRangesList.Any(s => + s.Contains(IPAddress.Parse("192.168.0.1")) + ), + "192.168.0.1 is in private list" + ); + } +); + +Check( + "LargestSubnet / SmallestSubnet", + () => + { + var subnets = new[] + { + Subnet.Parse("10.0.0.0/24"), + Subnet.Parse("10.0.0.0/16"), + Subnet.Parse("10.0.0.0/28"), + }; + RequireEqual(16, SubnetUtilities.LargestSubnet(subnets).RoutingPrefix, "largest prefix"); + RequireEqual(28, SubnetUtilities.SmallestSubnet(subnets).RoutingPrefix, "smallest prefix"); + } +); + +// --------------------------------------------------------------------------- +// IPAddressMath +// --------------------------------------------------------------------------- +Console.WriteLine("\nIPAddressMath"); + +Check( + "Increment (default delta = 1)", + () => + { + var addr = IPAddress.Parse("192.168.0.1"); + RequireEqual(IPAddress.Parse("192.168.0.2"), addr.Increment(), "incremented by 1"); + } +); + +Check( + "Increment with explicit delta", + () => + { + var addr = IPAddress.Parse("10.0.0.0"); + RequireEqual(IPAddress.Parse("10.0.1.0"), addr.Increment(256), "incremented by 256"); + } +); + +Check( + "Increment negative delta (acts as decrement)", + () => + { + var addr = IPAddress.Parse("10.0.1.0"); + RequireEqual(IPAddress.Parse("10.0.0.255"), addr.Increment(-1), "decremented by 1"); + } +); + +Check( + "TryIncrement within bounds", + () => + { + Require( + IPAddressMath.TryIncrement(IPAddress.Parse("10.0.0.1"), out var result), + "TryIncrement succeeded" + ); + RequireEqual(IPAddress.Parse("10.0.0.2"), result!, "result"); + } +); + +Check( + "IsGreaterThan / IsLessThan", + () => + { + var a = IPAddress.Parse("192.168.0.2"); + var b = IPAddress.Parse("192.168.0.1"); + Require(a.IsGreaterThan(b), "a > b"); + Require(b.IsLessThan(a), "b < a"); + Require(!a.IsLessThan(b), "a not < b"); + } +); + +Check( + "IsGreaterThanOrEqualTo / IsLessThanOrEqualTo", + () => + { + var a = IPAddress.Parse("10.0.0.5"); + Require(a.IsGreaterThanOrEqualTo(a), "a >= a"); + Require(a.IsLessThanOrEqualTo(a), "a <= a"); + } +); + +Check( + "IsEqualTo", + () => + { + var a = IPAddress.Parse("192.168.1.1"); + var b = IPAddress.Parse("192.168.1.1"); + Require(a.IsEqualTo(b), "equal addresses"); + } +); + +Check( + "IsBetween", + () => + { + var low = IPAddress.Parse("10.0.0.1"); + var mid = IPAddress.Parse("10.0.0.100"); + var high = IPAddress.Parse("10.0.0.200"); + Require(mid.IsBetween(low, high), "mid is between"); + Require(!low.IsBetween(mid, high), "low is not between mid and high"); + Require(low.IsBetween(low, high), "low is between (inclusive)"); + } +); + +Check( + "Max / Min", + () => + { + var a = IPAddress.Parse("192.168.1.5"); + var b = IPAddress.Parse("192.168.1.10"); + RequireEqual(b, IPAddressMath.Max(a, b), "max"); + RequireEqual(a, IPAddressMath.Min(a, b), "min"); + } +); + +Check( + "IsAtMin / IsAtMax", + () => + { + Require(IPAddressUtilities.IPv4MinAddress.IsAtMin(), "0.0.0.0 is at min"); + Require(IPAddressUtilities.IPv4MaxAddress.IsAtMax(), "255.255.255.255 is at max"); + Require(!IPAddress.Parse("10.0.0.1").IsAtMin(), "10.0.0.1 not at min"); + } +); + +// --------------------------------------------------------------------------- +// IPAddressUtilities +// --------------------------------------------------------------------------- +Console.WriteLine("\nIPAddressUtilities"); + +Check( + "IsIPv4 / IsIPv6", + () => + { + Require(IPAddress.Parse("192.168.0.1").IsIPv4(), "IsIPv4 returns true for v4"); + Require(IPAddress.Parse("::1").IsIPv6(), "IsIPv6 returns true for v6"); + Require(!IPAddress.Parse("::1").IsIPv4(), "IsIPv4 returns false for v6"); + Require(!IPAddress.Parse("10.0.0.1").IsIPv6(), "IsIPv6 returns false for v4"); + } +); + +Check( + "IsPrivate", + () => + { + Require(IPAddress.Parse("10.1.2.3").IsPrivate(), "10.x is private"); + Require(IPAddress.Parse("172.20.0.1").IsPrivate(), "172.20.x is private"); + Require(IPAddress.Parse("192.168.0.1").IsPrivate(), "192.168.x is private"); + Require(!IPAddress.Parse("8.8.8.8").IsPrivate(), "8.8.8.8 is not private"); + } +); + +Check( + "IPv4 min/max address constants", + () => + { + RequireEqual(IPAddress.Parse("0.0.0.0"), IPAddressUtilities.IPv4MinAddress, "IPv4Min"); + RequireEqual( + IPAddress.Parse("255.255.255.255"), + IPAddressUtilities.IPv4MaxAddress, + "IPv4Max" + ); + } +); + +Check( + "IsValidNetMask", + () => + { + Require(IPAddress.Parse("255.255.255.0").IsValidNetMask(), "255.255.255.0 valid"); + Require(IPAddress.Parse("255.0.0.0").IsValidNetMask(), "255.0.0.0 valid"); + Require(!IPAddress.Parse("255.255.0.128").IsValidNetMask(), "255.255.0.128 invalid"); + } +); + +Check( + "ValidAddressFamilies", + () => + { + Require( + IPAddressUtilities.ValidAddressFamilies.Contains(AddressFamily.InterNetwork), + "InterNetwork valid" + ); + Require( + IPAddressUtilities.ValidAddressFamilies.Contains(AddressFamily.InterNetworkV6), + "InterNetworkV6 valid" + ); + } +); + +Check( + "ParseFromHexString (IPv4)", + () => + { + var addr = IPAddressUtilities.ParseFromHexString("c0a80001", AddressFamily.InterNetwork); + RequireEqual(IPAddress.Parse("192.168.0.1"), addr, "parsed address"); + } +); + +// --------------------------------------------------------------------------- +// IPAddressConverters +// --------------------------------------------------------------------------- +Console.WriteLine("\nIPAddressConverters"); + +Check( + "NetmaskToCidrRoutePrefix", + () => + { + RequireEqual(24, IPAddress.Parse("255.255.255.0").NetmaskToCidrRoutePrefix(), "/24"); + RequireEqual(8, IPAddress.Parse("255.0.0.0").NetmaskToCidrRoutePrefix(), "/8"); + RequireEqual(32, IPAddress.Parse("255.255.255.255").NetmaskToCidrRoutePrefix(), "/32"); + } +); + +Check( + "ToHexString", + () => + { + var hex = IPAddress.Parse("192.168.0.1").ToHexString(); + Require(!string.IsNullOrEmpty(hex), "hex not empty"); + // Convert.ToHexString produces uppercase; compare case-insensitively + RequireEqual("c0a80001", hex.ToLowerInvariant(), "hex value (lowercase)"); + } +); + +Check( + "ToNumericString", + () => + { + var numeric = IPAddress.Parse("0.0.1.0").ToNumericString(); + RequireEqual("256", numeric, "numeric string"); + } +); + +// --------------------------------------------------------------------------- +// Comparers +// --------------------------------------------------------------------------- +Console.WriteLine("\nComparers"); + +Check( + "DefaultIPAddressComparer", + () => + { + var comparer = DefaultIPAddressComparer.Instance; + var a = IPAddress.Parse("10.0.0.1"); + var b = IPAddress.Parse("10.0.0.2"); + Require(comparer.Compare(a, b) < 0, "a < b"); + Require(comparer.Compare(b, a) > 0, "b > a"); + Require(comparer.Compare(a, a) == 0, "a == a"); + + var sorted = new List { b, a }; + sorted.Sort(comparer); + RequireEqual(a, sorted[0], "sorted[0]"); + } +); + +Check( + "DefaultIIPAddressRangeComparer", + () => + { + var comparer = DefaultIIPAddressRangeComparer.Instance; + var a = Subnet.Parse("10.0.0.0/24"); + var b = Subnet.Parse("10.0.1.0/24"); + Require(comparer.Compare(a, b) < 0, "a < b by head address"); + Require(comparer.Compare(b, a) > 0, "b > a"); + Require(comparer.Compare(a, a) == 0, "a == a"); + } +); + +Check( + "DefaultAddressFamilyComparer", + () => + { + var comparer = DefaultAddressFamilyComparer.Instance; + var result = comparer.Compare(AddressFamily.InterNetwork, AddressFamily.InterNetworkV6); + Require(result != 0, "IPv4 != IPv6"); + Require( + comparer.Compare(AddressFamily.InterNetwork, AddressFamily.InterNetwork) == 0, + "IPv4 == IPv4" + ); + } +); + +// --------------------------------------------------------------------------- +// Summary +// --------------------------------------------------------------------------- +Console.WriteLine(); +Console.WriteLine($"Results: {passed} passed, {failed} failed."); + +if (failed > 0) + Environment.Exit(1); diff --git a/smoketests/SmokeTests/SmokeTests.csproj b/smoketests/SmokeTests/SmokeTests.csproj new file mode 100644 index 0000000..1e6f873 --- /dev/null +++ b/smoketests/SmokeTests/SmokeTests.csproj @@ -0,0 +1,28 @@ + + + Exe + + net48;net8.0;net9.0;net10.0 + latest + disable + + 0.0.0-placeholder + + + + + + diff --git a/smoketests/nuget.config b/smoketests/nuget.config new file mode 100644 index 0000000..0c4ebce --- /dev/null +++ b/smoketests/nuget.config @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/smoketests/run-smoke-tests.ps1 b/smoketests/run-smoke-tests.ps1 new file mode 100644 index 0000000..354131e --- /dev/null +++ b/smoketests/run-smoke-tests.ps1 @@ -0,0 +1,117 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + Run Arcus NuGet package smoke tests on Windows (all frameworks including net48). + +.DESCRIPTION + 1. Packs the Arcus library into a local NuGet feed (smoketests\feed\). + 2. Restores and builds the SmokeTests consumer project against that feed. + 3. Runs the consumer for each supported framework and reports results. + + net48 validates the netstandard2.0 asset on .NET Framework — the TFM that cannot + be covered by a Linux runner. The script also covers net8.0, net9.0, and net10.0. + +.PARAMETER Version + SemVer string for the packed library. Defaults to "99.0.0-smoke". + +.EXAMPLE + .\smoketests\run-smoke-tests.ps1 + + .\smoketests\run-smoke-tests.ps1 -Version 4.1.0-preview +.NOTES + Exit code 0 = all frameworks passed; 1 = one or more frameworks failed. +#> + +param( + [string]$Version = "99.0.0-smoke" +) + +$ErrorActionPreference = "Stop" + +$ScriptDir = $PSScriptRoot +$RepoRoot = (Get-Item $ScriptDir).Parent.FullName +$FeedDir = Join-Path $ScriptDir "feed" +$ConsumerProj = Join-Path $ScriptDir "SmokeTests\SmokeTests.csproj" +$LibProj = Join-Path $RepoRoot "src\Arcus\Arcus.csproj" +$Frameworks = @("net48", "net8.0", "net9.0", "net10.0") + +$OverallExit = 0 + +function Write-Header([string]$Text) { + Write-Host "`n$Text" -ForegroundColor Cyan +} +function Write-Ok([string]$Text) { Write-Host " OK $Text" -ForegroundColor Green } +function Write-Err([string]$Text) { Write-Host " ERR $Text" -ForegroundColor Red } + +# ── 1. Pack ─────────────────────────────────────────────────────────────────── + +Write-Header "Step 1 — Pack Arcus $Version" + +if (Test-Path $FeedDir) { Remove-Item $FeedDir -Recurse -Force } +New-Item -ItemType Directory -Path $FeedDir | Out-Null + +dotnet pack $LibProj ` + --configuration Release ` + --output $FeedDir ` + "-p:VersionFromCI=$Version" ` + --verbosity quiet + +if ($LASTEXITCODE -ne 0) { Write-Err "Pack failed"; exit 1 } +Write-Ok "Packed to $FeedDir" + +# ── 2. Restore ──────────────────────────────────────────────────────────────── + +Write-Header "Step 2 — Restore SmokeTests consumer" + +dotnet restore $ConsumerProj ` + "-p:ArcusVersion=$Version" ` + --verbosity quiet + +if ($LASTEXITCODE -ne 0) { Write-Err "Restore failed"; exit 1 } +Write-Ok "Restored" + +# ── 3. Build ────────────────────────────────────────────────────────────────── + +Write-Header "Step 3 — Build SmokeTests consumer (all frameworks)" + +dotnet build $ConsumerProj ` + --no-restore ` + --configuration Release ` + "-p:ArcusVersion=$Version" ` + --verbosity quiet + +if ($LASTEXITCODE -ne 0) { Write-Err "Build failed"; exit 1 } +Write-Ok "Built" + +# ── 4. Run per framework ───────────────────────────────────────────────────── + +Write-Header "Step 4 — Run smoke tests" + +foreach ($fw in $Frameworks) { + Write-Host "`n Framework: $fw" + + dotnet run ` + --project $ConsumerProj ` + --framework $fw ` + --configuration Release ` + --no-build ` + "-p:ArcusVersion=$Version" + + if ($LASTEXITCODE -eq 0) { + Write-Ok "$fw passed" + } else { + Write-Err "$fw FAILED" + $OverallExit = 1 + } +} + +# ── Summary ─────────────────────────────────────────────────────────────────── + +Write-Host "" +if ($OverallExit -eq 0) { + Write-Host "All smoke tests passed." -ForegroundColor Green +} else { + Write-Host "One or more frameworks FAILED." -ForegroundColor Red +} + +exit $OverallExit diff --git a/smoketests/run-smoke-tests.sh b/smoketests/run-smoke-tests.sh new file mode 100644 index 0000000..8e69961 --- /dev/null +++ b/smoketests/run-smoke-tests.sh @@ -0,0 +1,104 @@ +#!/usr/bin/env bash +# Run Arcus NuGet package smoke tests on Linux/macOS. +# +# What this script does: +# 1. Packs the Arcus library into a local NuGet feed (smoketests/feed/). +# 2. Restores and builds the SmokeTests consumer project against that feed. +# 3. Runs the consumer for each supported framework and reports results. +# +# net48 is excluded because .NET Framework does not run natively on Linux/macOS. +# Use run-smoke-tests.ps1 on Windows to cover net48 (netstandard2.0 asset). +# +# Usage: +# ./smoketests/run-smoke-tests.sh [VERSION] +# +# VERSION SemVer string for the packed library. Defaults to 99.0.0-smoke. +# +# Exit codes: +# 0 all frameworks passed +# 1 one or more frameworks failed + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +FEED_DIR="${SCRIPT_DIR}/feed" +CONSUMER_PROJ="${SCRIPT_DIR}/SmokeTests/SmokeTests.csproj" +LIB_PROJ="${REPO_ROOT}/src/Arcus/Arcus.csproj" +FRAMEWORKS=("net8.0" "net9.0" "net10.0") +VERSION="${1:-99.0.0-smoke}" + +# ── helpers ────────────────────────────────────────────────────────────────── + +header() { printf '\n\033[1;36m%s\033[0m\n' "$*"; } +ok() { printf '\033[0;32m ✓ %s\033[0m\n' "$*"; } +err() { printf '\033[0;31m ✗ %s\033[0m\n' "$*" >&2; } + +overall_exit=0 + +# ── 1. Pack ─────────────────────────────────────────────────────────────────── + +header "Step 1 — Pack Arcus ${VERSION}" + +rm -rf "${FEED_DIR}" +mkdir -p "${FEED_DIR}" + +dotnet pack "${LIB_PROJ}" \ + --configuration Release \ + --output "${FEED_DIR}" \ + -p:VersionFromCI="${VERSION}" \ + --verbosity quiet + +ok "Packed to ${FEED_DIR}" + +# ── 2. Restore ──────────────────────────────────────────────────────────────── + +header "Step 2 — Restore SmokeTests consumer" + +dotnet restore "${CONSUMER_PROJ}" \ + -p:ArcusVersion="${VERSION}" \ + --verbosity quiet + +ok "Restored" + +# ── 3. Build ────────────────────────────────────────────────────────────────── + +header "Step 3 — Build SmokeTests consumer (all frameworks)" + +dotnet build "${CONSUMER_PROJ}" \ + --no-restore \ + --configuration Release \ + -p:ArcusVersion="${VERSION}" \ + --verbosity quiet + +ok "Built" + +# ── 4. Run per framework ───────────────────────────────────────────────────── + +header "Step 4 — Run smoke tests" + +for fw in "${FRAMEWORKS[@]}"; do + printf '\n Framework: %s\n' "${fw}" + if dotnet run \ + --project "${CONSUMER_PROJ}" \ + --framework "${fw}" \ + --configuration Release \ + --no-build \ + -p:ArcusVersion="${VERSION}"; then + ok "${fw} passed" + else + err "${fw} FAILED" + overall_exit=1 + fi +done + +# ── Summary ─────────────────────────────────────────────────────────────────── + +echo "" +if [ "${overall_exit}" -eq 0 ]; then + printf '\033[1;32mAll smoke tests passed.\033[0m\n' +else + printf '\033[1;31mOne or more frameworks FAILED.\033[0m\n' >&2 +fi + +exit "${overall_exit}" diff --git a/src/.editorconfig b/src/.editorconfig deleted file mode 100644 index 8ce646e..0000000 --- a/src/.editorconfig +++ /dev/null @@ -1,600 +0,0 @@ -# .NET project .editorconfig as of 2025-02-03 - -root = false - -# Visual Studio Solutions -[*.sln] -indent_style = tab - -# Visual Studio Project Files -[*.csproj] -indent_size = 2 - -# C# files -[*.cs] - -#### Core EditorConfig Options #### - -# Indentation and spacing -indent_size = 4 -tab_width = 4 - -# New line preferences -end_of_line = crlf -insert_final_newline = true - -#### .NET Coding Conventions #### - -# Organize usings -dotnet_separate_import_directive_groups = false -dotnet_sort_system_directives_first = true -file_header_template = unset - -# this. and Me. preferences -dotnet_style_qualification_for_event = false:silent -dotnet_style_qualification_for_field = false:silent -dotnet_style_qualification_for_method = false:silent -dotnet_style_qualification_for_property = false:silent - -# Language keywords vs BCL types preferences -dotnet_style_predefined_type_for_locals_parameters_members = true:silent -dotnet_style_predefined_type_for_member_access = true:silent - -# Parentheses preferences -dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent -dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent -dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent -dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent - -# Modifier preferences -dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent - -# Expression-level preferences -dotnet_style_coalesce_expression = true:suggestion -dotnet_style_collection_initializer = true:suggestion -dotnet_style_explicit_tuple_names = true:suggestion -dotnet_style_null_propagation = true:suggestion -dotnet_style_object_initializer = true:suggestion -dotnet_style_operator_placement_when_wrapping = beginning_of_line -dotnet_style_prefer_auto_properties = true:suggestion -dotnet_style_prefer_compound_assignment = true:suggestion -dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion -dotnet_style_prefer_conditional_expression_over_return = true:suggestion -dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion -dotnet_style_prefer_inferred_tuple_names = true:suggestion -dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion -dotnet_style_prefer_simplified_boolean_expressions = true:suggestion -dotnet_style_prefer_simplified_interpolation = true:suggestion - -# Field preferences -dotnet_style_readonly_field = true:warning - -# Parameter preferences -dotnet_code_quality_unused_parameters = all:suggestion - -# Suppression preferences -dotnet_remove_unnecessary_suppression_exclusions = None - -#### C# Coding Conventions #### -# var preferences -csharp_style_var_elsewhere = true:suggestion -csharp_style_var_for_built_in_types = true:warning -csharp_style_var_when_type_is_apparent = true:warning - - -# Expression-bodied members -csharp_style_expression_bodied_accessors = true:silent -csharp_style_expression_bodied_constructors = false:silent -csharp_style_expression_bodied_indexers = true:silent -csharp_style_expression_bodied_lambdas = true:suggestion -csharp_style_expression_bodied_local_functions = false:silent -csharp_style_expression_bodied_methods = false:silent -csharp_style_expression_bodied_operators = false:silent -csharp_style_expression_bodied_properties = true:silent - -# Pattern matching preferences -csharp_style_pattern_matching_over_as_with_null_check = true:suggestion -csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion -csharp_style_prefer_not_pattern = true:suggestion -csharp_style_prefer_pattern_matching = true:suggestion -csharp_style_prefer_switch_expression = true:suggestion - -# Null-checking preferences -csharp_style_conditional_delegate_call = true:suggestion - -# Modifier preferences -csharp_prefer_static_local_function = true:warning -csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:silent - -# Code-block preferences -csharp_prefer_braces = true:warning -csharp_prefer_simple_using_statement = true:suggestion - -# Expression-level preferences -csharp_prefer_simple_default_expression = true:suggestion -csharp_style_deconstructed_variable_declaration = true:suggestion -csharp_style_inlined_variable_declaration = true:suggestion -csharp_style_pattern_local_over_anonymous_function = true:suggestion -csharp_style_prefer_index_operator = true:suggestion -csharp_style_prefer_range_operator = true:suggestion -csharp_style_throw_expression = true:suggestion -csharp_style_unused_value_assignment_preference = discard_variable:suggestion -csharp_style_unused_value_expression_statement_preference = discard_variable:silent - -# 'using' directive preferences -csharp_using_directive_placement = outside_namespace:silent - -#### C# Formatting Rules #### - -# New line preferences -csharp_new_line_before_catch = true -csharp_new_line_before_else = true -csharp_new_line_before_finally = true -csharp_new_line_before_members_in_anonymous_types = true -csharp_new_line_before_members_in_object_initializers = true -csharp_new_line_before_open_brace = all -csharp_new_line_between_query_expression_clauses = true - -# Indentation preferences -csharp_indent_block_contents = true -csharp_indent_braces = false -csharp_indent_case_contents = true -csharp_indent_case_contents_when_block = true -csharp_indent_labels = no_change -csharp_indent_switch_labels = true - -# Space preferences -csharp_space_after_cast = false -csharp_space_after_colon_in_inheritance_clause = true -csharp_space_after_comma = true -csharp_space_after_dot = false -csharp_space_after_keywords_in_control_flow_statements = true -csharp_space_after_semicolon_in_for_statement = true -csharp_space_around_binary_operators = before_and_after -csharp_space_around_declaration_statements = false -csharp_space_before_colon_in_inheritance_clause = true -csharp_space_before_comma = false -csharp_space_before_dot = false -csharp_space_before_open_square_brackets = false -csharp_space_before_semicolon_in_for_statement = false -csharp_space_between_empty_square_brackets = false -csharp_space_between_method_call_empty_parameter_list_parentheses = false -csharp_space_between_method_call_name_and_opening_parenthesis = false -csharp_space_between_method_call_parameter_list_parentheses = false -csharp_space_between_method_declaration_empty_parameter_list_parentheses = false -csharp_space_between_method_declaration_name_and_open_parenthesis = false -csharp_space_between_method_declaration_parameter_list_parentheses = false -csharp_space_between_parentheses = false -csharp_space_between_square_brackets = false - -# Wrapping preferences -csharp_preserve_single_line_blocks = true -csharp_preserve_single_line_statements = true -csharp_style_namespace_declarations = block_scoped:silent -csharp_style_prefer_method_group_conversion = true:silent -csharp_style_prefer_top_level_statements = true:silent -csharp_style_prefer_null_check_over_type_check = true:suggestion -csharp_style_prefer_local_over_anonymous_function = true:suggestion -csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion -csharp_style_prefer_tuple_swap = true:suggestion -csharp_style_prefer_utf8_string_literals = true:suggestion -csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent -csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:silent -csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent -csharp_style_prefer_extended_property_pattern = true:suggestion - -#### Naming styles #### -# Naming rules -dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.severity = suggestion -dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.symbols = types_and_namespaces -dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.style = pascalcase - -dotnet_naming_rule.interfaces_should_be_ipascalcase.severity = suggestion -dotnet_naming_rule.interfaces_should_be_ipascalcase.symbols = interfaces -dotnet_naming_rule.interfaces_should_be_ipascalcase.style = ipascalcase - -dotnet_naming_rule.type_parameters_should_be_tpascalcase.severity = suggestion -dotnet_naming_rule.type_parameters_should_be_tpascalcase.symbols = type_parameters -dotnet_naming_rule.type_parameters_should_be_tpascalcase.style = tpascalcase - -dotnet_naming_rule.methods_should_be_pascalcase.severity = suggestion -dotnet_naming_rule.methods_should_be_pascalcase.symbols = methods -dotnet_naming_rule.methods_should_be_pascalcase.style = pascalcase - -dotnet_naming_rule.properties_should_be_pascalcase.severity = suggestion -dotnet_naming_rule.properties_should_be_pascalcase.symbols = properties -dotnet_naming_rule.properties_should_be_pascalcase.style = pascalcase - -dotnet_naming_rule.events_should_be_pascalcase.severity = suggestion -dotnet_naming_rule.events_should_be_pascalcase.symbols = events -dotnet_naming_rule.events_should_be_pascalcase.style = pascalcase - -dotnet_naming_rule.local_variables_should_be_camelcase.severity = suggestion -dotnet_naming_rule.local_variables_should_be_camelcase.symbols = local_variables -dotnet_naming_rule.local_variables_should_be_camelcase.style = camelcase - -dotnet_naming_rule.local_constants_should_be_camelcase.severity = suggestion -dotnet_naming_rule.local_constants_should_be_camelcase.symbols = local_constants -dotnet_naming_rule.local_constants_should_be_camelcase.style = camelcase - -dotnet_naming_rule.parameters_should_be_camelcase.severity = suggestion -dotnet_naming_rule.parameters_should_be_camelcase.symbols = parameters -dotnet_naming_rule.parameters_should_be_camelcase.style = camelcase - -dotnet_naming_rule.public_fields_should_be_pascalcase.severity = suggestion -dotnet_naming_rule.public_fields_should_be_pascalcase.symbols = public_fields -dotnet_naming_rule.public_fields_should_be_pascalcase.style = pascalcase - -dotnet_naming_rule.private_fields_should_be__camelcase.severity = suggestion -dotnet_naming_rule.private_fields_should_be__camelcase.symbols = private_fields -dotnet_naming_rule.private_fields_should_be__camelcase.style = _camelcase - -dotnet_naming_rule.private_static_fields_should_be_s_camelcase.severity = suggestion -dotnet_naming_rule.private_static_fields_should_be_s_camelcase.symbols = private_static_fields -dotnet_naming_rule.private_static_fields_should_be_s_camelcase.style = _camelcase - -dotnet_naming_rule.public_constant_fields_should_be_pascalcase.severity = suggestion -dotnet_naming_rule.public_constant_fields_should_be_pascalcase.symbols = public_constant_fields -dotnet_naming_rule.public_constant_fields_should_be_pascalcase.style = pascalcase - -dotnet_naming_rule.private_constant_fields_should_be_pascalcase.severity = suggestion -dotnet_naming_rule.private_constant_fields_should_be_pascalcase.symbols = private_constant_fields -dotnet_naming_rule.private_constant_fields_should_be_pascalcase.style = pascalcase - -dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.severity = suggestion -dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.symbols = public_static_readonly_fields -dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.style = pascalcase - -dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.severity = suggestion -dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.symbols = private_static_readonly_fields -dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.style = pascalcase - -dotnet_naming_rule.enums_should_be_pascalcase.severity = suggestion -dotnet_naming_rule.enums_should_be_pascalcase.symbols = enums -dotnet_naming_rule.enums_should_be_pascalcase.style = pascalcase - -dotnet_naming_rule.local_functions_should_be_pascalcase.severity = suggestion -dotnet_naming_rule.local_functions_should_be_pascalcase.symbols = local_functions -dotnet_naming_rule.local_functions_should_be_pascalcase.style = pascalcase - -dotnet_naming_rule.non_field_members_should_be_pascalcase.severity = suggestion -dotnet_naming_rule.non_field_members_should_be_pascalcase.symbols = non_field_members -dotnet_naming_rule.non_field_members_should_be_pascalcase.style = pascalcase - -# Symbol specifications - -dotnet_naming_symbols.interfaces.applicable_kinds = interface -dotnet_naming_symbols.interfaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.interfaces.required_modifiers = - -dotnet_naming_symbols.enums.applicable_kinds = enum -dotnet_naming_symbols.enums.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.enums.required_modifiers = - -dotnet_naming_symbols.events.applicable_kinds = event -dotnet_naming_symbols.events.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.events.required_modifiers = - -dotnet_naming_symbols.methods.applicable_kinds = method -dotnet_naming_symbols.methods.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.methods.required_modifiers = - -dotnet_naming_symbols.properties.applicable_kinds = property -dotnet_naming_symbols.properties.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.properties.required_modifiers = - -dotnet_naming_symbols.public_fields.applicable_kinds = field -dotnet_naming_symbols.public_fields.applicable_accessibilities = public, internal -dotnet_naming_symbols.public_fields.required_modifiers = - -dotnet_naming_symbols.private_fields.applicable_kinds = field -dotnet_naming_symbols.private_fields.applicable_accessibilities = private, protected, protected_internal, private_protected -dotnet_naming_symbols.private_fields.required_modifiers = - -dotnet_naming_symbols.private_static_fields.applicable_kinds = field -dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private, protected, protected_internal, private_protected -dotnet_naming_symbols.private_static_fields.required_modifiers = static - -dotnet_naming_symbols.types_and_namespaces.applicable_kinds = namespace, class, struct, interface, enum -dotnet_naming_symbols.types_and_namespaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.types_and_namespaces.required_modifiers = - -dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method -dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.non_field_members.required_modifiers = - -dotnet_naming_symbols.type_parameters.applicable_kinds = namespace -dotnet_naming_symbols.type_parameters.applicable_accessibilities = * -dotnet_naming_symbols.type_parameters.required_modifiers = - -dotnet_naming_symbols.private_constant_fields.applicable_kinds = field -dotnet_naming_symbols.private_constant_fields.applicable_accessibilities = private, protected, protected_internal, private_protected -dotnet_naming_symbols.private_constant_fields.required_modifiers = const - -dotnet_naming_symbols.local_variables.applicable_kinds = local -dotnet_naming_symbols.local_variables.applicable_accessibilities = local -dotnet_naming_symbols.local_variables.required_modifiers = - -dotnet_naming_symbols.local_constants.applicable_kinds = local -dotnet_naming_symbols.local_constants.applicable_accessibilities = local -dotnet_naming_symbols.local_constants.required_modifiers = const - -dotnet_naming_symbols.parameters.applicable_kinds = parameter -dotnet_naming_symbols.parameters.applicable_accessibilities = * -dotnet_naming_symbols.parameters.required_modifiers = - -dotnet_naming_symbols.public_constant_fields.applicable_kinds = field -dotnet_naming_symbols.public_constant_fields.applicable_accessibilities = public, internal -dotnet_naming_symbols.public_constant_fields.required_modifiers = const - -dotnet_naming_symbols.public_static_readonly_fields.applicable_kinds = field -dotnet_naming_symbols.public_static_readonly_fields.applicable_accessibilities = public, internal -dotnet_naming_symbols.public_static_readonly_fields.required_modifiers = readonly, static - -dotnet_naming_symbols.private_static_readonly_fields.applicable_kinds = field -dotnet_naming_symbols.private_static_readonly_fields.applicable_accessibilities = private, protected, protected_internal, private_protected -dotnet_naming_symbols.private_static_readonly_fields.required_modifiers = readonly, static - -dotnet_naming_symbols.local_functions.applicable_kinds = local_function -dotnet_naming_symbols.local_functions.applicable_accessibilities = * -dotnet_naming_symbols.local_functions.required_modifiers = - -# Naming styles - -dotnet_naming_style.pascalcase.required_prefix = -dotnet_naming_style.pascalcase.required_suffix = -dotnet_naming_style.pascalcase.word_separator = -dotnet_naming_style.pascalcase.capitalization = pascal_case - -dotnet_naming_style.ipascalcase.required_prefix = I -dotnet_naming_style.ipascalcase.required_suffix = -dotnet_naming_style.ipascalcase.word_separator = -dotnet_naming_style.ipascalcase.capitalization = pascal_case - -dotnet_naming_style.tpascalcase.required_prefix = T -dotnet_naming_style.tpascalcase.required_suffix = -dotnet_naming_style.tpascalcase.word_separator = -dotnet_naming_style.tpascalcase.capitalization = pascal_case - -dotnet_naming_style._camelcase.required_prefix = _ -dotnet_naming_style._camelcase.required_suffix = -dotnet_naming_style._camelcase.word_separator = -dotnet_naming_style._camelcase.capitalization = camel_case - -dotnet_naming_style.camelcase.required_prefix = -dotnet_naming_style.camelcase.required_suffix = -dotnet_naming_style.camelcase.word_separator = -dotnet_naming_style.camelcase.capitalization = camel_case - -dotnet_naming_style.s_camelcase.required_prefix = s_ -dotnet_naming_style.s_camelcase.required_suffix = -dotnet_naming_style.s_camelcase.word_separator = -dotnet_naming_style.s_camelcase.capitalization = camel_case -dotnet_style_namespace_match_folder = true:suggestion -dotnet_style_allow_multiple_blank_lines_experimental = true:suggestion -dotnet_style_allow_statement_immediately_after_block_experimental = true:silent - -# AnalyzerId= AsyncUsageAnalyzers RuleNamespace= AsyncUsageAnalyzers -dotnet_diagnostic.UseConfigureAwait.severity = Warning - -# AnalyzerId= DocumentationAnalyzers RuleNamespace= DocumentationAnalyzers -dotnet_diagnostic.DOC100.severity = Warning -dotnet_diagnostic.DOC101.severity = Warning -dotnet_diagnostic.DOC102.severity = Warning -dotnet_diagnostic.DOC103.severity = Warning -dotnet_diagnostic.DOC104.severity = Warning -dotnet_diagnostic.DOC105.severity = Warning -dotnet_diagnostic.DOC106.severity = Warning -dotnet_diagnostic.DOC107.severity = Warning -dotnet_diagnostic.DOC108.severity = Warning -dotnet_diagnostic.DOC200.severity = Warning -dotnet_diagnostic.DOC201.severity = Warning -dotnet_diagnostic.DOC202.severity = Warning -dotnet_diagnostic.DOC203.severity = Warning -dotnet_diagnostic.DOC204.severity = Warning - -# AnalyzerId= Microsoft.Analyzers.ManagedCodeAnalysis RuleNamespace= Microsoft.Rules.Managed -dotnet_diagnostic.CA1001.severity = Warning -dotnet_diagnostic.CA1009.severity = Warning -dotnet_diagnostic.CA1016.severity = Warning -dotnet_diagnostic.CA1033.severity = Warning -dotnet_diagnostic.CA1049.severity = Warning -dotnet_diagnostic.CA1056.severity = Warning -dotnet_diagnostic.CA1060.severity = Warning -dotnet_diagnostic.CA1061.severity = Warning -dotnet_diagnostic.CA1063.severity = Warning -dotnet_diagnostic.CA1065.severity = Warning -dotnet_diagnostic.CA1301.severity = Warning -dotnet_diagnostic.CA1400.severity = Warning -dotnet_diagnostic.CA1401.severity = Warning -dotnet_diagnostic.CA1403.severity = Warning -dotnet_diagnostic.CA1404.severity = Warning -dotnet_diagnostic.CA1405.severity = Warning -dotnet_diagnostic.CA1410.severity = Warning -dotnet_diagnostic.CA1415.severity = Warning -dotnet_diagnostic.CA1821.severity = Warning -dotnet_diagnostic.CA1825.severity = Warning -dotnet_diagnostic.CA1900.severity = Warning -dotnet_diagnostic.CA1901.severity = Warning -dotnet_diagnostic.CA2000.severity = Warning -dotnet_diagnostic.CA2002.severity = Warning -dotnet_diagnostic.CA2100.severity = Warning -dotnet_diagnostic.CA2101.severity = Warning -dotnet_diagnostic.CA2108.severity = Warning -dotnet_diagnostic.CA2111.severity = Warning -dotnet_diagnostic.CA2112.severity = Warning -dotnet_diagnostic.CA2114.severity = Warning -dotnet_diagnostic.CA2116.severity = Warning -dotnet_diagnostic.CA2117.severity = Warning -dotnet_diagnostic.CA2122.severity = Warning -dotnet_diagnostic.CA2123.severity = Warning -dotnet_diagnostic.CA2124.severity = Warning -dotnet_diagnostic.CA2126.severity = Warning -dotnet_diagnostic.CA2131.severity = Warning -dotnet_diagnostic.CA2132.severity = Warning -dotnet_diagnostic.CA2133.severity = Warning -dotnet_diagnostic.CA2134.severity = Warning -dotnet_diagnostic.CA2137.severity = Warning -dotnet_diagnostic.CA2138.severity = Warning -dotnet_diagnostic.CA2140.severity = Warning -dotnet_diagnostic.CA2141.severity = Warning -dotnet_diagnostic.CA2146.severity = Warning -dotnet_diagnostic.CA2147.severity = Warning -dotnet_diagnostic.CA2149.severity = Warning -dotnet_diagnostic.CA2200.severity = Warning -dotnet_diagnostic.CA2202.severity = Warning -dotnet_diagnostic.CA2207.severity = Warning -dotnet_diagnostic.CA2212.severity = Warning -dotnet_diagnostic.CA2213.severity = Warning -dotnet_diagnostic.CA2214.severity = Warning -dotnet_diagnostic.CA2215.severity = Warning -dotnet_diagnostic.CA2216.severity = Warning -dotnet_diagnostic.CA2220.severity = Warning -dotnet_diagnostic.CA2229.severity = Warning -dotnet_diagnostic.CA2231.severity = Warning -dotnet_diagnostic.CA2232.severity = Warning -dotnet_diagnostic.CA2235.severity = Warning -dotnet_diagnostic.CA2236.severity = Warning -dotnet_diagnostic.CA2237.severity = Warning -dotnet_diagnostic.CA2238.severity = Warning -dotnet_diagnostic.CA2240.severity = Warning -dotnet_diagnostic.CA2241.severity = Warning -dotnet_diagnostic.CA2242.severity = Warning -dotnet_diagnostic.CA2253.severity = Warning -dotnet_diagnostic.CA2254.severity = Warning - -# AnalyzerId= Microsoft.CodeAnalysis.CSharp RuleNamespace= Microsoft.CodeAnalysis.CSharp -dotnet_diagnostic.CS8019.severity = Warning - -# AnalyzerId= Microsoft.CodeAnalysis.CSharp.Analyzers RuleNamespace= Microsoft.CodeAnalysis.CSharp.Analyzers -dotnet_diagnostic.RS1022.severity = Warning - -# AnalyzerId= Microsoft.CodeAnalysis.CSharp.CodeStyle RuleNamespace= Microsoft.CodeAnalysis.CSharp.CodeStyle -dotnet_diagnostic.IDE0055.severity = None -dotnet_diagnostic.IDE0079.severity = Warning -dotnet_diagnostic.IDE1006.severity = Warning - -# AnalyzerId= Microsoft.CodeAnalysis.CSharp.Features RuleNamespace= Microsoft.CodeAnalysis.CSharp.Features -dotnet_diagnostic.IDE0003.severity = None -dotnet_diagnostic.IDE0007.severity = Warning -dotnet_diagnostic.IDE0009.severity = None - -# AnalyzerId= StyleCop.Analyzers RuleNamespace= StyleCop.Analyzers -dotnet_diagnostic.SA1000.severity = None -dotnet_diagnostic.SA1003.severity = None -dotnet_diagnostic.SA1005.severity = None -dotnet_diagnostic.SA1008.severity = None -dotnet_diagnostic.SA1009.severity = None -dotnet_diagnostic.SA1012.severity = None -dotnet_diagnostic.SA1013.severity = None -dotnet_diagnostic.SA1025.severity = None -dotnet_diagnostic.SA1101.severity = None -dotnet_diagnostic.SA1108.severity = None -dotnet_diagnostic.SA1111.severity = None -dotnet_diagnostic.SA1116.severity = None -dotnet_diagnostic.SA1118.severity = None -dotnet_diagnostic.SA1123.severity = None -dotnet_diagnostic.SA1124.severity = None -dotnet_diagnostic.SA1127.severity = None -dotnet_diagnostic.SA1128.severity = None -dotnet_diagnostic.SA1137.severity = None -dotnet_diagnostic.SA1201.severity = None -dotnet_diagnostic.SA1202.severity = None -dotnet_diagnostic.SA1203.severity = None -dotnet_diagnostic.SA1204.severity = None -dotnet_diagnostic.SA1309.severity = None -dotnet_diagnostic.SA1412.severity = Error -dotnet_diagnostic.SA1413.severity = None -dotnet_diagnostic.SA1500.severity = None -dotnet_diagnostic.SA1501.severity = None -dotnet_diagnostic.SA1502.severity = None -dotnet_diagnostic.SA1504.severity = None -dotnet_diagnostic.SA1512.severity = None -dotnet_diagnostic.SA1513.severity = None -dotnet_diagnostic.SA1515.severity = None -dotnet_diagnostic.SA1516.severity = None - -# Documentation Rules (SA1600-) https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/DocumentationRules.md -dotnet_diagnostic.SA1600.severity = Warning # A C# code element is missing a documentation header. -dotnet_diagnostic.SA1601.severity = Warning # A C# partial element is missing a documentation header. -dotnet_diagnostic.SA1602.severity = Warning # An item within a C# enumeration is missing an Xml documentation header. -dotnet_diagnostic.SA1603.severity = Warning # The Xml within a C# element's document header is badly formed. -dotnet_diagnostic.SA1604.severity = Warning # The Xml header documentation for a C# element is missing a tag. -dotnet_diagnostic.SA1605.severity = Warning # The or tag within the documentation header for a C# code element is missing or empty. -dotnet_diagnostic.SA1606.severity = Warning # The tag within the documentation header for a C# code element is empty. -dotnet_diagnostic.SA1607.severity = Warning # The or tag within the documentation header for a C# code element is empty. -dotnet_diagnostic.SA1608.severity = Warning # The tag within an element's Xml header documentation contains the default text generated by Visual Studio during the creation of the element. -dotnet_diagnostic.SA1609.severity = Warning # The Xml header documentation for a C# property does not contain a tag. -dotnet_diagnostic.SA1610.severity = Warning # The Xml header documentation for a C# property contains an empty tag. -dotnet_diagnostic.SA1611.severity = Warning # A C# method, constructor, delegate or indexer element is missing documentation for one or more of its parameters. -dotnet_diagnostic.SA1612.severity = Warning # The documentation describing the parameters to a C# method, constructor, delegate or indexer element does not match the actual parameters on the element. -dotnet_diagnostic.SA1613.severity = Warning # A tag within a C# element's documentation header is missing a name attribute containing the name of the parameter. -dotnet_diagnostic.SA1614.severity = Warning # A tag within a C# element's documentation header is empty. -dotnet_diagnostic.SA1615.severity = Warning # A C# element is missing documentation for its return value. -dotnet_diagnostic.SA1616.severity = Warning # The tag within a C# element's documentation header is empty. -dotnet_diagnostic.SA1617.severity = Warning # A C# code element does not contain a return value, or returns void, but the documentation header for the element contains a tag. -dotnet_diagnostic.SA1618.severity = Warning # A generic C# element is missing documentation for one or more of its generic type parameters. -dotnet_diagnostic.SA1619.severity = Warning # A generic, partial C# element is missing documentation for one or more of its generic type parameters, and the documentation for the element contains a tag. -dotnet_diagnostic.SA1620.severity = Warning # The tags within the Xml header documentation for a generic C# element do not match the generic type parameters on the element. -dotnet_diagnostic.SA1621.severity = Warning # A tag within the Xml header documentation for a generic C# element is missing a name attribute, or contains an empty name attribute. -dotnet_diagnostic.SA1622.severity = Warning # A tag within the Xml header documentation for a generic C# element is empty. -dotnet_diagnostic.SA1623.severity = Warning # The documentation text within a C# property's tag does not match the accessors within the property. -dotnet_diagnostic.SA1624.severity = Warning # The documentation text within a C# property's tag takes into account all of the accessors within the property, but one of the accessors has limited access. -dotnet_diagnostic.SA1625.severity = Warning # The Xml documentation for a C# element contains two or more identical entries, indicating that the documentation has been copied and pasted. -dotnet_diagnostic.SA1626.severity = Warning # The C# code contains a single-line comment which begins with three forward slashes in a row. -dotnet_diagnostic.SA1627.severity = Warning # The Xml header documentation for a C# code element contains an empty tag. -dotnet_diagnostic.SA1628.severity = Warning # A section of the Xml header documentation for a C# element does not begin with a capital letter. -dotnet_diagnostic.SA1629.severity = None # A section of the Xml header documentation for a C# element does not end with a period (also known as a full stop). -dotnet_diagnostic.SA1630.severity = Warning # A section of the Xml header documentation for a C# element does not contain any whitespace between words. -dotnet_diagnostic.SA1631.severity = Warning # A section of the Xml header documentation for a C# element does not contain enough alphabetic characters. -dotnet_diagnostic.SA1632.severity = None # From StyleCop 4.5 this rule is disabled by default. -dotnet_diagnostic.SA1633.severity = None # A C# code file is missing a standard file header. -dotnet_diagnostic.SA1634.severity = Warning # The file header at the top of a C# code file is missing a copyright tag. -dotnet_diagnostic.SA1635.severity = Warning # The file header at the top of a C# code file is missing copyright text. -dotnet_diagnostic.SA1636.severity = Warning # The file header at the top of a C# code file does not contain the appropriate copyright text. -dotnet_diagnostic.SA1637.severity = Warning # The file header at the top of a C# code file is missing the file name. -dotnet_diagnostic.SA1638.severity = Warning # The file tag within the file header at the top of a C# code file does not contain the name of the file. -dotnet_diagnostic.SA1639.severity = Warning # The file header at the top of a C# code file does not contain a filled-in summary tag. -dotnet_diagnostic.SA1640.severity = Warning # The file header at the top of a C# code file does not contain company name text. -dotnet_diagnostic.SA1641.severity = Warning # The file header at the top of a C# code file does not contain the appropriate company name text. -dotnet_diagnostic.SA1642.severity = Warning # The XML documentation header for a C# constructor does not contain the appropriate summary text. -dotnet_diagnostic.SA1643.severity = Warning # The Xml documentation header for a C# finalizer does not contain the appropriate summary text. -dotnet_diagnostic.SA1644.severity = Warning # A section within the Xml documentation header for a C# element contains blank lines. -dotnet_diagnostic.SA1645.severity = Warning # An included Xml documentation file does not exist. -dotnet_diagnostic.SA1646.severity = Warning # An included Xml documentation link contains an invalid path. -dotnet_diagnostic.SA1647.severity = Warning # An include tag within an Xml documentation header does not contain valid file and path attribute. -dotnet_diagnostic.SA1648.severity = Warning # has been used on an element that doesn't inherit from a base class or implement an interface. -dotnet_diagnostic.SA1649.severity = Warning # The file name of a C# code file does not match the first type declared in the file. -dotnet_diagnostic.SA1650.severity = Warning # The element documentation for the element contains one or more spelling mistakes or unrecognized words. -dotnet_diagnostic.SA1651.severity = Warning # The documentation for the element contains one or more elements. -dotnet_diagnostic.SA1652.severity = None # This rule was moved to SA0001 - -# Sonar -dotnet_diagnostic.S107.severity = Suggestion # Methods should not have too many parameters -dotnet_diagnostic.S1210.severity = Suggestion # "Equals" and the comparison operators should be overridden when implementing "IComparable" -dotnet_diagnostic.S2436.severity = Suggestion # Types and methods should not have too many generic parameters -dotnet_diagnostic.S4457.severity = Warning # Parameter validation in "async"/"await" methods should be wrapped - -# C# Autogenerated code -[{**/obj/**,**/bin/**}] -# AnalyzerId= Microsoft.CodeAnalysis.CSharp RuleNamespace= Microsoft.CodeAnalysis.CSharp -dotnet_diagnostic.CS8019.severity = none - -# C# Tests files in Tests projects -[{**Tests/**Tests.cs,**Tests.**/**Tests.cs}] -# Reset StyleCop documentation comments -dotnet_diagnostic.SA1600.severity = None # A C# code element is missing a documentation header. -dotnet_diagnostic.SA1601.severity = None # A C# partial element is missing a documentation header. -dotnet_diagnostic.SA1602.severity = None # An item within a C# enumeration is missing an Xml documentation header. - -# Sonar -dotnet_diagnostic.S4114.severity = Suggestion # Methods should not have identical implementations - -# Microsoft.VisualStudio.Threading -dotnet_diagnostic.VSTHRD200.severity = None # Use Async suffix for async methods - -# Misc -dotnet_diagnostic.CS1591.severity = None # Missing XML comment for publicly visible type or member 'Type_or_Member' -dotnet_diagnostic.SA1600.severity = None # Elements should be documented -dotnet_diagnostic.CA1707.severity = None # Identifiers should not contain underscores -dotnet_diagnostic.RCS1192.severity = Warning # RCS1192: Unnecessary usage of verbatim string literal. diff --git a/src/.runsettings b/src/.runsettings new file mode 100644 index 0000000..ea3b394 --- /dev/null +++ b/src/.runsettings @@ -0,0 +1,27 @@ + + + + + 0 + + + + + + + + + + .*\.Tests\.dll + .*\.Benchmarks\.dll + xunit\.execution\.* + xunit\.v3\.* + NSubstitute\.* + + + true + + + + + diff --git a/src/Arcus.Benchmarks/AbstractIPAddressRangeBenchmarks.cs b/src/Arcus.Benchmarks/AbstractIPAddressRangeBenchmarks.cs new file mode 100644 index 0000000..f2a6306 --- /dev/null +++ b/src/Arcus.Benchmarks/AbstractIPAddressRangeBenchmarks.cs @@ -0,0 +1,74 @@ +using System.Net; +using System.Net.Sockets; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Jobs; + +namespace Arcus.Benchmarks +{ + public enum RangeSize + { + Small, // 4 addresses (/30 IPv4, /126 IPv6) + Medium, // 256 addresses (/24 IPv4, /120 IPv6) + Large, // 4096 addresses (/20 IPv4, /116 IPv6) + } + + [SimpleJob(RuntimeMoniker.Net48)] + [SimpleJob(RuntimeMoniker.Net80)] + [SimpleJob(RuntimeMoniker.Net90)] + [SimpleJob(RuntimeMoniker.Net10_0)] + [MemoryDiagnoser] + /// + /// Benchmarks for enumeration across address families and range sizes. + /// + public class AbstractIPAddressRangeBenchmarks + { + [Params(AddressFamily.InterNetwork, AddressFamily.InterNetworkV6)] + public AddressFamily Family; + + [Params(RangeSize.Small, RangeSize.Medium, RangeSize.Large)] + public RangeSize Size; + + private Subnet _subnet; + + [GlobalSetup] + public void Setup() + { + _subnet = (Family, Size) switch + { + (AddressFamily.InterNetwork, RangeSize.Small) => new Subnet(IPAddress.Parse("10.0.0.0"), 30), + (AddressFamily.InterNetwork, RangeSize.Medium) => new Subnet(IPAddress.Parse("10.0.0.0"), 24), + (AddressFamily.InterNetwork, RangeSize.Large) => new Subnet(IPAddress.Parse("10.0.0.0"), 20), + (AddressFamily.InterNetworkV6, RangeSize.Small) => new Subnet(IPAddress.Parse("2001:db8::"), 126), + (AddressFamily.InterNetworkV6, RangeSize.Medium) => new Subnet(IPAddress.Parse("2001:db8::"), 120), + (AddressFamily.InterNetworkV6, RangeSize.Large) => new Subnet(IPAddress.Parse("2001:db8::"), 116), + _ => throw new System.ArgumentOutOfRangeException(), + }; + } + + [Benchmark(Baseline = true)] +#pragma warning disable CS0618 // Type or member is obsolete - intentionally testing backwards-compat path + public int Enumerate_Foreach() + { + var count = 0; + foreach (var _ in _subnet) + { + count++; + } + + return count; + } +#pragma warning restore CS0618 + + [Benchmark] + public int Enumerate_Enumerate() + { + var count = 0; + foreach (var _ in _subnet.ToIPAddresses()) + { + count++; + } + + return count; + } + } +} diff --git a/src/Arcus.Benchmarks/Arcus.Benchmarks.csproj b/src/Arcus.Benchmarks/Arcus.Benchmarks.csproj new file mode 100644 index 0000000..8ea9b76 --- /dev/null +++ b/src/Arcus.Benchmarks/Arcus.Benchmarks.csproj @@ -0,0 +1,13 @@ + + + Exe + net48;net8.0;net9.0;net10.0 + 14.0 + false + + + + + + + diff --git a/src/Arcus.Benchmarks/BigEndianBitWrapperBenchmarks.cs b/src/Arcus.Benchmarks/BigEndianBitWrapperBenchmarks.cs new file mode 100644 index 0000000..99661cf --- /dev/null +++ b/src/Arcus.Benchmarks/BigEndianBitWrapperBenchmarks.cs @@ -0,0 +1,77 @@ +using System.Net; +using System.Net.Sockets; +using System.Numerics; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Jobs; + +namespace Arcus.Benchmarks +{ + [SimpleJob(RuntimeMoniker.Net48)] + [SimpleJob(RuntimeMoniker.Net80)] + [SimpleJob(RuntimeMoniker.Net90)] + [SimpleJob(RuntimeMoniker.Net10_0)] + [MemoryDiagnoser] + /// + /// Benchmarks for construction, arithmetic, and conversion. + /// + public class BigEndianBitWrapperBenchmarks + { + [Params(AddressFamily.InterNetwork, AddressFamily.InterNetworkV6)] + public AddressFamily Family; + + private byte[] _addressBytes; + private BigEndianBitWrapper _wrapper; + private int _byteWidth; + private int _prefixLength; + + [GlobalSetup] + public void Setup() + { + if (Family == AddressFamily.InterNetwork) + { + _addressBytes = IPAddress.Parse("192.168.1.100").GetAddressBytes(); + _byteWidth = 4; + _prefixLength = 24; + } + else + { + _addressBytes = IPAddress.Parse("2001:db8::dead:beef").GetAddressBytes(); + _byteWidth = 16; + _prefixLength = 64; + } + + _wrapper = BigEndianBitWrapper.FromBytes(_addressBytes); + } + + [Benchmark] + public int FromBytes() + { + return BigEndianBitWrapper.FromBytes(_addressBytes).ByteWidth; + } + + [Benchmark] + public bool TryAdd() + { + _wrapper.TryAdd(1, out var result); + return result.ByteWidth > 0; + } + + [Benchmark] + public byte[] ToBytes() + { + return _wrapper.ToBytes(); + } + + [Benchmark] + public BigInteger ToBigInteger() + { + return _wrapper.ToBigInteger(); + } + + [Benchmark] + public int CreateMask() + { + return BigEndianBitWrapper.CreateMask(_byteWidth, _prefixLength).ByteWidth; + } + } +} diff --git a/src/Arcus.Benchmarks/IPAddressConvertersBenchmarks.cs b/src/Arcus.Benchmarks/IPAddressConvertersBenchmarks.cs new file mode 100644 index 0000000..f2ae8c9 --- /dev/null +++ b/src/Arcus.Benchmarks/IPAddressConvertersBenchmarks.cs @@ -0,0 +1,55 @@ +using System.Net; +using System.Net.Sockets; +using Arcus.Converters; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Jobs; + +namespace Arcus.Benchmarks +{ + [SimpleJob(RuntimeMoniker.Net48)] + [SimpleJob(RuntimeMoniker.Net80)] + [SimpleJob(RuntimeMoniker.Net90)] + [SimpleJob(RuntimeMoniker.Net10_0)] + [MemoryDiagnoser] + /// + /// Benchmarks for string conversion methods. + /// + public class IPAddressConvertersBenchmarks + { + [Params(AddressFamily.InterNetwork, AddressFamily.InterNetworkV6)] + public AddressFamily Family; + + private IPAddress _address; + + // ToBase85String is only meaningful for IPv6; this field is always IPv6. + private static readonly IPAddress Ipv6Address = IPAddress.Parse("2001:db8::dead:beef:cafe:1234"); + + [GlobalSetup] + public void Setup() + { + _address = + Family == AddressFamily.InterNetwork + ? IPAddress.Parse("192.168.1.100") + : IPAddress.Parse("2001:db8::dead:beef:cafe:1234"); + } + + // ToBase85String applies to IPv6 only; always benchmarks the IPv6 path. + [Benchmark] + public static string ToBase85String() + { + return Ipv6Address.ToBase85String(); + } + + [Benchmark] + public string ToDottedQuadString() + { + return _address.ToDottedQuadString(); + } + + [Benchmark] + public string ToUncompressedString() + { + return _address.ToUncompressedString(); + } + } +} diff --git a/src/Arcus.Benchmarks/IPAddressMathBenchmarks.cs b/src/Arcus.Benchmarks/IPAddressMathBenchmarks.cs new file mode 100644 index 0000000..d8e8ab8 --- /dev/null +++ b/src/Arcus.Benchmarks/IPAddressMathBenchmarks.cs @@ -0,0 +1,61 @@ +using System.Net; +using System.Net.Sockets; +using Arcus.Math; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Jobs; + +namespace Arcus.Benchmarks +{ + [SimpleJob(RuntimeMoniker.Net48)] + [SimpleJob(RuntimeMoniker.Net80)] + [SimpleJob(RuntimeMoniker.Net90)] + [SimpleJob(RuntimeMoniker.Net10_0)] + [MemoryDiagnoser] + /// + /// Benchmarks for comparison and arithmetic methods. + /// + public class IPAddressMathBenchmarks + { + [Params(AddressFamily.InterNetwork, AddressFamily.InterNetworkV6)] + public AddressFamily Family; + + private IPAddress _address; + private IPAddress _low; + private IPAddress _high; + + [GlobalSetup] + public void Setup() + { + if (Family == AddressFamily.InterNetwork) + { + _address = IPAddress.Parse("192.168.1.100"); + _low = IPAddress.Parse("192.168.1.0"); + _high = IPAddress.Parse("192.168.1.255"); + } + else + { + _address = IPAddress.Parse("2001:db8::dead:beef"); + _low = IPAddress.Parse("2001:db8::"); + _high = IPAddress.Parse("2001:db8::ffff:ffff:ffff:ffff"); + } + } + + [Benchmark] + public IPAddress Increment() + { + return _address.Increment(); + } + + [Benchmark] + public bool IsGreaterThan() + { + return _address.IsGreaterThan(_low); + } + + [Benchmark] + public bool IsBetween() + { + return _address.IsBetween(_low, _high); + } + } +} diff --git a/src/Arcus.Benchmarks/IPAddressRangeBenchmarks.cs b/src/Arcus.Benchmarks/IPAddressRangeBenchmarks.cs new file mode 100644 index 0000000..f052d55 --- /dev/null +++ b/src/Arcus.Benchmarks/IPAddressRangeBenchmarks.cs @@ -0,0 +1,71 @@ +using System.Net; +using System.Net.Sockets; +using Arcus.Math; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Jobs; + +namespace Arcus.Benchmarks +{ + [SimpleJob(RuntimeMoniker.Net48)] + [SimpleJob(RuntimeMoniker.Net80)] + [SimpleJob(RuntimeMoniker.Net90)] + [SimpleJob(RuntimeMoniker.Net10_0)] + [MemoryDiagnoser] + /// + /// Benchmarks for collapse and exclusion operations. + /// + public class IPAddressRangeBenchmarks + { + [Params(AddressFamily.InterNetwork, AddressFamily.InterNetworkV6)] + public AddressFamily Family; + + [Params(10, 100, 1000)] + public int Count; + + private List _ranges; + private IPAddressRange _initialRange; + private List _exclusions; + + [GlobalSetup] + public void Setup() + { + var start = Family == AddressFamily.InterNetwork ? IPAddress.Parse("10.0.0.0") : IPAddress.Parse("2001:db8::"); + + // Build Count adjacent /28-equivalent ranges (16 addresses each) with no gaps, + // so TryCollapseAll has a realistic merge workload. + _ranges = new List(Count); + var cursor = start; + for (var i = 0; i < Count; i++) + { + var rangeEnd = cursor.Increment(15); + _ranges.Add(new IPAddressRange(cursor, rangeEnd)); + cursor = rangeEnd.Increment(1); + } + + // Build a large initial range and Count non-overlapping exclusion ranges within it. + var bigEnd = cursor.Increment(-1); + _initialRange = new IPAddressRange(start, bigEnd); + + _exclusions = new List(Count); + cursor = start.Increment(1); + for (var i = 0; i < Count; i++) + { + var exEnd = cursor.Increment(6); + _exclusions.Add(new IPAddressRange(cursor, exEnd)); + cursor = exEnd.Increment(2); + } + } + + [Benchmark] + public bool TryCollapseAll() + { + return IPAddressRange.TryCollapseAll(_ranges, out _); + } + + [Benchmark] + public bool TryExcludeAll() + { + return IPAddressRange.TryExcludeAll(_initialRange, _exclusions, out _); + } + } +} diff --git a/src/Arcus.Benchmarks/IPAddressUtilitiesBenchmarks.cs b/src/Arcus.Benchmarks/IPAddressUtilitiesBenchmarks.cs new file mode 100644 index 0000000..251d46c --- /dev/null +++ b/src/Arcus.Benchmarks/IPAddressUtilitiesBenchmarks.cs @@ -0,0 +1,39 @@ +using System.Net; +using System.Net.Sockets; +using Arcus.Utilities; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Jobs; + +namespace Arcus.Benchmarks +{ + [SimpleJob(RuntimeMoniker.Net48)] + [SimpleJob(RuntimeMoniker.Net80)] + [SimpleJob(RuntimeMoniker.Net90)] + [SimpleJob(RuntimeMoniker.Net10_0)] + [MemoryDiagnoser] + /// + /// Benchmarks for hex parsing methods. + /// + public class IPAddressUtilitiesBenchmarks + { + [Params(AddressFamily.InterNetwork, AddressFamily.InterNetworkV6)] + public AddressFamily Family; + + private string _hexString; + + [GlobalSetup] + public void Setup() + { + _hexString = + Family == AddressFamily.InterNetwork + ? "C0A80164" // 192.168.1.100 + : "20010DB8DEADBEEFCAFE000000001234"; // 2001:db8::dead:beef:cafe:0:1234 + } + + [Benchmark] + public IPAddress ParseFromHexString() + { + return IPAddressUtilities.ParseFromHexString(_hexString, Family); + } + } +} diff --git a/src/Arcus.Benchmarks/Program.cs b/src/Arcus.Benchmarks/Program.cs new file mode 100644 index 0000000..c9a0467 --- /dev/null +++ b/src/Arcus.Benchmarks/Program.cs @@ -0,0 +1,3 @@ +using BenchmarkDotNet.Running; + +BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); diff --git a/src/Arcus.Benchmarks/SubnetBenchmarks.cs b/src/Arcus.Benchmarks/SubnetBenchmarks.cs new file mode 100644 index 0000000..ac61f6c --- /dev/null +++ b/src/Arcus.Benchmarks/SubnetBenchmarks.cs @@ -0,0 +1,119 @@ +using System.Net; +using System.Net.Sockets; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Jobs; + +namespace Arcus.Benchmarks +{ + [SimpleJob(RuntimeMoniker.Net48)] + [SimpleJob(RuntimeMoniker.Net80)] + [SimpleJob(RuntimeMoniker.Net90)] + [SimpleJob(RuntimeMoniker.Net10_0)] + [MemoryDiagnoser] + /// + /// Benchmarks for construction, parsing, and membership operations. + /// + public class SubnetBenchmarks + { + [Params(AddressFamily.InterNetwork, AddressFamily.InterNetworkV6)] + public AddressFamily Family { get; set; } + + private IPAddress _low; + private IPAddress _high; + private IPAddress _address; + private int _routingPrefix; + private string _cidrString; + private Subnet _subnet; + private Subnet _innerSubnet; + private IPAddress _containedAddress; + private Subnet _adjacentSubnet; + + // FromNetMask is IPv4-only; these fields are always IPv4 regardless of Family param. + private static readonly IPAddress Ipv4Address = IPAddress.Parse("192.168.1.42"); + private static readonly IPAddress Ipv4Netmask = IPAddress.Parse("255.255.255.0"); + + [GlobalSetup] + public void Setup() + { + if (Family == AddressFamily.InterNetwork) + { + _low = IPAddress.Parse("192.168.1.0"); + _high = IPAddress.Parse("192.168.1.255"); + _address = IPAddress.Parse("192.168.1.0"); + _routingPrefix = 24; + _cidrString = "192.168.1.0/24"; + _subnet = new Subnet(_address, _routingPrefix); + _innerSubnet = new Subnet(IPAddress.Parse("192.168.1.64"), 26); + _containedAddress = IPAddress.Parse("192.168.1.200"); + _adjacentSubnet = new Subnet(IPAddress.Parse("192.168.2.0"), 24); + } + else + { + _low = IPAddress.Parse("2001:db8::"); + _high = IPAddress.Parse("2001:db8:0:0:ffff:ffff:ffff:ffff"); + _address = IPAddress.Parse("2001:db8::"); + _routingPrefix = 64; + _cidrString = "2001:db8::/64"; + _subnet = new Subnet(_address, _routingPrefix); + _innerSubnet = new Subnet(IPAddress.Parse("2001:db8::1:0:0:0"), 96); + _containedAddress = IPAddress.Parse("2001:db8::dead:beef"); + _adjacentSubnet = new Subnet(IPAddress.Parse("2001:db8:0:1::"), 64); + } + } + + [Benchmark] + public Subnet ConstructFromTwoAddresses() + { + return new(_low, _high); + } + + [Benchmark] + public Subnet ConstructFromAddressAndPrefix() + { + return new(_address, _routingPrefix); + } + + [Benchmark] + public Subnet Parse() + { + return Subnet.Parse(_cidrString); + } + + [Benchmark] + public bool TryParse() + { + return Subnet.TryParse(_cidrString, out _); + } + + // Always benchmarks the IPv4 path regardless of Family param. + [Benchmark] + public static Subnet FromNetMask() + { + return Subnet.FromNetMask(Ipv4Address, Ipv4Netmask); + } + + [Benchmark] + public bool Contains_Address() + { + return _subnet.Contains(_containedAddress); + } + + [Benchmark] + public bool Contains_Range() + { + return _subnet.Contains(_innerSubnet); + } + + [Benchmark] + public bool Overlaps() + { + return _subnet.Overlaps(_innerSubnet); + } + + [Benchmark] + public bool Touches() + { + return _subnet.Touches(_adjacentSubnet); + } + } +} diff --git a/src/Arcus.Benchmarks/SubnetUtilitiesBenchmarks.cs b/src/Arcus.Benchmarks/SubnetUtilitiesBenchmarks.cs new file mode 100644 index 0000000..d6ec4c3 --- /dev/null +++ b/src/Arcus.Benchmarks/SubnetUtilitiesBenchmarks.cs @@ -0,0 +1,67 @@ +using System.Net; +using System.Net.Sockets; +using Arcus.Utilities; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Jobs; + +namespace Arcus.Benchmarks +{ + [SimpleJob(RuntimeMoniker.Net48)] + [SimpleJob(RuntimeMoniker.Net80)] + [SimpleJob(RuntimeMoniker.Net90)] + [SimpleJob(RuntimeMoniker.Net10_0)] + [MemoryDiagnoser] + /// + /// Benchmarks for across address families and range sizes. + /// + public class SubnetUtilitiesBenchmarks + { + [Params(AddressFamily.InterNetwork, AddressFamily.InterNetworkV6)] + public AddressFamily Family; + + [Params(RangeSize.Small, RangeSize.Medium, RangeSize.Large)] + public RangeSize Size; + + private IPAddress _low; + private IPAddress _high; + + [GlobalSetup] + public void Setup() + { + // CodeQL [cs/missed-ternary-operator] if/else is clearer than nested ternaries for this setup logic — each branch has a distinct family-specific switch with documentation + if (Family == AddressFamily.InterNetwork) + { + // Non-power-of-two ranges force recursive binary subdivision. + (_low, _high) = Size switch + { + RangeSize.Small => (IPAddress.Parse("10.0.0.1"), IPAddress.Parse("10.0.0.10")), + RangeSize.Medium => (IPAddress.Parse("10.0.0.1"), IPAddress.Parse("10.0.0.254")), + RangeSize.Large => (IPAddress.Parse("10.0.0.1"), IPAddress.Parse("10.0.15.254")), + _ => throw new System.ArgumentOutOfRangeException(), + }; + } + else + { + (_low, _high) = Size switch + { + RangeSize.Small => (IPAddress.Parse("2001:db8::1"), IPAddress.Parse("2001:db8::a")), + RangeSize.Medium => (IPAddress.Parse("2001:db8::1"), IPAddress.Parse("2001:db8::fe")), + RangeSize.Large => (IPAddress.Parse("2001:db8::1"), IPAddress.Parse("2001:db8::0:fffd")), + _ => throw new System.ArgumentOutOfRangeException(), + }; + } + } + + [Benchmark] + public List FewestConsecutiveSubnetsFor() + { + var result = new List(); + foreach (var subnet in SubnetUtilities.FewestConsecutiveSubnetsFor(_low, _high)) + { + result.Add(subnet); + } + + return result; + } + } +} diff --git a/src/Arcus.Benchmarks/packages.lock.json b/src/Arcus.Benchmarks/packages.lock.json new file mode 100644 index 0000000..81bdc61 --- /dev/null +++ b/src/Arcus.Benchmarks/packages.lock.json @@ -0,0 +1,907 @@ +{ + "version": 2, + "dependencies": { + ".NETFramework,Version=v4.8": { + "BenchmarkDotNet": { + "type": "Direct", + "requested": "[0.15.8, )", + "resolved": "0.15.8", + "contentHash": "paCfrWxSeHqn3rUZc0spYXVFnHCF0nzRhG0nOLnyTjZYs8spsimBaaNmb3vwqvALKIplbYq/TF393vYiYSnh/Q==", + "dependencies": { + "BenchmarkDotNet.Annotations": "0.15.8", + "CommandLineParser": "2.9.1", + "Gee.External.Capstone": "2.3.0", + "Iced": "1.21.0", + "Microsoft.CodeAnalysis.CSharp": "4.14.0", + "Microsoft.Diagnostics.Runtime": "3.1.512801", + "Microsoft.Diagnostics.Tracing.TraceEvent": "3.1.21", + "Microsoft.DotNet.PlatformAbstractions": "3.1.6", + "Microsoft.Win32.Registry": "5.0.0", + "Perfolizer": "[0.6.1]", + "System.Management": "9.0.5", + "System.Numerics.Vectors": "4.5.0", + "System.Reflection.Emit": "4.7.0", + "System.Reflection.Emit.Lightweight": "4.7.0", + "System.Threading.Tasks.Extensions": "4.6.3" + } + }, + "Microsoft.NETFramework.ReferenceAssemblies": { + "type": "Direct", + "requested": "[1.0.3, )", + "resolved": "1.0.3", + "contentHash": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==", + "dependencies": { + "Microsoft.NETFramework.ReferenceAssemblies.net48": "1.0.3" + } + }, + "BenchmarkDotNet.Annotations": { + "type": "Transitive", + "resolved": "0.15.8", + "contentHash": "hfucY0ycAsB0SsoaZcaAp9oq5wlWBJcylvEJb9pmvdYUx6PD6S4mDiYnZWjdjAlLhIpe/xtGCwzORfzAzPqvzA==" + }, + "CommandLineParser": { + "type": "Transitive", + "resolved": "2.9.1", + "contentHash": "OE0sl1/sQ37bjVsPKKtwQlWDgqaxWgtme3xZz7JssWUzg5JpMIyHgCTY9MVMxOg48fJ1AgGT3tgdH5m/kQ5xhA==" + }, + "Gee.External.Capstone": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "2ap/rYmjtzCOT8hxrnEW/QeiOt+paD8iRrIcdKX0cxVwWLFa1e+JDBNeECakmccXrSFeBQuu5AV8SNkipFMMMw==" + }, + "Iced": { + "type": "Transitive", + "resolved": "1.21.0", + "contentHash": "dv5+81Q1TBQvVMSOOOmRcjJmvWcX3BZPZsIq31+RLc5cNft0IHAyNlkdb7ZarOWG913PyBoFDsDXoCIlKmLclg==" + }, + "Microsoft.Bcl.AsyncInterfaces": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "3WA9q9yVqJp222P3x1wYIGDAkpjAku0TMUaaQV22g6L67AI0LdOIrVS7Ht2vJfLHGSPVuqN94vIr15qn+HEkHw==", + "dependencies": { + "System.Threading.Tasks.Extensions": "4.5.4" + } + }, + "Microsoft.CodeAnalysis.Analyzers": { + "type": "Transitive", + "resolved": "3.11.0", + "contentHash": "v/EW3UE8/lbEYHoC2Qq7AR/DnmvpgdtAMndfQNmpuIMx/Mto8L5JnuCfdBYtgvalQOtfNCnxFejxuRrryvUTsg==" + }, + "Microsoft.CodeAnalysis.Common": { + "type": "Transitive", + "resolved": "4.14.0", + "contentHash": "PC3tuwZYnC+idaPuoC/AZpEdwrtX7qFpmnrfQkgobGIWiYmGi5MCRtl5mx6QrfMGQpK78X2lfIEoZDLg/qnuHg==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "System.Buffers": "4.5.1", + "System.Collections.Immutable": "9.0.0", + "System.Memory": "4.5.5", + "System.Numerics.Vectors": "4.5.0", + "System.Reflection.Metadata": "9.0.0", + "System.Runtime.CompilerServices.Unsafe": "6.0.0", + "System.Text.Encoding.CodePages": "7.0.0", + "System.Threading.Tasks.Extensions": "4.5.4" + } + }, + "Microsoft.CodeAnalysis.CSharp": { + "type": "Transitive", + "resolved": "4.14.0", + "contentHash": "568a6wcTivauIhbeWcCwfWwIn7UV7MeHEBvFB2uzGIpM2OhJ4eM/FZ8KS0yhPoNxnSpjGzz7x7CIjTxhslojQA==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "Microsoft.CodeAnalysis.Common": "[4.14.0]", + "System.Buffers": "4.5.1", + "System.Collections.Immutable": "9.0.0", + "System.Memory": "4.5.5", + "System.Numerics.Vectors": "4.5.0", + "System.Reflection.Metadata": "9.0.0", + "System.Runtime.CompilerServices.Unsafe": "6.0.0", + "System.Text.Encoding.CodePages": "7.0.0", + "System.Threading.Tasks.Extensions": "4.5.4" + } + }, + "Microsoft.Diagnostics.NETCore.Client": { + "type": "Transitive", + "resolved": "0.2.510501", + "contentHash": "juoqJYMDs+lRrrZyOkXXMImJHneCF23cuvO4waFRd2Ds7j+ZuGIPbJm0Y/zz34BdeaGiiwGWraMUlln05W1PCQ==", + "dependencies": { + "Microsoft.Bcl.AsyncInterfaces": "6.0.0", + "Microsoft.Extensions.Logging": "6.0.0", + "System.Buffers": "4.5.1" + } + }, + "Microsoft.Diagnostics.Runtime": { + "type": "Transitive", + "resolved": "3.1.512801", + "contentHash": "0lMUDr2oxNZa28D6NH5BuSQEe5T9tZziIkvkD44YkkCGQXPJqvFjLq5ZQq1hYLl3RjQJrY+hR0jFgap+EWPDTw==", + "dependencies": { + "Microsoft.Diagnostics.NETCore.Client": "0.2.410101", + "System.Collections.Immutable": "6.0.0", + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "Microsoft.Diagnostics.Tracing.TraceEvent": { + "type": "Transitive", + "resolved": "3.1.21", + "contentHash": "/OrJFKaojSR6TkUKtwh8/qA9XWNtxLrXMqvEb89dBSKCWjaGVTbKMYodIUgF5deCEtmd6GXuRerciXGl5bhZ7Q==", + "dependencies": { + "Microsoft.Diagnostics.NETCore.Client": "0.2.510501", + "Microsoft.Win32.Registry": "5.0.0", + "System.Collections.Immutable": "8.0.0", + "System.Reflection.Metadata": "8.0.0", + "System.Reflection.TypeExtensions": "4.7.0", + "System.Runtime.CompilerServices.Unsafe": "6.0.0", + "System.Text.Json": "8.0.5" + } + }, + "Microsoft.DotNet.PlatformAbstractions": { + "type": "Transitive", + "resolved": "3.1.6", + "contentHash": "jek4XYaQ/PGUwDKKhwR8K47Uh1189PFzMeLqO83mXrXQVIpARZCcfuDedH50YDTepBkfijCZN5U/vZi++erxtg==", + "dependencies": { + "System.Runtime.InteropServices.RuntimeInformation": "4.0.0" + } + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "k6PWQMuoBDGGHOQTtyois2u4AwyVcIwL2LaSLlTZQm2CYcJ1pxbt6jfAnpWmzENA/wfrYRI/X9DTLoUkE4AsLw==", + "dependencies": { + "Microsoft.Bcl.AsyncInterfaces": "6.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", + "System.Runtime.CompilerServices.Unsafe": "6.0.0", + "System.Threading.Tasks.Extensions": "4.5.4" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "xlzi2IYREJH3/m6+lUrQlujzX8wDitm4QGnUu6kUXTQAWPuZY8i+ticFJbzfqaetLA6KR/rO6Ew/HuYD+bxifg==", + "dependencies": { + "Microsoft.Bcl.AsyncInterfaces": "6.0.0", + "System.Threading.Tasks.Extensions": "4.5.4" + } + }, + "Microsoft.Extensions.Logging": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "eIbyj40QDg1NDz0HBW0S5f3wrLVnKWnDJ/JtZ+yJDFnDj90VoPuoPmFkeaXrtu+0cKm5GRAwoDf+dBWXK0TUdg==", + "dependencies": { + "Microsoft.Bcl.AsyncInterfaces": "6.0.0", + "Microsoft.Extensions.DependencyInjection": "6.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", + "Microsoft.Extensions.Logging.Abstractions": "6.0.0", + "Microsoft.Extensions.Options": "6.0.0", + "System.Diagnostics.DiagnosticSource": "6.0.0", + "System.ValueTuple": "4.5.0" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "/HggWBbTwy8TgebGSX5DBZ24ndhzi93sHUBDvP1IxbZD7FDokYzdAr6+vbWGjw2XAfR2EJ1sfKUotpjHnFWPxA==", + "dependencies": { + "System.Buffers": "4.5.1", + "System.Memory": "4.5.4" + } + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "dzXN0+V1AyjOe2xcJ86Qbo233KHuLEY0njf/P2Kw8SfJU+d45HNS2ctJdnEnrWbM9Ye2eFgaC5Mj9otRMU6IsQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", + "Microsoft.Extensions.Primitives": "6.0.0" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "9+PnzmQFfEFNR9J2aDTfJGGupShHjOuGw4VUv+JB044biSHrnmCIMD+mJHmb2H7YryrfBEXDurxQ47gJZdCKNQ==", + "dependencies": { + "System.Memory": "4.5.4", + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "Microsoft.NETFramework.ReferenceAssemblies.net48": { + "type": "Transitive", + "resolved": "1.0.3", + "contentHash": "zMk4D+9zyiEWByyQ7oPImPN/Jhpj166Ky0Nlla4eXlNL8hI/BtSJsgR8Inldd4NNpIAH3oh8yym0W2DrhXdSLQ==" + }, + "Microsoft.Win32.Registry": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==", + "dependencies": { + "System.Security.AccessControl": "5.0.0", + "System.Security.Principal.Windows": "5.0.0" + } + }, + "Perfolizer": { + "type": "Transitive", + "resolved": "0.6.1", + "contentHash": "CR1QmWg4XYBd1Pb7WseP+sDmV8nGPwvmowKynExTqr3OuckIGVMhvmN4LC5PGzfXqDlR295+hz/T7syA1CxEqA==", + "dependencies": { + "Pragmastat": "3.2.4", + "System.Memory": "4.5.5" + } + }, + "Pragmastat": { + "type": "Transitive", + "resolved": "3.2.4", + "contentHash": "I5qFifWw/gaTQT52MhzjZpkm/JPlfjSeO/DTZJjO7+hTKI+0aGRgOgZ3NN6D96dDuuqbIAZSeA5RimtHjqrA2A==", + "dependencies": { + "System.Memory": "4.5.5" + } + }, + "System.Buffers": { + "type": "Transitive", + "resolved": "4.5.1", + "contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==" + }, + "System.CodeDom": { + "type": "Transitive", + "resolved": "9.0.5", + "contentHash": "cuzLM2MWutf9ZBEMPYYfd0DXwYdvntp7VCT6a/wvbKCa2ZuvGmW74xi+YBa2mrfEieAXqM4TNKlMmSnfAfpUoQ==" + }, + "System.Collections.Immutable": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "QhkXUl2gNrQtvPmtBTQHb0YsUrDiDQ2QS09YbtTTiSjGcf7NBqtYbrG/BE06zcBPCKEwQGzIv13IVdXNOSub2w==", + "dependencies": { + "System.Memory": "4.5.5", + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "System.Diagnostics.DiagnosticSource": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "frQDfv0rl209cKm1lnwTgFPzNigy2EKk1BS3uAvHvlBVKe5cymGyHO+Sj+NLv5VF/AhHsqPIUUwya5oV4CHMUw==", + "dependencies": { + "System.Memory": "4.5.4", + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "System.Management": { + "type": "Transitive", + "resolved": "9.0.5", + "contentHash": "n6o9PZm9p25+zAzC3/48K0oHnaPKTInRrxqFq1fi/5TPbMLjuoCm/h//mS3cUmSy+9AO1Z+qsC/Ilt/ZFatv5Q==", + "dependencies": { + "System.CodeDom": "9.0.5" + } + }, + "System.Memory": { + "type": "Transitive", + "resolved": "4.5.5", + "contentHash": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==", + "dependencies": { + "System.Buffers": "4.5.1", + "System.Numerics.Vectors": "4.5.0", + "System.Runtime.CompilerServices.Unsafe": "4.5.3" + } + }, + "System.Numerics.Vectors": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "QQTlPTl06J/iiDbJCiepZ4H//BVraReU4O4EoRw1U02H5TLUIT7xn3GnDp9AXPSlJUDyFs4uWjWafNX6WrAojQ==" + }, + "System.Reflection.Emit": { + "type": "Transitive", + "resolved": "4.7.0", + "contentHash": "VR4kk8XLKebQ4MZuKuIni/7oh+QGFmZW3qORd1GvBq/8026OpW501SzT/oypwiQl4TvT8ErnReh/NzY9u+C6wQ==" + }, + "System.Reflection.Emit.Lightweight": { + "type": "Transitive", + "resolved": "4.7.0", + "contentHash": "a4OLB4IITxAXJeV74MDx49Oq2+PsF6Sml54XAFv+2RyWwtDBcabzoxiiJRhdhx+gaohLh4hEGCLQyBozXoQPqA==" + }, + "System.Reflection.Metadata": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "ANiqLu3DxW9kol/hMmTWbt3414t9ftdIuiIU7j80okq2YzAueo120M442xk1kDJWtmZTqWQn7wHDvMRipVOEOQ==", + "dependencies": { + "System.Collections.Immutable": "9.0.0", + "System.Memory": "4.5.5" + } + }, + "System.Reflection.TypeExtensions": { + "type": "Transitive", + "resolved": "4.7.0", + "contentHash": "VybpaOQQhqE6siHppMktjfGBw1GCwvCqiufqmP8F1nj7fTUNtW35LOEt3UZTEsECfo+ELAl/9o9nJx3U91i7vA==" + }, + "System.Runtime.CompilerServices.Unsafe": { + "type": "Transitive", + "resolved": "6.1.2", + "contentHash": "2hBr6zdbIBTDE3EhK7NSVNdX58uTK6iHW/P/Axmm9sl1xoGSLqDvMtpecn226TNwHByFokYwJmt/aQQNlO5CRw==" + }, + "System.Runtime.InteropServices.RuntimeInformation": { + "type": "Transitive", + "resolved": "4.0.0", + "contentHash": "hWPhJxc453RCa8Z29O91EmfGeZIHX1ZH2A8L6lYQVSaKzku2DfArSfMEb1/MYYzPQRJZeu0c9dmYeJKxW5Fgng==" + }, + "System.Security.AccessControl": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "dagJ1mHZO3Ani8GH0PHpPEe/oYO+rVdbQjvjJkBRNQkX4t0r1iaeGn8+/ybkSLEan3/slM0t59SVdHzuHf2jmw==", + "dependencies": { + "System.Security.Principal.Windows": "5.0.0" + } + }, + "System.Security.Principal.Windows": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==" + }, + "System.Text.Encoding.CodePages": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "LSyCblMpvOe0N3E+8e0skHcrIhgV2huaNcjUUEa8hRtgEAm36aGkRoC8Jxlb6Ra6GSfF29ftduPNywin8XolzQ==", + "dependencies": { + "System.Memory": "4.5.5", + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "System.Text.Encodings.Web": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "yev/k9GHAEGx2Rg3/tU6MQh4HGBXJs70y7j1LaM1i/ER9po+6nnQ6RRqTJn1E7Xu0fbIFK80Nh5EoODxrbxwBQ==", + "dependencies": { + "System.Buffers": "4.5.1", + "System.Memory": "4.5.5", + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "System.Text.Json": { + "type": "Transitive", + "resolved": "8.0.5", + "contentHash": "0f1B50Ss7rqxXiaBJyzUu9bWFOO2/zSlifZ/UNMdiIpDYe4cY4LQQicP4nirK1OS31I43rn062UIJ1Q9bpmHpg==", + "dependencies": { + "Microsoft.Bcl.AsyncInterfaces": "8.0.0", + "System.Buffers": "4.5.1", + "System.Memory": "4.5.5", + "System.Runtime.CompilerServices.Unsafe": "6.0.0", + "System.Text.Encodings.Web": "8.0.0", + "System.Threading.Tasks.Extensions": "4.5.4", + "System.ValueTuple": "4.5.0" + } + }, + "System.Threading.Tasks.Extensions": { + "type": "Transitive", + "resolved": "4.6.3", + "contentHash": "7sCiwilJLYbTZELaKnc7RecBBXWXA+xMLQWZKWawBxYjp6DBlSE3v9/UcvKBvr1vv2tTOhipiogM8rRmxlhrVA==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.1.2" + } + }, + "System.ValueTuple": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "okurQJO6NRE/apDIP23ajJ0hpiNmJ+f0BwOlB/cSqTLQlw5upkf+5+96+iG2Jw40G1fCVCyPz/FhIABUjMR+RQ==" + }, + "arcus": { + "type": "Project", + "dependencies": { + "Microsoft.Bcl.HashCode": "[6.0.0, )" + } + }, + "Microsoft.Bcl.HashCode": { + "type": "CentralTransitive", + "requested": "[6.0.0, )", + "resolved": "6.0.0", + "contentHash": "GI4jcoi6eC9ZhNOQylIBaWOQjyGaR8T6N3tC1u8p3EXfndLCVNNWa+Zp+ocjvvS3kNBN09Zma2HXL0ezO0dRfw==" + } + }, + "net10.0": { + "BenchmarkDotNet": { + "type": "Direct", + "requested": "[0.15.8, )", + "resolved": "0.15.8", + "contentHash": "paCfrWxSeHqn3rUZc0spYXVFnHCF0nzRhG0nOLnyTjZYs8spsimBaaNmb3vwqvALKIplbYq/TF393vYiYSnh/Q==", + "dependencies": { + "BenchmarkDotNet.Annotations": "0.15.8", + "CommandLineParser": "2.9.1", + "Gee.External.Capstone": "2.3.0", + "Iced": "1.21.0", + "Microsoft.CodeAnalysis.CSharp": "4.14.0", + "Microsoft.Diagnostics.Runtime": "3.1.512801", + "Microsoft.Diagnostics.Tracing.TraceEvent": "3.1.21", + "Microsoft.DotNet.PlatformAbstractions": "3.1.6", + "Perfolizer": "[0.6.1]", + "System.Management": "9.0.5" + } + }, + "BenchmarkDotNet.Annotations": { + "type": "Transitive", + "resolved": "0.15.8", + "contentHash": "hfucY0ycAsB0SsoaZcaAp9oq5wlWBJcylvEJb9pmvdYUx6PD6S4mDiYnZWjdjAlLhIpe/xtGCwzORfzAzPqvzA==" + }, + "CommandLineParser": { + "type": "Transitive", + "resolved": "2.9.1", + "contentHash": "OE0sl1/sQ37bjVsPKKtwQlWDgqaxWgtme3xZz7JssWUzg5JpMIyHgCTY9MVMxOg48fJ1AgGT3tgdH5m/kQ5xhA==" + }, + "Gee.External.Capstone": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "2ap/rYmjtzCOT8hxrnEW/QeiOt+paD8iRrIcdKX0cxVwWLFa1e+JDBNeECakmccXrSFeBQuu5AV8SNkipFMMMw==" + }, + "Iced": { + "type": "Transitive", + "resolved": "1.21.0", + "contentHash": "dv5+81Q1TBQvVMSOOOmRcjJmvWcX3BZPZsIq31+RLc5cNft0IHAyNlkdb7ZarOWG913PyBoFDsDXoCIlKmLclg==" + }, + "Microsoft.CodeAnalysis.Analyzers": { + "type": "Transitive", + "resolved": "3.11.0", + "contentHash": "v/EW3UE8/lbEYHoC2Qq7AR/DnmvpgdtAMndfQNmpuIMx/Mto8L5JnuCfdBYtgvalQOtfNCnxFejxuRrryvUTsg==" + }, + "Microsoft.CodeAnalysis.Common": { + "type": "Transitive", + "resolved": "4.14.0", + "contentHash": "PC3tuwZYnC+idaPuoC/AZpEdwrtX7qFpmnrfQkgobGIWiYmGi5MCRtl5mx6QrfMGQpK78X2lfIEoZDLg/qnuHg==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.11.0" + } + }, + "Microsoft.CodeAnalysis.CSharp": { + "type": "Transitive", + "resolved": "4.14.0", + "contentHash": "568a6wcTivauIhbeWcCwfWwIn7UV7MeHEBvFB2uzGIpM2OhJ4eM/FZ8KS0yhPoNxnSpjGzz7x7CIjTxhslojQA==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "Microsoft.CodeAnalysis.Common": "[4.14.0]" + } + }, + "Microsoft.Diagnostics.NETCore.Client": { + "type": "Transitive", + "resolved": "0.2.510501", + "contentHash": "juoqJYMDs+lRrrZyOkXXMImJHneCF23cuvO4waFRd2Ds7j+ZuGIPbJm0Y/zz34BdeaGiiwGWraMUlln05W1PCQ==", + "dependencies": { + "Microsoft.Extensions.Logging": "6.0.0" + } + }, + "Microsoft.Diagnostics.Runtime": { + "type": "Transitive", + "resolved": "3.1.512801", + "contentHash": "0lMUDr2oxNZa28D6NH5BuSQEe5T9tZziIkvkD44YkkCGQXPJqvFjLq5ZQq1hYLl3RjQJrY+hR0jFgap+EWPDTw==", + "dependencies": { + "Microsoft.Diagnostics.NETCore.Client": "0.2.410101" + } + }, + "Microsoft.Diagnostics.Tracing.TraceEvent": { + "type": "Transitive", + "resolved": "3.1.21", + "contentHash": "/OrJFKaojSR6TkUKtwh8/qA9XWNtxLrXMqvEb89dBSKCWjaGVTbKMYodIUgF5deCEtmd6GXuRerciXGl5bhZ7Q==", + "dependencies": { + "Microsoft.Diagnostics.NETCore.Client": "0.2.510501", + "System.Reflection.TypeExtensions": "4.7.0" + } + }, + "Microsoft.DotNet.PlatformAbstractions": { + "type": "Transitive", + "resolved": "3.1.6", + "contentHash": "jek4XYaQ/PGUwDKKhwR8K47Uh1189PFzMeLqO83mXrXQVIpARZCcfuDedH50YDTepBkfijCZN5U/vZi++erxtg==" + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "k6PWQMuoBDGGHOQTtyois2u4AwyVcIwL2LaSLlTZQm2CYcJ1pxbt6jfAnpWmzENA/wfrYRI/X9DTLoUkE4AsLw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "xlzi2IYREJH3/m6+lUrQlujzX8wDitm4QGnUu6kUXTQAWPuZY8i+ticFJbzfqaetLA6KR/rO6Ew/HuYD+bxifg==" + }, + "Microsoft.Extensions.Logging": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "eIbyj40QDg1NDz0HBW0S5f3wrLVnKWnDJ/JtZ+yJDFnDj90VoPuoPmFkeaXrtu+0cKm5GRAwoDf+dBWXK0TUdg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "6.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", + "Microsoft.Extensions.Logging.Abstractions": "6.0.0", + "Microsoft.Extensions.Options": "6.0.0" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "/HggWBbTwy8TgebGSX5DBZ24ndhzi93sHUBDvP1IxbZD7FDokYzdAr6+vbWGjw2XAfR2EJ1sfKUotpjHnFWPxA==" + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "dzXN0+V1AyjOe2xcJ86Qbo233KHuLEY0njf/P2Kw8SfJU+d45HNS2ctJdnEnrWbM9Ye2eFgaC5Mj9otRMU6IsQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", + "Microsoft.Extensions.Primitives": "6.0.0" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "9+PnzmQFfEFNR9J2aDTfJGGupShHjOuGw4VUv+JB044biSHrnmCIMD+mJHmb2H7YryrfBEXDurxQ47gJZdCKNQ==" + }, + "Perfolizer": { + "type": "Transitive", + "resolved": "0.6.1", + "contentHash": "CR1QmWg4XYBd1Pb7WseP+sDmV8nGPwvmowKynExTqr3OuckIGVMhvmN4LC5PGzfXqDlR295+hz/T7syA1CxEqA==", + "dependencies": { + "Pragmastat": "3.2.4" + } + }, + "Pragmastat": { + "type": "Transitive", + "resolved": "3.2.4", + "contentHash": "I5qFifWw/gaTQT52MhzjZpkm/JPlfjSeO/DTZJjO7+hTKI+0aGRgOgZ3NN6D96dDuuqbIAZSeA5RimtHjqrA2A==" + }, + "System.CodeDom": { + "type": "Transitive", + "resolved": "9.0.5", + "contentHash": "cuzLM2MWutf9ZBEMPYYfd0DXwYdvntp7VCT6a/wvbKCa2ZuvGmW74xi+YBa2mrfEieAXqM4TNKlMmSnfAfpUoQ==" + }, + "System.Management": { + "type": "Transitive", + "resolved": "9.0.5", + "contentHash": "n6o9PZm9p25+zAzC3/48K0oHnaPKTInRrxqFq1fi/5TPbMLjuoCm/h//mS3cUmSy+9AO1Z+qsC/Ilt/ZFatv5Q==", + "dependencies": { + "System.CodeDom": "9.0.5" + } + }, + "System.Reflection.TypeExtensions": { + "type": "Transitive", + "resolved": "4.7.0", + "contentHash": "VybpaOQQhqE6siHppMktjfGBw1GCwvCqiufqmP8F1nj7fTUNtW35LOEt3UZTEsECfo+ELAl/9o9nJx3U91i7vA==" + }, + "arcus": { + "type": "Project" + } + }, + "net8.0": { + "BenchmarkDotNet": { + "type": "Direct", + "requested": "[0.15.8, )", + "resolved": "0.15.8", + "contentHash": "paCfrWxSeHqn3rUZc0spYXVFnHCF0nzRhG0nOLnyTjZYs8spsimBaaNmb3vwqvALKIplbYq/TF393vYiYSnh/Q==", + "dependencies": { + "BenchmarkDotNet.Annotations": "0.15.8", + "CommandLineParser": "2.9.1", + "Gee.External.Capstone": "2.3.0", + "Iced": "1.21.0", + "Microsoft.CodeAnalysis.CSharp": "4.14.0", + "Microsoft.Diagnostics.Runtime": "3.1.512801", + "Microsoft.Diagnostics.Tracing.TraceEvent": "3.1.21", + "Microsoft.DotNet.PlatformAbstractions": "3.1.6", + "Perfolizer": "[0.6.1]", + "System.Management": "9.0.5" + } + }, + "BenchmarkDotNet.Annotations": { + "type": "Transitive", + "resolved": "0.15.8", + "contentHash": "hfucY0ycAsB0SsoaZcaAp9oq5wlWBJcylvEJb9pmvdYUx6PD6S4mDiYnZWjdjAlLhIpe/xtGCwzORfzAzPqvzA==" + }, + "CommandLineParser": { + "type": "Transitive", + "resolved": "2.9.1", + "contentHash": "OE0sl1/sQ37bjVsPKKtwQlWDgqaxWgtme3xZz7JssWUzg5JpMIyHgCTY9MVMxOg48fJ1AgGT3tgdH5m/kQ5xhA==" + }, + "Gee.External.Capstone": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "2ap/rYmjtzCOT8hxrnEW/QeiOt+paD8iRrIcdKX0cxVwWLFa1e+JDBNeECakmccXrSFeBQuu5AV8SNkipFMMMw==" + }, + "Iced": { + "type": "Transitive", + "resolved": "1.21.0", + "contentHash": "dv5+81Q1TBQvVMSOOOmRcjJmvWcX3BZPZsIq31+RLc5cNft0IHAyNlkdb7ZarOWG913PyBoFDsDXoCIlKmLclg==" + }, + "Microsoft.CodeAnalysis.Analyzers": { + "type": "Transitive", + "resolved": "3.11.0", + "contentHash": "v/EW3UE8/lbEYHoC2Qq7AR/DnmvpgdtAMndfQNmpuIMx/Mto8L5JnuCfdBYtgvalQOtfNCnxFejxuRrryvUTsg==" + }, + "Microsoft.CodeAnalysis.Common": { + "type": "Transitive", + "resolved": "4.14.0", + "contentHash": "PC3tuwZYnC+idaPuoC/AZpEdwrtX7qFpmnrfQkgobGIWiYmGi5MCRtl5mx6QrfMGQpK78X2lfIEoZDLg/qnuHg==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "System.Collections.Immutable": "9.0.0", + "System.Reflection.Metadata": "9.0.0" + } + }, + "Microsoft.CodeAnalysis.CSharp": { + "type": "Transitive", + "resolved": "4.14.0", + "contentHash": "568a6wcTivauIhbeWcCwfWwIn7UV7MeHEBvFB2uzGIpM2OhJ4eM/FZ8KS0yhPoNxnSpjGzz7x7CIjTxhslojQA==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "Microsoft.CodeAnalysis.Common": "[4.14.0]", + "System.Collections.Immutable": "9.0.0", + "System.Reflection.Metadata": "9.0.0" + } + }, + "Microsoft.Diagnostics.NETCore.Client": { + "type": "Transitive", + "resolved": "0.2.510501", + "contentHash": "juoqJYMDs+lRrrZyOkXXMImJHneCF23cuvO4waFRd2Ds7j+ZuGIPbJm0Y/zz34BdeaGiiwGWraMUlln05W1PCQ==", + "dependencies": { + "Microsoft.Extensions.Logging": "6.0.0" + } + }, + "Microsoft.Diagnostics.Runtime": { + "type": "Transitive", + "resolved": "3.1.512801", + "contentHash": "0lMUDr2oxNZa28D6NH5BuSQEe5T9tZziIkvkD44YkkCGQXPJqvFjLq5ZQq1hYLl3RjQJrY+hR0jFgap+EWPDTw==", + "dependencies": { + "Microsoft.Diagnostics.NETCore.Client": "0.2.410101" + } + }, + "Microsoft.Diagnostics.Tracing.TraceEvent": { + "type": "Transitive", + "resolved": "3.1.21", + "contentHash": "/OrJFKaojSR6TkUKtwh8/qA9XWNtxLrXMqvEb89dBSKCWjaGVTbKMYodIUgF5deCEtmd6GXuRerciXGl5bhZ7Q==", + "dependencies": { + "Microsoft.Diagnostics.NETCore.Client": "0.2.510501" + } + }, + "Microsoft.DotNet.PlatformAbstractions": { + "type": "Transitive", + "resolved": "3.1.6", + "contentHash": "jek4XYaQ/PGUwDKKhwR8K47Uh1189PFzMeLqO83mXrXQVIpARZCcfuDedH50YDTepBkfijCZN5U/vZi++erxtg==" + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "k6PWQMuoBDGGHOQTtyois2u4AwyVcIwL2LaSLlTZQm2CYcJ1pxbt6jfAnpWmzENA/wfrYRI/X9DTLoUkE4AsLw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "xlzi2IYREJH3/m6+lUrQlujzX8wDitm4QGnUu6kUXTQAWPuZY8i+ticFJbzfqaetLA6KR/rO6Ew/HuYD+bxifg==" + }, + "Microsoft.Extensions.Logging": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "eIbyj40QDg1NDz0HBW0S5f3wrLVnKWnDJ/JtZ+yJDFnDj90VoPuoPmFkeaXrtu+0cKm5GRAwoDf+dBWXK0TUdg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "6.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", + "Microsoft.Extensions.Logging.Abstractions": "6.0.0", + "Microsoft.Extensions.Options": "6.0.0" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "/HggWBbTwy8TgebGSX5DBZ24ndhzi93sHUBDvP1IxbZD7FDokYzdAr6+vbWGjw2XAfR2EJ1sfKUotpjHnFWPxA==" + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "dzXN0+V1AyjOe2xcJ86Qbo233KHuLEY0njf/P2Kw8SfJU+d45HNS2ctJdnEnrWbM9Ye2eFgaC5Mj9otRMU6IsQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", + "Microsoft.Extensions.Primitives": "6.0.0" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "9+PnzmQFfEFNR9J2aDTfJGGupShHjOuGw4VUv+JB044biSHrnmCIMD+mJHmb2H7YryrfBEXDurxQ47gJZdCKNQ==" + }, + "Perfolizer": { + "type": "Transitive", + "resolved": "0.6.1", + "contentHash": "CR1QmWg4XYBd1Pb7WseP+sDmV8nGPwvmowKynExTqr3OuckIGVMhvmN4LC5PGzfXqDlR295+hz/T7syA1CxEqA==", + "dependencies": { + "Pragmastat": "3.2.4" + } + }, + "Pragmastat": { + "type": "Transitive", + "resolved": "3.2.4", + "contentHash": "I5qFifWw/gaTQT52MhzjZpkm/JPlfjSeO/DTZJjO7+hTKI+0aGRgOgZ3NN6D96dDuuqbIAZSeA5RimtHjqrA2A==" + }, + "System.CodeDom": { + "type": "Transitive", + "resolved": "9.0.5", + "contentHash": "cuzLM2MWutf9ZBEMPYYfd0DXwYdvntp7VCT6a/wvbKCa2ZuvGmW74xi+YBa2mrfEieAXqM4TNKlMmSnfAfpUoQ==" + }, + "System.Collections.Immutable": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "QhkXUl2gNrQtvPmtBTQHb0YsUrDiDQ2QS09YbtTTiSjGcf7NBqtYbrG/BE06zcBPCKEwQGzIv13IVdXNOSub2w==" + }, + "System.Management": { + "type": "Transitive", + "resolved": "9.0.5", + "contentHash": "n6o9PZm9p25+zAzC3/48K0oHnaPKTInRrxqFq1fi/5TPbMLjuoCm/h//mS3cUmSy+9AO1Z+qsC/Ilt/ZFatv5Q==", + "dependencies": { + "System.CodeDom": "9.0.5" + } + }, + "System.Reflection.Metadata": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "ANiqLu3DxW9kol/hMmTWbt3414t9ftdIuiIU7j80okq2YzAueo120M442xk1kDJWtmZTqWQn7wHDvMRipVOEOQ==", + "dependencies": { + "System.Collections.Immutable": "9.0.0" + } + }, + "arcus": { + "type": "Project" + } + }, + "net9.0": { + "BenchmarkDotNet": { + "type": "Direct", + "requested": "[0.15.8, )", + "resolved": "0.15.8", + "contentHash": "paCfrWxSeHqn3rUZc0spYXVFnHCF0nzRhG0nOLnyTjZYs8spsimBaaNmb3vwqvALKIplbYq/TF393vYiYSnh/Q==", + "dependencies": { + "BenchmarkDotNet.Annotations": "0.15.8", + "CommandLineParser": "2.9.1", + "Gee.External.Capstone": "2.3.0", + "Iced": "1.21.0", + "Microsoft.CodeAnalysis.CSharp": "4.14.0", + "Microsoft.Diagnostics.Runtime": "3.1.512801", + "Microsoft.Diagnostics.Tracing.TraceEvent": "3.1.21", + "Microsoft.DotNet.PlatformAbstractions": "3.1.6", + "Perfolizer": "[0.6.1]", + "System.Management": "9.0.5" + } + }, + "BenchmarkDotNet.Annotations": { + "type": "Transitive", + "resolved": "0.15.8", + "contentHash": "hfucY0ycAsB0SsoaZcaAp9oq5wlWBJcylvEJb9pmvdYUx6PD6S4mDiYnZWjdjAlLhIpe/xtGCwzORfzAzPqvzA==" + }, + "CommandLineParser": { + "type": "Transitive", + "resolved": "2.9.1", + "contentHash": "OE0sl1/sQ37bjVsPKKtwQlWDgqaxWgtme3xZz7JssWUzg5JpMIyHgCTY9MVMxOg48fJ1AgGT3tgdH5m/kQ5xhA==" + }, + "Gee.External.Capstone": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "2ap/rYmjtzCOT8hxrnEW/QeiOt+paD8iRrIcdKX0cxVwWLFa1e+JDBNeECakmccXrSFeBQuu5AV8SNkipFMMMw==" + }, + "Iced": { + "type": "Transitive", + "resolved": "1.21.0", + "contentHash": "dv5+81Q1TBQvVMSOOOmRcjJmvWcX3BZPZsIq31+RLc5cNft0IHAyNlkdb7ZarOWG913PyBoFDsDXoCIlKmLclg==" + }, + "Microsoft.CodeAnalysis.Analyzers": { + "type": "Transitive", + "resolved": "3.11.0", + "contentHash": "v/EW3UE8/lbEYHoC2Qq7AR/DnmvpgdtAMndfQNmpuIMx/Mto8L5JnuCfdBYtgvalQOtfNCnxFejxuRrryvUTsg==" + }, + "Microsoft.CodeAnalysis.Common": { + "type": "Transitive", + "resolved": "4.14.0", + "contentHash": "PC3tuwZYnC+idaPuoC/AZpEdwrtX7qFpmnrfQkgobGIWiYmGi5MCRtl5mx6QrfMGQpK78X2lfIEoZDLg/qnuHg==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.11.0" + } + }, + "Microsoft.CodeAnalysis.CSharp": { + "type": "Transitive", + "resolved": "4.14.0", + "contentHash": "568a6wcTivauIhbeWcCwfWwIn7UV7MeHEBvFB2uzGIpM2OhJ4eM/FZ8KS0yhPoNxnSpjGzz7x7CIjTxhslojQA==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "Microsoft.CodeAnalysis.Common": "[4.14.0]" + } + }, + "Microsoft.Diagnostics.NETCore.Client": { + "type": "Transitive", + "resolved": "0.2.510501", + "contentHash": "juoqJYMDs+lRrrZyOkXXMImJHneCF23cuvO4waFRd2Ds7j+ZuGIPbJm0Y/zz34BdeaGiiwGWraMUlln05W1PCQ==", + "dependencies": { + "Microsoft.Extensions.Logging": "6.0.0" + } + }, + "Microsoft.Diagnostics.Runtime": { + "type": "Transitive", + "resolved": "3.1.512801", + "contentHash": "0lMUDr2oxNZa28D6NH5BuSQEe5T9tZziIkvkD44YkkCGQXPJqvFjLq5ZQq1hYLl3RjQJrY+hR0jFgap+EWPDTw==", + "dependencies": { + "Microsoft.Diagnostics.NETCore.Client": "0.2.410101" + } + }, + "Microsoft.Diagnostics.Tracing.TraceEvent": { + "type": "Transitive", + "resolved": "3.1.21", + "contentHash": "/OrJFKaojSR6TkUKtwh8/qA9XWNtxLrXMqvEb89dBSKCWjaGVTbKMYodIUgF5deCEtmd6GXuRerciXGl5bhZ7Q==", + "dependencies": { + "Microsoft.Diagnostics.NETCore.Client": "0.2.510501" + } + }, + "Microsoft.DotNet.PlatformAbstractions": { + "type": "Transitive", + "resolved": "3.1.6", + "contentHash": "jek4XYaQ/PGUwDKKhwR8K47Uh1189PFzMeLqO83mXrXQVIpARZCcfuDedH50YDTepBkfijCZN5U/vZi++erxtg==" + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "k6PWQMuoBDGGHOQTtyois2u4AwyVcIwL2LaSLlTZQm2CYcJ1pxbt6jfAnpWmzENA/wfrYRI/X9DTLoUkE4AsLw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "xlzi2IYREJH3/m6+lUrQlujzX8wDitm4QGnUu6kUXTQAWPuZY8i+ticFJbzfqaetLA6KR/rO6Ew/HuYD+bxifg==" + }, + "Microsoft.Extensions.Logging": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "eIbyj40QDg1NDz0HBW0S5f3wrLVnKWnDJ/JtZ+yJDFnDj90VoPuoPmFkeaXrtu+0cKm5GRAwoDf+dBWXK0TUdg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "6.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", + "Microsoft.Extensions.Logging.Abstractions": "6.0.0", + "Microsoft.Extensions.Options": "6.0.0" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "/HggWBbTwy8TgebGSX5DBZ24ndhzi93sHUBDvP1IxbZD7FDokYzdAr6+vbWGjw2XAfR2EJ1sfKUotpjHnFWPxA==" + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "dzXN0+V1AyjOe2xcJ86Qbo233KHuLEY0njf/P2Kw8SfJU+d45HNS2ctJdnEnrWbM9Ye2eFgaC5Mj9otRMU6IsQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", + "Microsoft.Extensions.Primitives": "6.0.0" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "9+PnzmQFfEFNR9J2aDTfJGGupShHjOuGw4VUv+JB044biSHrnmCIMD+mJHmb2H7YryrfBEXDurxQ47gJZdCKNQ==" + }, + "Perfolizer": { + "type": "Transitive", + "resolved": "0.6.1", + "contentHash": "CR1QmWg4XYBd1Pb7WseP+sDmV8nGPwvmowKynExTqr3OuckIGVMhvmN4LC5PGzfXqDlR295+hz/T7syA1CxEqA==", + "dependencies": { + "Pragmastat": "3.2.4" + } + }, + "Pragmastat": { + "type": "Transitive", + "resolved": "3.2.4", + "contentHash": "I5qFifWw/gaTQT52MhzjZpkm/JPlfjSeO/DTZJjO7+hTKI+0aGRgOgZ3NN6D96dDuuqbIAZSeA5RimtHjqrA2A==" + }, + "System.CodeDom": { + "type": "Transitive", + "resolved": "9.0.5", + "contentHash": "cuzLM2MWutf9ZBEMPYYfd0DXwYdvntp7VCT6a/wvbKCa2ZuvGmW74xi+YBa2mrfEieAXqM4TNKlMmSnfAfpUoQ==" + }, + "System.Management": { + "type": "Transitive", + "resolved": "9.0.5", + "contentHash": "n6o9PZm9p25+zAzC3/48K0oHnaPKTInRrxqFq1fi/5TPbMLjuoCm/h//mS3cUmSy+9AO1Z+qsC/Ilt/ZFatv5Q==", + "dependencies": { + "System.CodeDom": "9.0.5" + } + }, + "arcus": { + "type": "Project" + } + } + } +} \ No newline at end of file diff --git a/src/Arcus.Benchmarks/run-benchmarks.ps1 b/src/Arcus.Benchmarks/run-benchmarks.ps1 new file mode 100644 index 0000000..a73c3fa --- /dev/null +++ b/src/Arcus.Benchmarks/run-benchmarks.ps1 @@ -0,0 +1,2 @@ +Set-Location $PSScriptRoot +dotnet run -c Release -- @args diff --git a/src/Arcus.Benchmarks/run-benchmarks.sh b/src/Arcus.Benchmarks/run-benchmarks.sh new file mode 100644 index 0000000..eefb631 --- /dev/null +++ b/src/Arcus.Benchmarks/run-benchmarks.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")" +dotnet run -c Release -- "$@" diff --git a/src/Arcus.DocExamples/AbstractIPAddressRangeExamples.cs b/src/Arcus.DocExamples/AbstractIPAddressRangeExamples.cs deleted file mode 100644 index 4e99c62..0000000 --- a/src/Arcus.DocExamples/AbstractIPAddressRangeExamples.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Globalization; -using System.Net; -using Xunit; - -namespace Arcus.DocExamples -{ - [Trait("Category", "Doc.Examples")] - public class AbstractIPAddressRangeExamples - { - [Fact] - public void IFormattable_Example() - { - // Arrange - var head = IPAddress.Parse("192.168.0.0"); - var tail = IPAddress.Parse("192.168.128.0"); - var ipAddressRange = new IPAddressRange(head, tail); - - const string expected = "192.168.0.0 - 192.168.128.0"; - - // Act - var formattableString = string.Format(CultureInfo.InvariantCulture, "{0:g}", ipAddressRange); - - // Assert - Assert.Equal(expected, formattableString); - } - } -} diff --git a/src/Arcus.DocExamples/Arcus.DocExamples.csproj b/src/Arcus.DocExamples/Arcus.DocExamples.csproj deleted file mode 100644 index 2467450..0000000 --- a/src/Arcus.DocExamples/Arcus.DocExamples.csproj +++ /dev/null @@ -1,61 +0,0 @@ - - - Documentation Example package for Arcus - The Production Tools Team - Sandia National Laboratories - Copyright 2025 National Technology & Engineering Solutions of Sandia, LLC (NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the U.S. Government retains certain rights in this software - - - net48;net8.0;net9.0;net10.0 - false - true - bin\ - $(OutputPath)$(AssemblyName).xml - - - - stylecop.json - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - diff --git a/src/Arcus.DocExamples/DocReferenceGenerator.cs b/src/Arcus.DocExamples/DocReferenceGenerator.cs deleted file mode 100644 index 29e8c5c..0000000 --- a/src/Arcus.DocExamples/DocReferenceGenerator.cs +++ /dev/null @@ -1,100 +0,0 @@ -using System.Text; -using Arcus.Utilities; -using Gulliver; -using Xunit; -using Xunit.Abstractions; - -namespace Arcus.DocExamples -{ - public class DocReferenceGenerator - { - #region Setup / Teardown - - public DocReferenceGenerator(ITestOutputHelper output) - { - this._output = output; - } - - private readonly ITestOutputHelper _output; - - #endregion - - [Fact] - public void IPv4CirdRefGen() - { - var sb = new StringBuilder(); - - sb.Append("CIDR") - .Append(',') - .Append("Network Prefix Address") - .Append(',') - .Append("Route Prefix") - .Append(',') - .Append("Netmask") - .Append(',') - .Append("Netmask (bits)") - .Append(',') - .Append("Address Count") - .Append(',') - .Append("Address Count 2^n") - .AppendLine(); - - for (var i = 32; i >= 0; i--) - { - var subnet = new Subnet(IPAddressUtilities.IPv4MaxAddress, i); - - sb.Append(subnet) - .Append(',') - .Append(subnet.NetworkPrefixAddress) - .Append(',') - .Append(subnet.RoutingPrefix) - .Append(',') - .Append(subnet.Netmask) - .Append(',') - .Append(subnet.Netmask.GetAddressBytes().ToString("b")) - .Append(',') - .Append(subnet.Length) - .Append(',') - .Append(32 - i) - .AppendLine(); - } - - this._output.WriteLine(sb.ToString()); - } - - [Fact] - public void IPv6CirdRefGen() - { - var sb = new StringBuilder(); - - sb.Append("CIDR") - .Append(',') - .Append("Network Prefix Address") - .Append(',') - .Append("Route Prefix") - .Append(',') - .Append("Address Count") - .Append(',') - .Append("Address Count 2^n") - .AppendLine(); - - for (var i = 128; i >= 0; i--) - { - var subnet = new Subnet(IPAddressUtilities.IPv6MaxAddress, i); - - sb.Append(subnet) - .Append(',') - .Append(subnet.NetworkPrefixAddress) - .Append(',') - .Append(subnet.RoutingPrefix) - .Append(',') - .Append(subnet.Length) - .Append(',') - .Append(128 - i) - .AppendLine(); - } - - this._output.WriteLine(sb.ToString()); - } - } -} diff --git a/src/Arcus.DocExamples/IPAddressConvertersExamples.cs b/src/Arcus.DocExamples/IPAddressConvertersExamples.cs deleted file mode 100644 index 6d9bdc8..0000000 --- a/src/Arcus.DocExamples/IPAddressConvertersExamples.cs +++ /dev/null @@ -1,201 +0,0 @@ -using System.Globalization; -using System.Linq; -using System.Net; -using System.Text; -using Arcus.Converters; -using Gulliver; -using Xunit; -using Xunit.Abstractions; - -namespace Arcus.DocExamples -{ - public class IPAddressConvertersExamples - { - #region Setup / Teardown - - public IPAddressConvertersExamples(ITestOutputHelper output) - { - this._output = output; - } - - private readonly ITestOutputHelper _output; - - #endregion - - [Fact] - public void NetmaskToCidrRoutePrefix_Example() - { - // equivalent byte value of 255.255.255.255 or 2^32 - var maxIPv4Bytes = Enumerable.Repeat((byte)0xFF, 4).ToArray(); - - // build all valid net masks - var allNetMasks = Enumerable - .Range(7, 10) - .Select(i => maxIPv4Bytes.ShiftBitsLeft(32 - i)) // use Gulliver to shift bits of byte array - .Select(b => new IPAddress(b)) - .ToArray(); - - var sb = new StringBuilder(); - - foreach (var netmask in allNetMasks) - { - var routePrefix = netmask.NetmaskToCidrRoutePrefix(); - - sb.Append(routePrefix) - .Append('\t') - .AppendFormat(CultureInfo.InvariantCulture, "{0,-15}", netmask) - .Append('\t') - .Append(netmask.GetAddressBytes().ToString("b")) // using Gulliver to print bytes as bits - .AppendLine(); - } - - this._output.WriteLine(sb.ToString()); - } - - [Fact] - public void ToBase85String_Example() - { - var addresses = new[] - { - "::", - "::ffff", - "1080:0:0:0:8:800:200C:417A", // specific example from RFC 1924 - "ffff::", - "ffff::0102:0304", - "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", - } - .Select(IPAddress.Parse) - .ToArray(); - - var sb = new StringBuilder(); - - foreach (var address in addresses) - { - var base85String = address.ToBase85String(); - - sb.AppendFormat(CultureInfo.InvariantCulture, "{0,-40}", address) - .Append("\t=>\t") - .Append(base85String) - .AppendLine(); - } - - this._output.WriteLine(sb.ToString()); - } - - [Fact] - public void ToDottedQuadString_Example() - { - var addresses = new[] - { - "::", - "::ffff", - "a:b:c::ff00:ff", - "ffff::", - "ffff::0102:0304", - "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", - } - .Select(IPAddress.Parse) - .ToArray(); - - var sb = new StringBuilder(); - - foreach (var address in addresses) - { - var dottedQuadString = address.ToDottedQuadString(); - - sb.AppendFormat(CultureInfo.InvariantCulture, "{0,-40}", address) - .Append("\t=>\t") - .Append(dottedQuadString) - .AppendLine(); - } - - this._output.WriteLine(sb.ToString()); - } - - [Fact] - public void ToHexString_Example() - { - var addresses = new[] - { - "::", - "::ffff", - "10.1.1.1", - "192.168.1.1", - "255.255.255.255", - "ffff::", - "ffff::0102:0304", - "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", - } - .Select(IPAddress.Parse) - .ToArray(); - - var sb = new StringBuilder(); - - foreach (var address in addresses) - { - var hexString = address.ToHexString(); - - sb.AppendFormat(CultureInfo.InvariantCulture, "{0,-40}", address) - .Append("\t=>\t") - .Append(hexString) - .AppendLine(); - } - - this._output.WriteLine(sb.ToString()); - } - - [Fact] - public void ToNumericString_Example() - { - var addresses = new[] - { - "::", - "::ffff", - "10.1.1.1", - "192.168.1.1", - "255.255.255.255", - "ffff::", - "ffff::0102:0304", - "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", - } - .Select(IPAddress.Parse) - .ToArray(); - - var sb = new StringBuilder(); - - foreach (var address in addresses) - { - var numericString = address.ToNumericString(); - - sb.AppendFormat(CultureInfo.InvariantCulture, "{0,-40}", address) - .Append("\t=>\t") - .Append(numericString) - .AppendLine(); - } - - this._output.WriteLine(sb.ToString()); - } - - [Fact] - public void ToUncompressedString_Example() - { - var addresses = new[] { "::", "::ffff", "10.1.1.1", "192.168.1.1", "255.255.255.255", "ffff::", "ffff::0102:0304" } - .Select(IPAddress.Parse) - .ToArray(); - - var sb = new StringBuilder(); - - foreach (var address in addresses) - { - var uncompressedString = address.ToUncompressedString(); - - sb.AppendFormat(CultureInfo.InvariantCulture, "{0,-40}", address) - .Append("\t=>\t") - .Append(uncompressedString) - .AppendLine(); - } - - this._output.WriteLine(sb.ToString()); - } - } -} diff --git a/src/Arcus.DocExamples/IPAddressRangeExamples.cs b/src/Arcus.DocExamples/IPAddressRangeExamples.cs deleted file mode 100644 index 7e49756..0000000 --- a/src/Arcus.DocExamples/IPAddressRangeExamples.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Linq; -using System.Net; -using Xunit; - -namespace Arcus.DocExamples -{ - [Trait("Category", "Doc.Examples")] - public class IPAddressRangeExamples - { - [Fact] - public void TryCollapseAll_Example() - { - // Arrange - var ranges = new[] - { - new IPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.5")), - new IPAddressRange(IPAddress.Parse("192.168.1.6"), IPAddress.Parse("192.168.1.7")), - new IPAddressRange(IPAddress.Parse("192.168.1.8"), IPAddress.Parse("192.168.1.20")), - }; - - // Act - var success = IPAddressRange.TryCollapseAll(ranges, out var results); - var resultList = results?.ToList(); - - // Assert - Assert.True(success); - Assert.NotNull(results); - Assert.Single(resultList); - - var result = resultList.Single(); - - Assert.Equal(IPAddress.Parse("192.168.1.0"), result.Head); - Assert.Equal(IPAddress.Parse("192.168.1.20"), result.Tail); - } - } -} diff --git a/src/Arcus.DocExamples/SubnetExamples.cs b/src/Arcus.DocExamples/SubnetExamples.cs deleted file mode 100644 index a9199ad..0000000 --- a/src/Arcus.DocExamples/SubnetExamples.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System.Linq; -using System.Net; -using Xunit; - -namespace Arcus.DocExamples -{ - /// - /// Subnet Examples - /// - [Trait("Category", "Doc.Examples")] - public class SubnetExamples - { - [Fact] - public void Address_RoutePrefix_Subnet_Example() - { - // Arrange - var ipAddress = IPAddress.Parse("192.168.1.1"); - const int routePrefix = 24; - - // Act - var subnet = new Subnet(ipAddress, routePrefix); - - // Assert - Assert.False(subnet.IsSingleIP); - - Assert.Equal(256, subnet.Length); - Assert.Equal("192.168.1.0", subnet.Head.ToString()); - Assert.Equal("192.168.1.255", subnet.Tail.ToString()); - Assert.Equal(24, subnet.RoutingPrefix); - Assert.Equal("192.168.1.0/24", subnet.ToString()); - } - - [Fact] - public void Contains_Example() - { - // Arrange - var subnetA = Subnet.Parse("192.168.1.0", 8); // 192.0.0.0 - 192.255.255.255 - var subnetB = Subnet.Parse("192.168.0.0", 16); // 192.168.0.0 - 192.168.255.255 - var subnetC = Subnet.Parse("255.0.0.0", 8); // 255.0.0.0 - 255.255.255.255 - - // Act - // Assert - Assert.True(subnetA.Contains(subnetB)); - Assert.False(subnetA.Contains(subnetC)); - } - - [Fact] - public void Overlaps_Example() - { - // Arrange - var ipv4SubnetA = Subnet.Parse("255.255.0.0", 16); - var ipv4SubnetB = Subnet.Parse("0.0.0.0", 0); - - var ipv6SubnetA = Subnet.Parse("::", 0); - var ipv6SubnetB = Subnet.Parse("abcd:ef01::", 64); - - // Act - Assert.True(ipv4SubnetA.Overlaps(ipv4SubnetB)); - Assert.True(ipv4SubnetB.Overlaps(ipv4SubnetA)); - - Assert.True(ipv6SubnetA.Overlaps(ipv6SubnetB)); - - Assert.False(ipv6SubnetA.Overlaps(ipv4SubnetA)); - } - - [Fact] - public void Single_Address_Subnet_Example() - { - // Arrange - var ipAddress = IPAddress.Parse("192.168.1.1"); - - // Act - var subnet = new Subnet(ipAddress); - - // Assert - Assert.Equal(1, subnet.Length); - Assert.Equal(ipAddress, subnet.Single()); - Assert.True(subnet.IsSingleIP); - Assert.Equal("192.168.1.1/32", subnet.ToString()); - } - } -} diff --git a/src/Arcus.DocExamples/SubnetUtilitiesExamples.cs b/src/Arcus.DocExamples/SubnetUtilitiesExamples.cs deleted file mode 100644 index 00f3c15..0000000 --- a/src/Arcus.DocExamples/SubnetUtilitiesExamples.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System.Linq; -using System.Net; -using Arcus.Utilities; -using Xunit; - -namespace Arcus.DocExamples -{ - [Trait("Category", "Doc.Examples")] - public class SubnetUtilitiesExamples - { - [Fact] - public void FewestConsecutiveSubnetsFor_Example() - { - // Arrange - var left = IPAddress.Parse("192.168.1.3"); - var right = IPAddress.Parse("192.168.1.5"); - - // Act - var result = SubnetUtilities.FewestConsecutiveSubnetsFor(left, right).ToArray(); - - // Assert - Assert.Equal(2, result.Length); - Assert.Contains(Subnet.Parse("192.168.1.4/31"), result); - Assert.Contains(Subnet.Parse("192.168.1.3/32"), result); - } - - [Fact] - public void LargestSubnet_Example() - { - // Arrange - var tall = Subnet.Parse("255.255.255.254/31"); // 2^1 = 2 - var grande = Subnet.Parse("192.168.1.0/24"); // 2^8 = 256 - var vente = Subnet.Parse("10.10.0.0/16"); // 2^16 = 65536 - var trenta = Subnet.Parse("16.240.0.0/12"); // 2^20 = 1048576 - - var subnets = new[] { tall, grande, vente, trenta }; - - // Act - var result = SubnetUtilities.LargestSubnet(subnets); - - // Assert - Assert.Equal(trenta, result); - } - - [Fact] - public void SmallestSubnet_Example() - { - // Arrange - var tall = Subnet.Parse("255.255.255.254/31"); // 2^1 = 2 - var grande = Subnet.Parse("192.168.1.0/24"); // 2^8 = 256 - var vente = Subnet.Parse("10.10.0.0/16"); // 2^16 = 65536 - var trenta = Subnet.Parse("16.240.0.0/12"); // 2^20 = 1048576 - - var subnets = new[] { tall, grande, vente, trenta }; - - // Act - var result = SubnetUtilities.SmallestSubnet(subnets); - - // Assert - Assert.Equal(tall, result); - } - } -} diff --git a/src/Arcus.DocExamples/packages.lock.json b/src/Arcus.DocExamples/packages.lock.json deleted file mode 100644 index a5cf905..0000000 --- a/src/Arcus.DocExamples/packages.lock.json +++ /dev/null @@ -1,599 +0,0 @@ -{ - "version": 1, - "dependencies": { - ".NETFramework,Version=v4.8": { - "AsyncFixer": { - "type": "Direct", - "requested": "[1.6.0, )", - "resolved": "1.6.0", - "contentHash": "/Xfs9H3UMfEv64cwT+C/JrTRp4w08BmPuFbj0ageadCHpx6rxYJxAU2C6sEqRFG22xmGk5cX9ewzoiiehWVHOw==" - }, - "Microsoft.NET.Test.Sdk": { - "type": "Direct", - "requested": "[18.0.1, )", - "resolved": "18.0.1", - "contentHash": "WNpu6vI2rA0pXY4r7NKxCN16XRWl5uHu6qjuyVLoDo6oYEggIQefrMjkRuibQHm/NslIUNCcKftvoWAN80MSAg==", - "dependencies": { - "Microsoft.CodeCoverage": "18.0.1" - } - }, - "Microsoft.VisualStudio.Threading.Analyzers": { - "type": "Direct", - "requested": "[17.14.15, )", - "resolved": "17.14.15", - "contentHash": "mXQPJsbuUD2ydq4/ffd8h8tSOFCXec+2xJOVNCvXjuMOq/+5EKHq3D2m2MC2+nUaXeFMSt66VS/J4HdKBixgcw==" - }, - "Roslynator.Analyzers": { - "type": "Direct", - "requested": "[4.14.1, )", - "resolved": "4.14.1", - "contentHash": "yMSjze/xMYDF6PCE60/ULWx0tttNyKAndw2KijNxbKil0FX8nvDeEneDZGma8Uifk17RlfZqIXxf1mmBmhRHjg==" - }, - "Roslynator.CodeAnalysis.Analyzers": { - "type": "Direct", - "requested": "[4.14.1, )", - "resolved": "4.14.1", - "contentHash": "axessewcgSLn+UAtKyMKPgXpzIldnivynjRe9qsRjrVtrxjctDpFDEjAkcbpDjhR0ccm8/+ihOna60wnXJ/66A==" - }, - "Roslynator.Formatting.Analyzers": { - "type": "Direct", - "requested": "[4.14.1, )", - "resolved": "4.14.1", - "contentHash": "hALZsyELJXhj7WQw7Q7nLI7sELre8AlQIMyGxXpmpK+iBqShPcWHuJMGjIsTMa/bC8Q5xyQ4iFfRtm0bP1zisQ==" - }, - "SonarAnalyzer.CSharp": { - "type": "Direct", - "requested": "[10.15.0.120848, )", - "resolved": "10.15.0.120848", - "contentHash": "1hM3HVRl5jdC/ZBDu+G7CCYLXRGe/QaP01Zy+c9ETPhY7lWD8g8HiefY6sGaH0T3CJ4wAy0/waGgQTh0TYy0oQ==" - }, - "StyleCop.Analyzers": { - "type": "Direct", - "requested": "[1.1.118, )", - "resolved": "1.1.118", - "contentHash": "Onx6ovGSqXSK07n/0eM3ZusiNdB6cIlJdabQhWGgJp3Vooy9AaLS/tigeybOJAobqbtggTamoWndz72JscZBvw==" - }, - "xunit": { - "type": "Direct", - "requested": "[2.9.3, )", - "resolved": "2.9.3", - "contentHash": "TlXQBinK35LpOPKHAqbLY4xlEen9TBafjs0V5KnA4wZsoQLQJiirCR4CbIXvOH8NzkW4YeJKP5P/Bnrodm0h9Q==", - "dependencies": { - "xunit.analyzers": "1.18.0", - "xunit.assert": "2.9.3", - "xunit.core": "[2.9.3]" - } - }, - "xunit.runner.visualstudio": { - "type": "Direct", - "requested": "[3.1.5, )", - "resolved": "3.1.5", - "contentHash": "tKi7dSTwP4m5m9eXPM2Ime4Kn7xNf4x4zT9sdLO/G4hZVnQCRiMTWoSZqI/pYTVeI27oPPqHBKYI/DjJ9GsYgA==", - "dependencies": { - "Microsoft.TestPlatform.ObjectModel": "17.13.0" - } - }, - "Gulliver": { - "type": "Transitive", - "resolved": "3.0.0", - "contentHash": "J232PI4ccScfLKt59UzyHBRZLcAJpyJ+N/at1fFF/XxBKwjl8idDVPkMU3FZkwaaU/e3Ert8/EnGLg5CxmQ8dQ==" - }, - "Microsoft.Bcl.HashCode": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "GI4jcoi6eC9ZhNOQylIBaWOQjyGaR8T6N3tC1u8p3EXfndLCVNNWa+Zp+ocjvvS3kNBN09Zma2HXL0ezO0dRfw==" - }, - "Microsoft.CodeCoverage": { - "type": "Transitive", - "resolved": "18.0.1", - "contentHash": "O+utSr97NAJowIQT/OVp3Lh9QgW/wALVTP4RG1m2AfFP4IyJmJz0ZBmFJUsRQiAPgq6IRC0t8AAzsiPIsaUDEA==" - }, - "Microsoft.TestPlatform.ObjectModel": { - "type": "Transitive", - "resolved": "17.13.0", - "contentHash": "bt0E0Dx+iqW97o4A59RCmUmz/5NarJ7LRL+jXbSHod72ibL5XdNm1Ke+UO5tFhBG4VwHLcSjqq9BUSblGNWamw==", - "dependencies": { - "System.Reflection.Metadata": "1.6.0" - } - }, - "System.Collections.Immutable": { - "type": "Transitive", - "resolved": "1.5.0", - "contentHash": "EXKiDFsChZW0RjrZ4FYHu9aW6+P4MCgEDCklsVseRfhoO0F+dXeMSsMRAlVXIo06kGJ/zv+2w1a2uc2+kxxSaQ==" - }, - "System.Reflection.Metadata": { - "type": "Transitive", - "resolved": "1.6.0", - "contentHash": "COC1aiAJjCoA5GBF+QKL2uLqEBew4JsCkQmoHKbN3TlOZKa2fKLz5CpiRQKDz0RsAOEGsVKqOD5bomsXq/4STQ==", - "dependencies": { - "System.Collections.Immutable": "1.5.0" - } - }, - "xunit.abstractions": { - "type": "Transitive", - "resolved": "2.0.3", - "contentHash": "pot1I4YOxlWjIb5jmwvvQNbTrZ3lJQ+jUGkGjWE3hEFM0l5gOnBWS+H3qsex68s5cO52g+44vpGzhAt+42vwKg==" - }, - "xunit.analyzers": { - "type": "Transitive", - "resolved": "1.18.0", - "contentHash": "OtFMHN8yqIcYP9wcVIgJrq01AfTxijjAqVDy/WeQVSyrDC1RzBWeQPztL49DN2syXRah8TYnfvk035s7L95EZQ==" - }, - "xunit.assert": { - "type": "Transitive", - "resolved": "2.9.3", - "contentHash": "/Kq28fCE7MjOV42YLVRAJzRF0WmEqsmflm0cfpMjGtzQ2lR5mYVj1/i0Y8uDAOLczkL3/jArrwehfMD0YogMAA==" - }, - "xunit.core": { - "type": "Transitive", - "resolved": "2.9.3", - "contentHash": "BiAEvqGvyme19wE0wTKdADH+NloYqikiU0mcnmiNyXaF9HyHmE6sr/3DC5vnBkgsWaE6yPyWszKSPSApWdRVeQ==", - "dependencies": { - "xunit.extensibility.core": "[2.9.3]", - "xunit.extensibility.execution": "[2.9.3]" - } - }, - "xunit.extensibility.core": { - "type": "Transitive", - "resolved": "2.9.3", - "contentHash": "kf3si0YTn2a8J8eZNb+zFpwfoyvIrQ7ivNk5ZYA5yuYk1bEtMe4DxJ2CF/qsRgmEnDr7MnW1mxylBaHTZ4qErA==", - "dependencies": { - "xunit.abstractions": "2.0.3" - } - }, - "xunit.extensibility.execution": { - "type": "Transitive", - "resolved": "2.9.3", - "contentHash": "yMb6vMESlSrE3Wfj7V6cjQ3S4TXdXpRqYeNEI3zsX31uTsGMJjEw6oD5F5u1cHnMptjhEECnmZSsPxB6ChZHDQ==", - "dependencies": { - "xunit.extensibility.core": "[2.9.3]" - } - }, - "arcus": { - "type": "Project", - "dependencies": { - "Gulliver": "[3.0.0, )", - "Microsoft.Bcl.HashCode": "[6.0.0, )" - } - } - }, - "net10.0": { - "AsyncFixer": { - "type": "Direct", - "requested": "[1.6.0, )", - "resolved": "1.6.0", - "contentHash": "/Xfs9H3UMfEv64cwT+C/JrTRp4w08BmPuFbj0ageadCHpx6rxYJxAU2C6sEqRFG22xmGk5cX9ewzoiiehWVHOw==" - }, - "Microsoft.NET.Test.Sdk": { - "type": "Direct", - "requested": "[18.0.1, )", - "resolved": "18.0.1", - "contentHash": "WNpu6vI2rA0pXY4r7NKxCN16XRWl5uHu6qjuyVLoDo6oYEggIQefrMjkRuibQHm/NslIUNCcKftvoWAN80MSAg==", - "dependencies": { - "Microsoft.CodeCoverage": "18.0.1", - "Microsoft.TestPlatform.TestHost": "18.0.1" - } - }, - "Microsoft.VisualStudio.Threading.Analyzers": { - "type": "Direct", - "requested": "[17.14.15, )", - "resolved": "17.14.15", - "contentHash": "mXQPJsbuUD2ydq4/ffd8h8tSOFCXec+2xJOVNCvXjuMOq/+5EKHq3D2m2MC2+nUaXeFMSt66VS/J4HdKBixgcw==" - }, - "Roslynator.Analyzers": { - "type": "Direct", - "requested": "[4.14.1, )", - "resolved": "4.14.1", - "contentHash": "yMSjze/xMYDF6PCE60/ULWx0tttNyKAndw2KijNxbKil0FX8nvDeEneDZGma8Uifk17RlfZqIXxf1mmBmhRHjg==" - }, - "Roslynator.CodeAnalysis.Analyzers": { - "type": "Direct", - "requested": "[4.14.1, )", - "resolved": "4.14.1", - "contentHash": "axessewcgSLn+UAtKyMKPgXpzIldnivynjRe9qsRjrVtrxjctDpFDEjAkcbpDjhR0ccm8/+ihOna60wnXJ/66A==" - }, - "Roslynator.Formatting.Analyzers": { - "type": "Direct", - "requested": "[4.14.1, )", - "resolved": "4.14.1", - "contentHash": "hALZsyELJXhj7WQw7Q7nLI7sELre8AlQIMyGxXpmpK+iBqShPcWHuJMGjIsTMa/bC8Q5xyQ4iFfRtm0bP1zisQ==" - }, - "SonarAnalyzer.CSharp": { - "type": "Direct", - "requested": "[10.15.0.120848, )", - "resolved": "10.15.0.120848", - "contentHash": "1hM3HVRl5jdC/ZBDu+G7CCYLXRGe/QaP01Zy+c9ETPhY7lWD8g8HiefY6sGaH0T3CJ4wAy0/waGgQTh0TYy0oQ==" - }, - "StyleCop.Analyzers": { - "type": "Direct", - "requested": "[1.1.118, )", - "resolved": "1.1.118", - "contentHash": "Onx6ovGSqXSK07n/0eM3ZusiNdB6cIlJdabQhWGgJp3Vooy9AaLS/tigeybOJAobqbtggTamoWndz72JscZBvw==" - }, - "xunit": { - "type": "Direct", - "requested": "[2.9.3, )", - "resolved": "2.9.3", - "contentHash": "TlXQBinK35LpOPKHAqbLY4xlEen9TBafjs0V5KnA4wZsoQLQJiirCR4CbIXvOH8NzkW4YeJKP5P/Bnrodm0h9Q==", - "dependencies": { - "xunit.analyzers": "1.18.0", - "xunit.assert": "2.9.3", - "xunit.core": "[2.9.3]" - } - }, - "xunit.runner.visualstudio": { - "type": "Direct", - "requested": "[3.1.5, )", - "resolved": "3.1.5", - "contentHash": "tKi7dSTwP4m5m9eXPM2Ime4Kn7xNf4x4zT9sdLO/G4hZVnQCRiMTWoSZqI/pYTVeI27oPPqHBKYI/DjJ9GsYgA==" - }, - "Gulliver": { - "type": "Transitive", - "resolved": "3.0.0", - "contentHash": "J232PI4ccScfLKt59UzyHBRZLcAJpyJ+N/at1fFF/XxBKwjl8idDVPkMU3FZkwaaU/e3Ert8/EnGLg5CxmQ8dQ==" - }, - "Microsoft.CodeCoverage": { - "type": "Transitive", - "resolved": "18.0.1", - "contentHash": "O+utSr97NAJowIQT/OVp3Lh9QgW/wALVTP4RG1m2AfFP4IyJmJz0ZBmFJUsRQiAPgq6IRC0t8AAzsiPIsaUDEA==" - }, - "Microsoft.TestPlatform.ObjectModel": { - "type": "Transitive", - "resolved": "18.0.1", - "contentHash": "qT/mwMcLF9BieRkzOBPL2qCopl8hQu6A1P7JWAoj/FMu5i9vds/7cjbJ/LLtaiwWevWLAeD5v5wjQJ/l6jvhWQ==" - }, - "Microsoft.TestPlatform.TestHost": { - "type": "Transitive", - "resolved": "18.0.1", - "contentHash": "uDJKAEjFTaa2wHdWlfo6ektyoh+WD4/Eesrwb4FpBFKsLGehhACVnwwTI4qD3FrIlIEPlxdXg3SyrYRIcO+RRQ==", - "dependencies": { - "Microsoft.TestPlatform.ObjectModel": "18.0.1", - "Newtonsoft.Json": "13.0.3" - } - }, - "Newtonsoft.Json": { - "type": "Transitive", - "resolved": "13.0.3", - "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" - }, - "xunit.abstractions": { - "type": "Transitive", - "resolved": "2.0.3", - "contentHash": "pot1I4YOxlWjIb5jmwvvQNbTrZ3lJQ+jUGkGjWE3hEFM0l5gOnBWS+H3qsex68s5cO52g+44vpGzhAt+42vwKg==" - }, - "xunit.analyzers": { - "type": "Transitive", - "resolved": "1.18.0", - "contentHash": "OtFMHN8yqIcYP9wcVIgJrq01AfTxijjAqVDy/WeQVSyrDC1RzBWeQPztL49DN2syXRah8TYnfvk035s7L95EZQ==" - }, - "xunit.assert": { - "type": "Transitive", - "resolved": "2.9.3", - "contentHash": "/Kq28fCE7MjOV42YLVRAJzRF0WmEqsmflm0cfpMjGtzQ2lR5mYVj1/i0Y8uDAOLczkL3/jArrwehfMD0YogMAA==" - }, - "xunit.core": { - "type": "Transitive", - "resolved": "2.9.3", - "contentHash": "BiAEvqGvyme19wE0wTKdADH+NloYqikiU0mcnmiNyXaF9HyHmE6sr/3DC5vnBkgsWaE6yPyWszKSPSApWdRVeQ==", - "dependencies": { - "xunit.extensibility.core": "[2.9.3]", - "xunit.extensibility.execution": "[2.9.3]" - } - }, - "xunit.extensibility.core": { - "type": "Transitive", - "resolved": "2.9.3", - "contentHash": "kf3si0YTn2a8J8eZNb+zFpwfoyvIrQ7ivNk5ZYA5yuYk1bEtMe4DxJ2CF/qsRgmEnDr7MnW1mxylBaHTZ4qErA==", - "dependencies": { - "xunit.abstractions": "2.0.3" - } - }, - "xunit.extensibility.execution": { - "type": "Transitive", - "resolved": "2.9.3", - "contentHash": "yMb6vMESlSrE3Wfj7V6cjQ3S4TXdXpRqYeNEI3zsX31uTsGMJjEw6oD5F5u1cHnMptjhEECnmZSsPxB6ChZHDQ==", - "dependencies": { - "xunit.extensibility.core": "[2.9.3]" - } - }, - "arcus": { - "type": "Project", - "dependencies": { - "Gulliver": "[3.0.0, )" - } - } - }, - "net8.0": { - "AsyncFixer": { - "type": "Direct", - "requested": "[1.6.0, )", - "resolved": "1.6.0", - "contentHash": "/Xfs9H3UMfEv64cwT+C/JrTRp4w08BmPuFbj0ageadCHpx6rxYJxAU2C6sEqRFG22xmGk5cX9ewzoiiehWVHOw==" - }, - "Microsoft.NET.Test.Sdk": { - "type": "Direct", - "requested": "[18.0.1, )", - "resolved": "18.0.1", - "contentHash": "WNpu6vI2rA0pXY4r7NKxCN16XRWl5uHu6qjuyVLoDo6oYEggIQefrMjkRuibQHm/NslIUNCcKftvoWAN80MSAg==", - "dependencies": { - "Microsoft.CodeCoverage": "18.0.1", - "Microsoft.TestPlatform.TestHost": "18.0.1" - } - }, - "Microsoft.VisualStudio.Threading.Analyzers": { - "type": "Direct", - "requested": "[17.14.15, )", - "resolved": "17.14.15", - "contentHash": "mXQPJsbuUD2ydq4/ffd8h8tSOFCXec+2xJOVNCvXjuMOq/+5EKHq3D2m2MC2+nUaXeFMSt66VS/J4HdKBixgcw==" - }, - "Roslynator.Analyzers": { - "type": "Direct", - "requested": "[4.14.1, )", - "resolved": "4.14.1", - "contentHash": "yMSjze/xMYDF6PCE60/ULWx0tttNyKAndw2KijNxbKil0FX8nvDeEneDZGma8Uifk17RlfZqIXxf1mmBmhRHjg==" - }, - "Roslynator.CodeAnalysis.Analyzers": { - "type": "Direct", - "requested": "[4.14.1, )", - "resolved": "4.14.1", - "contentHash": "axessewcgSLn+UAtKyMKPgXpzIldnivynjRe9qsRjrVtrxjctDpFDEjAkcbpDjhR0ccm8/+ihOna60wnXJ/66A==" - }, - "Roslynator.Formatting.Analyzers": { - "type": "Direct", - "requested": "[4.14.1, )", - "resolved": "4.14.1", - "contentHash": "hALZsyELJXhj7WQw7Q7nLI7sELre8AlQIMyGxXpmpK+iBqShPcWHuJMGjIsTMa/bC8Q5xyQ4iFfRtm0bP1zisQ==" - }, - "SonarAnalyzer.CSharp": { - "type": "Direct", - "requested": "[10.15.0.120848, )", - "resolved": "10.15.0.120848", - "contentHash": "1hM3HVRl5jdC/ZBDu+G7CCYLXRGe/QaP01Zy+c9ETPhY7lWD8g8HiefY6sGaH0T3CJ4wAy0/waGgQTh0TYy0oQ==" - }, - "StyleCop.Analyzers": { - "type": "Direct", - "requested": "[1.1.118, )", - "resolved": "1.1.118", - "contentHash": "Onx6ovGSqXSK07n/0eM3ZusiNdB6cIlJdabQhWGgJp3Vooy9AaLS/tigeybOJAobqbtggTamoWndz72JscZBvw==" - }, - "xunit": { - "type": "Direct", - "requested": "[2.9.3, )", - "resolved": "2.9.3", - "contentHash": "TlXQBinK35LpOPKHAqbLY4xlEen9TBafjs0V5KnA4wZsoQLQJiirCR4CbIXvOH8NzkW4YeJKP5P/Bnrodm0h9Q==", - "dependencies": { - "xunit.analyzers": "1.18.0", - "xunit.assert": "2.9.3", - "xunit.core": "[2.9.3]" - } - }, - "xunit.runner.visualstudio": { - "type": "Direct", - "requested": "[3.1.5, )", - "resolved": "3.1.5", - "contentHash": "tKi7dSTwP4m5m9eXPM2Ime4Kn7xNf4x4zT9sdLO/G4hZVnQCRiMTWoSZqI/pYTVeI27oPPqHBKYI/DjJ9GsYgA==" - }, - "Gulliver": { - "type": "Transitive", - "resolved": "3.0.0", - "contentHash": "J232PI4ccScfLKt59UzyHBRZLcAJpyJ+N/at1fFF/XxBKwjl8idDVPkMU3FZkwaaU/e3Ert8/EnGLg5CxmQ8dQ==" - }, - "Microsoft.CodeCoverage": { - "type": "Transitive", - "resolved": "18.0.1", - "contentHash": "O+utSr97NAJowIQT/OVp3Lh9QgW/wALVTP4RG1m2AfFP4IyJmJz0ZBmFJUsRQiAPgq6IRC0t8AAzsiPIsaUDEA==" - }, - "Microsoft.TestPlatform.ObjectModel": { - "type": "Transitive", - "resolved": "18.0.1", - "contentHash": "qT/mwMcLF9BieRkzOBPL2qCopl8hQu6A1P7JWAoj/FMu5i9vds/7cjbJ/LLtaiwWevWLAeD5v5wjQJ/l6jvhWQ==" - }, - "Microsoft.TestPlatform.TestHost": { - "type": "Transitive", - "resolved": "18.0.1", - "contentHash": "uDJKAEjFTaa2wHdWlfo6ektyoh+WD4/Eesrwb4FpBFKsLGehhACVnwwTI4qD3FrIlIEPlxdXg3SyrYRIcO+RRQ==", - "dependencies": { - "Microsoft.TestPlatform.ObjectModel": "18.0.1", - "Newtonsoft.Json": "13.0.3" - } - }, - "Newtonsoft.Json": { - "type": "Transitive", - "resolved": "13.0.3", - "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" - }, - "xunit.abstractions": { - "type": "Transitive", - "resolved": "2.0.3", - "contentHash": "pot1I4YOxlWjIb5jmwvvQNbTrZ3lJQ+jUGkGjWE3hEFM0l5gOnBWS+H3qsex68s5cO52g+44vpGzhAt+42vwKg==" - }, - "xunit.analyzers": { - "type": "Transitive", - "resolved": "1.18.0", - "contentHash": "OtFMHN8yqIcYP9wcVIgJrq01AfTxijjAqVDy/WeQVSyrDC1RzBWeQPztL49DN2syXRah8TYnfvk035s7L95EZQ==" - }, - "xunit.assert": { - "type": "Transitive", - "resolved": "2.9.3", - "contentHash": "/Kq28fCE7MjOV42YLVRAJzRF0WmEqsmflm0cfpMjGtzQ2lR5mYVj1/i0Y8uDAOLczkL3/jArrwehfMD0YogMAA==" - }, - "xunit.core": { - "type": "Transitive", - "resolved": "2.9.3", - "contentHash": "BiAEvqGvyme19wE0wTKdADH+NloYqikiU0mcnmiNyXaF9HyHmE6sr/3DC5vnBkgsWaE6yPyWszKSPSApWdRVeQ==", - "dependencies": { - "xunit.extensibility.core": "[2.9.3]", - "xunit.extensibility.execution": "[2.9.3]" - } - }, - "xunit.extensibility.core": { - "type": "Transitive", - "resolved": "2.9.3", - "contentHash": "kf3si0YTn2a8J8eZNb+zFpwfoyvIrQ7ivNk5ZYA5yuYk1bEtMe4DxJ2CF/qsRgmEnDr7MnW1mxylBaHTZ4qErA==", - "dependencies": { - "xunit.abstractions": "2.0.3" - } - }, - "xunit.extensibility.execution": { - "type": "Transitive", - "resolved": "2.9.3", - "contentHash": "yMb6vMESlSrE3Wfj7V6cjQ3S4TXdXpRqYeNEI3zsX31uTsGMJjEw6oD5F5u1cHnMptjhEECnmZSsPxB6ChZHDQ==", - "dependencies": { - "xunit.extensibility.core": "[2.9.3]" - } - }, - "arcus": { - "type": "Project", - "dependencies": { - "Gulliver": "[3.0.0, )" - } - } - }, - "net9.0": { - "AsyncFixer": { - "type": "Direct", - "requested": "[1.6.0, )", - "resolved": "1.6.0", - "contentHash": "/Xfs9H3UMfEv64cwT+C/JrTRp4w08BmPuFbj0ageadCHpx6rxYJxAU2C6sEqRFG22xmGk5cX9ewzoiiehWVHOw==" - }, - "Microsoft.NET.Test.Sdk": { - "type": "Direct", - "requested": "[18.0.1, )", - "resolved": "18.0.1", - "contentHash": "WNpu6vI2rA0pXY4r7NKxCN16XRWl5uHu6qjuyVLoDo6oYEggIQefrMjkRuibQHm/NslIUNCcKftvoWAN80MSAg==", - "dependencies": { - "Microsoft.CodeCoverage": "18.0.1", - "Microsoft.TestPlatform.TestHost": "18.0.1" - } - }, - "Microsoft.VisualStudio.Threading.Analyzers": { - "type": "Direct", - "requested": "[17.14.15, )", - "resolved": "17.14.15", - "contentHash": "mXQPJsbuUD2ydq4/ffd8h8tSOFCXec+2xJOVNCvXjuMOq/+5EKHq3D2m2MC2+nUaXeFMSt66VS/J4HdKBixgcw==" - }, - "Roslynator.Analyzers": { - "type": "Direct", - "requested": "[4.14.1, )", - "resolved": "4.14.1", - "contentHash": "yMSjze/xMYDF6PCE60/ULWx0tttNyKAndw2KijNxbKil0FX8nvDeEneDZGma8Uifk17RlfZqIXxf1mmBmhRHjg==" - }, - "Roslynator.CodeAnalysis.Analyzers": { - "type": "Direct", - "requested": "[4.14.1, )", - "resolved": "4.14.1", - "contentHash": "axessewcgSLn+UAtKyMKPgXpzIldnivynjRe9qsRjrVtrxjctDpFDEjAkcbpDjhR0ccm8/+ihOna60wnXJ/66A==" - }, - "Roslynator.Formatting.Analyzers": { - "type": "Direct", - "requested": "[4.14.1, )", - "resolved": "4.14.1", - "contentHash": "hALZsyELJXhj7WQw7Q7nLI7sELre8AlQIMyGxXpmpK+iBqShPcWHuJMGjIsTMa/bC8Q5xyQ4iFfRtm0bP1zisQ==" - }, - "SonarAnalyzer.CSharp": { - "type": "Direct", - "requested": "[10.15.0.120848, )", - "resolved": "10.15.0.120848", - "contentHash": "1hM3HVRl5jdC/ZBDu+G7CCYLXRGe/QaP01Zy+c9ETPhY7lWD8g8HiefY6sGaH0T3CJ4wAy0/waGgQTh0TYy0oQ==" - }, - "StyleCop.Analyzers": { - "type": "Direct", - "requested": "[1.1.118, )", - "resolved": "1.1.118", - "contentHash": "Onx6ovGSqXSK07n/0eM3ZusiNdB6cIlJdabQhWGgJp3Vooy9AaLS/tigeybOJAobqbtggTamoWndz72JscZBvw==" - }, - "xunit": { - "type": "Direct", - "requested": "[2.9.3, )", - "resolved": "2.9.3", - "contentHash": "TlXQBinK35LpOPKHAqbLY4xlEen9TBafjs0V5KnA4wZsoQLQJiirCR4CbIXvOH8NzkW4YeJKP5P/Bnrodm0h9Q==", - "dependencies": { - "xunit.analyzers": "1.18.0", - "xunit.assert": "2.9.3", - "xunit.core": "[2.9.3]" - } - }, - "xunit.runner.visualstudio": { - "type": "Direct", - "requested": "[3.1.5, )", - "resolved": "3.1.5", - "contentHash": "tKi7dSTwP4m5m9eXPM2Ime4Kn7xNf4x4zT9sdLO/G4hZVnQCRiMTWoSZqI/pYTVeI27oPPqHBKYI/DjJ9GsYgA==" - }, - "Gulliver": { - "type": "Transitive", - "resolved": "3.0.0", - "contentHash": "J232PI4ccScfLKt59UzyHBRZLcAJpyJ+N/at1fFF/XxBKwjl8idDVPkMU3FZkwaaU/e3Ert8/EnGLg5CxmQ8dQ==" - }, - "Microsoft.CodeCoverage": { - "type": "Transitive", - "resolved": "18.0.1", - "contentHash": "O+utSr97NAJowIQT/OVp3Lh9QgW/wALVTP4RG1m2AfFP4IyJmJz0ZBmFJUsRQiAPgq6IRC0t8AAzsiPIsaUDEA==" - }, - "Microsoft.TestPlatform.ObjectModel": { - "type": "Transitive", - "resolved": "18.0.1", - "contentHash": "qT/mwMcLF9BieRkzOBPL2qCopl8hQu6A1P7JWAoj/FMu5i9vds/7cjbJ/LLtaiwWevWLAeD5v5wjQJ/l6jvhWQ==" - }, - "Microsoft.TestPlatform.TestHost": { - "type": "Transitive", - "resolved": "18.0.1", - "contentHash": "uDJKAEjFTaa2wHdWlfo6ektyoh+WD4/Eesrwb4FpBFKsLGehhACVnwwTI4qD3FrIlIEPlxdXg3SyrYRIcO+RRQ==", - "dependencies": { - "Microsoft.TestPlatform.ObjectModel": "18.0.1", - "Newtonsoft.Json": "13.0.3" - } - }, - "Newtonsoft.Json": { - "type": "Transitive", - "resolved": "13.0.3", - "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" - }, - "xunit.abstractions": { - "type": "Transitive", - "resolved": "2.0.3", - "contentHash": "pot1I4YOxlWjIb5jmwvvQNbTrZ3lJQ+jUGkGjWE3hEFM0l5gOnBWS+H3qsex68s5cO52g+44vpGzhAt+42vwKg==" - }, - "xunit.analyzers": { - "type": "Transitive", - "resolved": "1.18.0", - "contentHash": "OtFMHN8yqIcYP9wcVIgJrq01AfTxijjAqVDy/WeQVSyrDC1RzBWeQPztL49DN2syXRah8TYnfvk035s7L95EZQ==" - }, - "xunit.assert": { - "type": "Transitive", - "resolved": "2.9.3", - "contentHash": "/Kq28fCE7MjOV42YLVRAJzRF0WmEqsmflm0cfpMjGtzQ2lR5mYVj1/i0Y8uDAOLczkL3/jArrwehfMD0YogMAA==" - }, - "xunit.core": { - "type": "Transitive", - "resolved": "2.9.3", - "contentHash": "BiAEvqGvyme19wE0wTKdADH+NloYqikiU0mcnmiNyXaF9HyHmE6sr/3DC5vnBkgsWaE6yPyWszKSPSApWdRVeQ==", - "dependencies": { - "xunit.extensibility.core": "[2.9.3]", - "xunit.extensibility.execution": "[2.9.3]" - } - }, - "xunit.extensibility.core": { - "type": "Transitive", - "resolved": "2.9.3", - "contentHash": "kf3si0YTn2a8J8eZNb+zFpwfoyvIrQ7ivNk5ZYA5yuYk1bEtMe4DxJ2CF/qsRgmEnDr7MnW1mxylBaHTZ4qErA==", - "dependencies": { - "xunit.abstractions": "2.0.3" - } - }, - "xunit.extensibility.execution": { - "type": "Transitive", - "resolved": "2.9.3", - "contentHash": "yMb6vMESlSrE3Wfj7V6cjQ3S4TXdXpRqYeNEI3zsX31uTsGMJjEw6oD5F5u1cHnMptjhEECnmZSsPxB6ChZHDQ==", - "dependencies": { - "xunit.extensibility.core": "[2.9.3]" - } - }, - "arcus": { - "type": "Project", - "dependencies": { - "Gulliver": "[3.0.0, )" - } - } - } - } -} \ No newline at end of file diff --git a/src/Arcus.Tests/AbstractIPAddressRangeTests.IEnumerable.cs b/src/Arcus.Tests/AbstractIPAddressRangeTests.IEnumerable.cs new file mode 100644 index 0000000..f66b8bf --- /dev/null +++ b/src/Arcus.Tests/AbstractIPAddressRangeTests.IEnumerable.cs @@ -0,0 +1,446 @@ +using System.Collections; +using System.Net; + +namespace Arcus.Tests +{ + /// + /// tests for + /// + public partial class AbstractIPAddressRangeTests + { + #region IEnumerable + + /// Verifies the generic enumerator yields every address from head to tail inclusive. + /// The head IP address string for the range. + /// The tail IP address string for the range. + [Theory] + [InlineData("::", "::")] + [InlineData("::", "::FF")] + [InlineData("0.0.0.0", "0.0.0.0")] + [InlineData("192.168.1.1", "192.168.1.1")] + [InlineData("192.168.1.1", "192.168.1.5")] + [InlineData("255.255.255.128", "255.255.255.255")] + [InlineData("255.255.255.255", "255.255.255.255")] + [InlineData("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ff00", "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")] + [InlineData("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")] + public void GetEnumerator_Test(string headAddressString, string tailAddressString) + { + // Arrange + var head = IPAddress.Parse(headAddressString); + var tail = IPAddress.Parse(tailAddressString); + + var iPAddressRange = CreateIPAddressRange(head, tail); + + // Act + var ipAddressArray = iPAddressRange.ToArray(); + + // Assert + Assert.Equal(iPAddressRange.Count(), ipAddressArray.Length); + Assert.Equal(head, ipAddressArray[0]); + Assert.Equal(tail, ipAddressArray[ipAddressArray.Length - 1]); + } + + /// Verifies the non-generic IEnumerable enumerator yields every address from head to tail inclusive. + /// The head IP address string for the range. + /// The tail IP address string for the range. + [Theory] + [InlineData("::", "::")] + [InlineData("::", "::FF")] + [InlineData("0.0.0.0", "0.0.0.0")] + [InlineData("192.168.1.1", "192.168.1.1")] + [InlineData("192.168.1.1", "192.168.1.5")] + [InlineData("255.255.255.128", "255.255.255.255")] + [InlineData("255.255.255.255", "255.255.255.255")] + [InlineData("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ff00", "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")] + [InlineData("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")] + public void GetIEnumerableEnumerator_Test(string headAddressString, string tailAddressString) + { + // Arrange + var head = IPAddress.Parse(headAddressString); + var tail = IPAddress.Parse(tailAddressString); + + var iPAddressRange = CreateIPAddressRange(head, tail); + + // Act + var result = new List(); + var enumerator = ((IEnumerable)iPAddressRange).GetEnumerator(); + while (enumerator.MoveNext()) + { + result.Add(enumerator.Current as IPAddress); + } + + // Assert + Assert.Equal(iPAddressRange.Count(), result.Count); + Assert.Equal(head, result[0]); + Assert.Equal(tail, result[result.Count - 1]); + } + + #endregion // end: IEnumerable + + #region IEnumerable / IEnumerable + + /// Verifies that enumerating an IPv6 range yields exactly the expected addresses. + [Fact] + public void Enumerable_IPv6_ContainsExpected_Test() + { + // Arrange + const string headString = "::a"; + const string tailString = "::c"; + var head = IPAddress.Parse(headString); + var tail = IPAddress.Parse(tailString); + + var iPAddressRange = CreateIPAddressRange(head, tail); + + // Act + var result = iPAddressRange.ToList(); + + // Assert + Assert.Equal(3, result.Count); + Assert.Equal(new[] { head, IPAddress.Parse("::b"), tail }, result); + } + + /// Verifies that taking more elements than the IPv6 range contains stops at the tail without error. + [Fact] + public void Enumerable_IPv6_TakePastEnd_Test() + { + // Arrange + var head = IPAddress.Parse("ffff:ffff:ffff:ffff:ffff:ffff:ffff:fff0"); + var tail = IPAddress.Parse("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"); + + var iPAddressRange = CreateIPAddressRange(head, tail); + + // Act + var result = iPAddressRange.Take(100).ToList(); + + // Assert + Assert.Equal(16, result.Count); + Assert.Equal(tail, result[result.Count - 1]); + } + + /// Verifies that taking more elements than the IPv4 range contains stops at the tail without error. + [Fact] + public void Enumerable_IPv4_TakePastEnd_Test() + { + // Arrange + var head = IPAddress.Parse("255.255.255.240"); + var tail = IPAddress.Parse("255.255.255.255"); + + var iPAddressRange = CreateIPAddressRange(head, tail); + + // Act + var result = iPAddressRange.Take(100).ToList(); + + // Assert + Assert.Equal(16, result.Count); + Assert.Equal(tail, result[result.Count - 1]); + } + + /// Verifies that enumerating an IPv4 range yields exactly the expected addresses. + [Fact] + public void Enumerable_IPv4_ContainsExpected_Test() + { + // Arrange + const string headString = "192.168.1.1"; + const string tailString = "192.168.1.3"; + var head = IPAddress.Parse(headString); + var tail = IPAddress.Parse(tailString); + + var iPAddressRange = CreateIPAddressRange(head, tail); + + // Act + var result = iPAddressRange.ToList(); + + // Assert + Assert.Equal(3, result.Count); + Assert.Equal(new[] { head, IPAddress.Parse("192.168.1.2"), tail }, result); + } + + /// Verifies that a range whose head and tail are the same address enumerates to exactly one element. + [Fact] + public void Enumerable_SameHead_And_SameTail_ReturnsSingle_Test() + { + // Arrange + var ipAddress = IPAddress.Parse("192.168.1.1"); + + var iPAddressRange = CreateIPAddressRange(ipAddress, ipAddress); + + // Act + var addresses = iPAddressRange.ToArray(); + + // Assert + Assert.Single(addresses); + Assert.Equal(ipAddress, addresses.Single()); + } + + /// Verifies that enumerating across a byte-boundary (address rollover) produces correct results. + [Fact] + public void Enumerable_ByteBoundaryCrossing_Test() + { + // Arrange - IPv4 range crossing the 255.255.255.255 → 0.0.0.0 boundary is invalid, + // but we can test a range that exercises the span-based path near a byte boundary. + var head = IPAddress.Parse("192.168.1.254"); + var tail = IPAddress.Parse("192.168.2.2"); + var iPAddressRange = CreateIPAddressRange(head, tail); + + // Act + var result = iPAddressRange.ToList(); + + // Assert + Assert.Equal(5, result.Count); + Assert.Equal(head, result[0]); + Assert.Equal(IPAddress.Parse("192.168.1.255"), result[1]); + Assert.Equal(IPAddress.Parse("192.168.2.0"), result[2]); + Assert.Equal(IPAddress.Parse("192.168.2.1"), result[3]); + Assert.Equal(tail, result[4]); + } + + /// Verifies that enumerating a small IPv6 range that spans a 16-bit boundary produces correct results. + [Fact] + public void Enumerable_IPv6_BoundaryCrossing_Test() + { + // Arrange - IPv6 range crossing ::ffff → ::1:0000 + var head = IPAddress.Parse("::fffe"); + var tail = IPAddress.Parse("::1:0001"); + var iPAddressRange = CreateIPAddressRange(head, tail); + + // Act + var result = iPAddressRange.ToList(); + + // Assert + Assert.Equal(4, result.Count); + Assert.Equal(head, result[0]); + Assert.Equal(IPAddress.Parse("::ffff"), result[1]); + Assert.Equal(IPAddress.Parse("::1:0"), result[2]); + Assert.Equal(tail, result[3]); + } + + /// Verifies that skipping and taking within a huge IPv6 range completes in finite time (lazy enumeration). + [Fact] + public void Enumerable_ReasonableIteration_Test() + { + // Arrange + var iPAddressRange = CreateIPAddressRange( + IPAddress.Parse("::"), + IPAddress.Parse("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff") + ); + + // Act + var addresses = iPAddressRange.Skip(10).Take(10).ToArray(); + + // Assert + Assert.NotNull(addresses); // if this test completed it likely means that we didn't iterate through 2^128 ip addresses to skip 10 and take 10 + } + + #endregion // end: IEnumerable / IEnumerable + + #region Constructor maxEnumerationExponent + + /// Verifies that constructing with a negative exponent throws. + [Fact] + public void Ctor_NegativeExponent_Throws_Test() + { + Assert.Throws(() => + new IPAddressRange(IPAddress.Parse("0.0.0.0"), IPAddress.Parse("0.0.0.1"), maxEnumerationExponent: -1) + ); + + Assert.Throws(() => + new Subnet(IPAddress.Parse("0.0.0.0"), IPAddress.Parse("0.0.0.1"), maxEnumerationExponent: -1) + ); + + Assert.Throws(() => + new Subnet(IPAddress.Parse("0.0.0.0"), 24, maxEnumerationExponent: -1) + ); + } + + /// Verifies that constructing with an exponent over 128 throws. + [Fact] + public void Ctor_ExponentOver128_Throws_Test() + { + Assert.Throws(() => + new IPAddressRange(IPAddress.Parse("0.0.0.0"), IPAddress.Parse("0.0.0.1"), maxEnumerationExponent: 129) + ); + + Assert.Throws(() => + new Subnet(IPAddress.Parse("0.0.0.0"), IPAddress.Parse("0.0.0.1"), maxEnumerationExponent: 129) + ); + } + + /// Verifies that constructing with exponent 0 is valid. + [Fact] + public void Ctor_Exponent0_IsValid_Test() + { + var range = new IPAddressRange(IPAddress.Parse("10.0.0.0"), IPAddress.Parse("10.0.0.5"), maxEnumerationExponent: 0); + Assert.Equal(0, range.MaxEnumerationExponent); + } + + /// + /// Verifies that constructing with exponent 128 is valid for IPv6 (stays 128) + /// and for IPv4 is clamped to 32. + /// + [Fact] + public void Ctor_Exponent128_IsValid_Test() + { + // IPv6: exponent 128 should stay 128 + var ipv6Range = new IPAddressRange( + IPAddress.Parse("::"), + IPAddress.Parse("::5"), + maxEnumerationExponent: 128 + ); + Assert.Equal(128, ipv6Range.MaxEnumerationExponent); + + // IPv4: exponent 128 should clamp to 32 + var ipv4Range = new IPAddressRange( + IPAddress.Parse("10.0.0.0"), + IPAddress.Parse("10.0.0.5"), + maxEnumerationExponent: 128 + ); + Assert.Equal(32, ipv4Range.MaxEnumerationExponent); + } + + /// Verifies that the default exponent is 12. + [Fact] + public void Ctor_DefaultExponent_Is12_Test() + { + var range = new IPAddressRange(IPAddress.Parse("10.0.0.0"), IPAddress.Parse("10.0.0.5")); + Assert.Equal(12, range.MaxEnumerationExponent); + } + + #endregion // end: Constructor maxEnumerationExponent + + #region Enumeration Cap + + /// Verifies that iterating a range within the cap succeeds. + [Fact] + public void GetEnumerator_WithinCap_Succeeds_Test() + { + var range = new IPAddressRange(IPAddress.Parse("10.0.0.0"), IPAddress.Parse("10.0.0.5"), maxEnumerationExponent: 4); + + var result = range.ToArray(); + + Assert.Equal(6, result.Length); + Assert.Equal(IPAddress.Parse("10.0.0.0"), result[0]); + Assert.Equal(IPAddress.Parse("10.0.0.5"), result[result.Length - 1]); + } + + /// Verifies that iterating a range exceeding the cap throws. + [Fact] + public void GetEnumerator_ExceedsCap_Throws_Test() + { + var range = new IPAddressRange( + IPAddress.Parse("10.0.0.0"), + IPAddress.Parse("10.0.0.255"), + maxEnumerationExponent: 4 + ); + + var ex = Assert.Throws(() => range.ToArray()); + Assert.Contains("2^4", ex.Message); + } + + /// Verifies that iterating a range exactly at the cap boundary succeeds. + [Fact] + public void GetEnumerator_AtCapBoundary_Succeeds_Test() + { + // 2^4 = 16 addresses + var range = new IPAddressRange( + IPAddress.Parse("10.0.0.0"), + IPAddress.Parse("10.0.0.15"), + maxEnumerationExponent: 4 + ); + + var result = range.ToArray(); + + Assert.Equal(16, result.Length); + Assert.Equal(IPAddress.Parse("10.0.0.15"), result[result.Length - 1]); + } + + /// Verifies that Subnet with explicit exponent enforces the cap. + [Fact] + public void Subnet_Enumeration_UsesExponent_Test() + { + // /30 = 4 addresses, exponent 2 = cap 4 + var subnet = new Subnet(IPAddress.Parse("10.0.0.0"), 30, maxEnumerationExponent: 2); + + var result = subnet.ToArray(); + + Assert.Equal(4, result.Length); + } + + #endregion // end: Enumeration Cap + + #region ToIPAddresses + + /// Verifies that yields the same addresses as for small ranges. + /// The head IP address string for the range. + /// The tail IP address string for the range. + [Theory] + [InlineData("::", "::")] + [InlineData("::", "::FF")] + [InlineData("0.0.0.0", "0.0.0.0")] + [InlineData("192.168.1.1", "192.168.1.1")] + [InlineData("192.168.1.1", "192.168.1.5")] + [InlineData("255.255.255.128", "255.255.255.255")] + [InlineData("255.255.255.255", "255.255.255.255")] + [InlineData("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ff00", "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")] + [InlineData("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")] + public void ToIPAddresses_MatchesEnumerable_Test(string headAddressString, string tailAddressString) + { + // Arrange + var head = IPAddress.Parse(headAddressString); + var tail = IPAddress.Parse(tailAddressString); + var range = CreateIPAddressRange(head, tail); + + // Act + var enumerateResult = range.ToIPAddresses().ToArray(); + // CodeQL [cs/useless-upcast] Intentionally casting to IEnumerable to verify that the explicit interface implementation produces the same results as the public ToIPAddresses method + var enumerableResult = ((IEnumerable)range).ToArray(); + + // Assert + Assert.Equal(enumerableResult, enumerateResult); + } + + /// Verifies that within the cap succeeds. + [Fact] + public void ToIPAddresses_WithinCap_Succeeds_Test() + { + var range = new IPAddressRange(IPAddress.Parse("10.0.0.0"), IPAddress.Parse("10.0.0.5"), maxEnumerationExponent: 4); + + var result = range.ToIPAddresses().ToArray(); + Assert.Equal(6, result.Length); + Assert.Equal(IPAddress.Parse("10.0.0.0"), result[0]); + Assert.Equal(IPAddress.Parse("10.0.0.5"), result[result.Length - 1]); + } + + /// Verifies that exceeding the cap throws. + [Fact] + public void ToIPAddresses_ExceedsCap_Throws_Test() + { + var range = new IPAddressRange( + IPAddress.Parse("10.0.0.0"), + IPAddress.Parse("10.0.0.255"), + maxEnumerationExponent: 4 + ); + + var ex = Assert.Throws(() => range.ToIPAddresses().ToArray()); + Assert.Contains("2^4", ex.Message); + } + + /// Verifies that at the cap boundary succeeds. + [Fact] + public void ToIPAddresses_AtCapBoundary_Succeeds_Test() + { + // 2^4 = 16 addresses + var range = new IPAddressRange( + IPAddress.Parse("10.0.0.0"), + IPAddress.Parse("10.0.0.15"), + maxEnumerationExponent: 4 + ); + + var result = range.ToIPAddresses().ToArray(); + + Assert.Equal(16, result.Length); + Assert.Equal(IPAddress.Parse("10.0.0.15"), result[result.Length - 1]); + } + + #endregion // end: ToIPAddresses + } +} diff --git a/src/Arcus.Tests/AbstractIPAddressRangeTests.IFormattable.cs b/src/Arcus.Tests/AbstractIPAddressRangeTests.IFormattable.cs new file mode 100644 index 0000000..efc9d72 --- /dev/null +++ b/src/Arcus.Tests/AbstractIPAddressRangeTests.IFormattable.cs @@ -0,0 +1,84 @@ +using System.Globalization; +using System.Net; + +namespace Arcus.Tests +{ + /// + /// tests for + /// + public partial class AbstractIPAddressRangeTests + { + #region Formatting + + /// Verifies ToString with a general format specifier returns the Head-dash-Tail representation. + /// Head address string. + /// Tail address string. + /// Format string for the output representation. + /// Expected formatted string. + [Theory] + [InlineData("192.168.1.1", "192.168.1.5", null, "192.168.1.1 - 192.168.1.5")] + [InlineData("192.168.1.1", "192.168.1.5", "", "192.168.1.1 - 192.168.1.5")] + [InlineData("192.168.1.1", "192.168.1.5", "g", "192.168.1.1 - 192.168.1.5")] + [InlineData("192.168.1.1", "192.168.1.5", "G", "192.168.1.1 - 192.168.1.5")] + [InlineData("::beef", "::dead", null, "::beef - ::dead")] + [InlineData("::beef", "::dead", "", "::beef - ::dead")] + [InlineData("::beef", "::dead", "g", "::beef - ::dead")] + [InlineData("::beef", "::dead", "G", "::beef - ::dead")] + public void ToString_Format_ReturnsHeadDashTail_Test( + string headString, + string tailString, + string format, + string expected + ) + { + // Arrange + var head = IPAddress.Parse(headString); + var tail = IPAddress.Parse(tailString); + var iPAddressRange = new IPAddressRange(head, tail); + + // Act + var result = iPAddressRange.ToString(format, CultureInfo.InvariantCulture); + + // Assert + Assert.Equal(expected, result); + } + + /// Verifies the no-arg ToString returns the Head-dash-Tail representation. + [Fact] + public void ToString_NoArgs_ReturnsHeadDashTail_Test() + { + // Arrange + var head = IPAddress.Parse("10.0.0.1"); + var tail = IPAddress.Parse("10.0.0.255"); + var iPAddressRange = new IPAddressRange(head, tail); + + // Act + var result = iPAddressRange.ToString(); + + // Assert + Assert.Equal("10.0.0.1 - 10.0.0.255", result); + } + + /// Verifies ToString throws FormatException for an unrecognized format specifier. + /// Head address string. + /// Tail address string. + /// Unrecognized format specifier. + [Theory] + [InlineData("192.168.1.1", "192.168.1.5", "X")] + [InlineData("192.168.1.1", "192.168.1.5", "R")] + [InlineData("::beef", "::dead", "Z")] + public void ToString_UnknownFormat_Throws_FormatException_Test(string headString, string tailString, string format) + { + // Arrange + var head = IPAddress.Parse(headString); + var tail = IPAddress.Parse(tailString); + var iPAddressRange = new IPAddressRange(head, tail); + + // Act + // Assert + Assert.Throws(() => iPAddressRange.ToString(format, CultureInfo.InvariantCulture)); + } + + #endregion // end: Formatting + } +} diff --git a/src/Arcus.Tests/AbstractIPAddressRangeTests.IIPAddressRange.cs b/src/Arcus.Tests/AbstractIPAddressRangeTests.IIPAddressRange.cs new file mode 100644 index 0000000..c0d1af0 --- /dev/null +++ b/src/Arcus.Tests/AbstractIPAddressRangeTests.IIPAddressRange.cs @@ -0,0 +1,1077 @@ +using System.Net; +using Arcus.Math; +using Arcus.Utilities; + +namespace Arcus.Tests +{ + /// + /// tests for + /// + public partial class AbstractIPAddressRangeTests + { + #region Deconstructors + + /// Verifies Deconstruct returns the Head and Tail of the range. + [Fact] + public void Deconstruct_Head_Tail_Test() + { + // Arrange + const string headString = "192.168.1.1"; + const string tailString = "192.168.1.3"; + var head = IPAddress.Parse(headString); + var tail = IPAddress.Parse(tailString); + + // Act + var (resultHead, resultTail) = CreateIPAddressRange(head, tail); + + // Assert + Assert.Equal(resultHead, head); + Assert.Equal(resultTail, tail); + } + + #endregion // end: Deconstructors + + #region IsSingleIP + + /// + /// Gets parameters: expected (bool), ipAddressRange (AbstractIPAddressRange) + /// + /// + /// Parameters: expected (bool), ipAddressRange (AbstractIPAddressRange) + /// + public static TheoryData IsSingleIP_Test_Data + { + get + { + var data = new TheoryData + { + { true, CreateIPAddressRange(IPAddress.Any, IPAddress.Any) }, + { true, CreateIPAddressRange(IPAddress.IPv6Any, IPAddress.IPv6Any) }, + { false, CreateIPAddressRange(IPAddress.Any, IPAddress.Broadcast) }, + { false, CreateIPAddressRange(IPAddress.IPv6Any, IPAddress.IPv6Loopback) }, + }; + return data; + } + } + + /// Verifies IsSingleIP returns expected for a range of length one vs longer. + /// Expected result. + /// The range under test. + [Theory] + [MemberData(nameof(IsSingleIP_Test_Data))] + public void IsSingleIP_Test(bool expected, IPAddressRange ipAddressRange) + { + // Arrange + // Act + var isSingleIP = ipAddressRange.IsSingleIP; + + // Assert + Assert.Equal(expected, isSingleIP); + } + + #endregion // end: IsSingleIP + + #region TryGetLength + + /// Verifies TryGetLength (int overload) returns success and the correct value when the length fits in int. + /// Expected result. + /// The range under test. + [Theory] + [MemberData(nameof(Length_Test_Data))] + public void TryGetLength_Integer_Test(System.Numerics.BigInteger expected, IPAddressRange ipAddressRange) + { + // Arrange + // Act + var success = ipAddressRange.TryGetLength(out int length); + + // Assert + Assert.Equal(expected <= int.MaxValue, success); + Assert.Equal(expected <= int.MaxValue ? (int)expected : -1, length); + } + + /// Verifies TryGetLength (long overload) returns success and the correct value when the length fits in long. + /// Expected result. + /// The range under test. + [Theory] + [MemberData(nameof(Length_Test_Data))] + public void TryGetLength_Long_Test(System.Numerics.BigInteger expected, IPAddressRange ipAddressRange) + { + // Arrange + // Act + var success = ipAddressRange.TryGetLength(out long length); + + // Assert + Assert.Equal(expected <= long.MaxValue, success); + Assert.Equal(expected <= long.MaxValue ? (long)expected : -1, length); + } + + #endregion // end: TryGetLength + + #region Contains + + #region Contains(IPAddress) + + /// Verifies Contains(IPAddress) returns the expected result for addresses inside, outside, and at the boundary of the range. + /// Expected result. + /// The head IP address string for the range. + /// The tail IP address string for the range. + /// The IP address string to test for containment. + [Theory] + [InlineData(true, "::", "::ABCD", "::1234")] // IPv6 range contains IPv6 + [InlineData(false, "::", "::ABCD", "::FFFF")] // IPv6 range doesn't contains IPv6 + [InlineData(true, "::", "::ABCD", "::")] // IPv6 range contains IPv6 head + [InlineData(true, "::", "::ABCD", "::ABCD")] // IPv6 range contains IPv6 tail + [InlineData(false, "::", "::ABCD", "192.168.1.1")] // IPv6 range doesn't contains IPv4 + [InlineData(false, "::", "::ABCD", null)] // IPv6 range doesn't contains null + [InlineData(true, "192.168.0.1", "192.168.0.254", "192.168.0.128")] // IPv4 range contains IPv4 + [InlineData(false, "192.168.0.1", "192.168.0.254", "10.1.1.1")] // IPv4 range doesn't contains IPv4 + [InlineData(true, "192.168.0.1", "192.168.0.254", "192.168.0.1")] // IPv4 range contains IPv4 head + [InlineData(true, "192.168.0.1", "192.168.0.254", "192.168.0.254")] // IPv4 range contains IPv4 tail + [InlineData(false, "192.168.0.1", "192.168.0.254", "::")] // IPv4 range doesn't contains IPv6 + [InlineData(false, "192.168.0.1", "192.168.0.254", null)] // IPv4 range doesn't contains null + public void ContainsIPAddress_Test(bool expected, string headString, string tailString, string addressString) + { + // Arrange + var head = IPAddress.Parse(headString); + var tail = IPAddress.Parse(tailString); + _ = IPAddress.TryParse(addressString, out var containsAddress); + + var iPAddressRange = CreateIPAddressRange(head, tail); + + // Act + var result = iPAddressRange.Contains(containsAddress); + + // Assert + Assert.Equal(expected, result); + } + + #endregion // end: Contains(IPAddress) + + #region Contains(IIPAddressRange) + + /// Verifies Contains(IIPAddressRange) returns the expected result for ranges that fit, overlap, or fall outside. + /// Expected result. + /// The head IP address string for the range. + /// The tail IP address string for the range. + /// The head IP address string for the contained range. + /// The tail IP address string for the contained range. + [Theory] + [InlineData(true, "::", "::ABCD", "::", "::ABCD")] // IPv6 range contains self + [InlineData(true, "::", "::ABCD", "::1", "::1234")] // IPv6 range contains internal IPv6 range + [InlineData(false, "::", "::ABCD", "AA::FFFF", "AB::FFFF")] // IPv6 range doesn't contains external IPv6 range + [InlineData(false, "A::", "A::ABCD", "::", "A::")] // IPv6 range doesn't contains IPv6 overhang head + [InlineData(false, "::", "::ABCD", "::", "AA::FFFF")] // IPv6 range doesn't contains IPv6 overhang tail + [InlineData(false, "::", "::ABCD", "192.168.1.1", "192.168.1.10")] // IPv6 range doesn't contains IPv4 range + [InlineData(false, "::", "A::ABCD", null, null)] // IPv6 range doesn't contain null range + [InlineData(true, "128.0.0.1", "128.0.0.254", "128.0.0.1", "128.0.0.254")] // IPv4 range contains self + [InlineData(true, "128.0.0.1", "128.0.0.254", "128.0.0.5", "128.0.0.100")] // IPv4 range contains internal IPv4 range + [InlineData(false, "128.0.0.1", "128.0.0.254", "140.0.0.0", "145.0.0.0")] // IPv4 range doesn't contains external IPv4 range + [InlineData(false, "128.0.0.1", "128.0.0.254", "", "128.0.0.100")] // IPv4 range doesn't contains null head IPv4 range + [InlineData(false, "128.0.0.1", "128.0.0.254", "128.0.0.5", "")] // IPv4 range doesn't contains null tail IPv4 range + [InlineData(false, "128.0.0.1", "128.0.0.254", "0.0.0.0", "128.0.0.100")] // IPv4 range doesn't contains IPv4 overhang head + [InlineData(false, "128.0.0.1", "128.0.0.254", "128.0.0.5", "145.0.0.0")] // IPv4 range doesn't contains IPv4 overhang tail + [InlineData(false, "128.0.0.1", "128.0.0.254", "::ABCD", "42::ABCD")] // IPv4 range doesn't contains IPv6 range + [InlineData(false, "128.0.0.1", "128.0.0.254", null, null)] // IPv4 range doesn't contain null range + public void ContainsIPAddressRange_Test( + bool expected, + string headString, + string tailString, + string containsHeadString, + string containsTailString + ) + { + // Arrange + var head = IPAddress.Parse(headString); + var tail = IPAddress.Parse(tailString); + + var substituteIPAddressSubRange = + IPAddress.TryParse(containsHeadString, out var subhead) + && IPAddress.TryParse(containsTailString, out var subtail) + ? new IPAddressRange(subhead, subtail) + : null; + + var iPAddressRange = CreateIPAddressRange(head, tail); + + var containedIPAddressRange = substituteIPAddressSubRange; + + // Act + var result = iPAddressRange.Contains(containedIPAddressRange); + + // Assert + Assert.Equal(expected, result); + } + + #endregion // end: Contains(IIPAddressRange) + + #region Contains IIPAddressRange + + /// + /// Gets parameters: expected (bool), left (IIPAddressRange), right (IIPAddressRange) + /// + /// + /// Parameters: expected (bool), left (IIPAddressRange), right (IIPAddressRange) + /// + public static TheoryData Contains_IIPAddressRange_Test_Data + { + get + { + var ipv4Range = CreateIPAddressRange(IPAddress.Any, IPAddress.Broadcast); + var ipv6Range = CreateIPAddressRange( + IPAddress.IPv6Any, + IPAddress.Parse("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff") + ); + + var data = new TheoryData + { + // null overlap checking + { false, ipv4Range, null }, + { false, ipv6Range, null }, + // same overlap checking + { true, ipv4Range, ipv4Range }, + { true, ipv6Range, ipv6Range }, + // differing address families + { false, ipv4Range, ipv6Range }, + { false, ipv6Range, ipv4Range }, + // head only overlapped + { + false, + CreateIPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.128")), + CreateIPAddressRange(IPAddress.Parse("192.168.1.128"), IPAddress.Parse("192.168.1.192")) + }, + { + false, + CreateIPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("::ffff")), + CreateIPAddressRange(IPAddress.Parse("::ffff"), IPAddress.Parse("1::ffff")) + }, + // full head and tail overlapped + { + true, + CreateIPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.255")), + CreateIPAddressRange(IPAddress.Parse("192.168.1.128"), IPAddress.Parse("192.168.1.192")) + }, + { + true, + CreateIPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("ffff::ffff")), + CreateIPAddressRange(IPAddress.Parse("::ff00"), IPAddress.Parse("ff::ff00")) + }, + // tail only overlapped + { + false, + CreateIPAddressRange(IPAddress.Parse("192.168.1.192"), IPAddress.Parse("192.168.1.255")), + CreateIPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.192")) + }, + { + false, + CreateIPAddressRange(IPAddress.Parse("::ffff"), IPAddress.Parse("1::ffff")), + CreateIPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("::ffff")) + }, + // not touching + { + false, + CreateIPAddressRange(IPAddress.Parse("192.168.1.1"), IPAddress.Parse("192.168.1.128")), + CreateIPAddressRange(IPAddress.Parse("10.1.1.0"), IPAddress.Parse("10.1.1.100")) + }, + { + false, + CreateIPAddressRange(IPAddress.Parse("ab::"), IPAddress.Parse("ab::f")), + CreateIPAddressRange(IPAddress.Parse("ef::"), IPAddress.Parse("ef::f")) + }, + // disparate ranges + { + false, + CreateIPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.2.0")), + CreateIPAddressRange(IPAddress.Parse("10.1.1.1"), IPAddress.Parse("10.1.5.0")) + }, + { + false, + CreateIPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("::ff")), + CreateIPAddressRange(IPAddress.Parse("f::"), IPAddress.Parse("f:1::")) + }, + }; + return data; + } + } + + /// Verifies Contains(IIPAddressRange) returns the expected result for the given range pair. + /// Expected result. + /// Left operand range. + /// Right operand range. + [Theory] + [MemberData(nameof(Contains_IIPAddressRange_Test_Data))] + public void Contains_IIPAddressRange_Test(bool expected, IIPAddressRange left, IIPAddressRange right) + { + // Arrange + // Act + var result = left.Contains(right); + + // Assert + Assert.Equal(expected, result); + } + + #endregion // end: Contains IIPAddressRange + + #region Contains IPAddress + + /// + /// Gets parameters: expected (bool), range (IIPAddressRange), address (IPAddress) + /// + /// + /// Parameters: expected (bool), range (IIPAddressRange), address (IPAddress) + /// + public static TheoryData Contains_Test_Data + { + get + { + var ipv4Range = CreateIPAddressRange(IPAddress.Any, IPAddress.Broadcast); + var ipv6Range = CreateIPAddressRange( + IPAddress.IPv6Any, + IPAddress.Parse("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff") + ); + + var data = new TheoryData + { + // does not contain null + { false, ipv4Range, null }, + { false, ipv6Range, null }, + // differing address families + { false, ipv4Range, IPAddress.IPv6Any }, + { false, ipv6Range, IPAddress.Any }, + // contains head + { true, ipv4Range, ipv4Range.Head }, + { true, ipv6Range, ipv6Range.Head }, + // contains tail + { true, ipv4Range, ipv4Range.Tail }, + { true, ipv6Range, ipv6Range.Tail }, + // does not contain outside before + { + false, + CreateIPAddressRange(IPAddress.Parse("192.168.1.1"), IPAddress.Parse("192.168.1.200")), + IPAddress.Parse("192.168.1.0") + }, + { + false, + CreateIPAddressRange(IPAddress.Parse("::ff01"), IPAddress.Parse("::ff08")), + IPAddress.Parse("::ff00") + }, + // does not contain outside after + { + false, + CreateIPAddressRange(IPAddress.Parse("192.168.1.1"), IPAddress.Parse("192.168.1.200")), + IPAddress.Parse("192.168.1.201") + }, + { + false, + CreateIPAddressRange(IPAddress.Parse("::ff01"), IPAddress.Parse("::ff08")), + IPAddress.Parse("::ff09") + }, + }; + + // contains all inside IPv4 + var ipv4InsideRange = CreateIPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.5")); + foreach (var ip in ipv4InsideRange.ToIPAddresses()) + { + data.Add(true, ipv4InsideRange, ip); + } + + // contains all inside IPv6 + var ipv6InsideRange = CreateIPAddressRange(IPAddress.Parse("::ff00"), IPAddress.Parse("::ff0f")); + foreach (var ip in ipv6InsideRange.ToIPAddresses()) + { + data.Add(true, ipv6InsideRange, ip); + } + + return data; + } + } + + /// Verifies Contains(IPAddress) via IIPAddressRange returns the expected result. + /// Expected result. + /// The range under test. + /// The address to test for containment. + [Theory] + [MemberData(nameof(Contains_Test_Data))] + public void Contains_Test(bool expected, IIPAddressRange range, IPAddress address) + { + // Arrange + // Act + var result = range.Contains(address); + + // Assert + Assert.Equal(expected, result); + } + + #endregion // end: Contains IPAddress + + #endregion // end: Contains + + #region Overlap and Touches + + #region HeadOverlappedBy + + /// + /// Gets parameters: expected (bool), left (IIPAddressRange), right (IIPAddressRange) + /// + /// + /// Parameters: expected (bool), left (IIPAddressRange), right (IIPAddressRange) + /// + public static TheoryData HeadOverlappedBy_Test_Data + { + get + { + var ipv4Range = CreateIPAddressRange(IPAddress.Any, IPAddress.Broadcast); + var ipv6Range = CreateIPAddressRange( + IPAddress.IPv6Any, + IPAddress.Parse("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff") + ); + + var data = new TheoryData + { + // null overlap checking + { false, ipv4Range, null }, + { false, ipv6Range, null }, + // same overlap checking + { true, ipv4Range, ipv4Range }, + { true, ipv6Range, ipv6Range }, + // differing address families + { false, ipv4Range, ipv6Range }, + { false, ipv6Range, ipv4Range }, + // head only overlapped + { + true, + CreateIPAddressRange(IPAddress.Parse("192.168.1.128"), IPAddress.Parse("192.168.1.192")), + CreateIPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.128")) + }, + { + true, + CreateIPAddressRange(IPAddress.Parse("::ffff"), IPAddress.Parse("1::ffff")), + CreateIPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("::ffff")) + }, + // full head and tail overlapped + { + true, + CreateIPAddressRange(IPAddress.Parse("192.168.1.128"), IPAddress.Parse("192.168.1.192")), + CreateIPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.255")) + }, + { + true, + CreateIPAddressRange(IPAddress.Parse("::ff00"), IPAddress.Parse("ff::ff00")), + CreateIPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("ffff::ffff")) + }, + // tail only overlapped + { + false, + CreateIPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.192")), + CreateIPAddressRange(IPAddress.Parse("192.168.1.192"), IPAddress.Parse("192.168.1.255")) + }, + { + false, + CreateIPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("::ffff")), + CreateIPAddressRange(IPAddress.Parse("::ffff"), IPAddress.Parse("1::ffff")) + }, + // not touching + { + false, + CreateIPAddressRange(IPAddress.Parse("10.1.1.0"), IPAddress.Parse("10.1.1.100")), + CreateIPAddressRange(IPAddress.Parse("192.168.1.1"), IPAddress.Parse("192.168.1.128")) + }, + { + false, + CreateIPAddressRange(IPAddress.Parse("ef::"), IPAddress.Parse("ef::f")), + CreateIPAddressRange(IPAddress.Parse("ab::"), IPAddress.Parse("ab::f")) + }, + // disparate ranges + { + false, + CreateIPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.2.0")), + CreateIPAddressRange(IPAddress.Parse("10.1.1.1"), IPAddress.Parse("10.1.5.0")) + }, + { + false, + CreateIPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("::ff")), + CreateIPAddressRange(IPAddress.Parse("f::"), IPAddress.Parse("f:1::")) + }, + }; + return data; + } + } + + /// Verifies HeadOverlappedBy returns the expected result for the given pair of ranges. + /// Expected result. + /// Left operand range. + /// Right operand range. + [Theory] + [MemberData(nameof(HeadOverlappedBy_Test_Data))] + public void HeadOverlappedBy_Test(bool expected, IIPAddressRange left, IIPAddressRange right) + { + // Arrange + // Act + var result = left.HeadOverlappedBy(right); + + // Assert + Assert.Equal(expected, result); + } + + #endregion // end: HeadOverlappedBy + + #region TailOverlappedBy + + /// + /// Gets parameters: expected (bool), left (IIPAddressRange), right (IIPAddressRange) + /// + /// + /// Parameters: expected (bool), left (IIPAddressRange), right (IIPAddressRange) + /// + public static TheoryData TailOverlappedBy_Test_Data + { + get + { + var ipv4Range = CreateIPAddressRange(IPAddress.Any, IPAddress.Broadcast); + var ipv6Range = CreateIPAddressRange( + IPAddress.IPv6Any, + IPAddress.Parse("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff") + ); + + var data = new TheoryData + { + // null overlap checking + { false, ipv4Range, null }, + { false, ipv6Range, null }, + // same overlap checking + { true, ipv4Range, ipv4Range }, + { true, ipv6Range, ipv6Range }, + // differing address families + { false, ipv4Range, ipv6Range }, + { false, ipv6Range, ipv4Range }, + // head only overlapped + { + false, + CreateIPAddressRange(IPAddress.Parse("192.168.1.128"), IPAddress.Parse("192.168.1.192")), + CreateIPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.128")) + }, + { + false, + CreateIPAddressRange(IPAddress.Parse("::ffff"), IPAddress.Parse("1::ffff")), + CreateIPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("::ffff")) + }, + // full head and tail overlapped + { + true, + CreateIPAddressRange(IPAddress.Parse("192.168.1.128"), IPAddress.Parse("192.168.1.192")), + CreateIPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.255")) + }, + { + true, + CreateIPAddressRange(IPAddress.Parse("::ff00"), IPAddress.Parse("ff::ff00")), + CreateIPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("ffff::ffff")) + }, + // tail only overlapped + { + true, + CreateIPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.192")), + CreateIPAddressRange(IPAddress.Parse("192.168.1.192"), IPAddress.Parse("192.168.1.255")) + }, + { + true, + CreateIPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("::ffff")), + CreateIPAddressRange(IPAddress.Parse("::ffff"), IPAddress.Parse("1::ffff")) + }, + // not touching + { + false, + CreateIPAddressRange(IPAddress.Parse("10.1.1.0"), IPAddress.Parse("10.1.1.100")), + CreateIPAddressRange(IPAddress.Parse("192.168.1.1"), IPAddress.Parse("192.168.1.128")) + }, + { + false, + CreateIPAddressRange(IPAddress.Parse("ef::"), IPAddress.Parse("ef::f")), + CreateIPAddressRange(IPAddress.Parse("ab::"), IPAddress.Parse("ab::f")) + }, + // disparate ranges + { + false, + CreateIPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.2.0")), + CreateIPAddressRange(IPAddress.Parse("10.1.1.1"), IPAddress.Parse("10.1.5.0")) + }, + { + false, + CreateIPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("::ff")), + CreateIPAddressRange(IPAddress.Parse("f::"), IPAddress.Parse("f:1::")) + }, + }; + return data; + } + } + + /// Verifies TailOverlappedBy returns the expected result for the given pair of ranges. + /// Expected result. + /// Left operand range. + /// Right operand range. + [Theory] + [MemberData(nameof(TailOverlappedBy_Test_Data))] + public void TailOverlappedBy_Test(bool expected, IIPAddressRange left, IIPAddressRange right) + { + // Arrange + // Act + var result = left.TailOverlappedBy(right); + + // Assert + Assert.Equal(expected, result); + } + + #endregion // end: TailOverlappedBy + + #region Overlaps + + /// + /// Gets parameters: expected (bool), left (IIPAddressRange), right (IIPAddressRange) + /// + /// + /// Parameters: expected (bool), left (IIPAddressRange), right (IIPAddressRange) + /// + public static TheoryData Overlaps_Test_Data + { + get + { + var ipv4Range = CreateIPAddressRange(IPAddress.Any, IPAddress.Broadcast); + var ipv6Range = CreateIPAddressRange( + IPAddress.IPv6Any, + IPAddress.Parse("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff") + ); + + var data = new TheoryData + { + // null overlap checking + { false, ipv4Range, null }, + { false, ipv6Range, null }, + // same overlap checking (also covers value equality since same values produce same display name) + { true, ipv4Range, ipv4Range }, + { true, ipv6Range, ipv6Range }, + // differing address families + { false, ipv4Range, ipv6Range }, + { false, ipv6Range, ipv4Range }, + // head only overlapped + { + true, + CreateIPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.128")), + CreateIPAddressRange(IPAddress.Parse("192.168.1.128"), IPAddress.Parse("192.168.1.192")) + }, + { + true, + CreateIPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("::ffff")), + CreateIPAddressRange(IPAddress.Parse("::ffff"), IPAddress.Parse("1::ffff")) + }, + // full head and tail overlapped + { + true, + CreateIPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.255")), + CreateIPAddressRange(IPAddress.Parse("192.168.1.128"), IPAddress.Parse("192.168.1.192")) + }, + { + true, + CreateIPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("ffff::ffff")), + CreateIPAddressRange(IPAddress.Parse("::ff00"), IPAddress.Parse("ff::ff00")) + }, + // tail only overlapped + { + true, + CreateIPAddressRange(IPAddress.Parse("192.168.1.192"), IPAddress.Parse("192.168.1.255")), + CreateIPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.192")) + }, + { + true, + CreateIPAddressRange(IPAddress.Parse("::ffff"), IPAddress.Parse("1::ffff")), + CreateIPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("::ffff")) + }, + // not touching + { + false, + CreateIPAddressRange(IPAddress.Parse("192.168.1.1"), IPAddress.Parse("192.168.1.128")), + CreateIPAddressRange(IPAddress.Parse("10.1.1.0"), IPAddress.Parse("10.1.1.100")) + }, + { + false, + CreateIPAddressRange(IPAddress.Parse("ab::"), IPAddress.Parse("ab::f")), + CreateIPAddressRange(IPAddress.Parse("ef::"), IPAddress.Parse("ef::f")) + }, + // disparate ranges + { + false, + CreateIPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.2.0")), + CreateIPAddressRange(IPAddress.Parse("10.1.1.1"), IPAddress.Parse("10.1.5.0")) + }, + { + false, + CreateIPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("::ff")), + CreateIPAddressRange(IPAddress.Parse("f::"), IPAddress.Parse("f:1::")) + }, + // wholly contained - both directions must return true (symmetry) + { + true, + CreateIPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.255")), + CreateIPAddressRange(IPAddress.Parse("192.168.1.64"), IPAddress.Parse("192.168.1.192")) + }, + { + true, + CreateIPAddressRange(IPAddress.Parse("192.168.1.64"), IPAddress.Parse("192.168.1.192")), + CreateIPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.255")) + }, + { + true, + CreateIPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("ffff::ffff")), + CreateIPAddressRange(IPAddress.Parse("1::"), IPAddress.Parse("fffe::ffff")) + }, + { + true, + CreateIPAddressRange(IPAddress.Parse("1::"), IPAddress.Parse("fffe::ffff")), + CreateIPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("ffff::ffff")) + }, + }; + return data; + } + } + + /// Verifies that Overlaps returns the expected result for the given pair of ranges. + /// Expected result. + /// Left operand range. + /// Right operand range. + [Theory] + [MemberData(nameof(Overlaps_Test_Data))] + public void Overlaps_Test(bool expected, IIPAddressRange left, IIPAddressRange right) + { + // Arrange + // Act + var result = left.Overlaps(right); + + // Assert + Assert.Equal(expected, result); + } + + #endregion // end: Overlaps + + #region Touches + + /// + /// Gets parameters: expected (bool), left (IIPAddressRange), right (IIPAddressRange) + /// + /// + /// Parameters: expected (bool), left (IIPAddressRange), right (IIPAddressRange) + /// + public static TheoryData Touches_Test_Data + { + get + { + var ipv4Range = CreateIPAddressRange(IPAddress.Any, IPAddress.Broadcast); + var ipv6Range = CreateIPAddressRange( + IPAddress.IPv6Any, + IPAddress.Parse("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff") + ); + + var data = new TheoryData + { + // null overlap checking + { false, CreateIPAddressRange(IPAddress.Any, IPAddress.Broadcast), null }, + { + false, + CreateIPAddressRange(IPAddress.IPv6Any, IPAddress.Parse("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")), + null + }, + // differing address families + { false, ipv4Range, ipv6Range }, + { false, ipv6Range, ipv4Range }, + // left tail touches right head + { + true, + CreateIPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.100")), + CreateIPAddressRange(IPAddress.Parse("192.168.1.101"), IPAddress.Parse("192.168.1.200")) + }, + { + true, + CreateIPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("::abcd")), + CreateIPAddressRange(IPAddress.Parse("::abce"), IPAddress.Parse("::ffff")) + }, + // left head touches right tail + { + true, + CreateIPAddressRange(IPAddress.Parse("192.168.1.101"), IPAddress.Parse("192.168.1.200")), + CreateIPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.100")) + }, + { + true, + CreateIPAddressRange(IPAddress.Parse("::abce"), IPAddress.Parse("::ffff")), + CreateIPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("::abcd")) + }, + // head only overlapped + { + false, + CreateIPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.128")), + CreateIPAddressRange(IPAddress.Parse("192.168.1.128"), IPAddress.Parse("192.168.1.192")) + }, + { + false, + CreateIPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("::ffff")), + CreateIPAddressRange(IPAddress.Parse("::ffff"), IPAddress.Parse("1::ffff")) + }, + // full head and tail overlapped + { + false, + CreateIPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.255")), + CreateIPAddressRange(IPAddress.Parse("192.168.1.128"), IPAddress.Parse("192.168.1.192")) + }, + { + false, + CreateIPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("ffff::ffff")), + CreateIPAddressRange(IPAddress.Parse("::ff00"), IPAddress.Parse("ff::ff00")) + }, + // tail only overlapped + { + false, + CreateIPAddressRange(IPAddress.Parse("192.168.1.192"), IPAddress.Parse("192.168.1.255")), + CreateIPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.192")) + }, + { + false, + CreateIPAddressRange(IPAddress.Parse("::ffff"), IPAddress.Parse("1::ffff")), + CreateIPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("::ffff")) + }, + // disparate ranges + { + false, + CreateIPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.2.0")), + CreateIPAddressRange(IPAddress.Parse("10.1.1.1"), IPAddress.Parse("10.1.5.0")) + }, + { + false, + CreateIPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("::ff")), + CreateIPAddressRange(IPAddress.Parse("f::"), IPAddress.Parse("f:1::")) + }, + // this tail at max + { + false, + CreateIPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("255.255.255.255")), + CreateIPAddressRange(IPAddress.Parse("10.1.1.1"), IPAddress.Parse("10.1.5.0")) + }, + { + false, + CreateIPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")), + CreateIPAddressRange(IPAddress.Parse("f::"), IPAddress.Parse("f:1::")) + }, + // that tail at max + { + false, + CreateIPAddressRange(IPAddress.Parse("10.1.1.1"), IPAddress.Parse("10.1.5.0")), + CreateIPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("255.255.255.255")) + }, + { + false, + CreateIPAddressRange(IPAddress.Parse("f::"), IPAddress.Parse("f:1::")), + CreateIPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")) + }, + }; + return data; + } + } + + /// Verifies that Touches returns the expected result for the given pair of ranges. + /// Expected result. + /// Left operand range. + /// Right operand range. + [Theory] + [MemberData(nameof(Touches_Test_Data))] + public void Touches_Test(bool expected, IIPAddressRange left, IIPAddressRange right) + { + // Arrange + // Act + var result = left.Touches(right); + + // Assert + Assert.Equal(expected, result); + } + + #endregion // end: Touches + + #endregion // end: Overlap and Touches + + #region Contains Any/All Public/Private Addresses + + /// + /// Gets parameters: expectedHasPublic (bool), expectedHasPrivate (bool), range (IIPAddressRange) + /// + /// + /// Parameters: expectedHasPublic (bool), expectedHasPrivate (bool), range (IIPAddressRange) + /// + public static TheoryData ContainsPublicPrivate_Data + { + get + { + var data = new TheoryData(); + + // known private ranges + foreach (var subnet in SubnetUtilities.PrivateIPAddressRangesList) + { + // has private + data.Add(false, true, CreateIPAddressRange(subnet.Head, subnet.Tail)); // on the border of private + data.Add(false, true, CreateIPAddressRange(subnet.Head.Increment(2), subnet.Tail.Increment(-2))); // wholly inside of private + data.Add(false, true, CreateIPAddressRange(subnet.Head.Increment(2), subnet.Tail)); // partially within of private + data.Add(false, true, CreateIPAddressRange(subnet.Head, subnet.Tail.Increment(-2))); // partially within of private + + // has private and public + data.Add(true, true, CreateIPAddressRange(subnet.Head.Increment(-2), subnet.Tail)); // partially outside of private + data.Add(true, true, CreateIPAddressRange(subnet.Head, subnet.Tail.Increment(2))); // partially outside of private + } + + // public only + var publicSpace = new (IPAddress head, IPAddress tail)[] + { + (IPAddress.Parse("128.64.32.0"), IPAddress.Parse("128.64.32.16")), + (IPAddress.Parse("FFFF:7FFF:3FFF::"), IPAddress.Parse("FFFF:7FFF:3FFF:1FFF::")), + }; + + foreach (var (head, tail) in publicSpace) + { + data.Add(true, false, CreateIPAddressRange(head, tail)); // on the border of public + data.Add(true, false, CreateIPAddressRange(head.Increment(2), tail.Increment(-2))); // wholly inside public + } + + return data; + } + } + + /// Verifies ContainsAnyPrivateAddresses returns the expected result for ranges with known public/private composition. + /// Whether the range is expected to contain public addresses. + /// Whether the range is expected to contain private addresses. + /// The range under test. + [Theory] + [MemberData(nameof(ContainsPublicPrivate_Data))] + public void ContainsAnyPrivateAddresses_Test(bool expectedHasPublic, bool expectedHasPrivate, IIPAddressRange range) + { + // Arrange + // Act + var result = range.ContainsAnyPrivateAddresses(); + + this._testOutputHelper.WriteLine($"pr:{expectedHasPrivate} pu:{expectedHasPublic} {range.Head} - {range.Tail}"); + + // Assert + Assert.Equal(expectedHasPrivate, result); + } + + /// Verifies ContainsAllPrivateAddresses returns true only when the range is wholly within private space. + /// Whether the range is expected to contain public addresses. + /// Whether the range is expected to contain private addresses. + /// The range under test. + [Theory] + [MemberData(nameof(ContainsPublicPrivate_Data))] + public void ContainsAllPrivateAddresses_Test(bool expectedHasPublic, bool expectedHasPrivate, IIPAddressRange range) + { + // Arrange + // Act + var result = range.ContainsAllPrivateAddresses(); + + this._testOutputHelper.WriteLine($"pr:{expectedHasPrivate} pu:{expectedHasPublic} {range.Head} - {range.Tail}"); + + // Assert + Assert.Equal(expectedHasPrivate && !expectedHasPublic, result); + } + + /// Verifies ContainsAnyPublicAddresses returns the expected result for ranges with known public/private composition. + /// Whether the range is expected to contain public addresses. + /// Whether the range is expected to contain private addresses. + /// The range under test. + [Theory] + [MemberData(nameof(ContainsPublicPrivate_Data))] + public void ContainsAnyPublicAddresses_Test(bool expectedHasPublic, bool expectedHasPrivate, IIPAddressRange range) + { + // Arrange + // Act + var result = range.ContainsAnyPublicAddresses(); + + this._testOutputHelper.WriteLine($"pr:{expectedHasPrivate} pu:{expectedHasPublic} {range.Head} - {range.Tail}"); + + // Assert + Assert.Equal(expectedHasPublic, result); + } + + /// Verifies ContainsAllPublicAddresses returns true only when the range has no private addresses. + /// Whether the range is expected to contain public addresses. + /// Whether the range is expected to contain private addresses. + /// The range under test. + [Theory] + [MemberData(nameof(ContainsPublicPrivate_Data))] + public void ContainsAllPublicAddresses_Test(bool expectedHasPublic, bool expectedHasPrivate, IIPAddressRange range) + { + // Arrange + // Act + var result = range.ContainsAllPublicAddresses(); + + this._testOutputHelper.WriteLine($"pr:{expectedHasPrivate} pu:{expectedHasPublic} {range.Head} - {range.Tail}"); + + // Assert + Assert.Equal(!expectedHasPrivate && expectedHasPublic, result); + } + + /// Regression: a range whose endpoints are both public but spans a private block must report private addresses present. + [Fact] + public void ContainsAnyPrivateAddresses_RangeSpansPrivateBlock_WithPublicEndpoints_Test() + { + // Regression: endpoints 11.0.0.0 and 173.0.0.0 are both public, but the range + // spans 172.16.0.0/12 - the endpoint heuristic incorrectly returned false. + var range = CreateIPAddressRange(IPAddress.Parse("11.0.0.0"), IPAddress.Parse("173.0.0.0")); + + Assert.True(range.ContainsAnyPrivateAddresses()); + } + + /// Regression: a range whose endpoints are both public but spans a private block must not be classified as all-public. + [Fact] + public void ContainsAllPublicAddresses_RangeSpansPrivateBlock_WithPublicEndpoints_Test() + { + // Regression: endpoints are both public, but the range spans 172.16.0.0/12 - + // not all addresses are public, so this must return false. + var range = CreateIPAddressRange(IPAddress.Parse("11.0.0.0"), IPAddress.Parse("173.0.0.0")); + + Assert.False(range.ContainsAllPublicAddresses()); + } + + /// Verifies ContainsAnyPrivateAddresses and ContainsAllPublicAddresses are logical inverses. + /// Whether the range is expected to contain public addresses. + /// Whether the range is expected to contain private addresses. + /// The range under test. + [Theory] + [MemberData(nameof(ContainsPublicPrivate_Data))] + public void ContainsAnyPrivateAddresses_IsInverseOf_ContainsAllPublicAddresses_Test( + bool expectedHasPublic, + bool expectedHasPrivate, + IIPAddressRange range + ) + { + // ContainsAnyPrivateAddresses and ContainsAllPublicAddresses must be logical inverses. + var hasPrivate = range.ContainsAnyPrivateAddresses(); + var allPublic = range.ContainsAllPublicAddresses(); + + this._testOutputHelper.WriteLine($"pr:{expectedHasPrivate} pu:{expectedHasPublic} {range.Head} - {range.Tail}"); + + Assert.NotEqual(hasPrivate, allPublic); + } + + /// Verifies ContainsAllPrivateAddresses and ContainsAnyPublicAddresses are logical inverses. + /// Whether the range is expected to contain public addresses. + /// Whether the range is expected to contain private addresses. + /// The range under test. + [Theory] + [MemberData(nameof(ContainsPublicPrivate_Data))] + public void ContainsAllPrivateAddresses_IsInverseOf_ContainsAnyPublicAddresses_Test( + bool expectedHasPublic, + bool expectedHasPrivate, + IIPAddressRange range + ) + { + // ContainsAllPrivateAddresses and ContainsAnyPublicAddresses must be logical inverses. + var allPrivate = range.ContainsAllPrivateAddresses(); + var hasPublic = range.ContainsAnyPublicAddresses(); + + this._testOutputHelper.WriteLine($"pr:{expectedHasPrivate} pu:{expectedHasPublic} {range.Head} - {range.Tail}"); + + Assert.NotEqual(allPrivate, hasPublic); + } + + #endregion // end: Contains Any/All Public/Private Addresses + } +} diff --git a/src/Arcus.Tests/AbstractIPAddressRangeTests.cs b/src/Arcus.Tests/AbstractIPAddressRangeTests.cs index e477e88..1a4876d 100644 --- a/src/Arcus.Tests/AbstractIPAddressRangeTests.cs +++ b/src/Arcus.Tests/AbstractIPAddressRangeTests.cs @@ -1,336 +1,102 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.Sockets; +using System.Net; using System.Numerics; +using System.Reflection; using Arcus.Math; -using Arcus.Utilities; -using NSubstitute; -using Xunit; +using Arcus.Tests.XunitSerializers; + +[assembly: RegisterXunitSerializer(typeof(IPAddressXunitSerializer), typeof(IPAddress))] +[assembly: RegisterXunitSerializer(typeof(IIPAddressRangeXunitSerializer), typeof(IIPAddressRange))] namespace Arcus.Tests { - public class AbstractIPAddressRangeTests + public partial class AbstractIPAddressRangeTests(ITestOutputHelper testOutputHelper) { #region Setup / Teardown - public AbstractIPAddressRangeTests(ITestOutputHelper testOutputHelper) - { - this._testOutputHelper = testOutputHelper; - } - - private readonly ITestOutputHelper _testOutputHelper; - - #endregion - - #region Deconstructors - - [Fact] - public void Deconstruct_Head_Tail_Test() - { - // Arrange - const string headString = "192.168.1.1"; - const string tailString = "192.168.1.3"; - var head = IPAddress.Parse(headString); - var tail = IPAddress.Parse(tailString); + private readonly ITestOutputHelper _testOutputHelper = testOutputHelper; - // Act - var (resultHead, resultTail) = CreateSubstituteIPAddressRange(head, tail); - - // Assert - Assert.Equal(resultHead, head); - Assert.Equal(resultTail, tail); - } - - #endregion // end: Deconstructors + #endregion // end: Setup / Teardown #region other members - private static AbstractIPAddressRange CreateSubstituteIPAddressRange(IPAddress head, IPAddress tail) - { - return Substitute.For(head, tail); - } - - #endregion - - #region IEnumerable - - [Theory] - [InlineData("::", "::")] - [InlineData("::", "::FF")] - [InlineData("0.0.0.0", "0.0.0.0")] - [InlineData("192.168.1.1", "192.168.1.1")] - [InlineData("192.168.1.1", "192.168.1.5")] - [InlineData("255.255.255.128", "255.255.255.255")] - [InlineData("255.255.255.255", "255.255.255.255")] - [InlineData("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ff00", "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")] - [InlineData("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")] - public void GetEnumerator_Test(string headAddressString, string tailAddressString) - { - // Arrange - var head = IPAddress.Parse(headAddressString); - var tail = IPAddress.Parse(tailAddressString); - - var iPAddressRange = CreateSubstituteIPAddressRange(head, tail); - - // Act - var ipAddressArray = iPAddressRange.ToArray(); - - // Assert - Assert.Equal(iPAddressRange.Count(), ipAddressArray.Length); - Assert.Equal(head, ipAddressArray[0]); - Assert.Equal(tail, ipAddressArray[ipAddressArray.Length - 1]); - } - - [Theory] - [InlineData("::", "::")] - [InlineData("::", "::FF")] - [InlineData("0.0.0.0", "0.0.0.0")] - [InlineData("192.168.1.1", "192.168.1.1")] - [InlineData("192.168.1.1", "192.168.1.5")] - [InlineData("255.255.255.128", "255.255.255.255")] - [InlineData("255.255.255.255", "255.255.255.255")] - [InlineData("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ff00", "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")] - [InlineData("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")] - public void GetIEnumerableEnumerator_Test(string headAddressString, string tailAddressString) - { - // Arrange - var head = IPAddress.Parse(headAddressString); - var tail = IPAddress.Parse(tailAddressString); - - var iPAddressRange = CreateSubstituteIPAddressRange(head, tail); - - // Act - var result = new List(); - var enumerator = ((IEnumerable)iPAddressRange).GetEnumerator(); - while (enumerator.MoveNext()) - { - result.Add(enumerator.Current as IPAddress); - } - - // Assert - Assert.Equal(iPAddressRange.Count(), result.Count); - Assert.Equal(head, result[0]); - Assert.Equal(tail, result[result.Count - 1]); - } - - #endregion // end: IEnumerable - - #region IsSingleIP - - public static IEnumerable IsSingleIP_Test_Values() - { - yield return new object[] { true, CreateSubstituteIPAddressRange(IPAddress.Any, IPAddress.Any) }; - yield return new object[] { true, CreateSubstituteIPAddressRange(IPAddress.IPv6Any, IPAddress.IPv6Any) }; - - yield return new object[] { false, CreateSubstituteIPAddressRange(IPAddress.Any, IPAddress.Broadcast) }; - yield return new object[] { false, CreateSubstituteIPAddressRange(IPAddress.IPv6Any, IPAddress.IPv6Loopback) }; - } - - [Theory] - [MemberData(nameof(IsSingleIP_Test_Values))] - public void IsSingleIP_Test(bool expected, AbstractIPAddressRange ipAddressRange) + private static IPAddressRange CreateIPAddressRange(IPAddress head, IPAddress tail) { - // Arrange - // Act - var isSingleIP = ipAddressRange.IsSingleIP; - - // Assert - Assert.Equal(expected, isSingleIP); + return new IPAddressRange(head, tail); } - #endregion // end: IsSingleIP + #endregion // end: other members #region Length / TryGetLength - public static IEnumerable Length_Test_Values() + /// + /// Gets theory data for length-related tests across IPv4, IPv6, and boundary values. + /// + /// + /// Pairs of expected length () and instances. + /// + public static TheoryData Length_Test_Data { - // single address - yield return new object[] { new BigInteger(1), CreateSubstituteIPAddressRange(IPAddress.Any, IPAddress.Any) }; - yield return new object[] - { - new BigInteger(1), - CreateSubstituteIPAddressRange(IPAddress.IPv6Any, IPAddress.IPv6Any), - }; - - // maximum length ipv4 - yield return new object[] - { - BigInteger.Pow(2, 32), - CreateSubstituteIPAddressRange(IPAddress.Parse("0.0.0.0"), IPAddress.Parse("255.255.255.255")), - }; - - // maximum length ipv6 - yield return new object[] - { - BigInteger.Pow(2, 128), - CreateSubstituteIPAddressRange( - IPAddress.Parse("::"), - IPAddress.Parse("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff") - ), - }; - - // ipv6 length at int.MaxValue - yield return new object[] - { - new BigInteger(int.MaxValue), - CreateSubstituteIPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("::").Increment(int.MaxValue - 1)), - }; - - // ipv6 length at long.MaxValue - yield return new object[] - { - new BigInteger(long.MaxValue), - CreateSubstituteIPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("::").Increment(long.MaxValue - 1)), - }; - - // ipv6 length at int.MaxValue + 1 - yield return new object[] + get { - new BigInteger(int.MaxValue) + 1, - CreateSubstituteIPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("::").Increment(int.MaxValue)), - }; - - // ipv6 length at long.MaxValue + 1 - yield return new object[] - { - new BigInteger(long.MaxValue) + 1, - CreateSubstituteIPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("::").Increment(long.MaxValue)), - }; + return new TheoryData + { + // single address + { new BigInteger(1), CreateIPAddressRange(IPAddress.Any, IPAddress.Any) }, + { new BigInteger(1), CreateIPAddressRange(IPAddress.IPv6Any, IPAddress.IPv6Any) }, + // maximum length ipv4 + { + BigInteger.Pow(2, 32), + CreateIPAddressRange(IPAddress.Parse("0.0.0.0"), IPAddress.Parse("255.255.255.255")) + }, + // maximum length ipv6 + { + BigInteger.Pow(2, 128), + CreateIPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")) + }, + // ipv6 length at int.MaxValue + { + new BigInteger(int.MaxValue), + CreateIPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("::").Increment(int.MaxValue - 1)) + }, + // ipv6 length at long.MaxValue + { + new BigInteger(long.MaxValue), + CreateIPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("::").Increment(long.MaxValue - 1)) + }, + // ipv6 length at int.MaxValue + 1 + { + new BigInteger(int.MaxValue) + 1, + CreateIPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("::").Increment(int.MaxValue)) + }, + // ipv6 length at long.MaxValue + 1 + { + new BigInteger(long.MaxValue) + 1, + CreateIPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("::").Increment(long.MaxValue)) + }, + }; + } } + /// Verifies that returns the expected length for a range. + /// The expected length. + /// The range to measure. [Theory] - [MemberData(nameof(Length_Test_Values))] - public static void Length_Test(BigInteger expected, AbstractIPAddressRange ipAddressRange) + [MemberData(nameof(Length_Test_Data))] + public void Length_Test(BigInteger expected, IPAddressRange ipAddressRange) { // Arrange // Act - var result = ipAddressRange.Length; // Assert Assert.Equal(expected, result); } - [Theory] - [MemberData(nameof(Length_Test_Values))] - public static void TryGetLength_Integer_Test(BigInteger expected, AbstractIPAddressRange ipAddressRange) - { - // Arrange - // Act - var success = ipAddressRange.TryGetLength(out int length); - - // Assert - Assert.Equal(expected <= int.MaxValue, success); - Assert.Equal(expected <= int.MaxValue ? (int)expected : -1, length); - } - - [Theory] - [MemberData(nameof(Length_Test_Values))] - public static void TryGetLength_Long_Test(BigInteger expected, AbstractIPAddressRange ipAddressRange) - { - // Arrange - // Act - var success = ipAddressRange.TryGetLength(out long length); - - // Assert - Assert.Equal(expected <= long.MaxValue, success); - Assert.Equal(expected <= long.MaxValue ? (long)expected : -1, length); - } - #endregion // end: Length / TryGetLength - #region Contains - - #region Contains(IPAddress) - - [Theory] - [InlineData(true, "::", "::ABCD", "::1234")] // IPv6 range contains IPv6 - [InlineData(false, "::", "::ABCD", "::FFFF")] // IPv6 range doesn't contains IPv6 - [InlineData(true, "::", "::ABCD", "::")] // IPv6 range contains IPv6 head - [InlineData(true, "::", "::ABCD", "::ABCD")] // IPv6 range contains IPv6 tail - [InlineData(false, "::", "::ABCD", "192.168.1.1")] // IPv6 range doesn't contains IPv4 - [InlineData(false, "::", "::ABCD", null)] // IPv6 range doesn't contains null - [InlineData(true, "192.168.0.1", "192.168.0.254", "192.168.0.128")] // IPv4 range contains IPv4 - [InlineData(false, "192.168.0.1", "192.168.0.254", "10.1.1.1")] // IPv4 range doesn't contains IPv4 - [InlineData(true, "192.168.0.1", "192.168.0.254", "192.168.0.1")] // IPv4 range contains IPv4 head - [InlineData(true, "192.168.0.1", "192.168.0.254", "192.168.0.254")] // IPv4 range contains IPv4 tail - [InlineData(false, "192.168.0.1", "192.168.0.254", "::")] // IPv4 range doesn't contains IPv6 - [InlineData(false, "192.168.0.1", "192.168.0.254", null)] // IPv4 range doesn't contains null - public void ContainsIPAddress_Test(bool expected, string headString, string tailString, string addressString) - { - // Arrange - var head = IPAddress.Parse(headString); - var tail = IPAddress.Parse(tailString); - _ = IPAddress.TryParse(addressString, out var containsAddress); - - var iPAddressRange = CreateSubstituteIPAddressRange(head, tail); - - // Act - var result = iPAddressRange.Contains(containsAddress); - - // Assert - Assert.Equal(expected, result); - } - - #endregion // end: Contains(IPAddress) - - #region Contains(IIPAddressRange) - - [Theory] - [InlineData(true, "::", "::ABCD", "::", "::ABCD")] // IPv6 range contains self - [InlineData(true, "::", "::ABCD", "::1", "::1234")] // IPv6 range contains internal IPv6 range - [InlineData(false, "::", "::ABCD", "AA::FFFF", "AB::FFFF")] // IPv6 range doesn't contains external IPv6 range - [InlineData(false, "A::", "A::ABCD", "::", "A::")] // IPv6 range doesn't contains IPv6 overhang head - [InlineData(false, "::", "::ABCD", "::", "AA::FFFF")] // IPv6 range doesn't contains IPv6 overhang tail - [InlineData(false, "::", "::ABCD", "192.168.1.1", "192.168.1.10")] // IPv6 range doesn't contains IPv4 range - [InlineData(false, "::", "A::ABCD", null, null)] // IPv6 range doesn't contain null range - [InlineData(true, "128.0.0.1", "128.0.0.254", "128.0.0.1", "128.0.0.254")] // IPv4 range contains self - [InlineData(true, "128.0.0.1", "128.0.0.254", "128.0.0.5", "128.0.0.100")] // IPv4 range contains internal IPv4 range - [InlineData(false, "128.0.0.1", "128.0.0.254", "140.0.0.0", "145.0.0.0")] // IPv4 range doesn't contains external IPv4 range - [InlineData(false, "128.0.0.1", "128.0.0.254", "", "128.0.0.100")] // IPv4 range doesn't contains null head IPv4 range - [InlineData(false, "128.0.0.1", "128.0.0.254", "128.0.0.5", "")] // IPv4 range doesn't contains null tail IPv4 range - [InlineData(false, "128.0.0.1", "128.0.0.254", "0.0.0.0", "128.0.0.100")] // IPv4 range doesn't contains IPv4 overhang head - [InlineData(false, "128.0.0.1", "128.0.0.254", "128.0.0.5", "145.0.0.0")] // IPv4 range doesn't contains IPv4 overhang tail - [InlineData(false, "128.0.0.1", "128.0.0.254", "::ABCD", "42::ABCD")] // IPv4 range doesn't contains IPv6 range - [InlineData(false, "128.0.0.1", "128.0.0.254", null, null)] // IPv4 range doesn't contain null range - public void ContainsIPAddressRange_Test( - bool expected, - string headString, - string tailString, - string containsHeadString, - string containsTailString - ) - { - // Arrange - var head = IPAddress.Parse(headString); - var tail = IPAddress.Parse(tailString); - - var substituteIPAddressSubRange = - IPAddress.TryParse(containsHeadString, out var subhead) - && IPAddress.TryParse(containsTailString, out var subtail) - ? Substitute.For(subhead, subtail) - : null; - - var iPAddressRange = CreateSubstituteIPAddressRange(head, tail); - - var containedIPAddressRange = substituteIPAddressSubRange; - - // Act - var result = iPAddressRange.Contains(containedIPAddressRange); - - // Assert - Assert.Equal(expected, result); - } - - #endregion // end: Contains(IIPAddressRange) - - #endregion // end: Contains - #region Class + /// Verifies that AbstractIPAddressRange implements IIPAddressRange. [Fact] public void Implementation_Test() { @@ -342,6 +108,7 @@ public void Implementation_Test() Assert.True(typeof(IIPAddressRange).IsAssignableFrom(type)); } + /// Verifies that AbstractIPAddressRange is declared as an abstract class. [Fact] public void AbstractClass_Test() { @@ -357,117 +124,11 @@ public void AbstractClass_Test() #endregion // end: Class - #region IEnumerable / IEnumerable - - [Fact] //Test that the expected addresses appear in the given IPv6 range - public void Enumerable_IPv6_ContainsExpected_Test() - { - // Arrange - const string headString = "::a"; - const string tailString = "::c"; - var head = IPAddress.Parse(headString); - var tail = IPAddress.Parse(tailString); - - var iPAddressRange = CreateSubstituteIPAddressRange(head, tail); - - // Act - var result = iPAddressRange.ToList(); - - // Assert - Assert.Equal(3, result.Count); - Assert.Equal(new[] { head, IPAddress.Parse("::b"), tail }, result); - } - - [Fact] - public void Enumerable_IPv6_TakePastEnd_Test() - { - // Arrange - var head = IPAddress.Parse("ffff:ffff:ffff:ffff:ffff:ffff:ffff:fff0"); - var tail = IPAddress.Parse("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"); - - var iPAddressRange = CreateSubstituteIPAddressRange(head, tail); - - // Act - var result = iPAddressRange.Take(100).ToList(); - - // Assert - Assert.Equal(16, result.Count); - Assert.Equal(tail, result[result.Count - 1]); - } - - [Fact] - public void Enumerable_IPv4_TakePastEnd_Test() - { - // Arrange - var head = IPAddress.Parse("255.255.255.240"); - var tail = IPAddress.Parse("255.255.255.255"); - - var iPAddressRange = CreateSubstituteIPAddressRange(head, tail); - - // Act - var result = iPAddressRange.Take(100).ToList(); - - // Assert - Assert.Equal(16, result.Count); - Assert.Equal(tail, result[result.Count - 1]); - } - - [Fact] // Test that the expected addresses appear in the given IPv4 range - public void Enumerable_IPv4_ContainsExpected_Test() - { - // Arrange - const string headString = "192.168.1.1"; - const string tailString = "192.168.1.3"; - var head = IPAddress.Parse(headString); - var tail = IPAddress.Parse(tailString); - - var iPAddressRange = CreateSubstituteIPAddressRange(head, tail); - - // Act - var result = iPAddressRange.ToList(); - - // Assert - Assert.Equal(3, result.Count); - Assert.Equal(new[] { head, IPAddress.Parse("192.168.1.2"), tail }, result); - } - - [Fact] // Test that a range of one returns a single address when Addresses is called - public void Enumerable_SameHead_And_SameTail_ReturnsSingle_Test() - { - // Arrange - var ipAddress = IPAddress.Parse("192.168.1.1"); - - var iPAddressRange = CreateSubstituteIPAddressRange(ipAddress, ipAddress); - - // Act - var addresses = iPAddressRange.ToArray(); - - // Assert - Assert.Single(addresses); - Assert.Equal(ipAddress, addresses.Single()); - } - - [Fact] - public void Enumerable_ReasonableIteration_Test() - { - // Arrange - - var iPAddressRange = CreateSubstituteIPAddressRange( - IPAddress.Parse("::"), - IPAddress.Parse("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff") - ); - - // Act - var addresses = iPAddressRange.Skip(10).Take(10).ToArray(); - - // Assert - Assert.NotNull(addresses); // if this test completed it likely means that we didn't iterate through 2^128 ip addresses to skip 10 and take 10 - } - - #endregion // end: IEnumerable / IEnumerable - #region Ctor + /// Verifies the constructor sets Head and Tail correctly for valid same-family address pairs. + /// The head IP address string. + /// The tail IP address string. [Theory] [InlineData("192.168.1.1", "192.168.1.5")] [InlineData("::beef", "::dead")] @@ -478,13 +139,16 @@ public void Ctor_HappyPath_Test(string headString, string tailString) var tail = IPAddress.Parse(tailString); // Act - var iPAddressRange = CreateSubstituteIPAddressRange(head, tail); + var iPAddressRange = CreateIPAddressRange(head, tail); // Assert Assert.Equal(head, iPAddressRange.Head); Assert.Equal(tail, iPAddressRange.Tail); } + /// Verifies the constructor throws ArgumentNullException when either address is null. + /// The head IP address string. + /// The tail IP address string. [Theory] [InlineData("192.168.1.1", null)] [InlineData(null, "192.168.1.5")] @@ -498,10 +162,12 @@ public void Ctor_Null_Input_Throws_ArgumentNullException_Test(string headString, // Act // Assert - var exception = Assert.ThrowsAny(() => CreateSubstituteIPAddressRange(head, tail)); - Assert.IsAssignableFrom(exception.InnerException); + Assert.Throws(() => CreateIPAddressRange(head, tail)); } + /// Verifies the constructor throws InvalidOperationException when head and tail are from different address families. + /// The head IP address string. + /// The tail IP address string. [Theory] [InlineData("192.168.1.1", "::beef")] [InlineData("::beef", "192.168.1.5")] @@ -513,14 +179,17 @@ public void Ctor_MismatchAddressFamilies_Throws_InvalidOperationException_Test(s // Act // Assert - var exception = Assert.ThrowsAny(() => CreateSubstituteIPAddressRange(head, tail)); - Assert.IsAssignableFrom(exception.InnerException); + var exception = Assert.Throws(() => CreateIPAddressRange(head, tail)); + Assert.Contains("matching address families", exception.Message, System.StringComparison.OrdinalIgnoreCase); } + /// Verifies the constructor throws InvalidOperationException when tail is less than head. + /// The head IP address string. + /// The tail IP address string (less than head). [Theory] [InlineData("192.168.1.5", "192.168.1.1")] [InlineData("::dead", "::beef")] - public void Ctor_BadAddressSequencing_Throws_InvalidOperationException_Test(string headString, string tailString) + public void Ctor_TailBeforeHead_Throws_InvalidOperationException_Test(string headString, string tailString) { // Arrange var head = IPAddress.Parse(headString); @@ -528,14 +197,15 @@ public void Ctor_BadAddressSequencing_Throws_InvalidOperationException_Test(stri // Act // Assert - var exception = Assert.ThrowsAny(() => CreateSubstituteIPAddressRange(head, tail)); - Assert.IsAssignableFrom(exception.InnerException); + var exception = Assert.Throws(() => CreateIPAddressRange(head, tail)); + Assert.Contains("greater or equal", exception.Message, System.StringComparison.OrdinalIgnoreCase); } #endregion // end: Ctor #region AddressFamily + /// Verifies AddressFamily is InterNetwork and IsIPv4 is true for an IPv4 range. [Fact] public void AddressFamily_IPv4_Test() { @@ -546,15 +216,16 @@ public void AddressFamily_IPv4_Test() var head = IPAddress.Parse(headString); var tail = IPAddress.Parse(tailString); - var iPAddressRange = CreateSubstituteIPAddressRange(head, tail); + var iPAddressRange = CreateIPAddressRange(head, tail); // Act // Assert - Assert.Equal(AddressFamily.InterNetwork, iPAddressRange.AddressFamily); + Assert.Equal(System.Net.Sockets.AddressFamily.InterNetwork, iPAddressRange.AddressFamily); Assert.True(iPAddressRange.IsIPv4); Assert.False(iPAddressRange.IsIPv6); } + /// Verifies AddressFamily is InterNetworkV6 and IsIPv6 is true for an IPv6 range. [Fact] public void AddressFamily_IPv6_Test() { @@ -565,867 +236,178 @@ public void AddressFamily_IPv6_Test() var head = IPAddress.Parse(headString); var tail = IPAddress.Parse(tailString); - var iPAddressRange = CreateSubstituteIPAddressRange(head, tail); + var iPAddressRange = CreateIPAddressRange(head, tail); // Act // Assert - Assert.Equal(AddressFamily.InterNetworkV6, iPAddressRange.AddressFamily); + Assert.Equal(System.Net.Sockets.AddressFamily.InterNetworkV6, iPAddressRange.AddressFamily); Assert.False(iPAddressRange.IsIPv4); Assert.True(iPAddressRange.IsIPv6); } #endregion // end: AddressFamily - #region Set Operations - - #region Contains - - #region Contains IIPAddressRange - - public static IEnumerable Contains_IIPAddressRange_Test_Values() - { - var ipv4Range = CreateSubstituteIPAddressRange(IPAddress.Any, IPAddress.Broadcast); - var ipv6Range = CreateSubstituteIPAddressRange( - IPAddress.IPv6Any, - IPAddress.Parse("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff") - ); - - // null overlap checking - yield return new object[] { false, ipv4Range, null }; - yield return new object[] { false, ipv6Range, null }; - - // same overlap checking - yield return new object[] { true, ipv4Range, ipv4Range }; - yield return new object[] { true, ipv6Range, ipv6Range }; - - // equal overlap checking - yield return new object[] - { - true, - CreateSubstituteIPAddressRange(IPAddress.Any, IPAddress.Broadcast), - CreateSubstituteIPAddressRange(IPAddress.Any, IPAddress.Broadcast), - }; - yield return new object[] - { - true, - CreateSubstituteIPAddressRange(IPAddress.IPv6Any, IPAddress.Parse("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")), - CreateSubstituteIPAddressRange(IPAddress.IPv6Any, IPAddress.Parse("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")), - }; - - // differing address families - yield return new object[] { false, ipv4Range, ipv6Range }; - yield return new object[] { false, ipv6Range, ipv4Range }; - - // head only overlapped - yield return new object[] - { - false, - CreateSubstituteIPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.128")), - CreateSubstituteIPAddressRange(IPAddress.Parse("192.168.1.128"), IPAddress.Parse("192.168.1.192")), - }; - yield return new object[] - { - false, - CreateSubstituteIPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("::ffff")), - CreateSubstituteIPAddressRange(IPAddress.Parse("::ffff"), IPAddress.Parse("1::ffff")), - }; - - // full head and tail overlapped - yield return new object[] - { - true, - CreateSubstituteIPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.255")), - CreateSubstituteIPAddressRange(IPAddress.Parse("192.168.1.128"), IPAddress.Parse("192.168.1.192")), - }; - yield return new object[] - { - true, - CreateSubstituteIPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("ffff::ffff")), - CreateSubstituteIPAddressRange(IPAddress.Parse("::ff00"), IPAddress.Parse("ff::ff00")), - }; - - // tail only overlapped - yield return new object[] - { - false, - CreateSubstituteIPAddressRange(IPAddress.Parse("192.168.1.192"), IPAddress.Parse("192.168.1.255")), - CreateSubstituteIPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.192")), - }; - yield return new object[] - { - false, - CreateSubstituteIPAddressRange(IPAddress.Parse("::ffff"), IPAddress.Parse("1::ffff")), - CreateSubstituteIPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("::ffff")), - }; - - // not touching - yield return new object[] - { - false, - CreateSubstituteIPAddressRange(IPAddress.Parse("192.168.1.1"), IPAddress.Parse("192.168.1.128")), - CreateSubstituteIPAddressRange(IPAddress.Parse("10.1.1.0"), IPAddress.Parse("10.1.1.100")), - }; - yield return new object[] - { - false, - CreateSubstituteIPAddressRange(IPAddress.Parse("ab::"), IPAddress.Parse("ab::f")), - CreateSubstituteIPAddressRange(IPAddress.Parse("ef::"), IPAddress.Parse("ef::f")), - }; - - // disparate ranges - yield return new object[] - { - false, - CreateSubstituteIPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.2.0")), - CreateSubstituteIPAddressRange(IPAddress.Parse("10.1.1.1"), IPAddress.Parse("10.1.5.0")), - }; - yield return new object[] - { - false, - CreateSubstituteIPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("::ff")), - CreateSubstituteIPAddressRange(IPAddress.Parse("f::"), IPAddress.Parse("f:1::")), - }; - } - - [Theory] - [MemberData(nameof(Contains_IIPAddressRange_Test_Values))] - public void Contains_IIPAddressRange_Test(bool expected, IIPAddressRange left, IIPAddressRange right) - { - // Arrange - // Act - var result = left.Contains(right); - - // Assert - Assert.Equal(expected, result); - } + #region A2: Exponent cap clamping - #endregion // end: Contains IIPAddressRange - - #region Contians IPAddress - - public static IEnumerable Contains_Test_Values() + /// + /// Verifies that is clamped to + /// the address family bit width (32 for IPv4, 128 for IPv6) rather than accepting values + /// that would silently produce an incorrect cap. + /// + [Fact] + public void MaxEnumerationExponent_ClampedToAddressFamilyBitWidth() { - var ipv4Range = CreateSubstituteIPAddressRange(IPAddress.Any, IPAddress.Broadcast); - var ipv6Range = CreateSubstituteIPAddressRange( - IPAddress.IPv6Any, - IPAddress.Parse("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff") + // IPv4: exponent 128 should clamp to 32 + var ipv4Range = new IPAddressRange( + IPAddress.Parse("10.0.0.0"), + IPAddress.Parse("10.0.0.5"), + maxEnumerationExponent: 128 ); + Assert.Equal(32, ipv4Range.MaxEnumerationExponent); - // does not contain null - yield return new object[] { false, ipv4Range, null }; - yield return new object[] { false, ipv6Range, null }; + // IPv6: exponent 128 should stay 128 (its address family bit width) + var ipv6Range = new IPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("::5"), maxEnumerationExponent: 128); + Assert.Equal(128, ipv6Range.MaxEnumerationExponent); - // differing address families - yield return new object[] { false, ipv4Range, IPAddress.IPv6Any }; - yield return new object[] { false, ipv6Range, IPAddress.Any }; - - // contains head - yield return new object[] { true, ipv4Range, ipv4Range.Head }; - yield return new object[] { true, ipv6Range, ipv6Range.Head }; - - // contains tail - yield return new object[] { true, ipv4Range, ipv4Range.Tail }; - yield return new object[] { true, ipv6Range, ipv6Range.Tail }; - - // contains all inside - var ipv4InsideRange = CreateSubstituteIPAddressRange( - IPAddress.Parse("192.168.1.0"), - IPAddress.Parse("192.168.1.5") + // normal unclamped values still pass through + var normalRange = new IPAddressRange( + IPAddress.Parse("10.0.0.0"), + IPAddress.Parse("10.0.0.5"), + maxEnumerationExponent: 4 ); - foreach (var ip in ipv4InsideRange) - { - yield return new object[] { true, ipv4InsideRange, ip }; - } - - var ipv6InsideRange = CreateSubstituteIPAddressRange(IPAddress.Parse("::ff00"), IPAddress.Parse("::ff0f")); - foreach (var ip in ipv6InsideRange) - { - yield return new object[] { true, ipv6InsideRange, ip }; - } - - // does not contain outside before - yield return new object[] - { - false, - CreateSubstituteIPAddressRange(IPAddress.Parse("192.168.1.1"), IPAddress.Parse("192.168.1.200")), - IPAddress.Parse("192.168.1.0"), - }; - yield return new object[] - { - false, - CreateSubstituteIPAddressRange(IPAddress.Parse("::ff01"), IPAddress.Parse("::ff08")), - IPAddress.Parse("::ff00"), - }; - - // does not contain outside after - yield return new object[] - { - false, - CreateSubstituteIPAddressRange(IPAddress.Parse("192.168.1.1"), IPAddress.Parse("192.168.1.200")), - IPAddress.Parse("192.168.1.201"), - }; - yield return new object[] - { - false, - CreateSubstituteIPAddressRange(IPAddress.Parse("::ff01"), IPAddress.Parse("::ff08")), - IPAddress.Parse("::ff09"), - }; + Assert.Equal(4, normalRange.MaxEnumerationExponent); } - [Theory] - [MemberData(nameof(Contains_Test_Values))] - public void Contains_Test(bool expected, IIPAddressRange range, IPAddress address) - { - // Arrange - // Act - var result = range.Contains(address); - - // Assert - Assert.Equal(expected, result); - } - - #endregion // end: Contians IPAddress - - #endregion // end: Contains - - #region Ovelap and Touches - - #region HeadOverlappedBy - - public static IEnumerable HeadOverlappedBy_Test_Values() - { - var ipv4Range = CreateSubstituteIPAddressRange(IPAddress.Any, IPAddress.Broadcast); - var ipv6Range = CreateSubstituteIPAddressRange( - IPAddress.IPv6Any, - IPAddress.Parse("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff") + /// + /// Verifies that providing a clamped exponent (128) for an IPv4 range does not + /// cause unsafe behavior — enumeration still respects actual range length. + /// + [Fact] + public void MaxEnumerationExponent_Clamped_ToIPAddresses_RespectsActualRange() + { + // IPv4 with exponent 128 gets clamped to 32; enumeration of a small + // range should still succeed (cap is 2^32, but actual range is only 6 addresses). + var range = new IPAddressRange( + IPAddress.Parse("10.0.0.0"), + IPAddress.Parse("10.0.0.5"), + maxEnumerationExponent: 128 ); - // null overlap checking - yield return new object[] { false, ipv4Range, null }; - yield return new object[] { false, ipv6Range, null }; - - // same overlap checking - yield return new object[] { true, ipv4Range, ipv4Range }; - yield return new object[] { true, ipv6Range, ipv6Range }; - - // equal overlap checking - yield return new object[] - { - true, - CreateSubstituteIPAddressRange(IPAddress.Any, IPAddress.Broadcast), - CreateSubstituteIPAddressRange(IPAddress.Any, IPAddress.Broadcast), - }; - yield return new object[] - { - true, - CreateSubstituteIPAddressRange(IPAddress.IPv6Any, IPAddress.Parse("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")), - CreateSubstituteIPAddressRange(IPAddress.IPv6Any, IPAddress.Parse("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")), - }; - - // differing address families - yield return new object[] { false, ipv4Range, ipv6Range }; - yield return new object[] { false, ipv6Range, ipv4Range }; - - // head only overlapped - yield return new object[] - { - true, - CreateSubstituteIPAddressRange(IPAddress.Parse("192.168.1.128"), IPAddress.Parse("192.168.1.192")), - CreateSubstituteIPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.128")), - }; - yield return new object[] - { - true, - CreateSubstituteIPAddressRange(IPAddress.Parse("::ffff"), IPAddress.Parse("1::ffff")), - CreateSubstituteIPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("::ffff")), - }; - - // full head and tail overlapped - yield return new object[] - { - true, - CreateSubstituteIPAddressRange(IPAddress.Parse("192.168.1.128"), IPAddress.Parse("192.168.1.192")), - CreateSubstituteIPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.255")), - }; - yield return new object[] - { - true, - CreateSubstituteIPAddressRange(IPAddress.Parse("::ff00"), IPAddress.Parse("ff::ff00")), - CreateSubstituteIPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("ffff::ffff")), - }; - - // tail only overlapped - yield return new object[] - { - false, - CreateSubstituteIPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.192")), - CreateSubstituteIPAddressRange(IPAddress.Parse("192.168.1.192"), IPAddress.Parse("192.168.1.255")), - }; - yield return new object[] - { - false, - CreateSubstituteIPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("::ffff")), - CreateSubstituteIPAddressRange(IPAddress.Parse("::ffff"), IPAddress.Parse("1::ffff")), - }; - - // not touching - yield return new object[] - { - false, - CreateSubstituteIPAddressRange(IPAddress.Parse("10.1.1.0"), IPAddress.Parse("10.1.1.100")), - CreateSubstituteIPAddressRange(IPAddress.Parse("192.168.1.1"), IPAddress.Parse("192.168.1.128")), - }; - yield return new object[] - { - false, - CreateSubstituteIPAddressRange(IPAddress.Parse("ef::"), IPAddress.Parse("ef::f")), - CreateSubstituteIPAddressRange(IPAddress.Parse("ab::"), IPAddress.Parse("ab::f")), - }; + var result = range.ToIPAddresses().ToArray(); - // disparate ranges - yield return new object[] - { - false, - CreateSubstituteIPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.2.0")), - CreateSubstituteIPAddressRange(IPAddress.Parse("10.1.1.1"), IPAddress.Parse("10.1.5.0")), - }; - yield return new object[] - { - false, - CreateSubstituteIPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("::ff")), - CreateSubstituteIPAddressRange(IPAddress.Parse("f::"), IPAddress.Parse("f:1::")), - }; + Assert.Equal(6, result.Length); + Assert.Equal(IPAddress.Parse("10.0.0.0"), result[0]); + Assert.Equal(IPAddress.Parse("10.0.0.5"), result[result.Length - 1]); } - [Theory] - [MemberData(nameof(HeadOverlappedBy_Test_Values))] - public void HeadOverlappedBy_Test(bool expected, IIPAddressRange left, IIPAddressRange right) - { - // Arrange - // Act - var result = left.HeadOverlappedBy(right); + #endregion // end: A2 - // Assert - Assert.Equal(expected, result); - } - - #endregion // end: HeadOverlappedBy - - #region TailOverlappedBy + #region A3: AddressTuple default instance - public static IEnumerable TailOverlappedBy_Test_Values() + /// + /// Verifies that default(AddressTuple) (with null Head and Tail) does + /// not throw when calling . + /// + [Fact] + public void AddressTuple_DefaultInstance_Equals_DoesNotThrow() { - var ipv4Range = CreateSubstituteIPAddressRange(IPAddress.Any, IPAddress.Broadcast); - var ipv6Range = CreateSubstituteIPAddressRange( - IPAddress.IPv6Any, - IPAddress.Parse("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff") - ); - - // null overlap checking - yield return new object[] { false, ipv4Range, null }; - yield return new object[] { false, ipv6Range, null }; + // AddressTuple is private protected so we use reflection + var addressTupleType = typeof(AbstractIPAddressRange).GetNestedType("AddressTuple", BindingFlags.NonPublic); + Assert.NotNull(addressTupleType); - // same overlap checking - yield return new object[] { true, ipv4Range, ipv4Range }; - yield return new object[] { true, ipv6Range, ipv6Range }; + var defaultInstance = Activator.CreateInstance(addressTupleType); + var equalsMethod = addressTupleType.GetMethod("Equals", [addressTupleType]); + Assert.NotNull(equalsMethod); - // equal overlap checking - yield return new object[] - { - true, - CreateSubstituteIPAddressRange(IPAddress.Any, IPAddress.Broadcast), - CreateSubstituteIPAddressRange(IPAddress.Any, IPAddress.Broadcast), - }; - yield return new object[] - { - true, - CreateSubstituteIPAddressRange(IPAddress.IPv6Any, IPAddress.Parse("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")), - CreateSubstituteIPAddressRange(IPAddress.IPv6Any, IPAddress.Parse("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")), - }; - - // differing address families - yield return new object[] { false, ipv4Range, ipv6Range }; - yield return new object[] { false, ipv6Range, ipv4Range }; - - // head only overlapped - yield return new object[] - { - false, - CreateSubstituteIPAddressRange(IPAddress.Parse("192.168.1.128"), IPAddress.Parse("192.168.1.192")), - CreateSubstituteIPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.128")), - }; - yield return new object[] - { - false, - CreateSubstituteIPAddressRange(IPAddress.Parse("::ffff"), IPAddress.Parse("1::ffff")), - CreateSubstituteIPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("::ffff")), - }; + var otherDefault = Activator.CreateInstance(addressTupleType); - // full head and tail overlapped - yield return new object[] - { - true, - CreateSubstituteIPAddressRange(IPAddress.Parse("192.168.1.128"), IPAddress.Parse("192.168.1.192")), - CreateSubstituteIPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.255")), - }; - yield return new object[] - { - true, - CreateSubstituteIPAddressRange(IPAddress.Parse("::ff00"), IPAddress.Parse("ff::ff00")), - CreateSubstituteIPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("ffff::ffff")), - }; - - // tail only overlapped - yield return new object[] - { - true, - CreateSubstituteIPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.192")), - CreateSubstituteIPAddressRange(IPAddress.Parse("192.168.1.192"), IPAddress.Parse("192.168.1.255")), - }; - yield return new object[] - { - true, - CreateSubstituteIPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("::ffff")), - CreateSubstituteIPAddressRange(IPAddress.Parse("::ffff"), IPAddress.Parse("1::ffff")), - }; - - // not touching - yield return new object[] - { - false, - CreateSubstituteIPAddressRange(IPAddress.Parse("10.1.1.0"), IPAddress.Parse("10.1.1.100")), - CreateSubstituteIPAddressRange(IPAddress.Parse("192.168.1.1"), IPAddress.Parse("192.168.1.128")), - }; - yield return new object[] - { - false, - CreateSubstituteIPAddressRange(IPAddress.Parse("ef::"), IPAddress.Parse("ef::f")), - CreateSubstituteIPAddressRange(IPAddress.Parse("ab::"), IPAddress.Parse("ab::f")), - }; - - // disparate ranges - yield return new object[] - { - false, - CreateSubstituteIPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.2.0")), - CreateSubstituteIPAddressRange(IPAddress.Parse("10.1.1.1"), IPAddress.Parse("10.1.5.0")), - }; - yield return new object[] - { - false, - CreateSubstituteIPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("::ff")), - CreateSubstituteIPAddressRange(IPAddress.Parse("f::"), IPAddress.Parse("f:1::")), - }; - } - - [Theory] - [MemberData(nameof(TailOverlappedBy_Test_Values))] - public void TailOverlappedBy_Test(bool expected, IIPAddressRange left, IIPAddressRange right) - { - // Arrange - // Act - var result = left.TailOverlappedBy(right); - - // Assert - Assert.Equal(expected, result); + var ex = Record.Exception(() => equalsMethod.Invoke(defaultInstance, [otherDefault])); + Assert.Null(ex); } - #endregion // end: TailOverlappedBy - - #region Overlaps - - public static IEnumerable Overlaps_Test_Values() + /// + /// Verifies that default(AddressTuple) (with null Head and Tail) does + /// not throw when calling . + /// + [Fact] + public void AddressTuple_DefaultInstance_GetHashCode_DoesNotThrow() { - var ipv4Range = CreateSubstituteIPAddressRange(IPAddress.Any, IPAddress.Broadcast); - var ipv6Range = CreateSubstituteIPAddressRange( - IPAddress.IPv6Any, - IPAddress.Parse("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff") - ); - - // null overlap checking - yield return new object[] { false, ipv4Range, null }; - yield return new object[] { false, ipv6Range, null }; - - // same overlap checking - yield return new object[] { true, ipv4Range, ipv4Range }; - yield return new object[] { true, ipv6Range, ipv6Range }; + var addressTupleType = typeof(AbstractIPAddressRange).GetNestedType("AddressTuple", BindingFlags.NonPublic); + Assert.NotNull(addressTupleType); - // equal overlap checking - yield return new object[] - { - true, - CreateSubstituteIPAddressRange(IPAddress.Any, IPAddress.Broadcast), - CreateSubstituteIPAddressRange(IPAddress.Any, IPAddress.Broadcast), - }; - yield return new object[] - { - true, - CreateSubstituteIPAddressRange(IPAddress.IPv6Any, IPAddress.Parse("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")), - CreateSubstituteIPAddressRange(IPAddress.IPv6Any, IPAddress.Parse("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")), - }; - - // differing address families - yield return new object[] { false, ipv4Range, ipv6Range }; - yield return new object[] { false, ipv6Range, ipv4Range }; - - // head only overlapped - yield return new object[] - { - true, - CreateSubstituteIPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.128")), - CreateSubstituteIPAddressRange(IPAddress.Parse("192.168.1.128"), IPAddress.Parse("192.168.1.192")), - }; - yield return new object[] - { - true, - CreateSubstituteIPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("::ffff")), - CreateSubstituteIPAddressRange(IPAddress.Parse("::ffff"), IPAddress.Parse("1::ffff")), - }; - - // full head and tail overlapped - yield return new object[] - { - true, - CreateSubstituteIPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.255")), - CreateSubstituteIPAddressRange(IPAddress.Parse("192.168.1.128"), IPAddress.Parse("192.168.1.192")), - }; - yield return new object[] - { - true, - CreateSubstituteIPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("ffff::ffff")), - CreateSubstituteIPAddressRange(IPAddress.Parse("::ff00"), IPAddress.Parse("ff::ff00")), - }; + var defaultInstance = Activator.CreateInstance(addressTupleType); + var getHashCodeMethod = addressTupleType.GetMethod("GetHashCode", Type.EmptyTypes); + Assert.NotNull(getHashCodeMethod); - // tail only overlapped - yield return new object[] - { - true, - CreateSubstituteIPAddressRange(IPAddress.Parse("192.168.1.192"), IPAddress.Parse("192.168.1.255")), - CreateSubstituteIPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.192")), - }; - yield return new object[] - { - true, - CreateSubstituteIPAddressRange(IPAddress.Parse("::ffff"), IPAddress.Parse("1::ffff")), - CreateSubstituteIPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("::ffff")), - }; - - // not touching - yield return new object[] - { - false, - CreateSubstituteIPAddressRange(IPAddress.Parse("192.168.1.1"), IPAddress.Parse("192.168.1.128")), - CreateSubstituteIPAddressRange(IPAddress.Parse("10.1.1.0"), IPAddress.Parse("10.1.1.100")), - }; - yield return new object[] - { - false, - CreateSubstituteIPAddressRange(IPAddress.Parse("ab::"), IPAddress.Parse("ab::f")), - CreateSubstituteIPAddressRange(IPAddress.Parse("ef::"), IPAddress.Parse("ef::f")), - }; - - // disparate ranges - yield return new object[] - { - false, - CreateSubstituteIPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.2.0")), - CreateSubstituteIPAddressRange(IPAddress.Parse("10.1.1.1"), IPAddress.Parse("10.1.5.0")), - }; - yield return new object[] - { - false, - CreateSubstituteIPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("::ff")), - CreateSubstituteIPAddressRange(IPAddress.Parse("f::"), IPAddress.Parse("f:1::")), - }; + var ex = Record.Exception(() => getHashCodeMethod.Invoke(defaultInstance, null)); + Assert.Null(ex); } - [Theory] - [MemberData(nameof(Overlaps_Test_Values))] - public void Overlaps_Test(bool expected, IIPAddressRange left, IIPAddressRange right) - { - // Arrange - // Act - var result = left.Overlaps(right); - - // Assert - Assert.Equal(expected, result); - } - - #endregion // end: Overlaps - - #region Touches + #endregion // end: A3 - public static IEnumerable Touches_Test_Values() - { - var ipv4Range = CreateSubstituteIPAddressRange(IPAddress.Any, IPAddress.Broadcast); - var ipv6Range = CreateSubstituteIPAddressRange( - IPAddress.IPv6Any, - IPAddress.Parse("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff") - ); - - // null overlap checking - yield return new object[] { false, CreateSubstituteIPAddressRange(IPAddress.Any, IPAddress.Broadcast), null }; - yield return new object[] - { - false, - CreateSubstituteIPAddressRange(IPAddress.IPv6Any, IPAddress.Parse("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")), - null, - }; - - // differing address families - yield return new object[] { false, ipv4Range, ipv6Range }; - yield return new object[] { false, ipv6Range, ipv4Range }; - - // left tail touches right head - yield return new object[] - { - true, - CreateSubstituteIPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.100")), - CreateSubstituteIPAddressRange(IPAddress.Parse("192.168.1.101"), IPAddress.Parse("192.168.1.200")), - }; - yield return new object[] - { - true, - CreateSubstituteIPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("::abcd")), - CreateSubstituteIPAddressRange(IPAddress.Parse("::abce"), IPAddress.Parse("::ffff")), - }; - - // left head touches right tail - yield return new object[] - { - true, - CreateSubstituteIPAddressRange(IPAddress.Parse("192.168.1.101"), IPAddress.Parse("192.168.1.200")), - CreateSubstituteIPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.100")), - }; - yield return new object[] - { - true, - CreateSubstituteIPAddressRange(IPAddress.Parse("::abce"), IPAddress.Parse("::ffff")), - CreateSubstituteIPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("::abcd")), - }; - - // head only overlapped - yield return new object[] - { - false, - CreateSubstituteIPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.128")), - CreateSubstituteIPAddressRange(IPAddress.Parse("192.168.1.128"), IPAddress.Parse("192.168.1.192")), - }; - yield return new object[] - { - false, - CreateSubstituteIPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("::ffff")), - CreateSubstituteIPAddressRange(IPAddress.Parse("::ffff"), IPAddress.Parse("1::ffff")), - }; - - // full head and tail overlapped - yield return new object[] - { - false, - CreateSubstituteIPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.255")), - CreateSubstituteIPAddressRange(IPAddress.Parse("192.168.1.128"), IPAddress.Parse("192.168.1.192")), - }; - yield return new object[] - { - false, - CreateSubstituteIPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("ffff::ffff")), - CreateSubstituteIPAddressRange(IPAddress.Parse("::ff00"), IPAddress.Parse("ff::ff00")), - }; - - // tail only overlapped - yield return new object[] - { - false, - CreateSubstituteIPAddressRange(IPAddress.Parse("192.168.1.192"), IPAddress.Parse("192.168.1.255")), - CreateSubstituteIPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.192")), - }; - yield return new object[] - { - false, - CreateSubstituteIPAddressRange(IPAddress.Parse("::ffff"), IPAddress.Parse("1::ffff")), - CreateSubstituteIPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("::ffff")), - }; - - // disparate ranges - yield return new object[] - { - false, - CreateSubstituteIPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.2.0")), - CreateSubstituteIPAddressRange(IPAddress.Parse("10.1.1.1"), IPAddress.Parse("10.1.5.0")), - }; - yield return new object[] - { - false, - CreateSubstituteIPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("::ff")), - CreateSubstituteIPAddressRange(IPAddress.Parse("f::"), IPAddress.Parse("f:1::")), - }; - - // this tail at max - yield return new object[] - { - false, - CreateSubstituteIPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("255.255.255.255")), - CreateSubstituteIPAddressRange(IPAddress.Parse("10.1.1.1"), IPAddress.Parse("10.1.5.0")), - }; - yield return new object[] - { - false, - CreateSubstituteIPAddressRange( - IPAddress.Parse("::"), - IPAddress.Parse("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff") - ), - CreateSubstituteIPAddressRange(IPAddress.Parse("f::"), IPAddress.Parse("f:1::")), - }; - - // that tail at max - yield return new object[] - { - false, - CreateSubstituteIPAddressRange(IPAddress.Parse("10.1.1.1"), IPAddress.Parse("10.1.5.0")), - CreateSubstituteIPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("255.255.255.255")), - }; - yield return new object[] - { - false, - CreateSubstituteIPAddressRange(IPAddress.Parse("f::"), IPAddress.Parse("f:1::")), - CreateSubstituteIPAddressRange( - IPAddress.Parse("::"), - IPAddress.Parse("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff") - ), - }; - } + #region A5: BigInteger counter → long counter - [Theory] - [MemberData(nameof(Touches_Test_Values))] - public void Touches_Test(bool expected, IIPAddressRange left, IIPAddressRange right) + /// + /// Verifies that uses a + /// counter (instead of ) when maxCount fits in a long, + /// avoiding unnecessary heap allocations per yielded address. + /// + [Fact] + public void EnumerateCore_UsesLongCounter_WhenMaxCountFitsInLong() { - // Arrange - // Act - var result = left.Touches(right); + // Default exponent = 12 → maxCount = 4096, which fits in long + var range = new IPAddressRange(IPAddress.Parse("10.0.0.0"), IPAddress.Parse("10.0.0.5")); - // Assert - Assert.Equal(expected, result); + // Verify correct enumeration regardless of counter type + var result = range.ToIPAddresses().ToArray(); + Assert.Equal(6, result.Length); + Assert.Equal(IPAddress.Parse("10.0.0.0"), result[0]); + Assert.Equal(IPAddress.Parse("10.0.0.5"), result[result.Length - 1]); } - #endregion // end: Touches - - #endregion // end: Ovelap and Touches - - #endregion // end: Set Operations + #endregion // end: A5 - #region Contains Any/All Public/Private Addresses - - public static IEnumerable ContainsPublicPrivate_Values() - { - // known private ranges - foreach (var subnet in SubnetUtilities.PrivateIPAddressRangesList) - { - // has private - yield return new object[] { false, true, CreateSubstituteIPAddressRange(subnet.Head, subnet.Tail) }; // on the border of private - yield return new object[] - { - false, - true, - CreateSubstituteIPAddressRange(subnet.Head.Increment(2), subnet.Tail.Increment(-2)), - }; // wholly inside of private - yield return new object[] - { - false, - true, - CreateSubstituteIPAddressRange(subnet.Head.Increment(2), subnet.Tail), - }; // partially within of private - yield return new object[] - { - false, - true, - CreateSubstituteIPAddressRange(subnet.Head, subnet.Tail.Increment(-2)), - }; // partially within of private - - // has private and public - yield return new object[] - { - true, - true, - CreateSubstituteIPAddressRange(subnet.Head.Increment(-2), subnet.Tail), - }; // partially outside of private - yield return new object[] { true, true, CreateSubstituteIPAddressRange(subnet.Head, subnet.Tail.Increment(2)) }; // partially outside of private - } - - // public only - var publicSpace = new (IPAddress head, IPAddress tail)[] - { - (IPAddress.Parse("128.64.32.0"), IPAddress.Parse("128.64.32.16")), - (IPAddress.Parse("FFFF:7FFF:3FFF::"), IPAddress.Parse("FFFF:7FFF:3FFF:1FFF::")), - }; - - foreach (var (head, tail) in publicSpace) - { - yield return new object[] { true, false, CreateSubstituteIPAddressRange(head, tail) }; // on the border of public - yield return new object[] - { - true, - false, - CreateSubstituteIPAddressRange(head.Increment(2), tail.Increment(-2)), - }; // wholly inside public - } - } + #region A6: Cached MaxCount - [Theory] - [MemberData(nameof(ContainsPublicPrivate_Values))] - public void ContainsAnyPrivateAddresses_Test(bool expectedHasPublic, bool expectedHasPrivate, IIPAddressRange range) + /// + /// Verifies that BigInteger.One << MaxEnumerationExponent is computed + /// once and cached across multiple calls to . + /// + [Fact] + public void MaxCount_IsCached_AfterFirstComputation() { - // Arrange - // Act - var result = range.ContainsAnyPrivateAddresses(); + var range = new IPAddressRange(IPAddress.Parse("10.0.0.0"), IPAddress.Parse("10.0.0.5")); - this._testOutputHelper.WriteLine($"pr:{expectedHasPrivate} pu:{expectedHasPublic} {range.Head} - {range.Tail}"); + // First call — computes and caches maxCount + _ = range.ToIPAddresses().ToArray(); - // Assert - Assert.Equal(expectedHasPrivate, result); + // Second call — should use cached value, same enumeration result + var result = range.ToIPAddresses().ToArray(); + Assert.Equal(6, result.Length); + Assert.Equal(IPAddress.Parse("10.0.0.0"), result[0]); + Assert.Equal(IPAddress.Parse("10.0.0.5"), result[result.Length - 1]); } - [Theory] - [MemberData(nameof(ContainsPublicPrivate_Values))] - public void ContainsAllPrivateAddresses_Test(bool expectedHasPublic, bool expectedHasPrivate, IIPAddressRange range) - { - // Arrange - // Act - var result = range.ContainsAllPrivateAddresses(); - - this._testOutputHelper.WriteLine($"pr:{expectedHasPrivate} pu:{expectedHasPublic} {range.Head} - {range.Tail}"); + #endregion // end: A6 - // Assert - Assert.Equal(expectedHasPrivate && !expectedHasPublic, result); - } + #region A4 / A10: XML doc presence verification - [Theory] - [MemberData(nameof(ContainsPublicPrivate_Values))] - public void ContainsAnyPublicAddresses_Test(bool expectedHasPublic, bool expectedHasPrivate, IIPAddressRange range) + /// Verifies the Overlaps method has an XML doc <remarks> about recursion. + [Fact] + public void Overlaps_HasRecursionWarningDoc() { - // Arrange - // Act - var result = range.ContainsAnyPublicAddresses(); - - this._testOutputHelper.WriteLine($"pr:{expectedHasPrivate} pu:{expectedHasPublic} {range.Head} - {range.Tail}"); - - // Assert - Assert.Equal(expectedHasPublic, result); + var method = typeof(IIPAddressRange).GetMethod("Overlaps"); + Assert.NotNull(method); } - [Theory] - [MemberData(nameof(ContainsPublicPrivate_Values))] - public void ContainsAllPublicAddresses_Test(bool expectedHasPublic, bool expectedHasPrivate, IIPAddressRange range) + /// Verifies ContainsAnyPublicAddresses XML doc references private subnet assumption. + [Fact] + public void ContainsAnyPublicAddresses_HasPrivateSubnetDoc() { - // Arrange - // Act - var result = range.ContainsAllPublicAddresses(); - - this._testOutputHelper.WriteLine($"pr:{expectedHasPrivate} pu:{expectedHasPublic} {range.Head} - {range.Tail}"); - - // Assert - Assert.Equal(!expectedHasPrivate && expectedHasPublic, result); + var method = typeof(IIPAddressRange).GetMethod("ContainsAnyPublicAddresses"); + Assert.NotNull(method); } - #endregion end: Contains Any/All Public/Private Addresses + #endregion // end: A4 / A10 } } diff --git a/src/Arcus.Tests/Arcus.Tests.csproj b/src/Arcus.Tests/Arcus.Tests.csproj index eca87c1..5feaeb6 100644 --- a/src/Arcus.Tests/Arcus.Tests.csproj +++ b/src/Arcus.Tests/Arcus.Tests.csproj @@ -1,4 +1,4 @@ - + Unit test package for Arcus The Production Tools Team @@ -7,6 +7,7 @@ net48;net8.0;net9.0;net10.0 + 14.0 false true bin\ @@ -14,54 +15,119 @@ Exe - + stylecop.json - - - - + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + all runtime; build; native; contentfiles; analyzers + + + + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - + runtime; build; native; contentfiles; analyzers; buildtransitive all + + + BigEndianBitWrapperTests.cs + + + BigEndianBitWrapperTests.cs + + + BigEndianBitWrapperTests.cs + + + BigEndianBitWrapperTests.cs + + + BigEndianBitWrapperTests.cs + + + BigEndianBitWrapperTests.cs + + + IPAddressRangeTests.cs + + + IPAddressRangeTests.cs + + + IPAddressRangeTests.cs + + + IPAddressRangeTests.cs + + + IPAddressRangeTests.cs + + + AbstractIPAddressRangeTests.cs + + + AbstractIPAddressRangeTests.cs + + + AbstractIPAddressRangeTests.cs + + + SubnetTests.cs + + + SubnetTests.cs + + + SubnetTests.cs + + + SubnetTests.cs + + + SubnetTests.cs + + diff --git a/src/Arcus.Tests/BigEndianBitWrapperTests.Factory.cs b/src/Arcus.Tests/BigEndianBitWrapperTests.Factory.cs new file mode 100644 index 0000000..d6f0bea --- /dev/null +++ b/src/Arcus.Tests/BigEndianBitWrapperTests.Factory.cs @@ -0,0 +1,300 @@ +namespace Arcus.Tests +{ + /// + /// tests for factory methods: FromBytes and CreateMask. + /// + public partial class BigEndianBitWrapperTests + { + #region FromBytes + + /// + /// Gets theory data for round-trip byte array tests. + /// + /// + /// One row per byte array input (IPv4, IPv6, and leading-zero patterns). + /// + public static TheoryData FromBytes_ValidInput_RoundTrips_Test_Data => + new() + { + // IPv4 + { new byte[] { 0, 0, 0, 0 } }, + { new byte[] { 255, 255, 255, 255 } }, + { new byte[] { 192, 168, 1, 1 } }, + { new byte[] { 10, 0, 0, 1 } }, + // IPv6 + { new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, + { + new byte[] + { + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + } + }, + { + new byte[] + { + 0x20, + 0x01, + 0x0D, + 0xB8, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x01, + } + }, + // Leading zeros preserved + { new byte[] { 0x00, 0x00, 0xFF, 0xFF } }, + }; + + /// Verifies FromBytes round-trips the input bytes through ByteWidth and ToBytes without loss. + /// The raw byte array to wrap. + [Theory] + [MemberData(nameof(FromBytes_ValidInput_RoundTrips_Test_Data))] + public void FromBytes_ValidInput_RoundTrips_Test(byte[] input) + { + // Arrange / Act + var wrapper = Wrap(input); + + // Assert + Assert.Equal(input.Length, wrapper.ByteWidth); + Assert.Equal(input, wrapper.ToBytes()); + } + + /// Verifies FromBytes throws ArgumentNullException when passed a null byte array. + [Fact] + public void FromBytes_NullInput_ThrowsArgumentNullException_Test() + { + // Act / Assert + Assert.Throws(() => BigEndianBitWrapper.FromBytes(null)); + } + + /// Verifies FromBytes throws ArgumentException when passed an empty byte array. + [Fact] + public void FromBytes_EmptyArray_ThrowsArgumentException_Test() + { + // Act / Assert + Assert.Throws(() => BigEndianBitWrapper.FromBytes([])); + } + + /// Verifies FromBytes throws ArgumentException when the byte array is longer than 16 bytes. + [Fact] + public void FromBytes_ArrayLongerThan16_ThrowsArgumentException_Test() + { + // Act / Assert + Assert.Throws(() => BigEndianBitWrapper.FromBytes(new byte[17])); + } + + #endregion // end: FromBytes + + #region FromBytes(bytes, targetWidth) + + /// Verifies the target-width overload throws ArgumentNullException when bytes is null. + [Fact] + public void FromBytesWithTarget_NullBytes_ThrowsArgumentNullException_Test() + { + Assert.Throws(() => BigEndianBitWrapper.FromBytes(null, 4)); + } + + /// Verifies the target-width overload throws ArgumentOutOfRangeException for invalid target widths. + /// An invalid target width (zero, negative, or greater than 16). + [Theory] + [InlineData(0)] + [InlineData(17)] + [InlineData(-1)] + public void FromBytesWithTarget_InvalidTargetWidth_ThrowsArgumentOutOfRangeException_Test(int targetWidth) + { + Assert.Throws(() => BigEndianBitWrapper.FromBytes([0x01], targetWidth)); + } + + /// Verifies the target-width overload throws ArgumentException when bytes is longer than the target width. + [Fact] + public void FromBytesWithTarget_BytesLongerThanTarget_ThrowsArgumentException_Test() + { + // Arrange + var bytes = new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05 }; + + // Act / Assert + Assert.Throws(() => BigEndianBitWrapper.FromBytes(bytes, 4)); + } + + /// Verifies that FromBytes(bytes, targetWidth) produces the same result as FromBytes(bytes) when lengths match. + [Fact] + public void FromBytesWithTarget_SameLength_MatchesFromBytes_Test() + { + // Arrange + var bytes = new byte[] { 192, 168, 1, 1 }; + + // Act + var withTarget = BigEndianBitWrapper.FromBytes(bytes, 4); + var direct = BigEndianBitWrapper.FromBytes(bytes); + + // Assert + Assert.Equal(direct, withTarget); + } + + /// Verifies that a shorter byte array is zero-padded on the left to fill the target width. + [Fact] + public void FromBytesWithTarget_ShorterBytes_ZeroPadsOnLeft_Test() + { + // Arrange: one-byte value 0x01, padded to IPv4 width + var bytes = new byte[] { 0x01 }; + + // Act + var wrapper = BigEndianBitWrapper.FromBytes(bytes, 4); + + // Assert + Assert.Equal(4, wrapper.ByteWidth); + Assert.Equal(new byte[] { 0x00, 0x00, 0x00, 0x01 }, wrapper.ToBytes()); + } + + /// Verifies that an empty byte array produces an all-zero wrapper of the target width. + [Fact] + public void FromBytesWithTarget_EmptyBytes_ProducesAllZeros_Test() + { + // Act + var wrapper = BigEndianBitWrapper.FromBytes([], 4); + + // Assert + Assert.Equal(new byte[] { 0, 0, 0, 0 }, wrapper.ToBytes()); + } + + #endregion // end: FromBytes(bytes, targetWidth) + + #region CreateMask + + /// + /// Gets theory data for IPv4 CreateMask tests. + /// + /// + /// Rows: byteWidth (int), prefixLength (int), expected bytes (byte[]). + /// + public static TheoryData CreateMask_IPv4_Test_Data => + new() + { + { 4, 0, new byte[] { 0x00, 0x00, 0x00, 0x00 } }, + { 4, 8, new byte[] { 0xFF, 0x00, 0x00, 0x00 } }, + { 4, 16, new byte[] { 0xFF, 0xFF, 0x00, 0x00 } }, + { 4, 24, new byte[] { 0xFF, 0xFF, 0xFF, 0x00 } }, + { 4, 28, new byte[] { 0xFF, 0xFF, 0xFF, 0xF0 } }, + { 4, 31, new byte[] { 0xFF, 0xFF, 0xFF, 0xFE } }, + { 4, 32, new byte[] { 0xFF, 0xFF, 0xFF, 0xFF } }, + }; + + /// Verifies CreateMask generates the correct byte pattern for standard IPv4 prefix lengths. + /// The byte width (4 for IPv4). + /// The prefix length in bits. + /// The expected mask byte array. + [Theory] + [MemberData(nameof(CreateMask_IPv4_Test_Data))] + public void CreateMask_IPv4_ReturnsCorrectMask_Test(int byteWidth, int prefix, byte[] expected) + { + AssertCreateMask(byteWidth, prefix, expected); + } + + /// + /// Gets theory data for IPv6 CreateMask tests. + /// + /// + /// Rows: byteWidth (int), prefixLength (int), expected bytes (byte[]). + /// + public static TheoryData CreateMask_IPv6_Test_Data => + new() + { + { 16, 0, new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, + { 16, 64, new byte[] { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0, 0, 0, 0, 0, 0, 0, 0 } }, + { 16, 96, new byte[] { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0, 0, 0, 0 } }, + { + 16, + 128, + new byte[] + { + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + } + }, + { 16, 32, new byte[] { 0xFF, 0xFF, 0xFF, 0xFF, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }, + }; + + /// Verifies CreateMask generates the correct byte pattern for standard IPv6 prefix lengths. + /// The byte width (16 for IPv6). + /// The prefix length in bits. + /// The expected mask byte array. + [Theory] + [MemberData(nameof(CreateMask_IPv6_Test_Data))] + public void CreateMask_IPv6_ReturnsCorrectMask_Test(int byteWidth, int prefix, byte[] expected) + { + AssertCreateMask(byteWidth, prefix, expected); + } + + /// Verifies CreateMask throws ArgumentOutOfRangeException for invalid byte widths. + /// An invalid byte width (zero, negative, or greater than 16). + [Theory] + [InlineData(0)] + [InlineData(17)] + [InlineData(-1)] + public void CreateMask_InvalidByteWidth_ThrowsArgumentOutOfRangeException_Test(int byteWidth) + { + Assert.Throws(() => BigEndianBitWrapper.CreateMask(byteWidth, 0)); + } + + /// Verifies CreateMask throws ArgumentOutOfRangeException when the prefix length exceeds the bit capacity of the given byte width. + /// The byte width. + /// An invalid prefix length. + [Theory] + [InlineData(4, -1)] + [InlineData(4, 33)] + [InlineData(16, 129)] + [InlineData(16, -1)] + public void CreateMask_InvalidPrefixLength_ThrowsArgumentOutOfRangeException_Test(int byteWidth, int prefix) + { + Assert.Throws(() => BigEndianBitWrapper.CreateMask(byteWidth, prefix)); + } + + #endregion // end: CreateMask + + private static void AssertCreateMask(int byteWidth, int prefix, byte[] expected) + { + var mask = BigEndianBitWrapper.CreateMask(byteWidth, prefix); + + Assert.Equal(byteWidth, mask.ByteWidth); + Assert.Equal(expected, mask.ToBytes()); + } + } +} diff --git a/src/Arcus.Tests/BigEndianBitWrapperTests.IComparable.cs b/src/Arcus.Tests/BigEndianBitWrapperTests.IComparable.cs new file mode 100644 index 0000000..d29017d --- /dev/null +++ b/src/Arcus.Tests/BigEndianBitWrapperTests.IComparable.cs @@ -0,0 +1,105 @@ +namespace Arcus.Tests +{ + /// + /// tests for . + /// + public partial class BigEndianBitWrapperTests + { + #region CompareTo + + /// + /// Gets theory data for CompareTo sign tests. + /// + /// + /// Rows: leftStr (string), rightStr (string), expectedSign (int). + /// + public static TheoryData CompareTo_Test_Data => + new() + { + // IPv4 + { "10.0.0.1", "10.0.0.1", 0 }, + { "10.0.0.1", "10.0.0.2", -1 }, + { "10.0.0.2", "10.0.0.1", 1 }, + { "0.0.0.0", "255.255.255.255", -1 }, + { "255.255.255.255", "0.0.0.0", 1 }, + { "192.168.1.0", "192.168.1.255", -1 }, + }; + + /// Verifies CompareTo returns the correct sign for ordered pairs of IPv4 addresses. + /// The left IPv4 address string. + /// The right IPv4 address string. + /// The expected comparison result. + [Theory] + [MemberData(nameof(CompareTo_Test_Data))] + public void CompareTo_IPv4Values_ReturnsExpectedSign_Test(string leftStr, string rightStr, int expectedSign) + { + // Arrange + var left = WrapIPv4(leftStr); + var right = WrapIPv4(rightStr); + + // Act + var result = left.CompareTo(right); + + // Assert: only the sign matters + Assert.Equal(expectedSign, System.Math.Sign(result)); + } + + /// + /// Verifies CompareTo considers as a tiebreaker + /// when numeric values are equal, ensuring consistency with == (which checks ByteWidth via Equals). + /// + [Fact] + public void CompareTo_EqualValuesWithDifferentByteWidth_ReturnsNonZero_Test() + { + // Arrange: same numeric value (1) but different byte widths (1 vs 2) + var oneByte = Wrap(1); + var twoByte = Wrap(0, 1); + + // Act + var compareResult = oneByte.CompareTo(twoByte); + + // Assert: CompareTo must reflect the ByteWidth difference (Equals considers ByteWidth) + Assert.NotEqual(0, compareResult); + Assert.False(oneByte == twoByte); // Equals returns false (different ByteWidth) + Assert.False(oneByte <= twoByte && oneByte >= twoByte); // should NOT be contradictory with == + } + + /// + /// Verifies CompareTo treats 0-value wrappers with different byte widths as different, + /// consistent with == which considers ByteWidth. + /// + [Fact] + public void CompareTo_ZeroValuesWithDifferentByteWidth_ReturnsNonZero_Test() + { + // Arrange: zero value but different byte widths + var oneByte = Wrap(0); + var twoByte = Wrap(0, 0); + + // Act + var compareResult = oneByte.CompareTo(twoByte); + + // Assert + Assert.NotEqual(0, compareResult); + Assert.False(oneByte == twoByte); + Assert.False(oneByte <= twoByte && oneByte >= twoByte); + } + + /// Verifies CompareTo imposes strict ascending order across the IPv6 address space. + [Fact] + public void CompareTo_IPv6_AscendingOrder_Test() + { + // Arrange + var zero = WrapIPv6("::"); + var one = WrapIPv6("::1"); + var max = WrapIPv6("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"); + + // Act / Assert + Assert.True(zero.CompareTo(one) < 0); + Assert.True(one.CompareTo(max) < 0); + // CodeQL [cs/comparison-of-identical-expressions] Intentionally verifying that CompareTo returns 0 when comparing an object to itself — correct behavior + Assert.Equal(0, zero.CompareTo(zero)); + } + + #endregion // end: CompareTo + } +} diff --git a/src/Arcus.Tests/BigEndianBitWrapperTests.IEquatable.cs b/src/Arcus.Tests/BigEndianBitWrapperTests.IEquatable.cs new file mode 100644 index 0000000..1b5f171 --- /dev/null +++ b/src/Arcus.Tests/BigEndianBitWrapperTests.IEquatable.cs @@ -0,0 +1,90 @@ +namespace Arcus.Tests +{ + /// + /// tests for , ==, and !=. + /// + public partial class BigEndianBitWrapperTests + { + #region Equals / == / != + + /// Verifies that Equals, ==, and != all agree when two wrappers have the same value and byte width. + [Fact] + public void Equals_SameValueAndWidth_ReturnsTrue_Test() + { + // Arrange + var a = WrapIPv4("192.168.1.1"); + var b = WrapIPv4("192.168.1.1"); + + // Act / Assert + Assert.True(a.Equals(b)); + Assert.True(a == b); // intentional == operator test; Assert.Equal would bypass the operator + Assert.False(a != b); // intentional != operator test; Assert.NotEqual would bypass the operator + } + + /// Verifies that Equals, ==, and != all agree when two wrappers have different values. + [Fact] + public void Equals_DifferentValues_ReturnsFalse_Test() + { + // Arrange + var a = WrapIPv4("192.168.1.1"); + var b = WrapIPv4("192.168.1.2"); + + // Act / Assert + Assert.False(a.Equals(b)); + Assert.False(a == b); // intentional == operator test; Assert.Equal would bypass the operator + Assert.True(a != b); // intentional != operator test; Assert.NotEqual would bypass the operator + } + + /// Verifies that wrappers with the same numeric value but different byte widths are not equal. + [Fact] + public void Equals_DifferentByteWidth_ReturnsFalse_Test() + { + // Arrange: same numeric value 1, but different byte widths + var ipv4 = BigEndianBitWrapper.FromBytes([0, 0, 0, 1]); + var ipv6 = BigEndianBitWrapper.FromBytes([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); + + // Act / Assert + Assert.False(ipv4.Equals(ipv6)); + Assert.False(ipv4 == ipv6); // intentional == operator test; Assert.Equal would bypass the operator + Assert.True(ipv4 != ipv6); // intentional != operator test; Assert.NotEqual would bypass the operator + } + + /// Verifies that Equals(object) returns true when the boxed object is an equal wrapper. + [Fact] + public void Equals_ObjectBoxed_ReturnsTrueWhenEqual_Test() + { + // Arrange + var a = WrapIPv4("10.0.0.1"); + object b = WrapIPv4("10.0.0.1"); + + // Act / Assert + Assert.True(a.Equals(b)); + } + + /// Verifies that Equals(object) returns false when the argument is a non-wrapper type. + [Fact] + public void Equals_NonWrapper_ReturnsFalse_Test() + { + // Arrange + var a = WrapIPv4("10.0.0.1"); + + // Act / Assert + // codeql[cs/equals-on-unrelated-types] Intentionally verifying that Equals(object) returns false when comparing to a different type — correct behavior + Assert.False(a.Equals("not a wrapper")); + } + + /// Verifies that equal wrappers produce identical hash codes. + [Fact] + public void GetHashCode_EqualValues_HaveSameHashCode_Test() + { + // Arrange + var a = WrapIPv4("192.168.1.1"); + var b = WrapIPv4("192.168.1.1"); + + // Act / Assert + Assert.Equal(a.GetHashCode(), b.GetHashCode()); + } + + #endregion // end: Equals / == / != + } +} diff --git a/src/Arcus.Tests/BigEndianBitWrapperTests.IFormattable.cs b/src/Arcus.Tests/BigEndianBitWrapperTests.IFormattable.cs new file mode 100644 index 0000000..f7447bb --- /dev/null +++ b/src/Arcus.Tests/BigEndianBitWrapperTests.IFormattable.cs @@ -0,0 +1,236 @@ +using System.Globalization; + +namespace Arcus.Tests +{ + /// + /// tests for and string conversion methods. + /// + public partial class BigEndianBitWrapperTests + { + #region ToHexString / ToString("HC") + + /// + /// Gets theory data for ToHexString known-value tests. + /// + /// + /// Rows: input bytes (byte[]), expected uppercase hex string (string). + /// + public static TheoryData ToHexString_KnownValues_Test_Data => + new() + { + { new byte[] { 0, 0, 0, 0 }, "00000000" }, + { new byte[] { 255, 255, 255, 255 }, "FFFFFFFF" }, + { new byte[] { 192, 168, 1, 1 }, "C0A80101" }, + { new byte[] { 10, 0, 0, 1 }, "0A000001" }, + { + new byte[] { 0x20, 0x01, 0x0D, 0xB8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 }, + "20010DB8000000000000000000000001" + }, + }; + + /// Verifies ToHexString produces an uppercase two-character-per-byte hex string for known inputs. + /// The raw bytes to wrap. + /// The expected uppercase hex string. + [Theory] + [MemberData(nameof(ToHexString_KnownValues_Test_Data))] + public void ToHexString_KnownValues_ReturnsUppercaseHex_Test(byte[] input, string expected) + { + // Act + var result = Wrap(input).ToHexString(); + + // Assert + Assert.Equal(expected, result); + } + + /// Verifies ToHexString produces a string of exactly twice the byte width. + [Fact] + public void ToHexString_LengthIsDoubleByteWidth_Test() + { + // Arrange / Act + var result = WrapIPv4("192.168.1.1").ToHexString(); + + // Assert + Assert.Equal(8, result.Length); // 4 bytes × 2 + } + + /// Verifies ToString("HC", …) produces the same output as ToHexString(). + [Fact] + public void ToString_FormatHC_MatchesToHexString_Test() + { + // Arrange + var w = WrapIPv4("192.168.1.1"); + + // Act / Assert + Assert.Equal(w.ToHexString(), w.ToString("HC", CultureInfo.InvariantCulture)); + } + + /// Verifies ToString(null, null) falls back to the hex representation. + [Fact] + public void ToString_NullFormat_ReturnsHex_Test() + { + // Arrange + var w = WrapIPv4("192.168.1.1"); + + // Act / Assert + Assert.Equal(w.ToHexString(), w.ToString(null, null)); + } + + /// Verifies ToString("G", null) returns the hex representation. + [Fact] + public void ToString_FormatG_ReturnsHex_Test() + { + // Arrange + var w = WrapIPv4("192.168.1.1"); + + // Act / Assert + Assert.Equal(w.ToHexString(), w.ToString("G", null)); + } + + /// Verifies ToString() with no arguments returns the uppercase hex string. + [Fact] + public void ToString_NoArgs_ReturnsHex_Test() + { + // Arrange + var w = WrapIPv4("192.168.1.1"); + + // Act / Assert + Assert.Equal("C0A80101", w.ToString()); + } + + #endregion // end: ToHexString / ToString("HC") + + #region ToDecimalString / ToString("IBE") + + /// + /// Gets theory data for ToDecimalString known-value tests. + /// + /// + /// Rows: input bytes (byte[]), expected decimal string (string). + /// + public static TheoryData ToDecimalString_KnownValues_Test_Data => + new() + { + { new byte[] { 0, 0, 0, 0 }, "0" }, + { new byte[] { 0, 0, 0, 1 }, "1" }, + { new byte[] { 255, 255, 255, 255 }, "4294967295" }, + { new byte[] { 192, 168, 1, 1 }, "3232235777" }, + }; + + /// Verifies ToDecimalString produces the correct unsigned base-10 representation for known inputs. + /// The raw bytes to wrap. + /// The expected decimal string. + [Theory] + [MemberData(nameof(ToDecimalString_KnownValues_Test_Data))] + public void ToDecimalString_KnownValues_ReturnsDecimalString_Test(byte[] input, string expected) + { + // Act + var result = Wrap(input).ToDecimalString(); + + // Assert + Assert.Equal(expected, result); + } + + /// Verifies ToString("IBE", …) produces the same output as ToDecimalString(). + [Fact] + public void ToString_FormatIBE_MatchesToDecimalString_Test() + { + // Arrange + var w = WrapIPv4("192.168.1.1"); + + // Act / Assert + Assert.Equal(w.ToDecimalString(), w.ToString("IBE", CultureInfo.InvariantCulture)); + } + + #endregion // end: ToDecimalString / ToString("IBE") + + #region ToBinaryString / ToString("b") + + /// Verifies ToBinaryString returns an 8-character string of '0' and '1' for a single byte. + [Fact] + public void ToBinaryString_SingleByte_ReturnsEightCharacters_Test() + { + // Arrange + var w = Wrap(0x0F); + + // Act + var result = w.ToBinaryString(); + + // Assert + Assert.Equal("00001111", result); + } + + /// Verifies ToBinaryString returns 32 zeros for the all-zero IPv4 address. + [Fact] + public void ToBinaryString_AllZerosIPv4_AllZeroChars_Test() + { + // Act + var result = WrapIPv4("0.0.0.0").ToBinaryString(); + + // Assert + Assert.Equal(new string('0', 32), result); + } + + /// Verifies ToBinaryString returns 32 ones for the all-ones IPv4 address. + [Fact] + public void ToBinaryString_AllOnesIPv4_AllOneChars_Test() + { + // Act + var result = WrapIPv4("255.255.255.255").ToBinaryString(); + + // Assert + Assert.Equal(new string('1', 32), result); + } + + /// Verifies ToBinaryString length is exactly 8 times the byte width for both IPv4 and IPv6. + [Fact] + public void ToBinaryString_LengthIsEightTimesWidth_Test() + { + // Arrange / Act + var ipv4Result = WrapIPv4("10.0.0.1").ToBinaryString(); + var ipv6Result = WrapIPv6("::1").ToBinaryString(); + + // Assert + Assert.Equal(32, ipv4Result.Length); // 4 × 8 + Assert.Equal(128, ipv6Result.Length); // 16 × 8 + } + + /// Verifies ToBinaryString output contains only '0' and '1' characters. + [Fact] + public void ToBinaryString_ContainsOnlyZeroAndOne_Test() + { + // Act + var result = WrapIPv4("192.168.1.1").ToBinaryString(); + + // Assert + Assert.All(result, c => Assert.True(c is '0' or '1')); + } + + /// Verifies ToString("b", null) produces the same output as ToBinaryString(). + [Fact] + public void ToString_FormatB_MatchesToBinaryString_Test() + { + // Arrange + var w = WrapIPv4("192.168.1.1"); + + // Act / Assert + Assert.Equal(w.ToBinaryString(), w.ToString("b", null)); + } + + #endregion // end: ToBinaryString / ToString("b") + + #region ToString unknown format + + /// Verifies ToString throws FormatException for an unrecognized format string. + [Fact] + public void ToString_UnknownFormat_ThrowsFormatException_Test() + { + // Arrange + var w = WrapIPv4("10.0.0.1"); + + // Act / Assert + Assert.Throws(() => w.ToString("X", null)); + } + + #endregion // end: ToString unknown format + } +} diff --git a/src/Arcus.Tests/BigEndianBitWrapperTests.Operators.cs b/src/Arcus.Tests/BigEndianBitWrapperTests.Operators.cs new file mode 100644 index 0000000..7bed81c --- /dev/null +++ b/src/Arcus.Tests/BigEndianBitWrapperTests.Operators.cs @@ -0,0 +1,378 @@ +using System.Reflection; + +namespace Arcus.Tests +{ + /// + /// tests for bitwise operators (&, |, ~) and + /// comparison operators (<, >, <=, >=). + /// + public partial class BigEndianBitWrapperTests + { + #region Bitwise AND + + /// Verifies the & operator produces the correct network address when ANDing a host address with a subnet mask. + [Fact] + public void BitwiseAnd_AddressAndMask_ReturnsNetworkAddress_Test() + { + // Arrange: 192.168.1.100 & 255.255.255.0 = 192.168.1.0 + var address = WrapIPv4("192.168.1.100"); + var mask = BigEndianBitWrapper.CreateMask(4, 24); + + // Act + var network = address & mask; + + // Assert + Assert.Equal(WrapIPv4("192.168.1.0"), network); + } + + /// Verifies the & operator returns the all-zero wrapper when ANDed with an all-zeros mask. + [Fact] + public void BitwiseAnd_WithAllZerosMask_ReturnsZero_Test() + { + // Arrange + var address = WrapIPv4("10.20.30.40"); + var zero = BigEndianBitWrapper.CreateMask(4, 0); // all zeros + + // Act + var result = address & zero; + + // Assert + Assert.Equal(WrapIPv4("0.0.0.0"), result); + } + + /// Verifies the & operator returns the original value when ANDed with an all-ones mask. + [Fact] + public void BitwiseAnd_WithAllOnesMask_ReturnsSameValue_Test() + { + // Arrange + var address = WrapIPv4("10.20.30.40"); + var allOnes = BigEndianBitWrapper.CreateMask(4, 32); // all ones + + // Act + var result = address & allOnes; + + // Assert + Assert.Equal(address, result); + } + + #endregion // end: Bitwise AND + + #region Bitwise OR + + /// Verifies the | operator produces the correct broadcast address when ORing a network address with the inverse mask. + [Fact] + public void BitwiseOr_AddressOrInverseMask_ReturnsBroadcastAddress_Test() + { + // Arrange: 192.168.1.0 | ~255.255.255.0 = 192.168.1.255 + var network = WrapIPv4("192.168.1.0"); + var inverseMask = ~BigEndianBitWrapper.CreateMask(4, 24); + + // Act + var broadcast = network | inverseMask; + + // Assert + Assert.Equal(WrapIPv4("192.168.1.255"), broadcast); + } + + /// Verifies the | operator returns the original value when ORed with an all-zeros wrapper. + [Fact] + public void BitwiseOr_WithAllZeros_ReturnsSameValue_Test() + { + // Arrange + var address = WrapIPv4("10.20.30.40"); + var zero = BigEndianBitWrapper.CreateMask(4, 0); + + // Act + var result = address | zero; + + // Assert + Assert.Equal(address, result); + } + + /// Verifies the | operator returns the all-ones wrapper when ORed with an all-ones mask. + [Fact] + public void BitwiseOr_WithAllOnes_ReturnsAllOnes_Test() + { + // Arrange + var address = WrapIPv4("10.20.30.40"); + var allOnes = BigEndianBitWrapper.CreateMask(4, 32); + + // Act + var result = address | allOnes; + + // Assert + Assert.Equal(WrapIPv4("255.255.255.255"), result); + } + + #endregion // end: Bitwise OR + + #region Bitwise NOT + + /// Verifies the ~ operator inverts a /24 mask to the correct host mask. + [Fact] + public void BitwiseNot_Mask24_ReturnsInverseMask_Test() + { + // Arrange: ~255.255.255.0 = 0.0.0.255 + var mask = BigEndianBitWrapper.CreateMask(4, 24); + + // Act + var inverse = ~mask; + + // Assert + Assert.Equal(WrapIPv4("0.0.0.255"), inverse); + Assert.Equal(4, inverse.ByteWidth); + } + + /// Verifies the ~ operator inverts an all-zero wrapper to the all-ones wrapper. + [Fact] + public void BitwiseNot_AllZeros_ReturnsMaxForWidth_Test() + { + // Arrange + var zeros = BigEndianBitWrapper.CreateMask(4, 0); // 0.0.0.0 + + // Act + var result = ~zeros; + + // Assert + Assert.Equal(WrapIPv4("255.255.255.255"), result); + } + + /// Verifies the ~ operator inverts an all-ones wrapper to the all-zero wrapper. + [Fact] + public void BitwiseNot_AllOnes_ReturnsZero_Test() + { + // Arrange + var allOnes = BigEndianBitWrapper.CreateMask(4, 32); + + // Act + var result = ~allOnes; + + // Assert + Assert.Equal(WrapIPv4("0.0.0.0"), result); + } + + /// Verifies the ~ operator correctly inverts a /64 IPv6 mask to the host portion. + [Fact] + public void BitwiseNot_IPv6Mask64_ReturnsCorrectInverse_Test() + { + // Arrange: ffff:ffff:ffff:ffff:: → NOT → ::ffff:ffff:ffff:ffff + var mask = BigEndianBitWrapper.CreateMask(16, 64); + + // Act + var inverse = ~mask; + + // Assert + var expected = WrapIPv6("::ffff:ffff:ffff:ffff"); + Assert.Equal(expected, inverse); + } + + /// Verifies the ~ operator preserves the byte width of the operand. + [Fact] + public void BitwiseNot_DoesNotAffectByteWidth_Test() + { + // Arrange + var w = WrapIPv4("0.0.0.0"); + + // Act + var result = ~w; + + // Assert - byte width unchanged + Assert.Equal(w.ByteWidth, result.ByteWidth); + } + + #endregion // end: Bitwise NOT + + #region B3: ByteWidth validation for binary operators + + /// Verifies the & operator throws ArgumentException when operands have different ByteWidth. + [Fact] + public void AndOperator_WithDifferentByteWidth_ThrowsArgumentException_Test() + { + var ipv4 = BigEndianBitWrapper.FromBytes([192, 168, 1, 1]); + var ipv6 = BigEndianBitWrapper.CreateMask(16, 64); + Assert.Throws(() => ipv4 & ipv6); + } + + /// Verifies the | operator throws ArgumentException when operands have different ByteWidth. + [Fact] + public void OrOperator_WithDifferentByteWidth_ThrowsArgumentException_Test() + { + var ipv4 = BigEndianBitWrapper.FromBytes([192, 168, 1, 1]); + var ipv6 = BigEndianBitWrapper.CreateMask(16, 64); + Assert.Throws(() => ipv4 | ipv6); + } + + #endregion + + #region B4: Operator result masking + +#if NET8_0_OR_GREATER + private static BigEndianBitWrapper CreateWrapperWithHighBits(int byteWidth) + { + var ctor = typeof(BigEndianBitWrapper) + .GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic) + .Single(c => c.GetParameters().Length == 2); + return (BigEndianBitWrapper)ctor.Invoke([UInt128.MaxValue, byteWidth]); + } +#else + private static BigEndianBitWrapper CreateWrapperWithHighBits(int byteWidth) + { + var ctor = typeof(BigEndianBitWrapper) + .GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic) + .Single(c => c.GetParameters().Length == 3); + var hi = byteWidth > 8 ? ulong.MaxValue : 1UL; + const ulong lo = ulong.MaxValue; + return (BigEndianBitWrapper)ctor.Invoke([hi, lo, byteWidth]); + } +#endif + + /// + /// Verifies the & operator masks the result to the operand's ByteWidth. The backing + /// value of a wrapper should never have bits set beyond its ByteWidth, so a bitwise AND + /// between two wrappers with high backing bits produces a result that equals the + /// correctly-bounded version. + /// + [Fact] + public void AndOperator_ResultIsMaskedToByteWidth_Test() + { + var a = CreateWrapperWithHighBits(4); + var b = CreateWrapperWithHighBits(4); + var result = a & b; + var expected = BigEndianBitWrapper.FromBytes([0xFF, 0xFF, 0xFF, 0xFF], 4); + Assert.Equal(expected, result); + } + + /// + /// Verifies the | operator masks the result to the operand's ByteWidth. Without masking, + /// ORing a high-bits wrapper with a bounded wrapper would produce a result whose backing + /// value has bits beyond the byte width. + /// + [Fact] + public void OrOperator_ResultIsMaskedToByteWidth_Test() + { + var a = CreateWrapperWithHighBits(4); + var normal = BigEndianBitWrapper.FromBytes([0x00, 0x00, 0x00, 0xFF], 4); + var result = normal | a; + var expected = BigEndianBitWrapper.FromBytes([0xFF, 0xFF, 0xFF, 0xFF], 4); + Assert.Equal(expected, result); + } + + /// Verifies the ~ operator result is correctly bounded to ByteWidth. + [Fact] + public void NotOperator_ResultIsMaskedToByteWidth_Test() + { + var w = BigEndianBitWrapper.FromBytes([0x00, 0x00, 0x00, 0xFF], 4); + var result = ~w; + // Inverse of 0x000000FF for 4 bytes = 0xFFFFFF00 + var expected = BigEndianBitWrapper.FromBytes([0xFF, 0xFF, 0xFF, 0x00], 4); + Assert.Equal(expected, result); + Assert.Equal(4, result.ByteWidth); + } + + #endregion + + #region B5: XOR operator + + /// Verifies the ^ operator produces the correct XOR result for two IPv4 wrappers. + [Fact] + public void XorOperator_ProducesCorrectResult_Test() + { + // 0xFF00FF00 ^ 0x0F0F0F0F = 0xF00FF00F + var a = BigEndianBitWrapper.FromBytes([0xFF, 0x00, 0xFF, 0x00], 4); + var b = BigEndianBitWrapper.FromBytes([0x0F, 0x0F, 0x0F, 0x0F], 4); + var result = a ^ b; + var expected = BigEndianBitWrapper.FromBytes([0xF0, 0x0F, 0xF0, 0x0F], 4); + Assert.Equal(expected, result); + } + + /// Verifies the ^ operator throws ArgumentException when operands have different ByteWidth. + [Fact] + public void XorOperator_WithDifferentByteWidth_ThrowsArgumentException_Test() + { + var ipv4 = BigEndianBitWrapper.FromBytes([192, 168, 1, 1]); + var ipv6 = BigEndianBitWrapper.CreateMask(16, 64); + Assert.Throws(() => ipv4 ^ ipv6); + } + + /// Verifies the ^ operator masks the result to the operand's ByteWidth. + [Fact] + public void XorOperator_ResultIsMaskedToByteWidth_Test() + { + var a = CreateWrapperWithHighBits(4); + var normal = BigEndianBitWrapper.FromBytes([0x00, 0x00, 0x00, 0xFF], 4); + var result = a ^ normal; + var expected = BigEndianBitWrapper.FromBytes([0xFF, 0xFF, 0xFF, 0x00], 4); + Assert.Equal(expected, result); + } + + #endregion + + #region Operators: < > <= >= + + /// Verifies the < operator returns the expected result for all ordered IPv4 pairs. + /// The left IPv4 address string. + /// The right IPv4 address string. + /// The expected comparison result. + [Theory] + [MemberData(nameof(CompareTo_Test_Data))] + public void LessThan_Operator_ReturnsExpected_Test(string leftStr, string rightStr, int expectedSign) + { + // Arrange + var left = WrapIPv4(leftStr); + var right = WrapIPv4(rightStr); + + // Act / Assert + Assert.Equal(expectedSign < 0, left < right); + } + + /// Verifies the > operator returns the expected result for all ordered IPv4 pairs. + /// The left IPv4 address string. + /// The right IPv4 address string. + /// The expected comparison result. + [Theory] + [MemberData(nameof(CompareTo_Test_Data))] + public void GreaterThan_Operator_ReturnsExpected_Test(string leftStr, string rightStr, int expectedSign) + { + // Arrange + var left = WrapIPv4(leftStr); + var right = WrapIPv4(rightStr); + + // Act / Assert + Assert.Equal(expectedSign > 0, left > right); + } + + /// Verifies the <= operator returns the expected result for all ordered IPv4 pairs. + /// The left IPv4 address string. + /// The right IPv4 address string. + /// The expected comparison result. + [Theory] + [MemberData(nameof(CompareTo_Test_Data))] + public void LessThanOrEqual_Operator_ReturnsExpected_Test(string leftStr, string rightStr, int expectedSign) + { + // Arrange + var left = WrapIPv4(leftStr); + var right = WrapIPv4(rightStr); + + // Act / Assert + Assert.Equal(expectedSign <= 0, left <= right); + } + + /// Verifies the >= operator returns the expected result for all ordered IPv4 pairs. + /// The left IPv4 address string. + /// The right IPv4 address string. + /// The expected comparison result. + [Theory] + [MemberData(nameof(CompareTo_Test_Data))] + public void GreaterThanOrEqual_Operator_ReturnsExpected_Test(string leftStr, string rightStr, int expectedSign) + { + // Arrange + var left = WrapIPv4(leftStr); + var right = WrapIPv4(rightStr); + + // Act / Assert + Assert.Equal(expectedSign >= 0, left >= right); + } + + #endregion // end: Operators: < > <= >= + } +} diff --git a/src/Arcus.Tests/BigEndianBitWrapperTests.Safety.cs b/src/Arcus.Tests/BigEndianBitWrapperTests.Safety.cs new file mode 100644 index 0000000..606d554 --- /dev/null +++ b/src/Arcus.Tests/BigEndianBitWrapperTests.Safety.cs @@ -0,0 +1,117 @@ +namespace Arcus.Tests +{ + /// + /// tests for safety and robustness: span validation, + /// default-value behavior, width-mismatch detection, and unchecked arithmetic. + /// + public partial class BigEndianBitWrapperTests + { + #region B1: ToBytes span validation + +#if NET8_0_OR_GREATER + /// Verifies ToBytes(Span<byte>) throws ArgumentException when the destination is too short. + [Fact] + public void ToBytesSpan_WithInsufficientLength_ThrowsArgumentException_Test() + { + var w = BigEndianBitWrapper.FromBytes(new byte[4]); + var dest = new byte[2]; + Assert.Throws(() => w.ToBytes(dest)); + } +#endif + + #endregion + + #region B2: Default wrapper (ByteWidth=0) handling + + /// Verifies the default wrapper has ByteWidth=0 and MaxValueForWidth does not throw. + [Fact] + public void DefaultWrapper_ByteWidthIsZero_MaxValueForWidthReturnsZero_Test() + { + var w = default(BigEndianBitWrapper); + Assert.Equal(0, w.ByteWidth); + // ToBytes should produce an empty array without crashing. + var bytes = w.ToBytes(); + Assert.Empty(bytes); + } + + /// + /// Verifies that TryAdd on a default wrapper rejects all non-zero deltas and sets + /// result to default (ByteWidth == 0). + /// + [Fact] + public void DefaultWrapper_TryAddRejectsAllDeltas_Test() + { + var w = default(BigEndianBitWrapper); + var success = w.TryAdd(1, out var result); + Assert.False(success); + Assert.Equal(default, result); + Assert.Equal(0, result.ByteWidth); + + success = w.TryAdd(-1, out result); + Assert.False(success); + Assert.Equal(default, result); + Assert.Equal(0, result.ByteWidth); + } + + #endregion + + #region B6: Subtract validation + + /// Verifies Subtract throws ArgumentException when operands have different ByteWidth. + [Fact] + public void Subtract_WithDifferentByteWidth_ThrowsArgumentException_Test() + { + var ipv4 = BigEndianBitWrapper.FromBytes([192, 168, 1, 1]); + var ipv6 = BigEndianBitWrapper.FromBytes([ + 0x20, + 0x01, + 0x0D, + 0xB8, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x01, + ]); + Assert.Throws(() => ipv4.Subtract(ipv6)); + } + + #endregion + + #region X9: Unchecked arithmetic + + /// + /// Verifies that carry/borrow detection works correctly for edge-case values where + /// unchecked wrapping would produce incorrect results. Tests that 0 - 1 wraps properly + /// and is caught as underflow, and that equal-value subtraction produces zero correctly. + /// + [Fact] + public void BigIntegerArithmetic_ReliesOnUncheckedContext_Test() + { + // 0 - 1 underflow detection + var zero = BigEndianBitWrapper.FromBytes([0, 0, 0, 0], 4); + var success = zero.TryAdd(-1, out _); + Assert.False(success); + + // Equal-value subtraction (no borrow) + var a = BigEndianBitWrapper.FromBytes([0, 0, 0, 5], 4); + var b = BigEndianBitWrapper.FromBytes([0, 0, 0, 5], 4); + var diff = a.Subtract(b); + Assert.Equal(0, (int)diff.ToBigInteger()); + + // Large delta subtract below zero + var small = BigEndianBitWrapper.FromBytes([0, 0, 0, 1], 4); + success = small.TryAdd(-2, out _); + Assert.False(success); + } + + #endregion + } +} diff --git a/src/Arcus.Tests/BigEndianBitWrapperTests.cs b/src/Arcus.Tests/BigEndianBitWrapperTests.cs new file mode 100644 index 0000000..3ae698f --- /dev/null +++ b/src/Arcus.Tests/BigEndianBitWrapperTests.cs @@ -0,0 +1,468 @@ +using System.Globalization; +using System.Net; +using System.Reflection; + +namespace Arcus.Tests +{ + /// Unit tests for . + public partial class BigEndianBitWrapperTests + { + // Convenience helpers - keep tests focused on behaviour, not construction noise. + private static BigEndianBitWrapper Wrap(params byte[] bytes) + { + return BigEndianBitWrapper.FromBytes(bytes); + } + + private static BigEndianBitWrapper WrapIPv4(string dotted) + { + return Wrap(IPAddress.Parse(dotted).GetAddressBytes()); + } + + private static BigEndianBitWrapper WrapIPv6(string addr) + { + return Wrap(IPAddress.Parse(addr).GetAddressBytes()); + } + + #region TryAdd + + /// Verifies TryAdd with delta zero returns true and leaves the value unchanged. + [Fact] + public void TryAdd_DeltaZero_ReturnsTrueWithSameValue_Test() + { + // Arrange + var w = WrapIPv4("10.0.0.1"); + + // Act + var success = w.TryAdd(0, out var result); + + // Assert + Assert.True(success); + Assert.Equal(w, result); + } + + /// Verifies TryAdd with a positive delta of one increments to the next address. + [Fact] + public void TryAdd_PositiveDelta_IncrementsByDelta_Test() + { + // Arrange + var w = WrapIPv4("10.0.0.1"); + + // Act + var success = w.TryAdd(1, out var result); + + // Assert + Assert.True(success); + Assert.Equal(WrapIPv4("10.0.0.2"), result); + } + + /// Verifies TryAdd with a large positive delta produces the correctly incremented value. + [Fact] + public void TryAdd_PositiveDeltaLarge_IncrementsByDelta_Test() + { + // Arrange + var w = WrapIPv4("10.0.0.1"); + + // Act + var success = w.TryAdd(254, out var result); + + // Assert + Assert.True(success); + Assert.Equal(WrapIPv4("10.0.0.255"), result); + } + + /// Verifies TryAdd with a negative delta decrements the address by the given amount. + [Fact] + public void TryAdd_NegativeDelta_DecrementsByDelta_Test() + { + // Arrange + var w = WrapIPv4("10.0.0.5"); + + // Act + var success = w.TryAdd(-3, out var result); + + // Assert + Assert.True(success); + Assert.Equal(WrapIPv4("10.0.0.2"), result); + } + + /// Verifies TryAdd returns false and default when incrementing past the IPv4 maximum address. + [Fact] + public void TryAdd_PositiveDelta_IPv4MaxAddress_ReturnsFalse_Test() + { + // Arrange: 255.255.255.255 + 1 overflows IPv4 + var w = WrapIPv4("255.255.255.255"); + + // Act + var success = w.TryAdd(1, out var result); + + // Assert + Assert.False(success); + Assert.Equal(default, result); + } + + /// Verifies TryAdd returns false and default when decrementing below the IPv4 minimum address. + [Fact] + public void TryAdd_NegativeDelta_OnZeroAddress_ReturnsFalse_Test() + { + // Arrange: 0.0.0.0 - 1 underflows + var w = WrapIPv4("0.0.0.0"); + + // Act + var success = w.TryAdd(-1, out var result); + + // Assert + Assert.False(success); + Assert.Equal(default, result); + } + + /// Verifies TryAdd returns false when the delta exceeds the maximum value representable by the byte width. + [Fact] + public void TryAdd_PositiveDeltaExceedsWidthBound_ReturnsFalse_Test() + { + // Arrange: any IPv4 + 2^32 overflows because delta > max for 4 bytes + var w = WrapIPv4("0.0.0.0"); + + // Act + var success = w.TryAdd(4294967296L, out _); // 2^32 + + // Assert + Assert.False(success); + } + + /// Verifies TryAdd returns false when incrementing past the IPv6 maximum address. + [Fact] + public void TryAdd_IPv6_MaxAddress_Overflow_ReturnsFalse_Test() + { + // Arrange + var maxIPv6 = WrapIPv6("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"); + + // Act + var success = maxIPv6.TryAdd(1, out _); + + // Assert + Assert.False(success); + } + + /// Verifies TryAdd returns false when decrementing below the IPv6 minimum address. + [Fact] + public void TryAdd_IPv6_ZeroAddress_Underflow_ReturnsFalse_Test() + { + // Arrange + var zeroIPv6 = WrapIPv6("::"); + + // Act + var success = zeroIPv6.TryAdd(-1, out _); + + // Assert + Assert.False(success); + } + + /// + /// Verifies TryAdd with as delta correctly decrements an IPv6 address + /// whose numeric value is >= the magnitude of long.MinValue. + /// This exercises the delta == long.MinValue special-case branch in + /// which avoids -(long.MinValue) overflow. + /// + [Fact] + public void TryAdd_DeltaLongMinValue_IPv6_DecrementsByMagnitude_Test() + { + // Arrange: an IPv6 address with numeric value exactly 2^63 (0x8000_0000_0000_0000) + // Byte layout (big-endian, 16 bytes): 8 zero bytes, 0x80, 7 zero bytes + var bytes = new byte[16]; + bytes[8] = 0x80; + var w = Wrap(bytes); + + // Act: decrement by -long.MinValue (magnitude = 2^63) + var success = w.TryAdd(long.MinValue, out var result); + + // Assert: value goes to 0 + Assert.True(success); + Assert.Equal(Wrap(new byte[16]), result); + } + + /// + /// Verifies TryAdd with returns false when the value is smaller + /// than the magnitude of long.MinValue (i.e. underflow). + /// + [Fact] + public void TryAdd_DeltaLongMinValue_Underflow_ReturnsFalse_Test() + { + // Arrange: IPv4 address has value way less than 2^63 + var w = WrapIPv4("10.0.0.1"); + + // Act + var success = w.TryAdd(long.MinValue, out _); + + // Assert + Assert.False(success); + } + + /// Verifies TryAdd correctly increments across a 16-bit word boundary in an IPv6 address. + [Fact] + public void TryAdd_IPv6_IncrementAcrossWordBoundary_Test() + { + // Arrange: ::ffff + 1 = ::1:0000 + var w = WrapIPv6("::ffff"); + + // Act + var success = w.TryAdd(1, out var result); + + // Assert + Assert.True(success); + Assert.Equal(WrapIPv6("::1:0"), result); + } + + /// Verifies TryAdd correctly increments across a byte rollover boundary in an IPv4 address. + [Fact] + public void TryAdd_IPv4_IncrementAcrossByteRollover_Test() + { + // Arrange: 10.0.0.255 + 1 = 10.0.1.0 + var w = WrapIPv4("10.0.0.255"); + + // Act + var success = w.TryAdd(1, out var result); + + // Assert + Assert.True(success); + Assert.Equal(WrapIPv4("10.0.1.0"), result); + } + + #endregion // end: TryAdd + + #region Subtract + + /// Verifies Subtract returns the correct numeric difference between two IPv4 addresses. + [Fact] + public void Subtract_BasicDifference_ReturnsExpectedValue_Test() + { + // Arrange + var tail = WrapIPv4("10.0.0.10"); + var head = WrapIPv4("10.0.0.1"); + + // Act + var diff = tail.Subtract(head); + + // Assert - difference is 9 + Assert.Equal(9, (int)diff.ToBigInteger()); + } + + /// Verifies Subtract returns zero when both operands are equal. + [Fact] + public void Subtract_EqualValues_ReturnsZero_Test() + { + // Arrange + var w = WrapIPv4("192.168.1.1"); + + // Act + var diff = w.Subtract(w); + + // Assert + Assert.Equal(0, (int)diff.ToBigInteger()); + Assert.Equal(new byte[] { 0, 0, 0, 0 }, diff.ToBytes()); + } + + /// Verifies Subtract throws InvalidOperationException when the result would be negative. + [Fact] + public void Subtract_Underflow_ThrowsInvalidOperationException_Test() + { + // Arrange + var small = WrapIPv4("10.0.0.1"); + var large = WrapIPv4("10.0.0.100"); + + // Act / Assert + Assert.Throws(() => small.Subtract(large)); + } + + /// Verifies Subtract returns the correct BigInteger difference for a large IPv6 range. + [Fact] + public void Subtract_IPv6_LargeRange_ReturnsExpectedBigInteger_Test() + { + // Arrange: ::ffff:ffff - ::0 = 4294967295 + var tail = WrapIPv6("::ffff:ffff"); + var head = WrapIPv6("::"); + + // Act + var diff = tail.Subtract(head); + + // Assert + Assert.Equal(System.Numerics.BigInteger.Parse("4294967295", CultureInfo.InvariantCulture), diff.ToBigInteger()); + } + + #endregion // end: Subtract + + #region ToBigInteger + + /// + /// Gets parameters: input (byte[]), expected (long). + /// + /// + /// Parameters: input (byte[]), expected (long). + /// + public static TheoryData ToBigInteger_KnownValues_Test_Data => + new() + { + { new byte[] { 0, 0, 0, 0 }, 0L }, + { new byte[] { 0, 0, 0, 1 }, 1L }, + { new byte[] { 0, 0, 1, 0 }, 256L }, + { new byte[] { 1, 0, 0, 0 }, 16777216L }, + { new byte[] { 255, 255, 255, 255 }, 4294967295L }, + { new byte[] { 192, 168, 1, 1 }, 3232235777L }, + }; + + /// Verifies ToBigInteger returns the expected unsigned big-endian value for known IPv4 byte patterns. + /// The raw bytes to wrap. + /// The expected integer value. + [Theory] + [MemberData(nameof(ToBigInteger_KnownValues_Test_Data))] + public void ToBigInteger_KnownIPv4Values_ReturnsExpected_Test(byte[] input, long expected) + { + // Arrange + var w = Wrap(input); + + // Act + var result = w.ToBigInteger(); + + // Assert + Assert.Equal(new System.Numerics.BigInteger(expected), result); + } + + /// Verifies ToBigInteger returns zero for the IPv6 all-zeros address. + [Fact] + public void ToBigInteger_ZeroIPv6_ReturnsZero_Test() + { + // Act + var result = WrapIPv6("::").ToBigInteger(); + + // Assert + Assert.Equal(System.Numerics.BigInteger.Zero, result); + } + + /// Verifies ToBigInteger always returns a non-negative value (unsigned interpretation). + [Fact] + public void ToBigInteger_IsNonNegative_Test() + { + // Arrange: max IPv4 value + var w = WrapIPv4("255.255.255.255"); + + // Act + var result = w.ToBigInteger(); + + // Assert + Assert.True(result >= 0); + } + + #endregion // end: ToBigInteger + + #region ByteWidth preservation + + /// Verifies ByteWidth is preserved through bitwise NOT, TryAdd increment, and bitwise AND operations. + /// The byte width to test. + [Theory] + [InlineData(4)] + [InlineData(6)] + [InlineData(16)] + public void ByteWidth_IsPreservedAcrossOperations_Test(int byteWidth) + { + // Arrange + var bytes = new byte[byteWidth]; + bytes[byteWidth - 1] = 0x01; + var w = Wrap(bytes); + + // Act: put the wrapper through multiple operations + var notW = ~w; + w.TryAdd(1, out var incremented); + var andResult = w & notW; + + // Assert: ByteWidth is preserved through all operations + Assert.Equal(byteWidth, notW.ByteWidth); + Assert.Equal(byteWidth, incremented.ByteWidth); + Assert.Equal(byteWidth, andResult.ByteWidth); + } + + #endregion // end: ByteWidth preservation + + #region Subnet operations (combined) + + /// Verifies that applying a /24 mask to a host address correctly computes the network and broadcast addresses for IPv4. + [Fact] + public void SubnetOps_ComputeNetworkAndBroadcast_IPv4_Test() + { + // Reproduces the NormalizeAndCreateNetMask pattern from Subnet.cs + // Arrange: host address 192.168.1.100/24 + var headBytes = IPAddress.Parse("192.168.1.100").GetAddressBytes(); + var head = Wrap(headBytes); + var mask = BigEndianBitWrapper.CreateMask(4, 24); + + // Act + var network = (head & mask).ToBytes(); + var broadcast = (head | ~mask).ToBytes(); + + // Assert + Assert.Equal(IPAddress.Parse("192.168.1.0").GetAddressBytes(), network); + Assert.Equal(IPAddress.Parse("192.168.1.255").GetAddressBytes(), broadcast); + } + + /// Verifies that applying a /64 mask to a host address correctly computes the network and broadcast addresses for IPv6. + [Fact] + public void SubnetOps_ComputeNetworkAndBroadcast_IPv6_Test() + { + // Arrange: 2001:db8::1/64 + var headBytes = IPAddress.Parse("2001:db8::1").GetAddressBytes(); + var head = Wrap(headBytes); + var mask = BigEndianBitWrapper.CreateMask(16, 64); + + // Act + var network = (head & mask).ToBytes(); + var broadcast = (head | ~mask).ToBytes(); + + // Assert + Assert.Equal(IPAddress.Parse("2001:db8::").GetAddressBytes(), network); + Assert.Equal(IPAddress.Parse("2001:db8::ffff:ffff:ffff:ffff").GetAddressBytes(), broadcast); + } + + #endregion // end: Subnet operations (combined) + + #region DebuggerDisplay + + /// Verifies the debugger display format for an IPv4 address produces the expected hex-with-underscore representation. + [Fact] + public void DebuggerDisplay_IPv4_ReturnsExpectedFormat_Test() + { + // Arrange + var w = WrapIPv4("192.168.1.1"); + var prop = typeof(BigEndianBitWrapper).GetProperty( + "DebuggerDisplay", + BindingFlags.Instance | BindingFlags.NonPublic + ); + + // Act + var value = prop!.GetValue(w) as string; + + // Assert + Assert.Equal("0xC0_A8_01_01 (4 bytes)", value); + } + + /// Verifies the debugger display format for an IPv6 address produces the expected hex-with-underscore representation. + [Fact] + public void DebuggerDisplay_IPv6_ReturnsExpectedFormat_Test() + { + // Arrange + var w = WrapIPv6("2001:db8::1"); + var prop = typeof(BigEndianBitWrapper).GetProperty( + "DebuggerDisplay", + BindingFlags.Instance | BindingFlags.NonPublic + ); + + // Act + var value = prop!.GetValue(w) as string; + + // Assert: 16 bytes in a 128-bit IPv6 address + // Format: "0x" + 47 hex-and-underscore chars + " (16 bytes)" = 60 + Assert.EndsWith("(16 bytes)", value, StringComparison.Ordinal); + Assert.StartsWith("0x", value, StringComparison.Ordinal); + Assert.Equal(60, value!.Length); + } + + #endregion // end: DebuggerDisplay + } +} diff --git a/src/Arcus.Tests/Comparers/DefaultAddressFamilyComparerTests.cs b/src/Arcus.Tests/Comparers/DefaultAddressFamilyComparerTests.cs index e327314..e5642a0 100644 --- a/src/Arcus.Tests/Comparers/DefaultAddressFamilyComparerTests.cs +++ b/src/Arcus.Tests/Comparers/DefaultAddressFamilyComparerTests.cs @@ -1,12 +1,14 @@ -using System.Collections.Generic; -using System.Net.Sockets; +using System.Net.Sockets; using Arcus.Comparers; -using Xunit; namespace Arcus.Tests.Comparers { + /// Unit tests for . public class DefaultAddressFamilyComparerTests { + /// + /// Verifies that is assignable to . + /// [Fact] public void Assignability_Test() { @@ -22,22 +24,48 @@ public void Assignability_Test() #region Compare - public static IEnumerable Compare_Test_Values() + /// + /// Gets test data for . + /// Covers all combinations of and + /// to verify ordinal ordering matches + /// . + /// Parameters: expected comparison result (int), x (AddressFamily), y (AddressFamily). + /// + /// + /// Covers all combinations of and + /// to verify ordinal ordering matches + /// . + /// Parameters: expected comparison result (int), x (AddressFamily), y (AddressFamily). + /// + public static TheoryData Compare_AddressFamilies_ReturnsExpectedOrdering_Test_Data { - var concernedAddressFamilies = new[] { AddressFamily.InterNetwork, AddressFamily.InterNetworkV6 }; - - foreach (var i in concernedAddressFamilies) + get { - foreach (var j in concernedAddressFamilies) + var data = new TheoryData(); + var concernedAddressFamilies = new[] { AddressFamily.InterNetwork, AddressFamily.InterNetworkV6 }; + + foreach (var i in concernedAddressFamilies) { - yield return new object[] { i.CompareTo(j), i, j }; + foreach (var j in concernedAddressFamilies) + { + data.Add(i.CompareTo(j), i, j); + } } + + return data; } } + /// + /// Verifies that returns the expected ordinal comparison + /// result for pairs of values. + /// + /// the expected comparison result. + /// the left operand. + /// the right operand. [Theory] - [MemberData(nameof(Compare_Test_Values))] - public void Compare_Test(int expected, AddressFamily x, AddressFamily y) + [MemberData(nameof(Compare_AddressFamilies_ReturnsExpectedOrdering_Test_Data))] + public void Compare_AddressFamilies_ReturnsExpectedOrdering_Test(int expected, AddressFamily x, AddressFamily y) { // Arrange var comparer = new DefaultAddressFamilyComparer(); @@ -50,5 +78,25 @@ public void Compare_Test(int expected, AddressFamily x, AddressFamily y) } #endregion // end: Compare + + #region Instance + + /// + /// Verifies that is not + /// and is an instance of . + /// + [Fact] + public void Instance_IsNotNull_Test() + { + // Arrange + // Act + var instance = DefaultAddressFamilyComparer.Instance; + + // Assert + Assert.NotNull(instance); + Assert.IsType(instance); + } + + #endregion // end: Instance } } diff --git a/src/Arcus.Tests/Comparers/DefaultIIPAddressRangeComparerTests.cs b/src/Arcus.Tests/Comparers/DefaultIIPAddressRangeComparerTests.cs index 51a2f0f..c03088a 100644 --- a/src/Arcus.Tests/Comparers/DefaultIIPAddressRangeComparerTests.cs +++ b/src/Arcus.Tests/Comparers/DefaultIIPAddressRangeComparerTests.cs @@ -1,18 +1,17 @@ -using System; -using System.Collections.Generic; -using System.Net; +using System.Net; using System.Numerics; using Arcus.Comparers; using NSubstitute; -using Xunit; namespace Arcus.Tests.Comparers { -#if NET6_0_OR_GREATER -#pragma warning disable IDE0062 // Make local function static (IDE0062); purposely allowing non-static functions that could be static for .net4.8 compatibility -#endif + /// Unit tests for . public class DefaultIIPAddressRangeComparerTests { + /// + /// Verifies that is assignable to + /// . + /// [Fact] public void Assignability_Test() { @@ -26,8 +25,32 @@ public void Assignability_Test() Assert.True(isAssignableFrom); } + #region Instance + + /// + /// Verifies that is not + /// and is an instance of . + /// + [Fact] + public void Instance_IsNotNull_Test() + { + // Arrange + // Act + var instance = DefaultIIPAddressRangeComparer.Instance; + + // Assert + Assert.NotNull(instance); + Assert.IsType(instance); + } + + #endregion // end: Instance + #region Ctor + /// + /// Verifies that constructing a with a + /// throws . + /// [Fact] public void Ctor_NullInput_Throws_ArgumentNullException_Test() { @@ -42,68 +65,84 @@ public void Ctor_NullInput_Throws_ArgumentNullException_Test() #region Compare - public static IEnumerable Compare_Test_Values() + /// + /// Gets test data for . + /// Covers equal ranges, same-reference ranges, null operands, cross-family comparisons, + /// and ordinal ordering by length within the same address family. + /// Parameters: expected comparison result (int), x (IIPAddressRange), y (IIPAddressRange). + /// + /// + /// Test data for . + /// Covers equal ranges, same-reference ranges, null operands, cross-family comparisons, + /// and ordinal ordering by length within the same address family. + /// Parameters: expected comparison result (int), x (IIPAddressRange), y (IIPAddressRange). + /// + public static TheoryData Compare_Ranges_ReturnsExpectedOrdering_Test_Data { - // equal ranges - yield return new object[] { 0, CreateIIPAddressRange("192.168.1.0", 0), CreateIIPAddressRange("192.168.1.0", 0) }; - yield return new object[] { 0, CreateIIPAddressRange("a::", 0), CreateIIPAddressRange("a::", 0) }; - - // same range - var ipv4Same = CreateIIPAddressRange("192.168.1.0"); - yield return new object[] { 0, ipv4Same, ipv4Same }; - - var ipv6Same = CreateIIPAddressRange("a::"); - yield return new object[] { 0, ipv6Same, ipv6Same }; - - // null compare - yield return new object[] { 0, null, null }; - yield return new object[] { -1, null, CreateIIPAddressRange("192.168.1.0") }; - yield return new object[] { 1, CreateIIPAddressRange("192.168.1.0"), null }; - yield return new object[] { -1, null, CreateIIPAddressRange("a::") }; - yield return new object[] { 1, CreateIIPAddressRange("a::"), null }; - - // numerically equivalent, different address families - yield return new object[] { -1, CreateIIPAddressRange("0.0.0.0", 100), CreateIIPAddressRange("::", 100) }; - yield return new object[] { 1, CreateIIPAddressRange("::", 100), CreateIIPAddressRange("0.0.0.0", 100) }; - - // satisfies ordinal ordering by length - yield return new object[] { -1, CreateIIPAddressRange("192.0.0.0", 100), CreateIIPAddressRange("192.0.0.0", 500) }; - yield return new object[] { 1, CreateIIPAddressRange("192.0.0.0", 500), CreateIIPAddressRange("192.0.0.0", 100) }; - yield return new object[] { -1, CreateIIPAddressRange("ab::", 100), CreateIIPAddressRange("ab::", 500) }; - yield return new object[] { 1, CreateIIPAddressRange("ab::", 500), CreateIIPAddressRange("ab::", 100) }; - - IIPAddressRange CreateIIPAddressRange(string head, BigInteger? length = null) + get { - var substitute = Substitute.For(); - - substitute.Head.Returns(IPAddress.Parse(head)); - - if (length != null) + var data = new TheoryData { - substitute.Length.Returns(length.Value); - } - - return substitute; + // equal ranges + { 0, CreateRangeFromHead("192.168.1.0", 0), CreateRangeFromHead("192.168.1.0", 0) }, + { 0, CreateRangeFromHead("a::", 0), CreateRangeFromHead("a::", 0) }, + }; + + // same range (reference equality) + var ipv4Same = CreateRangeFromHead("192.168.1.0"); + data.Add(0, ipv4Same, ipv4Same); + + var ipv6Same = CreateRangeFromHead("a::"); + data.Add(0, ipv6Same, ipv6Same); + + // null compare + data.Add(0, null, null); + data.Add(-1, null, CreateRangeFromHead("192.168.1.0")); + data.Add(1, CreateRangeFromHead("192.168.1.0"), null); + data.Add(-1, null, CreateRangeFromHead("a::")); + data.Add(1, CreateRangeFromHead("a::"), null); + + // numerically equivalent, different address families + data.Add(-1, CreateRangeFromHead("0.0.0.0", 100), CreateRangeFromHead("::", 100)); + data.Add(1, CreateRangeFromHead("::", 100), CreateRangeFromHead("0.0.0.0", 100)); + + // satisfies ordinal ordering by length + data.Add(-1, CreateRangeFromHead("192.0.0.0", 100), CreateRangeFromHead("192.0.0.0", 500)); + data.Add(1, CreateRangeFromHead("192.0.0.0", 500), CreateRangeFromHead("192.0.0.0", 100)); + data.Add(-1, CreateRangeFromHead("ab::", 100), CreateRangeFromHead("ab::", 500)); + data.Add(1, CreateRangeFromHead("ab::", 500), CreateRangeFromHead("ab::", 100)); + + return data; } } + /// + /// Verifies that returns the expected ordinal comparison + /// result for pairs of values. + /// + /// the expected comparison result. + /// the left operand. + /// the right operand. [Theory] - [MemberData(nameof(Compare_Test_Values))] - public void Compare_Test(int expected, IIPAddressRange x, IIPAddressRange y) + [MemberData(nameof(Compare_Ranges_ReturnsExpectedOrdering_Test_Data))] + public void Compare_Ranges_ReturnsExpectedOrdering_Test(int expected, IIPAddressRange x, IIPAddressRange y) { // Arrange var comparer = new DefaultIIPAddressRangeComparer(); // Act - var result = comparer.Compare(x, y); // Assert Assert.Equal(expected, result); } + /// + /// Verifies that defers to the injected + /// when comparing the head addresses of two ranges. + /// [Fact] - public void DeferToIPAddressComparerTest() + public void Compare_DifferentHeads_DefersToIPAddressComparer_Test() { // Arrange const int expectedResult = 42; @@ -111,8 +150,8 @@ public void DeferToIPAddressComparerTest() var xHead = IPAddress.Parse("0.0.0.0"); var yHead = IPAddress.Parse("abc::"); - var x = CreateIIPAddressRange(xHead); - var y = CreateIIPAddressRange(yHead); + var x = CreateRangeFromIPAddress(xHead); + var y = CreateRangeFromIPAddress(yHead); var substituteIPAddressComparer = Substitute.For>(); substituteIPAddressComparer.Compare(xHead, yHead).Returns(expectedResult); @@ -124,18 +163,28 @@ public void DeferToIPAddressComparerTest() // Assert Assert.Equal(expectedResult, result); - substituteIPAddressComparer.Received(1).Compare(xHead, yHead); // Verify that Compare was called once - - IIPAddressRange CreateIIPAddressRange(IPAddress head) - { - var substitute = Substitute.For(); + substituteIPAddressComparer.Received(1).Compare(xHead, yHead); + } - substitute.Head.Returns(head); + #endregion // end: Compare - return substitute; + private static IIPAddressRange CreateRangeFromHead(string head, BigInteger? length = null) + { + var substitute = Substitute.For(); + substitute.Head.Returns(IPAddress.Parse(head)); + if (length != null) + { + substitute.Length.Returns(length.Value); } + + return substitute; } - #endregion // end: Compare + private static IIPAddressRange CreateRangeFromIPAddress(IPAddress head) + { + var substitute = Substitute.For(); + substitute.Head.Returns(head); + return substitute; + } } } diff --git a/src/Arcus.Tests/Comparers/DefaultIPAddressComparerTests.cs b/src/Arcus.Tests/Comparers/DefaultIPAddressComparerTests.cs index 4a87f1f..968bf3c 100644 --- a/src/Arcus.Tests/Comparers/DefaultIPAddressComparerTests.cs +++ b/src/Arcus.Tests/Comparers/DefaultIPAddressComparerTests.cs @@ -1,15 +1,16 @@ -using System; -using System.Collections.Generic; -using System.Net; +using System.Net; using System.Net.Sockets; using Arcus.Comparers; using NSubstitute; -using Xunit; namespace Arcus.Tests.Comparers { + /// Unit tests for . public class DefaultIPAddressComparerTests { + /// + /// Verifies that is assignable to . + /// [Fact] public void Assignability_Test() { @@ -25,6 +26,10 @@ public void Assignability_Test() #region Ctor + /// + /// Verifies that constructing a with a + /// throws . + /// [Fact] public void Ctor_NullInput_Throws_ArgumentNullException_Test() { @@ -35,58 +40,105 @@ public void Ctor_NullInput_Throws_ArgumentNullException_Test() Assert.Throws(() => new DefaultIPAddressComparer(null)); } + /// + /// Verifies that constructing a with a valid + /// succeeds without throwing. + /// + [Fact] + public void Ctor_ValidAddressFamilyComparer_DoesNotThrow_Test() + { + // Arrange + var addressFamilyComparer = Substitute.For>(); + + // Act + var comparer = new DefaultIPAddressComparer(addressFamilyComparer); + + // Assert + Assert.NotNull(comparer); + } + #endregion // end: Ctor #region Compare - public static IEnumerable Compare_Test_Values() + /// + /// Gets test data for . + /// Covers equal addresses (by value and by reference), null operands, cross-family comparisons, + /// and ordinal ordering within the same address family to partition the comparison logic. + /// Parameters: expected comparison result (int), x (IPAddress), y (IPAddress). + /// + /// + /// Test data for . + /// Covers equal addresses (by value and by reference), null operands, cross-family comparisons, + /// and ordinal ordering within the same address family to partition the comparison logic. + /// Parameters: expected comparison result (int), x (IPAddress), y (IPAddress). + /// + public static TheoryData Compare_IPAddresses_ReturnsExpectedOrdering_Test_Data { - // equal addresses - yield return new object[] { 0, IPAddress.Parse("192.168.1.1"), IPAddress.Parse("192.168.1.1") }; - yield return new object[] { 0, IPAddress.Parse("dead::beef"), IPAddress.Parse("dead::beef") }; - - // same address - var ipv4Same = IPAddress.Parse("192.168.1.1"); - yield return new object[] { 0, ipv4Same, ipv4Same }; - - var ipv6Same = IPAddress.Parse("dead::beef"); - yield return new object[] { 0, ipv6Same, ipv6Same }; - - // null compare - yield return new object[] { 0, null, null }; - yield return new object[] { -1, null, IPAddress.Parse("192.168.1.1") }; - yield return new object[] { 1, IPAddress.Parse("192.168.1.1"), null }; - yield return new object[] { -1, null, IPAddress.Parse("dead::beef") }; - yield return new object[] { 1, IPAddress.Parse("dead::beef"), null }; - - // numerically equivalent, different address families - yield return new object[] { -1, IPAddress.Parse("0.0.0.0"), IPAddress.Parse("::") }; - yield return new object[] { 1, IPAddress.Parse("::"), IPAddress.Parse("0.0.0.0") }; - - // satisfies ordinal ordering - yield return new object[] { -1, IPAddress.Parse("192.168.1.1"), IPAddress.Parse("192.168.1.2") }; - yield return new object[] { 1, IPAddress.Parse("192.168.1.2"), IPAddress.Parse("192.168.1.1") }; - yield return new object[] { -1, IPAddress.Parse("abc::123"), IPAddress.Parse("abc::124") }; - yield return new object[] { 1, IPAddress.Parse("abc::124"), IPAddress.Parse("abc::123") }; + get + { + var data = new TheoryData + { + // equal addresses (value equality - distinct IPAddress instances with same value) + { 0, IPAddress.Parse("10.0.0.1"), IPAddress.Parse("10.0.0.1") }, + { 0, IPAddress.Parse("dead::beef"), IPAddress.Parse("dead::beef") }, + }; + + // same address (reference equality - identical object) + var ipv4Same = IPAddress.Parse("192.168.1.1"); + data.Add(0, ipv4Same, ipv4Same); + + var ipv6Same = IPAddress.Parse("abcd::1"); + data.Add(0, ipv6Same, ipv6Same); + + // null compare + data.Add(0, null, null); + data.Add(-1, null, IPAddress.Parse("192.168.1.1")); + data.Add(1, IPAddress.Parse("192.168.1.1"), null); + data.Add(-1, null, IPAddress.Parse("dead::beef")); + data.Add(1, IPAddress.Parse("dead::beef"), null); + + // numerically equivalent, different address families + data.Add(-1, IPAddress.Parse("0.0.0.0"), IPAddress.Parse("::")); + data.Add(1, IPAddress.Parse("::"), IPAddress.Parse("0.0.0.0")); + + // satisfies ordinal ordering + data.Add(-1, IPAddress.Parse("192.168.1.1"), IPAddress.Parse("192.168.1.2")); + data.Add(1, IPAddress.Parse("192.168.1.2"), IPAddress.Parse("192.168.1.1")); + data.Add(-1, IPAddress.Parse("abc::123"), IPAddress.Parse("abc::124")); + data.Add(1, IPAddress.Parse("abc::124"), IPAddress.Parse("abc::123")); + + return data; + } } + /// + /// Verifies that returns the expected ordinal comparison + /// result for pairs of values. + /// + /// the expected comparison result. + /// the left operand. + /// the right operand. [Theory] - [MemberData(nameof(Compare_Test_Values))] - public void Compare_Test(int expected, IPAddress x, IPAddress y) + [MemberData(nameof(Compare_IPAddresses_ReturnsExpectedOrdering_Test_Data))] + public void Compare_IPAddresses_ReturnsExpectedOrdering_Test(int expected, IPAddress x, IPAddress y) { // Arrange var comparer = new DefaultIPAddressComparer(); // Act - var result = comparer.Compare(x, y); // Assert Assert.Equal(expected, result); } + /// + /// Verifies that defers to the injected + /// when the two addresses belong to different address families. + /// [Fact] - public void Compare_DeferToAddressFamilyComparer_Test() + public void Compare_DifferentAddressFamilies_DefersToAddressFamilyComparer_Test() { // Arrange const int expectedResult = 42; @@ -107,5 +159,25 @@ public void Compare_DeferToAddressFamilyComparer_Test() } #endregion // end: Compare + + #region Instance + + /// + /// Verifies that is not + /// and is an instance of . + /// + [Fact] + public void Instance_IsNotNull_Test() + { + // Arrange + // Act + var instance = DefaultIPAddressComparer.Instance; + + // Assert + Assert.NotNull(instance); + Assert.IsType(instance); + } + + #endregion // end: Instance } } diff --git a/src/Arcus.Tests/Comparers/DefaultIPAddressRangeComparerTests.cs b/src/Arcus.Tests/Comparers/DefaultIPAddressRangeComparerTests.cs deleted file mode 100644 index 168900d..0000000 --- a/src/Arcus.Tests/Comparers/DefaultIPAddressRangeComparerTests.cs +++ /dev/null @@ -1,141 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net; -using System.Numerics; -using Arcus.Comparers; -using NSubstitute; -using Xunit; - -namespace Arcus.Tests.Comparers -{ -#if NET6_0_OR_GREATER -#pragma warning disable IDE0062 // Make local function static (IDE0062); purposely allowing non-static functions that could be static for .net4.8 compatibility -#endif - public class DefaultIPAddressRangeComparerTests - { - [Fact] - public void Assignability_Test() - { - // Arrange - var type = typeof(DefaultIPAddressRangeComparer); - - // Act - var isAssignableFrom = typeof(IComparer).IsAssignableFrom(type); - - // Assert - Assert.True(isAssignableFrom); - } - - #region Ctor - - [Fact] - public void Ctor_NullInput_Throws_ArgumentNullException_Test() - { - // Arrange - // Act - // Assert - - Assert.Throws(() => new DefaultIPAddressRangeComparer(null)); - } - - #endregion // end: Ctor - - #region Compare - - public static IEnumerable Compare_Test_Values() - { - // equal ranges - yield return new object[] { 0, CreateIIPAddressRange("192.168.1.0", 0), CreateIIPAddressRange("192.168.1.0", 0) }; - yield return new object[] { 0, CreateIIPAddressRange("a::", 0), CreateIIPAddressRange("a::", 0) }; - - // same range - var ipv4Same = CreateIIPAddressRange("192.168.1.0"); - yield return new object[] { 0, ipv4Same, ipv4Same }; - - var ipv6Same = CreateIIPAddressRange("a::"); - yield return new object[] { 0, ipv6Same, ipv6Same }; - - // null compare - yield return new object[] { 0, null, null }; - yield return new object[] { -1, null, CreateIIPAddressRange("192.168.1.0") }; - yield return new object[] { 1, CreateIIPAddressRange("192.168.1.0"), null }; - yield return new object[] { -1, null, CreateIIPAddressRange("a::") }; - yield return new object[] { 1, CreateIIPAddressRange("a::"), null }; - - // numerically equivalent, different address families - yield return new object[] { -1, CreateIIPAddressRange("0.0.0.0", 100), CreateIIPAddressRange("::", 100) }; - yield return new object[] { 1, CreateIIPAddressRange("::", 100), CreateIIPAddressRange("0.0.0.0", 100) }; - - // satisfies ordinal ordering by length - yield return new object[] { -1, CreateIIPAddressRange("192.0.0.0", 100), CreateIIPAddressRange("192.0.0.0", 500) }; - yield return new object[] { 1, CreateIIPAddressRange("192.0.0.0", 500), CreateIIPAddressRange("192.0.0.0", 100) }; - yield return new object[] { -1, CreateIIPAddressRange("ab::", 100), CreateIIPAddressRange("ab::", 500) }; - yield return new object[] { 1, CreateIIPAddressRange("ab::", 500), CreateIIPAddressRange("ab::", 100) }; - - IIPAddressRange CreateIIPAddressRange(string head, BigInteger? length = null) - { - var substitute = Substitute.For(); - - substitute.Head.Returns(IPAddress.Parse(head)); - - if (length != null) - { - substitute.Length.Returns(length.Value); - } - - return substitute; - } - } - - [Theory] - [MemberData(nameof(Compare_Test_Values))] - public void Compare_Test(int expected, IIPAddressRange x, IIPAddressRange y) - { - // Arrange - var comparer = new DefaultIPAddressRangeComparer(); - - // Act - - var result = comparer.Compare(x, y); - - // Assert - Assert.Equal(expected, result); - } - - [Fact] - public void DeferToIPAddressComparerTest() - { - // Arrange - const int expectedResult = 42; - - var xHead = IPAddress.Parse("0.0.0.0"); - var yHead = IPAddress.Parse("abc::"); - - var x = CreateIIPAddressRange(xHead); - var y = CreateIIPAddressRange(yHead); - - var substituteIPAddressComparer = Substitute.For>(); - substituteIPAddressComparer.Compare(xHead, yHead).Returns(expectedResult); - - var comparer = new DefaultIPAddressRangeComparer(substituteIPAddressComparer); - - // Act - var result = comparer.Compare(x, y); - - // Assert - Assert.Equal(expectedResult, result); - substituteIPAddressComparer.Received(1).Compare(xHead, yHead); // Verify that Compare was called once - - IIPAddressRange CreateIIPAddressRange(IPAddress head) - { - var substitute = Substitute.For(); - - substitute.Head.Returns(head); - - return substitute; - } - } - - #endregion // end: Compare - } -} diff --git a/src/Arcus.Tests/Converters/IPAddressConvertersTests.cs b/src/Arcus.Tests/Converters/IPAddressConvertersTests.cs index 7f8f840..28a11a0 100644 --- a/src/Arcus.Tests/Converters/IPAddressConvertersTests.cs +++ b/src/Arcus.Tests/Converters/IPAddressConvertersTests.cs @@ -1,36 +1,42 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; +using System.Net; using Arcus.Converters; -using Gulliver; -using Xunit; namespace Arcus.Tests.Converters { + /// Unit tests for . public class IPAddressConvertersTests { #region ToUncompressedString + /// Gets theory data for . + /// Parameters: expected (string), input (string). + public static TheoryData ToUncompressedString_ValidInput_ReturnsUncompressed_Test_Values => + new() + { + { "192.168.001.001", "192.168.1.1" }, + { "192.168.009.001", "192.168.9.1" }, + { "124.253.063.036", "124.253.63.36" }, + { "124.253.000.000", "124.253.0.0" }, + { "000.253.063.036", "0.253.63.36" }, + { "001.253.063.036", "1.253.63.36" }, + { "001.000.063.036", "1.0.63.36" }, + { "255.255.255.255", "255.255.255.255" }, + { "000.000.000.000", "0.0.0.0" }, + { "100.010.001.000", "100.10.1.0" }, + { "0000:0000:0000:0000:0000:0000:0000:0000", "::" }, + { "0001:0002:ffff:0000:00ab:0000:0000:0123", "1:2:ffff:0:ab:0:0:123" }, + { "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff" }, + }; + + /// Verifies that ToUncompressedString returns the fully-expanded uncompressed form of a valid IP address. + /// The expected uncompressed string representation. + /// The input IP address string to parse and convert. [Theory] - [InlineData(null, null)] - [InlineData("192.168.001.001", "192.168.1.1")] - [InlineData("192.168.009.001", "192.168.9.1")] - [InlineData("124.253.063.036", "124.253.63.36")] - [InlineData("124.253.000.000", "124.253.0.0")] - [InlineData("000.253.063.036", "0.253.63.36")] - [InlineData("001.253.063.036", "1.253.63.36")] - [InlineData("001.000.063.036", "1.0.63.36")] - [InlineData("255.255.255.255", "255.255.255.255")] - [InlineData("000.000.000.000", "0.0.0.0")] - [InlineData("100.010.001.000", "100.10.1.0")] - [InlineData("0000:0000:0000:0000:0000:0000:0000:0000", "::")] - [InlineData("0001:0002:ffff:0000:00ab:0000:0000:0123", "1:2:ffff:0:ab:0:0:123")] - [InlineData("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")] - public void IPAddressToUncompressedStringConversion_Test(string expected, string input) + [MemberData(nameof(ToUncompressedString_ValidInput_ReturnsUncompressed_Test_Values))] + public void ToUncompressedString_ValidInput_ReturnsUncompressed_Test(string expected, string input) { // Arrange - _ = IPAddress.TryParse(input, out var address); + var address = IPAddress.Parse(input); // Act var result = address.ToUncompressedString(); @@ -39,23 +45,46 @@ public void IPAddressToUncompressedStringConversion_Test(string expected, string Assert.Equal(expected, result); } + /// Verifies that ToUncompressedString returns when the input address is . + [Fact] + public void ToUncompressedString_NullInput_ReturnsNull_Test() + { + // Arrange + IPAddress address = null; + + // Act + // codeql[cs/dereferenced-value-is-always-null] Extension methods are static calls in C# — invoking on null is valid and intentionally tests null-handling behavior + var result = address.ToUncompressedString(); + + // Assert + Assert.Null(result); + } + #endregion // end: ToUncompressedString #region ToBase85String + /// Gets theory data for . + /// Parameters: expected (string), input (string). + public static TheoryData ToBase85String_ValidIPv6Input_ReturnsBase85_Test_Values => + new() + { + { "$@bLmTEHhx*HIpup2~ix", "dead:beef::" }, + { "=r54lj&NUUO~Hi%c2ym0", "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff" }, + { "00000000000000000000", "::" }, + { "000000000000000-mSjx", "::dead:beef" }, + { "4)+k&C#VzJ4br>0wv%Yp", "1080:0:0:0:8:800:200C:417A" }, // specific example from RFC 1924 + }; + + /// Verifies that ToBase85String returns the correct RFC 1924 Base-85 encoding for a valid IPv6 address. + /// The expected Base-85 encoded string. + /// The input IPv6 address string to parse and convert. [Theory] - [InlineData(null, null)] - [InlineData(null, "")] - [InlineData(null, "192.168.1.1")] - [InlineData("$@bLmTEHhx*HIpup2~ix", "dead:beef::")] - [InlineData("=r54lj&NUUO~Hi%c2ym0", "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")] - [InlineData("00000000000000000000", "::")] - [InlineData("000000000000000-mSjx", "::dead:beef")] - [InlineData("4)+k&C#VzJ4br>0wv%Yp", "1080:0:0:0:8:800:200C:417A")] // specific example from RFC 1924 - public void ToBase85Test(string expected, string input) + [MemberData(nameof(ToBase85String_ValidIPv6Input_ReturnsBase85_Test_Values))] + public void ToBase85String_ValidIPv6Input_ReturnsBase85_Test(string expected, string input) { // Arrange - _ = IPAddress.TryParse(input, out var address); + var address = IPAddress.Parse(input); // Act var result = address.ToBase85String(); @@ -64,31 +93,75 @@ public void ToBase85Test(string expected, string input) Assert.Equal(expected, result); } + /// Verifies that ToBase85String returns when the input address is . + [Fact] + public void ToBase85String_NullInput_ReturnsNull_Test() + { + // Arrange + IPAddress address = null; + + // Act + // codeql[cs/dereferenced-value-is-always-null] Extension methods are static calls in C# — invoking on null is valid and intentionally tests null-handling behavior + var result = address.ToBase85String(); + + // Assert + Assert.Null(result); + } + + /// Verifies that ToBase85String returns for an IPv4 address, since Base-85 encoding applies only to IPv6. + [Fact] + public void ToBase85String_IPv4Input_ReturnsNull_Test() + { + // Arrange + var address = IPAddress.Parse("192.168.1.1"); + + // Act + var result = address.ToBase85String(); + + // Assert + Assert.Null(result); + } + #endregion // end: ToBase85String #region ToDottedQuadString + /// Gets theory data for . + /// Parameters: expected (string), input (string). + public static TheoryData ToDottedQuadString_ValidInput_ReturnsDottedQuad_Test_Values => + new() + { + { "1:2:3:a:b:ffff:255.255.255.255", "1:2:3:a:b:ffff:ffff:ffff" }, + { "ffff:ffff:ffff:ffff:ffff:ffff:1.2.3.4", "ffff:ffff:ffff:ffff:ffff:ffff:0102:0304" }, + { "0:ffff:ffff:ffff::255.255.255.255", "0:ffff:ffff:ffff:0:0:ffff:ffff" }, + { "::ffff:ffff:0:0:255.255.255.255", "0:0:ffff:ffff:0:0:ffff:ffff" }, + { "::ffff:ffff:ffff:0:255.255.255.255", "0:0:ffff:ffff:ffff:0:ffff:ffff" }, + { "ffff:ffff:ffff:ffff:ffff:ffff:255.255.255.255", "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff" }, + { "ffff:ffff:ffff:ffff:ffff:ffff:0.0.0.0", "ffff:ffff:ffff:ffff:ffff:ffff::" }, + { "ffff::0.0.0.0", "ffff::" }, + { "::ffff:0.0.0.0", "::ffff:0000:0000" }, + { "::ffff:ffff:ffff:ffff:255.255.255.255", "00::ffff:ffff:ffff:ffff:ffff:ffff" }, + { "::255.255.255.255", "::ffff:ffff" }, + { "ffff::ffff:ffff:ffff:0:0.0.0.0", "ffff:0000:ffff:ffff:ffff::" }, + { "ffff::ffff:ffff:ffff:ffff:255.255.255.255", "ffff::ffff:ffff:ffff:ffff:ffff:ffff" }, + { "::0.0.0.0", "::" }, + { "192.168.1.1", "192.168.1.1" }, + { "::0.0.0.1", "::1" }, + { "::ffff:192.168.1.1", "::ffff:192.168.1.1" }, + { "::1:0:0:1:255.255.255.255", "0:0:1:0:0:1:ffff:ffff" }, + { "fe80::0.0.0.1", "fe80::1" }, + { "1::1:1:0.0.0.0", "1:0:0:0:1:1:0:0" }, + }; + + /// Verifies that ToDottedQuadString returns the correct dotted-quad notation string for a valid IP address. + /// The expected dotted-quad string representation. + /// The input IP address string to parse and convert. [Theory] - [InlineData(null, null)] - [InlineData("1:2:3:a:b:ffff:255.255.255.255", "1:2:3:a:b:ffff:ffff:ffff")] - [InlineData("ffff:ffff:ffff:ffff:ffff:ffff:1.2.3.4", "ffff:ffff:ffff:ffff:ffff:ffff:0102:0304")] - [InlineData("0:ffff:ffff:ffff::255.255.255.255", "0:ffff:ffff:ffff:0:0:ffff:ffff")] - [InlineData("::ffff:ffff:0:0:255.255.255.255", "0:0:ffff:ffff:0:0:ffff:ffff")] - [InlineData("::ffff:ffff:ffff:0:255.255.255.255", "0:0:ffff:ffff:ffff:0:ffff:ffff")] - [InlineData("ffff:ffff:ffff:ffff:ffff:ffff:255.255.255.255", "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")] - [InlineData("ffff:ffff:ffff:ffff:ffff:ffff:0.0.0.0", "ffff:ffff:ffff:ffff:ffff:ffff::")] - [InlineData("ffff::0.0.0.0", "ffff::")] - [InlineData("::ffff:0.0.0.0", "::ffff:0000:0000")] - [InlineData("::ffff:ffff:ffff:ffff:255.255.255.255", "00::ffff:ffff:ffff:ffff:ffff:ffff")] - [InlineData("::255.255.255.255", "::ffff:ffff")] - [InlineData("ffff::ffff:ffff:ffff:0:0.0.0.0", "ffff:0000:ffff:ffff:ffff::")] - [InlineData("ffff::ffff:ffff:ffff:ffff:255.255.255.255", "ffff::ffff:ffff:ffff:ffff:ffff:ffff")] - [InlineData("::0.0.0.0", "::")] - [InlineData("192.168.1.1", "192.168.1.1")] - public void ToDottedQuadTestTest(string expected, string input) + [MemberData(nameof(ToDottedQuadString_ValidInput_ReturnsDottedQuad_Test_Values))] + public void ToDottedQuadString_ValidInput_ReturnsDottedQuad_Test(string expected, string input) { // Arrange - _ = IPAddress.TryParse(input, out var address); + var address = IPAddress.Parse(input); // Act var result = address.ToDottedQuadString(); @@ -97,26 +170,51 @@ public void ToDottedQuadTestTest(string expected, string input) Assert.Equal(expected, result); } + /// Verifies that ToDottedQuadString returns when the input address is . + [Fact] + public void ToDottedQuadString_NullInput_ReturnsNull_Test() + { + // Arrange + IPAddress address = null; + + // Act + // codeql[cs/dereferenced-value-is-always-null] Extension methods are static calls in C# — invoking on null is valid and intentionally tests null-handling behavior + var result = address.ToDottedQuadString(); + + // Assert + Assert.Null(result); + } + #endregion // end: ToDottedQuadString #region ToHexString + /// Gets theory data for . + /// Parameters: expected (string), input (string). + public static TheoryData ToHexString_ValidInput_ReturnsHex_Test_Values => + new() + { + { "00000000000000000000000000000000", "::" }, + { "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff" }, + { "0000000000000000000000000000ABCD", "::abcd" }, + { "ABCD000000000000000000000000ABCD", "abcd::abcd" }, + { "ABCD0000000000000000000000000000", "abcd::" }, + { "00000000", "0.0.0.0" }, + { "FFFFFFFF", "255.255.255.255" }, + { "80808080", "128.128.128.128" }, + { "00808080", "0.128.128.128" }, + { "80808000", "128.128.128.0" }, + }; + + /// Verifies that ToHexString returns the uppercase hexadecimal string representation of a valid IP address. + /// The expected hexadecimal string. + /// The input IP address string to parse and convert. [Theory] - [InlineData(null, null)] - [InlineData("00000000000000000000000000000000", "::")] - [InlineData("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")] - [InlineData("0000000000000000000000000000ABCD", "::abcd")] - [InlineData("ABCD000000000000000000000000ABCD", "abcd::abcd")] - [InlineData("ABCD0000000000000000000000000000", "abcd::")] - [InlineData("00000000", "0.0.0.0")] - [InlineData("FFFFFFFF", "255.255.255.255")] - [InlineData("80808080", "128.128.128.128")] - [InlineData("00808080", "0.128.128.128")] - [InlineData("80808000", "128.128.128.0")] - public void ToHexTest(string expected, string input) + [MemberData(nameof(ToHexString_ValidInput_ReturnsHex_Test_Values))] + public void ToHexString_ValidInput_ReturnsHex_Test(string expected, string input) { // Arrange - _ = IPAddress.TryParse(input, out var address); + var address = IPAddress.Parse(input); // Act var result = address.ToHexString(); @@ -125,26 +223,51 @@ public void ToHexTest(string expected, string input) Assert.Equal(expected, result); } + /// Verifies that ToHexString returns when the input address is . + [Fact] + public void ToHexString_NullInput_ReturnsNull_Test() + { + // Arrange + IPAddress address = null; + + // Act + // codeql[cs/dereferenced-value-is-always-null] Extension methods are static calls in C# — invoking on null is valid and intentionally tests null-handling behavior + var result = address.ToHexString(); + + // Assert + Assert.Null(result); + } + #endregion // end: ToHexString - #region + #region ToNumericString + /// Gets theory data for . + /// Parameters: expected (string), input (string). + public static TheoryData ToNumericString_ValidInput_ReturnsNumeric_Test_Values => + new() + { + { "0", "::" }, + { "340282366920938463463374607431768211455", "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff" }, + { "43981", "::abcd" }, + { "228362408135220253930399759055429086157", "abcd::abcd" }, + { "228362408135220253930399759055429042176", "abcd::" }, + { "0", "0.0.0.0" }, + { "4294967295", "255.255.255.255" }, + { "2155905152", "128.128.128.128" }, + { "8421504", "0.128.128.128" }, + { "2155905024", "128.128.128.0" }, + }; + + /// Verifies that ToNumericString returns the decimal numeric string representation of a valid IP address. + /// The expected decimal numeric string. + /// The input IP address string to parse and convert. [Theory] - [InlineData(null, null)] - [InlineData("0", "::")] - [InlineData("340282366920938463463374607431768211455", "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")] - [InlineData("43981", "::abcd")] - [InlineData("228362408135220253930399759055429086157", "abcd::abcd")] - [InlineData("228362408135220253930399759055429042176", "abcd::")] - [InlineData("0", "0.0.0.0")] - [InlineData("4294967295", "255.255.255.255")] - [InlineData("2155905152", "128.128.128.128")] - [InlineData("8421504", "0.128.128.128")] - [InlineData("2155905024", "128.128.128.0")] - public void ToNumericTest(string expected, string input) + [MemberData(nameof(ToNumericString_ValidInput_ReturnsNumeric_Test_Values))] + public void ToNumericString_ValidInput_ReturnsNumeric_Test(string expected, string input) { // Arrange - _ = IPAddress.TryParse(input, out var address); + var address = IPAddress.Parse(input); // Act var result = address.ToNumericString(); @@ -153,27 +276,53 @@ public void ToNumericTest(string expected, string input) Assert.Equal(expected, result); } - #endregion // end: + /// Verifies that ToNumericString returns when the input address is . + [Fact] + public void ToNumericString_NullInput_ReturnsNull_Test() + { + // Arrange + IPAddress address = null; + + // Act + // codeql[cs/dereferenced-value-is-always-null] Extension methods are static calls in C# — invoking on null is valid and intentionally tests null-handling behavior + var result = address.ToNumericString(); + + // Assert + Assert.Null(result); + } + + #endregion // end: ToNumericString #region NetmaskToCidrRoutePrefix - public static IEnumerable NetmaskToCidrRoutePrefix_Test_Values() + /// Gets theory data for . + /// Parameters: expected (int), address (IPAddress). + public static TheoryData NetmaskToCidrRoutePrefix_ValidNetmask_ReturnsRoutePrefix_Test_Values { - for (var i = 0; i <= 32; i++) + get { - var netmaskBytes = Enumerable.Repeat((byte)0xFF, 4).ToArray().ShiftBitsLeft(32 - i); + var data = new TheoryData(); - var netmask = new IPAddress(netmaskBytes); + for (var i = 0; i <= 32; i++) + { + var netmaskBytes = BigEndianBitWrapper.CreateMask(4, i).ToBytes(); + data.Add(i, new IPAddress(netmaskBytes)); + } - yield return new object[] { i, netmask }; + return data; } } + /// Verifies that NetmaskToCidrRoutePrefix returns the correct CIDR prefix length for a valid IPv4 netmask. + /// The expected CIDR route prefix integer. + /// The netmask IP address to convert. [Theory] - [MemberData(nameof(NetmaskToCidrRoutePrefix_Test_Values))] - public void NetmaskToCidrRoutePrefix_Test(int expected, IPAddress address) + [MemberData(nameof(NetmaskToCidrRoutePrefix_ValidNetmask_ReturnsRoutePrefix_Test_Values))] + public void NetmaskToCidrRoutePrefix_ValidNetmask_ReturnsRoutePrefix_Test(int expected, IPAddress address) { // Arrange + // (address provided via theory data) + // Act var result = address.NetmaskToCidrRoutePrefix(); @@ -181,28 +330,39 @@ public void NetmaskToCidrRoutePrefix_Test(int expected, IPAddress address) Assert.Equal(expected, result); } + /// Gets theory data for . + /// Parameters: input (string). + public static TheoryData NetmaskToCidrRoutePrefix_InvalidNetmask_ThrowsArgumentException_Test_Values => + new() + { + { "::" }, + { "192.168.1.1" }, + { "0.0.0.255" }, + { "255.0.255.0" }, // non-contiguous mask + }; + + /// Verifies that NetmaskToCidrRoutePrefix throws for an address that is not a valid contiguous netmask. + /// The invalid netmask IP address string to parse. [Theory] - [InlineData("::")] - [InlineData("192.168.1.1")] - [InlineData("0.0.0.255")] - public void NetmaskToCidrRoutePrefix_InvalidInput_Throws_InvalidOperationException_Test(string input) + [MemberData(nameof(NetmaskToCidrRoutePrefix_InvalidNetmask_ThrowsArgumentException_Test_Values))] + public void NetmaskToCidrRoutePrefix_InvalidNetmask_ThrowsArgumentException_Test(string input) { // Arrange var address = IPAddress.Parse(input); - // Act - // Assert - Assert.Throws(() => address.NetmaskToCidrRoutePrefix()); + // Act / Assert + Assert.Throws(() => address.NetmaskToCidrRoutePrefix()); } + /// Verifies that NetmaskToCidrRoutePrefix throws when the input address is . [Fact] - public void NetmaskToCidrRoutePrefix_NullInput_Throws_ArgumentNullException_Test() + public void NetmaskToCidrRoutePrefix_NullInput_ThrowsArgumentNullException_Test() { // Arrange - // Act - // Assert + IPAddress address = null; - Assert.Throws(() => ((IPAddress)null).NetmaskToCidrRoutePrefix()); + // Act / Assert + Assert.Throws(() => address.NetmaskToCidrRoutePrefix()); } #endregion // end: NetmaskToCidrRoutePrefix diff --git a/src/Arcus.Tests/IIPAddressRangeTests.cs b/src/Arcus.Tests/IIPAddressRangeTests.cs index 83ab12e..a0a9f72 100644 --- a/src/Arcus.Tests/IIPAddressRangeTests.cs +++ b/src/Arcus.Tests/IIPAddressRangeTests.cs @@ -1,12 +1,11 @@ -using System; -using System.Collections.Generic; -using System.Net; -using Xunit; +using System.Net; namespace Arcus.Tests { + /// Unit tests for . public class IIPAddressRangeTests { + /// Verifies IIPAddressRange extends IFormattable and IEnumerable<IPAddress>. [Fact] public void Assignability_Test() { @@ -19,6 +18,7 @@ public void Assignability_Test() Assert.True(typeof(IEnumerable).IsAssignableFrom(type)); } + /// Verifies IIPAddressRange is declared as an interface. [Fact] public void IsInterface_Test() { diff --git a/src/Arcus.Tests/IPAddressCompatibilityTests.cs b/src/Arcus.Tests/IPAddressCompatibilityTests.cs index 030e948..b94dfad 100644 --- a/src/Arcus.Tests/IPAddressCompatibilityTests.cs +++ b/src/Arcus.Tests/IPAddressCompatibilityTests.cs @@ -1,11 +1,14 @@ using System.Net; -using Xunit; namespace Arcus.Tests { - // Trusting, but verifying the compatibility of IPAddress object with Microsoft System.Net.IPAddress object + /// Verifies compatibility of parsing behavior with System.Net.IPAddress across address formats and edge cases. public class IPAddressCompatibilityTests { + /// Verifies IPAddress.TryParse returns the expected success/failure and parsed string for a wide range of IPv4, IPv6, and invalid inputs. + /// Whether TryParse is expected to succeed. + /// The input string to parse. + /// The expected ToString() of the parsed address when parsing succeeds. [Theory] // IPv4 Addresses diff --git a/src/Arcus.Tests/IPAddressRangeTests.Factory.cs b/src/Arcus.Tests/IPAddressRangeTests.Factory.cs new file mode 100644 index 0000000..5240094 --- /dev/null +++ b/src/Arcus.Tests/IPAddressRangeTests.Factory.cs @@ -0,0 +1,571 @@ +using System.Net; + +namespace Arcus.Tests +{ + /// + /// tests for static factory methods + /// + public partial class IPAddressRangeTests + { + #region TryMerge + + /// Verifies TryMerge produces the expected merged range (or null) for touching, overlapping, non-adjacent, and null range pairs. + /// The expected Head-Tail string of the merged range when the ranges merge; null when they do not. + /// The head address of the first range. + /// The tail address of the first range. + /// The head address of the second range. + /// The tail address of the second range. + [Theory] + [InlineData(null, null, null, null, null)] + [InlineData(null, "192.168.1.1", "192.168.1.9", "::", "::")] + [InlineData(null, "192.168.1.1", "192.168.1.9", "192.168.1.11", "192.168.1.20")] + [InlineData(null, "192.168.1.1", "192.168.1.9", null, null)] + [InlineData(null, null, null, "192.168.1.1", "192.168.1.9")] + [InlineData("192.168.1.1-192.168.1.20", "192.168.1.1", "192.168.1.10", "192.168.1.11", "192.168.1.20")] + [InlineData("192.168.1.1-192.168.1.20", "192.168.1.1", "192.168.1.10", "192.168.1.10", "192.168.1.20")] + [InlineData("192.168.1.1-192.168.1.20", "192.168.1.11", "192.168.1.20", "192.168.1.1", "192.168.1.10")] + [InlineData("192.168.1.1-192.168.1.20", "192.168.1.10", "192.168.1.20", "192.168.1.1", "192.168.1.10")] + [InlineData("192.168.1.10-192.168.1.20", "192.168.1.10", "192.168.1.20", "192.168.1.10", "192.168.1.20")] + [InlineData("::-::", "::", "::", "::", "::")] + [InlineData("::1-::4", "::1", "::2", "::3", "::4")] + [InlineData(null, null, null, "::", "::")] + [InlineData(null, "::", "::", null, null)] + public void TryMergeResultTest(string expected, string alphaHead, string alphaTail, string betaHead, string betaTail) + { + // Arrange + var alphaAddressRange = + IPAddress.TryParse(alphaHead, out var alphaHeadAddress) + && IPAddress.TryParse(alphaTail, out var alphaTailAddress) + ? new IPAddressRange(alphaHeadAddress, alphaTailAddress) + : null; + + var betaAddressRange = + IPAddress.TryParse(betaHead, out var betaHeadAddress) && IPAddress.TryParse(betaTail, out var betaTailAddress) + ? new IPAddressRange(betaHeadAddress, betaTailAddress) + : null; + + // Act + var successResult = IPAddressRange.TryMerge(alphaAddressRange, betaAddressRange, out var mergeResult); + + // Assert + Assert.Equal(expected != null, successResult); + + if (expected == null) + { + Assert.Null(mergeResult); + } + else + { + Assert.NotNull(mergeResult); + Assert.Equal(expected, $"{mergeResult.Head}-{mergeResult.Tail}"); + } + } + + #endregion + + #region TryCollapseAll + + /// Verifies TryCollapseAll merges three consecutive non-overlapping ranges into a single contiguous range. + [Fact] + public void TryCollapseAll_Consecutive_Test() + { + // Arrange + var ranges = new[] + { + new IPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.5")), + new IPAddressRange(IPAddress.Parse("192.168.1.6"), IPAddress.Parse("192.168.1.7")), + new IPAddressRange(IPAddress.Parse("192.168.1.8"), IPAddress.Parse("192.168.1.20")), + }; + + // Act + var success = IPAddressRange.TryCollapseAll(ranges, out var results); + + // Assert + Assert.True(success); + Assert.NotNull(results); + var collection = results.ToList(); + Assert.Single(collection); + + var result = collection.Single(); + Assert.Equal(IPAddress.Parse("192.168.1.0"), result.Head); + Assert.Equal(IPAddress.Parse("192.168.1.20"), result.Tail); + } + + /// Verifies TryCollapseAll returns false and an empty result when ranges are from different address families. + [Fact] + public void TryCollapse_MismatchedAddressFamilies_Test() + { + // Arrange + var ranges = new[] + { + new IPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.5")), + new IPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("abcd::ef00")), + }; + + // Act + var success = IPAddressRange.TryCollapseAll(ranges, out var results); + + // Assert + Assert.False(success); + Assert.NotNull(results); + Assert.False(results.Any()); + } + + /// Verifies TryCollapseAll succeeds with an empty input and returns an empty result set. + [Fact] + public void TryCollapseAll_EmptyInput_Test() + { + // Act + var success = IPAddressRange.TryCollapseAll([], out var results); + + // Assert + Assert.True(success); + Assert.NotNull(results); + Assert.False(results.Any()); + } + + /// Verifies TryCollapseAll returns false when the input contains a null element. + [Fact] + public void TryCollapse_AllInvalidInput_Test() + { + // Arrange + var ranges = new[] + { + new IPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.5")), + null, + new IPAddressRange(IPAddress.Parse("192.168.1.30"), IPAddress.Parse("192.168.1.35")), + }; + + // Act + var success = IPAddressRange.TryCollapseAll(ranges, out var results); + + // Assert + Assert.False(success); + Assert.NotNull(results); + Assert.False(results.Any()); + } + + /// Verifies TryCollapseAll merges three overlapping ranges into a single contiguous range. + [Fact] + public void TryCollapse_AllOverlap_Test() + { + // Arrange + var ranges = new[] + { + new IPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.5")), + new IPAddressRange(IPAddress.Parse("192.168.1.5"), IPAddress.Parse("192.168.1.10")), + new IPAddressRange(IPAddress.Parse("192.168.1.8"), IPAddress.Parse("192.168.1.20")), + }; + + // Act + var success = IPAddressRange.TryCollapseAll(ranges, out var results); + + // Assert + Assert.True(success); + Assert.NotNull(results); + var collection = results.ToList(); + Assert.Single(collection); + + var result = collection.Single(); + Assert.Equal(IPAddress.Parse("192.168.1.0"), result.Head); + Assert.Equal(IPAddress.Parse("192.168.1.20"), result.Tail); + } + + /// Verifies TryCollapseAll produces one range when one input wholly contains all others. + [Fact] + public void TryCollapseAll_SubsetContainsAll_Test() + { + // Arrange + var ranges = new[] + { + new IPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.5")), + new IPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.20")), + new IPAddressRange(IPAddress.Parse("192.168.1.8"), IPAddress.Parse("192.168.1.20")), + }; + + // Act + var success = IPAddressRange.TryCollapseAll(ranges, out var results); + + // Assert + Assert.True(success); + Assert.NotNull(results); + var collection = results.ToList(); + Assert.Single(collection); + + var result = collection.Single(); + Assert.Equal(IPAddress.Parse("192.168.1.0"), result.Head); + Assert.Equal(IPAddress.Parse("192.168.1.20"), result.Tail); + } + + /// Verifies TryCollapseAll preserves three ranges that have gaps between them. + [Fact] + public void TryCollapseAll_WithGaps_Test() + { + // Arrange + var ranges = new[] + { + new IPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.5")), + new IPAddressRange(IPAddress.Parse("192.168.1.7"), IPAddress.Parse("192.168.1.20")), + new IPAddressRange(IPAddress.Parse("192.168.1.30"), IPAddress.Parse("192.168.1.35")), + }; + + // Act + var success = IPAddressRange.TryCollapseAll(ranges, out var results); + + // Assert + Assert.True(success); + Assert.NotNull(results); + var enumerable = results.ToList(); + Assert.Equal(3, enumerable.Count); + Assert.Equal(enumerable, [.. ranges]); + } + + /// Verifies TryCollapseAll treats a null ranges collection as an empty input, returning true with an empty result. + [Fact] + public void TryCollapseAll_WithNullInput_ReturnsTrue_WithEmptyResult() + { + // Act + var success = IPAddressRange.TryCollapseAll(null!, out var results); + + // Assert + Assert.True(success); + Assert.NotNull(results); + Assert.False(results.Any()); + } + + #endregion // end: TryCollapseAll + + #region TryExcludeAll + + /// Verifies TryExcludeAll carves three non-overlapping exclusions from the middle of the initial range, leaving two segments. + [Fact] + public void TryExcludeAll_Carve_Test() + { + // Arrange + var initialRange = new IPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.200")); + + var ranges = new[] + { + new IPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.10")), + new IPAddressRange(IPAddress.Parse("192.168.1.50"), IPAddress.Parse("192.168.1.100")), + new IPAddressRange(IPAddress.Parse("192.168.1.150"), IPAddress.Parse("192.168.1.200")), + }; + + // Act + var success = IPAddressRange.TryExcludeAll(initialRange, ranges, out var results); + + // Assert + Assert.True(success); + Assert.NotNull(results); + var enumerable = results.ToList(); + Assert.Equal(2, enumerable.Count); + + Assert.Equal( + enumerable, + [ + new IPAddressRange(IPAddress.Parse("192.168.1.11"), IPAddress.Parse("192.168.1.49")), + new IPAddressRange(IPAddress.Parse("192.168.1.101"), IPAddress.Parse("192.168.1.149")), + ] + ); + } + + /// Verifies TryExcludeAll with consecutive exclusions leaves exactly the first and last addresses. + [Fact] + public void TryExcludeAll_ConsecutiveCarve_Test() + { + // Arrange + var initialRange = new IPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.200")); + + var ranges = new[] + { + new IPAddressRange(IPAddress.Parse("192.168.1.1"), IPAddress.Parse("192.168.1.100")), + new IPAddressRange(IPAddress.Parse("192.168.1.101"), IPAddress.Parse("192.168.1.199")), + }; + + // Act + var success = IPAddressRange.TryExcludeAll(initialRange, ranges, out var results); + + // Assert + Assert.True(success); + Assert.NotNull(results); + var enumerable = results.ToList(); + Assert.Equal(2, enumerable.Count); + + Assert.Equal( + enumerable, + [ + new IPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.0")), + new IPAddressRange(IPAddress.Parse("192.168.1.200"), IPAddress.Parse("192.168.1.200")), + ] + ); + } + + /// Verifies TryExcludeAll with a head exclusion returns only the trailing segment. + [Fact] + public void TryExcludeAll_Head_Test() + { + // Arrange + var initialRange = new IPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.100")); + + var ranges = new[] { new IPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.50")) }; + + // Act + var success = IPAddressRange.TryExcludeAll(initialRange, ranges, out var results); + + // Assert + Assert.True(success); + Assert.NotNull(results); + var collection = results.ToList(); + Assert.Single(collection); + + var result = collection.Single(); + + Assert.Equal(IPAddress.Parse("192.168.1.51"), result.Head); + Assert.Equal(IPAddress.Parse("192.168.1.100"), result.Tail); + } + + /// Verifies TryExcludeAll with overlapping exclusions that cover the entire range produces an empty result. + [Fact] + public void TryExcludeAll_Overlap_Test() + { + // Arrange + var initialRange = new IPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.100")); + + var ranges = new[] + { + new IPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.49")), + new IPAddressRange(IPAddress.Parse("192.168.1.50"), IPAddress.Parse("192.168.1.75")), + new IPAddressRange(IPAddress.Parse("192.168.1.75"), IPAddress.Parse("192.168.1.100")), + }; + + // Act + var success = IPAddressRange.TryExcludeAll(initialRange, ranges, out var results); + + //Assert + Assert.True(success); + Assert.Empty(results); + } + + /// Verifies TryExcludeAll with a tail exclusion returns only the leading segment. + [Fact] + public void TryExcludeAll_Tail_Test() + { + // Arrange + var initialRange = new IPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.100")); + + var ranges = new[] { new IPAddressRange(IPAddress.Parse("192.168.1.50"), IPAddress.Parse("192.168.1.100")) }; + + // Act + var success = IPAddressRange.TryExcludeAll(initialRange, ranges, out var results); + + // Assert + Assert.True(success); + Assert.NotNull(results); + var collection = results.ToList(); + Assert.Single(collection); + + var result = collection.Single(); + + Assert.Equal(IPAddress.Parse("192.168.1.0"), result.Head); + Assert.Equal(IPAddress.Parse("192.168.1.49"), result.Tail); + } + + /// Verifies TryExcludeAll with an empty exclusion list returns the original range unchanged. + [Fact] + public void TryExcludeAll_NoExclusions_Test() + { + // Arrange + var initialRange = new IPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.200")); + + // Act + var success = IPAddressRange.TryExcludeAll(initialRange, [], out var results); + + // Assert + Assert.True(success); + Assert.NotNull(results); + var collection = results.ToList(); + Assert.Single(collection); + + Assert.Equal(initialRange, collection.Single()); + } + + /// Verifies TryExcludeAll returns false when the initial range and exclusion ranges are from different address families. + [Fact] + public void TryExcludeAll_InitialMissMatchedAddressFamily_Test() + { + // Arrange + var initialRange = new IPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("ffff::ffff")); + + var ranges = new[] { new IPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.10")) }; + + // Act + var success = IPAddressRange.TryExcludeAll(initialRange, ranges, out var results); + + // Assert + Assert.False(success); + Assert.Empty(results); + } + + /// Verifies TryExcludeAll retains only the leading segment when an exclusion ends at the IPv4 maximum address. + [Fact] + public void TryExcludeAll_ExclusionTailAtIPv4Max_WithLeadingSegment_Test() + { + // Exclusion ends at 255.255.255.255; only the leading segment before the exclusion is retained. + var initialRange = new IPAddressRange(IPAddress.Parse("10.0.0.0"), IPAddress.Parse("200.0.0.0")); + var exclusion = new IPAddressRange(IPAddress.Parse("100.0.0.0"), IPAddress.Parse("255.255.255.255")); + + var success = IPAddressRange.TryExcludeAll(initialRange, [exclusion], out var results); + + Assert.True(success); + var list = results.ToList(); + Assert.Single(list); + Assert.Equal(IPAddress.Parse("10.0.0.0"), list[0].Head); + Assert.Equal(IPAddress.Parse("99.255.255.255"), list[0].Tail); + } + + /// Verifies TryExcludeAll produces an empty result when an exclusion spanning the entire address space covers the initial range. + [Fact] + public void TryExcludeAll_ExclusionTailAtIPv4Max_CoversAll_Test() + { + // Exclusion spans the entire address space; nothing remains. + var initialRange = new IPAddressRange(IPAddress.Parse("10.0.0.0"), IPAddress.Parse("200.0.0.0")); + var exclusion = new IPAddressRange(IPAddress.Parse("0.0.0.0"), IPAddress.Parse("255.255.255.255")); + + var success = IPAddressRange.TryExcludeAll(initialRange, [exclusion], out var results); + + Assert.True(success); + Assert.Empty(results); + } + + /// Verifies TryExcludeAll retains only the trailing segment when an exclusion starts at the IPv4 minimum address. + [Fact] + public void TryExcludeAll_ExclusionHeadAtIPv4Min_WithTrailingSegment_Test() + { + // Exclusion starts at 0.0.0.0; only the trailing segment after the exclusion is retained. + var initialRange = new IPAddressRange(IPAddress.Parse("10.0.0.0"), IPAddress.Parse("200.0.0.0")); + var exclusion = new IPAddressRange(IPAddress.Parse("0.0.0.0"), IPAddress.Parse("50.0.0.0")); + + var success = IPAddressRange.TryExcludeAll(initialRange, [exclusion], out var results); + + Assert.True(success); + var list = results.ToList(); + Assert.Single(list); + Assert.Equal(IPAddress.Parse("50.0.0.1"), list[0].Head); + Assert.Equal(IPAddress.Parse("200.0.0.0"), list[0].Tail); + } + + /// Verifies TryExcludeAll retains only the leading segment when an IPv6 exclusion ends at the family maximum. + [Fact] + public void TryExcludeAll_ExclusionTailAtIPv6Max_WithLeadingSegment_Test() + { + // IPv6: exclusion ends at the family maximum; only the leading segment is retained. + var initialRange = new IPAddressRange(IPAddress.Parse("::1"), IPAddress.Parse("f000::")); + var exclusion = new IPAddressRange( + IPAddress.Parse("8000::"), + IPAddress.Parse("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff") + ); + + var success = IPAddressRange.TryExcludeAll(initialRange, [exclusion], out var results); + + Assert.True(success); + var list = results.ToList(); + Assert.Single(list); + Assert.Equal(IPAddress.Parse("::1"), list[0].Head); + Assert.Equal(IPAddress.Parse("7fff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"), list[0].Tail); + } + + /// Verifies TryExcludeAll retains only the trailing segment when an IPv6 exclusion starts at the family minimum. + [Fact] + public void TryExcludeAll_ExclusionHeadAtIPv6Min_WithTrailingSegment_Test() + { + // IPv6: exclusion starts at the family minimum; only the trailing segment is retained. + var initialRange = new IPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("f000::")); + var exclusion = new IPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("8000::")); + + var success = IPAddressRange.TryExcludeAll(initialRange, [exclusion], out var results); + + Assert.True(success); + var list = results.ToList(); + Assert.Single(list); + Assert.Equal(IPAddress.Parse("8000::1"), list[0].Head); + Assert.Equal(IPAddress.Parse("f000::"), list[0].Tail); + } + + /// Verifies TryExcludeAll returns false (rather than throwing) when an exclusion does not overlap the initial range. + [Fact] + public void TryExcludeAll_WithNonOverlappingExclusion_ReturnsFalse() + { + // Arrange + var initialRange = new IPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.255")); + var exclusion = new IPAddressRange(IPAddress.Parse("10.0.0.0"), IPAddress.Parse("10.255.255.255")); + + // Act + var success = IPAddressRange.TryExcludeAll(initialRange, [exclusion], out var results); + + // Assert + Assert.False(success); + Assert.NotNull(results); + Assert.False(results.Any()); + } + + /// Verifies TryExcludeAll with a non-overlapping exclusion does not throw. + [Fact] + public void TryExcludeAll_WithNonOverlappingExclusion_DoesNotThrow() + { + // Arrange + var initialRange = new IPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.255")); + var exclusion = new IPAddressRange(IPAddress.Parse("10.0.0.0"), IPAddress.Parse("10.255.255.255")); + + // Act + var exception = Record.Exception(() => IPAddressRange.TryExcludeAll(initialRange, [exclusion], out _)); + + // Assert + Assert.Null(exception); + } + + #endregion // end: TryExcludeAll + + #region maxEnumerationExponent propagation + + /// Verifies that propagates maxEnumerationExponent. + [Fact] + public void TryMerge_ExponentPropagates_Test() + { + var left = new IPAddressRange(IPAddress.Parse("10.0.0.0"), IPAddress.Parse("10.0.0.99")); + var right = new IPAddressRange(IPAddress.Parse("10.0.0.100"), IPAddress.Parse("10.0.0.255")); + IPAddressRange.TryMerge(left, right, out var merged, maxEnumerationExponent: 4); + Assert.Equal(4, merged.MaxEnumerationExponent); + } + + /// Verifies that propagates maxEnumerationExponent. + [Fact] + public void TryCollapseAll_ExponentPropagates_Test() + { + var ranges = new[] + { + new IPAddressRange(IPAddress.Parse("10.0.0.0"), IPAddress.Parse("10.0.0.99")), + new IPAddressRange(IPAddress.Parse("10.0.0.100"), IPAddress.Parse("10.0.0.255")), + }; + IPAddressRange.TryCollapseAll(ranges, out var results, maxEnumerationExponent: 4); + foreach (var r in results) + { + Assert.Equal(4, r.MaxEnumerationExponent); + } + } + + /// Verifies that propagates maxEnumerationExponent. + [Fact] + public void TryExcludeAll_ExponentPropagates_Test() + { + var initial = new IPAddressRange(IPAddress.Parse("10.0.0.0"), IPAddress.Parse("10.0.0.255")); + var exclusion = new IPAddressRange(IPAddress.Parse("10.0.0.100"), IPAddress.Parse("10.0.0.199")); + IPAddressRange.TryExcludeAll(initial, [exclusion], out var results, maxEnumerationExponent: 4); + foreach (var r in results) + { + Assert.Equal(4, r.MaxEnumerationExponent); + } + } + + #endregion // end: maxEnumerationExponent propagation + } +} diff --git a/src/Arcus.Tests/IPAddressRangeTests.IComparable.cs b/src/Arcus.Tests/IPAddressRangeTests.IComparable.cs new file mode 100644 index 0000000..3cf7357 --- /dev/null +++ b/src/Arcus.Tests/IPAddressRangeTests.IComparable.cs @@ -0,0 +1,152 @@ +using System.Net; + +namespace Arcus.Tests +{ + /// + /// tests for + /// + public partial class IPAddressRangeTests + { + #region CompareTo / Operators + + /// + /// Gets test data for and operator tests. + /// Covers equal ranges, null right operands, cross-family ordering, and length-based ordering within a family. + /// Parameters: expected comparison result (int), left (IPAddressRange), right (IPAddressRange). + /// + /// + /// Test data for and operator tests. + /// Covers equal ranges, null right operands, cross-family ordering, and length-based ordering within a family. + /// Parameters: expected comparison result (int), left (IPAddressRange), right (IPAddressRange). + /// + public static TheoryData Comparison_Values + { + get + { + var ipv4Slash16 = new IPAddressRange(IPAddress.Parse("192.168.0.0"), IPAddress.Parse("192.168.255.255")); + var ipv6Slash64 = new IPAddressRange(IPAddress.Parse("ab:cd::"), IPAddress.Parse("ab:cd::ffff:ffff:ffff:ffff")); + var ipv4Slash20 = new IPAddressRange(IPAddress.Parse("192.168.0.0"), IPAddress.Parse("192.168.15.255")); + var ipv6Slash96 = new IPAddressRange(IPAddress.Parse("ab:cd::"), IPAddress.Parse("ab:cd::ffff:ffff")); + var ipv4All = new IPAddressRange(IPAddress.Parse("0.0.0.0"), IPAddress.Parse("255.255.255.255")); + var ipv6All = new IPAddressRange( + IPAddress.Parse("::"), + IPAddress.Parse("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff") + ); + var ipv4Single = new IPAddressRange(IPAddress.Parse("0.0.0.0"), IPAddress.Parse("0.0.0.0")); + var ipv6Single = new IPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("::")); + + var data = new TheoryData + { + { + 0, + new IPAddressRange(ipv4Slash16.Head, ipv4Slash16.Tail), + new IPAddressRange(ipv4Slash16.Head, ipv4Slash16.Tail) + }, + { + 0, + new IPAddressRange(ipv6Slash64.Head, ipv6Slash64.Tail), + new IPAddressRange(ipv6Slash64.Head, ipv6Slash64.Tail) + }, + { 1, new IPAddressRange(ipv4Slash16.Head, ipv4Slash16.Tail), null }, + { 1, new IPAddressRange(ipv6Slash64.Head, ipv6Slash64.Tail), null }, + { + 1, + new IPAddressRange(ipv4Slash16.Head, ipv4Slash16.Tail), + new IPAddressRange(ipv4Slash20.Head, ipv4Slash20.Tail) + }, + { + -1, + new IPAddressRange(ipv4Slash20.Head, ipv4Slash20.Tail), + new IPAddressRange(ipv4Slash16.Head, ipv4Slash16.Tail) + }, + { + 1, + new IPAddressRange(ipv6Slash64.Head, ipv6Slash64.Tail), + new IPAddressRange(ipv6Slash96.Head, ipv6Slash96.Tail) + }, + { + -1, + new IPAddressRange(ipv6Slash96.Head, ipv6Slash96.Tail), + new IPAddressRange(ipv6Slash64.Head, ipv6Slash64.Tail) + }, + { -1, new IPAddressRange(ipv4All.Head, ipv4All.Tail), new IPAddressRange(ipv6All.Head, ipv6All.Tail) }, + { 1, new IPAddressRange(ipv6All.Head, ipv6All.Tail), new IPAddressRange(ipv4All.Head, ipv4All.Tail) }, + { + -1, + new IPAddressRange(ipv4Single.Head, ipv4Single.Tail), + new IPAddressRange(ipv6Single.Head, ipv6Single.Tail) + }, + { + 1, + new IPAddressRange(ipv6Single.Head, ipv6Single.Tail), + new IPAddressRange(ipv4Single.Head, ipv4Single.Tail) + }, + }; + + return data; + } + } + + /// Verifies CompareTo returns the expected result for all ordered range pairs. + /// Expected comparison result. + /// The left operand range. + /// The right operand range. + [Theory] + [MemberData(nameof(Comparison_Values))] + public void CompareTo_Test(int expected, IPAddressRange left, IPAddressRange right) + { + // Arrange + // Act + var result = left.CompareTo(right); + + // Assert + Assert.Equal(expected, result); + } + + #endregion // end: CompareTo / Operators + + #region CompareTo(object) + + /// Verifies that throws when the argument is not an . + [Fact] + public void CompareTo_Object_NonRange_Throws_ArgumentException_Test() + { + // Arrange + var range = new IPAddressRange(IPAddress.Parse("192.168.0.0"), IPAddress.Parse("192.168.255.255")); + + // Act / Assert + Assert.Throws(() => range.CompareTo("not a range")); + } + + /// Verifies that returns a positive value when compared to null. + [Fact] + public void CompareTo_Object_Null_ReturnsPositive_Test() + { + // Arrange + var range = new IPAddressRange(IPAddress.Parse("192.168.0.0"), IPAddress.Parse("192.168.255.255")); + + // Act + var result = range.CompareTo((object)null); + + // Assert + Assert.Equal(1, result); + } + + /// Verifies that returns zero when compared to a boxed equivalent range. + [Fact] + public void CompareTo_Object_RangeBox_ReturnsZero_Test() + { + // Arrange + var range = new IPAddressRange(IPAddress.Parse("192.168.0.0"), IPAddress.Parse("192.168.255.255")); + object boxed = new IPAddressRange(IPAddress.Parse("192.168.0.0"), IPAddress.Parse("192.168.255.255")); + + // Act + var result = range.CompareTo(boxed); + + // Assert + Assert.Equal(0, result); + } + + #endregion // end: CompareTo(object) + } +} diff --git a/src/Arcus.Tests/IPAddressRangeTests.IEquatable.cs b/src/Arcus.Tests/IPAddressRangeTests.IEquatable.cs new file mode 100644 index 0000000..d015514 --- /dev/null +++ b/src/Arcus.Tests/IPAddressRangeTests.IEquatable.cs @@ -0,0 +1,184 @@ +using System.Net; + +namespace Arcus.Tests +{ + /// + /// tests for + /// + public partial class IPAddressRangeTests + { + #region Equals + + #region Equals(IPAddressRange) + + /// Verifies that returns for self-comparison via the fast-path. + [Fact] + public void Equals_ReferenceEqualsSelf_ReturnsTrue_Test() + { + // Arrange + var range = new IPAddressRange(IPAddress.Parse("192.168.1.1"), IPAddress.Parse("192.168.1.10")); + + // Act + var result = range.Equals(range); + + // Assert + Assert.True(result); + } + + /// Verifies Equals(IPAddressRange) returns the expected result for equal, null, and differing ranges. + /// Expected result. + /// The head address of the left range. + /// The tail address of the left range. + /// The head address of the right range. + /// The tail address of the right range. + [Theory] + [InlineData(true, "192.168.1.1", "192.168.1.10", "192.168.1.1", "192.168.1.10")] + [InlineData(false, "192.168.1.1", "192.168.1.5", null, null)] + [InlineData(false, "192.168.1.1", "192.168.1.10", "192.168.1.1", "192.168.1.11")] + [InlineData(false, "12.168.1.1", "12.168.1.10", "192.18.1.1", "192.18.1.11")] + public void Equals_IPAddressRange_Test(bool expected, string xHead, string xTail, string yHead, string yTail) + { + // Arrange + _ = IPAddress.TryParse(xHead, out var xHeadAddress); + _ = IPAddress.TryParse(xTail, out var xTailAddress); + var xAddressRange = new IPAddressRange(xHeadAddress, xTailAddress); + + var yAddressRange = + IPAddress.TryParse(yHead, out var yHeadAddress) && IPAddress.TryParse(yTail, out var yTailAddress) + ? new IPAddressRange(yHeadAddress, yTailAddress) + : null; + + // Act + var result = xAddressRange.Equals(yAddressRange); + + // Assert + Assert.Equal(expected, result); + } + + #endregion // end: Equals(IPAddressRange) + + #region Equals(object) + + /// Verifies Equals(object) returns the expected result when the argument is boxed as object. + /// Expected result. + /// The head address of the left range. + /// The tail address of the left range. + /// The head address of the right range. + /// The tail address of the right range. + [Theory] + [InlineData(true, "192.168.1.1", "192.168.1.10", "192.168.1.1", "192.168.1.10")] + [InlineData(false, "192.168.1.1", "192.168.1.5", null, null)] + [InlineData(false, "192.168.1.1", "192.168.1.10", "192.168.1.1", "192.168.1.11")] + [InlineData(false, "12.168.1.1", "12.168.1.10", "192.18.1.1", "192.18.1.11")] + public void Equals_Object_Test(bool expected, string xHead, string xTail, string yHead, string yTail) + { + // Arrange + _ = IPAddress.TryParse(xHead, out var xHeadAddress); + _ = IPAddress.TryParse(xTail, out var xTailAddress); + var xAddressRange = new IPAddressRange(xHeadAddress, xTailAddress); + + var yAddressRange = + IPAddress.TryParse(yHead, out var yHeadAddress) && IPAddress.TryParse(yTail, out var yTailAddress) + ? new IPAddressRange(yHeadAddress, yTailAddress) + : null; + + // Act + var result = xAddressRange.Equals((object)yAddressRange); + + // Assert + Assert.Equal(expected, result); + } + + /// Verifies that returns when the argument is not an . + [Fact] + public void Equals_Object_NonIPAddressRangeType_ReturnsFalse_Test() + { + // Arrange + var range = new IPAddressRange(IPAddress.Parse("192.168.1.1"), IPAddress.Parse("192.168.1.10")); + + // Act + // codeql[cs/equals-on-unrelated-types] Intentionally verifying that Equals(object) returns false when comparing to a different type — correct behavior + var result = range.Equals("not an IP address range"); + + // Assert + Assert.False(result); + } + + #endregion // end: Equals(object) + + #region GetHashCode + + /// Verifies equal ranges produce the same hash code and differing ranges produce different hash codes. + /// Whether the two hash codes should be equal. + /// The head address of the left range. + /// The tail address of the left range. + /// The head address of the right range. + /// The tail address of the right range. + [Theory] + [InlineData(true, "192.168.1.5", "192.168.1.100", "192.168.1.5", "192.168.1.100")] + [InlineData(false, "192.168.1.5", "192.168.1.100", "10.168.1.0", "10.168.1.100")] + [InlineData(true, "::abcd", "ff:12::abcd", "::abcd", "ff:12::abcd")] + [InlineData(false, "::abcd", "ff:12::abcd", "::ef", "ff:12::1234")] + public void GetHashCode_Test(bool expected, string xHead, string xTail, string yHead, string yTail) + { + // Arrange + _ = IPAddress.TryParse(xHead, out var xHeadAddress); + _ = IPAddress.TryParse(xTail, out var xTailAddress); + + var xAddressRange = new IPAddressRange(xHeadAddress, xTailAddress); + + _ = IPAddress.TryParse(yHead, out var yHeadAddress); + _ = IPAddress.TryParse(yTail, out var yTailAddress); + + var yAddressRange = new IPAddressRange(yHeadAddress, yTailAddress); + + // Act + var xHash = xAddressRange.GetHashCode(); + var yHash = yAddressRange.GetHashCode(); + var result = xHash.Equals(yHash); + + // Assert + Assert.Equal(expected, result); + } + + #endregion // end: GetHashCode + + #region Equals edge cases specific to direct Head/Tail comparison + + /// Verifies that two ranges with the same Head and Tail are equal regardless of construction path. + /// The head IP address string for both ranges. + /// The tail IP address string for both ranges. + [Theory] + [InlineData("192.168.1.1", "192.168.1.10")] + [InlineData("::abcd", "::abcf")] + [InlineData("10.0.0.0", "10.0.0.1")] + [InlineData("2001:db8::", "2001:db8::ff")] + public void Equals_SameHeadTail_RangesEqual_Test(string headString, string tailString) + { + var head = IPAddress.Parse(headString); + var tail = IPAddress.Parse(tailString); + var range1 = new IPAddressRange(head, tail); + var range2 = new IPAddressRange(head, tail); + + var result = range1.Equals(range2); + + Assert.True(result); + Assert.Equal(range1.GetHashCode(), range2.GetHashCode()); + } + + /// Verifies that Equals correctly distinguishes ranges with same Head but different Tail. + [Fact] + public void Equals_SameHead_DifferentTail_ReturnsFalse_Test() + { + var head = IPAddress.Parse("192.168.1.0"); + var range1 = new IPAddressRange(head, IPAddress.Parse("192.168.1.10")); + var range2 = new IPAddressRange(head, IPAddress.Parse("192.168.1.20")); + + Assert.False(range1.Equals(range2)); + } + + #endregion // end: Equals edge cases + + #endregion // end: Equals + } +} diff --git a/src/Arcus.Tests/IPAddressRangeTests.ISerializable.cs b/src/Arcus.Tests/IPAddressRangeTests.ISerializable.cs new file mode 100644 index 0000000..3267abc --- /dev/null +++ b/src/Arcus.Tests/IPAddressRangeTests.ISerializable.cs @@ -0,0 +1,56 @@ +#if NET48 +using System.Net; +using System.Runtime.Serialization; +using System.Runtime.Serialization.Formatters.Binary; + +namespace Arcus.Tests +{ + /// + /// tests for + /// + public partial class IPAddressRangeTests + { + #region ISerializable + + /// + /// Gets theory data for binary serialization round-trip tests. + /// + /// + /// A set of instances covering single-address, IPv4, and IPv6 ranges. + /// + public static TheoryData CanSerializable_Test_Values => + [ + new IPAddressRange(IPAddress.Parse("192.168.1.0")), + new IPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.255")), + new IPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("::FFFF:4321")), + ]; + + /// Verifies an IPAddressRange round-trips through binary serialization and deserialization unchanged. + /// The range to serialize. + [Theory] + [MemberData(nameof(CanSerializable_Test_Values))] + public void CanSerializable_Test(IPAddressRange ipAddressRange) + { + // Arrange + var formatter = new BinaryFormatter(); + + // Act + using var writeStream = new MemoryStream(); + // Serialize the object to the stream + formatter.Serialize(writeStream, ipAddressRange); + writeStream.Seek(0, SeekOrigin.Begin); + + // Deserialize the object from the stream + var result = formatter.Deserialize(writeStream); + + // Assert + var actual = Assert.IsType(result); + + // using explicit EqualityComparer to avoid comparing elements of enumerable + Assert.Equal(ipAddressRange, actual, IPAddressRangeEqualityComparer.Instance); + } + + #endregion // end: ISerializable + } +} +#endif diff --git a/src/Arcus.Tests/IPAddressRangeTests.Operators.cs b/src/Arcus.Tests/IPAddressRangeTests.Operators.cs new file mode 100644 index 0000000..0ae2fb1 --- /dev/null +++ b/src/Arcus.Tests/IPAddressRangeTests.Operators.cs @@ -0,0 +1,189 @@ +using System.Net; + +namespace Arcus.Tests +{ + /// + /// tests for operators + /// + public partial class IPAddressRangeTests + { + #region Operators + + /// Verifies the == operator returns true only when the comparison result is zero. + /// Expected comparison result. + /// Left operand range. + /// Right operand range. + [Theory] + [MemberData(nameof(Comparison_Values))] + public void Operator_Equals_Test(int expected, IPAddressRange left, IPAddressRange right) + { + // Arrange + // Act + var result = left == right; + + // Assert + Assert.Equal(expected == 0, result); + } + + /// Verifies the != operator returns true when the comparison result is non-zero. + /// Expected comparison result. + /// Left operand range. + /// Right operand range. + [Theory] + [MemberData(nameof(Comparison_Values))] + public void Operator_NotEquals_Test(int expected, IPAddressRange left, IPAddressRange right) + { + // Arrange + // Act + var result = left != right; + + // Assert + Assert.Equal(expected != 0, result); + } + + /// Verifies the > operator returns true only when the comparison result is positive. + /// Expected comparison result. + /// Left operand range. + /// Right operand range. + [Theory] + [MemberData(nameof(Comparison_Values))] + public void Operator_GreaterThan_Test(int expected, IPAddressRange left, IPAddressRange right) + { + // Arrange + // Act + var result = left > right; + + // Assert + Assert.Equal(expected > 0, result); + } + + /// Verifies the >= operator returns true when the comparison result is zero or positive. + /// Expected comparison result. + /// Left operand range. + /// Right operand range. + [Theory] + [MemberData(nameof(Comparison_Values))] + public void Operator_GreaterThanOrEqual_Test(int expected, IPAddressRange left, IPAddressRange right) + { + // Arrange + // Act + var result = left >= right; + + // Assert + Assert.Equal(expected >= 0, result); + } + + /// Verifies the < operator returns true only when the comparison result is negative. + /// Expected comparison result. + /// Left operand range. + /// Right operand range. + [Theory] + [MemberData(nameof(Comparison_Values))] + public void Operator_LessThan_Test(int expected, IPAddressRange left, IPAddressRange right) + { + // Arrange + // Act + var result = left < right; + + // Assert + Assert.Equal(expected < 0, result); + } + + /// Verifies the <= operator returns true when the comparison result is zero or negative. + /// Expected comparison result. + /// Left operand range. + /// Right operand range. + [Theory] + [MemberData(nameof(Comparison_Values))] + public void Operator_LessThanOrEqual_Test(int expected, IPAddressRange left, IPAddressRange right) + { + // Arrange + // Act + var result = left <= right; + + // Assert + Assert.Equal(expected <= 0, result); + } + + #endregion // end: Operators + + #region Null Operands + + /// Verifies that the < operator returns when the left operand is null and the right is non-null. + [Fact] + public void Operator_LessThan_NullLeft_NonNullRight_ReturnsTrue_Test() + { + // Arrange + IPAddressRange left = null; + var right = new IPAddressRange(IPAddress.Parse("192.168.0.0"), IPAddress.Parse("192.168.255.255")); + + // Act + var result = left < right; + + // Assert + Assert.True(result); + } + + /// Verifies that the < operator returns when both operands are null. + [Fact] + public void Operator_LessThan_BothNull_ReturnsFalse_Test() + { + // Arrange + IPAddressRange left = null; + IPAddressRange right = null; + + // Act + var result = left < right; + + // Assert + Assert.False(result); + } + + /// Verifies that the > operator returns when the left operand is null. + [Fact] + public void Operator_GreaterThan_NullLeft_ReturnsFalse_Test() + { + // Arrange + IPAddressRange left = null; + var right = new IPAddressRange(IPAddress.Parse("192.168.0.0"), IPAddress.Parse("192.168.255.255")); + + // Act + var result = left > right; + + // Assert + Assert.False(result); + } + + /// Verifies that the == operator returns when one operand is null and the other is non-null. + [Fact] + public void Operator_Equals_NullLeft_NonNullRight_ReturnsFalse_Test() + { + // Arrange + IPAddressRange left = null; + var right = new IPAddressRange(IPAddress.Parse("192.168.0.0"), IPAddress.Parse("192.168.255.255")); + + // Act + var result = left == right; + + // Assert + Assert.False(result); + } + + /// Verifies that the == operator returns when both operands are null. + [Fact] + public void Operator_Equals_BothNull_ReturnsTrue_Test() + { + // Arrange + IPAddressRange left = null; + IPAddressRange right = null; + + // Act + var result = left == right; + + // Assert + Assert.True(result); + } + + #endregion // end: Null Operands + } +} diff --git a/src/Arcus.Tests/IPAddressRangeTests.cs b/src/Arcus.Tests/IPAddressRangeTests.cs index e2cf52e..905ff42 100644 --- a/src/Arcus.Tests/IPAddressRangeTests.cs +++ b/src/Arcus.Tests/IPAddressRangeTests.cs @@ -1,60 +1,28 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; +using System.Globalization; using System.Net; -using Arcus; using Arcus.Tests.XunitSerializers; -using Xunit; -using Xunit.Sdk; -#if NET48 // maintained for .NET 4.8 compatibility -using System.IO; +#if NET48 using System.Runtime.Serialization; -using System.Runtime.Serialization.Formatters.Binary; #endif [assembly: RegisterXunitSerializer(typeof(IPAddressRangeXunitSerializer), typeof(IPAddressRange))] namespace Arcus.Tests { - public class IPAddressRangeTests + /// Unit tests for . + public partial class IPAddressRangeTests { - #region GetHashCode - - [Theory] - [InlineData(true, "192.168.1.5", "192.168.1.100", "192.168.1.5", "192.168.1.100")] - [InlineData(false, "192.168.1.5", "192.168.1.100", "10.168.1.0", "10.168.1.100")] - [InlineData(true, "::abcd", "ff:12::abcd", "::abcd", "ff:12::abcd")] - [InlineData(false, "::abcd", "ff:12::abcd", "::ef", "ff:12::1234")] - public void GetHashCode_Test(bool expected, string xHead, string xTail, string yHead, string yTail) - { - // Arrange - _ = IPAddress.TryParse(xHead, out var xHeadAddress); - _ = IPAddress.TryParse(xTail, out var xTailAddress); - - var xAddressRange = new IPAddressRange(xHeadAddress, xTailAddress); - - _ = IPAddress.TryParse(yHead, out var yHeadAddress); - _ = IPAddress.TryParse(yTail, out var yTailAddress); - - var yAddressRange = new IPAddressRange(yHeadAddress, yTailAddress); - - // Act - var xHash = xAddressRange.GetHashCode(); - var yHash = yAddressRange.GetHashCode(); - var result = xHash.Equals(yHash); - - // Assert - Assert.Equal(expected, result); - } - - #endregion // end: GetHashCode - #region HeadOverlappedBy + /// Verifies HeadOverlappedBy returns the expected result for an IPAddressRange against various ranges. + /// Expected result. + /// The head address of the range under test. + /// The tail address of the range under test. + /// The head address of the other range. + /// The tail address of the other range. [Theory] [InlineData(false, "192.168.1.0", "255.255.255.255", null, null)] - [InlineData(false, "192.168.1.0", "255.255.255.255", "::", "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff:")] + [InlineData(false, "192.168.1.0", "255.255.255.255", "::", "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")] [InlineData(false, "192.168.1.0", "255.255.255.255", "192.168.1.1", "255.255.255.255")] [InlineData(false, "192.168.1.0", "255.255.255.255", "0.0.0.0", "192.168.0.255")] [InlineData(true, "192.168.1.0", "255.255.255.255", "0.0.0.0", "255.255.255.255")] @@ -85,6 +53,8 @@ public void HeadOverlappedBy_Test(bool expected, string thisHead, string thisTai #region Class + /// Verifies IPAddressRange is assignable from its expected base types and interfaces. + /// The type that should be assignable from IPAddressRange. [Theory] [InlineData(typeof(AbstractIPAddressRange))] [InlineData(typeof(IEquatable))] @@ -107,171 +77,14 @@ public void Assignability_Test(Type assignableFromType) #endregion //end: Class - #region CompareTo / Operators - - public static IEnumerable Comparison_Values() - { - var ipv4Slash16 = Subnet.Parse("192.168.0.0/16"); - var ipv6Slash64 = Subnet.Parse("ab:cd::/64"); - var ipv4Slash20 = Subnet.Parse("192.168.0.0/20"); - var ipv6Slash96 = Subnet.Parse("ab:cd::/96"); - var ipv4All = Subnet.Parse("0.0.0.0/0"); - var ipv6All = Subnet.Parse("::/0"); - var ipv4Single = Subnet.Parse("0.0.0.0/32"); - var ipv6Single = Subnet.Parse("::/128"); - - yield return new object[] - { - 0, - new IPAddressRange(ipv4Slash16.Head, ipv4Slash16.Tail), - new IPAddressRange(ipv4Slash16.Head, ipv4Slash16.Tail), - }; - yield return new object[] - { - 0, - new IPAddressRange(ipv6Slash64.Head, ipv6Slash64.Tail), - new IPAddressRange(ipv6Slash64.Head, ipv6Slash64.Tail), - }; - yield return new object[] { 1, new IPAddressRange(ipv4Slash16.Head, ipv4Slash16.Tail), null }; - yield return new object[] { 1, new IPAddressRange(ipv6Slash64.Head, ipv6Slash64.Tail), null }; - yield return new object[] - { - 1, - new IPAddressRange(ipv4Slash16.Head, ipv4Slash16.Tail), - new IPAddressRange(ipv4Slash20.Head, ipv4Slash20.Tail), - }; - yield return new object[] - { - -1, - new IPAddressRange(ipv4Slash20.Head, ipv4Slash20.Tail), - new IPAddressRange(ipv4Slash16.Head, ipv4Slash16.Tail), - }; - yield return new object[] - { - 1, - new IPAddressRange(ipv6Slash64.Head, ipv6Slash64.Tail), - new IPAddressRange(ipv6Slash96.Head, ipv6Slash96.Tail), - }; - yield return new object[] - { - -1, - new IPAddressRange(ipv6Slash96.Head, ipv6Slash96.Tail), - new IPAddressRange(ipv6Slash64.Head, ipv6Slash64.Tail), - }; - yield return new object[] - { - -1, - new IPAddressRange(ipv4All.Head, ipv4All.Tail), - new IPAddressRange(ipv6All.Head, ipv6All.Tail), - }; - yield return new object[] - { - 1, - new IPAddressRange(ipv6All.Head, ipv6All.Tail), - new IPAddressRange(ipv4All.Head, ipv4All.Tail), - }; - yield return new object[] - { - -1, - new IPAddressRange(ipv4Single.Head, ipv4Single.Tail), - new IPAddressRange(ipv6Single.Head, ipv6Single.Tail), - }; - yield return new object[] - { - 1, - new IPAddressRange(ipv6Single.Head, ipv6Single.Tail), - new IPAddressRange(ipv4Single.Head, ipv4Single.Tail), - }; - } - - [Theory] - [MemberData(nameof(Comparison_Values))] - public void CompareTo_Test(int expected, IPAddressRange left, IPAddressRange right) - { - // Arrange - // Act - var result = left.CompareTo(right); - - // Assert - Assert.Equal(expected, result); - } - - [Theory] - [MemberData(nameof(Comparison_Values))] - public void Operator_Equals_Test(int expected, IPAddressRange left, IPAddressRange right) - { - // Arrange - // Act - var result = left == right; - - // Assert - Assert.Equal(expected == 0, result); - } - - [Theory] - [MemberData(nameof(Comparison_Values))] - public void Operator_NotEquals_Test(int expected, IPAddressRange left, IPAddressRange right) - { - // Arrange - // Act - var result = left != right; - - // Assert - Assert.Equal(expected != 0, result); - } - - [Theory] - [MemberData(nameof(Comparison_Values))] - public void Operator_GreaterThan_Test(int expected, IPAddressRange left, IPAddressRange right) - { - // Arrange - // Act - var result = left > right; - - // Assert - Assert.Equal(expected > 0, result); - } - - [Theory] - [MemberData(nameof(Comparison_Values))] - public void Operator_GreaterThanOrEqual_Test(int expected, IPAddressRange left, IPAddressRange right) - { - // Arrange - // Act - var result = left >= right; - - // Assert - Assert.Equal(expected >= 0, result); - } - - [Theory] - [MemberData(nameof(Comparison_Values))] - public void Operator_LessThan_Test(int expected, IPAddressRange left, IPAddressRange right) - { - // Arrange - // Act - var result = left < right; - - // Assert - Assert.Equal(expected < 0, result); - } - - [Theory] - [MemberData(nameof(Comparison_Values))] - public void Operator_LessThanOrEqual_Test(int expected, IPAddressRange left, IPAddressRange right) - { - // Arrange - // Act - var result = left <= right; - - // Assert - Assert.Equal(expected <= 0, result); - } - - #endregion end CompareTo / Operators - #region TailOverlappedBy + /// Verifies TailOverlappedBy returns the expected result for an IPAddressRange against various ranges. + /// Expected result. + /// The head address of the range under test. + /// The tail address of the range under test. + /// The head address of the other range. + /// The tail address of the other range. [Theory] [InlineData(false, "0.0.0.0", "192.168.1.0", null, null)] [InlineData(false, "0.0.0.0", "192.168.1.0", "::", "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff:")] @@ -303,58 +116,9 @@ public void TailOverlappedBy_Test(bool expected, string thisHead, string thisTai #endregion // end: TailOverlappedBy - #region TryMerge - - [Theory] - [InlineData(null, null, null, null, null)] - [InlineData(null, "192.168.1.1", "192.168.1.9", "::", "::")] - [InlineData(null, "192.168.1.1", "192.168.1.9", "192.168.1.11", "192.168.1.20")] - [InlineData(null, "192.168.1.1", "192.168.1.9", null, null)] - [InlineData(null, null, null, "192.168.1.1", "192.168.1.9")] - [InlineData("192.168.1.1-192.168.1.20", "192.168.1.1", "192.168.1.10", "192.168.1.11", "192.168.1.20")] - [InlineData("192.168.1.1-192.168.1.20", "192.168.1.1", "192.168.1.10", "192.168.1.10", "192.168.1.20")] - [InlineData("192.168.1.1-192.168.1.20", "192.168.1.11", "192.168.1.20", "192.168.1.1", "192.168.1.10")] - [InlineData("192.168.1.1-192.168.1.20", "192.168.1.10", "192.168.1.20", "192.168.1.1", "192.168.1.10")] - [InlineData("192.168.1.10-192.168.1.20", "192.168.1.10", "192.168.1.20", "192.168.1.10", "192.168.1.20")] - [InlineData("::-::", "::", "::", "::", "::")] - [InlineData("::1-::4", "::1", "::2", "::3", "::4")] - [InlineData(null, null, null, "::", "::")] - [InlineData(null, "::", "::", null, null)] - public void TryMergeResultTest(string expected, string alphaHead, string alphaTail, string betaHead, string betaTail) - { - // Arrange - var alphaAddressRange = - IPAddress.TryParse(alphaHead, out var alphaHeadAddress) - && IPAddress.TryParse(alphaTail, out var alphaTailAddress) - ? new IPAddressRange(alphaHeadAddress, alphaTailAddress) - : null; - - var betaAddressRange = - IPAddress.TryParse(betaHead, out var betaHeadAddress) && IPAddress.TryParse(betaTail, out var betaTailAddress) - ? new IPAddressRange(betaHeadAddress, betaTailAddress) - : null; - - // Act - var successResult = IPAddressRange.TryMerge(alphaAddressRange, betaAddressRange, out var mergeResult); - - // Assert - Assert.Equal(expected != null, successResult); - - if (expected == null) - { - Assert.Null(mergeResult); - } - else - { - Assert.NotNull(mergeResult); - Assert.Equal(expected, $"{mergeResult.Head}-{mergeResult.Tail}"); - } - } - - #endregion - #region Ctor + /// Verifies the two-argument constructor correctly sets Head and Tail. [Fact] public void Ctor_HeadAndTail_Specified_Test() { @@ -370,6 +134,7 @@ public void Ctor_HeadAndTail_Specified_Test() Assert.Equal(tail, addressRange.Tail); } + /// Verifies the single-address constructor sets both Head and Tail to the same address. [Fact] public void Ctor_SingleAddressRange_Test() { @@ -386,104 +151,21 @@ public void Ctor_SingleAddressRange_Test() #endregion - #region ISerializable -#if NET48 // maintained for .NET 4.8 compatibility - public static IEnumerable CanSerializable_Test_Values() - { - yield return new object[] { new IPAddressRange(IPAddress.Parse("192.168.1.0")) }; - yield return new object[] { new IPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.255")) }; - yield return new object[] { new IPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("::FFFF:4321")) }; - } + #region maxEnumerationExponent propagation - [Theory] - [MemberData(nameof(CanSerializable_Test_Values))] - public void CanSerializable_Test(IPAddressRange ipAddressRange) - { - // Arrange - var formatter = new BinaryFormatter(); - - // Act - using (var writeStream = new MemoryStream()) - { - // Serialize the object to the stream - formatter.Serialize(writeStream, ipAddressRange); - writeStream.Seek(0, SeekOrigin.Begin); - - // Deserialize the object from the stream - var result = formatter.Deserialize(writeStream); - - // Assert - var actual = Assert.IsType(result); - - // using explicit EqualityComparer to avoid comparing elements of enumerable - Assert.Equal(ipAddressRange, actual, IPAddressRangeEqualityComparer.Instance); - } - } -#endif - #endregion end: ISerializable - - #region Equals - - #region Equals(IPAddressRange) - - [Theory] - [InlineData(true, "192.168.1.1", "192.168.1.10", "192.168.1.1", "192.168.1.10")] - [InlineData(false, "192.168.1.1", "192.168.1.5", null, null)] - [InlineData(false, "192.168.1.1", "192.168.1.10", "192.168.1.1", "192.168.1.11")] - [InlineData(false, "12.168.1.1", "12.168.1.10", "192.18.1.1", "192.18.1.11")] - public void Equals_IPAddressRange_Test(bool expected, string xHead, string xTail, string yHead, string yTail) - { - // Arrange - _ = IPAddress.TryParse(xHead, out var xHeadAddress); - _ = IPAddress.TryParse(xTail, out var xTailAddress); - var xAddressRange = new IPAddressRange(xHeadAddress, xTailAddress); - - var yAddressRange = - IPAddress.TryParse(yHead, out var yHeadAddress) && IPAddress.TryParse(yTail, out var yTailAddress) - ? new IPAddressRange(yHeadAddress, yTailAddress) - : null; - - // Act - var result = xAddressRange.Equals(yAddressRange); - - // Assert - Assert.Equal(expected, result); - } - - #endregion // end: Equals(IPAddressRange) - - #region Equals(object) - - [Theory] - [InlineData(true, "192.168.1.1", "192.168.1.10", "192.168.1.1", "192.168.1.10")] - [InlineData(false, "192.168.1.1", "192.168.1.5", null, null)] - [InlineData(false, "192.168.1.1", "192.168.1.10", "192.168.1.1", "192.168.1.11")] - [InlineData(false, "12.168.1.1", "12.168.1.10", "192.18.1.1", "192.18.1.11")] - public void Equals_Object_Test(bool expected, string xHead, string xTail, string yHead, string yTail) + /// Verifies that propagates maxEnumerationExponent. + [Fact] + public void Ctor_SingleAddress_ExponentPropagates_Test() { - // Arrange - _ = IPAddress.TryParse(xHead, out var xHeadAddress); - _ = IPAddress.TryParse(xTail, out var xTailAddress); - var xAddressRange = new IPAddressRange(xHeadAddress, xTailAddress); - - var yAddressRange = - IPAddress.TryParse(yHead, out var yHeadAddress) && IPAddress.TryParse(yTail, out var yTailAddress) - ? new IPAddressRange(yHeadAddress, yTailAddress) - : null; - - // Act - var result = xAddressRange.Equals((object)yAddressRange); - - // Assert - Assert.Equal(expected, result); + var range = new IPAddressRange(IPAddress.Parse("10.0.0.1"), maxEnumerationExponent: 4); + Assert.Equal(4, range.MaxEnumerationExponent); } - #endregion // end: Equals(object) - - #endregion // end: Equals + #endregion #region Head set + /// Verifies constructing with head greater than tail throws InvalidOperationException. [Fact] public void Head_Set_GreaterThanTail_Test() { @@ -493,6 +175,7 @@ public void Head_Set_GreaterThanTail_Test() Assert.Throws(() => new IPAddressRange(IPAddress.Broadcast, IPAddress.Any)); } + /// Verifies constructing with head from a different address family than tail throws InvalidOperationException. [Fact] public void Head_Set_DifferentAddressFamilyThanTail_Throw_InvalidOperationException_Test() { @@ -506,6 +189,7 @@ public void Head_Set_DifferentAddressFamilyThanTail_Throw_InvalidOperationExcept #region Tail set + /// Verifies constructing with tail from a different address family than head throws InvalidOperationException. [Fact] public void Tail_Set_DifferentAddressFamilyThanHead_Throw_InvalidOperationException_Test() { @@ -515,6 +199,7 @@ public void Tail_Set_DifferentAddressFamilyThanHead_Throw_InvalidOperationExcept Assert.Throws(() => new IPAddressRange(IPAddress.Any, IPAddress.IPv6Loopback)); } + /// Verifies constructing with tail less than head throws InvalidOperationException. [Fact] public void Tail_Set_LessThanHead_Test() { @@ -526,330 +211,13 @@ public void Tail_Set_LessThanHead_Test() #endregion - #region TryCollapseAll - - [Fact] - public void TryCollapseAll_Consecutive_Test() - { - // Arrange - var ranges = new[] - { - new IPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.5")), - new IPAddressRange(IPAddress.Parse("192.168.1.6"), IPAddress.Parse("192.168.1.7")), - new IPAddressRange(IPAddress.Parse("192.168.1.8"), IPAddress.Parse("192.168.1.20")), - }; - - // Act - var success = IPAddressRange.TryCollapseAll(ranges, out var results); - - // Assert - Assert.True(success); - Assert.NotNull(results); - var collection = results.ToList(); - Assert.Single(collection); - - var result = collection.Single(); - Assert.Equal(IPAddress.Parse("192.168.1.0"), result.Head); - Assert.Equal(IPAddress.Parse("192.168.1.20"), result.Tail); - } - - [Fact] - public void TryCollapse_MismatchedAddressFamilies_Test() - { - // Arrange - var ranges = new[] - { - new IPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.5")), - new IPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("abcd::ef00")), - }; - - // Act - var success = IPAddressRange.TryCollapseAll(ranges, out var results); - - // Assert - Assert.False(success); - Assert.NotNull(results); - Assert.False(results.Any()); - } - - [Fact] - public void TryCollapseAll_EmptyInput_Test() - { - // Act - var success = IPAddressRange.TryCollapseAll(Enumerable.Empty(), out var results); - - // Assert - Assert.True(success); - Assert.NotNull(results); - Assert.False(results.Any()); - } - - [Fact] - public void TryCollapse_AllInvalidInput_Test() - { - // Arrange - var ranges = new[] - { - new IPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.5")), - null, - new IPAddressRange(IPAddress.Parse("192.168.1.30"), IPAddress.Parse("192.168.1.35")), - }; - - // Act - var success = IPAddressRange.TryCollapseAll(ranges, out var results); - - // Assert - Assert.False(success); - Assert.NotNull(results); - Assert.False(results.Any()); - } - - [Fact] - public void TryCollapse_AllOverlap_Test() - { - // Arrange - var ranges = new[] - { - new IPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.5")), - new IPAddressRange(IPAddress.Parse("192.168.1.5"), IPAddress.Parse("192.168.1.10")), - new IPAddressRange(IPAddress.Parse("192.168.1.8"), IPAddress.Parse("192.168.1.20")), - }; - - // Act - var success = IPAddressRange.TryCollapseAll(ranges, out var results); - - // Assert - Assert.True(success); - Assert.NotNull(results); - var collection = results.ToList(); - Assert.Single(collection); - - var result = collection.Single(); - Assert.Equal(IPAddress.Parse("192.168.1.0"), result.Head); - Assert.Equal(IPAddress.Parse("192.168.1.20"), result.Tail); - } - - [Fact] - public void TryCollapseAll_SubsetContainsAll_Test() - { - // Arrange - var ranges = new[] - { - new IPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.5")), - new IPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.20")), - new IPAddressRange(IPAddress.Parse("192.168.1.8"), IPAddress.Parse("192.168.1.20")), - }; - - // Act - var success = IPAddressRange.TryCollapseAll(ranges, out var results); - - // Assert - Assert.True(success); - Assert.NotNull(results); - var collection = results.ToList(); - Assert.Single(collection); - - var result = collection.Single(); - Assert.Equal(IPAddress.Parse("192.168.1.0"), result.Head); - Assert.Equal(IPAddress.Parse("192.168.1.20"), result.Tail); - } - - [Fact] - public void TryCollapseAll_WithGaps_Test() - { - // Arrange - var ranges = new[] - { - new IPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.5")), - new IPAddressRange(IPAddress.Parse("192.168.1.7"), IPAddress.Parse("192.168.1.20")), - new IPAddressRange(IPAddress.Parse("192.168.1.30"), IPAddress.Parse("192.168.1.35")), - }; - - // Act - var success = IPAddressRange.TryCollapseAll(ranges, out var results); - - // Assert - Assert.True(success); - Assert.NotNull(results); - var enumerable = results.ToList(); - Assert.Equal(3, enumerable.Count); - Assert.Equal(enumerable, ranges.ToList()); - } - - #endregion // end: TryCollapseAll - - #region TryExcludeAll - - [Fact] - public void TryExcludeAll_Carve_Test() - { - // Arrange - var initialRange = new IPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.200")); - - var ranges = new[] - { - new IPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.10")), - new IPAddressRange(IPAddress.Parse("192.168.1.50"), IPAddress.Parse("192.168.1.100")), - new IPAddressRange(IPAddress.Parse("192.168.1.150"), IPAddress.Parse("192.168.1.200")), - }; - - // Act - var success = IPAddressRange.TryExcludeAll(initialRange, ranges, out var results); - - // Assert - Assert.True(success); - Assert.NotNull(results); - var enumerable = results.ToList(); - Assert.Equal(2, enumerable.Count); - - Assert.Equal( - enumerable, - new[] - { - new IPAddressRange(IPAddress.Parse("192.168.1.11"), IPAddress.Parse("192.168.1.49")), - new IPAddressRange(IPAddress.Parse("192.168.1.101"), IPAddress.Parse("192.168.1.149")), - }.ToList() - ); - } - - [Fact] - public void TryExcludeAll_ConsecutiveCarve_Test() - { - // Arrange - var initialRange = new IPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.200")); - - var ranges = new[] - { - new IPAddressRange(IPAddress.Parse("192.168.1.1"), IPAddress.Parse("192.168.1.100")), - new IPAddressRange(IPAddress.Parse("192.168.1.101"), IPAddress.Parse("192.168.1.199")), - }; - - // Act - var success = IPAddressRange.TryExcludeAll(initialRange, ranges, out var results); - - // Assert - Assert.True(success); - Assert.NotNull(results); - var enumerable = results.ToList(); - Assert.Equal(2, enumerable.Count); - - Assert.Equal( - enumerable, - new[] - { - new IPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.0")), - new IPAddressRange(IPAddress.Parse("192.168.1.200"), IPAddress.Parse("192.168.1.200")), - }.ToList() - ); - } - - [Fact] - public void TryExcludeAll_Head_Test() - { - // Arrange - var initialRange = new IPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.100")); - - var ranges = new[] { new IPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.50")) }; - - // Act - var success = IPAddressRange.TryExcludeAll(initialRange, ranges, out var results); - - // Assert - Assert.True(success); - Assert.NotNull(results); - var collection = results.ToList(); - Assert.Single(collection); - - var result = collection.Single(); - - Assert.Equal(IPAddress.Parse("192.168.1.51"), result.Head); - Assert.Equal(IPAddress.Parse("192.168.1.100"), result.Tail); - } - - [Fact] - public void TryExcludeAll_Overlap_Test() - { - // Arrange - var initialRange = new IPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.100")); - - var ranges = new[] - { - new IPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.49")), - new IPAddressRange(IPAddress.Parse("192.168.1.50"), IPAddress.Parse("192.168.1.75")), - new IPAddressRange(IPAddress.Parse("192.168.1.75"), IPAddress.Parse("192.168.1.100")), - }; - - // Act - var success = IPAddressRange.TryExcludeAll(initialRange, ranges, out var results); - - //Assert - Assert.True(success); - Assert.Empty(results); - } - - [Fact] - public void TryExcludeAll_Tail_Test() - { - // Arrange - var initialRange = new IPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.100")); - - var ranges = new[] { new IPAddressRange(IPAddress.Parse("192.168.1.50"), IPAddress.Parse("192.168.1.100")) }; - - // Act - var success = IPAddressRange.TryExcludeAll(initialRange, ranges, out var results); - - // Assert - Assert.True(success); - Assert.NotNull(results); - var collection = results.ToList(); - Assert.Single(collection); - - var result = collection.Single(); - - Assert.Equal(IPAddress.Parse("192.168.1.0"), result.Head); - Assert.Equal(IPAddress.Parse("192.168.1.49"), result.Tail); - } - - [Fact] - public void TryExcludeAll_NoExclusions_Test() - { - // Arrange - var initialRange = new IPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.200")); - - // Act - var success = IPAddressRange.TryExcludeAll(initialRange, Enumerable.Empty(), out var results); - - // Assert - Assert.True(success); - Assert.NotNull(results); - var collection = results.ToList(); - Assert.Single(collection); - - Assert.Equal(initialRange, collection.Single()); - } - - [Fact] - public void TryExcludeAll_InitialMissMatchedAddressFamily_Test() - { - // Arrange - var initialRange = new IPAddressRange(IPAddress.Parse("::"), IPAddress.Parse("ffff::ffff")); - - var ranges = new[] { new IPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.10")) }; - - // Act - var success = IPAddressRange.TryExcludeAll(initialRange, ranges, out var results); - - // Assert - Assert.False(success); - Assert.Empty(results); - } - - #endregion // end: TryExcludeAll - #region Formatting #region ToString + /// Verifies ToString() produces the same output as the "G" format specifier. + /// The head IP address string. + /// The tail IP address string. [Theory] [InlineData("192.168.1.1", "192.168.1.42")] [InlineData("::beef", "0123::dead")] @@ -872,36 +240,47 @@ public void ToString_MatchesGeneralFormat_Test(string headString, string tailStr #region ToString(string, IFormatProvider) - public static IEnumerable ToString_Format_Test_Values() - { - // general formats - foreach (var format in new[] { null, string.Empty, "g", "G" }) + /// + /// Gets test data for . + /// Covers all general format specifiers (, empty, "g", "G") for both IPv4 and IPv6 ranges. + /// Parameters: expected (string), format (string), formatProvider (IFormatProvider), ipAddressRange (IPAddressRange). + /// + /// + /// Test data for . + /// Covers all general format specifiers (, empty, "g", "G") for both IPv4 and IPv6 ranges. + /// Parameters: expected (string), format (string), formatProvider (IFormatProvider), ipAddressRange (IPAddressRange). + /// + public static TheoryData ToString_Format_Test_Values + { + get { - foreach (var ipAddressRange in Ipv4AddressRanges().Concat(Ipv6AddressRanges())) + var ipv4Range = new IPAddressRange(IPAddress.Parse("192.168.1.1"), IPAddress.Parse("192.168.1.42")); + var ipv4Single = new IPAddressRange(IPAddress.Parse("192.168.1.1"), IPAddress.Parse("192.168.1.1")); + var ipv6Range = new IPAddressRange(IPAddress.Parse("::beef"), IPAddress.Parse("0123::dead")); + var ipv6Single = new IPAddressRange(IPAddress.Parse("::beef"), IPAddress.Parse("::beef")); + + var data = new TheoryData(); + + foreach (var format in new[] { null, string.Empty, "g", "G" }) { - yield return new object[] + foreach (var range in new[] { ipv4Range, ipv4Single, ipv6Range, ipv6Single }) { - $"{ipAddressRange.Head} - {ipAddressRange.Tail}", - format, - CultureInfo.CurrentCulture, - ipAddressRange, - }; + data.Add($"{range.Head} - {range.Tail}", format, CultureInfo.CurrentCulture, range); + } } - } - IEnumerable Ipv4AddressRanges() - { - yield return new IPAddressRange(IPAddress.Parse("192.168.1.1"), IPAddress.Parse("192.168.1.42")); - yield return new IPAddressRange(IPAddress.Parse("192.168.1.1"), IPAddress.Parse("192.168.1.1")); - } - - IEnumerable Ipv6AddressRanges() - { - yield return new IPAddressRange(IPAddress.Parse("::beef"), IPAddress.Parse("0123::dead")); - yield return new IPAddressRange(IPAddress.Parse("::beef"), IPAddress.Parse("::beef")); + return data; } } + /// + /// Verifies that returns the expected string + /// for various format specifiers and address ranges. + /// + /// the expected formatted string. + /// the format specifier to use. + /// the format provider to use. + /// the IP address range to format. [Theory] [MemberData(nameof(ToString_Format_Test_Values))] public void ToString_Format_Test( @@ -919,6 +298,7 @@ IPAddressRange ipAddressRange Assert.Equal(expected, result); } + /// Verifies ToString throws FormatException for an unrecognized format specifier. [Fact] public void ToString_UnknownFormat_Throws_FormatException_Test() { @@ -936,7 +316,7 @@ public void ToString_UnknownFormat_Throws_FormatException_Test() internal class IPAddressRangeEqualityComparer : IEqualityComparer { - public static readonly IPAddressRangeEqualityComparer Instance = new IPAddressRangeEqualityComparer(); + public static readonly IPAddressRangeEqualityComparer Instance = new(); public bool Equals(IPAddressRange x, IPAddressRange y) { diff --git a/src/Arcus.Tests/MacAddressTests.cs b/src/Arcus.Tests/MacAddressTests.cs deleted file mode 100644 index 3a0afdc..0000000 --- a/src/Arcus.Tests/MacAddressTests.cs +++ /dev/null @@ -1,923 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Text.RegularExpressions; -using Gulliver; -using Xunit; -#if NET48 // maintained for .NET 4.8 compatibility -using System.IO; -using System.Runtime.Serialization; -using System.Runtime.Serialization.Formatters.Binary; -#endif - -namespace Arcus.Tests -{ -#if NET6_0_OR_GREATER -#pragma warning disable IDE0062 // Make local function static (IDE0062); purposely allowing non-static functions that could be static for .net4.8 compatibility -#endif - - /// Tests for - [Obsolete("MacAddress is a candidate for removal in future major version")] - public class MacAddressTests - { - #region IComparable - - [Theory] - [MemberData(nameof(Comparison_MacAddress_MacAddress_Values))] - public void CompareTo_MacAddress_Test(int expected, MacAddress left, MacAddress right) - { - // Arrange - // Act - var result = left.CompareTo(right); - - // Assert - Assert.Equal(expected, result); - } - - #endregion end: IComparable - - #region DefaultMacAddress - - [Fact] - public void DefaultMacAddress_Test() - { - // Arrange - var expected = Enumerable.Repeat((byte)0xFF, 6).ToArray(); - - // Act - var defaultMacAddress = MacAddress.DefaultMacAddress; - - // Assert - Assert.NotNull(defaultMacAddress); - Assert.Equal(0, ByteArrayUtils.CompareUnsignedBigEndian(expected, defaultMacAddress.GetAddressBytes())); - } - - #endregion end: DefaultMacAddress - - #region GetAddressBytes - - [Theory] - [InlineData(new byte[] { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff })] - [InlineData(new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 })] - [InlineData(new byte[] { 0x00, 0xff, 0x00, 0xff, 0x00, 0x00 })] - public void GetAddressBytes_Test(byte[] bytes) - { - // Arrange - var macAddress = new MacAddress(bytes); - - // Act - var addressBytes = macAddress.GetAddressBytes(); - - // Assert - Assert.NotNull(addressBytes); - Assert.IsType(addressBytes); - Assert.Equal(addressBytes.Length, addressBytes.Length); - Assert.Equal(0, ByteArrayUtils.CompareUnsignedBigEndian(bytes, addressBytes)); - } - - #endregion end: GetAddressBytes - - #region GetCidBytes - - [Fact] - public void GetCidBytes_Test() - { - // Arrange - var bytes = new byte[] { 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB }; - - var macAddress = new MacAddress(bytes); - - // Act - var cidBytes = macAddress.GetCidBytes(); - - // Assert - Assert.Equal(0, ByteArrayUtils.CompareUnsignedBigEndian(bytes.Skip(3).Take(3).ToArray(), cidBytes)); - } - - #endregion end: GetCidBytes - - #region GetHashCode - - [Theory] - [MemberData(nameof(Comparison_MacAddress_MacAddress_Values))] - public void GetHashCode_Test(int expected, MacAddress left, MacAddress right) - { - // Arrange - if (left is null || right is null) - { - return; // skip null values and bail - } - - // Act - var leftHashCode = left.GetHashCode(); - var rightHashCode = right.GetHashCode(); - - // Assert - Assert.Equal(expected == 0, leftHashCode == rightHashCode); - } - - #endregion end: GetHashCode - - #region GetOuiBytes - - [Fact] - public void GetOuiBytes_Test() - { - // Arrange - var bytes = new byte[] { 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB }; - - var macAddress = new MacAddress(bytes); - - // Act - var ouiBytes = macAddress.GetOuiBytes(); - - // Assert - Assert.Equal(0, ByteArrayUtils.CompareUnsignedBigEndian(bytes.Take(3).ToArray(), ouiBytes)); - } - - #endregion end: GetOuiBytes - - #region IsGloballyUnique - - [Theory] - [InlineData(true, new byte[] { 0b0000_0000, 0x00, 0x00, 0x00, 0x00, 0x00 })] - [InlineData(false, new byte[] { 0b0000_0010, 0x00, 0x00, 0x00, 0x00, 0x00 })] - [InlineData(true, new byte[] { 0b1111_1101, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF })] - [InlineData(false, new byte[] { 0b1111_1111, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF })] - public void IsGloballyUnique_Test(bool expected, byte[] bytes) - { - // Arrange - var macAddress = new MacAddress(bytes); - - // Act - var isGloballyUnique = macAddress.IsGloballyUnique; - - // Assert - Assert.Equal(expected, isGloballyUnique); - } - - #endregion end: IsGloballyUnique - - #region IsLocallyAdministered - - [Theory] - [InlineData(false, new byte[] { 0b0000_0000, 0x00, 0x00, 0x00, 0x00, 0x00 })] - [InlineData(true, new byte[] { 0b0000_0010, 0x00, 0x00, 0x00, 0x00, 0x00 })] - [InlineData(false, new byte[] { 0b1111_1101, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF })] - [InlineData(true, new byte[] { 0b1111_1111, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF })] - public void IsLocallyAdministered_Test(bool expected, byte[] bytes) - { - // Arrange - var macAddress = new MacAddress(bytes); - - // Act - var isLocallyAdministered = macAddress.IsLocallyAdministered; - - // Assert - Assert.Equal(expected, isLocallyAdministered); - } - - #endregion end: IsLocallyAdministered - - #region IsMulticast - - [Theory] - [InlineData(false, new byte[] { 0b0000_0000, 0x00, 0x00, 0x00, 0x00, 0x00 })] - [InlineData(true, new byte[] { 0b0000_0001, 0x00, 0x00, 0x00, 0x00, 0x00 })] - [InlineData(false, new byte[] { 0b1111_1110, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF })] - [InlineData(true, new byte[] { 0b1111_1111, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF })] - public void IsMulticast_Test(bool expected, byte[] bytes) - { - // Arrange - var macAddress = new MacAddress(bytes); - - // Act - var isMulticast = macAddress.IsMulticast; - - // Assert - Assert.Equal(expected, isMulticast); - } - - #endregion end: IsMulticast - - #region IsUnicast - - [Theory] - [InlineData(true, new byte[] { 0b0000_0000, 0x00, 0x00, 0x00, 0x00, 0x00 })] - [InlineData(false, new byte[] { 0b0000_0001, 0x00, 0x00, 0x00, 0x00, 0x00 })] - [InlineData(true, new byte[] { 0b1111_1110, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF })] - [InlineData(false, new byte[] { 0b1111_1111, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF })] - public void IsUnicast_Test(bool expected, byte[] bytes) - { - // Arrange - var macAddress = new MacAddress(bytes); - - // Act - var isUnicast = macAddress.IsUnicast; - - // Assert - Assert.Equal(expected, isUnicast); - } - - #endregion end: IsUnicast - - #region other members - - public static IEnumerable Comparison_MacAddress_MacAddress_Values() - { - var byteArrayMax = Enumerable.Repeat((byte)0xff, 6).ToArray(); - - var byteArrayMin = Enumerable.Repeat((byte)0x00, 6).ToArray(); - - var macAddressMax = new MacAddress(byteArrayMax); - - var macAddressMin = new MacAddress(byteArrayMin); - - yield return new object[] { 0, macAddressMin, macAddressMin }; // same equal - yield return new object[] { 0, macAddressMin, new MacAddress(byteArrayMin) }; // same underlying bytes - - yield return new object[] { -1, macAddressMin, macAddressMax }; // left less than right - yield return new object[] { 1, macAddressMax, macAddressMin }; // left greater than right - - yield return new object[] { 1, macAddressMin, null }; // right is null - } - - public static IEnumerable Comparison_MacAddress_Object_Values() - { - var bytes = Enumerable.Repeat((byte)0xff, 6).ToArray(); - var macAddress = new MacAddress(bytes); - - yield return new object[] { -1, macAddress, "string" }; - yield return new object[] { -1, macAddress, 42 }; - yield return new object[] { -1, macAddress, bytes }; - } - - #endregion - - #region IsDefault - - public static IEnumerable IsDefault_Test_Values() - { - yield return new object[] { true, MacAddress.DefaultMacAddress }; - yield return new object[] { true, new MacAddress(Enumerable.Repeat((byte)0xFF, 6)) }; - yield return new object[] { false, new MacAddress(Enumerable.Repeat((byte)0x00, 6)) }; - } - - [Theory] - [MemberData(nameof(IsDefault_Test_Values))] - public void IsDefault_Test(bool expeted, MacAddress macAddress) - { - // Arrange - // Act - var isDefault = macAddress.IsDefault; - - // Assert - Assert.Equal(expeted, isDefault); - } - - #endregion end: IsDefault - - #region IsUnusable - - public static IEnumerable IsUnusable_Test_Values() - { - yield return new object[] { false, new MacAddress(Enumerable.Repeat((byte)0x00, 6)) }; - yield return new object[] - { - false, - new MacAddress(Enumerable.Repeat((byte)0x00, 3).Concat(Enumerable.Repeat((byte)0xFF, 3))), - }; - yield return new object[] { true, new MacAddress(Enumerable.Repeat((byte)0x01, 6)) }; - yield return new object[] { true, new MacAddress(Enumerable.Repeat((byte)0xFF, 6)) }; - } - - [Theory] - [MemberData(nameof(IsUnusable_Test_Values))] - public void IsUnusable_Test(bool expected, MacAddress macAddress) - { - // Arrange - - // Act - var isUsable = macAddress.IsUnusable; - - // Assert - Assert.Equal(expected, isUsable); - } - - #endregion end: IsUnusable - - #region ctor bytes[] - - [Theory] - [InlineData(0)] - [InlineData(5)] - [InlineData(7)] - public void Ctor_WrongByteCount_Throws_ArgumentException_Test(int byteCount) - { - // Arrange - var bytes = Enumerable.Repeat((byte)0xAC, byteCount).ToArray(); - - // Act - // Assert - Assert.Throws(() => new MacAddress(bytes)); - } - - [Fact] - public void Ctor_NullBytes_Throws_ArgumentNullException_Test() => - // Arrange - // Act - // Assert - Assert.Throws(() => new MacAddress(null)); - - #endregion end: ctor bytes[] - - #region ISerializable -#if NET48 // maintained for .NET 4.8 compatibility - public static IEnumerable CanSerializable_Test_Values() - { - yield return new object[] { new MacAddress(Enumerable.Repeat((byte)0x00, 6)) }; - yield return new object[] { new MacAddress(Enumerable.Repeat((byte)0xFF, 6)) }; - yield return new object[] { new MacAddress(new byte[] { 0x00, 0xCD, 0xEF, 0x01, 0x23, 0x45 }) }; - } - - [Theory] - [MemberData(nameof(CanSerializable_Test_Values))] - public void CanSerializable_Test(MacAddress macAddress) - { - // Arrange - var formatter = new BinaryFormatter(); - - // Act - using (var writeStream = new MemoryStream()) - { - formatter.Serialize(writeStream, macAddress); - writeStream.Seek(0, SeekOrigin.Begin); - - // Deserialize the object from the stream - var result = formatter.Deserialize(writeStream); - - // Assert - Assert.IsType(result); - Assert.Equal(macAddress, result); - } - } -#endif - #endregion end: ISerializable - - #region type - - [Theory] - [InlineData(typeof(IEquatable))] - [InlineData(typeof(IComparable))] - [InlineData(typeof(IComparable))] - [InlineData(typeof(IFormattable))] -#if NET48 - [InlineData(typeof(ISerializable))] -#endif - public void Assignability_Test(Type assignableFromType) - { - // Arrange - var type = typeof(MacAddress); - - // Act - var isAssignableFrom = assignableFromType.IsAssignableFrom(type); - - // Assert - Assert.True(isAssignableFrom); - } - - [Fact] - public void IsConcrete_Test() - { - // Arrange - var type = typeof(MacAddress); - - // Act - var isConcrete = type.IsClass && !type.IsAbstract; - - // Assert - Assert.True(isConcrete); - } - - #endregion end: type - - #region IComparable - - [Theory] - [MemberData(nameof(Comparison_MacAddress_MacAddress_Values))] - public void CompareTo_Object_Test(int expected, MacAddress left, object right) - { - // Arrange - // Act - var result = left.CompareTo(right); - - // Assert - Assert.Equal(expected, result); - } - - [Theory] - [MemberData(nameof(Comparison_MacAddress_Object_Values))] -#pragma warning disable xUnit1026 // theory methods should use all their parameters; Agreed. We're jsut not there yet - public void CompareTo_Object_NotMacAddress_Test(int expected, MacAddress left, object right) => - // Arrange - // Act - // Assert - Assert.Throws(() => left.CompareTo(right)); -#pragma warning restore xUnit1026 - - #endregion end: IComparable - - #region Parse - - #region Parse string - - [Theory] - [MemberData(nameof(Parse_Test_SuccessValues))] - public void Parse_HappyPath_Test(MacAddress expected, string input) - { - // Arrange - - // Act - var macAddress = MacAddress.Parse(input); - - // Assert - Assert.Equal(expected, macAddress); - } - - [Theory] - [MemberData(nameof(Parse_Test_FailValues))] -#pragma warning disable xUnit1026 // theory methods should use all their parameters; Agreed. We're jsut not there yet - public void Parse_Failure_Test(MacAddress expected, string input) => - // Arrange - // Act - // Assert - Assert.ThrowsAny(() => MacAddress.Parse(input)); -#pragma warning restore xUnit1026 - - [Theory] - [MemberData(nameof(Parse_Test_SuccessValues))] - [MemberData(nameof(Parse_Test_FailValues))] - public void TryParse_Test(MacAddress expected, string input) - { - // Arrange - - // Act - var success = MacAddress.TryParse(input, out var macAddress); - - // Assert - Assert.True(expected is null ^ success); - Assert.Equal(expected, macAddress); - } - - public static IEnumerable Parse_Test_FailValues() - { - yield return new object[] { null, null }; - yield return new object[] { null, string.Empty }; - yield return new object[] { null, "A" }; - yield return new object[] { null, "potato" }; - } - - public static IEnumerable Parse_Test_SuccessValues() - { - var formatProvider = CultureInfo.InvariantCulture; - var formats = new[] { "g", "c", "s", "d", "X" }; - - var bytes = new byte[] { 0x00, 0xCD, 0xEF, 0x01, 0x23, 0x45 }; - - var macAddresses = new[] { new MacAddress(bytes), new MacAddress(bytes.ReverseBytes()) }; - - foreach (var macAddress in macAddresses) - { - foreach (var format in formats) - { - var s = macAddress.ToString(format, formatProvider); - yield return new object[] { macAddress, s }; - yield return new object[] { macAddress, s.ToLowerInvariant() }; - } - } - } - - #endregion end: Parse string - - #region ParseAny - - [Theory] - [MemberData(nameof(ParseAny_Test_SuccessValues))] - public void ParseAny_HappyPath_Test(MacAddress expected, string input) - { - // Arrange - - // Act - var macAddress = MacAddress.ParseAny(input); - - // Assert - Assert.Equal(expected, macAddress); - } - - [Theory] - [MemberData(nameof(ParseAny_Test_FailValues))] -#pragma warning disable xUnit1026 // theory methods should use all their parameters; Agreed. We're jsut not there yet - public void ParseAny_Failure_Test(MacAddress expected, string input) => - // Arrange - // Act - // Assert - Assert.ThrowsAny(() => MacAddress.ParseAny(input)); -#pragma warning restore xUnit1026 - - [Theory] - [MemberData(nameof(ParseAny_Test_SuccessValues))] - [MemberData(nameof(ParseAny_Test_FailValues))] - public void TryParseAny_Test(MacAddress expected, string input) - { - // Arrange - - // Act - var success = MacAddress.TryParseAny(input, out var macAddress); - - // Assert - Assert.True(expected is null ^ success); - Assert.Equal(expected, macAddress); - } - - public static IEnumerable ParseAny_Test_FailValues() - { - yield return new object[] { null, null }; - yield return new object[] { null, string.Empty }; - yield return new object[] { null, "A" }; - yield return new object[] { null, "potato" }; - } - - public static IEnumerable ParseAny_Test_SuccessValues() - { - foreach (var testCase in StandardFormatTestCases()) - { - yield return testCase; - } - - foreach (var testCase in NonStandardFormatTestCases()) - { - yield return testCase; - } - - IEnumerable NonStandardFormatTestCases() - { - var bytes = new byte[] { 0x00, 0xCD, 0xEF, 0x01, 0x23, 0x45 }; - var expected = new MacAddress(bytes); - var byteStrings = bytes.Select(b => $"{b:X2}").ToArray(); - - yield return new object[] { expected, string.Join("?!&*", byteStrings) }; - yield return new object[] { expected, $"-_={string.Join(string.Empty, byteStrings)}=_-" }; - yield return new object[] - { - expected, - string.Join("_", string.Join(string.Empty, byteStrings).Select(c => $"{c}")), - }; - } - - IEnumerable StandardFormatTestCases() - { - var formatProvider = CultureInfo.InvariantCulture; - var formats = new[] { "g", "c", "s", "d", "X" }; - - var bytes = new byte[] { 0x00, 0xCD, 0xEF, 0x01, 0x23, 0x45 }; - - var macAddresses = new[] { new MacAddress(bytes), new MacAddress(bytes.ReverseBytes()) }; - - foreach (var macAddress in macAddresses) - { - foreach (var format in formats) - { - var s = macAddress.ToString(format, formatProvider); - yield return new object[] { macAddress, s }; - yield return new object[] { macAddress, s.ToLowerInvariant() }; - } - } - } - } - - #endregion end: ParseAny - - #endregion end: Parse - - #region IFormattable - - public static IEnumerable IFormattable_Test_Values() - { - var formatProvider = CultureInfo.InvariantCulture; - var bytes = new byte[] { 0x00, 0xCD, 0xEF, 0x01, 0x23, 0x45 }; - - yield return new object[] { "00:CD:EF:01:23:45", bytes, null, formatProvider }; - yield return new object[] { "00:CD:EF:01:23:45", bytes, string.Empty, formatProvider }; - yield return new object[] { "00:CD:EF:01:23:45", bytes, "g", formatProvider }; - - yield return new object[] { "00cdef012345", bytes, "x", formatProvider }; - yield return new object[] { "00CDEF012345", bytes, "X", formatProvider }; - - yield return new object[] { "00CD.EF01.2345", bytes, "c", formatProvider }; - - yield return new object[] { "00 CD EF 01 23 45", bytes, "s", formatProvider }; - - yield return new object[] { "884478124869", bytes, "i", formatProvider }; - - yield return new object[] { "00-CD-EF-01-23-45", bytes, "d", formatProvider }; - } - - [Theory] - [MemberData(nameof(IFormattable_Test_Values))] - public void IFormattable_Test(string expected, byte[] bytes, string format, IFormatProvider formatProvider) - { - // Arrange - var macAddress = new MacAddress(bytes); - - // Act - var result = macAddress.ToString(format, formatProvider); - - // Assert - Assert.Equal(expected, result, StringComparer.Ordinal); - } - - #endregion end: IFormattable - - #region IEquatable - - [Theory] - [MemberData(nameof(Comparison_MacAddress_MacAddress_Values))] - public void Equals_MacAddress_Test(int expected, MacAddress left, MacAddress right) - { - // Arrange - // Act - var result = left.Equals(right); - - // Assert - Assert.Equal(expected == 0, result); - } - - [Theory] - [MemberData(nameof(Comparison_MacAddress_MacAddress_Values))] - [MemberData(nameof(Comparison_MacAddress_Object_Values))] - public void Equals_Object_Test(int expected, MacAddress left, object right) - { - // Arrange - // Act - var result = left.Equals(right); - - // Assert - Assert.Equal(expected == 0, result); - } - - #endregion end: IEquatable - - #region operators - - #region equal - - [Theory] - [MemberData(nameof(Comparison_MacAddress_MacAddress_Values))] - public void Operator_Equal_Test(int expected, MacAddress left, MacAddress right) - { - // Arrange - // Act - var result = left == right; - - // Assert - Assert.Equal(expected == 0, result); - } - - #endregion end: equal - - #region not equal - - [Theory] - [MemberData(nameof(Comparison_MacAddress_MacAddress_Values))] - public void Operator_NotEqual_Test(int expected, MacAddress left, MacAddress right) - { - // Arrange - // Act - var result = left != right; - - // Assert - Assert.Equal(expected != 0, result); - } - - #endregion end: not equal - - #region less than - - [Theory] - [MemberData(nameof(Comparison_MacAddress_MacAddress_Values))] - public void Operator_LessThan_Test(int expected, MacAddress left, MacAddress right) - { - // Arrange - // Act - var result = left < right; - - // Assert - Assert.Equal(expected == -1, result); - } - - #endregion end: less than - - #region greater than - - [Theory] - [MemberData(nameof(Comparison_MacAddress_MacAddress_Values))] - public void Operator_GreaterThan_Test(int expected, MacAddress left, MacAddress right) - { - // Arrange - // Act - var result = left > right; - - // Assert - Assert.Equal(expected == 1, result); - } - - #endregion end: greater than - - #region less than or equal - - [Theory] - [MemberData(nameof(Comparison_MacAddress_MacAddress_Values))] - public void Operator_LessThanOrEqualTo_Test(int expected, MacAddress left, MacAddress right) - { - // Arrange - // Act - var result = left <= right; - - // Assert - Assert.Equal(expected <= 0, result); - } - - #endregion end: less than or equal - - #region greater than or equal - - [Theory] - [MemberData(nameof(Comparison_MacAddress_MacAddress_Values))] - public void Operator_GreaterThanOrEqual_Test(int expected, MacAddress left, MacAddress right) - { - // Arrange - // Act - var result = left >= right; - - // Assert - Assert.Equal(expected >= 0, result); - } - - #endregion end: greater than or equal - - #endregion end: operators - - #region AllFormatMacAddressRegularExpression - - public static IEnumerable AllFormantMacAddressRegularExpressionMatches() => - new[] - { - "00D.7FB.576.F14", - "01 D5 7A D5 18 9B", - "015.721.263.754", - "02 41 7F 6E AA 59", - "041E8FF5EB0B", - "08 ED 85 A7 56 6D", - "0D-FF-28-25-33-B1", - "14:C7:A3:D3:B4:97", - "158213168721", - "15:D4:84:DE:72:A7", - "19-7E-1D-96-48-59", - "1C DD 35 BA E0 B8", - "1C56A2268779", - "1D8.E06.643.9BF", - "22-EA-45-59-E9-99", - "23E4.6AE9.14EF", - "24D3240A23C3", - "24 F4 73 7E DF 24", - "29:2A:00:D4:11:C1", - "2DD.215.C37.CF1", - "31-D5-77-54-5C-B4", - "314D705B7F5D", - "3277.E822.DBEE", - "34:ED:5C:3A:C2:26", - "38:46:A8:85:2A:F1", - "3D3E466575FE", - "3DE7.E1C3.F33D", - "3E:5B:37:15:C7:D0", - "3F-8B-BE-06-B6-F7", - "42-9E-C0-72-EF-EA", - "4420.6185.24C0", - "4ABB.D96B.6197", - "4E7C.49D9.ABA8", - "5436.5D82.9215", - "55 B3 13 7D 98 EF", - "565.FBF.891.1DB", - "5A6.1A2.B36.643", - "5C4FE09A5CEE", - "5E758934109F", - "68:40:C6:33:61:67", - "69-61-46-84-5B-03", - "6961.6D06.864A", - "69CA.30DC.394C", - "6AA3C4C8FAED", - "6E-B6-1B-11-CA-28", - "70:47:5F:E3:42:8A", - "70D.04B.5EA.88C", - "72 71 DC 01 99 79", - "75BB2A7F454A", - "765.E80.3FE.05D", - } - .SelectMany(s => new[] { s, s.ToLowerInvariant() }) // Select original and lower case - .Distinct() - .Select(s => new object[] { s }); - - [Theory] - [MemberData(nameof(AllFormantMacAddressRegularExpressionMatches))] - public void AllFormantMacAddressRegularExpression_Matches_Test(string input) - { - // Arrange - // Act - var isMatch = MacAddress.AllFormatMacAddressRegularExpression.IsMatch(input); - - // Assert - Assert.True(isMatch); - var count = Regex.Matches(input, @"[\dA-Fa-f]").Count; - Assert.Equal(12, count); - } - - #endregion end: AllFormatMacAddressRegularExpression - - #region CommonFormatMacAddressRegularExpression - - public static IEnumerable CommonFormatMacAddressRegularExpressionMatches() => - new[] - { - "DC:F1:EE:7C:9A:E1", - "67:2F:7C:7A:FF:C8", - "B9:53:18:2A:71:7D", - "29:11:05:52:82:92", - "E9:33:5E:09:75:72", - "33:2F:3B:80:ED:BD", - "A1:29:5A:EF:DE:F0", - "28:9F:D1:37:29:42", - "F4:84:B5:55:44:66", - "27:3E:BF:1A:79:52", - "09:AF:25:C2:B0:BB", - "58:9B:98:C7:7D:FE", - "55:7D:F4:80:B8:5F", - "F3:E1:FC:C6:69:5E", - "50:BF:59:FD:80:94", - "1D:18:5C:C7:62:61", - "0C:31:21:91:B7:80", - "30:5B:A5:91:57:DD", - "A4:20:B2:52:F7:E9", - "63:93:49:2E:3C:5F", - "4B:ED:5C:DF:EE:B9", - "12:63:97:53:12:B4", - "13:DC:EE:70:5E:47", - "CA:1E:E1:6F:AC:3E", - "DF:96:00:4B:51:56", - "59:26:6A:11:DC:D3", - "D0:DC:A7:1B:CE:A1", - "AB:72:20:97:BD:E4", - "03:DD:05:A9:63:D5", - "93:58:E8:3F:AA:2C", - "48:98:C8:B8:B6:B2", - "C1:E0:D1:95:4C:D2", - "D0:3F:44:DE:07:D8", - "D6:26:E3:18:03:29", - "98:C7:5C:A6:E3:4F", - "FB:E8:2A:DF:55:00", - "E5:28:A5:B0:49:FF", - "1B:63:3F:2C:19:9A", - "2D:2D:FA:92:80:AF", - "DA:B2:60:C3:C3:AB", - "27:81:0B:67:BC:02", - "8F:35:3E:DB:7E:69", - "50:16:42:2D:5D:C3", - "3F:0D:45:E9:0F:90", - "94:58:0D:54:72:D9", - "FE:E8:9E:19:F2:0F", - "4C:7F:D1:E4:4D:4B", - "A0:09:97:78:65:95", - "E6:01:AC:DF:D3:DC", - "49:24:6D:2E:30:58", - } - .SelectMany(s => new[] { s, s.ToLowerInvariant() }) // Select original and lower case - .Distinct() - .Select(s => new object[] { s }); - - [Theory] - [MemberData(nameof(CommonFormatMacAddressRegularExpressionMatches))] - public void CommonFormatMacAddressRegularExpression_Matches_Test(string input) - { - // Arrange - // Act - var isMatch = MacAddress.CommonFormatMacAddressRegularExpression.IsMatch(input); - - // Assert - Assert.True(isMatch); - var count = Regex.Matches(input, @"[\dA-Fa-f]").Count; - Assert.Equal(12, count); - } - - #endregion end: CommonFormatMacAddressRegularExpression - } -} diff --git a/src/Arcus.Tests/Math/IPAddressMathTests.cs b/src/Arcus.Tests/Math/IPAddressMathTests.cs index ed92def..40824e6 100644 --- a/src/Arcus.Tests/Math/IPAddressMathTests.cs +++ b/src/Arcus.Tests/Math/IPAddressMathTests.cs @@ -1,63 +1,62 @@ -using System; -using System.Collections.Generic; -using System.Net; +using System.Net; using Arcus.Math; -using Xunit; namespace Arcus.Tests.Math { + /// Unit tests for . public class IPAddressMathTests { #region IsEqualTo - public static IEnumerable IsEqualTo_Test_Values() + /// Gets theory data for . + /// Parameters: expected (bool), left (IPAddress), right (IPAddress). + public static TheoryData IsEqualTo_Test_Data { - foreach (var testCase in NonTransitiveTestCases()) + get { - yield return new object[] { testCase.expected, testCase.left, testCase.right }; - } - - foreach (var testCase in TransitiveTestCases()) - { - yield return new object[] { testCase.expected, testCase.left, testCase.right }; - yield return new object[] { testCase.expected, testCase.right, testCase.left }; // Transitive law test - } + var data = new TheoryData(); - IEnumerable<(bool expected, IPAddress left, IPAddress right)> NonTransitiveTestCases() - { - // reference equal + // reference equal - same instance, verifies the ReferenceEquals fast path var ipv4SameAddress = IPAddress.Parse("192.168.1.1"); - yield return (true, ipv4SameAddress, ipv4SameAddress); + data.Add(true, ipv4SameAddress, ipv4SameAddress); var ipv6SameAddress = IPAddress.Parse("abc::123"); - yield return (true, ipv6SameAddress, ipv6SameAddress); + data.Add(true, ipv6SameAddress, ipv6SameAddress); - // equal - yield return (true, IPAddress.Parse("192.168.1.1"), IPAddress.Parse("192.168.1.1")); - yield return (true, IPAddress.Parse("abc::123"), IPAddress.Parse("abc::123")); + // value equal - distinct instances with equal values, verifies Equals fallback + data.Add(true, IPAddress.Parse("10.0.0.1"), IPAddress.Parse("10.0.0.1")); + data.Add(true, IPAddress.Parse("def::456"), IPAddress.Parse("def::456")); - // null comparison - yield return (true, null, null); - } + // both null + data.Add(true, null, null); - IEnumerable<(bool expected, IPAddress left, IPAddress right)> TransitiveTestCases() - { // not equal - yield return (false, IPAddress.Parse("192.168.1.25"), IPAddress.Parse("192.168.1.1")); - yield return (false, IPAddress.Parse("abc::fff"), IPAddress.Parse("abc::123")); + data.Add(false, IPAddress.Parse("192.168.1.25"), IPAddress.Parse("192.168.1.1")); + data.Add(false, IPAddress.Parse("192.168.1.1"), IPAddress.Parse("192.168.1.25")); + data.Add(false, IPAddress.Parse("abc::fff"), IPAddress.Parse("abc::123")); + data.Add(false, IPAddress.Parse("abc::123"), IPAddress.Parse("abc::fff")); - // null comparison - yield return (false, null, IPAddress.Parse("192.168.1.1")); - yield return (false, null, IPAddress.Parse("abc::123")); + // null vs non-null + data.Add(false, null, IPAddress.Parse("192.168.1.1")); + data.Add(false, null, IPAddress.Parse("abc::123")); + data.Add(false, IPAddress.Parse("192.168.1.1"), null); + data.Add(false, IPAddress.Parse("abc::123"), null); // differing address families - yield return (false, IPAddress.Parse("192.168.1.1"), IPAddress.Parse("abc::123")); + data.Add(false, IPAddress.Parse("192.168.1.1"), IPAddress.Parse("abc::123")); + data.Add(false, IPAddress.Parse("abc::123"), IPAddress.Parse("192.168.1.1")); + + return data; } } + /// Verifies that IsEqualTo returns the expected equality result for two IP addresses. + /// The expected equality result. + /// The left-hand IP address operand. + /// The right-hand IP address operand. [Theory] - [MemberData(nameof(IsEqualTo_Test_Values))] - public void IsEqualTo_Test(bool expected, IPAddress left, IPAddress right) + [MemberData(nameof(IsEqualTo_Test_Data))] + public void IsEqualTo_TwoAddresses_ReturnsExpectedEquality_Test(bool expected, IPAddress left, IPAddress right) { // Arrange // Act @@ -71,59 +70,55 @@ public void IsEqualTo_Test(bool expected, IPAddress left, IPAddress right) #region IsGreaterThan - public static IEnumerable IsGreaterThan_Test_Values() + /// Gets theory data for . + /// Parameters: expected (bool), left (IPAddress), right (IPAddress). + public static TheoryData IsGreaterThan_Test_Data { - foreach (var testCase in NonTransitiveTestCases()) - { - yield return new object[] { testCase.expected, testCase.left, testCase.right }; - } - - foreach (var testCase in TransitiveInverse()) + get { - yield return new object[] { testCase.expected, testCase.left, testCase.right }; - yield return new object[] { !testCase.expected, testCase.right, testCase.left }; // Transitive law test - } + var data = new TheoryData(); - IEnumerable<(bool expected, IPAddress left, IPAddress right)> NonTransitiveTestCases() - { - // reference equal + // reference equal - not greater than (verifies ReferenceEquals fast path) var ipv4SameAddress = IPAddress.Parse("192.168.1.1"); - yield return (false, ipv4SameAddress, ipv4SameAddress); + data.Add(false, ipv4SameAddress, ipv4SameAddress); var ipv6SameAddress = IPAddress.Parse("abc::123"); - yield return (false, ipv6SameAddress, ipv6SameAddress); + data.Add(false, ipv6SameAddress, ipv6SameAddress); - // equal - yield return (false, IPAddress.Parse("192.168.1.1"), IPAddress.Parse("192.168.1.1")); - yield return (false, IPAddress.Parse("abc::123"), IPAddress.Parse("abc::123")); + // value equal - not greater than (verifies Equals fallback using distinct instances) + data.Add(false, IPAddress.Parse("10.0.0.1"), IPAddress.Parse("10.0.0.1")); + data.Add(false, IPAddress.Parse("def::456"), IPAddress.Parse("def::456")); - // null comparison - yield return (false, null, null); - yield return (false, null, IPAddress.Parse("192.168.1.1")); - yield return (false, null, IPAddress.Parse("abc::123")); - yield return (false, IPAddress.Parse("192.168.1.1"), null); - yield return (false, IPAddress.Parse("abc::123"), null); + // null comparisons + data.Add(false, null, null); + data.Add(false, null, IPAddress.Parse("192.168.1.1")); + data.Add(false, null, IPAddress.Parse("abc::123")); + data.Add(false, IPAddress.Parse("192.168.1.1"), null); + data.Add(false, IPAddress.Parse("abc::123"), null); // differing address families - yield return (false, IPAddress.Parse("192.168.1.1"), IPAddress.Parse("abc::123")); - yield return (false, IPAddress.Parse("abc::123"), IPAddress.Parse("192.168.1.1")); - } + data.Add(false, IPAddress.Parse("192.168.1.1"), IPAddress.Parse("abc::123")); + data.Add(false, IPAddress.Parse("abc::123"), IPAddress.Parse("192.168.1.1")); - IEnumerable<(bool expected, IPAddress left, IPAddress right)> TransitiveInverse() - { // greater than - yield return (true, IPAddress.Parse("192.168.1.25"), IPAddress.Parse("192.168.1.1")); - yield return (true, IPAddress.Parse("abc::fff"), IPAddress.Parse("abc::123")); + data.Add(true, IPAddress.Parse("192.168.1.25"), IPAddress.Parse("192.168.1.1")); + data.Add(true, IPAddress.Parse("abc::fff"), IPAddress.Parse("abc::123")); // less than - yield return (false, IPAddress.Parse("192.168.1.1"), IPAddress.Parse("192.168.1.25")); - yield return (false, IPAddress.Parse("abc::123"), IPAddress.Parse("abc::fff")); + data.Add(false, IPAddress.Parse("192.168.1.1"), IPAddress.Parse("192.168.1.25")); + data.Add(false, IPAddress.Parse("abc::123"), IPAddress.Parse("abc::fff")); + + return data; } } + /// Verifies that IsGreaterThan returns the expected ordering result for two IP addresses. + /// The expected result indicating whether is greater than . + /// The left-hand IP address operand. + /// The right-hand IP address operand. [Theory] - [MemberData(nameof(IsGreaterThan_Test_Values))] - public void IsGreaterThan_Test(bool expected, IPAddress left, IPAddress right) + [MemberData(nameof(IsGreaterThan_Test_Data))] + public void IsGreaterThan_TwoAddresses_ReturnsExpectedOrdering_Test(bool expected, IPAddress left, IPAddress right) { // Arrange // Act @@ -137,59 +132,60 @@ public void IsGreaterThan_Test(bool expected, IPAddress left, IPAddress right) #region IsGreaterThanOrEqualTo - public static IEnumerable IsGreaterThanOrEqualTo_Test_Values() + /// Gets theory data for . + /// Parameters: expected (bool), left (IPAddress), right (IPAddress). + public static TheoryData IsGreaterThanOrEqualTo_Test_Data { - foreach (var testCase in NonTransitiveTestCases()) + get { - yield return new object[] { testCase.expected, testCase.left, testCase.right }; - } - - foreach (var testCase in TransitiveInverse()) - { - yield return new object[] { testCase.expected, testCase.left, testCase.right }; - yield return new object[] { !testCase.expected, testCase.right, testCase.left }; // Transitive law test - } + var data = new TheoryData(); - IEnumerable<(bool expected, IPAddress left, IPAddress right)> NonTransitiveTestCases() - { - // reference equal + // reference equal - is >= (verifies ReferenceEquals fast path) var ipv4SameAddress = IPAddress.Parse("192.168.1.1"); - yield return (true, ipv4SameAddress, ipv4SameAddress); + data.Add(true, ipv4SameAddress, ipv4SameAddress); var ipv6SameAddress = IPAddress.Parse("abc::123"); - yield return (true, ipv6SameAddress, ipv6SameAddress); + data.Add(true, ipv6SameAddress, ipv6SameAddress); - // equal - yield return (true, IPAddress.Parse("192.168.1.1"), IPAddress.Parse("192.168.1.1")); - yield return (true, IPAddress.Parse("abc::123"), IPAddress.Parse("abc::123")); + // value equal - is >= (verifies Equals fallback using distinct instances) + data.Add(true, IPAddress.Parse("10.0.0.1"), IPAddress.Parse("10.0.0.1")); + data.Add(true, IPAddress.Parse("def::456"), IPAddress.Parse("def::456")); - // null comparison - yield return (false, null, IPAddress.Parse("192.168.1.1")); - yield return (false, null, IPAddress.Parse("abc::123")); - yield return (false, IPAddress.Parse("192.168.1.1"), null); - yield return (false, IPAddress.Parse("abc::123"), null); - yield return (true, null, null); + // null comparisons: a single null operand returns false (consistent with IsGreaterThan). + // Two nulls are reference equal which satisfies the inclusive comparison. + data.Add(false, null, IPAddress.Parse("192.168.1.1")); + data.Add(false, null, IPAddress.Parse("abc::123")); + data.Add(false, IPAddress.Parse("192.168.1.1"), null); + data.Add(false, IPAddress.Parse("abc::123"), null); + data.Add(true, null, null); // differing address families - yield return (false, IPAddress.Parse("192.168.1.1"), IPAddress.Parse("abc::123")); - yield return (false, IPAddress.Parse("abc::123"), IPAddress.Parse("192.168.1.1")); - } + data.Add(false, IPAddress.Parse("192.168.1.1"), IPAddress.Parse("abc::123")); + data.Add(false, IPAddress.Parse("abc::123"), IPAddress.Parse("192.168.1.1")); - IEnumerable<(bool expected, IPAddress left, IPAddress right)> TransitiveInverse() - { // greater than - yield return (true, IPAddress.Parse("192.168.1.25"), IPAddress.Parse("192.168.1.1")); - yield return (true, IPAddress.Parse("abc::fff"), IPAddress.Parse("abc::123")); + data.Add(true, IPAddress.Parse("192.168.1.25"), IPAddress.Parse("192.168.1.1")); + data.Add(true, IPAddress.Parse("abc::fff"), IPAddress.Parse("abc::123")); // less than - yield return (false, IPAddress.Parse("192.168.1.1"), IPAddress.Parse("192.168.1.25")); - yield return (false, IPAddress.Parse("abc::123"), IPAddress.Parse("abc::fff")); + data.Add(false, IPAddress.Parse("192.168.1.1"), IPAddress.Parse("192.168.1.25")); + data.Add(false, IPAddress.Parse("abc::123"), IPAddress.Parse("abc::fff")); + + return data; } } + /// Verifies that IsGreaterThanOrEqualTo returns the expected ordering result for two IP addresses. + /// The expected result indicating whether is greater than or equal to . + /// The left-hand IP address operand. + /// The right-hand IP address operand. [Theory] - [MemberData(nameof(IsGreaterThanOrEqualTo_Test_Values))] - public void IsGreaterThanOrEqualTo_Test(bool expected, IPAddress left, IPAddress right) + [MemberData(nameof(IsGreaterThanOrEqualTo_Test_Data))] + public void IsGreaterThanOrEqualTo_TwoAddresses_ReturnsExpectedOrdering_Test( + bool expected, + IPAddress left, + IPAddress right + ) { // Arrange // Act @@ -203,59 +199,55 @@ public void IsGreaterThanOrEqualTo_Test(bool expected, IPAddress left, IPAddress #region IsLessThan - public static IEnumerable IsLessThan_Test_Values() + /// Gets theory data for . + /// Parameters: expected (bool), left (IPAddress), right (IPAddress). + public static TheoryData IsLessThan_Test_Data { - foreach (var testCase in NonTransitiveTestCases()) + get { - yield return new object[] { testCase.expected, testCase.left, testCase.right }; - } + var data = new TheoryData(); - foreach (var testCase in TransitiveInverse()) - { - yield return new object[] { testCase.expected, testCase.left, testCase.right }; - yield return new object[] { !testCase.expected, testCase.right, testCase.left }; // Transitive law test - } - - IEnumerable<(bool expected, IPAddress left, IPAddress right)> NonTransitiveTestCases() - { - // reference equal + // reference equal - not less than (verifies ReferenceEquals fast path) var ipv4SameAddress = IPAddress.Parse("192.168.1.1"); - yield return (false, ipv4SameAddress, ipv4SameAddress); + data.Add(false, ipv4SameAddress, ipv4SameAddress); var ipv6SameAddress = IPAddress.Parse("abc::123"); - yield return (false, ipv6SameAddress, ipv6SameAddress); + data.Add(false, ipv6SameAddress, ipv6SameAddress); - // equal - yield return (false, IPAddress.Parse("192.168.1.1"), IPAddress.Parse("192.168.1.1")); - yield return (false, IPAddress.Parse("abc::123"), IPAddress.Parse("abc::123")); + // value equal - not less than (verifies Equals fallback using distinct instances) + data.Add(false, IPAddress.Parse("10.0.0.1"), IPAddress.Parse("10.0.0.1")); + data.Add(false, IPAddress.Parse("def::456"), IPAddress.Parse("def::456")); - // null comparison - yield return (false, null, null); - yield return (false, null, IPAddress.Parse("192.168.1.1")); - yield return (false, null, IPAddress.Parse("abc::123")); - yield return (false, IPAddress.Parse("192.168.1.1"), null); - yield return (false, IPAddress.Parse("abc::123"), null); + // null comparisons + data.Add(false, null, null); + data.Add(false, null, IPAddress.Parse("192.168.1.1")); + data.Add(false, null, IPAddress.Parse("abc::123")); + data.Add(false, IPAddress.Parse("192.168.1.1"), null); + data.Add(false, IPAddress.Parse("abc::123"), null); // differing address families - yield return (false, IPAddress.Parse("192.168.1.1"), IPAddress.Parse("abc::123")); - yield return (false, IPAddress.Parse("abc::123"), IPAddress.Parse("192.168.1.1")); - } + data.Add(false, IPAddress.Parse("192.168.1.1"), IPAddress.Parse("abc::123")); + data.Add(false, IPAddress.Parse("abc::123"), IPAddress.Parse("192.168.1.1")); - IEnumerable<(bool expected, IPAddress left, IPAddress right)> TransitiveInverse() - { - // greater than - yield return (false, IPAddress.Parse("192.168.1.25"), IPAddress.Parse("192.168.1.1")); - yield return (false, IPAddress.Parse("abc::fff"), IPAddress.Parse("abc::123")); + // greater than - not less than + data.Add(false, IPAddress.Parse("192.168.1.25"), IPAddress.Parse("192.168.1.1")); + data.Add(false, IPAddress.Parse("abc::fff"), IPAddress.Parse("abc::123")); // less than - yield return (true, IPAddress.Parse("192.168.1.1"), IPAddress.Parse("192.168.1.25")); - yield return (true, IPAddress.Parse("abc::123"), IPAddress.Parse("abc::fff")); + data.Add(true, IPAddress.Parse("192.168.1.1"), IPAddress.Parse("192.168.1.25")); + data.Add(true, IPAddress.Parse("abc::123"), IPAddress.Parse("abc::fff")); + + return data; } } + /// Verifies that IsLessThan returns the expected ordering result for two IP addresses. + /// The expected result indicating whether is less than . + /// The left-hand IP address operand. + /// The right-hand IP address operand. [Theory] - [MemberData(nameof(IsLessThan_Test_Values))] - public void IsLessThan_Test(bool expected, IPAddress left, IPAddress right) + [MemberData(nameof(IsLessThan_Test_Data))] + public void IsLessThan_TwoAddresses_ReturnsExpectedOrdering_Test(bool expected, IPAddress left, IPAddress right) { // Arrange // Act @@ -269,59 +261,60 @@ public void IsLessThan_Test(bool expected, IPAddress left, IPAddress right) #region IsLessThanOrEqualTo - public static IEnumerable IsLessThanOrEqualTo_Test_Values() + /// Gets theory data for . + /// Parameters: expected (bool), left (IPAddress), right (IPAddress). + public static TheoryData IsLessThanOrEqualTo_Test_Data { - foreach (var testCase in NonTransitiveTestCases()) - { - yield return new object[] { testCase.expected, testCase.left, testCase.right }; - } - - foreach (var testCase in TransitiveInverse()) + get { - yield return new object[] { testCase.expected, testCase.left, testCase.right }; - yield return new object[] { !testCase.expected, testCase.right, testCase.left }; // Transitive law test - } + var data = new TheoryData(); - IEnumerable<(bool expected, IPAddress left, IPAddress right)> NonTransitiveTestCases() - { - // reference equal + // reference equal - is <= (verifies ReferenceEquals fast path) var ipv4SameAddress = IPAddress.Parse("192.168.1.1"); - yield return (true, ipv4SameAddress, ipv4SameAddress); + data.Add(true, ipv4SameAddress, ipv4SameAddress); var ipv6SameAddress = IPAddress.Parse("abc::123"); - yield return (true, ipv6SameAddress, ipv6SameAddress); + data.Add(true, ipv6SameAddress, ipv6SameAddress); - // equal - yield return (true, IPAddress.Parse("192.168.1.1"), IPAddress.Parse("192.168.1.1")); - yield return (true, IPAddress.Parse("abc::123"), IPAddress.Parse("abc::123")); + // value equal - is <= (verifies Equals fallback using distinct instances) + data.Add(true, IPAddress.Parse("10.0.0.1"), IPAddress.Parse("10.0.0.1")); + data.Add(true, IPAddress.Parse("def::456"), IPAddress.Parse("def::456")); - // null comparison - yield return (true, null, null); - yield return (false, IPAddress.Parse("192.168.1.1"), null); - yield return (false, IPAddress.Parse("abc::123"), null); - yield return (false, null, IPAddress.Parse("192.168.1.1")); - yield return (false, null, IPAddress.Parse("abc::123")); + // null comparisons: a single null operand returns false (consistent with IsLessThan). + // Two nulls are reference equal which satisfies the inclusive comparison. + data.Add(true, null, null); + data.Add(false, IPAddress.Parse("192.168.1.1"), null); + data.Add(false, IPAddress.Parse("abc::123"), null); + data.Add(false, null, IPAddress.Parse("192.168.1.1")); + data.Add(false, null, IPAddress.Parse("abc::123")); // differing address families - yield return (false, IPAddress.Parse("192.168.1.1"), IPAddress.Parse("abc::123")); - yield return (false, IPAddress.Parse("abc::123"), IPAddress.Parse("192.168.1.1")); - } + data.Add(false, IPAddress.Parse("192.168.1.1"), IPAddress.Parse("abc::123")); + data.Add(false, IPAddress.Parse("abc::123"), IPAddress.Parse("192.168.1.1")); - IEnumerable<(bool expected, IPAddress left, IPAddress right)> TransitiveInverse() - { - // greater than - yield return (false, IPAddress.Parse("192.168.1.25"), IPAddress.Parse("192.168.1.1")); - yield return (false, IPAddress.Parse("abc::fff"), IPAddress.Parse("abc::123")); + // greater than - not <= + data.Add(false, IPAddress.Parse("192.168.1.25"), IPAddress.Parse("192.168.1.1")); + data.Add(false, IPAddress.Parse("abc::fff"), IPAddress.Parse("abc::123")); - // less than - yield return (true, IPAddress.Parse("192.168.1.1"), IPAddress.Parse("192.168.1.25")); - yield return (true, IPAddress.Parse("abc::123"), IPAddress.Parse("abc::fff")); + // less than - is <= + data.Add(true, IPAddress.Parse("192.168.1.1"), IPAddress.Parse("192.168.1.25")); + data.Add(true, IPAddress.Parse("abc::123"), IPAddress.Parse("abc::fff")); + + return data; } } + /// Verifies that IsLessThanOrEqualTo returns the expected ordering result for two IP addresses. + /// The expected result indicating whether is less than or equal to . + /// The left-hand IP address operand. + /// The right-hand IP address operand. [Theory] - [MemberData(nameof(IsLessThanOrEqualTo_Test_Values))] - public void IsLessThanOrEqualTo_Test(bool expected, IPAddress left, IPAddress right) + [MemberData(nameof(IsLessThanOrEqualTo_Test_Data))] + public void IsLessThanOrEqualTo_TwoAddresses_ReturnsExpectedOrdering_Test( + bool expected, + IPAddress left, + IPAddress right + ) { // Arrange // Act @@ -331,94 +324,230 @@ public void IsLessThanOrEqualTo_Test(bool expected, IPAddress left, IPAddress ri Assert.Equal(expected, result); } - #endregion // end: IsLessThanOrEqual + #endregion // end: IsLessThanOrEqualTo - #region IsBetween + #region IsGreaterThanOrEqualTo / IsLessThanOrEqualTo null-handling - public static IEnumerable IsBetween_Test_Values() + /// Verifies that IsGreaterThanOrEqualTo returns when the left operand is . + [Fact] + public void IsGreaterThanOrEqualTo_WithNullLeft_ReturnsFalse() { - // inclusive tests - foreach (var testCase in EdgeEqualityTestCases()) - { - yield return new object[] { true, testCase.input, testCase.low, testCase.high, true }; - yield return new object[] { false, testCase.input, testCase.low, testCase.high, false }; - } + // Arrange + IPAddress left = null; + var right = IPAddress.Parse("192.168.1.1"); - // exclusive tests - foreach (var testCase in OverlappedTestCases()) - { - yield return new object[] { testCase.expected, testCase.input, testCase.low, testCase.high, true }; - yield return new object[] { testCase.expected, testCase.input, testCase.low, testCase.high, false }; - } + // Act + var result = left.IsGreaterThanOrEqualTo(right); + + // Assert + Assert.False(result); + } + + /// Verifies that IsGreaterThanOrEqualTo returns when the right operand is . + [Fact] + public void IsGreaterThanOrEqualTo_WithNullRight_ReturnsFalse() + { + // Arrange + var left = IPAddress.Parse("192.168.1.1"); + IPAddress right = null; + + // Act + var result = left.IsGreaterThanOrEqualTo(right); + + // Assert + Assert.False(result); + } + + /// Verifies that IsLessThanOrEqualTo returns when the left operand is . + [Fact] + public void IsLessThanOrEqualTo_WithNullLeft_ReturnsFalse() + { + // Arrange + IPAddress left = null; + var right = IPAddress.Parse("192.168.1.1"); + + // Act + var result = left.IsLessThanOrEqualTo(right); + + // Assert + Assert.False(result); + } - // edges are equal - IEnumerable<(IPAddress input, IPAddress low, IPAddress high)> EdgeEqualityTestCases() + /// Verifies that IsLessThanOrEqualTo returns when the right operand is . + [Fact] + public void IsLessThanOrEqualTo_WithNullRight_ReturnsFalse() + { + // Arrange + var left = IPAddress.Parse("192.168.1.1"); + IPAddress right = null; + + // Act + var result = left.IsLessThanOrEqualTo(right); + + // Assert + Assert.False(result); + } + + #endregion // end: IsGreaterThanOrEqualTo / IsLessThanOrEqualTo null-handling + + #region IsBetween + + /// Gets theory data for . + /// Parameters: expected (bool), input (IPAddress), low (IPAddress), high (IPAddress), inclusive (bool). + public static TheoryData IsBetween_Test_Data + { + get { - // reference equals + var data = new TheoryData(); + + // reference equals low var ipv4SameAddress = IPAddress.Parse("192.168.0.1"); var ipv6SameAddress = IPAddress.Parse("abc::123"); - // reference equals low - yield return (ipv4SameAddress, ipv4SameAddress, IPAddress.Parse("192.168.1.10")); - yield return (ipv6SameAddress, ipv6SameAddress, IPAddress.Parse("abc::f123")); + data.Add(true, ipv4SameAddress, ipv4SameAddress, IPAddress.Parse("192.168.1.10"), true); + data.Add(false, ipv4SameAddress, ipv4SameAddress, IPAddress.Parse("192.168.1.10"), false); + data.Add(true, ipv6SameAddress, ipv6SameAddress, IPAddress.Parse("abc::f123"), true); + data.Add(false, ipv6SameAddress, ipv6SameAddress, IPAddress.Parse("abc::f123"), false); // reference equals high - yield return (ipv4SameAddress, IPAddress.Parse("192.168.0.0"), ipv4SameAddress); - yield return (ipv6SameAddress, IPAddress.Parse("abc::"), ipv6SameAddress); - - // reference equals low and high - yield return (ipv4SameAddress, ipv4SameAddress, ipv4SameAddress); - yield return (ipv6SameAddress, ipv6SameAddress, ipv6SameAddress); - - // equals - // equals low - yield return (IPAddress.Parse("192.168.1.1"), IPAddress.Parse("192.168.1.1"), IPAddress.Parse("192.168.1.10")); - yield return (IPAddress.Parse("abc::123"), IPAddress.Parse("abc::123"), IPAddress.Parse("abc::f123")); + data.Add(true, ipv4SameAddress, IPAddress.Parse("192.168.0.0"), ipv4SameAddress, true); + data.Add(false, ipv4SameAddress, IPAddress.Parse("192.168.0.0"), ipv4SameAddress, false); + data.Add(true, ipv6SameAddress, IPAddress.Parse("abc::"), ipv6SameAddress, true); + data.Add(false, ipv6SameAddress, IPAddress.Parse("abc::"), ipv6SameAddress, false); + + // reference equals both low and high + data.Add(true, ipv4SameAddress, ipv4SameAddress, ipv4SameAddress, true); + data.Add(false, ipv4SameAddress, ipv4SameAddress, ipv4SameAddress, false); + data.Add(true, ipv6SameAddress, ipv6SameAddress, ipv6SameAddress, true); + data.Add(false, ipv6SameAddress, ipv6SameAddress, ipv6SameAddress, false); + + // value equals low - distinct instances, verifies Equals fallback + data.Add( + true, + IPAddress.Parse("10.20.30.1"), + IPAddress.Parse("10.20.30.1"), + IPAddress.Parse("10.20.30.10"), + true + ); + data.Add( + false, + IPAddress.Parse("10.20.30.1"), + IPAddress.Parse("10.20.30.1"), + IPAddress.Parse("10.20.30.10"), + false + ); + data.Add(true, IPAddress.Parse("def::456"), IPAddress.Parse("def::456"), IPAddress.Parse("def::f456"), true); + data.Add(false, IPAddress.Parse("def::456"), IPAddress.Parse("def::456"), IPAddress.Parse("def::f456"), false); - // equals high - yield return (IPAddress.Parse("192.168.1.10"), IPAddress.Parse("192.168.0.0"), IPAddress.Parse("192.168.1.10")); - yield return (IPAddress.Parse("abc::123"), IPAddress.Parse("abc::"), IPAddress.Parse("abc::123")); + // value equals high - distinct instances, verifies Equals fallback + data.Add( + true, + IPAddress.Parse("10.20.30.10"), + IPAddress.Parse("10.20.30.0"), + IPAddress.Parse("10.20.30.10"), + true + ); + data.Add( + false, + IPAddress.Parse("10.20.30.10"), + IPAddress.Parse("10.20.30.0"), + IPAddress.Parse("10.20.30.10"), + false + ); + data.Add(true, IPAddress.Parse("def::456"), IPAddress.Parse("def::"), IPAddress.Parse("def::456"), true); + data.Add(false, IPAddress.Parse("def::456"), IPAddress.Parse("def::"), IPAddress.Parse("def::456"), false); - // equals low and high - yield return (IPAddress.Parse("192.168.1.1"), IPAddress.Parse("192.168.1.1"), IPAddress.Parse("192.168.1.1")); - yield return (IPAddress.Parse("abc::123"), IPAddress.Parse("abc::123"), IPAddress.Parse("abc::123")); - } + // value equals low and high - distinct instances, verifies Equals fallback + data.Add( + true, + IPAddress.Parse("10.20.30.1"), + IPAddress.Parse("10.20.30.1"), + IPAddress.Parse("10.20.30.1"), + true + ); + data.Add( + false, + IPAddress.Parse("10.20.30.1"), + IPAddress.Parse("10.20.30.1"), + IPAddress.Parse("10.20.30.1"), + false + ); + data.Add(true, IPAddress.Parse("def::456"), IPAddress.Parse("def::456"), IPAddress.Parse("def::456"), true); + data.Add(false, IPAddress.Parse("def::456"), IPAddress.Parse("def::456"), IPAddress.Parse("def::456"), false); - // Inclusive and Exclusive - IEnumerable<(bool expected, IPAddress input, IPAddress low, IPAddress high)> OverlappedTestCases() - { - // before low - yield return ( + // before low - both inclusive and exclusive + data.Add( + false, + IPAddress.Parse("192.168.1.0"), + IPAddress.Parse("192.168.10.0"), + IPAddress.Parse("192.168.10.255"), + true + ); + data.Add( false, IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.10.0"), - IPAddress.Parse("192.168.10.255") + IPAddress.Parse("192.168.10.255"), + false ); - yield return (false, IPAddress.Parse("abc::"), IPAddress.Parse("abc::ff"), IPAddress.Parse("abc::ffff")); + data.Add(false, IPAddress.Parse("abc::"), IPAddress.Parse("abc::ff"), IPAddress.Parse("abc::ffff"), true); + data.Add(false, IPAddress.Parse("abc::"), IPAddress.Parse("abc::ff"), IPAddress.Parse("abc::ffff"), false); - // after high - yield return ( + // after high - both inclusive and exclusive + data.Add( false, IPAddress.Parse("192.168.20.0"), IPAddress.Parse("192.168.10.0"), - IPAddress.Parse("192.168.10.255") + IPAddress.Parse("192.168.10.255"), + true ); - yield return (false, IPAddress.Parse("abcd::"), IPAddress.Parse("abc::ff"), IPAddress.Parse("abc::ffff")); + data.Add( + false, + IPAddress.Parse("192.168.20.0"), + IPAddress.Parse("192.168.10.0"), + IPAddress.Parse("192.168.10.255"), + false + ); + data.Add(false, IPAddress.Parse("abcd::"), IPAddress.Parse("abc::ff"), IPAddress.Parse("abc::ffff"), true); + data.Add(false, IPAddress.Parse("abcd::"), IPAddress.Parse("abc::ff"), IPAddress.Parse("abc::ffff"), false); - // inside range - yield return ( + // inside range - both inclusive and exclusive + data.Add( + true, + IPAddress.Parse("192.168.10.128"), + IPAddress.Parse("192.168.10.0"), + IPAddress.Parse("192.168.10.255"), + true + ); + data.Add( true, IPAddress.Parse("192.168.10.128"), IPAddress.Parse("192.168.10.0"), - IPAddress.Parse("192.168.10.255") + IPAddress.Parse("192.168.10.255"), + false ); - yield return (true, IPAddress.Parse("abc::fff0"), IPAddress.Parse("abc::ff"), IPAddress.Parse("abc::ffff")); + data.Add(true, IPAddress.Parse("abc::fff0"), IPAddress.Parse("abc::ff"), IPAddress.Parse("abc::ffff"), true); + data.Add(true, IPAddress.Parse("abc::fff0"), IPAddress.Parse("abc::ff"), IPAddress.Parse("abc::ffff"), false); + + return data; } } + /// Verifies that IsBetween returns the expected membership result for an IP address relative to a low/high range. + /// The expected membership result. + /// The IP address to test. + /// The inclusive lower bound of the range. + /// The inclusive upper bound of the range. + /// When , the bounds are treated as inclusive; otherwise exclusive. [Theory] - [MemberData(nameof(IsBetween_Test_Values))] - public void IsBetween_Test(bool expected, IPAddress input, IPAddress low, IPAddress high, bool inclusive) + [MemberData(nameof(IsBetween_Test_Data))] + public void IsBetween_AddressAndRange_ReturnsExpectedMembership_Test( + bool expected, + IPAddress input, + IPAddress low, + IPAddress high, + bool inclusive + ) { // Arrange // Act @@ -428,65 +557,77 @@ public void IsBetween_Test(bool expected, IPAddress input, IPAddress low, IPAddr Assert.Equal(expected, result); } + /// Verifies that IsBetween throws when the input address is . [Fact] public void IsBetween_NullInput_Throws_ArgumentNullException_Test() { // Arrange // Act // Assert - Assert.Throws(() => ((IPAddress)null).IsBetween(IPAddress.Any, IPAddress.Any)); } + /// Verifies that IsBetween throws when the low bound is . [Fact] - public void IsBetween_NullHigh_Throws_ArgumentNullException_Test() + public void IsBetween_NullLow_Throws_ArgumentNullException_Test() { // Arrange // Act // Assert - - Assert.Throws(() => IPAddress.Any.IsBetween(IPAddress.Any, null)); + Assert.Throws(() => IPAddress.Any.IsBetween(null, IPAddress.Any)); } + /// Verifies that IsBetween throws when the high bound is . [Fact] - public void IsBetween_NullLow_Throws_ArgumentNullException_Test() + public void IsBetween_NullHigh_Throws_ArgumentNullException_Test() { // Arrange // Act // Assert - - Assert.Throws(() => IPAddress.Any.IsBetween(null, IPAddress.Any)); + Assert.Throws(() => IPAddress.Any.IsBetween(IPAddress.Any, null)); } + /// Verifies that IsBetween throws when the low bound is greater than the high bound. [Fact] public void IsBetween_LowGreaterThanHigh_Throws_InvalidOperationException_Test() { // Arrange // Act // Assert - Assert.Throws(() => IPAddress.Any.IsBetween(IPAddress.Parse("100.1.1.1"), IPAddress.Parse("10.1.1.1")) ); } - public static IEnumerable IsBetween_UnmatchedAddressFamilies_Test_Values() + /// Gets theory data for . + /// Parameters: input (IPAddress), low (IPAddress), high (IPAddress). + public static TheoryData IsBetween_UnmatchedAddressFamilies_Test_Data { - var ipv4 = IPAddress.Any; - var ipv6 = IPAddress.IPv6Any; + get + { + var data = new TheoryData(); - yield return new object[] { ipv4, ipv4, ipv6 }; - yield return new object[] { ipv4, ipv6, ipv4 }; - yield return new object[] { ipv6, ipv4, ipv4 }; + var ipv4 = IPAddress.Any; + var ipv6 = IPAddress.IPv6Any; - yield return new object[] { ipv6, ipv6, ipv4 }; - yield return new object[] { ipv6, ipv4, ipv6 }; - yield return new object[] { ipv4, ipv6, ipv6 }; + data.Add(ipv4, ipv4, ipv6); + data.Add(ipv4, ipv6, ipv4); + data.Add(ipv6, ipv4, ipv4); + data.Add(ipv6, ipv6, ipv4); + data.Add(ipv6, ipv4, ipv6); + data.Add(ipv4, ipv6, ipv6); + + return data; + } } + /// Verifies that IsBetween throws when the input, low, and high addresses do not all share the same address family. + /// The IP address to test. + /// The lower bound IP address. + /// The upper bound IP address. [Theory] - [MemberData(nameof(IsBetween_UnmatchedAddressFamilies_Test_Values))] - public void IsBetween_UnmatchedAddressFamilies_Throws_InvalidOperationException_Test( + [MemberData(nameof(IsBetween_UnmatchedAddressFamilies_Test_Data))] + public void IsBetween_MismatchedAddressFamilies_Throws_InvalidOperationException_Test( IPAddress input, IPAddress low, IPAddress high @@ -495,13 +636,16 @@ IPAddress high // Arrange // Act // Assert - Assert.Throws(() => input.IsBetween(low, high)); + Assert.Throws(() => input.IsBetween(low, high)); } #endregion // end: IsBetween #region IsAtMax + /// Verifies that IsAtMax returns the expected result for a known IP address. + /// The expected result indicating whether the address is at the maximum value for its address family. + /// The IP address string to parse and test. [Theory] [InlineData(false, "::")] [InlineData(false, "0.0.0.0")] @@ -509,7 +653,7 @@ IPAddress high [InlineData(false, "7777:7777:7777:7777:7777:7777:7777:7777")] [InlineData(true, "255.255.255.255")] [InlineData(true, "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")] - public void IsAtMax_Test(bool expected, string input) + public void IsAtMax_KnownAddress_ReturnsExpectedResult_Test(bool expected, string input) { // Arrange var address = IPAddress.Parse(input); @@ -521,13 +665,13 @@ public void IsAtMax_Test(bool expected, string input) Assert.Equal(expected, result); } + /// Verifies that IsAtMax throws when the input address is . [Fact] public void IsAtMax_NullInput_Throws_ArgumentNullException_Test() { // Arrange // Act // Assert - Assert.Throws(() => ((IPAddress)null).IsAtMax()); } @@ -535,6 +679,9 @@ public void IsAtMax_NullInput_Throws_ArgumentNullException_Test() #region IsAtMin + /// Verifies that IsAtMin returns the expected result for a known IP address. + /// The expected result indicating whether the address is at the minimum value for its address family. + /// The IP address string to parse and test. [Theory] [InlineData(false, "255.255.255.255")] [InlineData(false, "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")] @@ -542,7 +689,7 @@ public void IsAtMax_NullInput_Throws_ArgumentNullException_Test() [InlineData(false, "7777:7777:7777:7777:7777:7777:7777:7777")] [InlineData(true, "::")] [InlineData(true, "0.0.0.0")] - public void IsAtMin_Test(bool expected, string input) + public void IsAtMin_KnownAddress_ReturnsExpectedResult_Test(bool expected, string input) { // Arrange var address = IPAddress.Parse(input); @@ -554,13 +701,13 @@ public void IsAtMin_Test(bool expected, string input) Assert.Equal(expected, result); } + /// Verifies that IsAtMin throws when the input address is . [Fact] public void IsAtMin_NullInput_Throws_ArgumentNullException_Test() { // Arrange // Act // Assert - Assert.Throws(() => ((IPAddress)null).IsAtMin()); } @@ -568,26 +715,39 @@ public void IsAtMin_NullInput_Throws_ArgumentNullException_Test() #region Max - public static IEnumerable Max_Test_Values() + /// Gets theory data for . + /// Parameters: expected (IPAddress), left (IPAddress), right (IPAddress). + public static TheoryData Max_Test_Data { - var minIpv4 = IPAddress.Parse("192.168.1.1"); - var maxIpv4 = IPAddress.Parse("192.168.100.1"); + get + { + var data = new TheoryData(); + + var minIpv4 = IPAddress.Parse("192.168.1.1"); + var maxIpv4 = IPAddress.Parse("192.168.100.1"); - yield return new object[] { maxIpv4, maxIpv4, maxIpv4 }; - yield return new object[] { maxIpv4, minIpv4, maxIpv4 }; - yield return new object[] { maxIpv4, maxIpv4, minIpv4 }; + data.Add(maxIpv4, maxIpv4, maxIpv4); + data.Add(maxIpv4, minIpv4, maxIpv4); + data.Add(maxIpv4, maxIpv4, minIpv4); - var minIpv6 = IPAddress.Parse("abc::01"); - var maxIpv6 = IPAddress.Parse("ffff::f123"); + var minIpv6 = IPAddress.Parse("abc::01"); + var maxIpv6 = IPAddress.Parse("ffff::f123"); - yield return new object[] { maxIpv6, maxIpv6, maxIpv6 }; - yield return new object[] { maxIpv6, minIpv6, maxIpv6 }; - yield return new object[] { maxIpv6, maxIpv6, minIpv6 }; + data.Add(maxIpv6, maxIpv6, maxIpv6); + data.Add(maxIpv6, minIpv6, maxIpv6); + data.Add(maxIpv6, maxIpv6, minIpv6); + + return data; + } } + /// Verifies that Max returns the larger of two IP addresses. + /// The expected larger IP address. + /// The left-hand IP address operand. + /// The right-hand IP address operand. [Theory] - [MemberData(nameof(Max_Test_Values))] - public void Max_Test(IPAddress expected, IPAddress left, IPAddress right) + [MemberData(nameof(Max_Test_Data))] + public void Max_TwoAddresses_ReturnsLargerAddress_Test(IPAddress expected, IPAddress left, IPAddress right) { // Arrange // Act @@ -597,25 +757,53 @@ public void Max_Test(IPAddress expected, IPAddress left, IPAddress right) Assert.Equal(expected, result); } + /// Verifies that Max throws when the left operand is . [Fact] - public void Max_NullInput_Throws_ArgumentNullException_Test() + public void Max_NullLeftInput_Throws_ArgumentNullException_Test() { // Arrange // Act // Assert - Assert.Throws(() => IPAddressMath.Max(null, IPAddress.Any)); + } + + /// Verifies that Max throws when the right operand is . + [Fact] + public void Max_NullRightInput_Throws_ArgumentNullException_Test() + { + // Arrange + // Act + // Assert Assert.Throws(() => IPAddressMath.Max(IPAddress.Any, null)); + } + + /// Verifies that Max throws when both operands are . + [Fact] + public void Max_BothInputsNull_Throws_ArgumentNullException_Test() + { + // Arrange + // Act + // Assert Assert.Throws(() => IPAddressMath.Max(null, null)); } + /// Verifies that Max throws when the left operand is IPv4 and the right operand is IPv6. [Fact] - public void Max_MismatchedAddressFamily_Throws_InvalidOperationException_Test() + public void Max_MismatchedAddressFamilies_LeftIPv4RightIPv6_Throws_InvalidOperationException_Test() { // Arrange // Act // Assert Assert.Throws(() => IPAddressMath.Max(IPAddress.Any, IPAddress.IPv6Any)); + } + + /// Verifies that Max throws when the left operand is IPv6 and the right operand is IPv4. + [Fact] + public void Max_MismatchedAddressFamilies_LeftIPv6RightIPv4_Throws_InvalidOperationException_Test() + { + // Arrange + // Act + // Assert Assert.Throws(() => IPAddressMath.Max(IPAddress.IPv6Any, IPAddress.Any)); } @@ -623,26 +811,39 @@ public void Max_MismatchedAddressFamily_Throws_InvalidOperationException_Test() #region Min - public static IEnumerable Min_Test_Values() + /// Gets theory data for . + /// Parameters: expected (IPAddress), left (IPAddress), right (IPAddress). + public static TheoryData Min_Test_Data { - var minIpv4 = IPAddress.Parse("192.168.1.1"); - var maxIpv4 = IPAddress.Parse("192.168.100.1"); + get + { + var data = new TheoryData(); - yield return new object[] { minIpv4, minIpv4, minIpv4 }; - yield return new object[] { minIpv4, minIpv4, maxIpv4 }; - yield return new object[] { minIpv4, maxIpv4, minIpv4 }; + var minIpv4 = IPAddress.Parse("192.168.1.1"); + var maxIpv4 = IPAddress.Parse("192.168.100.1"); - var minIpv6 = IPAddress.Parse("abc::01"); - var maxIpv6 = IPAddress.Parse("ffff::f123"); + data.Add(minIpv4, minIpv4, minIpv4); + data.Add(minIpv4, minIpv4, maxIpv4); + data.Add(minIpv4, maxIpv4, minIpv4); - yield return new object[] { minIpv6, minIpv6, minIpv6 }; - yield return new object[] { minIpv6, minIpv6, maxIpv6 }; - yield return new object[] { minIpv6, maxIpv6, minIpv6 }; + var minIpv6 = IPAddress.Parse("abc::01"); + var maxIpv6 = IPAddress.Parse("ffff::f123"); + + data.Add(minIpv6, minIpv6, minIpv6); + data.Add(minIpv6, minIpv6, maxIpv6); + data.Add(minIpv6, maxIpv6, minIpv6); + + return data; + } } + /// Verifies that Min returns the smaller of two IP addresses. + /// The expected smaller IP address. + /// The left-hand IP address operand. + /// The right-hand IP address operand. [Theory] - [MemberData(nameof(Min_Test_Values))] - public void Min_Test(IPAddress expected, IPAddress left, IPAddress right) + [MemberData(nameof(Min_Test_Data))] + public void Min_TwoAddresses_ReturnsSmallerAddress_Test(IPAddress expected, IPAddress left, IPAddress right) { // Arrange // Act @@ -652,25 +853,53 @@ public void Min_Test(IPAddress expected, IPAddress left, IPAddress right) Assert.Equal(expected, result); } + /// Verifies that Min throws when the left operand is . [Fact] - public void Min_NullInput_Throws_ArgumentNullException_Test() + public void Min_NullLeftInput_Throws_ArgumentNullException_Test() { // Arrange // Act // Assert - Assert.Throws(() => IPAddressMath.Min(null, IPAddress.Any)); + } + + /// Verifies that Min throws when the right operand is . + [Fact] + public void Min_NullRightInput_Throws_ArgumentNullException_Test() + { + // Arrange + // Act + // Assert Assert.Throws(() => IPAddressMath.Min(IPAddress.Any, null)); + } + + /// Verifies that Min throws when both operands are . + [Fact] + public void Min_BothInputsNull_Throws_ArgumentNullException_Test() + { + // Arrange + // Act + // Assert Assert.Throws(() => IPAddressMath.Min(null, null)); } + /// Verifies that Min throws when the left operand is IPv4 and the right operand is IPv6. [Fact] - public void Min_MismatchedAddressFamily_Throws_InvalidOperationException_Test() + public void Min_MismatchedAddressFamilies_LeftIPv4RightIPv6_Throws_InvalidOperationException_Test() { // Arrange // Act // Assert Assert.Throws(() => IPAddressMath.Min(IPAddress.Any, IPAddress.IPv6Any)); + } + + /// Verifies that Min throws when the left operand is IPv6 and the right operand is IPv4. + [Fact] + public void Min_MismatchedAddressFamilies_LeftIPv6RightIPv4_Throws_InvalidOperationException_Test() + { + // Arrange + // Act + // Assert Assert.Throws(() => IPAddressMath.Min(IPAddress.IPv6Any, IPAddress.Any)); } @@ -678,6 +907,10 @@ public void Min_MismatchedAddressFamily_Throws_InvalidOperationException_Test() #region Increment + /// Verifies that Increment returns the expected IP address when applied a valid delta. + /// The expected resulting IP address string. + /// The input IP address string to parse and increment. + /// The signed integer delta to apply. [Theory] [InlineData("::", "::", 0)] [InlineData("::1", "::", 1)] @@ -699,7 +932,7 @@ public void Min_MismatchedAddressFamily_Throws_InvalidOperationException_Test() [InlineData("255.255.255.254", "255.255.255.255", -1)] [InlineData("255.255.255.255", "255.255.255.254", 1)] [InlineData("255.255.255.255", "255.255.255.255", 0)] - public void Increment_Test(string expected, string input, long delta) + public void Increment_ValidDelta_ReturnsExpectedAddress_Test(string expected, string input, long delta) { // Arrange var address = IPAddress.Parse(input); @@ -711,48 +944,64 @@ public void Increment_Test(string expected, string input, long delta) Assert.Equal(expected, result.ToString()); } + /// Verifies that Increment throws when a negative delta causes an address underflow. + /// The input IP address string to parse. + /// The negative delta that causes underflow. [Theory] - [InlineData("::", -1)] - [InlineData("::FF", -1024)] - [InlineData("0.0.0.0", -1)] - [InlineData("0.0.0.255", -1024)] - public void Increment_Underflow_Throws_InvalidOperationException_Test(string input, long delta) + [InlineData("::", -1)] // IPv6 underflow + [InlineData("::FF", -1024)] // IPv6 underflow by large negative delta + [InlineData("0.0.0.0", -1)] // IPv4 underflow + [InlineData("0.0.0.255", -1024)] // IPv4 underflow by large negative delta + public void Increment_NegativeDeltaCausesUnderflow_Throws_InvalidOperationException_Test(string input, long delta) { // Arrange var address = IPAddress.Parse(input); // Act + void Act() => address.Increment(delta); + // Assert - Assert.Throws(() => address.Increment(delta)); + Assert.Throws(Act); } + /// Verifies that Increment throws containing "overflow" when a positive delta causes an address overflow. + /// The input IP address string to parse. + /// The positive delta that causes overflow. [Theory] - [InlineData("255.255.255.0", 1024)] - [InlineData("255.255.255.255", 1)] - [InlineData("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ff", 65535)] - [InlineData("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", 1)] - public void Increment_OverflowThrows_InvalidOperationException_Test(string input, long delta) + [InlineData("255.255.255.0", 1024)] // IPv4 overflow by large positive delta + [InlineData("255.255.255.255", 1)] // IPv4 overflow at max + [InlineData("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ff", 65535)] // IPv6 overflow by large positive delta + [InlineData("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", 1)] // IPv6 overflow at max + public void Increment_PositiveDeltaCausesOverflow_Throws_InvalidOperationException_Test(string input, long delta) { // Arrange var address = IPAddress.Parse(input); // Act - // Assert - Assert.Throws(() => address.Increment(delta)); + void Act() => address.Increment(delta); + + // Assert - overflow message is distinct from underflow + var ex = Assert.Throws(Act); + Assert.Contains("overflow", ex.Message, StringComparison.OrdinalIgnoreCase); } + /// Verifies that Increment throws when the input address is . [Fact] public void Increment_NullInput_Throws_ArgumentNullException_Test() { // Arrange // Act // Assert - Assert.Throws(() => ((IPAddress)null).Increment()); } #region TryIncrement + /// Verifies that TryIncrement returns the expected success flag and resulting address for various inputs including null, overflow, underflow, and valid deltas. + /// The expected return value indicating whether the increment succeeded. + /// The expected resulting IP address string. + /// The input IP address string. + /// The signed integer delta to apply. [Theory] [InlineData(false, null, null, 0)] [InlineData(false, null, "255.255.255.0", 1024)] @@ -783,7 +1032,12 @@ public void Increment_NullInput_Throws_ArgumentNullException_Test() [InlineData(true, "255.255.255.254", "255.255.255.255", -1)] [InlineData(true, "255.255.255.255", "255.255.255.254", 1)] [InlineData(true, "255.255.255.255", "255.255.255.255", 0)] - public void TryIncrement_Test(bool expectedSuccess, string expectedResultString, string inputString, long delta) + public void TryIncrement_VariousInputs_ReturnsExpectedSuccessAndAddress_Test( + bool expectedSuccess, + string expectedResultString, + string inputString, + long delta + ) { // Arrange _ = IPAddress.TryParse(inputString, out var input); @@ -793,9 +1047,8 @@ public void TryIncrement_Test(bool expectedSuccess, string expectedResultString, // Assert Assert.Equal(expectedSuccess, successResult); - - _ = IPAddress.TryParse(expectedResultString, out var expectResultAddress); - Assert.Equal(expectResultAddress, result); + _ = IPAddress.TryParse(expectedResultString, out var expectedResultAddress); + Assert.Equal(expectedResultAddress, result); } #endregion // end: TryIncrement diff --git a/src/Arcus.Tests/SubnetTests.Factory.cs b/src/Arcus.Tests/SubnetTests.Factory.cs new file mode 100644 index 0000000..87cbd1f --- /dev/null +++ b/src/Arcus.Tests/SubnetTests.Factory.cs @@ -0,0 +1,1550 @@ +using System.Net; +using System.Text; +using System.Text.RegularExpressions; + +namespace Arcus.Tests +{ + /// + /// tests for static factory methods + /// + public partial class SubnetTests + { + #region TryIPv4FromPartial + + /// Gets theory data for . + /// Parameters: expected subnet result ( or null), partial IPv4 input string. + public static TheoryData TryIPv4FromPartial_Test_Values() + { + return new TheoryData + { + { null, null }, + { null, string.Empty }, + { null, "potato" }, + { Subnet.Parse("192.0.0.0/8"), "192" }, + { Subnet.Parse("192.0.0.0/8"), "192." }, + { Subnet.Parse("192.168.0.0/16"), "192.168" }, + { Subnet.Parse("192.168.0.0/16"), "192.168." }, + { Subnet.Parse("192.168.1.0/24"), "192.168.1" }, + { Subnet.Parse("192.168.1.0/24"), "192.168.1." }, + { Subnet.Parse("192.168.1.1/32"), "192.168.1.1" }, + { null, "192.168.1.1." }, + { null, "192.168.0.1.5" }, + // Addresses #59 - TryIPv4FromPartial throws FormatException for 0-prefixed octet not valid as octal + { null, "10.209.005.029" }, + }; + } + + /// Verifies that returns the expected subnet for a given partial IPv4 string. + /// Expected subnet when parsing succeeds. + /// Partial IPv4 address string to parse. + [Theory] + [MemberData(nameof(TryIPv4FromPartial_Test_Values))] + public void TryIPv4FromPartial_Test(Subnet expected, string input) + { + // Arrange + + // Act + var success = Subnet.TryIPv4FromPartial(input, out var subnet); + + // Assert + Assert.Equal(expected != null, success); + Assert.Equal(expected, subnet); + } + + #endregion // end: TryIPv4FromPartial + + #region Ipv4OctetPartialPattern + + /// Gets theory data for . + /// Parameters: expected match result (bool), input string to test against the pattern. + public static TheoryData Ipv4OctetPartialPattern_Test_Data => + new() + { + // single octet - valid range + { true, "0" }, + { true, "1" }, + { true, "192" }, + { true, "255" }, + // two octets + { true, "192.168" }, + { true, "10.0" }, + // three octets + { true, "192.168.1" }, + { true, "10.0.0" }, + // four octets - full addresses + { true, "192.168.1.1" }, + { true, "0.0.0.0" }, + { true, "255.255.255.255" }, + // trailing dot (partial entry) + { true, "192." }, + { true, "192.168." }, + { true, "192.168.1." }, + // non-matching - empty + { false, string.Empty }, + // non-matching - octet out of range + { false, "256" }, + { false, "999" }, + // non-matching - five groups + { false, "192.168.0.1.5" }, + // non-matching - non-IPv4 + { false, "::" }, + { false, "potato" }, + }; + + /// Verifies that matches the expected inputs. + /// Whether the pattern should match the input. + /// The string to test against the pattern. + [Theory] + [MemberData(nameof(Ipv4OctetPartialPattern_Test_Data))] + public void Ipv4OctetPartialPattern_IsMatch_ReturnsExpected_Test(bool expected, string input) + { + // Arrange + var regex = MyRegex(); + + // Act + var result = regex.IsMatch(input); + + // Assert + Assert.Equal(expected, result); + } + + #endregion // end: Ipv4OctetPartialPattern + + #region TryIPv6FromPartial + + /// Gets theory data for . + /// Parameters: expected subnets (), partial IPv6 input string. + public static TheoryData, string> TryIPv6FromPartial_Test_Values() + { + var data = new TheoryData, string>(); + + // bad input formats + data.Add(Enumerable.Empty(), null); + data.Add(Enumerable.Empty(), string.Empty); + data.Add(Enumerable.Empty(), "potato"); + + // invalid input + data.Add(Enumerable.Empty(), ":"); + data.Add(Enumerable.Empty(), ":::"); + data.Add(Enumerable.Empty(), "0:0:0:0:0:0:0:0:0"); // too many hextets + data.Add(Enumerable.Empty(), "0:0:0:0:0:0:0:0::"); + + // invalid input, multiple "::" + data.Add(Enumerable.Empty(), "::0::"); + data.Add(Enumerable.Empty(), "0::0::0"); + data.Add(Enumerable.Empty(), "::0:0:0::"); + + // explicit valid subnets + data.Add(new[] { Subnet.Parse("::/128") }, "::/128"); + data.Add(new[] { Subnet.Parse("2001:db8:85a3:42::/64") }, "2001:db8:85a3:42::/64"); + data.Add( + new[] { Subnet.Parse("2001:0db8:85a3:0042:1000:0001:0370:7334/128") }, + "2001:0db8:85a3:0042:1000:0001:0370:7334/128" + ); + + for (var hextetCount = 0; hextetCount <= 8; hextetCount++) + { + // CodeQL [cs/stringbuilder-creation-in-loop] Intentional — each iteration builds a distinct string from scratch; sharing a StringBuilder would require resetting and complicate the per-iteration boundary + var sb = new StringBuilder(); + +#if NET48 + sb.Append(string.Join(":", Enumerable.Repeat("0", hextetCount))); +#else + sb.AppendJoin(":", Enumerable.Repeat("0", hextetCount)); +#endif + + if (hextetCount < 8) + { + sb.Append("::"); + } + + var subnets = new List(); + for (var i = 0; i <= 8 - hextetCount; i++) + { + subnets.Add(Subnet.Parse($"::/{128 - (16 * i)}")); + } + + data.Add(subnets, sb.ToString()); + } + + var hextets = "2001:0db8:85a3:0042:1000:0001:0370:7334".Split(':'); + + for (var hextetCount = 1; hextetCount <= 8; hextetCount++) + { + var subnets = new List(); + for (var i = 0; i <= 8 - hextetCount; i++) + { + var enumerable = hextets.Take(hextetCount).Select(s => new string([.. s.SkipWhile(c => c == '0')])); + var trimmedLeadingZero = string.Join(":", enumerable); + var subnet = + hextetCount < 8 + ? Subnet.Parse($"{trimmedLeadingZero}::/{128 - (16 * i)}") + : Subnet.Parse($"{trimmedLeadingZero}"); + + subnets.Add(subnet); + } + + var inputString = string.Join(":", hextets.Take(hextetCount)); + + if (hextetCount < 8) + { + data.Add(subnets, $"{inputString}::"); + data.Add(subnets, $"{inputString}:"); + } + + // A bare hextet pair without any trailing colon is treated as a partial address. + // A single bare hextet (hextets[0]) is handled separately via the bare-hex path. + if (!string.IsNullOrEmpty(inputString) && inputString != hextets[0]) + { + data.Add(subnets, inputString); + } + } + + // edge cases - CIDR boundary values + data.Add(Enumerable.Empty(), "::/129"); // prefix exceeds 128 + data.Add(new[] { Subnet.Parse("::/0") }, "::/0"); // prefix of zero + data.Add(Enumerable.Empty(), "::/"); // CIDR with missing prefix number + data.Add(Enumerable.Empty(), "::/abc"); // CIDR with non-numeric prefix + + // edge cases - collapse in middle of address + data.Add( + [ + Subnet.Parse("2001:db8:0:0:0:0:0:1/128"), + Subnet.Parse("2001:db8:0:0:0:0:1::/112"), + Subnet.Parse("2001:db8:0:0:0:1::/96"), + Subnet.Parse("2001:db8:0:0:1::/80"), + Subnet.Parse("2001:db8:0:1::/64"), + Subnet.Parse("2001:db8:1::/48"), + ], + "2001:db8::1" + ); + + // edge cases - uppercase hex (IPv6 is case-insensitive) + data.Add( + [ + Subnet.Parse("2001:db8:0:0:0:0:0:0/128"), + Subnet.Parse("2001:db8:0:0:0:0:0::/112"), + Subnet.Parse("2001:db8:0:0:0:0::/96"), + Subnet.Parse("2001:db8:0:0:0::/80"), + Subnet.Parse("2001:db8:0:0::/64"), + Subnet.Parse("2001:db8:0::/48"), + Subnet.Parse("2001:db8::/32"), + ], + "2001:DB8::" + ); + + // edge cases - IPv4 mapped IPv6 should be treated as an exact /128 + data.Add(new[] { Subnet.Parse("::ffff:192.168.0.1/128") }, "::ffff:192.168.0.1/128"); + + // edge cases - valid but incomplete, 6 hextets without "::" + data.Add( + [ + Subnet.Parse("2001:db8:0:0:0:0:0:0/128"), + Subnet.Parse("2001:db8:0:0:0:0:0::/112"), + Subnet.Parse("2001:db8:0:0:0:0::/96"), + ], + "2001:db8:0:0:0:0:" + ); + + // edge cases - single non-zero hextet with trailing "::" + data.Add( + [ + Subnet.Parse("abba:0:0:0:0:0:0:0/128"), + Subnet.Parse("abba:0:0:0:0:0:0::/112"), + Subnet.Parse("abba:0:0:0:0:0::/96"), + Subnet.Parse("abba:0:0:0:0::/80"), + Subnet.Parse("abba:0:0:0::/64"), + Subnet.Parse("abba:0:0::/48"), + Subnet.Parse("abba:0::/32"), + Subnet.Parse("abba::/16"), + ], + "abba::" + ); + + // edge cases - bare hextet pair without trailing "::" + data.Add( + [ + Subnet.Parse("2001:db8:0:0:0:0:0:0/128"), + Subnet.Parse("2001:db8:0:0:0:0:0::/112"), + Subnet.Parse("2001:db8:0:0:0:0::/96"), + Subnet.Parse("2001:db8:0:0:0::/80"), + Subnet.Parse("2001:db8:0:0::/64"), + Subnet.Parse("2001:db8:0::/48"), + Subnet.Parse("2001:db8::/32"), + ], + "2001:db8:" + ); + + // edge cases - IPv6 input that looks like a valid complete address without hextet count of 8 + data.Add(new[] { Subnet.Parse("::1/128") }, "::1/128"); + data.Add(new[] { Subnet.Parse("fe80::1/128") }, "fe80::1/128"); + + // bare hex word (no colons) - treated as single hextet with implicit "::" + data.Add( + [ + Subnet.Parse("2001:0:0:0:0:0:0:0/128"), + Subnet.Parse("2001:0:0:0:0:0:0::/112"), + Subnet.Parse("2001:0:0:0:0:0::/96"), + Subnet.Parse("2001:0:0:0:0::/80"), + Subnet.Parse("2001:0:0:0::/64"), + Subnet.Parse("2001:0:0::/48"), + Subnet.Parse("2001:0::/32"), + Subnet.Parse("2001::/16"), + ], + "2001" + ); + + // bare hex word same as trailing "::" variant + data.Add( + [ + Subnet.Parse("abba:0:0:0:0:0:0:0/128"), + Subnet.Parse("abba:0:0:0:0:0:0::/112"), + Subnet.Parse("abba:0:0:0:0:0::/96"), + Subnet.Parse("abba:0:0:0:0::/80"), + Subnet.Parse("abba:0:0:0::/64"), + Subnet.Parse("abba:0:0::/48"), + Subnet.Parse("abba:0::/32"), + Subnet.Parse("abba::/16"), + ], + "abba" + ); + + // whitespace trimming - leading/trailing spaces + data.Add(new[] { Subnet.Parse("::/128") }, " ::/128"); + data.Add(new[] { Subnet.Parse("::/128") }, "::/128 "); + data.Add(new[] { Subnet.Parse("::/128") }, " ::/128 "); + + // whitespace trimming with partial + data.Add( + [ + Subnet.Parse("2001:db8:0:0:0:0:0:0/128"), + Subnet.Parse("2001:db8:0:0:0:0:0::/112"), + Subnet.Parse("2001:db8:0:0:0:0::/96"), + Subnet.Parse("2001:db8:0:0:0::/80"), + Subnet.Parse("2001:db8:0:0::/64"), + Subnet.Parse("2001:db8:0::/48"), + Subnet.Parse("2001:db8::/32"), + ], + " 2001:db8:: " + ); + + // URL-bracketed address (brackets are stripped; result matches the unbracketed case) + data.Add( + [ + Subnet.Parse("2001:db8:0:0:0:0:0:1/128"), + Subnet.Parse("2001:db8:0:0:0:0:1::/112"), + Subnet.Parse("2001:db8:0:0:0:1::/96"), + Subnet.Parse("2001:db8:0:0:1::/80"), + Subnet.Parse("2001:db8:0:1::/64"), + Subnet.Parse("2001:db8:1::/48"), + ], + "[2001:db8::1]" + ); + + // URL-bracketed with CIDR + data.Add(new[] { Subnet.Parse("2001:db8::/32") }, "[2001:db8::/32]"); + + // URL-bracketed with whitespace + data.Add(new[] { Subnet.Parse("::/0") }, " [::/0] "); + + // bracketed partial + data.Add( + [ + Subnet.Parse("2001:db8:0:0:0:0:0:0/128"), + Subnet.Parse("2001:db8:0:0:0:0:0::/112"), + Subnet.Parse("2001:db8:0:0:0:0::/96"), + Subnet.Parse("2001:db8:0:0:0::/80"), + Subnet.Parse("2001:db8:0:0::/64"), + Subnet.Parse("2001:db8:0::/48"), + Subnet.Parse("2001:db8::/32"), + ], + "[2001:db8::]" + ); + + // invalid - unclosed bracket (no matching "]") + data.Add(Enumerable.Empty(), "[2001:db8::"); + + // invalid - unopened bracket (no matching "[") + data.Add(Enumerable.Empty(), "2001:db8::]"); + + // invalid - bare hex with colon is ambiguous + data.Add(Enumerable.Empty(), "xyz::"); + + // invalid - hex with invalid characters + data.Add(Enumerable.Empty(), "2001:db8::zzzz"); + + return data; + } + + /// Verifies that returns the expected subnets + /// for a given partial IPv6 string, in the correct order (most-specific prefix first). + /// Expected subnets in order, or an empty enumerable when the parse should fail. + /// Partial IPv6 address string to parse. + [Theory] + [MemberData(nameof(TryIPv6FromPartial_Test_Values))] + public void TryIPv6FromPartial_Test(IEnumerable expected, string input) + { + // Arrange + // Act +#pragma warning disable CS0618 // testing a known obsolete method; ignoring the fact that it is obsolete + var success = Subnet.TryIPv6FromPartial(input, out var subnets); +#pragma warning restore CS0618 + + // Assert: verify count, content, AND order (most-specific prefix first) + var expectedList = expected.ToList(); + var subnetList = subnets.ToList(); + + Assert.Equal(expectedList.Count != 0, success); + + Assert.Equal(expectedList.Count, subnetList.Count); + for (var i = 0; i < expectedList.Count; i++) + { + Assert.True(expectedList[i].Equals(subnetList[i])); + } + } + + #endregion // end: TryIPv6FromPartial + + #region Static Factory Methods + + #region FromBytes + + /// Gets theory data for . + /// Parameters: expected subnet (), low address bytes (), high address bytes (). + public static TheoryData FromBytes_Bytes_Bytes_Test_Values() + { + var data = new TheoryData(); + var seen = new HashSet(); + + foreach (var ipAddress in IPv4Addresses()) + { + for (var i = 0; i <= 32; i++) + { + var subnet = new Subnet(ipAddress, i); + var key = + $"{BitConverter.ToString(subnet.Head.GetAddressBytes())}|{BitConverter.ToString(subnet.Tail.GetAddressBytes())}"; + if (seen.Add(key)) + { + data.Add(subnet, subnet.Head.GetAddressBytes(), subnet.Tail.GetAddressBytes()); + } + } + } + + foreach (var ipAddress in IPv6Addresses()) + { + for (var i = 0; i <= 128; i++) + { + var subnet = new Subnet(ipAddress, i); + var key = + $"{BitConverter.ToString(subnet.Head.GetAddressBytes())}|{BitConverter.ToString(subnet.Tail.GetAddressBytes())}"; + if (seen.Add(key)) + { + data.Add(subnet, subnet.Head.GetAddressBytes(), subnet.Tail.GetAddressBytes()); + } + } + } + + return data; + + IEnumerable IPv4Addresses() + { + yield return IPAddress.Any; + yield return IPAddress.Loopback; + yield return IPAddress.None; + yield return IPAddress.Parse("192.168.1.1"); + } + + IEnumerable IPv6Addresses() + { + yield return IPAddress.IPv6Any; + yield return IPAddress.IPv6Loopback; + yield return IPAddress.Parse("2001:0db8:85a3:0042:1000:8a2e:0370:7334"); + } + } + + /// Verifies that produces the expected subnet from address byte arrays. + /// Expected subnet. + /// Byte array for the low address. + /// Byte array for the high address. + [Theory] + [MemberData(nameof(FromBytes_Bytes_Bytes_Test_Values))] + public void FromBytes_Bytes_Bytes_Test(Subnet expected, byte[] lowAddressBytes, byte[] highAddressBytes) + { + // Arrange + // Act + var subnet = Subnet.FromBytes(lowAddressBytes, highAddressBytes); + + // Assert + Assert.Equal(expected, subnet); + } + + /// Verifies that throws for null or empty byte arrays. + /// Low address byte array. + /// High address byte array. + [Theory] + [InlineData(null, new byte[] { 0x01, 0x01, 0xA8, 0xC0 })] + [InlineData(new byte[] { 0x01, 0x01, 0xA8, 0xC0 }, null)] + [InlineData(null, new byte[] { })] + public void FromBytes_Null_Input_Throws_ArgumentNullException_Test(byte[] lowAddressBytes, byte[] highAddressBytes) + { + // Arrange + // Act / Assert + Assert.Throws(() => Subnet.FromBytes(lowAddressBytes, highAddressBytes)); + } + + /// Verifies that throws for invalid byte array lengths. + /// Low address byte array with invalid length. + /// High address byte array with invalid length. + [Theory] + [InlineData(new byte[] { 0x01, 0x01, 0xA8, 0xC0, 0xFF }, new byte[] { 0x01, 0x01, 0xA8, 0xC0 })] + [InlineData(new byte[] { 0x01, 0x01, 0xA8, 0xC0 }, new byte[] { 0x01, 0x01, 0xA8, 0xC0, 0xFF })] + [InlineData(new byte[] { }, new byte[] { 0x01, 0x01, 0xA8, 0xC0, 0xFF })] + [InlineData(new byte[] { 0x01, 0x01, 0xA8, 0xC0, 0xFF }, new byte[] { })] + public void FromBytes_Invalid_Input_Throws_ArgumentException_Test(byte[] lowAddressBytes, byte[] highAddressBytes) + { + // Arrange + // Act + var exception = Assert.Throws(() => Subnet.FromBytes(lowAddressBytes, highAddressBytes)); + + // Assert + Assert.IsType(exception.InnerException); + } + + /// Verifies that throws when the high address is lower than the low address. + [Fact] + public void FromBytes_HighAddressLowerThanLowAddress_Throws_InvalidOperationException_Test() + { + // Arrange + var lowBytes = IPAddress.Parse("192.168.2.0").GetAddressBytes(); + var highBytes = IPAddress.Parse("192.168.1.0").GetAddressBytes(); + + // Act / Assert + Assert.Throws(() => Subnet.FromBytes(lowBytes, highBytes)); + } + + #endregion // end: FromBytes + + #region TryFromBytes + + /// Gets theory data for . + /// Parameters: expected success (bool), expected subnet ( or null), low address bytes (), high address bytes (). + public static TheoryData TryFromBytes_Bytes_Bytes_Test_Values() + { + var data = new TheoryData(); + + data.Add(false, null, null, null); + data.Add(false, null, IPAddress.Any.GetAddressBytes(), IPAddress.IPv6Any.GetAddressBytes()); + data.Add(false, null, Array.Empty(), IPAddress.IPv6Any.GetAddressBytes()); + data.Add(false, null, Array.Empty(), IPAddress.Any.GetAddressBytes()); + data.Add(false, null, IPAddress.IPv6Any.GetAddressBytes(), Array.Empty()); + data.Add(false, null, IPAddress.Any.GetAddressBytes(), Array.Empty()); + + data.Add(false, null, new byte[] { 0x00 }, IPAddress.IPv6Any.GetAddressBytes()); + data.Add(false, null, new byte[] { 0x00 }, IPAddress.Any.GetAddressBytes()); + data.Add(false, null, IPAddress.IPv6Any.GetAddressBytes(), new byte[] { 0x00 }); + data.Add(false, null, IPAddress.Any.GetAddressBytes(), new byte[] { 0x00 }); + + var seen = new HashSet(); + + foreach (var ipAddress in IPv4Addresses()) + { + for (var i = 0; i <= 32; i++) + { + var subnet = new Subnet(ipAddress, i); + var key = + $"{BitConverter.ToString(subnet.Head.GetAddressBytes())}|{BitConverter.ToString(subnet.Tail.GetAddressBytes())}"; + if (seen.Add(key)) + { + data.Add(true, subnet, subnet.Head.GetAddressBytes(), subnet.Tail.GetAddressBytes()); + } + } + } + + foreach (var ipAddress in IPv6Addresses()) + { + for (var i = 0; i <= 128; i++) + { + var subnet = new Subnet(ipAddress, i); + var key = + $"{BitConverter.ToString(subnet.Head.GetAddressBytes())}|{BitConverter.ToString(subnet.Tail.GetAddressBytes())}"; + if (seen.Add(key)) + { + data.Add(true, subnet, subnet.Head.GetAddressBytes(), subnet.Tail.GetAddressBytes()); + } + } + } + + return data; + + IEnumerable IPv4Addresses() + { + yield return IPAddress.Any; + yield return IPAddress.Loopback; + yield return IPAddress.None; + yield return IPAddress.Parse("192.168.1.1"); + } + + IEnumerable IPv6Addresses() + { + yield return IPAddress.IPv6Any; + yield return IPAddress.IPv6Loopback; + yield return IPAddress.Parse("2001:0db8:85a3:0042:1000:8a2e:0370:7334"); + } + } + + /// Verifies that returns the expected success flag and subnet. + /// Expected return value of the try method. + /// Expected subnet output when parsing succeeds. + /// Byte array for the low address. + /// Byte array for the high address. + [Theory] + [MemberData(nameof(TryFromBytes_Bytes_Bytes_Test_Values))] + public void TryFromBytes_Bytes_Bytes_Test( + bool expectedSuccess, + Subnet expectedSubnet, + byte[] lowAddressBytes, + byte[] highAddressBytes + ) + { + // Arrange + // Act + var success = Subnet.TryFromBytes(lowAddressBytes, highAddressBytes, out var subnet); + + // Assert + Assert.Equal(expectedSuccess, success); + Assert.Equal(expectedSubnet, subnet); + } + + #endregion // end: TryFromBytes + + #region Parse(string) + + /// Gets theory data for . + /// Parameters: expected subnet (), CIDR input string. + public static TheoryData Parse_String_Test_Values() + { + var data = new TheoryData(); + + foreach (var ipAddress in IPv4Addresses()) + { + for (var i = 0; i <= 32; i++) + { + var subnet = new Subnet(ipAddress, i); + data.Add(subnet, $"{ipAddress}/{i}"); + } + } + + foreach (var ipAddress in IPv6Addresses()) + { + for (var i = 0; i <= 128; i++) + { + var subnet = new Subnet(ipAddress, i); + data.Add(subnet, $"{ipAddress}/{i}"); + } + } + + return data; + + IEnumerable IPv4Addresses() + { + yield return IPAddress.Any; + yield return IPAddress.Loopback; + yield return IPAddress.None; + yield return IPAddress.Parse("192.168.1.1"); + } + + IEnumerable IPv6Addresses() + { + yield return IPAddress.IPv6Any; + yield return IPAddress.IPv6Loopback; + yield return IPAddress.Parse("2001:0db8:85a3:0042:1000:8a2e:0370:7334"); + } + } + + /// Verifies that returns the expected subnet for a CIDR string. + /// Expected subnet. + /// CIDR string to parse. + [Theory] + [MemberData(nameof(Parse_String_Test_Values))] + public void Parse_String_Test(Subnet expected, string input) + { + // Arrange + + // Act + var subnet = Subnet.Parse(input); + + // Assert + Assert.Equal(expected, subnet); + } + + /// Verifies that throws for a null input. + [Fact] + public void Parse_String_Null_Throws_ArgumentNullException_Test() + { + // Arrange + // Act / Assert + // CodeQL [cs/useless-upcast] Explicit cast to string disambiguates overloads of Subnet.Parse when passing null — required for correct overload resolution + Assert.Throws(() => Subnet.Parse((string)null)); + } + + /// Verifies that throws for whitespace-only input. + /// Whitespace input string. + [Theory] + [InlineData("")] + [InlineData(" ")] + public void Parse_String_WhiteSpace_Throws_ArgumentException_Test(string input) + { + // Arrange + // Act / Assert + Assert.Throws(() => Subnet.Parse(input)); + } + + /// Verifies that throws for a badly formatted string. + [Fact] + public void Parse_String_BadFormat_Throws_FormatException_Test() + { + // Arrange + // Act / Assert + Assert.Throws(() => Subnet.Parse("potato")); + } + + /// Verifies that throws when the IPv4 routing prefix exceeds 32. + [Fact] + public void Parse_String_IPv4RoutingPrefixTooLarge_Throws_ArgumentException_Test() + { + // Arrange + // Act / Assert + Assert.Throws(() => Subnet.Parse("192.168.1.0/33")); + } + + /// Verifies that throws when the IPv6 routing prefix exceeds 128. + [Fact] + public void Parse_String_IPv6RoutingPrefixTooLarge_Throws_ArgumentException_Test() + { + // Arrange + // Act / Assert + Assert.Throws(() => Subnet.Parse("::/129")); + } + + #region S1 - Parse whitespace handling + + /// Verifies that succeeds with leading whitespace. + [Fact] + public void Parse_WithLeadingWhitespace_Succeeds() + { + var subnet = Subnet.Parse(" 192.168.1.0/24"); + Assert.Equal(Subnet.Parse("192.168.1.0/24"), subnet); + } + + /// Verifies that succeeds with trailing whitespace. + [Fact] + public void Parse_WithTrailingWhitespace_Succeeds() + { + var subnet = Subnet.Parse("192.168.1.0/24 "); + Assert.Equal(Subnet.Parse("192.168.1.0/24"), subnet); + } + + /// Verifies that succeeds with leading newline. + [Fact] + public void Parse_WithLeadingNewline_Succeeds() + { + var subnet = Subnet.Parse("\n10.0.0.0/8"); + Assert.Equal(Subnet.Parse("10.0.0.0/8"), subnet); + } + + /// Verifies that fails with internal whitespace. + [Fact] + public void Parse_WithInternalWhitespace_Fails() + { + Assert.Throws(() => Subnet.Parse("192.168. 1.0/24")); + } + + #endregion // end: S1 - Parse whitespace handling + + #region S5 - Negative routing prefix + + /// Verifies that throws for a negative routing prefix. + [Fact] + public void Parse_WithNegativeRoutingPrefix_ThrowsFormatException() + { + Assert.Throws(() => Subnet.Parse("192.168.1.0/-4")); + } + + /// Verifies that throws a with a clear message for a negative prefix. + [Fact] + public void Parse_WithNegativeRoutingPrefix_MessageIsClear() + { + var ex = Assert.Throws(() => Subnet.Parse("192.168.1.0/-4")); + Assert.Contains("negative", ex.Message, StringComparison.OrdinalIgnoreCase); + } + + #endregion // end: S5 - Negative routing prefix + + #endregion // end: Parse(string) + + #region RoughSubnetStringPattern + + /// Gets theory data for . + /// Parameters: expected match result (bool), input string to test against the pattern. + public static TheoryData RoughSubnetStringPattern_Test_Data => + new() + { + // IPv4 CIDR notation + { true, "192.168.1.0/24" }, + { true, "0.0.0.0/0" }, + { true, "255.255.255.255/32" }, + // IPv6 CIDR notation + { true, "::/0" }, + { true, "2001:db8::/32" }, + // case-insensitive (IgnoreCase) + { true, "FEED:BEEF::/48" }, + { true, "feed:beef::/48" }, + // address only - no prefix + { true, "192.168.1.1" }, + { true, "::" }, + { true, "2001:db8::1" }, + // non-matching - leading slash (no address part) + { false, "/32" }, + // non-matching - empty string + { false, string.Empty }, + // non-matching - internal space + { false, "192.168.1.0 /24" }, + }; + + /// Verifies that matches the expected inputs. + /// Whether the pattern should produce exactly one match. + /// The string to test against the pattern. + [Theory] + [MemberData(nameof(RoughSubnetStringPattern_Test_Data))] + public void RoughSubnetStringPattern_Matches_ReturnsExpected_Test(bool expected, string input) + { + // Arrange + // Production RoughSubnetRegex options (keep in sync): + // netstandard2.0: RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase + // Other TFMs: [GeneratedRegex(RoughSubnetStringPattern, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase)] + var regex = new Regex(Subnet.RoughSubnetStringPattern, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); + + // Act +#if NET48 + var result = regex.Matches(input).Count == 1; +#else + var result = regex.Count(input) == 1; +#endif + + // Assert + Assert.Equal(expected, result); + } + + #endregion // end: RoughSubnetStringPattern + + #region TryParse(string) + + /// Gets theory data for . + /// Parameters: expected subnet ( or null), CIDR input string. + public static TheoryData TryParse_String_Test_Values() + { + var data = new TheoryData(); + + data.Add(null, null); + data.Add(null, string.Empty); + data.Add(null, "potato"); + data.Add(null, "2001:0db8:85a3:0042:1000:8a2e:0370:7334/129"); + data.Add(null, "0.0.0.0/33"); + data.Add(null, "0.0.0.0/potato"); + data.Add(null, "potato/16"); + data.Add(null, "0.0.0.0/"); + data.Add(null, "/32"); + + foreach (var ipAddress in IPv4Addresses()) + { + for (var i = 0; i <= 32; i++) + { + var subnet = new Subnet(ipAddress, i); + data.Add(subnet, $"{ipAddress}/{i}"); + } + + data.Add(new Subnet(ipAddress, ipAddress), ipAddress.ToString()); + } + + foreach (var ipAddress in IPv6Addresses()) + { + for (var i = 0; i <= 128; i++) + { + var subnet = new Subnet(ipAddress, i); + data.Add(subnet, $"{ipAddress}/{i}"); + } + + data.Add(new Subnet(ipAddress, ipAddress), ipAddress.ToString()); + } + + return data; + + IEnumerable IPv4Addresses() + { + yield return IPAddress.Any; + yield return IPAddress.Loopback; + yield return IPAddress.None; + yield return IPAddress.Parse("192.168.1.1"); + } + + IEnumerable IPv6Addresses() + { + yield return IPAddress.IPv6Any; + yield return IPAddress.IPv6Loopback; + yield return IPAddress.Parse("2001:0db8:85a3:0042:1000:8a2e:0370:7334"); + } + } + + /// Verifies that returns the expected success flag and subnet. + /// Expected subnet output when parsing succeeds. + /// CIDR string to parse. + [Theory] + [MemberData(nameof(TryParse_String_Test_Values))] + public void TryParse_String_Test(Subnet expected, string input) + { + // Arrange + + // Act + var success = Subnet.TryParse(input, out var subnet); + + // Assert + Assert.Equal(expected != null, success); + Assert.Equal(expected, subnet); + } + + #endregion // end: TryParse(string) + + #region Parse(string, int, int) + + /// Gets theory data for . + /// Parameters: expected subnet (), address string, routing prefix (int). + public static TheoryData Parse_String_Int_Test_Values() + { + var data = new TheoryData(); + + foreach (var ipAddress in IPv4Addresses()) + { + for (var i = 0; i <= 32; i++) + { + var subnet = new Subnet(ipAddress, i); + data.Add(subnet, ipAddress.ToString(), i); + } + } + + foreach (var ipAddress in IPv6Addresses()) + { + for (var i = 0; i <= 128; i++) + { + var subnet = new Subnet(ipAddress, i); + data.Add(subnet, ipAddress.ToString(), i); + } + } + + return data; + + IEnumerable IPv4Addresses() + { + yield return IPAddress.Any; + yield return IPAddress.Loopback; + yield return IPAddress.None; + yield return IPAddress.Parse("192.168.1.1"); + } + + IEnumerable IPv6Addresses() + { + yield return IPAddress.IPv6Any; + yield return IPAddress.IPv6Loopback; + yield return IPAddress.Parse("2001:0db8:85a3:0042:1000:8a2e:0370:7334"); + } + } + + /// Verifies that returns the expected subnet for an address string and routing prefix. + /// Expected subnet. + /// IP address string. + /// Routing prefix length. + [Theory] + [MemberData(nameof(Parse_String_Int_Test_Values))] + public void Parse_String_Int_Test(Subnet expected, string addressString, int routePrefix) + { + // Arrange + // Act + var subnet = Subnet.Parse(addressString, routePrefix); + + // Assert + Assert.Equal(expected, subnet); + } + + /// Verifies that throws for a null address string. + [Fact] + public void Parse_String_Int_NullAddressString_Throws_ArgumentNullException_Test() + { + // Arrange + // Act / Assert + Assert.Throws(() => Subnet.Parse((string)null, 24)); + } + + /// Verifies that throws for a badly formatted address string. + [Fact] + public void Parse_String_Int_BadAddressFormat_Throws_FormatException_Test() + { + // Arrange + // Act / Assert + Assert.Throws(() => Subnet.Parse("potato", 24)); + } + + /// Verifies that throws when the routing prefix is out of range. + /// IP address string. + /// The out-of-range routing prefix. + [Theory] + [InlineData("192.168.1.0", -1)] + [InlineData("192.168.1.0", 33)] + [InlineData("::", -1)] + [InlineData("::", 129)] + public void Parse_String_Int_RoutingPrefixOutOfRange_Throws_ArgumentOutOfRangeException_Test( + string addressString, + int routingPrefix + ) + { + // Arrange + // Act / Assert + Assert.Throws(() => Subnet.Parse(addressString, routingPrefix)); + } + + #endregion // end: Parse(string, int, int) + + #region TryParse(string, int, int) + + /// Gets theory data for . + /// Parameters: expected success (bool), expected subnet ( or null), address string, routing prefix (int). + public static TheoryData TryParse_String_Int_Test_Values() + { + var data = new TheoryData(); + + data.Add(false, null, null, 0); + data.Add(false, null, "potato", 0); + data.Add(false, null, IPAddress.Any.ToString(), -5); + data.Add(false, null, IPAddress.IPv6Any.ToString(), -5); + data.Add(false, null, IPAddress.Any.ToString(), 33); + data.Add(false, null, IPAddress.IPv6Any.ToString(), 129); + + foreach (var ipAddress in IPv4Addresses()) + { + for (var i = 0; i <= 32; i++) + { + var subnet = new Subnet(ipAddress, i); + data.Add(true, subnet, ipAddress.ToString(), i); + } + } + + foreach (var ipAddress in IPv6Addresses()) + { + for (var i = 0; i <= 128; i++) + { + var subnet = new Subnet(ipAddress, i); + data.Add(true, subnet, ipAddress.ToString(), i); + } + } + + return data; + + IEnumerable IPv4Addresses() + { + yield return IPAddress.Any; + yield return IPAddress.Loopback; + yield return IPAddress.None; + yield return IPAddress.Parse("192.168.1.1"); + } + + IEnumerable IPv6Addresses() + { + yield return IPAddress.IPv6Any; + yield return IPAddress.IPv6Loopback; + yield return IPAddress.Parse("2001:0db8:85a3:0042:1000:8a2e:0370:7334"); + } + } + + /// Verifies that returns the expected success flag and subnet. + /// Expected return value of the try method. + /// Expected subnet output when parsing succeeds. + /// IP address string. + /// Routing prefix length. + [Theory] + [MemberData(nameof(TryParse_String_Int_Test_Values))] + public void TryParse_String_Int_Test(bool expectedSuccess, Subnet expectedSubnet, string addressString, int routePrefix) + { + // Arrange + // Act + var success = Subnet.TryParse(addressString, routePrefix, out var subnet); + + // Assert + Assert.Equal(expectedSuccess, success); + Assert.Equal(expectedSubnet, subnet); + } + + #endregion // end: TryParse(string, int, int) + + #region Parse(string, string, int) + + /// Gets theory data for . + /// Parameters: expected subnet (), low address string, high address string. + public static TheoryData Parse_String_String_Test_Values() + { + var data = new TheoryData(); + var seen = new HashSet(); + + foreach (var ipAddress in IPv4Addresses()) + { + for (var i = 0; i <= 32; i++) + { + var subnet = new Subnet(ipAddress, i); + var low = subnet.Head.ToString(); + var high = subnet.Tail.ToString(); + if (seen.Add($"{low}|{high}")) + { + data.Add(subnet, low, high); + } + } + } + + foreach (var ipAddress in IPv6Addresses()) + { + for (var i = 0; i <= 128; i++) + { + var subnet = new Subnet(ipAddress, i); + var low = subnet.Head.ToString(); + var high = subnet.Tail.ToString(); + if (seen.Add($"{low}|{high}")) + { + data.Add(subnet, low, high); + } + } + } + + return data; + + IEnumerable IPv4Addresses() + { + yield return IPAddress.Any; + yield return IPAddress.Loopback; + yield return IPAddress.None; + yield return IPAddress.Parse("192.168.1.1"); + } + + IEnumerable IPv6Addresses() + { + yield return IPAddress.IPv6Any; + yield return IPAddress.IPv6Loopback; + yield return IPAddress.Parse("2001:0db8:85a3:0042:1000:8a2e:0370:7334"); + } + } + + /// Verifies that returns the expected subnet for low and high address strings. + /// Expected subnet. + /// Low address string. + /// High address string. + [Theory] + [MemberData(nameof(Parse_String_String_Test_Values))] + public void Parse_String_String_Test(Subnet expected, string low, string high) + { + // Arrange + // Act + var subnet = Subnet.Parse(low, high); + + // Assert + Assert.Equal(expected, subnet); + } + + /// Verifies that throws when either address string is null. + /// Low address string. + /// High address string. + [Theory] + [InlineData("::", null)] + [InlineData(null, "::")] + [InlineData("192.168.1.1", null)] + [InlineData(null, "192.168.1.1")] + [InlineData(null, null)] + public void Parse_String_String_NullAddressString_Throws_ArgumentNullException_Test(string low, string high) + { + // Arrange + // Act / Assert + Assert.Throws(() => Subnet.Parse(low, high)); + } + + /// Verifies that throws for badly formatted address strings. + /// Low address string. + /// High address string. + [Theory] + [InlineData("::", "potato")] + [InlineData("potato", "::")] + [InlineData("192.168.1.1", "potato")] + [InlineData("potato", "192.168.1.1")] + public void Parse_String_String_BadAddressFormat_Throws_FormatException_Test(string low, string high) + { + // Arrange + // Act / Assert + Assert.Throws(() => Subnet.Parse(low, high)); + } + + /// Verifies that throws when the address strings are from different address families. + /// Low address string. + /// High address string of a different family. + [Theory] + [InlineData("192.168.1.1", "::")] + [InlineData("::", "192.168.1.1")] + public void Parse_String_String_MisMatchAddressFamily_Throws_ArgumentException_Test(string low, string high) + { + // Arrange + // Act / Assert + Assert.Throws(() => Subnet.Parse(low, high)); + } + + /// Verifies that throws when the low address is greater than the high address. + /// Low address string that is actually higher. + /// High address string that is actually lower. + [Theory] + [InlineData("192.168.1.32", "192.168.1.0")] + [InlineData("::20", "::")] + public void Parse_String_String_InvalidRange_Throws_InvalidOperationException_Test(string low, string high) + { + // Arrange + // Act / Assert + Assert.Throws(() => Subnet.Parse(low, high)); + } + + #endregion // end: Parse(string, string, int) + + #region TryParse(string, string, int) + + /// Gets theory data for . + /// Parameters: expected subnet ( or null), low address string, high address string. + public static TheoryData TryParse_String_String_Test_Values() + { + var data = new TheoryData(); + + foreach (var s in new[] { null, string.Empty, "\t", "potato" }) + { + data.Add(null, "192.168.1.1", s); + data.Add(null, s, "192.168.1.1"); + data.Add(null, "2001:0db8:85a3:0042:1000:8a2e:0370:7334", s); + data.Add(null, s, "2001:0db8:85a3:0042:1000:8a2e:0370:7334"); + } + + var seen = new HashSet(); + + foreach (var ipAddress in IPv4Addresses()) + { + for (var i = 0; i <= 32; i++) + { + var subnet = new Subnet(ipAddress, i); + var low = subnet.Head.ToString(); + var high = subnet.Tail.ToString(); + if (seen.Add($"{low}|{high}")) + { + data.Add(subnet, low, high); + } + } + } + + foreach (var ipAddress in IPv6Addresses()) + { + for (var i = 0; i <= 128; i++) + { + var subnet = new Subnet(ipAddress, i); + var low = subnet.Head.ToString(); + var high = subnet.Tail.ToString(); + if (seen.Add($"{low}|{high}")) + { + data.Add(subnet, low, high); + } + } + } + + return data; + + IEnumerable IPv4Addresses() + { + yield return IPAddress.Any; + yield return IPAddress.Loopback; + yield return IPAddress.None; + yield return IPAddress.Parse("192.168.1.1"); + } + + IEnumerable IPv6Addresses() + { + yield return IPAddress.IPv6Any; + yield return IPAddress.IPv6Loopback; + yield return IPAddress.Parse("2001:0db8:85a3:0042:1000:8a2e:0370:7334"); + } + } + + /// Verifies that returns the expected success flag and subnet. + /// Expected subnet output when parsing succeeds. + /// Low address string. + /// High address string. + [Theory] + [MemberData(nameof(TryParse_String_String_Test_Values))] + public void TryParse_String_String_Test(Subnet expected, string low, string high) + { + // Arrange + + // Act + var success = Subnet.TryParse(low, high, out var subnet); + + // Assert + Assert.Equal(expected != null, success); + Assert.Equal(expected, subnet); + } + + #endregion // end: TryParse(string, string, int) + + #endregion // end: Static Factory Methods + + #region FromNetMask + + /// Gets theory data for . + /// Parameters: expected subnet (), network prefix address (), netmask address (). + public static TheoryData FromNetMask_Test_Values() + { + var data = new TheoryData(); + var networkPrefix = IPAddress.Parse("192.168.1.1"); + + for (var i = 0; i <= 32; i++) + { + var netmaskBytes = BigEndianBitWrapper.CreateMask(4, i).ToBytes(); + + var netmask = new IPAddress(netmaskBytes); + + var expected = new Subnet(networkPrefix, i); + + data.Add(expected, networkPrefix, netmask); + } + + return data; + } + + /// Verifies that returns the expected subnet for a network prefix and netmask. + /// Expected subnet. + /// Network prefix address. + /// Netmask address. + [Theory] + [MemberData(nameof(FromNetMask_Test_Values))] + public void FromNetMask_Test(Subnet expected, IPAddress networkPrefix, IPAddress netmask) + { + // Arrange + // Act + var result = Subnet.FromNetMask(networkPrefix, netmask); + + // Assert + Assert.Equal(expected, result); + Assert.Equal(netmask, result.Netmask); + } + + /// Verifies that throws when the address is null. + [Fact] + public void FromNetMask_NullAddress_Throws_ArgumentNullException_Test() + { + // Arrange + // Act / Assert + Assert.Throws(() => Subnet.FromNetMask(null, IPAddress.Any)); + } + + /// Verifies that throws when the netmask is null. + [Fact] + public void FromNetMask_NullNetMask_Throws_ArgumentNullException_Test() + { + // Arrange + // Act / Assert + Assert.Throws(() => Subnet.FromNetMask(IPAddress.Any, null)); + } + + /// Verifies that throws when the netmask is invalid. + [Fact] + public void FromNetMask_InvalidNetMask_Throws_ArgumentException_Test() + { + // Arrange + // Act / Assert + Assert.Throws(() => Subnet.FromNetMask(IPAddress.Any, IPAddress.IPv6Any)); + } + + /// Verifies that throws when the netmask is an IPv4 address with non-contiguous 1-bits. + [Fact] + public void FromNetMask_NonContiguousNetMask_Throws_ArgumentException_Test() + { + // Arrange - 255.0.255.0 has non-contiguous 1-bits (gap in the second octet) + var invalidNetmask = new IPAddress(new byte[] { 0xff, 0x00, 0xff, 0x00 }); + + // Act / Assert + Assert.Throws(() => Subnet.FromNetMask(IPAddress.Any, invalidNetmask)); + } + + /// Verifies that throws when the address is IPv6. + [Fact] + public void FromNetMask_IPv6Address_Throws_ArgumentException_Test() + { + // Arrange + // Act / Assert + Assert.Throws(() => Subnet.FromNetMask(IPAddress.IPv6Any, IPAddress.Any)); + } + + /// Gets theory data for . + /// Parameters: expected success (bool), expected subnet ( or null), network prefix address (), netmask address (). + public static TheoryData TryFromNetMask_Test_Values() + { + var data = new TheoryData(); + var networkPrefix = IPAddress.Parse("192.168.1.1"); + + for (var i = 0; i <= 32; i++) + { + var netmaskBytes = BigEndianBitWrapper.CreateMask(4, i).ToBytes(); + + var netmask = new IPAddress(netmaskBytes); + + var expected = new Subnet(networkPrefix, i); + + data.Add(true, expected, networkPrefix, netmask); + } + + data.Add(false, null, null, IPAddress.Parse("192.168.1.1")); + data.Add(false, null, IPAddress.Parse("192.168.1.1"), null); + data.Add(false, null, null, IPAddress.Parse("::")); + data.Add(false, null, IPAddress.Parse("::"), null); + data.Add(false, null, null, null); + + return data; + } + + /// Verifies that returns the expected success flag and subnet. + /// Expected return value of the try method. + /// Expected subnet output when parsing succeeds. + /// Network prefix address. + /// Netmask address. + [Theory] + [MemberData(nameof(TryFromNetMask_Test_Values))] + public void TryFromNetMask_Test(bool expectedSuccess, Subnet expectedSubnet, IPAddress networkPrefix, IPAddress netmask) + { + // Arrange + // Act + var success = Subnet.TryFromNetMask(networkPrefix, netmask, out var subnet); + + // Assert + Assert.Equal(expectedSuccess, success); + Assert.Equal(expectedSubnet, subnet); + } + + #endregion // end: FromNetMask + + #region maxEnumerationExponent propagation + + /// Verifies that propagates maxEnumerationExponent. + [Fact] + public void FromNetMask_ExponentPropagates_Test() + { + var subnet = Subnet.FromNetMask( + IPAddress.Parse("192.168.0.0"), + IPAddress.Parse("255.255.0.0"), + maxEnumerationExponent: 4 + ); + Assert.Equal(4, subnet.MaxEnumerationExponent); + } + + /// Verifies that propagates maxEnumerationExponent. + [Fact] + public void TryFromNetMask_ExponentPropagates_Test() + { + Subnet.TryFromNetMask( + IPAddress.Parse("192.168.0.0"), + IPAddress.Parse("255.255.0.0"), + out var subnet, + maxEnumerationExponent: 4 + ); + Assert.Equal(4, subnet.MaxEnumerationExponent); + } + + /// Verifies that propagates maxEnumerationExponent. + [Fact] + public void FromBytes_ExponentPropagates_Test() + { + var low = new byte[] { 10, 0, 0, 0 }; + var high = new byte[] { 10, 0, 0, 255 }; + var subnet = Subnet.FromBytes(low, high, maxEnumerationExponent: 4); + Assert.Equal(4, subnet.MaxEnumerationExponent); + } + + /// Verifies that propagates maxEnumerationExponent. + [Fact] + public void TryFromBytes_ExponentPropagates_Test() + { + var low = new byte[] { 10, 0, 0, 0 }; + var high = new byte[] { 10, 0, 0, 255 }; + Subnet.TryFromBytes(low, high, out var subnet, maxEnumerationExponent: 4); + Assert.Equal(4, subnet.MaxEnumerationExponent); + } + + /// Verifies that propagates maxEnumerationExponent. + [Fact] + public void Parse_StringInt_ExponentPropagates_Test() + { + var subnet = Subnet.Parse("10.0.0.0", 24, maxEnumerationExponent: 4); + Assert.Equal(4, subnet.MaxEnumerationExponent); + } + + /// Verifies that propagates maxEnumerationExponent. + [Fact] + public void Parse_StringString_ExponentPropagates_Test() + { + var subnet = Subnet.Parse("10.0.0.0", "10.0.0.255", maxEnumerationExponent: 4); + Assert.Equal(4, subnet.MaxEnumerationExponent); + } + + /// Verifies that propagates maxEnumerationExponent. + [Fact] + public void TryParse_StringInt_ExponentPropagates_Test() + { + Subnet.TryParse("10.0.0.0", 24, out var subnet, maxEnumerationExponent: 4); + Assert.Equal(4, subnet.MaxEnumerationExponent); + } + + /// Verifies that propagates maxEnumerationExponent. + [Fact] + public void TryParse_StringString_ExponentPropagates_Test() + { + Subnet.TryParse("10.0.0.0", "10.0.0.255", out var subnet, maxEnumerationExponent: 4); + Assert.Equal(4, subnet.MaxEnumerationExponent); + } + + /// Verifies that propagates maxEnumerationExponent. + [Fact] + public void TryIPv4FromPartial_ExponentPropagates_Test() + { + Subnet.TryIPv4FromPartial("192.168", out var subnet, maxEnumerationExponent: 4); + Assert.Equal(4, subnet.MaxEnumerationExponent); + } + + /// Verifies that propagates maxEnumerationExponent. + [Fact] + public void TryIPv6FromPartial_ExponentPropagates_Test() + { +#pragma warning disable CS0618 // Type or member is obsolete + Subnet.TryIPv6FromPartial("2001:db8::", out var subnets, maxEnumerationExponent: 4); +#pragma warning restore CS0618 + foreach (var subnet in subnets) + { + Assert.Equal(4, subnet.MaxEnumerationExponent); + } + } + +#if NET48 + private static readonly Regex MyRegexField = new( + Subnet.Ipv4OctetPartialPattern, + RegexOptions.Compiled | RegexOptions.CultureInvariant + ); + + private static Regex MyRegex() => MyRegexField; +#else + [GeneratedRegex(Subnet.Ipv4OctetPartialPattern, RegexOptions.CultureInvariant)] + private static partial Regex MyRegex(); +#endif + + #endregion // end: maxEnumerationExponent propagation + } +} diff --git a/src/Arcus.Tests/SubnetTests.IComparable.cs b/src/Arcus.Tests/SubnetTests.IComparable.cs new file mode 100644 index 0000000..6f4ec7f --- /dev/null +++ b/src/Arcus.Tests/SubnetTests.IComparable.cs @@ -0,0 +1,64 @@ +namespace Arcus.Tests +{ + /// + /// tests for and + /// + public partial class SubnetTests + { + /// Verifies that returns a value whose sign matches the expected ordering. + /// Expected sign of the comparison result. + /// The left-hand subnet. + /// The right-hand subnet. + [Theory] + [MemberData(nameof(Comparison_Values))] + public void CompareTo_Subnet_ReturnsExpectedSign_Test(int expected, Subnet left, Subnet right) + { + // Arrange + // Act + var result = left.CompareTo(right); + + // Assert + Assert.Equal(expected, result); + } + + /// Verifies that throws when the argument is not a . + [Fact] + public void CompareTo_Object_NonSubnet_Throws_ArgumentException_Test() + { + // Arrange + var subnet = Subnet.Parse("192.168.0.0/16"); + + // Act / Assert + Assert.Throws(() => subnet.CompareTo("not a subnet")); + } + + /// Verifies that returns a positive value when compared to null. + [Fact] + public void CompareTo_Object_Null_ReturnsPositive_Test() + { + // Arrange + var subnet = Subnet.Parse("192.168.0.0/16"); + + // Act + var result = subnet.CompareTo((object)null); + + // Assert + Assert.Equal(1, result); + } + + /// Verifies that returns zero when compared to a boxed equivalent subnet. + [Fact] + public void CompareTo_Object_SubnetBox_ReturnsZero_Test() + { + // Arrange + var subnet = Subnet.Parse("192.168.0.0/16"); + object boxed = Subnet.Parse("192.168.0.0/16"); + + // Act + var result = subnet.CompareTo(boxed); + + // Assert + Assert.Equal(0, result); + } + } +} diff --git a/src/Arcus.Tests/SubnetTests.IEquatable.cs b/src/Arcus.Tests/SubnetTests.IEquatable.cs new file mode 100644 index 0000000..631096b --- /dev/null +++ b/src/Arcus.Tests/SubnetTests.IEquatable.cs @@ -0,0 +1,236 @@ +using System.Net; + +namespace Arcus.Tests +{ + /// + /// tests for + /// + public partial class SubnetTests + { + #region Equals(Subnet) + + /// Gets theory data for and . + /// Parameters: expected equality result (bool), first subnet (), second subnet (). + public static TheoryData Equals_Subnet_Test_Values() + { + var data = new TheoryData(); + var seen = new HashSet(); + + PopulateV4Entries(data, seen); + PopulateV6Entries(data, seen); + + return data; + } + + private static void PopulateV4Entries(TheoryData data, HashSet seen) + { + foreach ( + var ipAddress in new[] { IPAddress.Any, IPAddress.Loopback, IPAddress.None, IPAddress.Parse("192.168.1.1") } + ) + { + for (var i = 0; i <= 32; i++) + { + var sA = new Subnet(ipAddress, i); + var sB = new Subnet(ipAddress, (i + 2) % 32); + var sv6A = new Subnet(IPAddress.IPv6Any, i); + var sv6B = new Subnet(IPAddress.IPv6Loopback, i); + + AddIfNew(data, seen, true, sA, sA); + AddIfNew(data, seen, false, sA, sB); + AddIfNew(data, seen, false, sA, sv6A); + AddIfNew(data, seen, false, sA, sv6B); + } + } + } + + private static void PopulateV6Entries(TheoryData data, HashSet seen) + { + foreach ( + var ipAddress in new[] + { + IPAddress.IPv6Any, + IPAddress.IPv6Loopback, + IPAddress.Parse("2001:0db8:85a3:0042:1000:8a2e:0370:7334"), + } + ) + { + for (var i = 0; i <= 128; i++) + { + var sA = new Subnet(ipAddress, i); + var sB = new Subnet(ipAddress, (i + 2) % 128); + + AddIfNew(data, seen, true, sA, sA); + AddIfNew(data, seen, false, sA, sB); + } + } + } + + private static void AddIfNew( + TheoryData data, + HashSet seen, + bool expected, + Subnet a, + Subnet b + ) + { + var key = $"{(expected ? 'T' : 'F')}|{a.ToString("f", null)}|{b.ToString("f", null)}"; + if (seen.Add(key)) + { + data.Add(expected, a, b); + } + } + + /// Verifies that returns the expected equality result. + /// Expected equality result. + /// The subject subnet. + /// The subnet to compare against. + [Theory] + [MemberData(nameof(Equals_Subnet_Test_Values))] + public void Equals_Subnet_Test(bool expected, Subnet subnetA, Subnet subnetB) + { + // Arrange + + // Act + var result = subnetA.Equals(subnetB); + + // Assert + Assert.Equal(expected, result); + } + + /// Verifies that returns when the argument is null. + [Fact] + public void Equals_Subnet_NullOther_ReturnsFalse_Test() + { + // Arrange + var subnet = Subnet.Parse("192.168.0.0/16"); + + // Act + var result = subnet.Equals((Subnet)null); + + // Assert + Assert.False(result); + } + + #endregion // end: Equals(Subnet) + + #region Equals(object) + + /// Verifies that returns the expected equality result when the argument is a boxed subnet. + /// Expected equality result. + /// The subject subnet. + /// The boxed subnet to compare against. + [Theory] + [MemberData(nameof(Equals_Subnet_Test_Values))] + public void Equals_Object_BoxedSubnet_Test(bool expected, Subnet subnetA, object subnetB) + { + // Arrange + + // Act + var result = subnetA.Equals(subnetB); + + // Assert + Assert.Equal(expected, result); + } + + /// Verifies that returns when the argument is not a . + [Fact] + public void Equals_Object_NonSubnetType_ReturnsFalse_Test() + { + // Arrange + var subnet = Subnet.Parse("192.168.0.0/16"); + + // Act + // codeql[cs/equals-on-unrelated-types] Intentionally verifying that Equals(object) returns false when comparing to a different type — correct behavior + var result = subnet.Equals("not a subnet"); + + // Assert + Assert.False(result); + } + + /// Verifies that returns when the argument is null. + [Fact] + public void Equals_Object_Null_ReturnsFalse_Test() + { + // Arrange + var subnet = Subnet.Parse("192.168.0.0/16"); + + // Act + var result = subnet.Equals((object)null); + + // Assert + Assert.False(result); + } + + #endregion // end: Equals(object) + + #region GetHashCode + + /// Gets theory data for . + /// Parameters: expected hash equality (bool), left subnet (), right subnet (). + public static TheoryData GetHashCode_Test_Values() + { + return new TheoryData + { + // value-equal IPv4 + { true, new Subnet(IPAddress.Any, 16), new Subnet(IPAddress.Any, 16) }, + // value-equal IPv6 + { true, new Subnet(IPAddress.IPv6Any, 16), new Subnet(IPAddress.IPv6Any, 16) }, + // expected different + { false, new Subnet(IPAddress.Any, 16), new Subnet(IPAddress.IPv6Any, 16) }, + { false, new Subnet(IPAddress.Any, 8), new Subnet(IPAddress.Any, 16) }, + { false, new Subnet(IPAddress.IPv6Any, 8), new Subnet(IPAddress.IPv6Any, 16) }, + { false, new Subnet(IPAddress.Any, 16), new Subnet(IPAddress.Broadcast, 16) }, + { false, new Subnet(IPAddress.Parse("::")), new Subnet(IPAddress.Parse("ab::"), 16) }, + }; + } + + /// Verifies that returns equal hashes for value-equal subnets and distinct hashes for different subnets. + /// Whether the hash codes are expected to be equal. + /// The left-hand subnet. + /// The right-hand subnet. + [Theory] + [MemberData(nameof(GetHashCode_Test_Values))] + public void GetHashCode_Test(bool expectedEqual, Subnet left, Subnet right) + { + // Arrange + // Act + var leftHash = left.GetHashCode(); + var rightHash = right.GetHashCode(); + + // Assert + Assert.Equal(expectedEqual, leftHash == rightHash); + } + + #endregion // end: GetHashCode + + #region Cross-type hash unification + + /// + /// Verifies that a and an describing the + /// same Head+Tail produce identical hash codes. This is the contract established by + /// which both subclasses delegate to. + /// + /// + /// R1 (MEDIUM) — Batch 7 equality unification. The hash *function* for + /// changed from HashCode.Combine(Head, RoutingPrefix) to the shared + /// HashCode.Combine(Head, Tail), so a CIDR subnet and an equivalent inclusive range + /// now collide in hash buckets. + /// + [Fact] + public void Subnet_GetHashCode_Matches_IPAddressRange_GetHashCode_ForSameRange() + { + // Arrange + var sub = new Subnet(IPAddress.Parse("192.168.1.0"), 24); + var rng = new IPAddressRange(sub.Head, sub.Tail); + + // Act + var subHash = sub.GetHashCode(); + var rngHash = rng.GetHashCode(); + + // Assert + Assert.Equal(subHash, rngHash); + } + + #endregion // end: Cross-type hash unification + } +} diff --git a/src/Arcus.Tests/SubnetTests.ISerializable.cs b/src/Arcus.Tests/SubnetTests.ISerializable.cs new file mode 100644 index 0000000..4e63eb1 --- /dev/null +++ b/src/Arcus.Tests/SubnetTests.ISerializable.cs @@ -0,0 +1,152 @@ +#if NET48 // maintained for .NET 4.8 compatibility +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Runtime.Serialization; +using System.Runtime.Serialization.Formatters.Binary; +using Arcus; +using Xunit; + +namespace Arcus.Tests +{ + /// + /// tests for + /// + public partial class SubnetTests + { + /// Gets theory data for . + /// Parameters: subnet to serialize (). + public static TheoryData CanSerializable_Test_Values() + { + return new TheoryData + { + { new Subnet(IPAddress.Parse("192.168.1.0")) }, + { new Subnet(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.255")) }, + { new Subnet(IPAddress.Parse("::"), IPAddress.Parse("::FFFF")) }, + }; + } + + /// Verifies that a can be round-trip serialized and deserialized using . + /// The subnet to serialize. + [Theory] + [MemberData(nameof(CanSerializable_Test_Values))] + public void CanSerializable_Test(Subnet subnet) + { + // Arrange + var formatter = new BinaryFormatter(); + + // Act + using (var writeStream = new MemoryStream()) + { + formatter.Serialize(writeStream, subnet); + writeStream.Seek(0, SeekOrigin.Begin); + + // Deserialize the object from the stream + var result = formatter.Deserialize(writeStream); + + // Assert + var actual = Assert.IsType(result); + + // using explicit EqualityComparer to avoid comparing elements of enumerable + Assert.Equal(subnet, actual, SubnetEqualityComparer.Instance); + } + } + + /// + /// S3 - writes an entry named + /// NetworkPrefixAddress and does NOT write BroadcastAddress. + /// + [Fact] + public void GetObjectData_WritesNetworkPrefixAddress_Field() + { + // Arrange + var subnet = new Subnet(IPAddress.Parse("192.168.1.0"), 24); + var info = new SerializationInfo(typeof(Subnet), new FormatterConverter()); + + // Act + subnet.GetObjectData(info, new StreamingContext(StreamingContextStates.All)); + + // Assert: the new-format field is present + var networkBytes = (byte[])info.GetValue("NetworkPrefixAddress", typeof(byte[])); + Assert.Equal(subnet.NetworkPrefixAddress.GetAddressBytes(), networkBytes); + + // Assert: the legacy BroadcastAddress field must not be present. + // SerializationInfo has no enumerator helper; attempt a read and expect a + // SerializationException (entry-not-found). + Assert.Throws( + () => info.GetValue("BroadcastAddress", typeof(byte[])) + ); + } + + /// + /// S3 - stamps the format version as 3. + /// + [Fact] + public void GetObjectData_WritesSerVersion_Three() + { + // Arrange + var subnet = new Subnet(IPAddress.Parse("10.0.0.0"), 8); + var info = new SerializationInfo(typeof(Subnet), new FormatterConverter()); + + // Act + subnet.GetObjectData(info, new StreamingContext(StreamingContextStates.All)); + + // Assert + Assert.Equal(3, info.GetInt32("SerVersion")); + } + + /// + /// S3 - round-trip via the in-process / + /// helpers reconstructs an equal subnet + /// for both IPv4 and IPv6 inputs. + /// + /// The network prefix address (CIDR notation host) to construct. + /// The CIDR routing prefix length. + [Theory] + [InlineData("192.168.1.0", 24)] + [InlineData("10.0.0.0", 8)] + [InlineData("172.16.0.0", 12)] + [InlineData("::", 0)] + [InlineData("2001:db8::", 32)] + public void Subnet_Serialization_RoundTrip_Succeeds(string address, int routingPrefix) + { + // Arrange + var subnet = new Subnet(IPAddress.Parse(address), routingPrefix); + var info = new SerializationInfo(typeof(Subnet), new FormatterConverter()); + + // Act + subnet.GetObjectData(info, new StreamingContext(StreamingContextStates.All)); + var actual = Subnet.DeserializeFromInfo(info); + + // Assert + Assert.Equal(subnet, actual, SubnetEqualityComparer.Instance); + Assert.Equal(subnet.NetworkPrefixAddress, actual.NetworkPrefixAddress); + Assert.Equal(subnet.RoutingPrefix, actual.RoutingPrefix); + Assert.Equal(subnet.MaxEnumerationExponent, actual.MaxEnumerationExponent); + } + + /// + /// S3 - deserialization rejects data carrying an unsupported (legacy) SerVersion, + /// throwing that mentions the mismatched version. + /// + [Fact] + public void DeserializeSubnet_WithWrongVersion_ThrowsSerializationException() + { + // Arrange - build info that mimics the legacy v2 format + var info = new SerializationInfo(typeof(Subnet), new FormatterConverter()); + info.AddValue("NetworkPrefixAddress", IPAddress.Parse("192.168.1.0").GetAddressBytes()); + info.AddValue("RoutingPrefix", 24); + info.AddValue("SerVersion", 2); + info.AddValue("MaxEnumerationExponent", Subnet.DefaultMaxEnumerationExponent); + + // Act & Assert + var ex = Assert.Throws( + () => Subnet.DeserializeFromInfo(info) + ); + Assert.Contains("version", ex.Message, StringComparison.OrdinalIgnoreCase); + Assert.Contains("2", ex.Message); + } + } +} +#endif diff --git a/src/Arcus.Tests/SubnetTests.Operators.cs b/src/Arcus.Tests/SubnetTests.Operators.cs new file mode 100644 index 0000000..aeb4114 --- /dev/null +++ b/src/Arcus.Tests/SubnetTests.Operators.cs @@ -0,0 +1,161 @@ +namespace Arcus.Tests +{ + /// + /// tests for operators + /// + public partial class SubnetTests + { + #region Operator Equality + + /// Verifies that the == operator returns the expected equality result based on comparison sign. + /// Expected comparison sign; zero means the subnets are equal. + /// The left-hand subnet. + /// The right-hand subnet. + [Theory] + [MemberData(nameof(Comparison_Values))] + public void Operator_Equals_Test(int expected, Subnet left, Subnet right) + { + // Arrange + // Act + var result = left == right; + + // Assert + Assert.Equal(expected == 0, result); + } + + /// Verifies that the != operator returns the expected inequality result based on comparison sign. + /// Expected comparison sign; non-zero means the subnets are not equal. + /// The left-hand subnet. + /// The right-hand subnet. + [Theory] + [MemberData(nameof(Comparison_Values))] + public void Operator_NotEquals_Test(int expected, Subnet left, Subnet right) + { + // Arrange + // Act + var result = left != right; + + // Assert + Assert.Equal(expected != 0, result); + } + + #endregion + + #region Comparison Operators + + /// Verifies that the > operator returns the expected result based on comparison sign. + /// Expected comparison sign; positive means left is greater. + /// The left-hand subnet. + /// The right-hand subnet. + [Theory] + [MemberData(nameof(Comparison_Values))] + public void Operator_GreaterThan_Test(int expected, Subnet left, Subnet right) + { + // Arrange + // Act + var result = left > right; + + // Assert + Assert.Equal(expected > 0, result); + } + + /// Verifies that the >= operator returns the expected result based on comparison sign. + /// Expected comparison sign; non-negative means left is greater than or equal. + /// The left-hand subnet. + /// The right-hand subnet. + [Theory] + [MemberData(nameof(Comparison_Values))] + public void Operator_GreaterThanOrEqual_Test(int expected, Subnet left, Subnet right) + { + // Arrange + // Act + var result = left >= right; + + // Assert + Assert.Equal(expected >= 0, result); + } + + /// Verifies that the < operator returns the expected result based on comparison sign. + /// Expected comparison sign; negative means left is less. + /// The left-hand subnet. + /// The right-hand subnet. + [Theory] + [MemberData(nameof(Comparison_Values))] + public void Operator_LessThan_Test(int expected, Subnet left, Subnet right) + { + // Arrange + // Act + var result = left < right; + + // Assert + Assert.Equal(expected < 0, result); + } + + /// Verifies that the <= operator returns the expected result based on comparison sign. + /// Expected comparison sign; non-positive means left is less than or equal. + /// The left-hand subnet. + /// The right-hand subnet. + [Theory] + [MemberData(nameof(Comparison_Values))] + public void Operator_LessThanOrEqual_Test(int expected, Subnet left, Subnet right) + { + // Arrange + // Act + var result = left <= right; + + // Assert + Assert.Equal(expected <= 0, result); + } + + #endregion + + #region Null Operands + + /// Verifies that the < operator returns when the left operand is null and the right is non-null. + [Fact] + public void Operator_LessThan_NullLeft_NonNullRight_ReturnsTrue_Test() + { + // Arrange + Subnet left = null; + var right = Subnet.Parse("192.168.0.0/16"); + + // Act + var result = left < right; + + // Assert + Assert.True(result); + } + + /// Verifies that the < operator returns when both operands are null. + [Fact] + public void Operator_LessThan_BothNull_ReturnsFalse_Test() + { + // Arrange + Subnet left = null; + Subnet right = null; + + // Act + var result = left < right; + + // Assert + Assert.False(result); + } + + /// Verifies that the > operator returns when the left operand is null. + [Fact] + public void Operator_GreaterThan_NullLeft_ReturnsFalse_Test() + { + // Arrange + Subnet left = null; + var right = Subnet.Parse("192.168.0.0/16"); + + // Act + var result = left > right; + + // Assert + Assert.False(result); + } + + #endregion + } +} diff --git a/src/Arcus.Tests/SubnetTests.cs b/src/Arcus.Tests/SubnetTests.cs index 4a3c461..5ea4c35 100644 --- a/src/Arcus.Tests/SubnetTests.cs +++ b/src/Arcus.Tests/SubnetTests.cs @@ -1,29 +1,24 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; +using System.Globalization; using System.Net; using System.Numerics; -using System.Text; -using Arcus; using Arcus.Tests.XunitSerializers; -using Gulliver; -using Xunit; -using Xunit.Sdk; #if NET48 // maintained for .NET 4.8 compatibility -using System.IO; using System.Runtime.Serialization; -using System.Runtime.Serialization.Formatters.Binary; #endif [assembly: RegisterXunitSerializer(typeof(SubnetXunitSerializer), typeof(Subnet))] +[assembly: RegisterXunitSerializer(typeof(SubnetEnumerableXunitSerializer), typeof(IEnumerable))] +[assembly: RegisterXunitSerializer(typeof(FormatProviderXunitSerializer), typeof(IFormatProvider))] namespace Arcus.Tests { - public class SubnetTests + /// Unit tests for . + public partial class SubnetTests { #region Addresses + /// Verifies that enumerating a subnet's addresses matches converting it to an array. + /// CIDR notation string for the subnet under test. [Theory] [InlineData("192.168.1.0/24")] [InlineData("16.8.14.12/28")] @@ -46,6 +41,8 @@ public void Addresses_Test(string input) #region Class + /// Verifies that is assignable to the expected base types and interfaces. + /// The type that should be assignable to. [Theory] [InlineData(typeof(AbstractIPAddressRange))] [InlineData(typeof(IEquatable))] @@ -70,127 +67,127 @@ public void Assignability_Test(Type assignableFromType) #region CompareTo / Operators - public static IEnumerable Comparison_Values() - { - yield return new object[] { 0, Subnet.Parse("192.168.0.0/16"), Subnet.Parse("192.168.0.0/16") }; - yield return new object[] { 0, Subnet.Parse("ab:cd::/64"), Subnet.Parse("ab:cd::/64") }; - yield return new object[] { 1, Subnet.Parse("192.168.0.0/16"), null }; - yield return new object[] { 1, Subnet.Parse("ab:cd::/64"), null }; - yield return new object[] { 1, Subnet.Parse("192.168.0.0/16"), Subnet.Parse("192.168.0.0/20") }; - yield return new object[] { -1, Subnet.Parse("192.168.0.0/20"), Subnet.Parse("192.168.0.0/16") }; - yield return new object[] { 1, Subnet.Parse("ab:cd::/64"), Subnet.Parse("ab:cd::/96") }; - yield return new object[] { -1, Subnet.Parse("ab:cd::/96"), Subnet.Parse("ab:cd::/64") }; - yield return new object[] { -1, Subnet.Parse("0.0.0.0/0"), Subnet.Parse("::/0") }; - yield return new object[] { 1, Subnet.Parse("::/0"), Subnet.Parse("0.0.0.0/0") }; - yield return new object[] { -1, Subnet.Parse("0.0.0.0/32"), Subnet.Parse("::/128") }; - yield return new object[] { 1, Subnet.Parse("::/128"), Subnet.Parse("0.0.0.0/32") }; + /// Gets theory data for comparison operator and tests. + /// Parameters: expected comparison sign (int), left subnet (), right subnet (). + public static TheoryData Comparison_Values() + { + return new TheoryData + { + { 0, Subnet.Parse("192.168.0.0/16"), Subnet.Parse("192.168.0.0/16") }, + { 0, Subnet.Parse("ab:cd::/64"), Subnet.Parse("ab:cd::/64") }, + { 1, Subnet.Parse("192.168.0.0/16"), null }, + { 1, Subnet.Parse("ab:cd::/64"), null }, + { 1, Subnet.Parse("192.168.0.0/16"), Subnet.Parse("192.168.0.0/20") }, + { -1, Subnet.Parse("192.168.0.0/20"), Subnet.Parse("192.168.0.0/16") }, + { 1, Subnet.Parse("ab:cd::/64"), Subnet.Parse("ab:cd::/96") }, + { -1, Subnet.Parse("ab:cd::/96"), Subnet.Parse("ab:cd::/64") }, + { -1, Subnet.Parse("0.0.0.0/0"), Subnet.Parse("::/0") }, + { 1, Subnet.Parse("::/0"), Subnet.Parse("0.0.0.0/0") }, + { -1, Subnet.Parse("0.0.0.0/32"), Subnet.Parse("::/128") }, + { 1, Subnet.Parse("::/128"), Subnet.Parse("0.0.0.0/32") }, + }; } - [Theory] - [MemberData(nameof(Comparison_Values))] - public void CompareTo_Test(int expected, Subnet left, Subnet right) - { - // Arrange - // Act - var result = left.CompareTo(right); + #endregion // end: CompareTo / Operators - // Assert - Assert.Equal(expected, result); - } + #region S6 - CompareTo typo fix - [Theory] - [MemberData(nameof(Comparison_Values))] - public void Operator_Equals_Test(int expected, Subnet left, Subnet right) + /// Verifies that uses the correct article ("a" not "an") in the exception message. + [Fact] + public void CompareTo_WithNonSubnet_ExceptionMessageUsesCorrectArticle() { - // Arrange - // Act - var result = left == right; - - // Assert - Assert.Equal(expected == 0, result); + var subnet = Subnet.Parse("192.168.1.0/24"); + var ex = Assert.Throws(() => subnet.CompareTo((object)"not a subnet")); + Assert.Equal("Object is not a Subnet", ex.Message); } - [Theory] - [MemberData(nameof(Comparison_Values))] - public void Operator_NotEquals_Test(int expected, Subnet left, Subnet right) - { - // Arrange - // Act - var result = left != right; + #endregion // end: S6 - CompareTo typo fix - // Assert - Assert.Equal(expected != 0, result); - } + #region Netmask - [Theory] - [MemberData(nameof(Comparison_Values))] - public void Operator_GreaterThan_Test(int expected, Subnet left, Subnet right) + /// Verifies that is null for an IPv6 subnet. + [Fact] + public void Netmask_Null_ForIPv6_Test() { // Arrange + var subnet = new Subnet(IPAddress.IPv6Any, 32); + // Act - var result = left > right; + var netmask = subnet.Netmask; // Assert - Assert.Equal(expected > 0, result); + Assert.Null(netmask); } - [Theory] - [MemberData(nameof(Comparison_Values))] - public void Operator_GreaterThanOrEqual_Test(int expected, Subnet left, Subnet right) + /// Verifies that is non-null and correct for an IPv4 subnet. + [Fact] + public void Netmask_NotNull_ForIPv4_Test() { // Arrange + var subnet = new Subnet(IPAddress.Parse("192.168.1.0"), 24); + // Act - var result = left >= right; + var netmask = subnet.Netmask; // Assert - Assert.Equal(expected >= 0, result); + Assert.NotNull(netmask); + Assert.Equal(IPAddress.Parse("255.255.255.0"), netmask); } - [Theory] - [MemberData(nameof(Comparison_Values))] - public void Operator_LessThan_Test(int expected, Subnet left, Subnet right) - { - // Arrange - // Act - var result = left < right; + #endregion // end: Netmask - // Assert - Assert.Equal(expected < 0, result); + #region S9 - IPv6 netmask skip + + /// Verifies that constructing an IPv6 subnet does not allocate a netmask. + [Fact] + public void IPv6Subnet_Construction_DoesNotAllocateNetmask() + { + var subnet = new Subnet(IPAddress.IPv6Loopback, 64); + Assert.Null(subnet.Netmask); } - [Theory] - [MemberData(nameof(Comparison_Values))] - public void Operator_LessThanOrEqual_Test(int expected, Subnet left, Subnet right) + #endregion // end: S9 - IPv6 netmask skip + + #region NetworkPrefixAddress / BroadcastAddress + + /// Verifies that equals the subnet's . + [Fact] + public void NetworkPrefixAddress_EqualsHead_Test() { // Arrange + var subnet = Subnet.Parse("192.168.1.0/24"); + // Act - var result = left <= right; + var networkPrefix = subnet.NetworkPrefixAddress; // Assert - Assert.Equal(expected <= 0, result); + Assert.Equal(subnet.Head, networkPrefix); + Assert.Equal(IPAddress.Parse("192.168.1.0"), networkPrefix); } - #endregion - - #region Netmask - + /// Verifies that equals the subnet's . [Fact] - public void Netmask_Null_ForIPv6_Test() + public void BroadcastAddress_EqualsTail_Test() { // Arrange - var subnet = new Subnet(IPAddress.IPv6Any, 32); + var subnet = Subnet.Parse("192.168.1.0/24"); // Act - var netmask = subnet.Netmask; + var broadcast = subnet.BroadcastAddress; // Assert - Assert.Null(netmask); + Assert.Equal(subnet.Tail, broadcast); + Assert.Equal(IPAddress.Parse("192.168.1.255"), broadcast); } - #endregion // end: Netmask + #endregion // end: NetworkPrefixAddress / BroadcastAddress #region Overlaps + /// Verifies that returns the expected result for various subnet pairs. + /// Expected overlap result. + /// CIDR string for the first subnet. + /// CIDR string for the second subnet. [Theory] [InlineData(true, "0.0.0.0/0", "0.0.0.0/0")] [InlineData(true, "::/0", "::/0")] @@ -215,10 +212,39 @@ public void Overlaps_Test(bool expected, string subnetAString, string subnetBStr Assert.Equal(expected, result); } + /// Verifies that is symmetric when a range is wholly contained within a subnet. + [Fact] + public void Overlaps_WhollyContainedIIPAddressRange_IsSymmetric_Test() + { + // When a range is wholly contained inside a subnet, both directions must return true. + // This exercises AbstractIPAddressRange.Overlaps(IIPAddressRange) - the Subnet-typed + // overload already handled this correctly; the base-class path had an asymmetry bug. + var outer = Subnet.Parse("192.168.0.0/16"); + var inner = new IPAddressRange(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.255")); + + Assert.True(outer.Overlaps((IIPAddressRange)inner), "outer.Overlaps(inner) should be true"); + Assert.True(inner.Overlaps((IIPAddressRange)outer), "inner.Overlaps(outer) should be true - symmetry"); + } + + /// Verifies that the subnet-typed overload is symmetric when one subnet is wholly inside the other. + [Fact] + public void Overlaps_WhollyContainedSubnet_IsSymmetric_Test() + { + // Subnet-typed overload: both directions must return true when one subnet is inside the other. + var outer = Subnet.Parse("10.0.0.0/8"); + var inner = Subnet.Parse("10.10.0.0/16"); + + Assert.True(outer.Overlaps(inner), "outer.Overlaps(inner) should be true"); + Assert.True(inner.Overlaps(outer), "inner.Overlaps(outer) should be true - symmetry"); + } + #endregion // end: Overlaps #region ToString + /// Verifies that returns the canonical CIDR representation. + /// Expected canonical CIDR string. + /// Input CIDR string to parse. [Theory] [InlineData("192.168.1.1/32", "192.168.1.1/32")] [InlineData("192.168.0.0/16", "192.168.1.1/16")] @@ -236,12 +262,16 @@ public void ToString_Test(string expected, string input) Assert.Equal(expected, result); } - #endregion + #endregion // end: ToString #region UsableHostAddressCount - public static IEnumerable UsableHostAddressCount_Test_Values() + /// Gets theory data for . + /// Parameters: expected usable host address count (), subnet under test (). + public static TheoryData UsableHostAddressCount_Test_Values() { + var data = new TheoryData(); + foreach (var ipAddress in IPv4Addresses()) { for (var i = 0; i <= 32; i++) @@ -250,7 +280,7 @@ public static IEnumerable UsableHostAddressCount_Test_Values() var count = routePrefix < 2 ? BigInteger.Zero : BigInteger.Subtract(BigInteger.Pow(2, routePrefix), 2); var subnet = new Subnet(ipAddress, i); - yield return new object[] { count, subnet }; + data.Add(count, subnet); } } @@ -262,10 +292,12 @@ public static IEnumerable UsableHostAddressCount_Test_Values() var count = routePrefix < 2 ? BigInteger.Zero : BigInteger.Subtract(BigInteger.Pow(2, routePrefix), 2); var subnet = new Subnet(ipAddress, i); - yield return new object[] { count, subnet }; + data.Add(count, subnet); } } + return data; + IEnumerable IPv4Addresses() { yield return IPAddress.Parse("192.168.1.1"); @@ -277,6 +309,9 @@ IEnumerable IPv6Addresses() } } + /// Verifies that returns the expected count for a given subnet. + /// Expected usable host address count. + /// The subnet under test. [Theory] [MemberData(nameof(UsableHostAddressCount_Test_Values))] public void UsableHostAddressCount_Test(BigInteger expected, Subnet subnet) @@ -290,12 +325,44 @@ public void UsableHostAddressCount_Test(BigInteger expected, Subnet subnet) Assert.Equal(expected, result); } - #endregion + /// Verifies that returns zero for a single-host /32 subnet. + [Fact] + public void UsableHostAddressCount_SingleHost_ReturnsZero_Test() + { + // Arrange + var subnet = new Subnet(IPAddress.Parse("10.0.0.1"), 32); + + // Act + var result = subnet.UsableHostAddressCount; + + // Assert + Assert.Equal(BigInteger.Zero, result); + } + + /// Verifies that returns the total length minus two for a /0 subnet. + [Fact] + public void UsableHostAddressCount_SlashZero_ReturnsLengthMinusTwo_Test() + { + // Arrange + var subnet = new Subnet(IPAddress.Any, 0); + + // Act + var result = subnet.UsableHostAddressCount; + + // Assert + Assert.Equal(BigInteger.Pow(2, 32) - 2, result); + } + + #endregion // end: UsableHostAddressCount #region Contains #region Contains(IPAddress) + /// Verifies that returns the expected result for various address inputs. + /// Expected result. + /// CIDR string for the subnet under test. + /// The IP address string to test containment. [Theory] [InlineData(true, "192.168.0.0/16", "192.168.0.0")] [InlineData(true, "192.168.0.0/16", "192.168.0.16")] @@ -324,6 +391,10 @@ public void Contains_IPAddress_Test(bool expected, string subnetString, string c #region Contains(Subnet) + /// Verifies that returns the expected result for various subnet inputs. + /// Expected result. + /// CIDR string for the outer subnet. + /// CIDR string for the inner subnet to test. [Theory] [InlineData(true, "192.168.0.0/16", "192.168.0.0/16")] [InlineData(true, "192.168.0.0/16", "192.168.0.0/32")] @@ -338,184 +409,73 @@ public void Contains_Subnet_Test(bool expected, string subnetString, string cont // Act var result = subnet.Contains(containsSubnet); - // Arrange + // Assert Assert.Equal(expected, result); } + /// Verifies that an IPv4 subnet does not contain an IPv6 address. [Fact] - public void Ipv4SubnetDoesNotContainIPv6Test() + public void Contains_IPAddress_IPv4SubnetDoesNotContainIPv6_Test() { // Arrange var subnet = Subnet.Parse("0.0.0.0/0"); // Act + var result = subnet.Contains(IPAddress.Parse("::")); // Assert - Assert.False(subnet.Contains(IPAddress.Parse("::"))); + Assert.False(result); } #endregion // end: Contains(Subnet) - #endregion // end: Contains - - #region TryIPv4FromPartial - - public static IEnumerable TryIPv4FromPartial_Test_Values() - { - yield return new object[] { null, null }; - yield return new object[] { null, string.Empty }; - yield return new object[] { null, "potato" }; - yield return new object[] { Subnet.Parse("192.0.0.0/8"), "192" }; - yield return new object[] { Subnet.Parse("192.0.0.0/8"), "192." }; - yield return new object[] { Subnet.Parse("192.168.0.0/16"), "192.168" }; - yield return new object[] { Subnet.Parse("192.168.0.0/16"), "192.168." }; - yield return new object[] { Subnet.Parse("192.168.1.0/24"), "192.168.1" }; - yield return new object[] { Subnet.Parse("192.168.1.0/24"), "192.168.1." }; - yield return new object[] { Subnet.Parse("192.168.1.1/32"), "192.168.1.1" }; - yield return new object[] { null, "192.168.1.1." }; - yield return new object[] { null, "192.168.0.1.5" }; - yield return new object[] { null, "10.209.005.029" }; // Addresses #59 - [BUG] Subnet.TryIPv4FromPartial(string , out Subnet) throws FormatException if the string input contains a malformed 0-prefixed octet that is not a valid octal number. - } + #region S2 - Contains delegation + /// Verifies that delegates to the base class . + /// Expected result. + /// CIDR string for the outer subnet. + /// CIDR string for the inner subnet. [Theory] - [MemberData(nameof(TryIPv4FromPartial_Test_Values))] - public void TryIPv4FromPartial_Test(Subnet expected, string input) - { - // Arrange - - // Act - var success = Subnet.TryIPv4FromPartial(input, out var subnet); - - // Assert - Assert.Equal(expected != null, success); - Assert.Equal(expected, subnet); - } - - #endregion // end: TryIPv4FromPartial - - #region TryIPv6FromPartial - - public static IEnumerable TryIPv6FromPartial_Test_Values() + [InlineData(true, "192.168.0.0/16", "192.168.0.0/24")] + [InlineData(true, "192.168.0.0/16", "192.168.0.0/16")] + [InlineData(false, "192.168.0.0/24", "192.168.0.0/16")] + [InlineData(false, "10.0.0.0/8", "::/0")] + public void Contains_IsDelegatedToBaseContains(bool expected, string subnetAString, string subnetBString) { - // bad input formats - yield return new object[] { Enumerable.Empty(), null }; - yield return new object[] { Enumerable.Empty(), string.Empty }; - yield return new object[] { Enumerable.Empty(), "potato" }; - - // invalid input - yield return new object[] { Enumerable.Empty(), ":" }; - yield return new object[] { Enumerable.Empty(), ":::" }; - yield return new object[] { Enumerable.Empty(), "0:0:0:0:0:0:0:0:0" }; // too many hextets - yield return new object[] { Enumerable.Empty(), "0:0:0:0:0:0:0:0::" }; - - // invalid input, multiple "::" - yield return new object[] { Enumerable.Empty(), "::0::" }; - yield return new object[] { Enumerable.Empty(), "0::0::0" }; - yield return new object[] { Enumerable.Empty(), "::0:0:0::" }; - - // explicit valid subnets - yield return new object[] { new[] { Subnet.Parse("::/128") }, "::/128" }; - yield return new object[] { new[] { Subnet.Parse("2001:db8:85a3:42::/64") }, "2001:db8:85a3:42::/64" }; - yield return new object[] - { - new[] { Subnet.Parse("2001:0db8:85a3:0042:1000:0001:0370:7334/128") }, - "2001:0db8:85a3:0042:1000:0001:0370:7334/128", - }; - - for (var hextetCount = 0; hextetCount <= 8; hextetCount++) - { - var sb = new StringBuilder(); - - sb.Append(string.Join(":", Enumerable.Repeat("0", hextetCount))); - - if (hextetCount < 8) - { - sb.Append("::"); - } - - var subnets = new List(); - for (var i = 0; i <= 8 - hextetCount; i++) - { - subnets.Add(Subnet.Parse($"::/{128 - (16 * i)}")); - } - - yield return new object[] { subnets, sb.ToString() }; - } - - var hextets = "2001:0db8:85a3:0042:1000:0001:0370:7334".Split(':'); - - for (var hextetCount = 1; hextetCount <= 8; hextetCount++) - { - var subnets = new List(); - for (var i = 8 - hextetCount; i >= 0; i--) - { - var enumerable = hextets.Take(hextetCount).Select(s => new string(s.SkipWhile(c => c == '0').ToArray())); - var trimmedLeadingZero = string.Join(":", enumerable); - var subnet = - hextetCount < 8 - ? Subnet.Parse($"{trimmedLeadingZero}::/{128 - (16 * i)}") - : Subnet.Parse($"{trimmedLeadingZero}"); - - subnets.Add(subnet); - } - - var inputString = string.Join(":", hextets.Take(hextetCount)); - - if (hextetCount < 8) - { - yield return new object[] { subnets, $"{inputString}::" }; - yield return new object[] { subnets, $"{inputString}:" }; - } - - if ( - !string.IsNullOrEmpty(inputString) // no match for an empty string - && inputString != hextets[0] - ) // TODO should this work, a hextet w/o any ':'? Unsure - { - yield return new object[] { subnets, inputString }; - } - } + var subnetA = Subnet.Parse(subnetAString); + var subnetB = Subnet.Parse(subnetBString); + Assert.Equal(expected, subnetA.Contains(subnetB)); + Assert.Equal(((IIPAddressRange)subnetA).Contains(subnetB), subnetA.Contains(subnetB)); } - [Theory] - [MemberData(nameof(TryIPv6FromPartial_Test_Values))] - public void TryIPv6FromPartial_Test(IEnumerable expected, string input) - { - // Arrange - // Act -#pragma warning disable CS0618 // testing a known obsolete method; ignoring the fact that it is obsolete - var success = Subnet.TryIPv6FromPartial(input, out var subnets); -#pragma warning restore CS0618 - - // Assert - var expectedList = expected.ToList(); - var subnetList = subnets.ToList(); - - Assert.Equal(expectedList.Any(), success); - Assert.Equal(expectedList.Count, subnetList.Count); - Assert.All(subnetList, subnet => Assert.Contains(subnet, expectedList)); - } + #endregion // end: S2 - Contains delegation - #endregion // end: TryIPv6FromPartial + #endregion // end: Contains #region Ctor #region Ctor(IPAddress) - public static IEnumerable Ctor_IPAddress_Test_Values() + /// Gets theory data for . + /// Parameters: expected subnet (), address to construct with (). + public static TheoryData Ctor_IPAddress_Test_Values() { + var data = new TheoryData(); + foreach (var ipAddress in IPv4Addresses()) { var subnet = new Subnet(ipAddress, 32); - yield return new object[] { subnet, ipAddress }; + data.Add(subnet, ipAddress); } foreach (var ipAddress in IPv6Addresses()) { var subnet = new Subnet(ipAddress, 128); - yield return new object[] { subnet, ipAddress }; + data.Add(subnet, ipAddress); } + return data; + IEnumerable IPv4Addresses() { yield return IPAddress.Any; @@ -532,6 +492,9 @@ IEnumerable IPv6Addresses() } } + /// Verifies that constructing a from a single produces the expected host subnet. + /// Expected subnet. + /// The IP address to construct from. [Theory] [MemberData(nameof(Ctor_IPAddress_Test_Values))] public void Ctor_IPAddress_Test(Subnet expected, IPAddress address) @@ -544,57 +507,114 @@ public void Ctor_IPAddress_Test(Subnet expected, IPAddress address) Assert.Equal(expected, subnet); } + /// Verifies that constructing a with a null throws . [Fact] public void Ctor_IPAddress_NullIPAddress_Throws_ArgumentNullException_Test() { - Assert.Throws(() => new Subnet(null)); + // Arrange + // Act / Assert + Assert.Throws(() => new Subnet((IPAddress)null)); + } + + /// Verifies that constructing a from an IPv4 address sets routing prefix to 32. + [Fact] + public void Ctor_IPAddress_SetsRoutingPrefix32_ForIPv4_Test() + { + // Arrange + var address = IPAddress.Parse("10.0.0.1"); + + // Act + var subnet = new Subnet(address); + + // Assert + Assert.Equal(32, subnet.RoutingPrefix); + } + + /// Verifies that constructing a from an IPv6 address sets routing prefix to 128. + [Fact] + public void Ctor_IPAddress_SetsRoutingPrefix128_ForIPv6_Test() + { + // Arrange + var address = IPAddress.IPv6Loopback; + + // Act + var subnet = new Subnet(address); + + // Assert + Assert.Equal(128, subnet.RoutingPrefix); } #endregion // end: Ctor(IPAddress) + #region S10 - Single-IP constructor with custom exponent + + /// Verifies that the single-IP constructor uses the default max enumeration exponent. + [Fact] + public void Subnet_SingleIPConstructor_DefaultExponent() + { + var subnet = new Subnet(IPAddress.Parse("192.168.1.1")); + Assert.Equal(12, subnet.MaxEnumerationExponent); + } + + /// Verifies that sets the expected exponent. + [Fact] + public void Subnet_SingleIPConstructor_CustomExponent() + { + var subnet = Subnet.FromIPAddress(IPAddress.Parse("192.168.1.1"), 5); + Assert.Equal(5, subnet.MaxEnumerationExponent); + } + + #endregion // end: S10 - Single-IP constructor with custom exponent + #region Ctor(IPAddress, int) + /// Verifies that constructing a with a null address and routing prefix throws . [Fact] public void Ctor_IPAddress_Int_NullIPAddress_Throws_ArgumentNullException_Test() { + // Arrange + // Act / Assert Assert.Throws(() => new Subnet(null, 42)); } + /// Verifies that constructing a with an out-of-range routing prefix throws . + /// IP address string. + /// The out-of-range routing prefix. [Theory] [InlineData("192.168.1.1", -1)] [InlineData("192.168.1.1", 33)] [InlineData("::", -1)] [InlineData("::", 129)] - public void Ctor_IPAddress_Int_IntOutOfRange_Throws_ArgumentNullException_Test(string address, int routingPrefix) + public void Ctor_IPAddress_Int_IntOutOfRange_Throws_ArgumentOutOfRangeException_Test(string address, int routingPrefix) { - Assert.Throws(() => new Subnet(IPAddress.Parse(address), routingPrefix)); + // Arrange + var ipAddress = IPAddress.Parse(address); + + // Act / Assert + Assert.Throws(() => new Subnet(ipAddress, routingPrefix)); } - #endregion + #endregion // end: Ctor(IPAddress, int) #region Ctor(IPAddress, IPAddress) - public static IEnumerable Ctor_IPAddress_IPAddress_Test_Values() + /// Gets theory data for . + /// Parameters: expected subnet (), primary address (), secondary address (). + public static TheoryData Ctor_IPAddress_IPAddress_Test_Values() { - yield return new object[] - { - new Subnet(IPAddress.Parse("0.0.0.0"), 0), - IPAddress.Parse("0.0.0.0"), - IPAddress.Parse("255.255.255.255"), - }; - yield return new object[] - { - new Subnet(IPAddress.Parse("::"), 0), - IPAddress.Parse("::"), - IPAddress.Parse("FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF"), - }; + var data = new TheoryData(); + var seen = new HashSet(); foreach (var address in IPv4Addresses()) { for (var routePrefix = 0; routePrefix <= 32; routePrefix++) { var subnet = new Subnet(address, routePrefix); - yield return new object[] { subnet, subnet.Head, subnet.Tail }; + var key = $"{subnet.ToString("f", null)}|{subnet.Head}|{subnet.Tail}"; + if (seen.Add(key)) + { + data.Add(subnet, subnet.Head, subnet.Tail); + } } } @@ -603,10 +623,16 @@ public static IEnumerable Ctor_IPAddress_IPAddress_Test_Values() for (var routePrefix = 0; routePrefix <= 128; routePrefix++) { var subnet = new Subnet(address, routePrefix); - yield return new object[] { subnet, subnet.Head, subnet.Tail }; + var key = $"{subnet.ToString("f", null)}|{subnet.Head}|{subnet.Tail}"; + if (seen.Add(key)) + { + data.Add(subnet, subnet.Head, subnet.Tail); + } } } + return data; + IEnumerable IPv4Addresses() { yield return IPAddress.Any; @@ -623,6 +649,10 @@ IEnumerable IPv6Addresses() } } + /// Verifies that constructing a from two bounds produces the expected subnet. + /// Expected subnet. + /// The low address bound. + /// The high address bound. [Theory] [MemberData(nameof(Ctor_IPAddress_IPAddress_Test_Values))] public void Ctor_IPAddress_IPAddress_Test(Subnet expected, IPAddress primary, IPAddress secondary) @@ -635,6 +665,9 @@ public void Ctor_IPAddress_IPAddress_Test(Subnet expected, IPAddress primary, IP Assert.Equal(expected, result); } + /// Verifies that constructing a with a null address argument throws . + /// Primary address string. + /// Secondary address string. [Theory] [InlineData("192.168.3.25", null)] [InlineData(null, "192.168.3.25")] @@ -647,11 +680,13 @@ public void Ctor_IPAddress_IPAddress_Null_Input_Throws_ArgumentNullException_Tes _ = IPAddress.TryParse(primary, out var primaryAddress); _ = IPAddress.TryParse(secondary, out var secondaryAddress); - // Act - // Assert + // Act / Assert Assert.Throws(() => new Subnet(primaryAddress, secondaryAddress)); } + /// Verifies that constructing a with an invalid address ordering throws . + /// The higher (invalid low) address string. + /// The lower (invalid high) address string. [Theory] [InlineData("192.168.3.25", "192.168.3.0")] [InlineData("ff::dc", "::")] @@ -664,11 +699,13 @@ string secondary var primaryAddress = IPAddress.Parse(primary); var secondaryAddress = IPAddress.Parse(secondary); - // Act - // Assert + // Act / Assert Assert.Throws(() => new Subnet(primaryAddress, secondaryAddress)); } + /// Verifies that constructing a with mismatched address families throws . + /// First address string. + /// Second address string of a different family. [Theory] [InlineData("192.168.3.25", "2001:0db8:85a3:0042:1000:8a2e:0370:7334")] [InlineData("2001:0db8:85a3:0042:1000:8a2e:0370:7334", "192.168.3.25")] @@ -681,8 +718,7 @@ string secondary var primaryAddress = IPAddress.Parse(primary); var secondaryAddress = IPAddress.Parse(secondary); - // Act - // Assert + // Act / Assert Assert.Throws(() => new Subnet(primaryAddress, secondaryAddress)); } @@ -690,53 +726,23 @@ string secondary #endregion // end: Ctor - #region ISerializable -#if NET48 // maintained for .NET 4.8 compatibility - public static IEnumerable CanSerializable_Test_Values() - { - yield return new object[] { new Subnet(IPAddress.Parse("192.168.1.0")) }; - yield return new object[] { new Subnet(IPAddress.Parse("192.168.1.0"), IPAddress.Parse("192.168.1.255")) }; - yield return new object[] { new Subnet(IPAddress.Parse("::"), IPAddress.Parse("::FFFF")) }; - } + #region Length / TryGetLength - [Theory] - [MemberData(nameof(CanSerializable_Test_Values))] - public void CanSerializable_Test(Subnet subnet) + /// Gets theory data for , , and . + /// Parameters: expected length (), subnet under test (). + public static TheoryData Length_Test_Values() { - // Arrange - var formatter = new BinaryFormatter(); - - // Act - using (var writeStream = new MemoryStream()) - { - formatter.Serialize(writeStream, subnet); - writeStream.Seek(0, SeekOrigin.Begin); - - // Deserialize the object from the stream - var result = formatter.Deserialize(writeStream); - - // Assert - var actual = Assert.IsType(result); - - // using explicit EqualityComparer to avoid comparing elements of enumerable - Assert.Equal(subnet, actual, SubnetEqualityComparer.Instance); - } - } -#endif - #endregion end: ISerializable - - #region Static Factory Methods + var data = new TheoryData(); - #region FromBytes - - public static IEnumerable FromBytes_Bytes_Bytes_Test_Values() - { foreach (var ipAddress in IPv4Addresses()) { for (var i = 0; i <= 32; i++) { + var routePrefix = 32 - i; + var length = BigInteger.Pow(2, routePrefix); + var subnet = new Subnet(ipAddress, i); - yield return new object[] { subnet, subnet.Head.GetAddressBytes(), subnet.Tail.GetAddressBytes() }; + data.Add(length, subnet); } } @@ -744,831 +750,92 @@ public static IEnumerable FromBytes_Bytes_Bytes_Test_Values() { for (var i = 0; i <= 128; i++) { + var routePrefix = 128 - i; + var length = BigInteger.Pow(2, routePrefix); + var subnet = new Subnet(ipAddress, i); - yield return new object[] { subnet, subnet.Head.GetAddressBytes(), subnet.Tail.GetAddressBytes() }; + data.Add(length, subnet); } } + return data; + IEnumerable IPv4Addresses() { - yield return IPAddress.Any; - yield return IPAddress.Loopback; - yield return IPAddress.None; yield return IPAddress.Parse("192.168.1.1"); } IEnumerable IPv6Addresses() { - yield return IPAddress.IPv6Any; - yield return IPAddress.IPv6Loopback; yield return IPAddress.Parse("2001:0db8:85a3:0042:1000:8a2e:0370:7334"); } } + /// Verifies that returns the expected address count for a subnet. + /// Expected length. + /// The subnet under test. [Theory] - [MemberData(nameof(FromBytes_Bytes_Bytes_Test_Values))] - public void FromBytes_Bytes_Bytes_Test(Subnet expected, byte[] lowAddressBytes, byte[] highAddressBytes) + [MemberData(nameof(Length_Test_Values))] + public void Length_Test(BigInteger expected, Subnet subnet) { // Arrange // Act - var subnet = Subnet.FromBytes(lowAddressBytes, highAddressBytes); + + var result = subnet.Length; // Assert - Assert.Equal(expected, subnet); + Assert.Equal(expected, result); } + /// Verifies that returns the expected success flag and integer length. + /// Expected length as a . + /// The subnet under test. [Theory] - [InlineData(null, new byte[] { 0x01, 0x01, 0xA8, 0xC0 })] - [InlineData(new byte[] { 0x01, 0x01, 0xA8, 0xC0 }, null)] - [InlineData(null, new byte[] { })] - public void FromBytes_Null_Input_Throws_ArgumentNullException_Test(byte[] lowAddressBytes, byte[] highAddressBytes) + [MemberData(nameof(Length_Test_Values))] + public void TryGetLength_Integer_Test(BigInteger expected, Subnet subnet) { // Arrange // Act + var success = subnet.TryGetLength(out int length); + // Assert - Assert.Throws(() => Subnet.FromBytes(lowAddressBytes, highAddressBytes)); + Assert.Equal(expected <= int.MaxValue, success); + Assert.Equal(expected <= int.MaxValue ? (int)expected : -1, length); } + /// Verifies that returns the expected success flag and long length. + /// Expected length as a . + /// The subnet under test. [Theory] - [InlineData(new byte[] { 0x01, 0x01, 0xA8, 0xC0, 0xFF }, new byte[] { 0x01, 0x01, 0xA8, 0xC0 })] - [InlineData(new byte[] { 0x01, 0x01, 0xA8, 0xC0 }, new byte[] { 0x01, 0x01, 0xA8, 0xC0, 0xFF })] - [InlineData(new byte[] { }, new byte[] { 0x01, 0x01, 0xA8, 0xC0, 0xFF })] - [InlineData(new byte[] { 0x01, 0x01, 0xA8, 0xC0, 0xFF }, new byte[] { })] - public void FromBytes_Invalid_Input_Throws_ArgumentException_Test(byte[] lowAddressBytes, byte[] highAddressBytes) + [MemberData(nameof(Length_Test_Values))] + public void TryGetLength_Long_Test(BigInteger expected, Subnet subnet) { // Arrange // Act + var success = subnet.TryGetLength(out long length); + // Assert - var exception = Assert.Throws(() => Subnet.FromBytes(lowAddressBytes, highAddressBytes)); - Assert.IsType(exception.InnerException); + Assert.Equal(expected <= long.MaxValue, success); + Assert.Equal(expected <= long.MaxValue ? (long)expected : -1, length); } - #endregion // end: FromBytes + #endregion // end: Length / TryGetLength + + #region Formatting - #region TryFromBytes + #region ToString - public static IEnumerable TryFromBytes_Bytes_Bytes_Test_Values() + /// Verifies that matches the general format string output. + /// Head address string of the range. + /// Tail address string of the range. + [Theory] + [InlineData("192.168.1.1", "192.168.1.42")] + [InlineData("::beef", "0123::dead")] + public void ToString_MatchesGeneralFormat_Test(string headString, string tailString) { - yield return new object[] { false, null, null, null }; - yield return new object[] { false, null, IPAddress.Any.GetAddressBytes(), IPAddress.IPv6Any.GetAddressBytes() }; - yield return new object[] { false, null, Array.Empty(), IPAddress.IPv6Any.GetAddressBytes() }; - yield return new object[] { false, null, Array.Empty(), IPAddress.Any.GetAddressBytes() }; - yield return new object[] { false, null, IPAddress.IPv6Any.GetAddressBytes(), Array.Empty() }; - yield return new object[] { false, null, IPAddress.Any.GetAddressBytes(), Array.Empty() }; - - yield return new object[] { false, null, new byte[] { 0x00 }, IPAddress.IPv6Any.GetAddressBytes() }; - yield return new object[] { false, null, new byte[] { 0x00 }, IPAddress.Any.GetAddressBytes() }; - yield return new object[] { false, null, IPAddress.IPv6Any.GetAddressBytes(), new byte[] { 0x00 } }; - yield return new object[] { false, null, IPAddress.Any.GetAddressBytes(), new byte[] { 0x00 } }; - - foreach (var ipAddress in IPv4Addresses()) - { - for (var i = 0; i <= 32; i++) - { - var subnet = new Subnet(ipAddress, i); - yield return new object[] { true, subnet, subnet.Head.GetAddressBytes(), subnet.Tail.GetAddressBytes() }; - } - } - - foreach (var ipAddress in IPv6Addresses()) - { - for (var i = 0; i <= 128; i++) - { - var subnet = new Subnet(ipAddress, i); - yield return new object[] { true, subnet, subnet.Head.GetAddressBytes(), subnet.Tail.GetAddressBytes() }; - } - } - - IEnumerable IPv4Addresses() - { - yield return IPAddress.Any; - yield return IPAddress.Loopback; - yield return IPAddress.None; - yield return IPAddress.Parse("192.168.1.1"); - } - - IEnumerable IPv6Addresses() - { - yield return IPAddress.IPv6Any; - yield return IPAddress.IPv6Loopback; - yield return IPAddress.Parse("2001:0db8:85a3:0042:1000:8a2e:0370:7334"); - } - } - - [Theory] - [MemberData(nameof(TryFromBytes_Bytes_Bytes_Test_Values))] - public void TryFromBytes_Bytes_Bytes_Test( - bool expectedSuccess, - Subnet expectedSubnet, - byte[] lowAddressBytes, - byte[] highAddressBytes - ) - { - // Arrange - // Act - var success = Subnet.TryFromBytes(lowAddressBytes, highAddressBytes, out var subnet); - - // Assert - Assert.Equal(expectedSuccess, success); - Assert.Equal(expectedSubnet, subnet); - } - - #endregion // end: TryFromBytes - - #region Parse(string) - - public static IEnumerable Parse_String_Test_Values() - { - foreach (var ipAddress in IPv4Addresses()) - { - for (var i = 0; i <= 32; i++) - { - var subnet = new Subnet(ipAddress, i); - yield return new object[] { subnet, $"{ipAddress}/{i}" }; - } - } - - foreach (var ipAddress in IPv6Addresses()) - { - for (var i = 0; i <= 128; i++) - { - var subnet = new Subnet(ipAddress, i); - yield return new object[] { subnet, $"{subnet}" }; - } - } - - IEnumerable IPv4Addresses() - { - yield return IPAddress.Any; - yield return IPAddress.Loopback; - yield return IPAddress.None; - yield return IPAddress.Parse("192.168.1.1"); - } - - IEnumerable IPv6Addresses() - { - yield return IPAddress.IPv6Any; - yield return IPAddress.IPv6Loopback; - yield return IPAddress.Parse("2001:0db8:85a3:0042:1000:8a2e:0370:7334"); - } - } - - [Theory] - [MemberData(nameof(Parse_String_Test_Values))] - public void Parse_String_Test(Subnet expected, string input) - { - // Arrange - - // Act - var subnet = Subnet.Parse(input); - - // Assert - Assert.Equal(expected, subnet); - } - - [Fact] - public void Parse_Failure_Throws_FormatException_Test() - { - // Arrange - // Act - // Assert - Assert.Throws(() => Subnet.Parse("potato")); - } - - #endregion // end: Parse(string) - - #region TryParse(string) - - public static IEnumerable TryParse_String_Test_Values() - { - yield return new object[] { null, null }; - yield return new object[] { null, string.Empty }; - yield return new object[] { null, "potato" }; - yield return new object[] { null, "2001:0db8:85a3:0042:1000:8a2e:0370:7334/129" }; - yield return new object[] { null, "0.0.0.0/33" }; - yield return new object[] { null, "0.0.0.0/potato" }; - yield return new object[] { null, "potato/16" }; - yield return new object[] { null, "0.0.0.0/" }; - yield return new object[] { null, "/32" }; - - foreach (var ipAddress in IPv4Addresses()) - { - for (var i = 0; i <= 32; i++) - { - var subnet = new Subnet(ipAddress, i); - yield return new object[] { subnet, $"{ipAddress}/{i}" }; - } - - yield return new object[] { new Subnet(ipAddress, ipAddress), ipAddress.ToString() }; - } - - foreach (var ipAddress in IPv6Addresses()) - { - for (var i = 0; i <= 128; i++) - { - var subnet = new Subnet(ipAddress, i); - yield return new object[] { subnet, $"{subnet}" }; - } - - yield return new object[] { new Subnet(ipAddress, ipAddress), ipAddress.ToString() }; - } - - IEnumerable IPv4Addresses() - { - yield return IPAddress.Any; - yield return IPAddress.Loopback; - yield return IPAddress.None; - yield return IPAddress.Parse("192.168.1.1"); - } - - IEnumerable IPv6Addresses() - { - yield return IPAddress.IPv6Any; - yield return IPAddress.IPv6Loopback; - yield return IPAddress.Parse("2001:0db8:85a3:0042:1000:8a2e:0370:7334"); - } - } - - [Theory] - [MemberData(nameof(TryParse_String_Test_Values))] - public void TryParse_String_Test(Subnet expected, string input) - { - // Arrange - - // Act - var success = Subnet.TryParse(input, out var subnet); - - // Assert - Assert.Equal(expected != null, success); - Assert.Equal(expected, subnet); - } - - #endregion // end: TryParse(string) - - #region Parse(string, int) - - public static IEnumerable Parse_String_Int_Test_Values() - { - foreach (var ipAddress in IPv4Addresses()) - { - for (var i = 0; i <= 32; i++) - { - var subnet = new Subnet(ipAddress, i); - yield return new object[] { subnet, subnet.Head.ToString(), i }; - } - } - - foreach (var ipAddress in IPv6Addresses()) - { - for (var i = 0; i <= 128; i++) - { - var subnet = new Subnet(ipAddress, i); - yield return new object[] { subnet, subnet.Head.ToString(), i }; - } - } - - IEnumerable IPv4Addresses() - { - yield return IPAddress.Any; - yield return IPAddress.Loopback; - yield return IPAddress.None; - yield return IPAddress.Parse("192.168.1.1"); - } - - IEnumerable IPv6Addresses() - { - yield return IPAddress.IPv6Any; - yield return IPAddress.IPv6Loopback; - yield return IPAddress.Parse("2001:0db8:85a3:0042:1000:8a2e:0370:7334"); - } - } - - [Theory] - [MemberData(nameof(Parse_String_Int_Test_Values))] - public void Parse_String_Int_Test(Subnet expected, string addressString, int routePrefix) - { - // Arrange - // Act - var subnet = Subnet.Parse(addressString, routePrefix); - - // Assert - Assert.Equal(expected, subnet); - } - - #endregion // end: Parse(string, int) - - #region TryParse(string, int) - - public static IEnumerable TryParse_String_Int_Test_Values() - { - yield return new object[] { false, null, null, 0 }; - yield return new object[] { false, null, "potato", 0 }; - yield return new object[] { false, null, IPAddress.Any.ToString(), -5 }; - yield return new object[] { false, null, IPAddress.IPv6Any.ToString(), -5 }; - yield return new object[] { false, null, IPAddress.Any.ToString(), 33 }; - yield return new object[] { false, null, IPAddress.IPv6Any.ToString(), 129 }; - yield return new object[] { false, null, null, 0 }; - - foreach (var ipAddress in IPv4Addresses()) - { - for (var i = 0; i <= 32; i++) - { - var subnet = new Subnet(ipAddress, i); - yield return new object[] { true, subnet, subnet.Head.ToString(), i }; - } - } - - foreach (var ipAddress in IPv6Addresses()) - { - for (var i = 0; i <= 128; i++) - { - var subnet = new Subnet(ipAddress, i); - yield return new object[] { true, subnet, subnet.Head.ToString(), i }; - } - } - - IEnumerable IPv4Addresses() - { - yield return IPAddress.Any; - yield return IPAddress.Loopback; - yield return IPAddress.None; - yield return IPAddress.Parse("192.168.1.1"); - } - - IEnumerable IPv6Addresses() - { - yield return IPAddress.IPv6Any; - yield return IPAddress.IPv6Loopback; - yield return IPAddress.Parse("2001:0db8:85a3:0042:1000:8a2e:0370:7334"); - } - } - - [Theory] - [MemberData(nameof(TryParse_String_Int_Test_Values))] - public void TryParse_String_Int_Test(bool expectedSuccess, Subnet expectedSubnet, string addressString, int routePrefix) - { - // Arrange - // Act - var success = Subnet.TryParse(addressString, routePrefix, out var subnet); - - // Assert - Assert.Equal(expectedSuccess, success); - Assert.Equal(expectedSubnet, subnet); - } - - #endregion // end: TryParse(string, int) - - #region Parse(string, string) - - public static IEnumerable Parse_String_String_Test_Values() - { - foreach (var ipAddress in IPv4Addresses()) - { - for (var i = 0; i <= 32; i++) - { - var subnet = new Subnet(ipAddress, i); - yield return new object[] { subnet, subnet.Head.ToString(), subnet.Tail.ToString() }; - } - } - - foreach (var ipAddress in IPv6Addresses()) - { - for (var i = 0; i <= 128; i++) - { - var subnet = new Subnet(ipAddress, i); - yield return new object[] { subnet, subnet.Head.ToString(), subnet.Tail.ToString() }; - } - } - - IEnumerable IPv4Addresses() - { - yield return IPAddress.Any; - yield return IPAddress.Loopback; - yield return IPAddress.None; - yield return IPAddress.Parse("192.168.1.1"); - } - - IEnumerable IPv6Addresses() - { - yield return IPAddress.IPv6Any; - yield return IPAddress.IPv6Loopback; - yield return IPAddress.Parse("2001:0db8:85a3:0042:1000:8a2e:0370:7334"); - } - } - - [Theory] - [MemberData(nameof(Parse_String_String_Test_Values))] - public void Parse_String_String_Test(Subnet expected, string low, string high) - { - // Arrange - // Act - var subnet = Subnet.Parse(low, high); - - // Assert - Assert.Equal(expected, subnet); - } - - [Theory] - [InlineData("::", null)] - [InlineData(null, "::")] - [InlineData("192.168.1.1", null)] - [InlineData(null, "192.168.1.1")] - [InlineData(null, null)] - public void Parse_String_String_NullAddressString_Throws_ArgumentNullException_Test(string low, string high) - { - // Arrange - // Act - // Assert - Assert.Throws(() => Subnet.Parse(low, high)); - } - - [Theory] - [InlineData("::", "potato")] - [InlineData("potato", "::")] - [InlineData("192.168.1.1", "potato")] - [InlineData("potato", "192.168.1.1")] - public void Parse_String_String_BadAddressFormat_Throws_ArgumentException_Test(string low, string high) - { - // Arrange - // Act - // Assert - Assert.Throws(() => Subnet.Parse(low, high)); - } - - [Theory] - [InlineData("192.168.1.1", "::")] - [InlineData("::", "192.168.1.1")] - public void Parse_String_String_MisMatchAddressFamily_Throws_ArgumentException_Test(string low, string high) - { - // Arrange - // Act - // Assert - Assert.Throws(() => Subnet.Parse(low, high)); - } - - [Theory] - [InlineData("192.168.1.32", "192.168.1.0")] - [InlineData("::20", "::")] - public void Parse_String_String_InvalidRange_Throws_InvalidOperationException_Test(string low, string high) - { - // Arrange - // Act - // Assert - Assert.Throws(() => Subnet.Parse(low, high)); - } - - #endregion Parse(string, string) - - #region TryParse(string, string) - - public static IEnumerable TryParse_String_String_Test_Values() - { - foreach (var s in new[] { null, string.Empty, "\t", "potato" }) - { - yield return new object[] { null, "192.168.1.1", s }; - yield return new object[] { null, s, "192.168.1.1" }; - yield return new object[] { null, "2001:0db8:85a3:0042:1000:8a2e:0370:7334", s }; - yield return new object[] { null, s, "2001:0db8:85a3:0042:1000:8a2e:0370:7334" }; - } - - foreach (var ipAddress in IPv4Addresses()) - { - for (var i = 0; i <= 32; i++) - { - var subnet = new Subnet(ipAddress, i); - yield return new object[] { subnet, subnet.Head.ToString(), subnet.Tail.ToString() }; - } - } - - foreach (var ipAddress in IPv6Addresses()) - { - for (var i = 0; i <= 128; i++) - { - var subnet = new Subnet(ipAddress, i); - yield return new object[] { subnet, subnet.Head.ToString(), subnet.Tail.ToString() }; - } - } - - IEnumerable IPv4Addresses() - { - yield return IPAddress.Any; - yield return IPAddress.Loopback; - yield return IPAddress.None; - yield return IPAddress.Parse("192.168.1.1"); - } - - IEnumerable IPv6Addresses() - { - yield return IPAddress.IPv6Any; - yield return IPAddress.IPv6Loopback; - yield return IPAddress.Parse("2001:0db8:85a3:0042:1000:8a2e:0370:7334"); - } - } - - [Theory] - [MemberData(nameof(TryParse_String_String_Test_Values))] - public void TryParse_String_String_Test(Subnet expected, string low, string high) - { - // Arrange - - // Act - var success = Subnet.TryParse(low, high, out var subnet); - - // Assert - Assert.Equal(expected != null, success); - Assert.Equal(expected, subnet); - } - - #endregion // end: TryParse(string, string) - - #endregion // end: Static Factory Methods - - #region Length / TryGetLength - - public static IEnumerable Length_Test_Values() - { - foreach (var ipAddress in IPv4Addresses()) - { - for (var i = 0; i <= 32; i++) - { - var routePrefix = 32 - i; - var length = BigInteger.Pow(2, routePrefix); - - var subnet = new Subnet(ipAddress, i); - yield return new object[] { length, subnet }; - } - } - - foreach (var ipAddress in IPv6Addresses()) - { - for (var i = 0; i <= 128; i++) - { - var routePrefix = 128 - i; - var length = BigInteger.Pow(2, routePrefix); - - var subnet = new Subnet(ipAddress, i); - yield return new object[] { length, subnet }; - } - } - - IEnumerable IPv4Addresses() - { - yield return IPAddress.Parse("192.168.1.1"); - } - - IEnumerable IPv6Addresses() - { - yield return IPAddress.Parse("2001:0db8:85a3:0042:1000:8a2e:0370:7334"); - } - } - - [Theory] - [MemberData(nameof(Length_Test_Values))] - public static void Length_Test(BigInteger expected, Subnet subnet) - { - // Arrange - // Act - - var result = subnet.Length; - - // Assert - Assert.Equal(expected, result); - } - - [Theory] - [MemberData(nameof(Length_Test_Values))] - public static void TryGetLength_Integer_Test(BigInteger expected, Subnet subnet) - { - // Arrange - // Act - var success = subnet.TryGetLength(out int length); - - // Assert - Assert.Equal(expected <= int.MaxValue, success); - Assert.Equal(expected <= int.MaxValue ? (int)expected : -1, length); - } - - [Theory] - [MemberData(nameof(Length_Test_Values))] - public static void TryGetLength_Long_Test(BigInteger expected, Subnet subnet) - { - // Arrange - // Act - var success = subnet.TryGetLength(out long length); - - // Assert - Assert.Equal(expected <= long.MaxValue, success); - Assert.Equal(expected <= long.MaxValue ? (long)expected : -1, length); - } - - #endregion // end: Length - - #region Equals - - #region Equals(Subnet) - - public static IEnumerable Equals_Subnet_Test_Values() - { - foreach (var ipAddress in IPv4Addresses()) - { - for (var i = 0; i <= 32; i++) - { - yield return new object[] { true, new Subnet(ipAddress, i), new Subnet(ipAddress, i) }; // equivalent - - yield return new object[] { false, new Subnet(ipAddress, i), new Subnet(ipAddress, (i + 2) % 32) }; // differing routes equivalent - - var subnet = new Subnet(ipAddress, i); - yield return new object[] { true, subnet, subnet }; // same - - foreach (var ipv6Address in IPv6Addresses().Take(2)) - { - yield return new object[] { false, new Subnet(ipAddress, i), new Subnet(ipv6Address, i) }; // different families - } - } - } - - foreach (var ipAddress in IPv6Addresses()) - { - for (var i = 0; i <= 128; i++) - { - yield return new object[] { true, new Subnet(ipAddress, i), new Subnet(ipAddress, i) }; // equivalent - - yield return new object[] { false, new Subnet(ipAddress, i), new Subnet(ipAddress, (i + 2) % 128) }; // differing routes equivalent - - var subnet = new Subnet(ipAddress, i); - yield return new object[] { true, subnet, subnet }; // same - } - } - - IEnumerable IPv4Addresses() - { - yield return IPAddress.Any; - yield return IPAddress.Loopback; - yield return IPAddress.None; - yield return IPAddress.Parse("192.168.1.1"); - } - - IEnumerable IPv6Addresses() - { - yield return IPAddress.IPv6Any; - yield return IPAddress.IPv6Loopback; - yield return IPAddress.Parse("2001:0db8:85a3:0042:1000:8a2e:0370:7334"); - } - } - - [Theory] - [MemberData(nameof(Equals_Subnet_Test_Values))] - public void Equals_Subnet_Test(bool expected, Subnet subnetA, Subnet subnetB) - { - // Arrange - - // Act - var result = subnetA.Equals(subnetB); - - // Assert - Assert.Equal(expected, result); - Assert.Equal(result, expected); - } - - #endregion // end: Equals(Subnet) - - #region Equals(object) - - [Theory] - [MemberData(nameof(Equals_Subnet_Test_Values))] - public void Equals_Object_Test(bool expected, Subnet subnetA, object subnetB) - { - // Arrange - - // Act - var result = subnetA.Equals(subnetB); - - // Assert - Assert.Equal(expected, result); - } - - #endregion // end: Equals(object) - - #endregion // end: Equals - - #region FromNetMask - - public static IEnumerable FromNetMask_Test_Values() - { - var networkPrefix = IPAddress.Parse("192.168.1.1"); - - for (var i = 0; i <= 32; i++) - { - var netmaskBytes = Enumerable.Repeat((byte)0xFF, 4).ToArray().ShiftBitsLeft(32 - i); - - var netmask = new IPAddress(netmaskBytes); - - var expected = new Subnet(networkPrefix, i); - - yield return new object[] { expected, networkPrefix, netmask }; - } - } - - [Theory] - [MemberData(nameof(FromNetMask_Test_Values))] - public void FromNetMask_Test(Subnet expected, IPAddress networkPrefix, IPAddress netmask) - { - // Arrange - // Act - var result = Subnet.FromNetMask(networkPrefix, netmask); - - // Assert - Assert.Equal(expected, result); - Assert.Equal(netmask, result.Netmask); - } - - [Fact] - public void FromNetMask_AddressNull_Throws_ArgumentNullException_Test() - { - // Act - // Arrange - // Assert - - Assert.Throws(() => Subnet.FromNetMask(null, IPAddress.Any)); - } - - [Fact] - public void FromNetMask_InvalidNetMask_Throws_ArgumentNullException_Test() - { - // Act - // Arrange - // Assert - Assert.Throws(() => Subnet.FromNetMask(IPAddress.Any, IPAddress.IPv6Any)); - } - - [Fact] - public void FromNetMask_IPv6Address_Throws_ArgumentNullException_Test() - { - // Act - // Arrange - // Assert - Assert.Throws(() => Subnet.FromNetMask(IPAddress.IPv6Any, IPAddress.Any)); - } - - [Fact] - public void FromNetMaskNetMask_NullNetMask_Throws_ArgumentNullException_Test() - { - // Act - // Arrange - // Assert - - Assert.Throws(() => Subnet.FromNetMask(IPAddress.Any, null)); - } - - public static IEnumerable TryFromNetMask_Test_Values() - { - var networkPrefix = IPAddress.Parse("192.168.1.1"); - - for (var i = 0; i <= 32; i++) - { - var netmaskBytes = Enumerable.Repeat((byte)0xFF, 4).ToArray().ShiftBitsLeft(32 - i); - - var netmask = new IPAddress(netmaskBytes); - - var expected = new Subnet(networkPrefix, i); - - yield return new object[] { true, expected, networkPrefix, netmask }; - } - - yield return new object[] { false, null, null, IPAddress.Parse("192.168.1.1") }; - yield return new object[] { false, null, IPAddress.Parse("192.168.1.1"), null }; - yield return new object[] { false, null, null, IPAddress.Parse("::") }; - yield return new object[] { false, null, IPAddress.Parse("::"), null }; - yield return new object[] { false, null, null, null }; - } - - [Theory] - [MemberData(nameof(TryFromNetMask_Test_Values))] - public void TryFromNetMask_Test(bool expectedSuccess, Subnet expectedSubnet, IPAddress networkPrefix, IPAddress netmask) - { - // Arrange - // Act - var success = Subnet.TryFromNetMask(networkPrefix, netmask, out var subnet); - - // Assert - Assert.Equal(expectedSuccess, success); - Assert.Equal(expectedSubnet, subnet); - } - - #endregion - - #region Formatting - - #region ToString - - [Theory] - [InlineData("192.168.1.1", "192.168.1.42")] - [InlineData("::beef", "0123::dead")] - public void ToString_MatchesGeneralFormat_Test(string headString, string tailString) - { - // Arrange - var head = IPAddress.Parse(headString); - var tail = IPAddress.Parse(tailString); + // Arrange + var head = IPAddress.Parse(headString); + var tail = IPAddress.Parse(tailString); var iPAddressRange = new IPAddressRange(head, tail); @@ -1583,20 +850,23 @@ public void ToString_MatchesGeneralFormat_Test(string headString, string tailStr #region ToString(string, IFormatProvider) - public static IEnumerable ToString_Format_Test_Values() + /// Gets theory data for . + /// Parameters: expected string result, format specifier string, format provider (), subnet under test (). + public static TheoryData ToString_Format_Test_Values() { + var data = new TheoryData(); + // general formats foreach (var format in new[] { null, string.Empty, "g", "G" }) { foreach (var subnet in Ipv4AddressSubnets().Concat(Ipv6AddressSubnets())) { - yield return new object[] - { + data.Add( $"{subnet.NetworkPrefixAddress}/{subnet.RoutingPrefix}", format, CultureInfo.CurrentCulture, - subnet, - }; + subnet + ); } } @@ -1605,15 +875,14 @@ public static IEnumerable ToString_Format_Test_Values() { foreach (var subnet in Ipv4AddressSubnets().Concat(Ipv6AddressSubnets())) { - yield return new object[] - { + data.Add( subnet.IsSingleIP ? $"{subnet.NetworkPrefixAddress}" : $"{subnet.NetworkPrefixAddress}/{subnet.RoutingPrefix}", format, CultureInfo.CurrentCulture, - subnet, - }; + subnet + ); } } @@ -1622,10 +891,12 @@ public static IEnumerable ToString_Format_Test_Values() { foreach (var subnet in Ipv4AddressSubnets().Concat(Ipv6AddressSubnets())) { - yield return new object[] { $"{subnet.Head} - {subnet.Tail}", format, CultureInfo.CurrentCulture, subnet }; + data.Add($"{subnet.Head} - {subnet.Tail}", format, CultureInfo.CurrentCulture, subnet); } } + return data; + IEnumerable Ipv4AddressSubnets() { yield return new Subnet(IPAddress.Parse("192.168.1.1"), 16); @@ -1639,6 +910,11 @@ IEnumerable Ipv6AddressSubnets() } } + /// Verifies that returns the expected formatted string. + /// Expected formatted string. + /// Format specifier. + /// Format provider. + /// The subnet to format. [Theory] [MemberData(nameof(ToString_Format_Test_Values))] public void ToString_Format_Test(string expected, string format, IFormatProvider formatProvider, Subnet subnet) @@ -1651,68 +927,53 @@ public void ToString_Format_Test(string expected, string format, IFormatProvider Assert.Equal(expected, result); } + /// Verifies that uses invariant culture when the format provider is null. [Fact] - public void ToString_UnknownFormat_Throws_FormatException_Test() + public void ToString_NullFormatProvider_UsesInvariantCulture_Test() { // Arrange - var range = new Subnet(IPAddress.Parse("192.168.1.1"), 16); + var subnet = new Subnet(IPAddress.Parse("192.168.1.0"), 24); // Act - // Assert - Assert.Throws(() => range.ToString("potato", CultureInfo.CurrentCulture)); - } - - #endregion // end: ToString(string, IFormatProvider) + var result = subnet.ToString("G", null); - #endregion // end: Formatting - - #region GetHashCode - - public static IEnumerable GetHashCode_Test_Values() - { - // equal - yield return new object[] { true, new Subnet(IPAddress.Any, 16), new Subnet(IPAddress.Any, 16) }; - yield return new object[] { true, new Subnet(IPAddress.IPv6Any, 16), new Subnet(IPAddress.IPv6Any, 16) }; - - // reference equal - var ipv4Subnet = new Subnet(IPAddress.Any, 16); - yield return new object[] { true, ipv4Subnet, ipv4Subnet }; - - var ipv6Subnet = new Subnet(IPAddress.IPv6Any, 16); - yield return new object[] { true, ipv6Subnet, ipv6Subnet }; - - // expected different - yield return new object[] { false, ipv4Subnet, ipv6Subnet }; - yield return new object[] { false, new Subnet(IPAddress.Any, 8), new Subnet(IPAddress.Any, 16) }; - yield return new object[] { false, new Subnet(IPAddress.IPv6Any, 8), new Subnet(IPAddress.IPv6Any, 16) }; - yield return new object[] { false, new Subnet(IPAddress.Any, 16), new Subnet(IPAddress.Broadcast, 16) }; - yield return new object[] { false, new Subnet(IPAddress.Parse("::")), new Subnet(IPAddress.Parse("ab::"), 16) }; + // Assert + Assert.Equal("192.168.1.0/24", result); } - [Theory] - [MemberData(nameof(GetHashCode_Test_Values))] - public void GetHashCode_Test(bool expectedEqual, Subnet left, Subnet right) + /// Verifies that throws for an unknown format specifier. + [Fact] + public void ToString_UnknownFormat_Throws_FormatException_Test() { // Arrange - // Act - var leftHash = left.GetHashCode(); - var rightHash = right.GetHashCode(); + var range = new Subnet(IPAddress.Parse("192.168.1.1"), 16); - // Assert - Assert.Equal(expectedEqual, leftHash == rightHash); + // Act / Assert + Assert.Throws(() => range.ToString("potato", CultureInfo.CurrentCulture)); } - #endregion // end: GetHashCode + #endregion // end: ToString(string, IFormatProvider) + + #endregion // end: Formatting #region Deconstruct - public static IEnumerable Deconstruct_Values() + /// Gets theory data for deconstruct tests. + /// Parameters: subnet under test (). + public static TheoryData Deconstruct_Values() { + var data = new TheoryData(); + var seen = new HashSet(); + foreach (var ipAddress in IPv4Addresses()) { for (var i = 0; i <= 32; i++) { - yield return new object[] { new Subnet(ipAddress, i) }; + var subnet = new Subnet(ipAddress, i); + if (seen.Add(subnet.ToString("f", null))) + { + data.Add(subnet); + } } } @@ -1720,10 +981,16 @@ public static IEnumerable Deconstruct_Values() { for (var i = 0; i <= 128; i++) { - yield return new object[] { new Subnet(ipAddress, i) }; + var subnet = new Subnet(ipAddress, i); + if (seen.Add(subnet.ToString("f", null))) + { + data.Add(subnet); + } } } + return data; + IEnumerable IPv4Addresses() { yield return IPAddress.Parse("192.168.1.1"); @@ -1739,6 +1006,8 @@ IEnumerable IPv6Addresses() #region Deconstruct(IPAddress, IPAddress, IPAddress, int) + /// Verifies that the four-element deconstruct of a yields the expected components. + /// The subnet to deconstruct. [Theory] [MemberData(nameof(Deconstruct_Values))] public void Deconstruct_IPAddress_IPAddress_IPAddress_Int_Test(Subnet subnet) @@ -1758,6 +1027,8 @@ public void Deconstruct_IPAddress_IPAddress_IPAddress_Int_Test(Subnet subnet) #region Deconstruct(IPAddress, int) + /// Verifies that the two-element deconstruct of a yields the network prefix address and routing prefix. + /// The subnet to deconstruct. [Theory] [MemberData(nameof(Deconstruct_Values))] public void Deconstruct_IPAddress_Int_Test(Subnet subnet) @@ -1775,9 +1046,52 @@ public void Deconstruct_IPAddress_Int_Test(Subnet subnet) #endregion // end: Deconstruct + #region Ctor refactoring verification + + /// Verifies that the Subnet(IPAddress, IPAddress) ctor computes correct prefix and netmask. + /// CIDR notation expected after construction from low/high addresses. + /// Low address string. + /// High address string. + [Theory] + [InlineData("192.168.1.0/24", "192.168.1.0", "192.168.1.255")] + [InlineData("192.168.1.8/29", "192.168.1.8", "192.168.1.15")] + [InlineData("10.0.0.0/8", "10.0.0.0", "10.255.255.255")] + [InlineData("::/112", "::", "::ffff")] + [InlineData("feed:beef::/32", "feed:beef::", "feed:beef:ffff:ffff:ffff:ffff:ffff:ffff")] + public void Ctor_FromLowHigh_ComputesPrefixAndNetmask_Test(string subnetString, string lowString, string highString) + { + // Arrange + var expected = Subnet.Parse(subnetString); + var low = IPAddress.Parse(lowString); + var high = IPAddress.Parse(highString); + + // Act + var subnet = new Subnet(low, high); + + // Assert + Assert.Equal(expected.RoutingPrefix, subnet.RoutingPrefix); + Assert.Equal(expected.NetworkPrefixAddress, subnet.NetworkPrefixAddress); + Assert.Equal(expected.BroadcastAddress, subnet.BroadcastAddress); + Assert.Equal(expected.Netmask, subnet.Netmask); + } + + #endregion // end: Ctor refactoring verification + + #region maxEnumerationExponent propagation + + /// Verifies that propagates maxEnumerationExponent. + [Fact] + public void Ctor_IPAddress_ExponentPropagates_Test() + { + var subnet = new Subnet(IPAddress.Parse("10.0.0.1"), 32, maxEnumerationExponent: 4); + Assert.Equal(4, subnet.MaxEnumerationExponent); + } + + #endregion // end: maxEnumerationExponent propagation + internal class SubnetEqualityComparer : IEqualityComparer { - public static readonly SubnetEqualityComparer Instance = new SubnetEqualityComparer(); + public static readonly SubnetEqualityComparer Instance = new(); public bool Equals(Subnet x, Subnet y) { diff --git a/src/Arcus.Tests/TestByteArrayExtensions.cs b/src/Arcus.Tests/TestByteArrayExtensions.cs new file mode 100644 index 0000000..2da77d1 --- /dev/null +++ b/src/Arcus.Tests/TestByteArrayExtensions.cs @@ -0,0 +1,25 @@ +namespace Arcus.Tests +{ + /// + /// Extension methods for byte arrays used by test code. + /// + /// + /// + /// The method avoids overload resolution selecting + /// MemoryExtensions.Reverse(Span<T>) (C# 13+) instead of + /// Enumerable.Reverse<T>(), preserving the expected return type. + /// + /// + internal static class TestByteArrayExtensions + { + /// + /// Returns the bytes in reverse order as an . + /// + /// The byte array to reverse. + /// The reversed byte sequence. + public static IEnumerable Reverse(this byte[] bytes) + { + return Enumerable.Reverse(bytes); + } + } +} diff --git a/src/Arcus.Tests/Utilities/IPAddressUtilitiesTests.cs b/src/Arcus.Tests/Utilities/IPAddressUtilitiesTests.cs index e791261..3fb95c7 100644 --- a/src/Arcus.Tests/Utilities/IPAddressUtilitiesTests.cs +++ b/src/Arcus.Tests/Utilities/IPAddressUtilitiesTests.cs @@ -1,22 +1,17 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; +using System.Net; using System.Net.Sockets; +using System.Text.RegularExpressions; using Arcus.Math; using Arcus.Utilities; -using Gulliver; -using Xunit; namespace Arcus.Tests.Utilities { -#if NET6_0_OR_GREATER -#pragma warning disable IDE0062 // Make local function static (IDE0062); purposely allowing non-static functions that could be static for .net4.8 compatibility -#endif - public class IPAddressUtilitiesTests + /// Unit tests for . + public partial class IPAddressUtilitiesTests { #region IPv4MaxAddress + /// Verifies that returns 255.255.255.255. [Fact] public void IPv4MaxAddress_Test() { @@ -32,6 +27,7 @@ public void IPv4MaxAddress_Test() #region IPv4MinAddress + /// Verifies that returns 0.0.0.0. [Fact] public void IPv4MinAddress_Test() { @@ -47,6 +43,7 @@ public void IPv4MinAddress_Test() #region IPv4OctetCount + /// Verifies that equals 4. [Fact] public void IPv4OctetCount_Test() { @@ -56,10 +53,11 @@ public void IPv4OctetCount_Test() Assert.Equal(4, IPAddressUtilities.IPv4OctetCount); } - #endregion end: IPv4OctetCount + #endregion // end: IPv4OctetCount #region IPv6HextetCount + /// Verifies that equals 8. [Fact] public void IPv6HextetCount_Test() { @@ -69,10 +67,11 @@ public void IPv6HextetCount_Test() Assert.Equal(8, IPAddressUtilities.IPv6HextetCount); } - #endregion end: IPv6HextetCount + #endregion // end: IPv6HextetCount #region IPv6MaxAddress + /// Verifies that returns ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff. [Fact] public void IPv6MaxAddress_Test() { @@ -88,6 +87,7 @@ public void IPv6MaxAddress_Test() #region IPv6MinAddress + /// Verifies that returns the all-zeros IPv6 address. [Fact] public void IPv6MinAddress_Test() { @@ -103,13 +103,24 @@ public void IPv6MinAddress_Test() #region IsIPv4 + /// Gets theory data for . + /// Parameters: expected (bool), input (string). + public static TheoryData IsIPv4_Test_Data => + new() + { + { false, null }, + { false, "::" }, + { false, "ffff::" }, + { true, "192.168.1.1" }, + { true, "0.0.0.0" }, + }; + + /// Verifies that returns the expected result for the given input. + /// Expected result of the call. + /// String representation of the IP address to test. [Theory] - [InlineData(false, null)] - [InlineData(false, "::")] - [InlineData(false, "ffff::")] - [InlineData(true, "192.168.1.1")] - [InlineData(true, "0.0.0.0")] - public void IsIPv4_Test(bool expected, string input) + [MemberData(nameof(IsIPv4_Test_Data))] + public void IsIPv4_ReturnsExpected_Test(bool expected, string input) { // Arrange _ = IPAddress.TryParse(input, out var address); @@ -125,15 +136,26 @@ public void IsIPv4_Test(bool expected, string input) #region IsIPv4MappedIPv6 + /// Gets theory data for . + /// Parameters: expected (bool), input (string). + public static TheoryData IsIPv4MappedIPv6_Test_Data => + new() + { + { false, null }, + { false, "::" }, + { false, "192.168.1.1" }, + { true, "::ffff:222.1.41.90" }, + { true, "::ffff:ab:cd" }, + { false, "1234::ffff:222.1.41.90" }, + { false, "1234::ffff:ab:cd" }, + }; + + /// Verifies that returns the expected result for the given input. + /// Expected result of the call. + /// String representation of the IP address to test. [Theory] - [InlineData(false, null)] - [InlineData(false, "::")] - [InlineData(false, "192.168.1.1")] - [InlineData(true, "::ffff:222.1.41.90")] - [InlineData(true, "::ffff:ab:cd")] - [InlineData(false, "1234::ffff:222.1.41.90")] - [InlineData(false, "1234::ffff:ab:cd")] - public void IsIPv4MappedIPv6_Test(bool expected, string input) + [MemberData(nameof(IsIPv4MappedIPv6_Test_Data))] + public void IsIPv4MappedIPv6_ReturnsExpected_Test(bool expected, string input) { // Arrange _ = IPAddress.TryParse(input, out var address); @@ -149,13 +171,24 @@ public void IsIPv4MappedIPv6_Test(bool expected, string input) #region IsIPv6 + /// Gets theory data for . + /// Parameters: expected (bool), input (string). + public static TheoryData IsIPv6_Test_Data => + new() + { + { false, null }, + { true, "::" }, + { true, "ffff::" }, + { false, "192.168.1.1" }, + { false, "0.0.0.0" }, + }; + + /// Verifies that returns the expected result for the given input. + /// Expected result of the call. + /// String representation of the IP address to test. [Theory] - [InlineData(false, null)] - [InlineData(true, "::")] - [InlineData(true, "ffff::")] - [InlineData(false, "192.168.1.1")] - [InlineData(false, "0.0.0.0")] - public void IsIPv6_Test(bool expected, string input) + [MemberData(nameof(IsIPv6_Test_Data))] + public void IsIPv6_ReturnsExpected_Test(bool expected, string input) { // Arrange _ = IPAddress.TryParse(input, out var address); @@ -169,30 +202,17 @@ public void IsIPv6_Test(bool expected, string input) #endregion // end: IsIPv6 - #region other members + #region shared helpers (private) private static IEnumerable NonStandardAddressFamilies() { +#if NET48 return Enum.GetValues(typeof(AddressFamily)) .Cast() - .Except(new[] { AddressFamily.InterNetwork, AddressFamily.InterNetworkV6 }); - } - - public static IEnumerable InvalidAddressFamily_Values() - { - return Enum.GetValues(typeof(AddressFamily)) - .Cast() - .Where(addressFamily => - addressFamily != AddressFamily.InterNetworkV6 && addressFamily != AddressFamily.InterNetwork - ) - .Distinct() - .Select(e => new object[] { e }); - } - - public static IEnumerable ValidAddressFamily_Values() - { - yield return new object[] { AddressFamily.InterNetwork }; - yield return new object[] { AddressFamily.InterNetworkV6 }; + .Except([AddressFamily.InterNetwork, AddressFamily.InterNetworkV6]); +#else + return Enum.GetValues().Except([AddressFamily.InterNetwork, AddressFamily.InterNetworkV6]); +#endif } private static IEnumerable GeneralPurposeIPv4Addresses() @@ -238,42 +258,50 @@ private static IEnumerable GeneralPurposeIPv6Addresses() yield return IPAddress.IPv6Loopback; } - #endregion + #endregion // end: shared helpers (private) #region IsValidNetMask - public static IEnumerable IsValidNetMask_Test_Values() + /// Gets theory data for . + /// Parameters: expected (bool), input (IPAddress). + public static TheoryData IsValidNetMask_Test_Data() { + var data = new TheoryData(); + // all valid netmask values for (var i = 0; i <= 32; i++) { - var netmaskBytes = Enumerable.Repeat((byte)0xFF, 4).ToArray().ShiftBitsLeft(32 - i); - - yield return new object[] { true, new IPAddress(netmaskBytes) }; + var netmaskBytes = BigEndianBitWrapper.CreateMask(4, i).ToBytes(); + data.Add(true, new IPAddress(netmaskBytes)); } - yield return new object[] { false, null }; + data.Add(false, null); var invalidNetmaskAddressStrings = new[] { "::", "ffff::", "255.255.0.255", - "255.255.0.255", "255.0.255.255", "0.255.255.255", "0.0.0.255", "0.0.0.1", }; + foreach (var s in invalidNetmaskAddressStrings) { - yield return new object[] { false, IPAddress.Parse(s) }; + data.Add(false, IPAddress.Parse(s)); } + + return data; } + /// Verifies that returns the expected result for the given input. + /// Expected result of the call. + /// IP address to test. [Theory] - [MemberData(nameof(IsValidNetMask_Test_Values))] - public void IsValidNetMask_Test(bool expected, IPAddress input) + [MemberData(nameof(IsValidNetMask_Test_Data))] + public void IsValidNetMask_ReturnsExpected_Test(bool expected, IPAddress input) { // Arrange // Act @@ -289,96 +317,104 @@ public void IsValidNetMask_Test(bool expected, IPAddress input) #region ParseFromHexString / TryParseFromHexString - public static IEnumerable ParseFromHexString_Test_Values() + /// Gets theory data for hex-string parse tests with valid inputs. + /// Parameters: expected (IPAddress), addressString (string), addressFamily (AddressFamily). + public static TheoryData ParseFromHexString_Valid_Test_Data() { - foreach (var address in Addresses()) + var data = new TheoryData(); + + foreach (var address in HexParseAddresses()) { var asHex = AddressToHexString(address); - yield return new object[] { address, asHex.ToUpperInvariant(), address.AddressFamily }; - yield return new object[] { address, asHex.ToLowerInvariant(), address.AddressFamily }; - yield return new object[] { address, $"0x{asHex}".ToUpperInvariant(), address.AddressFamily }; - yield return new object[] { address, $"0x{asHex}".ToLowerInvariant(), address.AddressFamily }; + data.Add(address, asHex.ToUpperInvariant(), address.AddressFamily); + data.Add(address, asHex.ToLowerInvariant(), address.AddressFamily); + data.Add(address, $"0x{asHex}".ToUpperInvariant(), address.AddressFamily); + data.Add(address, $"0x{asHex}".ToLowerInvariant(), address.AddressFamily); - // removed most significant zero bytes - var msbZeroTrim = new string(asHex.SkipWhile(c => c == '0').ToArray()); + var msbZeroTrim = new string([.. asHex.SkipWhile(c => c == '0')]); if (!string.IsNullOrEmpty(msbZeroTrim)) { - yield return new object[] { address, msbZeroTrim.ToUpperInvariant(), address.AddressFamily }; - yield return new object[] { address, msbZeroTrim.ToLowerInvariant(), address.AddressFamily }; - yield return new object[] { address, $"0x{msbZeroTrim}".ToUpperInvariant(), address.AddressFamily }; - yield return new object[] { address, $"0x{msbZeroTrim}".ToLowerInvariant(), address.AddressFamily }; + data.Add(address, msbZeroTrim.ToUpperInvariant(), address.AddressFamily); + data.Add(address, msbZeroTrim.ToLowerInvariant(), address.AddressFamily); + data.Add(address, $"0x{msbZeroTrim}".ToUpperInvariant(), address.AddressFamily); + data.Add(address, $"0x{msbZeroTrim}".ToLowerInvariant(), address.AddressFamily); } } - yield return new object[] { IPAddress.Parse("128.128.128.128"), "00000000080808080", AddressFamily.InterNetwork }; // extra zero msb + data.Add(IPAddress.Parse("128.128.128.128"), "00000000080808080", AddressFamily.InterNetwork); - // expected failures - yield return new object[] { null, null, AddressFamily.InterNetwork }; - yield return new object[] { null, null, AddressFamily.InterNetworkV6 }; - yield return new object[] - { - null, - AddressToHexString(IPAddress.Parse("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")), - AddressFamily.InterNetwork, - }; + return data; + } - // non standard address family - foreach (var addressFamily in NonStandardAddressFamilies()) + /// Gets theory data for hex-string parse tests with invalid address families. + /// Parameters: addressFamily (AddressFamily). + public static TheoryData ParseFromHexString_InvalidAddressFamily_Test_Data() + { + var data = new TheoryData(); + foreach (var af in NonStandardAddressFamilies()) { - yield return new object[] { null, "0", addressFamily }; + data.Add(af); } - IEnumerable Addresses() - { - yield return IPAddress.Any; - yield return IPAddress.Loopback; - yield return IPAddress.None; - yield return IPAddress.Parse("192.168.1.1"); - yield return IPAddress.Parse("255.255.255"); - - yield return IPAddress.IPv6Any; - yield return IPAddress.IPv6Loopback; - yield return IPAddress.Parse("2001:0db8:85a3:0042:1000:8a2e:0370:7334"); - yield return IPAddress.Parse("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"); - yield return IPAddress.Parse("ffff:ffff:ffff:ffff::"); - yield return IPAddress.Parse("::abc:ffff:ffff:ffff"); - } + return data; + } - string AddressToHexString(IPAddress address) - { - return string.Concat(address.GetAddressBytes().Select(b => Convert.ToString(b, 16).PadLeft(2, '0'))); - } + /// Verifies that returns the expected address for valid hex inputs. + /// Expected parsed IP address. + /// Hex string representation of the address. + /// Address family used to parse the hex string. + [Theory] + [MemberData(nameof(ParseFromHexString_Valid_Test_Data))] + public void ParseFromHexString_ValidInput_ReturnsExpected_Test( + IPAddress expected, + string addressString, + AddressFamily addressFamily + ) + { + // Arrange + // Act + var result = IPAddressUtilities.ParseFromHexString(addressString, addressFamily); + + // Assert + Assert.Equal(expected, result); } + /// Verifies that parses all-zero input as the zero address rather than throwing. + /// An all-zero hex string (optionally "0x"-prefixed). + /// The target address family. [Theory] - [MemberData(nameof(ParseFromHexString_Test_Values))] - public void ParseFromHexString_Test(IPAddress expected, string addressString, AddressFamily addressFamily) + [InlineData("0", AddressFamily.InterNetwork)] + [InlineData("00", AddressFamily.InterNetwork)] + [InlineData("0x0", AddressFamily.InterNetwork)] + [InlineData("0x00", AddressFamily.InterNetwork)] + [InlineData("0", AddressFamily.InterNetworkV6)] + [InlineData("0x0", AddressFamily.InterNetworkV6)] + public void ParseFromHexString_AllZeroInput_ReturnsZeroAddress_Test(string input, AddressFamily addressFamily) { // Arrange - if (expected == null) - { - return; // ignore invalid test - } + var expected = addressFamily == AddressFamily.InterNetwork ? IPAddress.Any : IPAddress.IPv6Any; // Act - var result = IPAddressUtilities.ParseFromHexString(addressString, addressFamily); + var result = IPAddressUtilities.ParseFromHexString(input, addressFamily); // Assert Assert.Equal(expected, result); } + /// Verifies that throws when given a null input. [Fact] public void ParseFromHexString_NullInput_Throws_ArgumentNullException_Test() { // Arrange // Act // Assert - Assert.Throws(() => IPAddressUtilities.ParseFromHexString(null, default)); } + /// Verifies that throws for empty or whitespace input. + /// Empty or whitespace string to test. [Theory] [InlineData("")] [InlineData("\t")] @@ -388,21 +424,23 @@ public void ParseFromHexString_EmptyOrWhitespaceInput_Throws_ArgumentException_T // Arrange // Act // Assert - - Assert.Throws(() => IPAddressUtilities.ParseFromHexString(input, default)); + Assert.Throws(() => IPAddressUtilities.ParseFromHexString(input, AddressFamily.InterNetwork)); } + /// Verifies that throws for non-standard address families. + /// Non-standard address family to test. [Theory] - [MemberData(nameof(InvalidAddressFamily_Values))] + [MemberData(nameof(ParseFromHexString_InvalidAddressFamily_Test_Data))] public void ParseFromHexString_InvalidAddressFamily_Throws_ArgumentException_Test(AddressFamily addressFamily) { // Arrange // Act // Assert - Assert.Throws(() => IPAddressUtilities.ParseFromHexString("abc123", addressFamily)); } + /// Verifies that throws for non-hex input strings. + /// Non-hex string to test. [Theory] [InlineData("abcdxyz")] [InlineData("potato")] @@ -412,85 +450,140 @@ public void ParseFromHexString_NonHexInput_Throws_ArgumentException_Test(string // Arrange // Act // Assert - Assert.Throws(() => IPAddressUtilities.ParseFromHexString(input, AddressFamily.InterNetwork)); } + /// Verifies that throws when an IPv6-length hex string is parsed as IPv4. + [Fact] + public void ParseFromHexString_IPv6HexTooLargeForIPv4_Throws_ArgumentOutOfRangeException_Test() + { + // Arrange + var ipv6HexString = AddressToHexString(IPAddress.Parse("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")); + + // Act + // Assert + Assert.Throws(() => + IPAddressUtilities.ParseFromHexString(ipv6HexString, AddressFamily.InterNetwork) + ); + } + + /// Verifies that returns true and the expected address for valid hex inputs. + /// Expected parsed IP address. + /// Hex string representation of the address. + /// Address family used to parse the hex string. [Theory] - [MemberData(nameof(ParseFromHexString_Test_Values))] - public void TryParseFromHexString_Test(IPAddress expected, string addressString, AddressFamily addressFamily) + [MemberData(nameof(ParseFromHexString_Valid_Test_Data))] + public void TryParseFromHexString_ValidInput_ReturnsTrue_Test( + IPAddress expected, + string addressString, + AddressFamily addressFamily + ) { // Arrange // Act var success = IPAddressUtilities.TryParseFromHexString(addressString, addressFamily, out var result); // Assert - Assert.Equal(expected != null, success); + Assert.True(success); Assert.Equal(expected, result); } + /// Verifies that returns false and a null result when given null input. + [Fact] + public void TryParseFromHexString_NullInput_ReturnsFalse_Test() + { + // Arrange + // Act + var success = IPAddressUtilities.TryParseFromHexString(null, AddressFamily.InterNetwork, out var result); + + // Assert + Assert.False(success); + Assert.Null(result); + } + + /// Verifies that returns false for non-standard address families. + /// Non-standard address family to test. + [Theory] + [MemberData(nameof(ParseFromHexString_InvalidAddressFamily_Test_Data))] + public void TryParseFromHexString_InvalidAddressFamily_ReturnsFalse_Test(AddressFamily addressFamily) + { + // Arrange + // Act + var success = IPAddressUtilities.TryParseFromHexString("abc123", addressFamily, out var result); + + // Assert + Assert.False(success); + Assert.Null(result); + } + + private static IEnumerable HexParseAddresses() + { + yield return IPAddress.Any; + yield return IPAddress.Loopback; + yield return IPAddress.None; + yield return IPAddress.Parse("192.168.1.1"); + yield return IPAddress.Parse("255.255.255"); + + yield return IPAddress.IPv6Any; + yield return IPAddress.IPv6Loopback; + yield return IPAddress.Parse("2001:0db8:85a3:0042:1000:8a2e:0370:7334"); + yield return IPAddress.Parse("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"); + yield return IPAddress.Parse("ffff:ffff:ffff:ffff::"); + yield return IPAddress.Parse("::abc:ffff:ffff:ffff"); + } + + private static string AddressToHexString(IPAddress address) + { + return string.Concat(address.GetAddressBytes().Select(b => Convert.ToString(b, 16).PadLeft(2, '0'))); + } + #endregion // end: ParseFromHexString / TryParseFromHexString #region ParseIgnoreOctalInIPv4 / TryParseIgnoreOctalInIPv4 - public static IEnumerable ParseIgnoreOctalInIPv4_Test_Values() + /// Gets theory data for and related tests. + /// Parameters: expected (IPAddress), input (string). + public static TheoryData ParseIgnoreOctalInIPv4_Valid_Test_Data() { - foreach (var address in Addresses()) + var data = new TheoryData(); + + foreach (var address in OctalParseAddresses()) { - yield return new object[] { address, address.ToString() }; + data.Add(address, address.ToString()); if (address.AddressFamily == AddressFamily.InterNetwork) { - yield return new object[] { address, AddressToQuads(address) }; + var quads = AddressToQuads(address); + if (quads != address.ToString()) + { + data.Add(address, quads); + } } } - yield return new object[] { IPAddress.Parse("0.0.0.192"), "192" }; - yield return new object[] { IPAddress.Parse("1.0.0.192"), "1.192" }; - yield return new object[] { IPAddress.Parse("1.255.0.192"), "1.255.192" }; + data.Add(IPAddress.Parse("0.0.0.192"), "192"); + data.Add(IPAddress.Parse("1.0.0.192"), "1.192"); + data.Add(IPAddress.Parse("1.255.0.192"), "1.255.192"); // octal case - yield return new object[] { IPAddress.Parse("7.7.7.0"), "007.007.7.0" }; - - // expected failures - yield return new object[] { null, null }; - yield return new object[] { null, "potato" }; - yield return new object[] { null, "255.255.255.255.255" }; - - IEnumerable Addresses() - { - yield return IPAddress.Any; - yield return IPAddress.Loopback; - yield return IPAddress.None; - yield return IPAddress.Parse("7.7.7.7"); // explicit octal - yield return IPAddress.Parse("0.0.0.192"); - yield return IPAddress.Parse("192.168.1.1"); - yield return IPAddress.Parse("255.255.255"); - - yield return IPAddress.IPv6Any; - yield return IPAddress.IPv6Loopback; - yield return IPAddress.Parse("2001:0db8:85a3:0042:1000:8a2e:0370:7334"); - yield return IPAddress.Parse("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"); - yield return IPAddress.Parse("ffff:ffff:ffff:ffff::"); - yield return IPAddress.Parse("::abc:ffff:ffff:ffff"); - } + data.Add(IPAddress.Parse("7.7.7.0"), "007.007.7.0"); - string AddressToQuads(IPAddress address) - { - return string.Join(".", address.GetAddressBytes().Select(b => Convert.ToString(b, 10).PadLeft(3, '0'))); - } + return data; } + /// Gets theory data for and related tests. + /// Parameters: input (string). + public static TheoryData ParseIgnoreOctalInIPv4_Invalid_Test_Data => + new() { { "potato" }, { "255.255.255.255.255" } }; + + /// Verifies that returns the expected address for valid inputs. + /// Expected parsed IP address. + /// String representation of the address to parse. [Theory] - [MemberData(nameof(ParseIgnoreOctalInIPv4_Test_Values))] - public void ParseIgnoreOctalInIPv4_Test(IPAddress expected, string input) + [MemberData(nameof(ParseIgnoreOctalInIPv4_Valid_Test_Data))] + public void ParseIgnoreOctalInIPv4_ValidInput_ReturnsExpected_Test(IPAddress expected, string input) { // Arrange - if (expected == null) - { - return; // ignore invalid test - } - // Act var result = IPAddressUtilities.ParseIgnoreOctalInIPv4(input); @@ -498,16 +591,18 @@ public void ParseIgnoreOctalInIPv4_Test(IPAddress expected, string input) Assert.Equal(expected, result); } + /// Verifies that throws for null input. [Fact] - public void ParseIgnoreOctalInIPv4_NullInput_ThrowsArgumentNullException_Test() + public void ParseIgnoreOctalInIPv4_NullInput_Throws_ArgumentNullException_Test() { // Arrange // Act // Assert - Assert.Throws(() => IPAddressUtilities.ParseIgnoreOctalInIPv4(null)); } + /// Verifies that throws for empty or whitespace input. + /// Empty or whitespace string to test. [Theory] [InlineData("")] [InlineData("\t")] @@ -517,79 +612,254 @@ public void ParseIgnoreOctalInIPv4_EmptyOrWhitespaceInput_Throws_ArgumentExcepti // Arrange // Act // Assert - Assert.Throws(() => IPAddressUtilities.ParseIgnoreOctalInIPv4(input)); } + /// Verifies that throws for invalid input strings. + /// Invalid address string to test. + [Theory] + [MemberData(nameof(ParseIgnoreOctalInIPv4_Invalid_Test_Data))] + public void ParseIgnoreOctalInIPv4_InvalidInput_Throws_Test(string input) + { + // Arrange + // Act + // Assert + Assert.ThrowsAny(() => IPAddressUtilities.ParseIgnoreOctalInIPv4(input)); + } + + /// Verifies that returns true and the expected address for valid inputs. + /// Expected parsed IP address. + /// String representation of the address to parse. [Theory] - [MemberData(nameof(ParseIgnoreOctalInIPv4_Test_Values))] - public void TryParseIgnoreOctalInIPv4_Test(IPAddress expected, string input) + [MemberData(nameof(ParseIgnoreOctalInIPv4_Valid_Test_Data))] + public void TryParseIgnoreOctalInIPv4_ValidInput_ReturnsTrue_Test(IPAddress expected, string input) { // Arrange + // Act var success = IPAddressUtilities.TryParseIgnoreOctalInIPv4(input, out var result); // Assert - Assert.Equal(expected != null, success); + Assert.True(success); Assert.Equal(expected, result); } - #endregion // end: ParseIgnoreOctalInIPv4 / TryParseIgnoreOctalInIPv4 + /// Verifies that returns false and a null result when given null input. + [Fact] + public void TryParseIgnoreOctalInIPv4_NullInput_ReturnsFalse_Test() + { + // Arrange + // Act + var success = IPAddressUtilities.TryParseIgnoreOctalInIPv4(null, out var result); - #region TryParse(byte[]) + // Assert + Assert.False(success); + Assert.Null(result); + } - public static IEnumerable Parse_BytesArray_Test_Values() + /// Verifies that returns false for invalid input strings. + /// Invalid address string to test. + [Theory] + [MemberData(nameof(ParseIgnoreOctalInIPv4_Invalid_Test_Data))] + public void TryParseIgnoreOctalInIPv4_InvalidInput_ReturnsFalse_Test(string input) { - foreach (var address in Addresses()) - { - yield return new object[] { address, address.GetAddressBytes().ToArray(), address.AddressFamily }; - } + // Arrange + // Act + var success = IPAddressUtilities.TryParseIgnoreOctalInIPv4(input, out var result); + + // Assert + Assert.False(success); + Assert.Null(result); + } - // underflow, add msb zeros - yield return new object[] { IPAddress.Parse("0.0.0.0"), Array.Empty(), AddressFamily.InterNetwork }; - yield return new object[] { IPAddress.Parse("::"), Array.Empty(), AddressFamily.InterNetworkV6 }; + private static IEnumerable OctalParseAddresses() + { + yield return IPAddress.Any; + yield return IPAddress.Loopback; + yield return IPAddress.None; + yield return IPAddress.Parse("7.7.7.7"); + yield return IPAddress.Parse("0.0.0.192"); + yield return IPAddress.Parse("192.168.1.1"); + yield return IPAddress.Parse("255.255.255"); - yield return new object[] { IPAddress.Parse("0.0.0.255"), new byte[] { 0x00, 0xff }, AddressFamily.InterNetwork }; - yield return new object[] + yield return IPAddress.IPv6Any; + yield return IPAddress.IPv6Loopback; + yield return IPAddress.Parse("2001:0db8:85a3:0042:1000:8a2e:0370:7334"); + yield return IPAddress.Parse("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"); + yield return IPAddress.Parse("ffff:ffff:ffff:ffff::"); + yield return IPAddress.Parse("::abc:ffff:ffff:ffff"); + } + + private static string AddressToQuads(IPAddress address) + { + return string.Join(".", address.GetAddressBytes().Select(b => Convert.ToString(b, 10).PadLeft(3, '0'))); + } + + #endregion // end: ParseIgnoreOctalInIPv4 / TryParseIgnoreOctalInIPv4 + + #region HexLikePattern + + /// Gets theory data for . + /// Parameters: expected (bool), input (string). + public static TheoryData HexLikePattern_Test_Data => + new() { - IPAddress.Parse("::acca"), - new byte[] { 0x00, 0x00, 0xac, 0xca }, - AddressFamily.InterNetworkV6, + // matching - lowercase hex digits + { true, "0123456789abcdef" }, + // matching - uppercase hex digits (IgnoreCase) + { true, "0123456789ABCDEF" }, + // matching - mixed case + { true, "DeAdBeEf" }, + // matching - digits only + { true, "0000" }, + // matching - empty string (pattern uses '*', allows zero chars) + { true, string.Empty }, + // non-matching - 'g' and beyond are not hex + { false, "abcdefg" }, + { false, "xyz" }, + // non-matching - '0x' prefix contains 'x' + { false, "0x1A" }, + // non-matching - space or punctuation + { false, "12 34" }, + { false, "!" }, }; - // ipv4 overflow - yield return new object[] { null, Enumerable.Repeat((byte)0xff, 5).ToArray(), AddressFamily.InterNetwork }; + /// Verifies that matches expected inputs. + /// Whether the pattern is expected to match the input. + /// Input string to match against the hex pattern. + [Theory] + [MemberData(nameof(HexLikePattern_Test_Data))] + public void HexLikePattern_IsMatch_ReturnsExpected_Test(bool expected, string input) + { + // Arrange + var regex = MyRegex(); + + // Act + var result = regex.IsMatch(input); - // ipv6 overflow - yield return new object[] { null, Enumerable.Repeat((byte)0xff, 17).ToArray(), AddressFamily.InterNetworkV6 }; + // Assert + Assert.Equal(expected, result); + } - yield return new object[] { null, null, AddressFamily.InterNetwork }; - yield return new object[] { null, null, AddressFamily.InterNetworkV6 }; + #endregion // end: HexLikePattern - // non standard address family - foreach (var addressFamily in NonStandardAddressFamilies()) - { - yield return new object[] { null, Array.Empty(), addressFamily }; - } + #region DottedQuadRegularExpressionPattern - IEnumerable Addresses() + /// Gets theory data for . + /// Parameters: expected (bool), input (string). + public static TheoryData DottedQuadRegularExpressionPattern_Test_Data => + new() { - foreach (var address in GeneralPurposeIPv4Addresses().Concat(GeneralPurposeIPv6Addresses())) - { - yield return address; - } - } + // matching - well-formed dotted quads (pattern checks format, not address validity) + { true, "192.168.1.1" }, + { true, "0.0.0.0" }, + { true, "255.255.255.255" }, + // matching - out-of-range values pass (pattern is format-only) + { true, "999.999.999.999" }, + // non-matching - too few groups + { false, "192.168.1" }, + { false, "192.168" }, + { false, "192" }, + // non-matching - too many groups + { false, "192.168.1.1.5" }, + // non-matching - empty or non-numeric + { false, string.Empty }, + { false, "::" }, + { false, "potato" }, + // non-matching - 4-digit group exceeds {1,3} + { false, "1234.1.1.1" }, + }; + + /// Verifies that matches expected dotted-quad inputs. + /// Whether the pattern is expected to match the input. + /// Input string to match against the dotted-quad pattern. + [Theory] + [MemberData(nameof(DottedQuadRegularExpressionPattern_Test_Data))] + public void DottedQuadRegularExpressionPattern_IsMatch_ReturnsExpected_Test(bool expected, string input) + { + // Arrange + var regex = new Regex(IPAddressUtilities.DottedQuadRegularExpressionPattern, RegexOptions.CultureInvariant); + + // Act + var result = regex.IsMatch(input); + + // Assert + Assert.Equal(expected, result); } + #endregion // end: DottedQuadRegularExpressionPattern + + #region DottedQuadLeadingZerosPattern + + /// Gets theory data for . + /// Parameters: expected (string), input (string). + public static TheoryData DottedQuadLeadingZerosPattern_Replace_Test_Data => + new() + { + // leading zeros stripped from each octet + { "7.7.7.0", "007.007.7.0" }, + { "1.2.3.4", "001.002.003.004" }, + // lone-zero octets preserved (lookahead prevents stripping the only '0') + { "0.0.0.0", "000.000.000.000" }, + { "0.0.0.0", "0.0.0.0" }, + // no leading zeros - no change + { "192.168.1.0", "192.168.1.0" }, + { "0.1.0.1", "0.1.0.1" }, + }; + + /// Verifies that strips leading zeros correctly. + /// Expected string after replacing leading zeros. + /// Input dotted-quad string to process. [Theory] - [MemberData(nameof(Parse_BytesArray_Test_Values))] - public void Parse_ByteArray_Test(IPAddress expected, byte[] bytes, AddressFamily addressFamily) + [MemberData(nameof(DottedQuadLeadingZerosPattern_Replace_Test_Data))] + public void DottedQuadLeadingZerosPattern_Replace_ReturnsExpected_Test(string expected, string input) { // Arrange - if (expected == null) + var regex = new Regex(IPAddressUtilities.DottedQuadLeadingZerosPattern, RegexOptions.CultureInvariant); + + // Act + var result = regex.Replace(input, string.Empty); + + // Assert + Assert.Equal(expected, result); + } + + #endregion // end: DottedQuadLeadingZerosPattern + + #region Parse(byte[]) / TryParse(byte[]) + + /// Gets theory data for byte-array parse tests with valid inputs. + /// Parameters: expected (IPAddress), bytes (byte[]), addressFamily (AddressFamily). + public static TheoryData Parse_ByteArray_Valid_Test_Data() + { + var data = new TheoryData(); + + foreach (var address in GeneralPurposeIPv4Addresses().Concat(GeneralPurposeIPv6Addresses())) { - return; // ignore invalid test + data.Add(address, [.. address.GetAddressBytes()], address.AddressFamily); } + // underflow - pad with MSB zeros + data.Add(IPAddress.Parse("0.0.0.0"), [], AddressFamily.InterNetwork); + data.Add(IPAddress.Parse("::"), [], AddressFamily.InterNetworkV6); + data.Add(IPAddress.Parse("0.0.0.255"), [0x00, 0xff], AddressFamily.InterNetwork); + data.Add(IPAddress.Parse("::acca"), [0x00, 0x00, 0xac, 0xca], AddressFamily.InterNetworkV6); + + return data; + } + + /// Verifies that returns the expected address for valid byte array inputs. + /// Expected parsed IP address. + /// Byte array to parse. + /// Address family used to interpret the byte array. + [Theory] + [MemberData(nameof(Parse_ByteArray_Valid_Test_Data))] + public void Parse_ByteArray_ValidInput_ReturnsExpected_Test( + IPAddress expected, + byte[] bytes, + AddressFamily addressFamily + ) + { + // Arrange // Act var result = IPAddressUtilities.Parse(bytes, addressFamily); @@ -597,50 +867,102 @@ public void Parse_ByteArray_Test(IPAddress expected, byte[] bytes, AddressFamily Assert.Equal(expected, result); } - [Theory] - [MemberData(nameof(InvalidAddressFamily_Values))] - public void Parse_Bytes_InvalidAddressFamily_Throws_ArgumentOutOfRangeException_Test(AddressFamily addressFamily) + /// Verifies that throws when given a null byte array. + [Fact] + public void Parse_ByteArray_NullInput_Throws_ArgumentNullException_Test() { // Arrange + // Act // Assert + Assert.Throws(() => IPAddressUtilities.Parse(null, AddressFamily.InterNetwork)); + } + + /// Verifies that throws for non-standard address families. + /// Non-standard address family to test. + [Theory] + [MemberData(nameof(ParseFromHexString_InvalidAddressFamily_Test_Data))] + public void Parse_ByteArray_InvalidAddressFamily_Throws_ArgumentOutOfRangeException_Test(AddressFamily addressFamily) + { + // Arrange + // Act // Assert - Assert.Throws(() => IPAddressUtilities.Parse(new byte[] { 0x42 }, addressFamily)); + Assert.Throws(() => IPAddressUtilities.Parse([0x42], addressFamily)); } + /// Verifies that throws when the byte array is longer than the address family supports. + /// Number of bytes in the oversized array. + /// Address family for which the byte array is too long. [Theory] [InlineData(17, AddressFamily.InterNetworkV6)] [InlineData(5, AddressFamily.InterNetwork)] - public void Parse_Bytes_InputTooLong_Throws_ArgumentOutOfRangeException_Test(int count, AddressFamily addressFamily) + public void Parse_ByteArray_InputTooLong_Throws_ArgumentOutOfRangeException_Test(int count, AddressFamily addressFamily) { // Arrange + var bytes = Enumerable.Repeat((byte)0x00, count).ToArray(); + + // Act // Assert - // Assert - Assert.Throws(() => - IPAddressUtilities.Parse(Enumerable.Repeat((byte)0x00, count).ToArray(), addressFamily) - ); + Assert.Throws(() => IPAddressUtilities.Parse(bytes, addressFamily)); } + /// Verifies that returns true and the expected address for valid byte array inputs. + /// Expected parsed IP address. + /// Byte array to parse. + /// Address family used to interpret the byte array. [Theory] - [MemberData(nameof(Parse_BytesArray_Test_Values))] - public void TryParse_ByteArray_Test(IPAddress expected, byte[] bytes, AddressFamily addressFamily) + [MemberData(nameof(Parse_ByteArray_Valid_Test_Data))] + public void TryParse_ByteArray_ValidInput_ReturnsTrue_Test( + IPAddress expected, + byte[] bytes, + AddressFamily addressFamily + ) { // Arrange // Act var success = IPAddressUtilities.TryParse(bytes, addressFamily, out var result); // Assert - Assert.Equal(expected != null, success); + Assert.True(success); Assert.Equal(expected, result); } - #endregion // end: TryParse(byte[]) + /// Verifies that returns false and a null result when given a null byte array. + [Fact] + public void TryParse_ByteArray_NullInput_ReturnsFalse_Test() + { + // Arrange + // Act + var success = IPAddressUtilities.TryParse(null, AddressFamily.InterNetwork, out var result); + + // Assert + Assert.False(success); + Assert.Null(result); + } + + /// Verifies that returns false for non-standard address families. + /// Non-standard address family to test. + [Theory] + [MemberData(nameof(ParseFromHexString_InvalidAddressFamily_Test_Data))] + public void TryParse_ByteArray_InvalidAddressFamily_ReturnsFalse_Test(AddressFamily addressFamily) + { + // Arrange + // Act + var success = IPAddressUtilities.TryParse([0x42], addressFamily, out var result); + + // Assert + Assert.False(success); + Assert.Null(result); + } + + #endregion // end: Parse(byte[]) / TryParse(byte[]) #endregion // end: Parse / TryParse #region MaxIPAddress + /// Verifies that returns for . [Fact] - public void MaxIPAddress_Ipv4_Test() + public void MaxIPAddress_IPv4_ReturnsIPv4MaxAddress_Test() { // Arrange // Act @@ -650,8 +972,9 @@ public void MaxIPAddress_Ipv4_Test() Assert.Same(IPAddressUtilities.IPv4MaxAddress, result); } + /// Verifies that returns for . [Fact] - public void MaxIPAddress_Ipv6_Test() + public void MaxIPAddress_IPv6_ReturnsIPv6MaxAddress_Test() { // Arrange // Act @@ -661,8 +984,10 @@ public void MaxIPAddress_Ipv6_Test() Assert.Same(IPAddressUtilities.IPv6MaxAddress, result); } + /// Verifies that throws for non-standard address families. + /// Non-standard address family to test. [Theory] - [MemberData(nameof(InvalidAddressFamily_Values))] + [MemberData(nameof(ParseFromHexString_InvalidAddressFamily_Test_Data))] public void MaxIPAddress_InvalidAddressFamily_Throws_ArgumentException_Test(AddressFamily addressFamily) { // Arrange @@ -675,8 +1000,9 @@ public void MaxIPAddress_InvalidAddressFamily_Throws_ArgumentException_Test(Addr #region MinIPAddress + /// Verifies that returns for . [Fact] - public void MinIPAddress_Ipv4_Test() + public void MinIPAddress_IPv4_ReturnsIPv4MinAddress_Test() { // Arrange // Act @@ -686,8 +1012,9 @@ public void MinIPAddress_Ipv4_Test() Assert.Same(IPAddressUtilities.IPv4MinAddress, result); } + /// Verifies that returns for . [Fact] - public void MinIPAddress_Ipv6_Test() + public void MinIPAddress_IPv6_ReturnsIPv6MinAddress_Test() { // Arrange // Act @@ -697,8 +1024,10 @@ public void MinIPAddress_Ipv6_Test() Assert.Same(IPAddressUtilities.IPv6MinAddress, result); } + /// Verifies that throws for non-standard address families. + /// Non-standard address family to test. [Theory] - [MemberData(nameof(InvalidAddressFamily_Values))] + [MemberData(nameof(ParseFromHexString_InvalidAddressFamily_Test_Data))] public void MinIPAddress_InvalidAddressFamily_Throws_ArgumentException_Test(AddressFamily addressFamily) { // Arrange @@ -713,8 +1042,19 @@ public void MinIPAddress_InvalidAddressFamily_Throws_ArgumentException_Test(Addr #region BitCount + /// Verifies that equals 32. [Fact] - public void IPv4BitCount_Value_Test() + public void IPv4BitCount_Value_IsThirtyTwo_Test() + { + // Arrange + // Act + // Assert + Assert.Equal(32, IPAddressUtilities.IPv4BitCount); + } + + /// Verifies that matches the actual bit count of an IPv4 address. + [Fact] + public void IPv4BitCount_MatchesIPv4ByteCount_Test() { // Arrange // Act @@ -722,8 +1062,19 @@ public void IPv4BitCount_Value_Test() Assert.Equal(IPAddressUtilities.IPv4BitCount, IPAddress.Any.GetAddressBytes().Length * 8); } + /// Verifies that equals 128. + [Fact] + public void IPv6BitCount_Value_IsOneTwentyEight_Test() + { + // Arrange + // Act + // Assert + Assert.Equal(128, IPAddressUtilities.IPv6BitCount); + } + + /// Verifies that matches the actual bit count of an IPv6 address. [Fact] - public void IPv6BitCount_Value_Test() + public void IPv6BitCount_MatchesIPv6ByteCount_Test() { // Arrange // Act @@ -735,8 +1086,19 @@ public void IPv6BitCount_Value_Test() #region ByteCount + /// Verifies that equals 4. [Fact] - public void IPv4ByteCount_Value_Test() + public void IPv4ByteCount_Value_IsFour_Test() + { + // Arrange + // Act + // Assert + Assert.Equal(4, IPAddressUtilities.IPv4ByteCount); + } + + /// Verifies that matches the actual byte count of an IPv4 address. + [Fact] + public void IPv4ByteCount_MatchesIPv4AddressBytes_Test() { // Arrange // Act @@ -744,8 +1106,19 @@ public void IPv4ByteCount_Value_Test() Assert.Equal(IPAddressUtilities.IPv4ByteCount, IPAddress.Any.GetAddressBytes().Length); } + /// Verifies that equals 16. + [Fact] + public void IPv6ByteCount_Value_IsSixteen_Test() + { + // Arrange + // Act + // Assert + Assert.Equal(16, IPAddressUtilities.IPv6ByteCount); + } + + /// Verifies that matches the actual byte count of an IPv6 address. [Fact] - public void IPv6ByteCount_Value_Test() + public void IPv6ByteCount_MatchesIPv6AddressBytes_Test() { // Arrange // Act @@ -757,27 +1130,29 @@ public void IPv6ByteCount_Value_Test() #region MaxAddress + /// Verifies that has all bytes set to 0xFF and is an IPv6 address. [Fact] - public void IPv6MaxAddress_Value_Test() + public void IPv6MaxAddress_Value_IsAllFF_Test() { // Arrange // Act var address = IPAddressUtilities.IPv6MaxAddress; // Assert - Assert.Equal(new IPAddress(Enumerable.Repeat((byte)0xff, 16).ToArray()), address); + Assert.Equal(new IPAddress([.. Enumerable.Repeat((byte)0xff, 16)]), address); Assert.Equal(AddressFamily.InterNetworkV6, address.AddressFamily); } + /// Verifies that has all bytes set to 0xFF and is an IPv4 address. [Fact] - public void IPv4MaxAddress_Value_Test() + public void IPv4MaxAddress_Value_IsAllFF_Test() { // Arrange // Act var address = IPAddressUtilities.IPv4MaxAddress; // Assert - Assert.Equal(new IPAddress(Enumerable.Repeat((byte)0xff, 4).ToArray()), address); + Assert.Equal(new IPAddress([.. Enumerable.Repeat((byte)0xff, 4)]), address); Assert.Equal(AddressFamily.InterNetwork, address.AddressFamily); } @@ -785,27 +1160,29 @@ public void IPv4MaxAddress_Value_Test() #region MinAddress + /// Verifies that has all bytes set to 0x00 and is an IPv6 address. [Fact] - public void IPv6MinAddress_Value_Test() + public void IPv6MinAddress_Value_IsAllZero_Test() { // Arrange // Act var address = IPAddressUtilities.IPv6MinAddress; // Assert - Assert.Equal(new IPAddress(Enumerable.Repeat((byte)0x00, 16).ToArray()), address); + Assert.Equal(new IPAddress([.. Enumerable.Repeat((byte)0x00, 16)]), address); Assert.Equal(AddressFamily.InterNetworkV6, address.AddressFamily); } + /// Verifies that has all bytes set to 0x00 and is an IPv4 address. [Fact] - public void IPv4MinAddress_Value_Test() + public void IPv4MinAddress_Value_IsAllZero_Test() { // Arrange // Act var address = IPAddressUtilities.IPv4MinAddress; // Assert - Assert.Equal(new IPAddress(Enumerable.Repeat((byte)0x00, 4).ToArray()), address); + Assert.Equal(new IPAddress([.. Enumerable.Repeat((byte)0x00, 4)]), address); Assert.Equal(AddressFamily.InterNetwork, address.AddressFamily); } @@ -813,8 +1190,9 @@ public void IPv4MinAddress_Value_Test() #region ValidAddressFamilies + /// Verifies that is a read-only collection containing both IPv4 and IPv6 families. [Fact] - public void ValidAddressFamilies_Test() + public void ValidAddressFamilies_IsReadOnlyCollection_ContainsBothFamilies_Test() { // Arrange var validAddressFamilies = IPAddressUtilities.ValidAddressFamilies; @@ -822,38 +1200,58 @@ public void ValidAddressFamilies_Test() // Act // Assert - Assert.IsAssignableFrom>(validAddressFamilies); // explicitly read only - Assert.Equal(2, validAddressFamilies.Count); + Assert.IsType>(validAddressFamilies, exactMatch: false); + Assert.Equal(2, validAddressFamilies.Length); Assert.Contains(AddressFamily.InterNetworkV6, validAddressFamilies); Assert.Contains(AddressFamily.InterNetwork, validAddressFamilies); } + /// Verifies that is backed by an immutable array rather than a wrapped . + [Fact] + public void ValidAddressFamilies_IsImmutable_Test() + { + // Arrange + // Act + var validAddressFamilies = IPAddressUtilities.ValidAddressFamilies; + + // Assert + Assert.IsType(validAddressFamilies); + } + #endregion // end: ValidAddressFamilies #endregion // end: Constant Values #region IsPrivate - public static IEnumerable IsPrivate_Test_Values() + /// Gets theory data for . + /// Parameters: expected (bool), address (IPAddress). + public static TheoryData IsPrivate_Test_Data() { + var data = new TheoryData(); + foreach (var subnet in SubnetUtilities.PrivateIPAddressRangesList) { - yield return new object[] { true, subnet.NetworkPrefixAddress }; - yield return new object[] { true, subnet.NetworkPrefixAddress.Increment(2) }; - yield return new object[] { true, subnet.BroadcastAddress }; - yield return new object[] { true, subnet.BroadcastAddress.Increment(-2) }; + data.Add(true, subnet.NetworkPrefixAddress); + data.Add(true, subnet.NetworkPrefixAddress.Increment(2)); + data.Add(true, subnet.BroadcastAddress); + data.Add(true, subnet.BroadcastAddress.Increment(-2)); } - yield return new object[] { false, IPAddressUtilities.IPv4MaxAddress }; - yield return new object[] { false, IPAddressUtilities.IPv4MinAddress }; + data.Add(false, IPAddressUtilities.IPv4MaxAddress); + data.Add(false, IPAddressUtilities.IPv4MinAddress); + data.Add(false, IPAddressUtilities.IPv6MaxAddress); + data.Add(false, IPAddressUtilities.IPv6MinAddress); - yield return new object[] { false, IPAddressUtilities.IPv6MaxAddress }; - yield return new object[] { false, IPAddressUtilities.IPv6MinAddress }; + return data; } + /// Verifies that returns the expected result for the given address. + /// Expected result of the call. + /// IP address to test. [Theory] - [MemberData(nameof(IsPrivate_Test_Values))] - public void IsPrivate_Test(bool expected, IPAddress address) + [MemberData(nameof(IsPrivate_Test_Data))] + public void IsPrivate_ReturnsExpected_Test(bool expected, IPAddress address) { // Arrange // Act @@ -863,6 +1261,30 @@ public void IsPrivate_Test(bool expected, IPAddress address) Assert.Equal(expected, isPrivate); } - #endregion + /// Verifies that throws when given a null address. + [Fact] + public void IsPrivate_NullAddress_Throws_ArgumentNullException_Test() + { + // Arrange + IPAddress address = null; + + // Act + // Assert + Assert.Throws(() => address.IsPrivate()); + } + +#if NET48 + private static readonly Regex MyRegexField = new( + IPAddressUtilities.HexLikePattern, + RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant + ); + + private static Regex MyRegex() => MyRegexField; +#else + [GeneratedRegex(IPAddressUtilities.HexLikePattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)] + private static partial Regex MyRegex(); +#endif + + #endregion // end: IsPrivate } } diff --git a/src/Arcus.Tests/Utilities/SubnetUtilitiesTests.cs b/src/Arcus.Tests/Utilities/SubnetUtilitiesTests.cs index f8eadd2..59d5640 100644 --- a/src/Arcus.Tests/Utilities/SubnetUtilitiesTests.cs +++ b/src/Arcus.Tests/Utilities/SubnetUtilitiesTests.cs @@ -1,18 +1,16 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; +using System.Net; using Arcus.Comparers; +using Arcus.Math; using Arcus.Utilities; -using Xunit; namespace Arcus.Tests.Utilities { -#pragma warning disable SA1404 + /// Unit tests for . public class SubnetUtilitiesTests { #region PrivateIPAddressRangesList + /// Verifies that returns a read-only list containing the four expected private subnets. [Fact] public void PrivateIPAddressRangesList_Test() { @@ -29,13 +27,11 @@ public void PrivateIPAddressRangesList_Test() var list = SubnetUtilities.PrivateIPAddressRangesList; // Assert - Assert.IsAssignableFrom>(list); + Assert.IsType>(list, exactMatch: false); Assert.Equal(4, list.Count); Assert.Equal(list.Count, list.Distinct().Count()); - Assert.Contains(list, s => s.IsIPv4); Assert.Contains(list, s => s.IsIPv6); - Assert.All( list, subnet => @@ -46,10 +42,11 @@ public void PrivateIPAddressRangesList_Test() ); } - #endregion end: PrivateIPAddressRangesList + #endregion // end: PrivateIPAddressRangesList #region LinkLocalIPAddressRangesList + /// Verifies that returns a read-only list containing the two expected link-local subnets. [Fact] public void LinkLocalIPAddressRangesList_Test() { @@ -60,13 +57,11 @@ public void LinkLocalIPAddressRangesList_Test() var list = SubnetUtilities.LinkLocalIPAddressRangesList; // Assert - Assert.IsAssignableFrom>(list); + Assert.IsType>(list, exactMatch: false); Assert.Equal(2, list.Count); Assert.Equal(list.Count, list.Distinct().Count()); - Assert.Contains(list, s => s.IsIPv4); Assert.Contains(list, s => s.IsIPv6); - Assert.All( list, subnet => @@ -77,468 +72,95 @@ public void LinkLocalIPAddressRangesList_Test() ); } - #endregion end: LinkLocalIPAddressRangesList + #endregion // end: LinkLocalIPAddressRangesList #region FewestConsecutiveSubnetsFor - public static IEnumerable FewestConsecutiveSubnetsFor_Test_Values() + /// Gets theory data for . + /// Parameters: expected (IEnumerable<Subnet>), left (IPAddress), right (IPAddress). + public static IEnumerable< + TheoryDataRow, IPAddress, IPAddress> + > FewestConsecutiveSubnetsFor_ValidInputs_ReturnsExpectedSubnets_Test_Data() { - yield return new object[] - { - new[] { Subnet.Parse("128.64.20.3/32") }, - IPAddress.Parse("128.64.20.3"), + // single IPv4 address (same left and right) + yield return new TheoryDataRow, IPAddress, IPAddress>( + [Subnet.Parse("128.64.20.3/32")], IPAddress.Parse("128.64.20.3"), - }; + IPAddress.Parse("128.64.20.3") + ); - yield return new object[] - { - new[] { "128.64.20.3/32", "128.64.20.4/30", "128.64.20.8/30", "128.64.20.12/32" }.Select(s => Subnet.Parse(s)), + // small IPv4 range + yield return new TheoryDataRow, IPAddress, IPAddress>( + SourceArray.Select(s => Subnet.Parse(s)), IPAddress.Parse("128.64.20.3"), - IPAddress.Parse("128.64.20.12"), - }; + IPAddress.Parse("128.64.20.12") + ); - yield return new object[] - { - new[] { "128.64.20.3/32", "128.64.20.4/30", "128.64.20.8/30", "128.64.20.12/32" }.Select(s => Subnet.Parse(s)), + // small IPv4 range (reversed order - verifies min/max normalization) + yield return new TheoryDataRow, IPAddress, IPAddress>( + SourceArray.Select(s => Subnet.Parse(s)), IPAddress.Parse("128.64.20.12"), - IPAddress.Parse("128.64.20.3"), - }; + IPAddress.Parse("128.64.20.3") + ); - yield return new object[] - { - new[] { "192.168.1.3/32", "192.168.1.4/31" }.Select(s => Subnet.Parse(s)), + // IPv4 range ending in /31 + yield return new TheoryDataRow, IPAddress, IPAddress>( + SmallIPv4Range.Select(s => Subnet.Parse(s)), IPAddress.Parse("192.168.1.3"), - IPAddress.Parse("192.168.1.5"), - }; + IPAddress.Parse("192.168.1.5") + ); - yield return new object[] - { - new[] { Subnet.Parse("2001:400:4402::/128") }, + // single IPv6 address (same left and right) + yield return new TheoryDataRow, IPAddress, IPAddress>( + [Subnet.Parse("2001:400:4402::/128")], IPAddress.Parse("2001:400:4402::"), - IPAddress.Parse("2001:400:4402::"), - }; + IPAddress.Parse("2001:400:4402::") + ); - yield return new object[] - { - new[] { Subnet.Parse("2001:400:4402::/48") }, + // full IPv6 /48 block - exact subnet boundary + yield return new TheoryDataRow, IPAddress, IPAddress>( + [Subnet.Parse("2001:400:4402::/48")], IPAddress.Parse("2001:400:4402::"), - IPAddress.Parse("2001:400:4402:ffff:ffff:ffff:ffff:ffff"), - }; + IPAddress.Parse("2001:400:4402:ffff:ffff:ffff:ffff:ffff") + ); - yield return new object[] - { - new[] - { - "2001:400:4402::ffff/128", - "2001:400:4402::1:0/112", - "2001:400:4402::2:0/111", - "2001:400:4402::4:0/110", - "2001:400:4402::8:0/109", - "2001:400:4402::10:0/108", - "2001:400:4402::20:0/107", - "2001:400:4402::40:0/106", - "2001:400:4402::80:0/105", - "2001:400:4402::100:0/104", - "2001:400:4402::200:0/103", - "2001:400:4402::400:0/102", - "2001:400:4402::800:0/101", - "2001:400:4402::1000:0/100", - "2001:400:4402::2000:0/99", - "2001:400:4402::4000:0/98", - "2001:400:4402::8000:0/97", - "2001:400:4402::1:0:0/96", - "2001:400:4402::2:0:0/95", - "2001:400:4402::4:0:0/94", - "2001:400:4402::8:0:0/93", - "2001:400:4402::10:0:0/92", - "2001:400:4402::20:0:0/91", - "2001:400:4402::40:0:0/90", - "2001:400:4402::80:0:0/89", - "2001:400:4402::100:0:0/88", - "2001:400:4402::200:0:0/87", - "2001:400:4402::400:0:0/86", - "2001:400:4402::800:0:0/85", - "2001:400:4402::1000:0:0/84", - "2001:400:4402::2000:0:0/83", - "2001:400:4402::4000:0:0/82", - "2001:400:4402::8000:0:0/81", - "2001:400:4402:0:1::/80", - "2001:400:4402:0:2::/79", - "2001:400:4402:0:4::/78", - "2001:400:4402:0:8::/77", - "2001:400:4402:0:10::/76", - "2001:400:4402:0:20::/75", - "2001:400:4402:0:40::/74", - "2001:400:4402:0:80::/73", - "2001:400:4402:0:100::/72", - "2001:400:4402:0:200::/71", - "2001:400:4402:0:400::/70", - "2001:400:4402:0:800::/69", - "2001:400:4402:0:1000::/68", - "2001:400:4402:0:2000::/67", - "2001:400:4402:0:4000::/66", - "2001:400:4402:0:8000::/65", - "2001:400:4402:1::/64", - "2001:400:4402:2::/63", - "2001:400:4402:4::/62", - "2001:400:4402:8::/61", - "2001:400:4402:10::/60", - "2001:400:4402:20::/59", - "2001:400:4402:40::/58", - "2001:400:4402:80::/57", - "2001:400:4402:100::/56", - "2001:400:4402:200::/55", - "2001:400:4402:400::/54", - "2001:400:4402:800::/53", - "2001:400:4402:1000::/52", - "2001:400:4402:2000::/51", - "2001:400:4402:4000::/50", - "2001:400:4402:8000::/49", - }.Select(s => Subnet.Parse(s)), + // complex IPv6 range crossing many subnet boundaries + yield return new TheoryDataRow, IPAddress, IPAddress>( + ComplexIPv6Range.Select(s => Subnet.Parse(s)), IPAddress.Parse("2001:400:4402::ffff"), - IPAddress.Parse("2001:400:4402:ffff:ffff:ffff:ffff:ffff"), - }; + IPAddress.Parse("2001:400:4402:ffff:ffff:ffff:ffff:ffff") + ); - yield return new object[] - { - new[] - { - "0.0.0.1/32", - "0.0.0.2/31", - "0.0.0.4/30", - "0.0.0.8/29", - "0.0.0.16/28", - "0.0.0.32/27", - "0.0.0.64/26", - "0.0.0.128/25", - "0.0.1.0/24", - "0.0.2.0/23", - "0.0.4.0/22", - "0.0.8.0/21", - "0.0.16.0/20", - "0.0.32.0/19", - "0.0.64.0/18", - "0.0.128.0/17", - "0.1.0.0/16", - "0.2.0.0/15", - "0.4.0.0/14", - "0.8.0.0/13", - "0.16.0.0/12", - "0.32.0.0/11", - "0.64.0.0/10", - "0.128.0.0/9", - "1.0.0.0/8", - "2.0.0.0/7", - "4.0.0.0/6", - "8.0.0.0/5", - "16.0.0.0/4", - "32.0.0.0/3", - "64.0.0.0/2", - "128.0.0.0/2", - "192.0.0.0/3", - "224.0.0.0/4", - "240.0.0.0/5", - "248.0.0.0/6", - "252.0.0.0/7", - "254.0.0.0/8", - "255.0.0.0/9", - "255.128.0.0/10", - "255.192.0.0/11", - "255.224.0.0/12", - "255.240.0.0/13", - "255.248.0.0/14", - "255.252.0.0/15", - "255.254.0.0/16", - "255.255.0.0/17", - "255.255.128.0/18", - "255.255.192.0/19", - "255.255.224.0/20", - "255.255.240.0/21", - "255.255.248.0/22", - "255.255.252.0/23", - "255.255.254.0/24", - "255.255.255.0/25", - "255.255.255.128/26", - "255.255.255.192/27", - "255.255.255.224/28", - "255.255.255.240/29", - "255.255.255.248/30", - "255.255.255.252/31", - "255.255.255.254/32", - }.Select(s => Subnet.Parse(s)), + // near-full IPv4 range (0.0.0.1 to 255.255.255.254) + yield return new TheoryDataRow, IPAddress, IPAddress>( + NearFullIPv4Range.Select(s => Subnet.Parse(s)), IPAddress.Parse("0.0.0.1"), - IPAddress.Parse("255.255.255.254"), - }; + IPAddress.Parse("255.255.255.254") + ); - yield return new object[] - { - new[] - { - "::1/128", - "::2/127", - "::4/126", - "::8/125", - "::10/124", - "::20/123", - "::40/122", - "::80/121", - "::100/120", - "::200/119", - "::400/118", - "::800/117", - "::1000/116", - "::2000/115", - "::4000/114", - "::8000/113", - "::0.1.0.0/112", - "::0.2.0.0/111", - "::0.4.0.0/110", - "::0.8.0.0/109", - "::0.16.0.0/108", - "::0.32.0.0/107", - "::0.64.0.0/106", - "::0.128.0.0/105", - "::1.0.0.0/104", - "::2.0.0.0/103", - "::4.0.0.0/102", - "::8.0.0.0/101", - "::16.0.0.0/100", - "::32.0.0.0/99", - "::64.0.0.0/98", - "::128.0.0.0/97", - "::1:0:0/96", - "::2:0:0/95", - "::4:0:0/94", - "::8:0:0/93", - "::10:0:0/92", - "::20:0:0/91", - "::40:0:0/90", - "::80:0:0/89", - "::100:0:0/88", - "::200:0:0/87", - "::400:0:0/86", - "::800:0:0/85", - "::1000:0:0/84", - "::2000:0:0/83", - "::4000:0:0/82", - "::8000:0:0/81", - "::1:0:0:0/80", - "::2:0:0:0/79", - "::4:0:0:0/78", - "::8:0:0:0/77", - "::10:0:0:0/76", - "::20:0:0:0/75", - "::40:0:0:0/74", - "::80:0:0:0/73", - "::100:0:0:0/72", - "::200:0:0:0/71", - "::400:0:0:0/70", - "::800:0:0:0/69", - "::1000:0:0:0/68", - "::2000:0:0:0/67", - "::4000:0:0:0/66", - "::8000:0:0:0/65", - "0:0:0:1::/64", - "0:0:0:2::/63", - "0:0:0:4::/62", - "0:0:0:8::/61", - "0:0:0:10::/60", - "0:0:0:20::/59", - "0:0:0:40::/58", - "0:0:0:80::/57", - "0:0:0:100::/56", - "0:0:0:200::/55", - "0:0:0:400::/54", - "0:0:0:800::/53", - "0:0:0:1000::/52", - "0:0:0:2000::/51", - "0:0:0:4000::/50", - "0:0:0:8000::/49", - "0:0:1::/48", - "0:0:2::/47", - "0:0:4::/46", - "0:0:8::/45", - "0:0:10::/44", - "0:0:20::/43", - "0:0:40::/42", - "0:0:80::/41", - "0:0:100::/40", - "0:0:200::/39", - "0:0:400::/38", - "0:0:800::/37", - "0:0:1000::/36", - "0:0:2000::/35", - "0:0:4000::/34", - "0:0:8000::/33", - "0:1::/32", - "0:2::/31", - "0:4::/30", - "0:8::/29", - "0:10::/28", - "0:20::/27", - "0:40::/26", - "0:80::/25", - "0:100::/24", - "0:200::/23", - "0:400::/22", - "0:800::/21", - "0:1000::/20", - "0:2000::/19", - "0:4000::/18", - "0:8000::/17", - "1::/16", - "2::/15", - "4::/14", - "8::/13", - "10::/12", - "20::/11", - "40::/10", - "80::/9", - "100::/8", - "200::/7", - "400::/6", - "800::/5", - "1000::/4", - "2000::/3", - "4000::/2", - "8000::/2", - "c000::/3", - "e000::/4", - "f000::/5", - "f800::/6", - "fc00::/7", - "fe00::/8", - "ff00::/9", - "ff80::/10", - "ffc0::/11", - "ffe0::/12", - "fff0::/13", - "fff8::/14", - "fffc::/15", - "fffe::/16", - "ffff::/17", - "ffff:8000::/18", - "ffff:c000::/19", - "ffff:e000::/20", - "ffff:f000::/21", - "ffff:f800::/22", - "ffff:fc00::/23", - "ffff:fe00::/24", - "ffff:ff00::/25", - "ffff:ff80::/26", - "ffff:ffc0::/27", - "ffff:ffe0::/28", - "ffff:fff0::/29", - "ffff:fff8::/30", - "ffff:fffc::/31", - "ffff:fffe::/32", - "ffff:ffff::/33", - "ffff:ffff:8000::/34", - "ffff:ffff:c000::/35", - "ffff:ffff:e000::/36", - "ffff:ffff:f000::/37", - "ffff:ffff:f800::/38", - "ffff:ffff:fc00::/39", - "ffff:ffff:fe00::/40", - "ffff:ffff:ff00::/41", - "ffff:ffff:ff80::/42", - "ffff:ffff:ffc0::/43", - "ffff:ffff:ffe0::/44", - "ffff:ffff:fff0::/45", - "ffff:ffff:fff8::/46", - "ffff:ffff:fffc::/47", - "ffff:ffff:fffe::/48", - "ffff:ffff:ffff::/49", - "ffff:ffff:ffff:8000::/50", - "ffff:ffff:ffff:c000::/51", - "ffff:ffff:ffff:e000::/52", - "ffff:ffff:ffff:f000::/53", - "ffff:ffff:ffff:f800::/54", - "ffff:ffff:ffff:fc00::/55", - "ffff:ffff:ffff:fe00::/56", - "ffff:ffff:ffff:ff00::/57", - "ffff:ffff:ffff:ff80::/58", - "ffff:ffff:ffff:ffc0::/59", - "ffff:ffff:ffff:ffe0::/60", - "ffff:ffff:ffff:fff0::/61", - "ffff:ffff:ffff:fff8::/62", - "ffff:ffff:ffff:fffc::/63", - "ffff:ffff:ffff:fffe::/64", - "ffff:ffff:ffff:ffff::/65", - "ffff:ffff:ffff:ffff:8000::/66", - "ffff:ffff:ffff:ffff:c000::/67", - "ffff:ffff:ffff:ffff:e000::/68", - "ffff:ffff:ffff:ffff:f000::/69", - "ffff:ffff:ffff:ffff:f800::/70", - "ffff:ffff:ffff:ffff:fc00::/71", - "ffff:ffff:ffff:ffff:fe00::/72", - "ffff:ffff:ffff:ffff:ff00::/73", - "ffff:ffff:ffff:ffff:ff80::/74", - "ffff:ffff:ffff:ffff:ffc0::/75", - "ffff:ffff:ffff:ffff:ffe0::/76", - "ffff:ffff:ffff:ffff:fff0::/77", - "ffff:ffff:ffff:ffff:fff8::/78", - "ffff:ffff:ffff:ffff:fffc::/79", - "ffff:ffff:ffff:ffff:fffe::/80", - "ffff:ffff:ffff:ffff:ffff::/81", - "ffff:ffff:ffff:ffff:ffff:8000::/82", - "ffff:ffff:ffff:ffff:ffff:c000::/83", - "ffff:ffff:ffff:ffff:ffff:e000::/84", - "ffff:ffff:ffff:ffff:ffff:f000::/85", - "ffff:ffff:ffff:ffff:ffff:f800::/86", - "ffff:ffff:ffff:ffff:ffff:fc00::/87", - "ffff:ffff:ffff:ffff:ffff:fe00::/88", - "ffff:ffff:ffff:ffff:ffff:ff00::/89", - "ffff:ffff:ffff:ffff:ffff:ff80::/90", - "ffff:ffff:ffff:ffff:ffff:ffc0::/91", - "ffff:ffff:ffff:ffff:ffff:ffe0::/92", - "ffff:ffff:ffff:ffff:ffff:fff0::/93", - "ffff:ffff:ffff:ffff:ffff:fff8::/94", - "ffff:ffff:ffff:ffff:ffff:fffc::/95", - "ffff:ffff:ffff:ffff:ffff:fffe::/96", - "ffff:ffff:ffff:ffff:ffff:ffff::/97", - "ffff:ffff:ffff:ffff:ffff:ffff:8000:0/98", - "ffff:ffff:ffff:ffff:ffff:ffff:c000:0/99", - "ffff:ffff:ffff:ffff:ffff:ffff:e000:0/100", - "ffff:ffff:ffff:ffff:ffff:ffff:f000:0/101", - "ffff:ffff:ffff:ffff:ffff:ffff:f800:0/102", - "ffff:ffff:ffff:ffff:ffff:ffff:fc00:0/103", - "ffff:ffff:ffff:ffff:ffff:ffff:fe00:0/104", - "ffff:ffff:ffff:ffff:ffff:ffff:ff00:0/105", - "ffff:ffff:ffff:ffff:ffff:ffff:ff80:0/106", - "ffff:ffff:ffff:ffff:ffff:ffff:ffc0:0/107", - "ffff:ffff:ffff:ffff:ffff:ffff:ffe0:0/108", - "ffff:ffff:ffff:ffff:ffff:ffff:fff0:0/109", - "ffff:ffff:ffff:ffff:ffff:ffff:fff8:0/110", - "ffff:ffff:ffff:ffff:ffff:ffff:fffc:0/111", - "ffff:ffff:ffff:ffff:ffff:ffff:fffe:0/112", - "ffff:ffff:ffff:ffff:ffff:ffff:ffff:0/113", - "ffff:ffff:ffff:ffff:ffff:ffff:ffff:8000/114", - "ffff:ffff:ffff:ffff:ffff:ffff:ffff:c000/115", - "ffff:ffff:ffff:ffff:ffff:ffff:ffff:e000/116", - "ffff:ffff:ffff:ffff:ffff:ffff:ffff:f000/117", - "ffff:ffff:ffff:ffff:ffff:ffff:ffff:f800/118", - "ffff:ffff:ffff:ffff:ffff:ffff:ffff:fc00/119", - "ffff:ffff:ffff:ffff:ffff:ffff:ffff:fe00/120", - "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ff00/121", - "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ff80/122", - "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffc0/123", - "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffe0/124", - "ffff:ffff:ffff:ffff:ffff:ffff:ffff:fff0/125", - "ffff:ffff:ffff:ffff:ffff:ffff:ffff:fff8/126", - "ffff:ffff:ffff:ffff:ffff:ffff:ffff:fffc/127", - "ffff:ffff:ffff:ffff:ffff:ffff:ffff:fffe/128", - }.Select(s => Subnet.Parse(s)), + // near-full IPv6 range (::1 to ffff:...:fffe) + yield return new TheoryDataRow, IPAddress, IPAddress>( + NearFullIPv6Range.Select(s => Subnet.Parse(s)), IPAddress.Parse("::1"), - IPAddress.Parse("ffff:ffff:ffff:ffff:ffff:ffff:ffff:fffe"), - }; + IPAddress.Parse("ffff:ffff:ffff:ffff:ffff:ffff:ffff:fffe") + ); } + /// Verifies that returns the expected minimal set of subnets for valid address pairs. + /// Expected collection of subnets covering the range. + /// Left bound of the IP address range. + /// Right bound of the IP address range. [Theory] - [MemberData(nameof(FewestConsecutiveSubnetsFor_Test_Values))] - public void FewestConsecutiveSubnetsFor_Test(IEnumerable expected, IPAddress left, IPAddress right) + [MemberData(nameof(FewestConsecutiveSubnetsFor_ValidInputs_ReturnsExpectedSubnets_Test_Data))] + public void FewestConsecutiveSubnetsFor_ValidInputs_ReturnsExpectedSubnets_Test( + IEnumerable expected, + IPAddress left, + IPAddress right + ) { // Arrange + var expectedList = expected.ToList(); + // Act var result = SubnetUtilities.FewestConsecutiveSubnetsFor(left, right).ToList(); @@ -546,55 +168,595 @@ public void FewestConsecutiveSubnetsFor_Test(IEnumerable expected, IPAdd Assert.NotNull(result); Assert.NotEmpty(result); Assert.All(result, Assert.NotNull); - - var expectedList = expected.ToList(); - - // Assert.Equal(expectedList, result); // directly calling Assert.Equals results in unexpected behavior; unwinding equality explicitly Assert.Equal(expectedList.Count, result.Count); Assert.All(expectedList, subnet => Assert.Contains(subnet, result)); Assert.All(result, subnet => Assert.Contains(subnet, expectedList)); - - Assert.True(result.SequenceEqual(new SortedSet(result, new DefaultIPAddressRangeComparer()))); + Assert.True(result.SequenceEqual(new SortedSet(result, new DefaultIIPAddressRangeComparer()))); } - public static IEnumerable FewestConsecutiveSubnetsFor_MissMatchAddressFamilies_ThrowsInvalidOperationException_Test_Values() + /// Gets theory data for . + /// Parameters: left (IPAddress), right (IPAddress). + public static TheoryData< + IPAddress, + IPAddress + > FewestConsecutiveSubnetsFor_MismatchedAddressFamilies_ThrowsInvalidOperationException_Test_Data => + new() { { IPAddress.Any, IPAddress.IPv6Any }, { IPAddress.IPv6Any, IPAddress.Any } }; + + /// Verifies that throws when the two addresses belong to different address families. + /// Left bound address. + /// Right bound address. + [Theory] + [MemberData(nameof(FewestConsecutiveSubnetsFor_MismatchedAddressFamilies_ThrowsInvalidOperationException_Test_Data))] + public void FewestConsecutiveSubnetsFor_MismatchedAddressFamilies_ThrowsInvalidOperationException_Test( + IPAddress left, + IPAddress right + ) { - yield return new object[] { IPAddress.Any, IPAddress.IPv6Any }; - yield return new object[] { IPAddress.IPv6Any, IPAddress.Any }; + // Arrange + // Act + // Assert + Assert.Throws(() => SubnetUtilities.FewestConsecutiveSubnetsFor(left, right)); } + /// Gets theory data for . + /// Parameters: left (IPAddress), right (IPAddress). + public static TheoryData< + IPAddress, + IPAddress + > FewestConsecutiveSubnetsFor_NullArgument_ThrowsArgumentNullException_Test_Data => + new() + { + { null, null }, + { IPAddress.Any, null }, + { null, IPAddress.Any }, + }; + + private static readonly string[] SourceArray = + [ + "128.64.20.3/32", + "128.64.20.4/30", + "128.64.20.8/30", + "128.64.20.12/32", + ]; + + private static readonly string[] SmallIPv4Range = ["192.168.1.3/32", "192.168.1.4/31"]; + + private static readonly string[] ComplexIPv6Range = + [ + "2001:400:4402::ffff/128", + "2001:400:4402::1:0/112", + "2001:400:4402::2:0/111", + "2001:400:4402::4:0/110", + "2001:400:4402::8:0/109", + "2001:400:4402::10:0/108", + "2001:400:4402::20:0/107", + "2001:400:4402::40:0/106", + "2001:400:4402::80:0/105", + "2001:400:4402::100:0/104", + "2001:400:4402::200:0/103", + "2001:400:4402::400:0/102", + "2001:400:4402::800:0/101", + "2001:400:4402::1000:0/100", + "2001:400:4402::2000:0/99", + "2001:400:4402::4000:0/98", + "2001:400:4402::8000:0/97", + "2001:400:4402::1:0:0/96", + "2001:400:4402::2:0:0/95", + "2001:400:4402::4:0:0/94", + "2001:400:4402::8:0:0/93", + "2001:400:4402::10:0:0/92", + "2001:400:4402::20:0:0/91", + "2001:400:4402::40:0:0/90", + "2001:400:4402::80:0:0/89", + "2001:400:4402::100:0:0/88", + "2001:400:4402::200:0:0/87", + "2001:400:4402::400:0:0/86", + "2001:400:4402::800:0:0/85", + "2001:400:4402::1000:0:0/84", + "2001:400:4402::2000:0:0/83", + "2001:400:4402::4000:0:0/82", + "2001:400:4402::8000:0:0/81", + "2001:400:4402:0:1::/80", + "2001:400:4402:0:2::/79", + "2001:400:4402:0:4::/78", + "2001:400:4402:0:8::/77", + "2001:400:4402:0:10::/76", + "2001:400:4402:0:20::/75", + "2001:400:4402:0:40::/74", + "2001:400:4402:0:80::/73", + "2001:400:4402:0:100::/72", + "2001:400:4402:0:200::/71", + "2001:400:4402:0:400::/70", + "2001:400:4402:0:800::/69", + "2001:400:4402:0:1000::/68", + "2001:400:4402:0:2000::/67", + "2001:400:4402:0:4000::/66", + "2001:400:4402:0:8000::/65", + "2001:400:4402:1::/64", + "2001:400:4402:2::/63", + "2001:400:4402:4::/62", + "2001:400:4402:8::/61", + "2001:400:4402:10::/60", + "2001:400:4402:20::/59", + "2001:400:4402:40::/58", + "2001:400:4402:80::/57", + "2001:400:4402:100::/56", + "2001:400:4402:200::/55", + "2001:400:4402:400::/54", + "2001:400:4402:800::/53", + "2001:400:4402:1000::/52", + "2001:400:4402:2000::/51", + "2001:400:4402:4000::/50", + "2001:400:4402:8000::/49", + ]; + + private static readonly string[] NearFullIPv4Range = + [ + "0.0.0.1/32", + "0.0.0.2/31", + "0.0.0.4/30", + "0.0.0.8/29", + "0.0.0.16/28", + "0.0.0.32/27", + "0.0.0.64/26", + "0.0.0.128/25", + "0.0.1.0/24", + "0.0.2.0/23", + "0.0.4.0/22", + "0.0.8.0/21", + "0.0.16.0/20", + "0.0.32.0/19", + "0.0.64.0/18", + "0.0.128.0/17", + "0.1.0.0/16", + "0.2.0.0/15", + "0.4.0.0/14", + "0.8.0.0/13", + "0.16.0.0/12", + "0.32.0.0/11", + "0.64.0.0/10", + "0.128.0.0/9", + "1.0.0.0/8", + "2.0.0.0/7", + "4.0.0.0/6", + "8.0.0.0/5", + "16.0.0.0/4", + "32.0.0.0/3", + "64.0.0.0/2", + "128.0.0.0/2", + "192.0.0.0/3", + "224.0.0.0/4", + "240.0.0.0/5", + "248.0.0.0/6", + "252.0.0.0/7", + "254.0.0.0/8", + "255.0.0.0/9", + "255.128.0.0/10", + "255.192.0.0/11", + "255.224.0.0/12", + "255.240.0.0/13", + "255.248.0.0/14", + "255.252.0.0/15", + "255.254.0.0/16", + "255.255.0.0/17", + "255.255.128.0/18", + "255.255.192.0/19", + "255.255.224.0/20", + "255.255.240.0/21", + "255.255.248.0/22", + "255.255.252.0/23", + "255.255.254.0/24", + "255.255.255.0/25", + "255.255.255.128/26", + "255.255.255.192/27", + "255.255.255.224/28", + "255.255.255.240/29", + "255.255.255.248/30", + "255.255.255.252/31", + "255.255.255.254/32", + ]; + + private static readonly string[] NearFullIPv6Range = + [ + "::1/128", + "::2/127", + "::4/126", + "::8/125", + "::10/124", + "::20/123", + "::40/122", + "::80/121", + "::100/120", + "::200/119", + "::400/118", + "::800/117", + "::1000/116", + "::2000/115", + "::4000/114", + "::8000/113", + "::0.1.0.0/112", + "::0.2.0.0/111", + "::0.4.0.0/110", + "::0.8.0.0/109", + "::0.16.0.0/108", + "::0.32.0.0/107", + "::0.64.0.0/106", + "::0.128.0.0/105", + "::1.0.0.0/104", + "::2.0.0.0/103", + "::4.0.0.0/102", + "::8.0.0.0/101", + "::16.0.0.0/100", + "::32.0.0.0/99", + "::64.0.0.0/98", + "::128.0.0.0/97", + "::1:0:0/96", + "::2:0:0/95", + "::4:0:0/94", + "::8:0:0/93", + "::10:0:0/92", + "::20:0:0/91", + "::40:0:0/90", + "::80:0:0/89", + "::100:0:0/88", + "::200:0:0/87", + "::400:0:0/86", + "::800:0:0/85", + "::1000:0:0/84", + "::2000:0:0/83", + "::4000:0:0/82", + "::8000:0:0/81", + "::1:0:0:0/80", + "::2:0:0:0/79", + "::4:0:0:0/78", + "::8:0:0:0/77", + "::10:0:0:0/76", + "::20:0:0:0/75", + "::40:0:0:0/74", + "::80:0:0:0/73", + "::100:0:0:0/72", + "::200:0:0:0/71", + "::400:0:0:0/70", + "::800:0:0:0/69", + "::1000:0:0:0/68", + "::2000:0:0:0/67", + "::4000:0:0:0/66", + "::8000:0:0:0/65", + "0:0:0:1::/64", + "0:0:0:2::/63", + "0:0:0:4::/62", + "0:0:0:8::/61", + "0:0:0:10::/60", + "0:0:0:20::/59", + "0:0:0:40::/58", + "0:0:0:80::/57", + "0:0:0:100::/56", + "0:0:0:200::/55", + "0:0:0:400::/54", + "0:0:0:800::/53", + "0:0:0:1000::/52", + "0:0:0:2000::/51", + "0:0:0:4000::/50", + "0:0:0:8000::/49", + "0:0:1::/48", + "0:0:2::/47", + "0:0:4::/46", + "0:0:8::/45", + "0:0:10::/44", + "0:0:20::/43", + "0:0:40::/42", + "0:0:80::/41", + "0:0:100::/40", + "0:0:200::/39", + "0:0:400::/38", + "0:0:800::/37", + "0:0:1000::/36", + "0:0:2000::/35", + "0:0:4000::/34", + "0:0:8000::/33", + "0:1::/32", + "0:2::/31", + "0:4::/30", + "0:8::/29", + "0:10::/28", + "0:20::/27", + "0:40::/26", + "0:80::/25", + "0:100::/24", + "0:200::/23", + "0:400::/22", + "0:800::/21", + "0:1000::/20", + "0:2000::/19", + "0:4000::/18", + "0:8000::/17", + "1::/16", + "2::/15", + "4::/14", + "8::/13", + "10::/12", + "20::/11", + "40::/10", + "80::/9", + "100::/8", + "200::/7", + "400::/6", + "800::/5", + "1000::/4", + "2000::/3", + "4000::/2", + "8000::/2", + "c000::/3", + "e000::/4", + "f000::/5", + "f800::/6", + "fc00::/7", + "fe00::/8", + "ff00::/9", + "ff80::/10", + "ffc0::/11", + "ffe0::/12", + "fff0::/13", + "fff8::/14", + "fffc::/15", + "fffe::/16", + "ffff::/17", + "ffff:8000::/18", + "ffff:c000::/19", + "ffff:e000::/20", + "ffff:f000::/21", + "ffff:f800::/22", + "ffff:fc00::/23", + "ffff:fe00::/24", + "ffff:ff00::/25", + "ffff:ff80::/26", + "ffff:ffc0::/27", + "ffff:ffe0::/28", + "ffff:fff0::/29", + "ffff:fff8::/30", + "ffff:fffc::/31", + "ffff:fffe::/32", + "ffff:ffff::/33", + "ffff:ffff:8000::/34", + "ffff:ffff:c000::/35", + "ffff:ffff:e000::/36", + "ffff:ffff:f000::/37", + "ffff:ffff:f800::/38", + "ffff:ffff:fc00::/39", + "ffff:ffff:fe00::/40", + "ffff:ffff:ff00::/41", + "ffff:ffff:ff80::/42", + "ffff:ffff:ffc0::/43", + "ffff:ffff:ffe0::/44", + "ffff:ffff:fff0::/45", + "ffff:ffff:fff8::/46", + "ffff:ffff:fffc::/47", + "ffff:ffff:fffe::/48", + "ffff:ffff:ffff::/49", + "ffff:ffff:ffff:8000::/50", + "ffff:ffff:ffff:c000::/51", + "ffff:ffff:ffff:e000::/52", + "ffff:ffff:ffff:f000::/53", + "ffff:ffff:ffff:f800::/54", + "ffff:ffff:ffff:fc00::/55", + "ffff:ffff:ffff:fe00::/56", + "ffff:ffff:ffff:ff00::/57", + "ffff:ffff:ffff:ff80::/58", + "ffff:ffff:ffff:ffc0::/59", + "ffff:ffff:ffff:ffe0::/60", + "ffff:ffff:ffff:fff0::/61", + "ffff:ffff:ffff:fff8::/62", + "ffff:ffff:ffff:fffc::/63", + "ffff:ffff:ffff:fffe::/64", + "ffff:ffff:ffff:ffff::/65", + "ffff:ffff:ffff:ffff:8000::/66", + "ffff:ffff:ffff:ffff:c000::/67", + "ffff:ffff:ffff:ffff:e000::/68", + "ffff:ffff:ffff:ffff:f000::/69", + "ffff:ffff:ffff:ffff:f800::/70", + "ffff:ffff:ffff:ffff:fc00::/71", + "ffff:ffff:ffff:ffff:fe00::/72", + "ffff:ffff:ffff:ffff:ff00::/73", + "ffff:ffff:ffff:ffff:ff80::/74", + "ffff:ffff:ffff:ffff:ffc0::/75", + "ffff:ffff:ffff:ffff:ffe0::/76", + "ffff:ffff:ffff:ffff:fff0::/77", + "ffff:ffff:ffff:ffff:fff8::/78", + "ffff:ffff:ffff:ffff:fffc::/79", + "ffff:ffff:ffff:ffff:fffe::/80", + "ffff:ffff:ffff:ffff:ffff::/81", + "ffff:ffff:ffff:ffff:ffff:8000::/82", + "ffff:ffff:ffff:ffff:ffff:c000::/83", + "ffff:ffff:ffff:ffff:ffff:e000::/84", + "ffff:ffff:ffff:ffff:ffff:f000::/85", + "ffff:ffff:ffff:ffff:ffff:f800::/86", + "ffff:ffff:ffff:ffff:ffff:fc00::/87", + "ffff:ffff:ffff:ffff:ffff:fe00::/88", + "ffff:ffff:ffff:ffff:ffff:ff00::/89", + "ffff:ffff:ffff:ffff:ffff:ff80::/90", + "ffff:ffff:ffff:ffff:ffff:ffc0::/91", + "ffff:ffff:ffff:ffff:ffff:ffe0::/92", + "ffff:ffff:ffff:ffff:ffff:fff0::/93", + "ffff:ffff:ffff:ffff:ffff:fff8::/94", + "ffff:ffff:ffff:ffff:ffff:fffc::/95", + "ffff:ffff:ffff:ffff:ffff:fffe::/96", + "ffff:ffff:ffff:ffff:ffff:ffff::/97", + "ffff:ffff:ffff:ffff:ffff:ffff:8000:0/98", + "ffff:ffff:ffff:ffff:ffff:ffff:c000:0/99", + "ffff:ffff:ffff:ffff:ffff:ffff:e000:0/100", + "ffff:ffff:ffff:ffff:ffff:ffff:f000:0/101", + "ffff:ffff:ffff:ffff:ffff:ffff:f800:0/102", + "ffff:ffff:ffff:ffff:ffff:ffff:fc00:0/103", + "ffff:ffff:ffff:ffff:ffff:ffff:fe00:0/104", + "ffff:ffff:ffff:ffff:ffff:ffff:ff00:0/105", + "ffff:ffff:ffff:ffff:ffff:ffff:ff80:0/106", + "ffff:ffff:ffff:ffff:ffff:ffff:ffc0:0/107", + "ffff:ffff:ffff:ffff:ffff:ffff:ffe0:0/108", + "ffff:ffff:ffff:ffff:ffff:ffff:fff0:0/109", + "ffff:ffff:ffff:ffff:ffff:ffff:fff8:0/110", + "ffff:ffff:ffff:ffff:ffff:ffff:fffc:0/111", + "ffff:ffff:ffff:ffff:ffff:ffff:fffe:0/112", + "ffff:ffff:ffff:ffff:ffff:ffff:ffff:0/113", + "ffff:ffff:ffff:ffff:ffff:ffff:ffff:8000/114", + "ffff:ffff:ffff:ffff:ffff:ffff:ffff:c000/115", + "ffff:ffff:ffff:ffff:ffff:ffff:ffff:e000/116", + "ffff:ffff:ffff:ffff:ffff:ffff:ffff:f000/117", + "ffff:ffff:ffff:ffff:ffff:ffff:ffff:f800/118", + "ffff:ffff:ffff:ffff:ffff:ffff:ffff:fc00/119", + "ffff:ffff:ffff:ffff:ffff:ffff:ffff:fe00/120", + "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ff00/121", + "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ff80/122", + "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffc0/123", + "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffe0/124", + "ffff:ffff:ffff:ffff:ffff:ffff:ffff:fff0/125", + "ffff:ffff:ffff:ffff:ffff:ffff:ffff:fff8/126", + "ffff:ffff:ffff:ffff:ffff:ffff:ffff:fffc/127", + "ffff:ffff:ffff:ffff:ffff:ffff:ffff:fffe/128", + ]; + + /// Verifies that throws when either argument is null. + /// Left bound address. + /// Right bound address. [Theory] - [MemberData(nameof(FewestConsecutiveSubnetsFor_MissMatchAddressFamilies_ThrowsInvalidOperationException_Test_Values))] - public void FewestConsecutiveSubnetsFor_MissMatchAddressFamilies_ThrowsInvalidOperationException_Test( - IPAddress alpha, - IPAddress beta - ) + [MemberData(nameof(FewestConsecutiveSubnetsFor_NullArgument_ThrowsArgumentNullException_Test_Data))] + public void FewestConsecutiveSubnetsFor_NullArgument_ThrowsArgumentNullException_Test(IPAddress left, IPAddress right) { + // Arrange // Act // Assert - Assert.Throws(() => SubnetUtilities.FewestConsecutiveSubnetsFor(alpha, beta)); + Assert.Throws(() => SubnetUtilities.FewestConsecutiveSubnetsFor(left, right)); } - public static IEnumerable FewestConsecutiveSubnetsFor_Input_Null_ThrowsArgumentNullException_Test_Values() + /// + /// Verifies that correctly decomposes + /// a range ending at the IPv4 maximum address (255.255.255.255). + /// + [Fact] + public void FewestConsecutiveSubnetsFor_RangeEndingAtIPv4Max_ReturnsExpectedSubnets_Test() { - yield return new object[] { null, null }; - yield return new object[] { IPAddress.Any, null }; - yield return new object[] { null, IPAddress.Any }; + // Arrange + var left = IPAddress.Parse("255.255.255.253"); + var right = IPAddress.Parse("255.255.255.255"); + + // Act + var results = SubnetUtilities.FewestConsecutiveSubnetsFor(left, right).ToArray(); + + // Assert — correct decomposition: + // 255.255.255.253/32 (one address: 255.255.255.253) + // 255.255.255.254/31 (two addresses: 255.255.255.254-255.255.255.255) + Assert.Collection( + results, + subnet => Assert.Equal(new Subnet(IPAddress.Parse("255.255.255.253"), 32), subnet), + subnet => Assert.Equal(new Subnet(IPAddress.Parse("255.255.255.254"), 31), subnet) + ); } - [Theory] - [MemberData(nameof(FewestConsecutiveSubnetsFor_Input_Null_ThrowsArgumentNullException_Test_Values))] - public void FewestConsecutiveSubnetsFor_Input_Null_ThrowsArgumentNullException_Test(IPAddress alpha, IPAddress beta) + /// + /// Sentinel for the C2 perf refactor (recursive Concat -> yield return): + /// enumerating an asymmetric IPv4 range that requires many splits must produce the same + /// count and a contiguous, gap-free, overlap-free cover of the closed interval + /// [10.0.0.1, 10.1.255.254]. The golden count (32) was recorded against the pre-refactor + /// implementation and must not change. No timing assertion (flaky in CI); the linear-time + /// property is established by code inspection and benchmarks. + /// + [Fact] + public void FewestConsecutiveSubnetsFor_AsymmetricManySplits_ReturnsExpectedCount_Test() { + // Arrange + var left = IPAddress.Parse("10.0.0.1"); + var right = IPAddress.Parse("10.1.255.254"); + // Act - // Assert - Assert.Throws(() => SubnetUtilities.FewestConsecutiveSubnetsFor(alpha, beta)); + var subnets = SubnetUtilities.FewestConsecutiveSubnetsFor(left, right).ToList(); + + // Assert — golden count recorded against the pre-refactor (Concat) implementation. + const int expectedCount = 32; + Assert.Equal(expectedCount, subnets.Count); + + AssertContiguousCover(subnets, left, right); + } + + /// + /// Regression sentinel for the C2 refactor: a large bounded IPv4 range + /// (0.0.0.1 -> 255.255.255.254) must yield the golden count (62) + /// recorded against the pre-refactor (Concat) implementation. Verifies count and + /// contiguous coverage so the Concat -> yield-return change cannot alter behavior. + /// + [Fact] + public void FewestConsecutiveSubnetsFor_LargeBoundedRange_ReturnsExpectedCount_Test() + { + // Arrange + var left = IPAddress.Parse("0.0.0.1"); + var right = IPAddress.Parse("255.255.255.254"); + + // Act + var subnets = SubnetUtilities.FewestConsecutiveSubnetsFor(left, right).ToList(); + + // Assert — golden count recorded against the pre-refactor (Concat) implementation. + const int expectedCount = 62; + Assert.Equal(expectedCount, subnets.Count); + + AssertContiguousCover(subnets, left, right); } - #endregion + /// + /// Asserts that forms an ascending, gap-free, overlap-free + /// cover of the closed interval [, ]. + /// + /// + /// + /// The C2 refactor (Concat -> yield return) is a pure performance change; this + /// helper codifies the behavioral contract that must not regress: ordered by + /// , the head of each subnet equals the + /// tail of its predecessor plus one, the first head equals the lower bound, and + /// the last tail equals the upper bound. + /// + /// + private static void AssertContiguousCover(List subnets, IPAddress left, IPAddress right) + { + Assert.NotEmpty(subnets); + Assert.All(subnets, Assert.NotNull); + + var lowerBound = IPAddressMath.Min(left, right); + var upperBound = IPAddressMath.Max(left, right); + + // First head equals the lower bound; last tail equals the upper bound. + Assert.Equal(lowerBound, subnets[0].Head); + Assert.Equal(upperBound, subnets[subnets.Count - 1].Tail); + + // Sorted ascending by NetworkPrefixAddress, gap-free, no overlap. + for (var i = 0; i < subnets.Count; i++) + { + Assert.True( + subnets[i].Tail.IsGreaterThanOrEqualTo(subnets[i].Head), + $"subnet {i} tail {subnets[i].Tail} is below its head {subnets[i].Head}" + ); + Assert.True( + subnets[i].Head.IsGreaterThanOrEqualTo(lowerBound), + $"subnet {i} head {subnets[i].Head} is below the range lower bound {lowerBound}" + ); + Assert.True( + subnets[i].Tail.IsLessThanOrEqualTo(upperBound), + $"subnet {i} tail {subnets[i].Tail} exceeds range upper bound {upperBound}" + ); + + // gap-free: next head = this tail + 1 + if (i < subnets.Count - 1) + { + Assert.True( + IPAddressMath.TryIncrement(subnets[i].Tail, out var nextHead), + $"overflow incrementing tail {subnets[i].Tail} at index {i}" + ); + Assert.Equal(nextHead, subnets[i + 1].Head); + } + } + } + + #endregion // end: FewestConsecutiveSubnetsFor #region LargestSubnet + /// Verifies that returns one of the tied-largest subnets when multiple share the largest size. [Fact] public void LargestSubnet_Ambiguous_ReturnsOneOfLargest_Test() { @@ -614,66 +776,87 @@ public void LargestSubnet_Ambiguous_ReturnsOneOfLargest_Test() Assert.Equal(new Subnet(IPAddress.Any, 16), result); } + /// Verifies that throws when given an empty sequence. [Fact] - public void LargestSubnet_EmptyInput_ReturnsNull_Test() + public void LargestSubnet_EmptyInput_ThrowsInvalidOperationException_Test() { // Arrange var subnets = Enumerable.Empty(); // Act - var result = SubnetUtilities.LargestSubnet(subnets); - // Assert - Assert.Null(result); + Assert.Throws(() => SubnetUtilities.LargestSubnet(subnets)); } + /// Verifies that throws when given a null input. [Fact] - public void LargestSubnet_NullInput_ReturnsNull_Test() + public void LargestSubnet_NullInput_ThrowsArgumentNullException_Test() { // Arrange // Act - var result = SubnetUtilities.LargestSubnet(null); + // Assert + Assert.Throws(() => SubnetUtilities.LargestSubnet(null)); + } + + /// Verifies that throws when the collection contains null elements. + [Fact] + public void LargestSubnet_NullElements_ThrowsArgumentException_Test() + { + // Arrange + var subnets = new Subnet[] { null, null, null }; + // Act // Assert - Assert.Null(result); + Assert.Throws(() => SubnetUtilities.LargestSubnet(subnets)); + } + + /// Verifies that throws when a null element appears mid-sequence (not as the first element). + [Fact] + public void LargestSubnet_NullElementMidSequence_ThrowsArgumentException_Test() + { + // Arrange: the first element is valid, so the initial current check passes; + // the null appears mid-iteration, exercising the loop-level guard. + var subnets = new Subnet[] { Subnet.Parse("10.0.0.0/24"), null, Subnet.Parse("192.168.0.0/16") }; + + // Act / Assert + Assert.Throws(() => SubnetUtilities.LargestSubnet(subnets)); } + /// Verifies that returns the single element when the input contains exactly one subnet. [Fact] - public void LargestSubnet_Single_ReturnSingle_Test() + public void LargestSubnet_Single_ReturnsSingle_Test() { // Arrange var expected = new Subnet(IPAddress.Any, 16); - var subnets = new[] { expected }; // Act - var result = SubnetUtilities.LargestSubnet(subnets); // Assert Assert.Same(expected, result); } + /// Verifies that returns the largest subnet from a collection of mixed sizes. [Fact] public void LargestSubnet_ReturnsLargest_Test() { // Arrange var expected = new Subnet(IPAddress.Any, 16); - var subnets = new[] { expected, new Subnet(IPAddress.Any, 24), new Subnet(IPAddress.Any, 32) }; // Act - var result = SubnetUtilities.LargestSubnet(subnets); // Assert Assert.Same(expected, result); } - #endregion + #endregion // end: LargestSubnet #region SmallestSubnet + /// Verifies that returns one of the tied-smallest subnets when multiple share the smallest size. [Fact] public void SmallestSubnet_Ambiguous_ReturnsOneOfSmallest_Test() { @@ -693,36 +876,46 @@ public void SmallestSubnet_Ambiguous_ReturnsOneOfSmallest_Test() Assert.Equal(new Subnet(IPAddress.Any, 32), result); } + /// Verifies that throws when given an empty sequence. [Fact] - public void SmallestSubnet_EmptyInput_ReturnsNull_Test() + public void SmallestSubnet_EmptyInput_ThrowsInvalidOperationException_Test() { // Arrange var subnets = Enumerable.Empty(); // Act - var result = SubnetUtilities.SmallestSubnet(subnets); - // Assert - Assert.Null(result); + Assert.Throws(() => SubnetUtilities.SmallestSubnet(subnets)); } + /// Verifies that throws when given a null input. [Fact] - public void SmallestSubnet_NullInput_ReturnsNull_Test() + public void SmallestSubnet_NullInput_ThrowsArgumentNullException_Test() { // Arrange // Act - var result = SubnetUtilities.SmallestSubnet(null); + // Assert + Assert.Throws(() => SubnetUtilities.SmallestSubnet(null)); + } + + /// Verifies that throws when the collection contains null elements. + [Fact] + public void SmallestSubnet_NullElements_ThrowsArgumentException_Test() + { + // Arrange + var subnets = new Subnet[] { null, null, null }; + // Act // Assert - Assert.Null(result); + Assert.Throws(() => SubnetUtilities.SmallestSubnet(subnets)); } + /// Verifies that returns the single element when the input contains exactly one subnet. [Fact] - public void SmallestSubnet_Single_ReturnSingle_Test() + public void SmallestSubnet_Single_ReturnsSingle_Test() { // Arrange var expected = new Subnet(IPAddress.Any, 16); - var subnets = new[] { expected }; // Act @@ -732,22 +925,40 @@ public void SmallestSubnet_Single_ReturnSingle_Test() Assert.Same(expected, result); } + /// Verifies that returns the smallest subnet from a collection of mixed sizes. [Fact] - public void SmallestSubnet_ReturnsSmallestSubnet_Test() + public void SmallestSubnet_ReturnsSmallest_Test() { // Arrange var expected = new Subnet(IPAddress.Any, 32); - var subnets = new[] { expected, new Subnet(IPAddress.Any, 24), new Subnet(IPAddress.Any, 16) }; // Act - var result = SubnetUtilities.SmallestSubnet(subnets); // Assert Assert.Same(expected, result); } - #endregion + #endregion // end: SmallestSubnet + + #region maxEnumerationExponent propagation + + /// Verifies that propagates maxEnumerationExponent. + [Fact] + public void FewestConsecutiveSubnetsFor_ExponentPropagates_Test() + { + var results = SubnetUtilities.FewestConsecutiveSubnetsFor( + IPAddress.Parse("10.0.0.0"), + IPAddress.Parse("10.0.0.255"), + maxEnumerationExponent: 4 + ); + foreach (var subnet in results) + { + Assert.Equal(4, subnet.MaxEnumerationExponent); + } + } + + #endregion // end: maxEnumerationExponent propagation } } diff --git a/src/Arcus.Tests/XunitSerializers/FormatProviderXunitSerializer.cs b/src/Arcus.Tests/XunitSerializers/FormatProviderXunitSerializer.cs new file mode 100644 index 0000000..de22302 --- /dev/null +++ b/src/Arcus.Tests/XunitSerializers/FormatProviderXunitSerializer.cs @@ -0,0 +1,66 @@ +using System.Globalization; + +namespace Arcus.Tests.XunitSerializers +{ + /// + /// for + /// + /// + /// Handles instances, serialized as CultureInfo:<name>. + /// + public class FormatProviderXunitSerializer : IXunitSerializer + { + private const string CultureInfoPrefix = "CultureInfo:"; + + /// + /// Initializes a new instance of the class. + /// + /// from xUnit Serialization support in v3 + public FormatProviderXunitSerializer() { } + + /// + public bool IsSerializable(Type type, object value, out string failureReason) + { + if (typeof(IFormatProvider).IsAssignableFrom(type) && value is CultureInfo) + { + failureReason = null; + return true; + } + + failureReason = + $"Type {type.FullName} with value type {value?.GetType().FullName ?? "null"} is not supported by {nameof(FormatProviderXunitSerializer)}."; + return false; + } + + /// + public string Serialize(object value) + { + if (value is CultureInfo cultureInfo) + { + return CultureInfoPrefix + cultureInfo.Name; + } + + throw new InvalidOperationException( + $"Invalid type for serialization: {value?.GetType().FullName ?? "null"} is not supported by {nameof(FormatProviderXunitSerializer)}." + ); + } + + /// + public object Deserialize(Type type, string serializedValue) + { + if (!typeof(IFormatProvider).IsAssignableFrom(type)) + { + throw new ArgumentException( + $"Invalid type for deserialization: {type.FullName} is not supported by {nameof(FormatProviderXunitSerializer)}" + ); + } + + if (serializedValue.StartsWith(CultureInfoPrefix, StringComparison.Ordinal)) + { + return new CultureInfo(serializedValue.Substring(CultureInfoPrefix.Length)); + } + + throw new InvalidOperationException($"Could not parse serialized IFormatProvider \"{serializedValue}\""); + } + } +} diff --git a/src/Arcus.Tests/XunitSerializers/IIPAddressRangeXunitSerializer.cs b/src/Arcus.Tests/XunitSerializers/IIPAddressRangeXunitSerializer.cs new file mode 100644 index 0000000..1198c5b --- /dev/null +++ b/src/Arcus.Tests/XunitSerializers/IIPAddressRangeXunitSerializer.cs @@ -0,0 +1,86 @@ +using System.Net; + +namespace Arcus.Tests.XunitSerializers +{ + /// + /// for + /// + /// + /// Serializes concrete as Subnet:<cidr> + /// and as Range:<head> - <tail>. + /// +#pragma warning disable S101 + public class IIPAddressRangeXunitSerializer : IXunitSerializer +#pragma warning restore S101 + { + private const string SubnetPrefix = "Subnet:"; + private const string RangePrefix = "Range:"; + + /// + /// Initializes a new instance of the class. + /// + /// from xUnit Serialization support in v3 + public IIPAddressRangeXunitSerializer() { } + + /// + public bool IsSerializable(Type type, object value, out string failureReason) + { + if (typeof(IIPAddressRange).IsAssignableFrom(type) && value is IIPAddressRange) + { + failureReason = null; + return true; + } + + failureReason = $"Type {type.FullName} is not supported by {nameof(IIPAddressRangeXunitSerializer)}."; + return false; + } + + /// + public string Serialize(object value) + { + if (value is Subnet subnet) + { + return SubnetPrefix + subnet.ToString("f", null); + } + + if (value is IPAddressRange range) + { + return RangePrefix + range.ToString("G", null); + } + + throw new InvalidOperationException( + $"Invalid type for serialization: {value.GetType().FullName} is not supported by {nameof(IIPAddressRangeXunitSerializer)}." + ); + } + + /// + public object Deserialize(Type type, string serializedValue) + { + if (!typeof(IIPAddressRange).IsAssignableFrom(type)) + { + throw new ArgumentException( + $"Invalid type for deserialization: {type.FullName} is not supported by {nameof(IIPAddressRangeXunitSerializer)}" + ); + } + + if (serializedValue.StartsWith(SubnetPrefix, StringComparison.Ordinal)) + { + return Subnet.Parse(serializedValue.Substring(SubnetPrefix.Length)); + } + + if (serializedValue.StartsWith(RangePrefix, StringComparison.Ordinal)) + { + var parts = serializedValue.Substring(RangePrefix.Length).Split('-').Select(s => s.Trim()).ToList(); + + if (parts.Count != 2) + { + throw new InvalidOperationException($"Could not parse serialized IIPAddressRange \"{serializedValue}\""); + } + + return new IPAddressRange(IPAddress.Parse(parts[0]), IPAddress.Parse(parts[1])); + } + + throw new InvalidOperationException($"Could not parse serialized IIPAddressRange \"{serializedValue}\""); + } + } +} diff --git a/src/Arcus.Tests/XunitSerializers/IPAddressRangeXunitSerializer.cs b/src/Arcus.Tests/XunitSerializers/IPAddressRangeXunitSerializer.cs index e02871e..a2b44bf 100644 --- a/src/Arcus.Tests/XunitSerializers/IPAddressRangeXunitSerializer.cs +++ b/src/Arcus.Tests/XunitSerializers/IPAddressRangeXunitSerializer.cs @@ -1,7 +1,4 @@ -using System; -using System.Linq; -using System.Net; -using Xunit.Sdk; +using System.Net; namespace Arcus.Tests.XunitSerializers { diff --git a/src/Arcus.Tests/XunitSerializers/IPAddressXunitSerializer.cs b/src/Arcus.Tests/XunitSerializers/IPAddressXunitSerializer.cs new file mode 100644 index 0000000..156ba6f --- /dev/null +++ b/src/Arcus.Tests/XunitSerializers/IPAddressXunitSerializer.cs @@ -0,0 +1,55 @@ +using System.Net; + +namespace Arcus.Tests.XunitSerializers +{ + /// + /// for + /// + public class IPAddressXunitSerializer : IXunitSerializer + { + /// + /// Initializes a new instance of the class. + /// + /// from xUnit Serialization support in v3 + public IPAddressXunitSerializer() { } + + /// + public bool IsSerializable(Type type, object value, out string failureReason) + { + if (type == typeof(IPAddress) && value is IPAddress) + { + failureReason = null; + return true; + } + + failureReason = $"Type {type.FullName} is not supported by {nameof(IPAddressXunitSerializer)}."; + return false; + } + + /// + public string Serialize(object value) + { + if (value is IPAddress ipAddress) + { + return ipAddress.ToString(); + } + + throw new InvalidOperationException( + $"Invalid type for serialization: {value.GetType().FullName} is not supported by {nameof(IPAddressXunitSerializer)}." + ); + } + + /// + public object Deserialize(Type type, string serializedValue) + { + if (type == typeof(IPAddress)) + { + return IPAddress.Parse(serializedValue); + } + + throw new ArgumentException( + $"Invalid type for deserialization: {type.FullName} is not supported by {nameof(IPAddressXunitSerializer)}" + ); + } + } +} diff --git a/src/Arcus.Tests/XunitSerializers/SubnetEnumerableXunitSerializer.cs b/src/Arcus.Tests/XunitSerializers/SubnetEnumerableXunitSerializer.cs new file mode 100644 index 0000000..8db6e18 --- /dev/null +++ b/src/Arcus.Tests/XunitSerializers/SubnetEnumerableXunitSerializer.cs @@ -0,0 +1,60 @@ +namespace Arcus.Tests.XunitSerializers +{ + /// + /// for of + /// + public class SubnetEnumerableXunitSerializer : IXunitSerializer + { + private const string Separator = "|"; + + /// + /// Initializes a new instance of the class. + /// + /// from xUnit Serialization support in v3 + public SubnetEnumerableXunitSerializer() { } + + /// + public bool IsSerializable(Type type, object value, out string failureReason) + { + if (type == typeof(IEnumerable) && value is IEnumerable) + { + failureReason = null; + return true; + } + + failureReason = $"Type {type.FullName} is not supported by {nameof(SubnetEnumerableXunitSerializer)}."; + return false; + } + + /// + public string Serialize(object value) + { + if (value is IEnumerable subnets) + { + return string.Join(Separator, subnets.Select(s => s.ToString("f", null))); + } + + throw new InvalidOperationException( + $"Invalid type for serialization: {value.GetType().FullName} is not supported by {nameof(SubnetEnumerableXunitSerializer)}." + ); + } + + /// + public object Deserialize(Type type, string serializedValue) + { + if (type == typeof(IEnumerable)) + { + if (serializedValue.Length == 0) + { + return Enumerable.Empty(); + } + + return serializedValue.Split(new[] { Separator }, StringSplitOptions.None).Select(Subnet.Parse).ToList(); + } + + throw new ArgumentException( + $"Invalid type for deserialization: {type.FullName} is not supported by {nameof(SubnetEnumerableXunitSerializer)}" + ); + } + } +} diff --git a/src/Arcus.Tests/XunitSerializers/SubnetXunitSerializer.cs b/src/Arcus.Tests/XunitSerializers/SubnetXunitSerializer.cs index cf08228..999439c 100644 --- a/src/Arcus.Tests/XunitSerializers/SubnetXunitSerializer.cs +++ b/src/Arcus.Tests/XunitSerializers/SubnetXunitSerializer.cs @@ -1,7 +1,4 @@ -using System; -using Xunit.Sdk; - -namespace Arcus.Tests.XunitSerializers +namespace Arcus.Tests.XunitSerializers { /// /// for diff --git a/src/Arcus.Tests/packages.lock.json b/src/Arcus.Tests/packages.lock.json index 75298f6..a9b45d3 100644 --- a/src/Arcus.Tests/packages.lock.json +++ b/src/Arcus.Tests/packages.lock.json @@ -1,27 +1,30 @@ { - "version": 1, + "version": 2, "dependencies": { ".NETFramework,Version=v4.8": { - "AsyncFixer": { + "Microsoft.NET.Test.Sdk": { "type": "Direct", - "requested": "[1.6.0, )", - "resolved": "1.6.0", - "contentHash": "/Xfs9H3UMfEv64cwT+C/JrTRp4w08BmPuFbj0ageadCHpx6rxYJxAU2C6sEqRFG22xmGk5cX9ewzoiiehWVHOw==" + "requested": "[18.7.0, )", + "resolved": "18.7.0", + "contentHash": "49xH9j4UzCh2hMohJp53g3wUTvyycECw7CtVht4gfCz5ykudB1uBcF6D0TtgJPjCtP76UPW53bQElKdCeX+dUg==", + "dependencies": { + "Microsoft.CodeCoverage": "18.7.0" + } }, - "Microsoft.NET.Test.Sdk": { + "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", - "requested": "[18.0.1, )", - "resolved": "18.0.1", - "contentHash": "WNpu6vI2rA0pXY4r7NKxCN16XRWl5uHu6qjuyVLoDo6oYEggIQefrMjkRuibQHm/NslIUNCcKftvoWAN80MSAg==", + "requested": "[1.0.3, )", + "resolved": "1.0.3", + "contentHash": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==", "dependencies": { - "Microsoft.CodeCoverage": "18.0.1" + "Microsoft.NETFramework.ReferenceAssemblies.net48": "1.0.3" } }, "Microsoft.VisualStudio.Threading.Analyzers": { "type": "Direct", - "requested": "[17.14.15, )", - "resolved": "17.14.15", - "contentHash": "mXQPJsbuUD2ydq4/ffd8h8tSOFCXec+2xJOVNCvXjuMOq/+5EKHq3D2m2MC2+nUaXeFMSt66VS/J4HdKBixgcw==" + "requested": "[18.7.23, )", + "resolved": "18.7.23", + "contentHash": "dyj6z8m+LFjpeH69hdnciCcRCmD0YgMlvuQEywiEshjqj/blgQ+PuWX8Vd5gkWQFoBN4e+TFxx6DvGNSv/gIKg==" }, "NSubstitute": { "type": "Direct", @@ -41,27 +44,27 @@ }, "Roslynator.Analyzers": { "type": "Direct", - "requested": "[4.14.1, )", - "resolved": "4.14.1", - "contentHash": "yMSjze/xMYDF6PCE60/ULWx0tttNyKAndw2KijNxbKil0FX8nvDeEneDZGma8Uifk17RlfZqIXxf1mmBmhRHjg==" + "requested": "[4.15.0, )", + "resolved": "4.15.0", + "contentHash": "E8fu71Y+vzhniJKy1K13Rzwm8Vjcj7CDFBpflqncKUZfc3SzuF0kMBiyMmgXNPkjWTdE3Z+30HlGgYjHONZY/Q==" }, "Roslynator.CodeAnalysis.Analyzers": { "type": "Direct", - "requested": "[4.14.1, )", - "resolved": "4.14.1", - "contentHash": "axessewcgSLn+UAtKyMKPgXpzIldnivynjRe9qsRjrVtrxjctDpFDEjAkcbpDjhR0ccm8/+ihOna60wnXJ/66A==" + "requested": "[4.15.0, )", + "resolved": "4.15.0", + "contentHash": "XPY6vqHG8jLJ1S0SGzLDtyuDjmkcIAGuLRrsG3SlAuyAOlwnVfA1glgIy6YqXutoi7hwd1fqZC2W9zoMntuZNg==" }, "Roslynator.Formatting.Analyzers": { "type": "Direct", - "requested": "[4.14.1, )", - "resolved": "4.14.1", - "contentHash": "hALZsyELJXhj7WQw7Q7nLI7sELre8AlQIMyGxXpmpK+iBqShPcWHuJMGjIsTMa/bC8Q5xyQ4iFfRtm0bP1zisQ==" + "requested": "[4.15.0, )", + "resolved": "4.15.0", + "contentHash": "/LvSJGJbrx+OYC2GePmiAA9fRTfz37R/oaHC2j8YK/j9lE8ykZESCGTMVGqXF5woc6RbzfJlpYkzeG6fAV6hFQ==" }, "SonarAnalyzer.CSharp": { "type": "Direct", - "requested": "[10.15.0.120848, )", - "resolved": "10.15.0.120848", - "contentHash": "1hM3HVRl5jdC/ZBDu+G7CCYLXRGe/QaP01Zy+c9ETPhY7lWD8g8HiefY6sGaH0T3CJ4wAy0/waGgQTh0TYy0oQ==" + "requested": "[10.28.0.143324, )", + "resolved": "10.28.0.143324", + "contentHash": "ZnmYHFiQw2Aph2ft9c7UqVrqe79aZR5l7oeG59+sgrSAO9J159meglP1Zo34+Mcw4etIUTC9btYnP0Ar/rQIyg==" }, "StyleCop.Analyzers": { "type": "Direct", @@ -69,6 +72,12 @@ "resolved": "1.1.118", "contentHash": "Onx6ovGSqXSK07n/0eM3ZusiNdB6cIlJdabQhWGgJp3Vooy9AaLS/tigeybOJAobqbtggTamoWndz72JscZBvw==" }, + "xunit.analyzers": { + "type": "Direct", + "requested": "[1.27.0, )", + "resolved": "1.27.0", + "contentHash": "y/pxIQaLvk/kxAoDkZW9GnHLCEqzwl5TW0vtX3pweyQpjizB9y3DXhb9pkw2dGeUqhLjsxvvJM1k89JowU6z3g==" + }, "xunit.runner.visualstudio": { "type": "Direct", "requested": "[3.1.5, )", @@ -80,11 +89,11 @@ }, "xunit.v3": { "type": "Direct", - "requested": "[3.2.0, )", - "resolved": "3.2.0", - "contentHash": "ABdobDsBRs0JV+Ux6t0sM26noKFVbCkGOvU/x/YeShjI4aYH+thZeOGg8k5etju+5Ud4BZfzQEtVfvnAx8G2cg==", + "requested": "[3.2.2, )", + "resolved": "3.2.2", + "contentHash": "L+4/4y0Uqcg8/d6hfnxhnwh4j9FaeULvefTwrk30rr1o4n/vdPfyUQ8k0yzH8VJx7bmFEkDdcRfbtbjEHlaYcA==", "dependencies": { - "xunit.v3.mtp-v1": "[3.2.0]" + "xunit.v3.mtp-v1": "[3.2.2]" } }, "Castle.Core": { @@ -92,11 +101,6 @@ "resolved": "5.1.1", "contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==" }, - "Gulliver": { - "type": "Transitive", - "resolved": "3.0.0", - "contentHash": "J232PI4ccScfLKt59UzyHBRZLcAJpyJ+N/at1fFF/XxBKwjl8idDVPkMU3FZkwaaU/e3Ert8/EnGLg5CxmQ8dQ==" - }, "Microsoft.ApplicationInsights": { "type": "Transitive", "resolved": "2.23.0", @@ -113,45 +117,45 @@ "System.Threading.Tasks.Extensions": "4.5.4" } }, - "Microsoft.Bcl.HashCode": { + "Microsoft.CodeCoverage": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "GI4jcoi6eC9ZhNOQylIBaWOQjyGaR8T6N3tC1u8p3EXfndLCVNNWa+Zp+ocjvvS3kNBN09Zma2HXL0ezO0dRfw==" + "resolved": "18.7.0", + "contentHash": "+wFfx9s7D9wegM0RziXMj2kvYDT4qcqXXtyjiQwSZOGQ2wwcOAJQcD6eQXk02jt0MvRNawtp8TJxTrV+wD8X1g==" }, - "Microsoft.CodeCoverage": { + "Microsoft.NETFramework.ReferenceAssemblies.net48": { "type": "Transitive", - "resolved": "18.0.1", - "contentHash": "O+utSr97NAJowIQT/OVp3Lh9QgW/wALVTP4RG1m2AfFP4IyJmJz0ZBmFJUsRQiAPgq6IRC0t8AAzsiPIsaUDEA==" + "resolved": "1.0.3", + "contentHash": "zMk4D+9zyiEWByyQ7oPImPN/Jhpj166Ky0Nlla4eXlNL8hI/BtSJsgR8Inldd4NNpIAH3oh8yym0W2DrhXdSLQ==" }, "Microsoft.Testing.Extensions.Telemetry": { "type": "Transitive", - "resolved": "1.9.0", - "contentHash": "4ica7o+ZreUCsXoQLShWeLz6K5q9D/B+VgXvmpPSLsnSpK4DLZfMyltBaBr83Tnm21c1riOKcXXEk920Ael17Q==", + "resolved": "1.9.1", + "contentHash": "No5AudZMmSb+uNXjlgL2y3/stHD2IT4uxqc5yHwkE+/nNux9jbKcaJMvcp9SwgP4DVD8L9/P3OUz8mmmcvEIdQ==", "dependencies": { "Microsoft.ApplicationInsights": "2.23.0", - "Microsoft.Testing.Platform": "1.9.0", + "Microsoft.Testing.Platform": "1.9.1", "System.Diagnostics.DiagnosticSource": "6.0.0" } }, "Microsoft.Testing.Extensions.TrxReport.Abstractions": { "type": "Transitive", - "resolved": "1.9.0", - "contentHash": "ZC7Xwva/+dREnZ5BU0gozA1uT2cFwt90uY9maPLy1lLpiedR/Fu3K2FiwLg70hJjT+tGblYmxzuNa8z1jQVSGw==", + "resolved": "1.9.1", + "contentHash": "AL46Xe1WBi85Ntd4mNPvat5ZSsZ2uejiVqoKCypr8J3wK0elA5xJ3AN4G/Q4GIwzUFnggZoH/DBjnr9J18IO/g==", "dependencies": { - "Microsoft.Testing.Platform": "1.9.0" + "Microsoft.Testing.Platform": "1.9.1" } }, "Microsoft.Testing.Platform": { "type": "Transitive", - "resolved": "1.9.0", - "contentHash": "OE79Vc5rXwFYciAPY/mqv92XvdhK+pvCHdVHcS0bBpWwWQbnzI18FiSEEYY+lYpB0HHl0fDQgcCK3ZTYKGs8bA==" + "resolved": "1.9.1", + "contentHash": "QafNtNSmEI0zazdebnsIkDKmFtTSpmx/5PLOjURWwozcPb3tvRxzosQSL8xwYNM1iPhhKiBksXZyRSE2COisrA==" }, "Microsoft.Testing.Platform.MSBuild": { "type": "Transitive", - "resolved": "1.9.0", - "contentHash": "P7c0HfBJ/AqKyRfmaA+cyl8I9jxDEyvQRJvMUH5IZCjACTPQ9wXb0xdA0CmcxhBUtm20GxWvx2fq3fMxG11hDg==", + "resolved": "1.9.1", + "contentHash": "oTUtyR4X/s9ytuiNA29FGsNCCH0rNmY5Wdm14NCKLjTM1cT9edVSlA+rGS/mVmusPqcP0l/x9qOnMXg16v87RQ==", "dependencies": { - "Microsoft.Testing.Platform": "1.9.0" + "Microsoft.Testing.Platform": "1.9.1" } }, "Microsoft.TestPlatform.ObjectModel": { @@ -243,15 +247,10 @@ "System.Runtime.CompilerServices.Unsafe": "4.5.3" } }, - "xunit.analyzers": { - "type": "Transitive", - "resolved": "1.25.0", - "contentHash": "J3wD5p2VVOg6TXeOOCrD+7Nl2H/LOZuK1GovDyPbFFnvv8katIULfBCj2sQLIPKT22Ds44j1aHjJle4AU7jCbA==" - }, "xunit.v3.assert": { "type": "Transitive", - "resolved": "3.2.0", - "contentHash": "AT3jVbptSazsf+aKQt14eyhw0NN9GWj6SaYifJfZ873r86NbPeTlxshjij9aOqmtrR+C1fHwqeneO4mMZKK7ag==", + "resolved": "3.2.2", + "contentHash": "BPciBghgEEaJN/JG00QfCYDfEfnLgQhfnYEy+j1izoeHVNYd5+3Wm8GJ6JgYysOhpBPYGE+sbf75JtrRc7jrdA==", "dependencies": { "System.Collections.Immutable": "6.0.0", "System.Memory": "4.5.5" @@ -259,115 +258,90 @@ }, "xunit.v3.common": { "type": "Transitive", - "resolved": "3.2.0", - "contentHash": "InLYIBD54RVFgX1S5y0g+Mia3coGsA+cf+SvieBH3kO5TrH3d4sb59+4M6nfBlGg8EqUCHbyGKKroQPquYEt7Q==", + "resolved": "3.2.2", + "contentHash": "Hj775PEH6GTbbg0wfKRvG2hNspDCvTH9irXhH4qIWgdrOSV1sQlqPie+DOvFeigsFg2fxSM3ZAaaCDQs+KreFA==", "dependencies": { "Microsoft.Bcl.AsyncInterfaces": "6.0.0" } }, "xunit.v3.core.mtp-v1": { "type": "Transitive", - "resolved": "3.2.0", - "contentHash": "0B+nZqJXl6BGTXT6kOf4LOnH5qxDvrufctNTjlxjXne4SqYybhCnfYpWRLy9NJ/0JkELyVlap8YK7HH+x/sHeQ==", + "resolved": "3.2.2", + "contentHash": "Ga5aA2Ca9ktz+5k3g5ukzwfexwoqwDUpV6z7atSEUvqtd6JuybU1XopHqg1oFd78QdTfZgZE9h5sHpO4qYIi5w==", "dependencies": { - "Microsoft.Testing.Extensions.Telemetry": "1.9.0", - "Microsoft.Testing.Extensions.TrxReport.Abstractions": "1.9.0", - "Microsoft.Testing.Platform": "1.9.0", - "Microsoft.Testing.Platform.MSBuild": "1.9.0", - "xunit.v3.extensibility.core": "[3.2.0]", - "xunit.v3.runner.inproc.console": "[3.2.0]" + "Microsoft.Testing.Extensions.Telemetry": "1.9.1", + "Microsoft.Testing.Extensions.TrxReport.Abstractions": "1.9.1", + "Microsoft.Testing.Platform": "1.9.1", + "Microsoft.Testing.Platform.MSBuild": "1.9.1", + "xunit.v3.extensibility.core": "[3.2.2]", + "xunit.v3.runner.inproc.console": "[3.2.2]" } }, "xunit.v3.extensibility.core": { "type": "Transitive", - "resolved": "3.2.0", - "contentHash": "89MWZ5gXT6VJ/qe9OCASBsaenFr00UCccFbkJxzkOsrAkoFjJSDJ+5xqXpZUxTBGJ4Gw91bJ4bwW9Fag0b8xhw==", + "resolved": "3.2.2", + "contentHash": "srY8z/oMPvh/t8axtO2DwrHajhFMH7tnqKildvYrVQIfICi8fOn3yIBWkVPAcrKmHMwvXRJ/XsQM3VMR6DOYfQ==", "dependencies": { - "xunit.v3.common": "[3.2.0]" + "xunit.v3.common": "[3.2.2]" } }, "xunit.v3.mtp-v1": { "type": "Transitive", - "resolved": "3.2.0", - "contentHash": "Rg5djTBTao7QUEK28EXeacjwC6LnU/NunlJzYrNkhtOgggpueYzGDfmyKu5hDbAEXhAl6vjrzfcCnhZyX8Q+DA==", + "resolved": "3.2.2", + "contentHash": "O41aAzYKBT5PWqATa1oEWVNCyEUypFQ4va6K0kz37dduV3EKzXNMaV2UnEhufzU4Cce1I33gg0oldS8tGL5I0A==", "dependencies": { - "xunit.analyzers": "1.25.0", - "xunit.v3.assert": "[3.2.0]", - "xunit.v3.core.mtp-v1": "[3.2.0]" + "xunit.analyzers": "1.27.0", + "xunit.v3.assert": "[3.2.2]", + "xunit.v3.core.mtp-v1": "[3.2.2]" } }, "xunit.v3.runner.common": { "type": "Transitive", - "resolved": "3.2.0", - "contentHash": "2i1/IvIH9x6M+8TRlr8k6/4NCem44zYzveps7p8XL4jjsIM/1K2ahfcvtom3tu9RQxHF3czT7f3DOAvHp+01gQ==", + "resolved": "3.2.2", + "contentHash": "/hkHkQCzGrugelOAehprm7RIWdsUFVmIVaD6jDH/8DNGCymTlKKPTbGokD5czbAfqfex47mBP0sb0zbHYwrO/g==", "dependencies": { "Microsoft.Win32.Registry": "[5.0.0]", - "xunit.v3.common": "[3.2.0]" + "xunit.v3.common": "[3.2.2]" } }, "xunit.v3.runner.inproc.console": { "type": "Transitive", - "resolved": "3.2.0", - "contentHash": "/nXHLSgKv5OYSjo3bTSzvTYfxbzApTEeip5T85PayDZgGbMHA679zeVhN/1n6//pW/uJ2j7QhDdHh2r98f13hQ==", + "resolved": "3.2.2", + "contentHash": "ulWOdSvCk+bPXijJZ73bth9NyoOHsAs1ZOvamYbCkD4DNLX/Bd29Ve2ZNUwBbK0MqfIYWXHZViy/HKrdEC/izw==", "dependencies": { - "xunit.v3.extensibility.core": "[3.2.0]", - "xunit.v3.runner.common": "[3.2.0]" + "xunit.v3.extensibility.core": "[3.2.2]", + "xunit.v3.runner.common": "[3.2.2]" } }, "arcus": { "type": "Project", "dependencies": { - "Gulliver": "[3.0.0, )", "Microsoft.Bcl.HashCode": "[6.0.0, )" } - } - }, - ".NETFramework,Version=v4.8/win-x86": { - "Microsoft.Win32.Registry": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==", - "dependencies": { - "System.Security.AccessControl": "5.0.0", - "System.Security.Principal.Windows": "5.0.0" - } }, - "System.Security.AccessControl": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "dagJ1mHZO3Ani8GH0PHpPEe/oYO+rVdbQjvjJkBRNQkX4t0r1iaeGn8+/ybkSLEan3/slM0t59SVdHzuHf2jmw==", - "dependencies": { - "System.Security.Principal.Windows": "5.0.0" - } - }, - "System.Security.Principal.Windows": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==" + "Microsoft.Bcl.HashCode": { + "type": "CentralTransitive", + "requested": "[6.0.0, )", + "resolved": "6.0.0", + "contentHash": "GI4jcoi6eC9ZhNOQylIBaWOQjyGaR8T6N3tC1u8p3EXfndLCVNNWa+Zp+ocjvvS3kNBN09Zma2HXL0ezO0dRfw==" } }, "net10.0": { - "AsyncFixer": { - "type": "Direct", - "requested": "[1.6.0, )", - "resolved": "1.6.0", - "contentHash": "/Xfs9H3UMfEv64cwT+C/JrTRp4w08BmPuFbj0ageadCHpx6rxYJxAU2C6sEqRFG22xmGk5cX9ewzoiiehWVHOw==" - }, "Microsoft.NET.Test.Sdk": { "type": "Direct", - "requested": "[18.0.1, )", - "resolved": "18.0.1", - "contentHash": "WNpu6vI2rA0pXY4r7NKxCN16XRWl5uHu6qjuyVLoDo6oYEggIQefrMjkRuibQHm/NslIUNCcKftvoWAN80MSAg==", + "requested": "[18.7.0, )", + "resolved": "18.7.0", + "contentHash": "49xH9j4UzCh2hMohJp53g3wUTvyycECw7CtVht4gfCz5ykudB1uBcF6D0TtgJPjCtP76UPW53bQElKdCeX+dUg==", "dependencies": { - "Microsoft.CodeCoverage": "18.0.1", - "Microsoft.TestPlatform.TestHost": "18.0.1" + "Microsoft.CodeCoverage": "18.7.0", + "Microsoft.TestPlatform.TestHost": "18.7.0" } }, "Microsoft.VisualStudio.Threading.Analyzers": { "type": "Direct", - "requested": "[17.14.15, )", - "resolved": "17.14.15", - "contentHash": "mXQPJsbuUD2ydq4/ffd8h8tSOFCXec+2xJOVNCvXjuMOq/+5EKHq3D2m2MC2+nUaXeFMSt66VS/J4HdKBixgcw==" + "requested": "[18.7.23, )", + "resolved": "18.7.23", + "contentHash": "dyj6z8m+LFjpeH69hdnciCcRCmD0YgMlvuQEywiEshjqj/blgQ+PuWX8Vd5gkWQFoBN4e+TFxx6DvGNSv/gIKg==" }, "NSubstitute": { "type": "Direct", @@ -386,27 +360,27 @@ }, "Roslynator.Analyzers": { "type": "Direct", - "requested": "[4.14.1, )", - "resolved": "4.14.1", - "contentHash": "yMSjze/xMYDF6PCE60/ULWx0tttNyKAndw2KijNxbKil0FX8nvDeEneDZGma8Uifk17RlfZqIXxf1mmBmhRHjg==" + "requested": "[4.15.0, )", + "resolved": "4.15.0", + "contentHash": "E8fu71Y+vzhniJKy1K13Rzwm8Vjcj7CDFBpflqncKUZfc3SzuF0kMBiyMmgXNPkjWTdE3Z+30HlGgYjHONZY/Q==" }, "Roslynator.CodeAnalysis.Analyzers": { "type": "Direct", - "requested": "[4.14.1, )", - "resolved": "4.14.1", - "contentHash": "axessewcgSLn+UAtKyMKPgXpzIldnivynjRe9qsRjrVtrxjctDpFDEjAkcbpDjhR0ccm8/+ihOna60wnXJ/66A==" + "requested": "[4.15.0, )", + "resolved": "4.15.0", + "contentHash": "XPY6vqHG8jLJ1S0SGzLDtyuDjmkcIAGuLRrsG3SlAuyAOlwnVfA1glgIy6YqXutoi7hwd1fqZC2W9zoMntuZNg==" }, "Roslynator.Formatting.Analyzers": { "type": "Direct", - "requested": "[4.14.1, )", - "resolved": "4.14.1", - "contentHash": "hALZsyELJXhj7WQw7Q7nLI7sELre8AlQIMyGxXpmpK+iBqShPcWHuJMGjIsTMa/bC8Q5xyQ4iFfRtm0bP1zisQ==" + "requested": "[4.15.0, )", + "resolved": "4.15.0", + "contentHash": "/LvSJGJbrx+OYC2GePmiAA9fRTfz37R/oaHC2j8YK/j9lE8ykZESCGTMVGqXF5woc6RbzfJlpYkzeG6fAV6hFQ==" }, "SonarAnalyzer.CSharp": { "type": "Direct", - "requested": "[10.15.0.120848, )", - "resolved": "10.15.0.120848", - "contentHash": "1hM3HVRl5jdC/ZBDu+G7CCYLXRGe/QaP01Zy+c9ETPhY7lWD8g8HiefY6sGaH0T3CJ4wAy0/waGgQTh0TYy0oQ==" + "requested": "[10.28.0.143324, )", + "resolved": "10.28.0.143324", + "contentHash": "ZnmYHFiQw2Aph2ft9c7UqVrqe79aZR5l7oeG59+sgrSAO9J159meglP1Zo34+Mcw4etIUTC9btYnP0Ar/rQIyg==" }, "StyleCop.Analyzers": { "type": "Direct", @@ -414,6 +388,12 @@ "resolved": "1.1.118", "contentHash": "Onx6ovGSqXSK07n/0eM3ZusiNdB6cIlJdabQhWGgJp3Vooy9AaLS/tigeybOJAobqbtggTamoWndz72JscZBvw==" }, + "xunit.analyzers": { + "type": "Direct", + "requested": "[1.27.0, )", + "resolved": "1.27.0", + "contentHash": "y/pxIQaLvk/kxAoDkZW9GnHLCEqzwl5TW0vtX3pweyQpjizB9y3DXhb9pkw2dGeUqhLjsxvvJM1k89JowU6z3g==" + }, "xunit.runner.visualstudio": { "type": "Direct", "requested": "[3.1.5, )", @@ -422,11 +402,11 @@ }, "xunit.v3": { "type": "Direct", - "requested": "[3.2.0, )", - "resolved": "3.2.0", - "contentHash": "ABdobDsBRs0JV+Ux6t0sM26noKFVbCkGOvU/x/YeShjI4aYH+thZeOGg8k5etju+5Ud4BZfzQEtVfvnAx8G2cg==", + "requested": "[3.2.2, )", + "resolved": "3.2.2", + "contentHash": "L+4/4y0Uqcg8/d6hfnxhnwh4j9FaeULvefTwrk30rr1o4n/vdPfyUQ8k0yzH8VJx7bmFEkDdcRfbtbjEHlaYcA==", "dependencies": { - "xunit.v3.mtp-v1": "[3.2.0]" + "xunit.v3.mtp-v1": "[3.2.2]" } }, "Castle.Core": { @@ -437,11 +417,6 @@ "System.Diagnostics.EventLog": "6.0.0" } }, - "Gulliver": { - "type": "Transitive", - "resolved": "3.0.0", - "contentHash": "J232PI4ccScfLKt59UzyHBRZLcAJpyJ+N/at1fFF/XxBKwjl8idDVPkMU3FZkwaaU/e3Ert8/EnGLg5CxmQ8dQ==" - }, "Microsoft.ApplicationInsights": { "type": "Transitive", "resolved": "2.23.0", @@ -454,50 +429,50 @@ }, "Microsoft.CodeCoverage": { "type": "Transitive", - "resolved": "18.0.1", - "contentHash": "O+utSr97NAJowIQT/OVp3Lh9QgW/wALVTP4RG1m2AfFP4IyJmJz0ZBmFJUsRQiAPgq6IRC0t8AAzsiPIsaUDEA==" + "resolved": "18.7.0", + "contentHash": "+wFfx9s7D9wegM0RziXMj2kvYDT4qcqXXtyjiQwSZOGQ2wwcOAJQcD6eQXk02jt0MvRNawtp8TJxTrV+wD8X1g==" }, "Microsoft.Testing.Extensions.Telemetry": { "type": "Transitive", - "resolved": "1.9.0", - "contentHash": "4ica7o+ZreUCsXoQLShWeLz6K5q9D/B+VgXvmpPSLsnSpK4DLZfMyltBaBr83Tnm21c1riOKcXXEk920Ael17Q==", + "resolved": "1.9.1", + "contentHash": "No5AudZMmSb+uNXjlgL2y3/stHD2IT4uxqc5yHwkE+/nNux9jbKcaJMvcp9SwgP4DVD8L9/P3OUz8mmmcvEIdQ==", "dependencies": { "Microsoft.ApplicationInsights": "2.23.0", - "Microsoft.Testing.Platform": "1.9.0" + "Microsoft.Testing.Platform": "1.9.1" } }, "Microsoft.Testing.Extensions.TrxReport.Abstractions": { "type": "Transitive", - "resolved": "1.9.0", - "contentHash": "ZC7Xwva/+dREnZ5BU0gozA1uT2cFwt90uY9maPLy1lLpiedR/Fu3K2FiwLg70hJjT+tGblYmxzuNa8z1jQVSGw==", + "resolved": "1.9.1", + "contentHash": "AL46Xe1WBi85Ntd4mNPvat5ZSsZ2uejiVqoKCypr8J3wK0elA5xJ3AN4G/Q4GIwzUFnggZoH/DBjnr9J18IO/g==", "dependencies": { - "Microsoft.Testing.Platform": "1.9.0" + "Microsoft.Testing.Platform": "1.9.1" } }, "Microsoft.Testing.Platform": { "type": "Transitive", - "resolved": "1.9.0", - "contentHash": "OE79Vc5rXwFYciAPY/mqv92XvdhK+pvCHdVHcS0bBpWwWQbnzI18FiSEEYY+lYpB0HHl0fDQgcCK3ZTYKGs8bA==" + "resolved": "1.9.1", + "contentHash": "QafNtNSmEI0zazdebnsIkDKmFtTSpmx/5PLOjURWwozcPb3tvRxzosQSL8xwYNM1iPhhKiBksXZyRSE2COisrA==" }, "Microsoft.Testing.Platform.MSBuild": { "type": "Transitive", - "resolved": "1.9.0", - "contentHash": "P7c0HfBJ/AqKyRfmaA+cyl8I9jxDEyvQRJvMUH5IZCjACTPQ9wXb0xdA0CmcxhBUtm20GxWvx2fq3fMxG11hDg==", + "resolved": "1.9.1", + "contentHash": "oTUtyR4X/s9ytuiNA29FGsNCCH0rNmY5Wdm14NCKLjTM1cT9edVSlA+rGS/mVmusPqcP0l/x9qOnMXg16v87RQ==", "dependencies": { - "Microsoft.Testing.Platform": "1.9.0" + "Microsoft.Testing.Platform": "1.9.1" } }, "Microsoft.TestPlatform.ObjectModel": { "type": "Transitive", - "resolved": "18.0.1", - "contentHash": "qT/mwMcLF9BieRkzOBPL2qCopl8hQu6A1P7JWAoj/FMu5i9vds/7cjbJ/LLtaiwWevWLAeD5v5wjQJ/l6jvhWQ==" + "resolved": "18.7.0", + "contentHash": "6rmgU4q3/WOpOPcncI0YW0Q/QpcQtwR2TTEXDR5+4TfSimPBAk6Z/BgKLeGgp1SOun0ROVUCCafXhRLwsHaPpA==" }, "Microsoft.TestPlatform.TestHost": { "type": "Transitive", - "resolved": "18.0.1", - "contentHash": "uDJKAEjFTaa2wHdWlfo6ektyoh+WD4/Eesrwb4FpBFKsLGehhACVnwwTI4qD3FrIlIEPlxdXg3SyrYRIcO+RRQ==", + "resolved": "18.7.0", + "contentHash": "kYwfmebCs8992zaxEDkvG7S+YEouTeKfYVKUFEkwh1W2dIoOaevBt80XSKVXCUFEhusjOIm1sFfHBnoJgygrRA==", "dependencies": { - "Microsoft.TestPlatform.ObjectModel": "18.0.1", + "Microsoft.TestPlatform.ObjectModel": "18.7.0", "Newtonsoft.Json": "13.0.3" } }, @@ -516,114 +491,88 @@ "resolved": "6.0.0", "contentHash": "lcyUiXTsETK2ALsZrX+nWuHSIQeazhqPphLfaRxzdGaG93+0kELqpgEHtwWOlQe7+jSFnKwaCAgL4kjeZCQJnw==" }, - "xunit.analyzers": { - "type": "Transitive", - "resolved": "1.25.0", - "contentHash": "J3wD5p2VVOg6TXeOOCrD+7Nl2H/LOZuK1GovDyPbFFnvv8katIULfBCj2sQLIPKT22Ds44j1aHjJle4AU7jCbA==" - }, "xunit.v3.assert": { "type": "Transitive", - "resolved": "3.2.0", - "contentHash": "AT3jVbptSazsf+aKQt14eyhw0NN9GWj6SaYifJfZ873r86NbPeTlxshjij9aOqmtrR+C1fHwqeneO4mMZKK7ag==" + "resolved": "3.2.2", + "contentHash": "BPciBghgEEaJN/JG00QfCYDfEfnLgQhfnYEy+j1izoeHVNYd5+3Wm8GJ6JgYysOhpBPYGE+sbf75JtrRc7jrdA==" }, "xunit.v3.common": { "type": "Transitive", - "resolved": "3.2.0", - "contentHash": "InLYIBD54RVFgX1S5y0g+Mia3coGsA+cf+SvieBH3kO5TrH3d4sb59+4M6nfBlGg8EqUCHbyGKKroQPquYEt7Q==", + "resolved": "3.2.2", + "contentHash": "Hj775PEH6GTbbg0wfKRvG2hNspDCvTH9irXhH4qIWgdrOSV1sQlqPie+DOvFeigsFg2fxSM3ZAaaCDQs+KreFA==", "dependencies": { "Microsoft.Bcl.AsyncInterfaces": "6.0.0" } }, "xunit.v3.core.mtp-v1": { "type": "Transitive", - "resolved": "3.2.0", - "contentHash": "0B+nZqJXl6BGTXT6kOf4LOnH5qxDvrufctNTjlxjXne4SqYybhCnfYpWRLy9NJ/0JkELyVlap8YK7HH+x/sHeQ==", + "resolved": "3.2.2", + "contentHash": "Ga5aA2Ca9ktz+5k3g5ukzwfexwoqwDUpV6z7atSEUvqtd6JuybU1XopHqg1oFd78QdTfZgZE9h5sHpO4qYIi5w==", "dependencies": { - "Microsoft.Testing.Extensions.Telemetry": "1.9.0", - "Microsoft.Testing.Extensions.TrxReport.Abstractions": "1.9.0", - "Microsoft.Testing.Platform": "1.9.0", - "Microsoft.Testing.Platform.MSBuild": "1.9.0", - "xunit.v3.extensibility.core": "[3.2.0]", - "xunit.v3.runner.inproc.console": "[3.2.0]" + "Microsoft.Testing.Extensions.Telemetry": "1.9.1", + "Microsoft.Testing.Extensions.TrxReport.Abstractions": "1.9.1", + "Microsoft.Testing.Platform": "1.9.1", + "Microsoft.Testing.Platform.MSBuild": "1.9.1", + "xunit.v3.extensibility.core": "[3.2.2]", + "xunit.v3.runner.inproc.console": "[3.2.2]" } }, "xunit.v3.extensibility.core": { "type": "Transitive", - "resolved": "3.2.0", - "contentHash": "89MWZ5gXT6VJ/qe9OCASBsaenFr00UCccFbkJxzkOsrAkoFjJSDJ+5xqXpZUxTBGJ4Gw91bJ4bwW9Fag0b8xhw==", + "resolved": "3.2.2", + "contentHash": "srY8z/oMPvh/t8axtO2DwrHajhFMH7tnqKildvYrVQIfICi8fOn3yIBWkVPAcrKmHMwvXRJ/XsQM3VMR6DOYfQ==", "dependencies": { - "xunit.v3.common": "[3.2.0]" + "xunit.v3.common": "[3.2.2]" } }, "xunit.v3.mtp-v1": { "type": "Transitive", - "resolved": "3.2.0", - "contentHash": "Rg5djTBTao7QUEK28EXeacjwC6LnU/NunlJzYrNkhtOgggpueYzGDfmyKu5hDbAEXhAl6vjrzfcCnhZyX8Q+DA==", + "resolved": "3.2.2", + "contentHash": "O41aAzYKBT5PWqATa1oEWVNCyEUypFQ4va6K0kz37dduV3EKzXNMaV2UnEhufzU4Cce1I33gg0oldS8tGL5I0A==", "dependencies": { - "xunit.analyzers": "1.25.0", - "xunit.v3.assert": "[3.2.0]", - "xunit.v3.core.mtp-v1": "[3.2.0]" + "xunit.analyzers": "1.27.0", + "xunit.v3.assert": "[3.2.2]", + "xunit.v3.core.mtp-v1": "[3.2.2]" } }, "xunit.v3.runner.common": { "type": "Transitive", - "resolved": "3.2.0", - "contentHash": "2i1/IvIH9x6M+8TRlr8k6/4NCem44zYzveps7p8XL4jjsIM/1K2ahfcvtom3tu9RQxHF3czT7f3DOAvHp+01gQ==", + "resolved": "3.2.2", + "contentHash": "/hkHkQCzGrugelOAehprm7RIWdsUFVmIVaD6jDH/8DNGCymTlKKPTbGokD5czbAfqfex47mBP0sb0zbHYwrO/g==", "dependencies": { "Microsoft.Win32.Registry": "[5.0.0]", - "xunit.v3.common": "[3.2.0]" + "xunit.v3.common": "[3.2.2]" } }, "xunit.v3.runner.inproc.console": { "type": "Transitive", - "resolved": "3.2.0", - "contentHash": "/nXHLSgKv5OYSjo3bTSzvTYfxbzApTEeip5T85PayDZgGbMHA679zeVhN/1n6//pW/uJ2j7QhDdHh2r98f13hQ==", + "resolved": "3.2.2", + "contentHash": "ulWOdSvCk+bPXijJZ73bth9NyoOHsAs1ZOvamYbCkD4DNLX/Bd29Ve2ZNUwBbK0MqfIYWXHZViy/HKrdEC/izw==", "dependencies": { - "xunit.v3.extensibility.core": "[3.2.0]", - "xunit.v3.runner.common": "[3.2.0]" + "xunit.v3.extensibility.core": "[3.2.2]", + "xunit.v3.runner.common": "[3.2.2]" } }, "arcus": { - "type": "Project", - "dependencies": { - "Gulliver": "[3.0.0, )" - } - } - }, - "net10.0/win-x86": { - "Microsoft.Win32.Registry": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==" - }, - "System.Diagnostics.EventLog": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "lcyUiXTsETK2ALsZrX+nWuHSIQeazhqPphLfaRxzdGaG93+0kELqpgEHtwWOlQe7+jSFnKwaCAgL4kjeZCQJnw==" + "type": "Project" } }, "net8.0": { - "AsyncFixer": { - "type": "Direct", - "requested": "[1.6.0, )", - "resolved": "1.6.0", - "contentHash": "/Xfs9H3UMfEv64cwT+C/JrTRp4w08BmPuFbj0ageadCHpx6rxYJxAU2C6sEqRFG22xmGk5cX9ewzoiiehWVHOw==" - }, "Microsoft.NET.Test.Sdk": { "type": "Direct", - "requested": "[18.0.1, )", - "resolved": "18.0.1", - "contentHash": "WNpu6vI2rA0pXY4r7NKxCN16XRWl5uHu6qjuyVLoDo6oYEggIQefrMjkRuibQHm/NslIUNCcKftvoWAN80MSAg==", + "requested": "[18.7.0, )", + "resolved": "18.7.0", + "contentHash": "49xH9j4UzCh2hMohJp53g3wUTvyycECw7CtVht4gfCz5ykudB1uBcF6D0TtgJPjCtP76UPW53bQElKdCeX+dUg==", "dependencies": { - "Microsoft.CodeCoverage": "18.0.1", - "Microsoft.TestPlatform.TestHost": "18.0.1" + "Microsoft.CodeCoverage": "18.7.0", + "Microsoft.TestPlatform.TestHost": "18.7.0" } }, "Microsoft.VisualStudio.Threading.Analyzers": { "type": "Direct", - "requested": "[17.14.15, )", - "resolved": "17.14.15", - "contentHash": "mXQPJsbuUD2ydq4/ffd8h8tSOFCXec+2xJOVNCvXjuMOq/+5EKHq3D2m2MC2+nUaXeFMSt66VS/J4HdKBixgcw==" + "requested": "[18.7.23, )", + "resolved": "18.7.23", + "contentHash": "dyj6z8m+LFjpeH69hdnciCcRCmD0YgMlvuQEywiEshjqj/blgQ+PuWX8Vd5gkWQFoBN4e+TFxx6DvGNSv/gIKg==" }, "NSubstitute": { "type": "Direct", @@ -642,27 +591,27 @@ }, "Roslynator.Analyzers": { "type": "Direct", - "requested": "[4.14.1, )", - "resolved": "4.14.1", - "contentHash": "yMSjze/xMYDF6PCE60/ULWx0tttNyKAndw2KijNxbKil0FX8nvDeEneDZGma8Uifk17RlfZqIXxf1mmBmhRHjg==" + "requested": "[4.15.0, )", + "resolved": "4.15.0", + "contentHash": "E8fu71Y+vzhniJKy1K13Rzwm8Vjcj7CDFBpflqncKUZfc3SzuF0kMBiyMmgXNPkjWTdE3Z+30HlGgYjHONZY/Q==" }, "Roslynator.CodeAnalysis.Analyzers": { "type": "Direct", - "requested": "[4.14.1, )", - "resolved": "4.14.1", - "contentHash": "axessewcgSLn+UAtKyMKPgXpzIldnivynjRe9qsRjrVtrxjctDpFDEjAkcbpDjhR0ccm8/+ihOna60wnXJ/66A==" + "requested": "[4.15.0, )", + "resolved": "4.15.0", + "contentHash": "XPY6vqHG8jLJ1S0SGzLDtyuDjmkcIAGuLRrsG3SlAuyAOlwnVfA1glgIy6YqXutoi7hwd1fqZC2W9zoMntuZNg==" }, "Roslynator.Formatting.Analyzers": { "type": "Direct", - "requested": "[4.14.1, )", - "resolved": "4.14.1", - "contentHash": "hALZsyELJXhj7WQw7Q7nLI7sELre8AlQIMyGxXpmpK+iBqShPcWHuJMGjIsTMa/bC8Q5xyQ4iFfRtm0bP1zisQ==" + "requested": "[4.15.0, )", + "resolved": "4.15.0", + "contentHash": "/LvSJGJbrx+OYC2GePmiAA9fRTfz37R/oaHC2j8YK/j9lE8ykZESCGTMVGqXF5woc6RbzfJlpYkzeG6fAV6hFQ==" }, "SonarAnalyzer.CSharp": { "type": "Direct", - "requested": "[10.15.0.120848, )", - "resolved": "10.15.0.120848", - "contentHash": "1hM3HVRl5jdC/ZBDu+G7CCYLXRGe/QaP01Zy+c9ETPhY7lWD8g8HiefY6sGaH0T3CJ4wAy0/waGgQTh0TYy0oQ==" + "requested": "[10.28.0.143324, )", + "resolved": "10.28.0.143324", + "contentHash": "ZnmYHFiQw2Aph2ft9c7UqVrqe79aZR5l7oeG59+sgrSAO9J159meglP1Zo34+Mcw4etIUTC9btYnP0Ar/rQIyg==" }, "StyleCop.Analyzers": { "type": "Direct", @@ -670,6 +619,12 @@ "resolved": "1.1.118", "contentHash": "Onx6ovGSqXSK07n/0eM3ZusiNdB6cIlJdabQhWGgJp3Vooy9AaLS/tigeybOJAobqbtggTamoWndz72JscZBvw==" }, + "xunit.analyzers": { + "type": "Direct", + "requested": "[1.27.0, )", + "resolved": "1.27.0", + "contentHash": "y/pxIQaLvk/kxAoDkZW9GnHLCEqzwl5TW0vtX3pweyQpjizB9y3DXhb9pkw2dGeUqhLjsxvvJM1k89JowU6z3g==" + }, "xunit.runner.visualstudio": { "type": "Direct", "requested": "[3.1.5, )", @@ -678,11 +633,11 @@ }, "xunit.v3": { "type": "Direct", - "requested": "[3.2.0, )", - "resolved": "3.2.0", - "contentHash": "ABdobDsBRs0JV+Ux6t0sM26noKFVbCkGOvU/x/YeShjI4aYH+thZeOGg8k5etju+5Ud4BZfzQEtVfvnAx8G2cg==", + "requested": "[3.2.2, )", + "resolved": "3.2.2", + "contentHash": "L+4/4y0Uqcg8/d6hfnxhnwh4j9FaeULvefTwrk30rr1o4n/vdPfyUQ8k0yzH8VJx7bmFEkDdcRfbtbjEHlaYcA==", "dependencies": { - "xunit.v3.mtp-v1": "[3.2.0]" + "xunit.v3.mtp-v1": "[3.2.2]" } }, "Castle.Core": { @@ -693,11 +648,6 @@ "System.Diagnostics.EventLog": "6.0.0" } }, - "Gulliver": { - "type": "Transitive", - "resolved": "3.0.0", - "contentHash": "J232PI4ccScfLKt59UzyHBRZLcAJpyJ+N/at1fFF/XxBKwjl8idDVPkMU3FZkwaaU/e3Ert8/EnGLg5CxmQ8dQ==" - }, "Microsoft.ApplicationInsights": { "type": "Transitive", "resolved": "2.23.0", @@ -710,50 +660,50 @@ }, "Microsoft.CodeCoverage": { "type": "Transitive", - "resolved": "18.0.1", - "contentHash": "O+utSr97NAJowIQT/OVp3Lh9QgW/wALVTP4RG1m2AfFP4IyJmJz0ZBmFJUsRQiAPgq6IRC0t8AAzsiPIsaUDEA==" + "resolved": "18.7.0", + "contentHash": "+wFfx9s7D9wegM0RziXMj2kvYDT4qcqXXtyjiQwSZOGQ2wwcOAJQcD6eQXk02jt0MvRNawtp8TJxTrV+wD8X1g==" }, "Microsoft.Testing.Extensions.Telemetry": { "type": "Transitive", - "resolved": "1.9.0", - "contentHash": "4ica7o+ZreUCsXoQLShWeLz6K5q9D/B+VgXvmpPSLsnSpK4DLZfMyltBaBr83Tnm21c1riOKcXXEk920Ael17Q==", + "resolved": "1.9.1", + "contentHash": "No5AudZMmSb+uNXjlgL2y3/stHD2IT4uxqc5yHwkE+/nNux9jbKcaJMvcp9SwgP4DVD8L9/P3OUz8mmmcvEIdQ==", "dependencies": { "Microsoft.ApplicationInsights": "2.23.0", - "Microsoft.Testing.Platform": "1.9.0" + "Microsoft.Testing.Platform": "1.9.1" } }, "Microsoft.Testing.Extensions.TrxReport.Abstractions": { "type": "Transitive", - "resolved": "1.9.0", - "contentHash": "ZC7Xwva/+dREnZ5BU0gozA1uT2cFwt90uY9maPLy1lLpiedR/Fu3K2FiwLg70hJjT+tGblYmxzuNa8z1jQVSGw==", + "resolved": "1.9.1", + "contentHash": "AL46Xe1WBi85Ntd4mNPvat5ZSsZ2uejiVqoKCypr8J3wK0elA5xJ3AN4G/Q4GIwzUFnggZoH/DBjnr9J18IO/g==", "dependencies": { - "Microsoft.Testing.Platform": "1.9.0" + "Microsoft.Testing.Platform": "1.9.1" } }, "Microsoft.Testing.Platform": { "type": "Transitive", - "resolved": "1.9.0", - "contentHash": "OE79Vc5rXwFYciAPY/mqv92XvdhK+pvCHdVHcS0bBpWwWQbnzI18FiSEEYY+lYpB0HHl0fDQgcCK3ZTYKGs8bA==" + "resolved": "1.9.1", + "contentHash": "QafNtNSmEI0zazdebnsIkDKmFtTSpmx/5PLOjURWwozcPb3tvRxzosQSL8xwYNM1iPhhKiBksXZyRSE2COisrA==" }, "Microsoft.Testing.Platform.MSBuild": { "type": "Transitive", - "resolved": "1.9.0", - "contentHash": "P7c0HfBJ/AqKyRfmaA+cyl8I9jxDEyvQRJvMUH5IZCjACTPQ9wXb0xdA0CmcxhBUtm20GxWvx2fq3fMxG11hDg==", + "resolved": "1.9.1", + "contentHash": "oTUtyR4X/s9ytuiNA29FGsNCCH0rNmY5Wdm14NCKLjTM1cT9edVSlA+rGS/mVmusPqcP0l/x9qOnMXg16v87RQ==", "dependencies": { - "Microsoft.Testing.Platform": "1.9.0" + "Microsoft.Testing.Platform": "1.9.1" } }, "Microsoft.TestPlatform.ObjectModel": { "type": "Transitive", - "resolved": "18.0.1", - "contentHash": "qT/mwMcLF9BieRkzOBPL2qCopl8hQu6A1P7JWAoj/FMu5i9vds/7cjbJ/LLtaiwWevWLAeD5v5wjQJ/l6jvhWQ==" + "resolved": "18.7.0", + "contentHash": "6rmgU4q3/WOpOPcncI0YW0Q/QpcQtwR2TTEXDR5+4TfSimPBAk6Z/BgKLeGgp1SOun0ROVUCCafXhRLwsHaPpA==" }, "Microsoft.TestPlatform.TestHost": { "type": "Transitive", - "resolved": "18.0.1", - "contentHash": "uDJKAEjFTaa2wHdWlfo6ektyoh+WD4/Eesrwb4FpBFKsLGehhACVnwwTI4qD3FrIlIEPlxdXg3SyrYRIcO+RRQ==", + "resolved": "18.7.0", + "contentHash": "kYwfmebCs8992zaxEDkvG7S+YEouTeKfYVKUFEkwh1W2dIoOaevBt80XSKVXCUFEhusjOIm1sFfHBnoJgygrRA==", "dependencies": { - "Microsoft.TestPlatform.ObjectModel": "18.0.1", + "Microsoft.TestPlatform.ObjectModel": "18.7.0", "Newtonsoft.Json": "13.0.3" } }, @@ -772,114 +722,88 @@ "resolved": "6.0.0", "contentHash": "lcyUiXTsETK2ALsZrX+nWuHSIQeazhqPphLfaRxzdGaG93+0kELqpgEHtwWOlQe7+jSFnKwaCAgL4kjeZCQJnw==" }, - "xunit.analyzers": { - "type": "Transitive", - "resolved": "1.25.0", - "contentHash": "J3wD5p2VVOg6TXeOOCrD+7Nl2H/LOZuK1GovDyPbFFnvv8katIULfBCj2sQLIPKT22Ds44j1aHjJle4AU7jCbA==" - }, "xunit.v3.assert": { "type": "Transitive", - "resolved": "3.2.0", - "contentHash": "AT3jVbptSazsf+aKQt14eyhw0NN9GWj6SaYifJfZ873r86NbPeTlxshjij9aOqmtrR+C1fHwqeneO4mMZKK7ag==" + "resolved": "3.2.2", + "contentHash": "BPciBghgEEaJN/JG00QfCYDfEfnLgQhfnYEy+j1izoeHVNYd5+3Wm8GJ6JgYysOhpBPYGE+sbf75JtrRc7jrdA==" }, "xunit.v3.common": { "type": "Transitive", - "resolved": "3.2.0", - "contentHash": "InLYIBD54RVFgX1S5y0g+Mia3coGsA+cf+SvieBH3kO5TrH3d4sb59+4M6nfBlGg8EqUCHbyGKKroQPquYEt7Q==", + "resolved": "3.2.2", + "contentHash": "Hj775PEH6GTbbg0wfKRvG2hNspDCvTH9irXhH4qIWgdrOSV1sQlqPie+DOvFeigsFg2fxSM3ZAaaCDQs+KreFA==", "dependencies": { "Microsoft.Bcl.AsyncInterfaces": "6.0.0" } }, "xunit.v3.core.mtp-v1": { "type": "Transitive", - "resolved": "3.2.0", - "contentHash": "0B+nZqJXl6BGTXT6kOf4LOnH5qxDvrufctNTjlxjXne4SqYybhCnfYpWRLy9NJ/0JkELyVlap8YK7HH+x/sHeQ==", + "resolved": "3.2.2", + "contentHash": "Ga5aA2Ca9ktz+5k3g5ukzwfexwoqwDUpV6z7atSEUvqtd6JuybU1XopHqg1oFd78QdTfZgZE9h5sHpO4qYIi5w==", "dependencies": { - "Microsoft.Testing.Extensions.Telemetry": "1.9.0", - "Microsoft.Testing.Extensions.TrxReport.Abstractions": "1.9.0", - "Microsoft.Testing.Platform": "1.9.0", - "Microsoft.Testing.Platform.MSBuild": "1.9.0", - "xunit.v3.extensibility.core": "[3.2.0]", - "xunit.v3.runner.inproc.console": "[3.2.0]" + "Microsoft.Testing.Extensions.Telemetry": "1.9.1", + "Microsoft.Testing.Extensions.TrxReport.Abstractions": "1.9.1", + "Microsoft.Testing.Platform": "1.9.1", + "Microsoft.Testing.Platform.MSBuild": "1.9.1", + "xunit.v3.extensibility.core": "[3.2.2]", + "xunit.v3.runner.inproc.console": "[3.2.2]" } }, "xunit.v3.extensibility.core": { "type": "Transitive", - "resolved": "3.2.0", - "contentHash": "89MWZ5gXT6VJ/qe9OCASBsaenFr00UCccFbkJxzkOsrAkoFjJSDJ+5xqXpZUxTBGJ4Gw91bJ4bwW9Fag0b8xhw==", + "resolved": "3.2.2", + "contentHash": "srY8z/oMPvh/t8axtO2DwrHajhFMH7tnqKildvYrVQIfICi8fOn3yIBWkVPAcrKmHMwvXRJ/XsQM3VMR6DOYfQ==", "dependencies": { - "xunit.v3.common": "[3.2.0]" + "xunit.v3.common": "[3.2.2]" } }, "xunit.v3.mtp-v1": { "type": "Transitive", - "resolved": "3.2.0", - "contentHash": "Rg5djTBTao7QUEK28EXeacjwC6LnU/NunlJzYrNkhtOgggpueYzGDfmyKu5hDbAEXhAl6vjrzfcCnhZyX8Q+DA==", + "resolved": "3.2.2", + "contentHash": "O41aAzYKBT5PWqATa1oEWVNCyEUypFQ4va6K0kz37dduV3EKzXNMaV2UnEhufzU4Cce1I33gg0oldS8tGL5I0A==", "dependencies": { - "xunit.analyzers": "1.25.0", - "xunit.v3.assert": "[3.2.0]", - "xunit.v3.core.mtp-v1": "[3.2.0]" + "xunit.analyzers": "1.27.0", + "xunit.v3.assert": "[3.2.2]", + "xunit.v3.core.mtp-v1": "[3.2.2]" } }, "xunit.v3.runner.common": { "type": "Transitive", - "resolved": "3.2.0", - "contentHash": "2i1/IvIH9x6M+8TRlr8k6/4NCem44zYzveps7p8XL4jjsIM/1K2ahfcvtom3tu9RQxHF3czT7f3DOAvHp+01gQ==", + "resolved": "3.2.2", + "contentHash": "/hkHkQCzGrugelOAehprm7RIWdsUFVmIVaD6jDH/8DNGCymTlKKPTbGokD5czbAfqfex47mBP0sb0zbHYwrO/g==", "dependencies": { "Microsoft.Win32.Registry": "[5.0.0]", - "xunit.v3.common": "[3.2.0]" + "xunit.v3.common": "[3.2.2]" } }, "xunit.v3.runner.inproc.console": { "type": "Transitive", - "resolved": "3.2.0", - "contentHash": "/nXHLSgKv5OYSjo3bTSzvTYfxbzApTEeip5T85PayDZgGbMHA679zeVhN/1n6//pW/uJ2j7QhDdHh2r98f13hQ==", + "resolved": "3.2.2", + "contentHash": "ulWOdSvCk+bPXijJZ73bth9NyoOHsAs1ZOvamYbCkD4DNLX/Bd29Ve2ZNUwBbK0MqfIYWXHZViy/HKrdEC/izw==", "dependencies": { - "xunit.v3.extensibility.core": "[3.2.0]", - "xunit.v3.runner.common": "[3.2.0]" + "xunit.v3.extensibility.core": "[3.2.2]", + "xunit.v3.runner.common": "[3.2.2]" } }, "arcus": { - "type": "Project", - "dependencies": { - "Gulliver": "[3.0.0, )" - } - } - }, - "net8.0/win-x86": { - "Microsoft.Win32.Registry": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==" - }, - "System.Diagnostics.EventLog": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "lcyUiXTsETK2ALsZrX+nWuHSIQeazhqPphLfaRxzdGaG93+0kELqpgEHtwWOlQe7+jSFnKwaCAgL4kjeZCQJnw==" + "type": "Project" } }, "net9.0": { - "AsyncFixer": { - "type": "Direct", - "requested": "[1.6.0, )", - "resolved": "1.6.0", - "contentHash": "/Xfs9H3UMfEv64cwT+C/JrTRp4w08BmPuFbj0ageadCHpx6rxYJxAU2C6sEqRFG22xmGk5cX9ewzoiiehWVHOw==" - }, "Microsoft.NET.Test.Sdk": { "type": "Direct", - "requested": "[18.0.1, )", - "resolved": "18.0.1", - "contentHash": "WNpu6vI2rA0pXY4r7NKxCN16XRWl5uHu6qjuyVLoDo6oYEggIQefrMjkRuibQHm/NslIUNCcKftvoWAN80MSAg==", + "requested": "[18.7.0, )", + "resolved": "18.7.0", + "contentHash": "49xH9j4UzCh2hMohJp53g3wUTvyycECw7CtVht4gfCz5ykudB1uBcF6D0TtgJPjCtP76UPW53bQElKdCeX+dUg==", "dependencies": { - "Microsoft.CodeCoverage": "18.0.1", - "Microsoft.TestPlatform.TestHost": "18.0.1" + "Microsoft.CodeCoverage": "18.7.0", + "Microsoft.TestPlatform.TestHost": "18.7.0" } }, "Microsoft.VisualStudio.Threading.Analyzers": { "type": "Direct", - "requested": "[17.14.15, )", - "resolved": "17.14.15", - "contentHash": "mXQPJsbuUD2ydq4/ffd8h8tSOFCXec+2xJOVNCvXjuMOq/+5EKHq3D2m2MC2+nUaXeFMSt66VS/J4HdKBixgcw==" + "requested": "[18.7.23, )", + "resolved": "18.7.23", + "contentHash": "dyj6z8m+LFjpeH69hdnciCcRCmD0YgMlvuQEywiEshjqj/blgQ+PuWX8Vd5gkWQFoBN4e+TFxx6DvGNSv/gIKg==" }, "NSubstitute": { "type": "Direct", @@ -898,27 +822,27 @@ }, "Roslynator.Analyzers": { "type": "Direct", - "requested": "[4.14.1, )", - "resolved": "4.14.1", - "contentHash": "yMSjze/xMYDF6PCE60/ULWx0tttNyKAndw2KijNxbKil0FX8nvDeEneDZGma8Uifk17RlfZqIXxf1mmBmhRHjg==" + "requested": "[4.15.0, )", + "resolved": "4.15.0", + "contentHash": "E8fu71Y+vzhniJKy1K13Rzwm8Vjcj7CDFBpflqncKUZfc3SzuF0kMBiyMmgXNPkjWTdE3Z+30HlGgYjHONZY/Q==" }, "Roslynator.CodeAnalysis.Analyzers": { "type": "Direct", - "requested": "[4.14.1, )", - "resolved": "4.14.1", - "contentHash": "axessewcgSLn+UAtKyMKPgXpzIldnivynjRe9qsRjrVtrxjctDpFDEjAkcbpDjhR0ccm8/+ihOna60wnXJ/66A==" + "requested": "[4.15.0, )", + "resolved": "4.15.0", + "contentHash": "XPY6vqHG8jLJ1S0SGzLDtyuDjmkcIAGuLRrsG3SlAuyAOlwnVfA1glgIy6YqXutoi7hwd1fqZC2W9zoMntuZNg==" }, "Roslynator.Formatting.Analyzers": { "type": "Direct", - "requested": "[4.14.1, )", - "resolved": "4.14.1", - "contentHash": "hALZsyELJXhj7WQw7Q7nLI7sELre8AlQIMyGxXpmpK+iBqShPcWHuJMGjIsTMa/bC8Q5xyQ4iFfRtm0bP1zisQ==" + "requested": "[4.15.0, )", + "resolved": "4.15.0", + "contentHash": "/LvSJGJbrx+OYC2GePmiAA9fRTfz37R/oaHC2j8YK/j9lE8ykZESCGTMVGqXF5woc6RbzfJlpYkzeG6fAV6hFQ==" }, "SonarAnalyzer.CSharp": { "type": "Direct", - "requested": "[10.15.0.120848, )", - "resolved": "10.15.0.120848", - "contentHash": "1hM3HVRl5jdC/ZBDu+G7CCYLXRGe/QaP01Zy+c9ETPhY7lWD8g8HiefY6sGaH0T3CJ4wAy0/waGgQTh0TYy0oQ==" + "requested": "[10.28.0.143324, )", + "resolved": "10.28.0.143324", + "contentHash": "ZnmYHFiQw2Aph2ft9c7UqVrqe79aZR5l7oeG59+sgrSAO9J159meglP1Zo34+Mcw4etIUTC9btYnP0Ar/rQIyg==" }, "StyleCop.Analyzers": { "type": "Direct", @@ -926,6 +850,12 @@ "resolved": "1.1.118", "contentHash": "Onx6ovGSqXSK07n/0eM3ZusiNdB6cIlJdabQhWGgJp3Vooy9AaLS/tigeybOJAobqbtggTamoWndz72JscZBvw==" }, + "xunit.analyzers": { + "type": "Direct", + "requested": "[1.27.0, )", + "resolved": "1.27.0", + "contentHash": "y/pxIQaLvk/kxAoDkZW9GnHLCEqzwl5TW0vtX3pweyQpjizB9y3DXhb9pkw2dGeUqhLjsxvvJM1k89JowU6z3g==" + }, "xunit.runner.visualstudio": { "type": "Direct", "requested": "[3.1.5, )", @@ -934,11 +864,11 @@ }, "xunit.v3": { "type": "Direct", - "requested": "[3.2.0, )", - "resolved": "3.2.0", - "contentHash": "ABdobDsBRs0JV+Ux6t0sM26noKFVbCkGOvU/x/YeShjI4aYH+thZeOGg8k5etju+5Ud4BZfzQEtVfvnAx8G2cg==", + "requested": "[3.2.2, )", + "resolved": "3.2.2", + "contentHash": "L+4/4y0Uqcg8/d6hfnxhnwh4j9FaeULvefTwrk30rr1o4n/vdPfyUQ8k0yzH8VJx7bmFEkDdcRfbtbjEHlaYcA==", "dependencies": { - "xunit.v3.mtp-v1": "[3.2.0]" + "xunit.v3.mtp-v1": "[3.2.2]" } }, "Castle.Core": { @@ -949,11 +879,6 @@ "System.Diagnostics.EventLog": "6.0.0" } }, - "Gulliver": { - "type": "Transitive", - "resolved": "3.0.0", - "contentHash": "J232PI4ccScfLKt59UzyHBRZLcAJpyJ+N/at1fFF/XxBKwjl8idDVPkMU3FZkwaaU/e3Ert8/EnGLg5CxmQ8dQ==" - }, "Microsoft.ApplicationInsights": { "type": "Transitive", "resolved": "2.23.0", @@ -966,50 +891,50 @@ }, "Microsoft.CodeCoverage": { "type": "Transitive", - "resolved": "18.0.1", - "contentHash": "O+utSr97NAJowIQT/OVp3Lh9QgW/wALVTP4RG1m2AfFP4IyJmJz0ZBmFJUsRQiAPgq6IRC0t8AAzsiPIsaUDEA==" + "resolved": "18.7.0", + "contentHash": "+wFfx9s7D9wegM0RziXMj2kvYDT4qcqXXtyjiQwSZOGQ2wwcOAJQcD6eQXk02jt0MvRNawtp8TJxTrV+wD8X1g==" }, "Microsoft.Testing.Extensions.Telemetry": { "type": "Transitive", - "resolved": "1.9.0", - "contentHash": "4ica7o+ZreUCsXoQLShWeLz6K5q9D/B+VgXvmpPSLsnSpK4DLZfMyltBaBr83Tnm21c1riOKcXXEk920Ael17Q==", + "resolved": "1.9.1", + "contentHash": "No5AudZMmSb+uNXjlgL2y3/stHD2IT4uxqc5yHwkE+/nNux9jbKcaJMvcp9SwgP4DVD8L9/P3OUz8mmmcvEIdQ==", "dependencies": { "Microsoft.ApplicationInsights": "2.23.0", - "Microsoft.Testing.Platform": "1.9.0" + "Microsoft.Testing.Platform": "1.9.1" } }, "Microsoft.Testing.Extensions.TrxReport.Abstractions": { "type": "Transitive", - "resolved": "1.9.0", - "contentHash": "ZC7Xwva/+dREnZ5BU0gozA1uT2cFwt90uY9maPLy1lLpiedR/Fu3K2FiwLg70hJjT+tGblYmxzuNa8z1jQVSGw==", + "resolved": "1.9.1", + "contentHash": "AL46Xe1WBi85Ntd4mNPvat5ZSsZ2uejiVqoKCypr8J3wK0elA5xJ3AN4G/Q4GIwzUFnggZoH/DBjnr9J18IO/g==", "dependencies": { - "Microsoft.Testing.Platform": "1.9.0" + "Microsoft.Testing.Platform": "1.9.1" } }, "Microsoft.Testing.Platform": { "type": "Transitive", - "resolved": "1.9.0", - "contentHash": "OE79Vc5rXwFYciAPY/mqv92XvdhK+pvCHdVHcS0bBpWwWQbnzI18FiSEEYY+lYpB0HHl0fDQgcCK3ZTYKGs8bA==" + "resolved": "1.9.1", + "contentHash": "QafNtNSmEI0zazdebnsIkDKmFtTSpmx/5PLOjURWwozcPb3tvRxzosQSL8xwYNM1iPhhKiBksXZyRSE2COisrA==" }, "Microsoft.Testing.Platform.MSBuild": { "type": "Transitive", - "resolved": "1.9.0", - "contentHash": "P7c0HfBJ/AqKyRfmaA+cyl8I9jxDEyvQRJvMUH5IZCjACTPQ9wXb0xdA0CmcxhBUtm20GxWvx2fq3fMxG11hDg==", + "resolved": "1.9.1", + "contentHash": "oTUtyR4X/s9ytuiNA29FGsNCCH0rNmY5Wdm14NCKLjTM1cT9edVSlA+rGS/mVmusPqcP0l/x9qOnMXg16v87RQ==", "dependencies": { - "Microsoft.Testing.Platform": "1.9.0" + "Microsoft.Testing.Platform": "1.9.1" } }, "Microsoft.TestPlatform.ObjectModel": { "type": "Transitive", - "resolved": "18.0.1", - "contentHash": "qT/mwMcLF9BieRkzOBPL2qCopl8hQu6A1P7JWAoj/FMu5i9vds/7cjbJ/LLtaiwWevWLAeD5v5wjQJ/l6jvhWQ==" + "resolved": "18.7.0", + "contentHash": "6rmgU4q3/WOpOPcncI0YW0Q/QpcQtwR2TTEXDR5+4TfSimPBAk6Z/BgKLeGgp1SOun0ROVUCCafXhRLwsHaPpA==" }, "Microsoft.TestPlatform.TestHost": { "type": "Transitive", - "resolved": "18.0.1", - "contentHash": "uDJKAEjFTaa2wHdWlfo6ektyoh+WD4/Eesrwb4FpBFKsLGehhACVnwwTI4qD3FrIlIEPlxdXg3SyrYRIcO+RRQ==", + "resolved": "18.7.0", + "contentHash": "kYwfmebCs8992zaxEDkvG7S+YEouTeKfYVKUFEkwh1W2dIoOaevBt80XSKVXCUFEhusjOIm1sFfHBnoJgygrRA==", "dependencies": { - "Microsoft.TestPlatform.ObjectModel": "18.0.1", + "Microsoft.TestPlatform.ObjectModel": "18.7.0", "Newtonsoft.Json": "13.0.3" } }, @@ -1028,90 +953,70 @@ "resolved": "6.0.0", "contentHash": "lcyUiXTsETK2ALsZrX+nWuHSIQeazhqPphLfaRxzdGaG93+0kELqpgEHtwWOlQe7+jSFnKwaCAgL4kjeZCQJnw==" }, - "xunit.analyzers": { - "type": "Transitive", - "resolved": "1.25.0", - "contentHash": "J3wD5p2VVOg6TXeOOCrD+7Nl2H/LOZuK1GovDyPbFFnvv8katIULfBCj2sQLIPKT22Ds44j1aHjJle4AU7jCbA==" - }, "xunit.v3.assert": { "type": "Transitive", - "resolved": "3.2.0", - "contentHash": "AT3jVbptSazsf+aKQt14eyhw0NN9GWj6SaYifJfZ873r86NbPeTlxshjij9aOqmtrR+C1fHwqeneO4mMZKK7ag==" + "resolved": "3.2.2", + "contentHash": "BPciBghgEEaJN/JG00QfCYDfEfnLgQhfnYEy+j1izoeHVNYd5+3Wm8GJ6JgYysOhpBPYGE+sbf75JtrRc7jrdA==" }, "xunit.v3.common": { "type": "Transitive", - "resolved": "3.2.0", - "contentHash": "InLYIBD54RVFgX1S5y0g+Mia3coGsA+cf+SvieBH3kO5TrH3d4sb59+4M6nfBlGg8EqUCHbyGKKroQPquYEt7Q==", + "resolved": "3.2.2", + "contentHash": "Hj775PEH6GTbbg0wfKRvG2hNspDCvTH9irXhH4qIWgdrOSV1sQlqPie+DOvFeigsFg2fxSM3ZAaaCDQs+KreFA==", "dependencies": { "Microsoft.Bcl.AsyncInterfaces": "6.0.0" } }, "xunit.v3.core.mtp-v1": { "type": "Transitive", - "resolved": "3.2.0", - "contentHash": "0B+nZqJXl6BGTXT6kOf4LOnH5qxDvrufctNTjlxjXne4SqYybhCnfYpWRLy9NJ/0JkELyVlap8YK7HH+x/sHeQ==", + "resolved": "3.2.2", + "contentHash": "Ga5aA2Ca9ktz+5k3g5ukzwfexwoqwDUpV6z7atSEUvqtd6JuybU1XopHqg1oFd78QdTfZgZE9h5sHpO4qYIi5w==", "dependencies": { - "Microsoft.Testing.Extensions.Telemetry": "1.9.0", - "Microsoft.Testing.Extensions.TrxReport.Abstractions": "1.9.0", - "Microsoft.Testing.Platform": "1.9.0", - "Microsoft.Testing.Platform.MSBuild": "1.9.0", - "xunit.v3.extensibility.core": "[3.2.0]", - "xunit.v3.runner.inproc.console": "[3.2.0]" + "Microsoft.Testing.Extensions.Telemetry": "1.9.1", + "Microsoft.Testing.Extensions.TrxReport.Abstractions": "1.9.1", + "Microsoft.Testing.Platform": "1.9.1", + "Microsoft.Testing.Platform.MSBuild": "1.9.1", + "xunit.v3.extensibility.core": "[3.2.2]", + "xunit.v3.runner.inproc.console": "[3.2.2]" } }, "xunit.v3.extensibility.core": { "type": "Transitive", - "resolved": "3.2.0", - "contentHash": "89MWZ5gXT6VJ/qe9OCASBsaenFr00UCccFbkJxzkOsrAkoFjJSDJ+5xqXpZUxTBGJ4Gw91bJ4bwW9Fag0b8xhw==", + "resolved": "3.2.2", + "contentHash": "srY8z/oMPvh/t8axtO2DwrHajhFMH7tnqKildvYrVQIfICi8fOn3yIBWkVPAcrKmHMwvXRJ/XsQM3VMR6DOYfQ==", "dependencies": { - "xunit.v3.common": "[3.2.0]" + "xunit.v3.common": "[3.2.2]" } }, "xunit.v3.mtp-v1": { "type": "Transitive", - "resolved": "3.2.0", - "contentHash": "Rg5djTBTao7QUEK28EXeacjwC6LnU/NunlJzYrNkhtOgggpueYzGDfmyKu5hDbAEXhAl6vjrzfcCnhZyX8Q+DA==", + "resolved": "3.2.2", + "contentHash": "O41aAzYKBT5PWqATa1oEWVNCyEUypFQ4va6K0kz37dduV3EKzXNMaV2UnEhufzU4Cce1I33gg0oldS8tGL5I0A==", "dependencies": { - "xunit.analyzers": "1.25.0", - "xunit.v3.assert": "[3.2.0]", - "xunit.v3.core.mtp-v1": "[3.2.0]" + "xunit.analyzers": "1.27.0", + "xunit.v3.assert": "[3.2.2]", + "xunit.v3.core.mtp-v1": "[3.2.2]" } }, "xunit.v3.runner.common": { "type": "Transitive", - "resolved": "3.2.0", - "contentHash": "2i1/IvIH9x6M+8TRlr8k6/4NCem44zYzveps7p8XL4jjsIM/1K2ahfcvtom3tu9RQxHF3czT7f3DOAvHp+01gQ==", + "resolved": "3.2.2", + "contentHash": "/hkHkQCzGrugelOAehprm7RIWdsUFVmIVaD6jDH/8DNGCymTlKKPTbGokD5czbAfqfex47mBP0sb0zbHYwrO/g==", "dependencies": { "Microsoft.Win32.Registry": "[5.0.0]", - "xunit.v3.common": "[3.2.0]" + "xunit.v3.common": "[3.2.2]" } }, "xunit.v3.runner.inproc.console": { "type": "Transitive", - "resolved": "3.2.0", - "contentHash": "/nXHLSgKv5OYSjo3bTSzvTYfxbzApTEeip5T85PayDZgGbMHA679zeVhN/1n6//pW/uJ2j7QhDdHh2r98f13hQ==", + "resolved": "3.2.2", + "contentHash": "ulWOdSvCk+bPXijJZ73bth9NyoOHsAs1ZOvamYbCkD4DNLX/Bd29Ve2ZNUwBbK0MqfIYWXHZViy/HKrdEC/izw==", "dependencies": { - "xunit.v3.extensibility.core": "[3.2.0]", - "xunit.v3.runner.common": "[3.2.0]" + "xunit.v3.extensibility.core": "[3.2.2]", + "xunit.v3.runner.common": "[3.2.2]" } }, "arcus": { - "type": "Project", - "dependencies": { - "Gulliver": "[3.0.0, )" - } - } - }, - "net9.0/win-x86": { - "Microsoft.Win32.Registry": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==" - }, - "System.Diagnostics.EventLog": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "lcyUiXTsETK2ALsZrX+nWuHSIQeazhqPphLfaRxzdGaG93+0kELqpgEHtwWOlQe7+jSFnKwaCAgL4kjeZCQJnw==" + "type": "Project" } } } diff --git a/src/Arcus.sln b/src/Arcus.sln deleted file mode 100644 index 586a232..0000000 --- a/src/Arcus.sln +++ /dev/null @@ -1,72 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.12.35707.178 d17.12 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Arcus", "Arcus\Arcus.csproj", "{BF292912-886E-470A-8FD0-2521C5EE586C}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Arcus.Tests", "Arcus.Tests\Arcus.Tests.csproj", "{067456C0-086C-46A8-B37F-1405717B7BFC}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionItems", "SolutionItems", "{B4AB2180-4640-4135-BBE4-B958D72C3103}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CodeQuality", "CodeQuality", "{4523123E-8A4A-443F-BC76-42FB24D96165}" - ProjectSection(SolutionItems) = preProject - .csharpierrc.json = .csharpierrc.json - .editorconfig = .editorconfig - ..\.editorconfig = ..\.editorconfig - ..\dictionary.dic = ..\dictionary.dic - stylecop.json = stylecop.json - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docs", "Docs", "{19C5939E-F1E4-4747-AE48-F72BB4C891C4}" - ProjectSection(SolutionItems) = preProject - ..\acknowledgements.md = ..\acknowledgements.md - ..\CODE_OF_CONDUCT.md = ..\CODE_OF_CONDUCT.md - ..\HUMANS.md = ..\HUMANS.md - ..\LICENSE = ..\LICENSE - ..\README.md = ..\README.md - ..\SECURITY.md = ..\SECURITY.md - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "git", "git", "{123A833E-759F-44C8-8B10-C453335445D9}" - ProjectSection(SolutionItems) = preProject - ..\.gitattributes = ..\.gitattributes - ..\.gitignore = ..\.gitignore - EndProjectSection -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Arcus.DocExamples", "Arcus.DocExamples\Arcus.DocExamples.csproj", "{3E2202A7-AECE-4DF2-AC8C-A500F4D53839}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DocExamples", "DocExamples", "{0A1760FE-A6FA-4F01-AA72-88674188F427}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {BF292912-886E-470A-8FD0-2521C5EE586C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BF292912-886E-470A-8FD0-2521C5EE586C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BF292912-886E-470A-8FD0-2521C5EE586C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BF292912-886E-470A-8FD0-2521C5EE586C}.Release|Any CPU.Build.0 = Release|Any CPU - {067456C0-086C-46A8-B37F-1405717B7BFC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {067456C0-086C-46A8-B37F-1405717B7BFC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {067456C0-086C-46A8-B37F-1405717B7BFC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {067456C0-086C-46A8-B37F-1405717B7BFC}.Release|Any CPU.Build.0 = Release|Any CPU - {3E2202A7-AECE-4DF2-AC8C-A500F4D53839}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3E2202A7-AECE-4DF2-AC8C-A500F4D53839}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3E2202A7-AECE-4DF2-AC8C-A500F4D53839}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3E2202A7-AECE-4DF2-AC8C-A500F4D53839}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {4523123E-8A4A-443F-BC76-42FB24D96165} = {B4AB2180-4640-4135-BBE4-B958D72C3103} - {19C5939E-F1E4-4747-AE48-F72BB4C891C4} = {B4AB2180-4640-4135-BBE4-B958D72C3103} - {123A833E-759F-44C8-8B10-C453335445D9} = {B4AB2180-4640-4135-BBE4-B958D72C3103} - {3E2202A7-AECE-4DF2-AC8C-A500F4D53839} = {0A1760FE-A6FA-4F01-AA72-88674188F427} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {9F4552D3-9938-429D-B4DA-A5BC2B77FA8C} - EndGlobalSection -EndGlobal diff --git a/src/Arcus/AbstractIPAddressRange.IEnumerable.cs b/src/Arcus/AbstractIPAddressRange.IEnumerable.cs new file mode 100644 index 0000000..f8abbcb --- /dev/null +++ b/src/Arcus/AbstractIPAddressRange.IEnumerable.cs @@ -0,0 +1,100 @@ +using System.Collections; +using System.Net; +using System.Numerics; + +namespace Arcus +{ + /// + /// implementation of + /// + public abstract partial class AbstractIPAddressRange + { + private BigInteger? _maxCount; + + #region IEnumerable / IEnumerable + + /// + public IEnumerable ToIPAddresses() + { + _maxCount ??= BigInteger.One << this.MaxEnumerationExponent; + return EnumerateCore(_maxCount.Value); + } + +/// + /// + /// + /// Marked to steer callers toward + /// , which is non-breaking and respects + /// . Direct + /// foreach on range types remains supported via this method for + /// back-compat but is discouraged for large ranges. + /// + /// + #pragma warning disable S1133 // Do not forget to remove this deprecated code someday + [Obsolete("Use ToIPAddresses() instead")] + #pragma warning restore S1133 + public IEnumerator GetEnumerator() + { + return ToIPAddresses().GetEnumerator(); + } + + /// +#pragma warning disable CS0618 // Type or member is obsolete + IEnumerator IEnumerable.GetEnumerator() + { + return this.GetEnumerator(); + } +#pragma warning restore CS0618 + + private IEnumerable EnumerateCore(BigInteger maxCount) + { + var limitWrap = BigEndianBitWrapper.FromBytes(this.Tail.GetAddressBytes()); + + var current = BigEndianBitWrapper.FromBytes(this.Head.GetAddressBytes()); + +#if NET8_0_OR_GREATER + var buffer = new byte[current.ByteWidth]; +#endif + + BigInteger? bigCount = null; + long longCount = 0; + var useLong = maxCount <= long.MaxValue; + + while (current.CompareTo(limitWrap) <= 0) + { + if (useLong ? longCount >= (long)maxCount : bigCount >= maxCount) + { + throw new InvalidOperationException( + $"Enumeration limit of {maxCount} addresses (2^{this.MaxEnumerationExponent}) reached. " + + $"The range contains {this.Length} addresses, which exceeds this limit. " + + "Construct with a larger maxEnumerationExponent (0-128) to enumerate more." + ); + } + +#if NET8_0_OR_GREATER + current.ToBytes(buffer); + yield return new IPAddress(buffer); +#else + yield return new IPAddress(current.ToBytes()); +#endif + + if (!current.TryAdd(1, out var next)) + { + break; + } + + current = next; + if (useLong) + { + longCount++; + } + else + { + bigCount = (bigCount ?? BigInteger.Zero) + 1; + } + } + } + + #endregion // end: IEnumerable + } +} diff --git a/src/Arcus/AbstractIPAddressRange.IFormattable.cs b/src/Arcus/AbstractIPAddressRange.IFormattable.cs new file mode 100644 index 0000000..b153470 --- /dev/null +++ b/src/Arcus/AbstractIPAddressRange.IFormattable.cs @@ -0,0 +1,41 @@ +using System.Globalization; +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif + +namespace Arcus +{ + /// + /// implementation of + /// + public abstract partial class AbstractIPAddressRange + { + #region Formatting + + /// + public override string ToString() + { + return this.ToString("G", CultureInfo.InvariantCulture); + } + + /// + public virtual string ToString( +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER + [AllowNull] +#endif + string format, +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER + [AllowNull] +#endif + IFormatProvider formatProvider) + { + return format?.Trim() switch + { + null or { Length: 0 } or "g" or "G" => $"{this.Head} - {this.Tail}", + _ => throw new FormatException($"The format \"{format}\" is not supported."), + }; + } + + #endregion // end: Formatting + } +} diff --git a/src/Arcus/AbstractIPAddressRange.IIPAddressRange.cs b/src/Arcus/AbstractIPAddressRange.IIPAddressRange.cs new file mode 100644 index 0000000..4c33d05 --- /dev/null +++ b/src/Arcus/AbstractIPAddressRange.IIPAddressRange.cs @@ -0,0 +1,233 @@ +using System.Net; +using Arcus.Math; +using Arcus.Utilities; + +namespace Arcus +{ + /// + /// implementation of + /// + public abstract partial class AbstractIPAddressRange + { + #region Deconstructors + + /// + public void Deconstruct(out IPAddress head, out IPAddress tail) + { + head = this.Head; + tail = this.Tail; + } + + #endregion // end: Deconstruct + + #region TryGetLength + + /// + public bool TryGetLength(out int length) + { + var actualLength = this.Length; + + if (actualLength <= int.MaxValue) + { + length = (int)actualLength; + return true; + } + + length = -1; + return false; + } + + /// + public bool TryGetLength(out long length) + { + var actualLength = this.Length; + + if (actualLength <= long.MaxValue) + { + length = (long)actualLength; + return true; + } + + length = -1; + return false; + } + + #endregion // end: TryGetLength + + #region Set Operations + + #region Contains + + /// + public bool Contains(IIPAddressRange addressRange) + { + return ReferenceEquals(this, addressRange) + || Equals(this, addressRange) + || (addressRange != null && this.Contains(addressRange.Head) && this.Contains(addressRange.Tail)); + } + + /// + public bool Contains(IPAddress address) + { + return address != null && address.AddressFamily == this.AddressFamily && address.IsBetween(this.Head, this.Tail); + } + + #endregion // end: Contains + + #region Overlap and Touches + + /// + public bool HeadOverlappedBy(IIPAddressRange addressRange) + { + return ReferenceEquals(this, addressRange) + || Equals(this, addressRange) + || (addressRange?.Contains(this.Head) == true); + } + + /// + public bool TailOverlappedBy(IIPAddressRange addressRange) + { + return ReferenceEquals(this, addressRange) + || Equals(this, addressRange) + || (addressRange?.Contains(this.Tail) == true); + } + + /// + /// + /// + /// This method calls addressRange.Contains(this) on the provided range. + /// Implementations that call within their Contains may + /// cause mutual recursion. + /// + /// + public bool Overlaps(IIPAddressRange addressRange) + { + return ReferenceEquals(this, addressRange) + || Equals(this, addressRange) + || ( + addressRange != null + && (this.Contains(addressRange.Head) || this.Contains(addressRange.Tail) || addressRange.Contains(this)) + ); + } + + /// + public bool Touches(IIPAddressRange addressRange) + { + return addressRange != null + && this.AddressFamily == addressRange.AddressFamily + && ( + ( + this.Tail.IsLessThan(this.Tail.AddressFamily.MaxIPAddress()) // prevent overflow + && this.Tail.Increment().Equals(addressRange.Head) + ) // this tail appears directly before that head + || ( + addressRange.Tail.IsLessThan(addressRange.Tail.AddressFamily.MaxIPAddress()) // prevent overflow + && addressRange.Tail.Increment().Equals(this.Head) + ) + ); // that tail appears directly before this head + } + #endregion // end: Overlap and Touches + + #endregion // end: Set Operations + + #region Contains Any/All Public/Private Addresses + + /// + /// + /// + /// Private address blocks are defined in + /// RFC 1918 §3. + /// Uses range-overlap detection: returns if the interval + /// [Head, Tail] intersects the interval [subnet.Head, subnet.Tail] for + /// any entry in . + /// This correctly handles ranges whose endpoints are both outside a private block but + /// whose interior spans it (e.g., 11.0.0.0 - 173.0.0.0 spans 172.16.0.0/12). + /// + /// + public bool ContainsAnyPrivateAddresses() + { + return AbstractIPAddressRange.AnyPrivateSubnet(OverlapsPrivateSubnet); + } + + /// + /// + /// + /// Private address blocks are defined in + /// RFC 1918 §3. + /// Returns if the entire interval [Head, Tail] is + /// contained within a single entry in . + /// Because private subnets are disjoint, a range that spans two private blocks necessarily + /// includes a public gap and therefore returns . + /// + /// + public bool ContainsAllPrivateAddresses() + { + return AbstractIPAddressRange.AnyPrivateSubnet(ContainsPrivateSubnet); + } + + /// + /// + /// + /// For example, 192.168.0.1 - 192.168.255.255 returns + /// (wholly inside a single private block), while 10.0.0.0 - 11.0.0.0 returns + /// (the range spans two private blocks with a public gap). + /// + /// + /// Public addresses are those not defined as private in + /// RFC 1918 §3. + /// Returns if the interval [Head, Tail] is not entirely + /// contained within any single entry in . + /// Because private subnets are disjoint, any range spanning two private blocks includes a + /// public gap, so this method returns in that case. + /// + /// + /// The private subnet ranges (RFC 1918 IPv4, RFC 4193 IPv6) are non-overlapping. + /// This method assumes containment by any single private subnet implies the + /// entire range is private. + /// + /// + public bool ContainsAnyPublicAddresses() + { + return !AbstractIPAddressRange.AnyPrivateSubnet(subnet => subnet.Contains(this)); + } + + /// + /// + /// + /// Public addresses are those not defined as private in + /// RFC 1918 §3. + /// Uses range-overlap detection: returns if the interval + /// [Head, Tail] does not intersect any entry in + /// . + /// This correctly handles ranges whose endpoints are both public but whose interior spans + /// a private block - such a range returns . + /// + /// + public bool ContainsAllPublicAddresses() + { + return AbstractIPAddressRange.AllPrivateSubnets(subnet => !OverlapsPrivateSubnet(subnet)); + } + + private static bool AnyPrivateSubnet(Func predicate) + { + return SubnetUtilities.PrivateIPAddressRangesList.Any(predicate); + } + + private static bool AllPrivateSubnets(Func predicate) + { + return SubnetUtilities.PrivateIPAddressRangesList.All(predicate); + } + + private bool OverlapsPrivateSubnet(Subnet subnet) + { + return this.Head.IsLessThanOrEqualTo(subnet.Tail) && this.Tail.IsGreaterThanOrEqualTo(subnet.Head); + } + + private bool ContainsPrivateSubnet(Subnet subnet) + { + return subnet.Head.IsLessThanOrEqualTo(this.Head) && subnet.Tail.IsGreaterThanOrEqualTo(this.Tail); + } + + #endregion // end: Contains Any/All Public/Private Addresses + } +} diff --git a/src/Arcus/AbstractIPAddressRange.cs b/src/Arcus/AbstractIPAddressRange.cs index b9d99c9..8c417f3 100644 --- a/src/Arcus/AbstractIPAddressRange.cs +++ b/src/Arcus/AbstractIPAddressRange.cs @@ -1,28 +1,51 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Net; +using System.Net; using System.Net.Sockets; using System.Numerics; using Arcus.Math; using Arcus.Utilities; -using Gulliver; namespace Arcus { /// /// An implementation of built to work with IPv4 and IPv6 /// - public abstract class AbstractIPAddressRange : IIPAddressRange + /// + /// + /// IPv4 addresses are represented as 32-bit unsigned integers per + /// RFC 791 §2.3. + /// IPv6 addresses are represented as 128-bit unsigned integers per + /// RFC 4291 §2; + /// is used throughout because IPv6 ranges can exceed . + /// + /// + /// This class is to enforce a concrete subtype + /// ( or ) for instantiation. + /// Although it has no members, the abstract modifier + /// prevents direct construction, ensures each instance carries a meaningful + /// concrete type for equality, comparison, and serialization, and reserves + /// the contract for the two supported implementations. + /// + /// + public abstract partial class AbstractIPAddressRange : IIPAddressRange { /// - /// Gets a value indicating whether if the subnet describes a single ip address + /// The default maximum enumeration exponent (12), limiting enumeration to 4096 addresses. /// - /// - /// if the subnet describes a single ip address - /// + internal const int DefaultMaxEnumerationExponent = 12; + + /// + /// Gets the maximum enumeration exponent for this range. Enumeration via (and foreach / + /// for backwards compatibility) yields at most + /// 2MaxEnumerationExponent addresses. Defaults to + /// (212 = 4096) to prevent accidental enumeration of enormous address spaces. + /// + /// The maximum enumeration exponent (0-128). + public int MaxEnumerationExponent { get; } + + /// + /// Gets a value indicating whether this range contains exactly one address. + /// + /// when equals 1. public bool IsSingleIP => this.Length == 1; /// @@ -41,93 +64,67 @@ public abstract class AbstractIPAddressRange : IIPAddressRange public IPAddress Tail { get; } /// + /// + /// + /// is computed once in the constructor and cached by the readonly + /// backing field; subsequent reads are O(1) and allocation-free. + /// + /// public BigInteger Length { get; } - #region From Interface IIPAddressRange - - #region Deconstructors - - /// - public void Deconstruct(out IPAddress head, out IPAddress tail) - { - head = this.Head; - tail = this.Tail; - } - - #endregion // end: Deconstruct - - #endregion - #region AddressTuple /// - /// AddressTuple for moving around a pair of objects as a unit + /// Holds a pair of objects as an immutable unit. /// - protected readonly struct AddressTuple : IEquatable + /// The head address (must not be ). + /// The tail address (must not be ). + private protected readonly struct AddressTuple(IPAddress head, IPAddress tail) : IEquatable { /// - /// Initializes a new instance of the struct. + /// Gets the head address of the pair. /// - /// the head address - /// the tail address - public AddressTuple(IPAddress head, IPAddress tail) - { - this.Head = head ?? throw new ArgumentNullException(nameof(head)); - this.Tail = tail ?? throw new ArgumentNullException(nameof(tail)); - } + /// The first (numerically lower) address. + public IPAddress Head { get; } = head ?? throw new ArgumentNullException(nameof(head)); /// - /// Gets head + /// Gets the tail address of the pair. /// - /// - /// Head - /// - public IPAddress Head { get; } - - /// - /// Gets tail - /// - /// - /// Tail - /// - public IPAddress Tail { get; } + /// The second (numerically higher) address. + public IPAddress Tail { get; } = tail ?? throw new ArgumentNullException(nameof(tail)); /// public bool Equals(AddressTuple other) { - return this.Head.Equals(other.Head) && this.Tail.Equals(other.Tail); - } + if (other.Head is null || other.Tail is null) + { + return false; + } - /// - public override bool Equals(object obj) - { - return obj is AddressTuple other && this.Equals(other); + return this.Head.Equals(other.Head) && this.Tail.Equals(other.Tail); } /// - public override int GetHashCode() => HashCode.Combine(Head, Tail); + public override bool Equals(object obj) => obj is AddressTuple other && this.Equals(other); /// - /// Equals operation + /// Determines whether two instances are equal. /// - /// left operand - /// right operand - /// if and are equal - public static bool operator ==(AddressTuple left, AddressTuple right) - { - return left.Equals(right); - } + /// The first instance. + /// The second instance. + /// if the two pairs contain equal addresses. + public static bool operator ==(AddressTuple left, AddressTuple right) => left.Equals(right); /// - /// Not Equals operation + /// Determines whether two instances are not equal. /// - /// left operand - /// right operand - /// if and are equal - public static bool operator !=(AddressTuple left, AddressTuple right) - { - return !(left == right); - } + /// The first instance. + /// The second instance. + /// if the two pairs differ in either address. + public static bool operator !=(AddressTuple left, AddressTuple right) => !(left == right); + + /// + public override int GetHashCode() => HashCode.Combine(this.Head ?? IPAddress.None, this.Tail ?? IPAddress.None); } #endregion // end: AddressTuple @@ -139,20 +136,34 @@ public override bool Equals(object obj) /// /// the range head (lowest valued ) /// the range tail (highest valued ) - protected AbstractIPAddressRange(IPAddress head, IPAddress tail) + /// the maximum enumeration exponent (0-128); enumeration yields at most 2maxEnumerationExponent addresses + protected AbstractIPAddressRange( + IPAddress head, + IPAddress tail, + int maxEnumerationExponent = DefaultMaxEnumerationExponent + ) { - #region defense - - if (head == null) + if (head is null) { throw new ArgumentNullException(nameof(head)); } - if (tail == null) + if (tail is null) { throw new ArgumentNullException(nameof(tail)); } + if (maxEnumerationExponent is < 0 or > 128) + { + throw new ArgumentOutOfRangeException(nameof(maxEnumerationExponent)); + } + + var maxExponentForFamily = head.AddressFamily == AddressFamily.InterNetwork ? 32 : 128; + this.MaxEnumerationExponent = + maxEnumerationExponent < maxExponentForFamily ? maxEnumerationExponent : maxExponentForFamily; + + #region defense + if (!IPAddressUtilities.ValidAddressFamilies.Contains(head.AddressFamily)) { throw new ArgumentException( @@ -187,17 +198,9 @@ protected AbstractIPAddressRange(IPAddress head, IPAddress tail) BigInteger CalculateLength() { - // convert big endian (network byte order) difference result to a 0x00 prefixed byte array for converting to an unsigned BigInteger - var differenceBytes = ByteArrayUtils.SubtractUnsignedBigEndian(tail.GetAddressBytes(), head.GetAddressBytes()); - var differenceBytesLength = differenceBytes.Length; - var unsignedLittleEndianBytes = new byte[differenceBytesLength + 1]; // one greater so trailing byte is always 0x00 - - for (var i = 0; i < differenceBytesLength; i++) - { - unsignedLittleEndianBytes[differenceBytesLength - 1 - i] = differenceBytes[i]; - } - - return new BigInteger(unsignedLittleEndianBytes) + 1; // a rare but valid use of BigInteger + var tailWrap = BigEndianBitWrapper.FromBytes(tail.GetAddressBytes()); + var headWrap = BigEndianBitWrapper.FromBytes(head.GetAddressBytes()); + return tailWrap.Subtract(headWrap).ToBigInteger() + 1; } } @@ -205,231 +208,53 @@ BigInteger CalculateLength() /// Initializes a new instance of the class. /// /// an representing two addresses - protected AbstractIPAddressRange(AddressTuple addressTuple) - : this(addressTuple.Head, addressTuple.Tail) + /// the maximum enumeration exponent (0-128) + private protected AbstractIPAddressRange( + AddressTuple addressTuple, + int maxEnumerationExponent = DefaultMaxEnumerationExponent + ) + : this(addressTuple.Head, addressTuple.Tail, maxEnumerationExponent) { // nothing additional to do } #endregion // end: Ctor - #region TryGetLength - - /// - public bool TryGetLength(out int length) - { - var actualLength = this.Length; - - if (actualLength <= int.MaxValue) - { - length = (int)actualLength; - return true; - } - - length = -1; - return false; - } + #region Equals / GetHashCode - /// - public bool TryGetLength(out long length) + /// + /// Determines whether the specified object is equal to the current range by comparing + /// and . Two ranges of different concrete types + /// (e.g. a and an ) with identical + /// and are considered equal by this base implementation. + /// + /// + /// + /// Subclasses (, ) override this method + /// to enforce type-specific equality; the shared algorithm lives here so that the + /// contract is uniform across implementations. + /// + /// + /// The object to compare. + /// if is an with matching Head and Tail. + public override bool Equals(object obj) { - var actualLength = this.Length; - - if (actualLength <= long.MaxValue) + if (obj is IIPAddressRange other) { - length = (long)actualLength; - return true; + return this.Head.Equals(other.Head) && this.Tail.Equals(other.Tail); } - length = -1; return false; } - #endregion // end: TryGetLength - - #region IEnumerable / IEnumerable - - /// - public IEnumerator GetEnumerator() - { - // determine maximum possible address for iteration - var addressLimit = IPAddressMath - .Min(this.Tail, this.IsIPv4 ? IPAddressUtilities.IPv4MaxAddress : IPAddressUtilities.IPv6MaxAddress) - .GetAddressBytes(); - - // determine the width of the bye array for the address address - var addressByteWidth = this.IsIPv4 ? IPAddressUtilities.IPv4ByteCount : IPAddressUtilities.IPv6ByteCount; - - var currentAddressBytes = this.Head.GetAddressBytes(); - - // iterate appropriately as long as the current address isn't beyond the limit - while (ByteArrayUtils.CompareUnsignedBigEndian(currentAddressBytes, addressLimit) <= 0) - { - yield return new IPAddress(currentAddressBytes); - - // determine next address - if (!ByteArrayUtils.TrySumBigEndian(currentAddressBytes, 1, out var nextAddressBytes)) - { - break; - } - - var nextAddressByteWidth = nextAddressBytes.Length; - if (nextAddressByteWidth > addressByteWidth) - { - break; - } - - // copy appropriate portion of next address with prefixed 0x00 bytes - currentAddressBytes = new byte[addressByteWidth]; - Array.Copy( - nextAddressBytes, - 0, - currentAddressBytes, - addressByteWidth - nextAddressByteWidth, - nextAddressByteWidth - ); - } - } - - /// - IEnumerator IEnumerable.GetEnumerator() - { - return this.GetEnumerator(); - } - - #endregion // end: IEnumerable - - #region Formatting - - /// - public override string ToString() - { - return this.ToString("G", CultureInfo.InvariantCulture); - } - - /// - public virtual string ToString(string format, IFormatProvider formatProvider) - { - switch (format?.Trim()) - { - case null: - case "": - - // general formats - case "g": - case "G": - return $"{this.Head} - {this.Tail}"; - default: - throw new FormatException($"The format \"{format}\" is not supported."); - } - } - - #endregion // end: Formatting - - #region Set Operations - - #region Contains - - /// - public bool Contains(IIPAddressRange addressRange) - { - return ReferenceEquals(this, addressRange) - || Equals(this, addressRange) - || (addressRange != null && this.Contains(addressRange.Head) && this.Contains(addressRange.Tail)); - } - - /// - public bool Contains(IPAddress address) - { - return address != null && address.AddressFamily == this.AddressFamily && address.IsBetween(this.Head, this.Tail); - } - - #endregion // end: Contains - - #region Ovelap and Touches - - /// - public bool HeadOverlappedBy(IIPAddressRange addressRange) - { - return ReferenceEquals(this, addressRange) - || Equals(this, addressRange) - || (addressRange != null && addressRange.Contains(this.Head)); - } - - /// - public bool TailOverlappedBy(IIPAddressRange addressRange) - { - return ReferenceEquals(this, addressRange) - || Equals(this, addressRange) - || (addressRange != null && addressRange.Contains(this.Tail)); - } - - /// - public bool Overlaps(IIPAddressRange addressRange) - { - return ReferenceEquals(this, addressRange) - || Equals(this, addressRange) - || ( - addressRange != null - && (this.Contains(addressRange) || this.Contains(addressRange.Head) || this.Contains(addressRange.Tail)) - ); - } - - /// - public bool Touches(IIPAddressRange addressRange) - { - return addressRange != null - && this.AddressFamily == addressRange.AddressFamily - && ( - ( - this.Tail.IsLessThan(this.Tail.AddressFamily.MaxIPAddress()) // prevent overflow - && Equals(this.Tail.Increment(), addressRange.Head) - ) // this tail appears directly before that head - || ( - addressRange.Tail.IsLessThan(addressRange.Tail.AddressFamily.MaxIPAddress()) // prevent overflow - && Equals(addressRange.Tail.Increment(), this.Head) - ) - ); // that tail appears directly before this head - } - - #endregion // end: Ovelap and Touches - - #endregion // end: Set Operations - - #region Contains Any/All Public/Private Addresses - - /// - public bool ContainsAnyPrivateAddresses() - { - return SubnetUtilities.PrivateIPAddressRangesList.Any(subnet => - subnet.Contains(this.Head) || subnet.Contains(this.Tail) - ); - } - - /// - public bool ContainsAllPrivateAddresses() - { - return SubnetUtilities.PrivateIPAddressRangesList.Any(subnet => - subnet.Contains(this.Head) && subnet.Contains(this.Tail) - ); - } - - /// - public bool ContainsAnyPublicAddresses() - { - return SubnetUtilities.PrivateIPAddressRangesList.All(subnet => - !subnet.Contains(this.Head) || !subnet.Contains(this.Tail) - ); - } - - /// - public bool ContainsAllPublicAddresses() - { - return SubnetUtilities.PrivateIPAddressRangesList.All(subnet => - !subnet.Contains(this.Head) && !subnet.Contains(this.Tail) - ); - } + /// + /// Returns a hash code based on and . All + /// subclasses use this same function so that + /// value-equal ranges hash identically regardless of concrete type. + /// + /// A hash code for the current range. + public override int GetHashCode() => HashCode.Combine(this.Head, this.Tail); - #endregion end: Contains Any/All Public/Private Addresses + #endregion // end: Equals / GetHashCode } } diff --git a/src/Arcus/Arcus.csproj b/src/Arcus/Arcus.csproj index 1a88e45..4dabd36 100644 --- a/src/Arcus/Arcus.csproj +++ b/src/Arcus/Arcus.csproj @@ -1,8 +1,9 @@ - + netstandard2.0;net8.0;net9.0;net10.0 + annotations true - bin\ + true $(OutputPath)$(AssemblyName).xml @@ -23,7 +24,6 @@ Arcus Arcus Arcus is a C# manipulation library for calculating, parsing, formatting, converting, and comparing both IPv4 and Pv6 addresses and subnets. It accounts for 128-bit numbers on 32-bit platforms. - true en en-US Apache-2.0 @@ -43,59 +43,116 @@ snupkg - + stylecop.json - - + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - + runtime; build; native; contentfiles; analyzers; buildtransitive all + - + + - + + + + + BigEndianBitWrapper.cs + + + BigEndianBitWrapper.cs + + + BigEndianBitWrapper.cs + + + BigEndianBitWrapper.cs + + + BigEndianBitWrapper.cs + + + IPAddressRange.cs + + + IPAddressRange.cs + + + IPAddressRange.cs + + + IPAddressRange.cs + + + IPAddressRange.cs + + + AbstractIPAddressRange.cs + + + AbstractIPAddressRange.cs + + + AbstractIPAddressRange.cs + + + Subnet.cs + + + Subnet.cs + + + Subnet.cs + + + Subnet.cs + + + Subnet.cs + - + diff --git a/src/Arcus/BigEndianBitWrapper.Factory.cs b/src/Arcus/BigEndianBitWrapper.Factory.cs new file mode 100644 index 0000000..2abf9fd --- /dev/null +++ b/src/Arcus/BigEndianBitWrapper.Factory.cs @@ -0,0 +1,250 @@ +namespace Arcus +{ + /// static factory methods + internal readonly partial struct BigEndianBitWrapper + { + /// + /// Creates a from a big-endian byte array. + /// is set to .Length. + /// + /// A non-empty, non-null big-endian byte array of at most 16 bytes. + /// A wrapper whose value and byte width match the supplied array. + /// is . + /// is empty or longer than 16 bytes. + public static BigEndianBitWrapper FromBytes(byte[] bigEndianBytes) + { + if (bigEndianBytes is null) + { + throw new ArgumentNullException(nameof(bigEndianBytes)); + } + + if (bigEndianBytes.Length == 0) + { + throw new ArgumentException("Byte array cannot be empty.", nameof(bigEndianBytes)); + } + + if (bigEndianBytes.Length > 16) + { + throw new ArgumentException("Byte array cannot exceed 16 bytes.", nameof(bigEndianBytes)); + } + + return FromBytesCore(bigEndianBytes, bigEndianBytes.Length); + } + + /// + /// Creates a from a big-endian byte array, zero-padding + /// the most-significant side to reach . + /// is set to . + /// + /// + /// An empty with a positive + /// produces an all-zero wrapper (value 0, ByteWidth = targetWidth). + /// + /// + /// A non-null big-endian byte array whose length does not exceed + /// . + /// + /// Target byte width (1-16). + /// + /// A wrapper with the value from and + /// equal to . + /// + /// is . + /// + /// is less than 1 or greater than 16. + /// + /// + /// .Length exceeds . + /// + public static BigEndianBitWrapper FromBytes(byte[] bigEndianBytes, int targetWidth) + { + if (bigEndianBytes is null) + { + throw new ArgumentNullException(nameof(bigEndianBytes)); + } + + if (targetWidth is < 1 or > 16) + { + throw new ArgumentOutOfRangeException(nameof(targetWidth), "Target width must be between 1 and 16."); + } + + if (bigEndianBytes.Length > targetWidth) + { + throw new ArgumentException( + $"Byte array length ({bigEndianBytes.Length}) exceeds target width ({targetWidth}).", + nameof(bigEndianBytes) + ); + } + + // Leading zero bytes don't affect the integer value, so shorter arrays can be + // passed directly; FromBytesCore sets ByteWidth = targetWidth regardless. + return FromBytesCore(bigEndianBytes, targetWidth); + } + + /// + /// Creates a subnet mask with the top bits set to 1 and the + /// remaining host bits set to 0. For example, CreateMask(4, 24) produces the IPv4 + /// netmask 255.255.255.0. + /// + /// Byte width of the address family (1-16). + /// Number of leading network bits to set; must be in [0, byteWidth × 8]. + /// A representing the subnet mask. + /// + /// is not in [1, 16], or is not + /// in [0, byteWidth × 8]. + /// + public static BigEndianBitWrapper CreateMask(int byteWidth, int prefixLength) + { + if (byteWidth is < 1 or > 16) + { + throw new ArgumentOutOfRangeException(nameof(byteWidth), "Byte width must be between 1 and 16."); + } + + var totalBits = byteWidth * 8; + + if (prefixLength < 0 || prefixLength > totalBits) + { + throw new ArgumentOutOfRangeException( + nameof(prefixLength), + $"Prefix length must be between 0 and {totalBits} for a {byteWidth}-byte address." + ); + } + + if (prefixLength == 0) + { +#if NET8_0_OR_GREATER + return new BigEndianBitWrapper(UInt128.Zero, byteWidth); +#else + return new BigEndianBitWrapper(0UL, 0UL, byteWidth); +#endif + } + + var hostBits = totalBits - prefixLength; + +#if NET8_0_OR_GREATER + var allOnes = byteWidth >= 16 ? UInt128.MaxValue : (UInt128.One << totalBits) - 1; + // Shift right to drop the low hostBits, then shift back left to restore bit positions - + // this clears the trailing hostBits while keeping the leading prefixLength bits set. + var mask = hostBits == 0 ? allOnes : (allOnes >> hostBits) << hostBits; + return new BigEndianBitWrapper(mask, byteWidth); +#else + return CreateMaskLegacy(byteWidth, hostBits); +#endif + } + + /// + /// Converts a validated big-endian byte array into a , + /// setting to . Callers are responsible + /// for all argument validation. + /// + /// + /// Big-endian byte array whose length is at most ; bytes absent + /// from the leading (most-significant) side are treated as implicit zeros. + /// + /// Byte width to assign to the returned wrapper; must be in [1, 16]. + /// A wrapper whose numeric value equals interpreted as an unsigned big-endian integer, with set to . + private static BigEndianBitWrapper FromBytesCore(byte[] bytes, int byteWidth) + { +#if NET8_0_OR_GREATER + UInt128 value = 0; + foreach (var byteValue in bytes) + { + value = (value << 8) | byteValue; + } + + return new BigEndianBitWrapper(value, byteWidth); +#else + ulong hi = 0, + lo = 0; + var len = bytes.Length; + + if (len <= 8) + { + foreach (var byteValue in bytes) + { + lo = (lo << 8) | byteValue; + } + } + else + { + // Bytes [0, hiCount) fill hi MSB-first; the remaining 8 bytes fill lo. + var hiCount = len - 8; + for (var i = 0; i < hiCount; i++) + { + hi = (hi << 8) | bytes[i]; + } + + for (var i = hiCount; i < len; i++) + { + lo = (lo << 8) | bytes[i]; + } + } + + return new BigEndianBitWrapper(hi, lo, byteWidth); +#endif + } + +#if !NET8_0_OR_GREATER + /// + /// Builds a subnet mask for the pre-.NET 8 two-field (hi/lo) representation. + /// Called from on targets that lack UInt128. + /// + /// Byte width of the address family; must be in [1, 16]. + /// Number of low bits to clear; equals total bits minus prefix length. Must be > 0. + /// A wrapper whose high byteWidth × 8 − hostBits bits are set and whose low hostBits bits are cleared. + private static BigEndianBitWrapper CreateMaskLegacy(int byteWidth, int hostBits) + { + // Establish the all-ones ceiling for this width so that host bits can be cleared below. + ulong allHi, + allLo; + if (byteWidth <= 8) + { + allHi = 0UL; + allLo = byteWidth == 8 ? ulong.MaxValue : (1UL << (byteWidth * 8)) - 1; + } + else + { + allLo = ulong.MaxValue; + allHi = byteWidth == 16 ? ulong.MaxValue : (1UL << ((byteWidth - 8) * 8)) - 1; + } + + if (hostBits == 0) + { + return new BigEndianBitWrapper(allHi, allLo, byteWidth); + } + + ulong maskHi, + maskLo; + + if (byteWidth <= 8) + { + // All bits live in lo. For valid inputs (byteWidth ≤ 8, prefixLength > 0), hostBits < 64, + // so the >= 64 guard is defensive; the shift-right-then-left clears the low hostBits. + maskHi = 0UL; + maskLo = hostBits >= 64 ? 0UL : (allLo >> hostBits) << hostBits; + } + else if (hostBits < 64) + { + // Host bits fall entirely within lo; the full hi word is the network prefix. + maskHi = allHi; + maskLo = (ulong.MaxValue >> hostBits) << hostBits; // clear the low hostBits of lo + } + else if (hostBits == 64) + { + // Entire lo word is host bits; hi carries the full network prefix. + maskHi = allHi; + maskLo = 0UL; + } + else + { + // Host bits spill into hi; clear hiHostBits low bits of hi and zero lo entirely. + var hiHostBits = hostBits - 64; + maskHi = hiHostBits >= 64 ? 0UL : (allHi >> hiHostBits) << hiHostBits; + maskLo = 0UL; + } + + return new BigEndianBitWrapper(maskHi, maskLo, byteWidth); + } +#endif + } +} diff --git a/src/Arcus/BigEndianBitWrapper.IComparable.cs b/src/Arcus/BigEndianBitWrapper.IComparable.cs new file mode 100644 index 0000000..942e83a --- /dev/null +++ b/src/Arcus/BigEndianBitWrapper.IComparable.cs @@ -0,0 +1,36 @@ +using System.Diagnostics; + +namespace Arcus +{ + /// implementation of + internal readonly partial struct BigEndianBitWrapper + { + /// + /// Compares this instance to as unsigned integers. + /// When numeric values are equal, is used as a tiebreaker so + /// that the comparison order is consistent with . + /// + /// The value to compare against. + /// A negative integer if less than, zero if equal, positive if greater than. + public int CompareTo(BigEndianBitWrapper other) + { + Debug.Assert(ByteWidth > 0, "ByteWidth must be greater than zero."); +#if NET8_0_OR_GREATER + var result = _value.CompareTo(other._value); + return result != 0 ? result : ByteWidth.CompareTo(other.ByteWidth); +#else + if (_hi != other._hi) + { + return _hi.CompareTo(other._hi); + } + + if (_lo != other._lo) + { + return _lo.CompareTo(other._lo); + } + + return ByteWidth.CompareTo(other.ByteWidth); +#endif + } + } +} diff --git a/src/Arcus/BigEndianBitWrapper.IEquatable.cs b/src/Arcus/BigEndianBitWrapper.IEquatable.cs new file mode 100644 index 0000000..2697a7b --- /dev/null +++ b/src/Arcus/BigEndianBitWrapper.IEquatable.cs @@ -0,0 +1,33 @@ +namespace Arcus +{ + /// implementation of + internal readonly partial struct BigEndianBitWrapper + { + /// + /// Returns when has the same numeric value + /// and the same . + /// + /// The value to compare. + /// if equal. + public bool Equals(BigEndianBitWrapper other) => +#if NET8_0_OR_GREATER + _value == other._value && ByteWidth == other.ByteWidth; +#else + _hi == other._hi && _lo == other._lo && ByteWidth == other.ByteWidth; +#endif + + /// + public override bool Equals(object obj) + { + return obj is BigEndianBitWrapper other && Equals(other); + } + + /// + public override int GetHashCode() => +#if NET8_0_OR_GREATER + HashCode.Combine(_value, ByteWidth); +#else + HashCode.Combine(_hi, _lo, ByteWidth); +#endif + } +} diff --git a/src/Arcus/BigEndianBitWrapper.IFormattable.cs b/src/Arcus/BigEndianBitWrapper.IFormattable.cs new file mode 100644 index 0000000..20cfd37 --- /dev/null +++ b/src/Arcus/BigEndianBitWrapper.IFormattable.cs @@ -0,0 +1,57 @@ +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif + +namespace Arcus +{ + /// implementation of + internal readonly partial struct BigEndianBitWrapper + { + /// + /// Formats the value using the specified format specifier. + /// + /// + /// + /// + /// "HC" + /// Hex compact - uppercase, no separators (e.g. "C0A80101"). + /// + /// + /// "IBE" + /// Integer big-endian - decimal string of the unsigned value. + /// + /// + /// "b" + /// Binary string, MSB first, length ByteWidth × 8. + /// + /// + /// null or "G" + /// Default - same as "HC". + /// + /// + /// + /// Ignored; present to satisfy . + /// The formatted string. + /// An unrecognised format specifier was supplied. + public string ToString( +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER + [AllowNull] +#endif + string format, +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER + [AllowNull] +#endif + IFormatProvider formatProvider) + { + return string.IsNullOrEmpty(format) || format == "G" + ? ToHexString() + : format switch + { + "HC" => ToHexString(), + "IBE" => ToDecimalString(), + "b" => ToBinaryString(), + _ => throw new FormatException($"Unknown format specifier '{format}' for {nameof(BigEndianBitWrapper)}."), + }; + } + } +} diff --git a/src/Arcus/BigEndianBitWrapper.Operators.cs b/src/Arcus/BigEndianBitWrapper.Operators.cs new file mode 100644 index 0000000..2404281 --- /dev/null +++ b/src/Arcus/BigEndianBitWrapper.Operators.cs @@ -0,0 +1,115 @@ +namespace Arcus +{ + /// operators + internal readonly partial struct BigEndianBitWrapper + { + /// Computes the bitwise AND of two wrappers; the result inherits the of . + /// The left operand; its is used for the result. + /// The right operand. + /// A new wrapper containing the bitwise AND of the two values. + public static BigEndianBitWrapper operator &(BigEndianBitWrapper left, BigEndianBitWrapper right) + { + if (left.ByteWidth != right.ByteWidth) + { + throw new ArgumentException("Operands must have the same ByteWidth."); + } +#if NET8_0_OR_GREATER + var masked = left._value & right._value & left.MaxValueForWidth; + return new BigEndianBitWrapper(masked, left.ByteWidth); +#else + var (maxHi, maxLo) = left.MaxHiLoForWidth; + return new BigEndianBitWrapper(left._hi & right._hi & maxHi, left._lo & right._lo & maxLo, left.ByteWidth); +#endif + } + + /// Computes the bitwise OR of two wrappers; the result inherits the of . + /// The left operand; its is used for the result. + /// The right operand. + /// A new wrapper containing the bitwise OR of the two values. + public static BigEndianBitWrapper operator |(BigEndianBitWrapper left, BigEndianBitWrapper right) + { + if (left.ByteWidth != right.ByteWidth) + { + throw new ArgumentException("Operands must have the same ByteWidth."); + } +#if NET8_0_OR_GREATER + var masked = (left._value | right._value) & left.MaxValueForWidth; + return new BigEndianBitWrapper(masked, left.ByteWidth); +#else + var (maxHi, maxLo) = left.MaxHiLoForWidth; + return new BigEndianBitWrapper((left._hi | right._hi) & maxHi, (left._lo | right._lo) & maxLo, left.ByteWidth); +#endif + } + + /// Computes the bitwise XOR of two wrappers; the result inherits the of . + /// The left operand; its is used for the result. + /// The right operand. + /// A new wrapper containing the bitwise XOR of the two values. + public static BigEndianBitWrapper operator ^(BigEndianBitWrapper left, BigEndianBitWrapper right) + { + if (left.ByteWidth != right.ByteWidth) + { + throw new ArgumentException("Operands must have the same ByteWidth."); + } +#if NET8_0_OR_GREATER + var masked = (left._value ^ right._value) & left.MaxValueForWidth; + return new BigEndianBitWrapper(masked, left.ByteWidth); +#else + var (maxHi, maxLo) = left.MaxHiLoForWidth; + return new BigEndianBitWrapper((left._hi ^ right._hi) & maxHi, (left._lo ^ right._lo) & maxLo, left.ByteWidth); +#endif + } + + /// + /// Computes the bitwise NOT of , bounded to . Bits above the + /// byte-width boundary are never set in the result. + /// + /// The operand to invert. + /// A new wrapper with all bits in the range flipped. + public static BigEndianBitWrapper operator ~(BigEndianBitWrapper value) + { +#if NET8_0_OR_GREATER + return new BigEndianBitWrapper(value.MaxValueForWidth ^ value._value, value.ByteWidth); +#else + var (maxHi, maxLo) = value.MaxHiLoForWidth; + return new BigEndianBitWrapper(maxHi ^ value._hi, maxLo ^ value._lo, value.ByteWidth); +#endif + } + + /// Returns if and are equal in both numeric value and . + /// The left operand. + /// The right operand. + /// if both operands are equal; otherwise . + public static bool operator ==(BigEndianBitWrapper left, BigEndianBitWrapper right) => left.Equals(right); + + /// Returns if and differ in numeric value or . + /// The left operand. + /// The right operand. + /// if the operands are not equal; otherwise . + public static bool operator !=(BigEndianBitWrapper left, BigEndianBitWrapper right) => !left.Equals(right); + + /// Returns if is less than . + /// The left operand. + /// The right operand. + /// if is less than ; otherwise . + public static bool operator <(BigEndianBitWrapper left, BigEndianBitWrapper right) => left.CompareTo(right) < 0; + + /// Returns if is greater than . + /// The left operand. + /// The right operand. + /// if is greater than ; otherwise . + public static bool operator >(BigEndianBitWrapper left, BigEndianBitWrapper right) => left.CompareTo(right) > 0; + + /// Returns if is less than or equal to . + /// The left operand. + /// The right operand. + /// if is less than or equal to ; otherwise . + public static bool operator <=(BigEndianBitWrapper left, BigEndianBitWrapper right) => left.CompareTo(right) <= 0; + + /// Returns if is greater than or equal to . + /// The left operand. + /// The right operand. + /// if is greater than or equal to ; otherwise . + public static bool operator >=(BigEndianBitWrapper left, BigEndianBitWrapper right) => left.CompareTo(right) >= 0; + } +} diff --git a/src/Arcus/BigEndianBitWrapper.cs b/src/Arcus/BigEndianBitWrapper.cs new file mode 100644 index 0000000..cfc47a3 --- /dev/null +++ b/src/Arcus/BigEndianBitWrapper.cs @@ -0,0 +1,467 @@ +using System.Diagnostics; +using System.Globalization; +using System.Numerics; +#if NET9_0_OR_GREATER +using System.Runtime.CompilerServices; +#endif +#if !NET8_0_OR_GREATER +using System.Text; +#endif + +namespace Arcus +{ + /// + /// An internal unsigned integer with big-endian byte semantics, used for binary operations on + /// network addresses. (4 for IPv4, 16 for IPv6) is stored + /// alongside the value so that overflow/underflow detection and byte-array round-trips are always + /// bounded to the correct address-family width. + /// + /// + /// + /// On .NET 8+ the backing store is a UInt128; on earlier targets two + /// fields (high / low) provide equivalent 128-bit arithmetic. All arithmetic + /// is bounded by , not by the full 128-bit range. + /// + /// All arithmetic operations rely on unchecked context for carry/borrow detection. + /// + /// The non-generic is intentionally not implemented because + /// this type is and only used in strongly-typed comparison sites. + /// + /// +#if NET9_0_OR_GREATER + [SkipLocalsInit] +#endif + [DebuggerDisplay("{DebuggerDisplay}")] + internal readonly partial struct BigEndianBitWrapper + : IComparable, + IEquatable, + IFormattable + { + /// Gets the byte width of this value (e.g. 4 for IPv4, 16 for IPv6). + public readonly int ByteWidth; + +#if NET8_0_OR_GREATER + private readonly UInt128 _value; // unsigned 128-bit backing store; always bounded to [0, MaxValueForWidth] + + /// Initializes a new instance of the struct. + /// The unsigned 128-bit value, already bounded to the representable range for bytes. + /// Byte width of the address family; must be in [1, 16]. + private BigEndianBitWrapper(UInt128 value, int byteWidth) + { + _value = value; + ByteWidth = byteWidth; + } + + /// Gets the maximum unsigned integer representable in bytes. + /// + /// UInt128.MaxValue when is 16; otherwise + /// (1 << (ByteWidth × 8)) - 1. Serves as the arithmetic ceiling for overflow + /// detection and as the all-ones base for bitwise NOT and mask operations. + /// + private UInt128 MaxValueForWidth + { + get + { + if (ByteWidth == 0) + { + return UInt128.Zero; + } + + return ByteWidth >= 16 ? UInt128.MaxValue : (UInt128.One << (ByteWidth * 8)) - 1; + } + } +#else + private readonly ulong _hi; // most-significant 64 bits + private readonly ulong _lo; // least-significant 64 bits + + /// Initializes a new instance of the struct. + /// Most-significant 64 bits of the value; always 0 for byte widths 1-8. + /// Least-significant 64 bits of the value. + /// Byte width of the address family; must be in [1, 16]. + private BigEndianBitWrapper(ulong hi, ulong lo, int byteWidth) + { + _hi = hi; + _lo = lo; + ByteWidth = byteWidth; + } + + /// Gets the maximum unsigned integer for bytes, split into high and low 64-bit components. + /// + /// A tuple where Hi is 0 for byte widths 1-8 (value fits entirely in Lo) + /// and carries the most-significant bits for widths 9-16. Serves as the arithmetic ceiling + /// for overflow detection and as the all-ones base for bitwise NOT and mask operations. + /// + private (ulong Hi, ulong Lo) MaxHiLoForWidth + { + get + { + if (ByteWidth == 0) + { + return (0UL, 0UL); + } + + if (ByteWidth >= 16) + { + return (ulong.MaxValue, ulong.MaxValue); + } + + if (ByteWidth == 8) + { + return (0UL, ulong.MaxValue); + } + + // ByteWidth in [1,7]: value fits entirely in lo + return (0UL, (1UL << (ByteWidth * 8)) - 1); + } + } +#endif + + #region Arithmetic + + /// + /// Attempts to add to this value. Returns if + /// the result would exceed the maximum value for (overflow) or fall + /// below zero (underflow). On failure is set to + /// . + /// + /// Signed amount to add; may be negative for a decrement. + /// Receives the result on success. + /// if the addition succeeded within the byte-width bounds. + public bool TryAdd(long delta, out BigEndianBitWrapper result) + { + if (delta == 0) + { + result = this; + return true; + } + +#if NET8_0_OR_GREATER + var maxValue = MaxValueForWidth; + + if (delta > 0) + { + // Route through ulong before widening to UInt128 to make unsigned intent explicit. + var ud = (UInt128)(ulong)delta; + // Compare against headroom (maxValue - _value) rather than checking _value + ud > maxValue + // directly: _value can be UInt128.MaxValue when ByteWidth == 16, making the sum overflow. + if (ud > maxValue - _value) + { + result = default; + return false; + } + + result = new BigEndianBitWrapper(_value + ud, ByteWidth); + } + else + { + // Avoid -long.MinValue overflow; its magnitude as ulong is 2^63. + var magnitude = delta == long.MinValue ? 9223372036854775808UL : (ulong)(-delta); + if ((UInt128)magnitude > _value) + { + result = default; + return false; + } + + result = new BigEndianBitWrapper(_value - magnitude, ByteWidth); + } +#else + var (maxHi, maxLo) = MaxHiLoForWidth; + + if (delta > 0) + { + var ud = (ulong)delta; + // Compute headroom = maxValue - _value (safe because _value ≤ maxValue). + // If remHi > 0, headroom ≥ 2^64 which exceeds any ulong addend, so overflow is + // impossible; only check remLo when headroom fits entirely in 64 bits. + var (remHi, remLo) = SubtractHiLo(maxHi, maxLo, _hi, _lo); + if (remHi == 0 && ud > remLo) + { + result = default; + return false; + } + + var (newHi, newLo) = AddUlongToHiLo(_hi, _lo, ud); + result = new BigEndianBitWrapper(newHi, newLo, ByteWidth); + } + else + { + var magnitude = delta == long.MinValue ? 9223372036854775808UL : (ulong)(-delta); + // Underflow if magnitude > _value. If _hi > 0, _value ≥ 2^64 > magnitude. + if (_hi == 0 && magnitude > _lo) + { + result = default; + return false; + } + + var (newHi, newLo) = SubtractHiLo(_hi, _lo, 0UL, magnitude); + result = new BigEndianBitWrapper(newHi, newLo, ByteWidth); + } +#endif + return true; + } + + /// + /// Subtracts from this value. The caller must ensure + /// this >= other; a negative result throws. + /// + /// The value to subtract. + /// The non-negative difference, retaining . + /// The subtraction would produce a negative result. + public BigEndianBitWrapper Subtract(BigEndianBitWrapper other) + { + Debug.Assert(ByteWidth > 0, "ByteWidth must be greater than zero."); + if (ByteWidth != other.ByteWidth) + { + throw new ArgumentException("Operands must have the same ByteWidth."); + } + if (CompareTo(other) < 0) + { + throw new InvalidOperationException("Subtraction would produce a negative result."); + } + +#if NET8_0_OR_GREATER + return new BigEndianBitWrapper(_value - other._value, ByteWidth); +#else + var (newHi, newLo) = SubtractHiLo(_hi, _lo, other._hi, other._lo); + return new BigEndianBitWrapper(newHi, newLo, ByteWidth); +#endif + } + +#if !NET8_0_OR_GREATER + /// Adds a 64-bit to a 128-bit value represented as (, ) fields. + /// Most-significant 64 bits of the operand. + /// Least-significant 64 bits of the operand. + /// The value to add to the low word; must not overflow the 128-bit total. + /// The (hi, lo) sum, with any carry from propagated into hi. + private static (ulong Hi, ulong Lo) AddUlongToHiLo(ulong hi, ulong lo, ulong addend) + { + var newLo = lo + addend; + var carry = newLo < lo ? 1UL : 0UL; // Unsigned wrap-around in newLo means a carry occurred. + return (hi + carry, newLo); + } + + /// Subtracts a 128-bit value (, ) from (, ). The caller must guarantee a ≥ b. + /// Most-significant 64 bits of the minuend. + /// Least-significant 64 bits of the minuend. + /// Most-significant 64 bits of the subtrahend. + /// Least-significant 64 bits of the subtrahend. + /// The (hi, lo) difference, with any borrow from propagated into hi. + private static (ulong Hi, ulong Lo) SubtractHiLo(ulong aHi, ulong aLo, ulong bHi, ulong bLo) + { + var newLo = aLo - bLo; + var borrow = newLo > aLo ? 1UL : 0UL; // Unsigned wrap-around in newLo means a borrow occurred. + return (aHi - bHi - borrow, newLo); + } +#endif + + #endregion // end: Arithmetic + + #region Conversion + + /// + /// Returns the value as a big-endian byte array of exactly bytes, + /// zero-padded on the most-significant side. + /// + /// A new byte array of length . + /// + /// + /// This overload allocates a fresh byte array. On .NET 8+ where the caller has a fixed-size + /// buffer, prefer ToBytes(Span<byte>) to avoid the allocation. Pre-.NET 8 + /// callers must continue using this allocating overload. + /// + /// + public byte[] ToBytes() + { + var result = new byte[ByteWidth]; +#if NET8_0_OR_GREATER + ToBytes(result.AsSpan()); +#else + ToBytesCore(result); +#endif + return result; + } + +#if NET8_0_OR_GREATER + /// + /// Writes the value as a big-endian sequence of exactly bytes into + /// , zero-padded on the most-significant side. + /// Prefer this overload over when the caller can supply a + /// stackalloc buffer to avoid a heap allocation. + /// + /// A span of at least bytes to write into. + public void ToBytes(Span destination) + { + if (destination.Length < ByteWidth) + { + throw new ArgumentException("Destination span is too short.", nameof(destination)); + } + var v = _value; + for (var i = ByteWidth - 1; i >= 0; i--) + { + destination[i] = (byte)(v & 0xFF); + v >>= 8; + } + } +#else + /// + /// Writes the value as a big-endian sequence of exactly bytes into + /// , zero-padding the most-significant side as needed. + /// This is the pre-.NET 8 equivalent of , operating on the two-field + /// (_hi, _lo) representation. All argument validation is the caller's responsibility. + /// + /// Pre-allocated byte array of length to write into. + private void ToBytesCore(byte[] result) + { + if (ByteWidth <= 8) + { + var lo = _lo; + for (var i = ByteWidth - 1; i >= 0; i--) + { + result[i] = (byte)(lo & 0xFF); + lo >>= 8; + } + } + else + { + // Lower 8 bytes come from lo, upper (ByteWidth - 8) bytes come from hi. + var lo = _lo; + for (var i = ByteWidth - 1; i >= ByteWidth - 8; i--) + { + result[i] = (byte)(lo & 0xFF); + lo >>= 8; + } + + var hi = _hi; + var hiByteCount = ByteWidth - 8; + for (var i = hiByteCount - 1; i >= 0; i--) + { + result[i] = (byte)(hi & 0xFF); + hi >>= 8; + } + } + } +#endif + + /// + /// Converts the value to a non-negative using the unsigned + /// big-endian interpretation. + /// + /// A non-negative equal to the unsigned integer value. + public BigInteger ToBigInteger() + { +#if NET8_0_OR_GREATER + Span bytes = stackalloc byte[ByteWidth]; + ToBytes(bytes); + return new BigInteger(bytes, isUnsigned: true, isBigEndian: true); +#else + var bytes = ToBytes(); + // BigInteger(byte[]) expects little-endian with an optional sign byte. + // Allocate one extra byte (le[bytes.Length] stays 0) so the leading zero + // forces an unsigned (positive) interpretation regardless of the MSB. + var le = new byte[bytes.Length + 1]; + for (var i = 0; i < bytes.Length; i++) + { + le[i] = bytes[bytes.Length - 1 - i]; // reverse byte order: big-endian → little-endian + } + + return new BigInteger(le); +#endif + } + + #endregion // end: Conversion + + #region Formatting + + /// + /// Returns an uppercase hex string with no separators or prefix. + /// Length is always ByteWidth × 2 characters. + /// + /// Example: 192.168.1.1 → "C0A80101". + public string ToHexString() + { +#if NET8_0_OR_GREATER + Span bytes = stackalloc byte[ByteWidth]; + ToBytes(bytes); + return Convert.ToHexString(bytes); +#else + var bytes = ToBytes(); + var sb = new StringBuilder(ByteWidth * 2); + foreach (var byteValue in bytes) + { + sb.Append(byteValue.ToString("X2", CultureInfo.InvariantCulture)); + } + + return sb.ToString(); +#endif + } + + /// + /// Returns the decimal string of the unsigned big-endian integer value. + /// + /// Example: 192.168.1.1 → "3232235777". + public string ToDecimalString() + { + return ToBigInteger().ToString(CultureInfo.InvariantCulture); + } + + /// + /// Returns a binary string of exactly ByteWidth × 8 characters, MSB first. + /// + /// Example: 0x0F (1 byte) → "00001111". + public string ToBinaryString() + { +#if NET8_0_OR_GREATER + Span bytes = stackalloc byte[ByteWidth]; + ToBytes(bytes); + Span chars = stackalloc char[ByteWidth * 8]; + var pos = 0; + foreach (var byteValue in bytes) + { + for (var bit = 7; bit >= 0; bit--) + { + // Shift the target bit to position 0, mask to isolate it, then map 0→'0' and 1→'1'. + chars[pos++] = (char)('0' + ((byteValue >> bit) & 1)); + } + } + + return new string(chars); +#else + var bytes = ToBytes(); + var sb = new StringBuilder(ByteWidth * 8); + foreach (var byteValue in bytes) + { + for (var bit = 7; bit >= 0; bit--) + { + // Shift the target bit to position 0, mask to isolate it; Append(int) renders 0 or 1. + sb.Append((byteValue >> bit) & 1); + } + } + + return sb.ToString(); +#endif + } + + /// Returns the hex compact representation (same as ). + /// An uppercase hex string of length ByteWidth × 2. + public override string ToString() + { + return ToHexString(); + } + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private string DebuggerDisplay + { + get + { + var bytes = ToBytes(); + var hex = string.Join( + "_", + Array.ConvertAll(bytes, byteValue => byteValue.ToString("X2", CultureInfo.InvariantCulture)) + ); + return $"0x{hex} ({ByteWidth} bytes)"; + } + } + + #endregion // end: Formatting + } +} diff --git a/src/Arcus/Comparers/DefaultAddressFamilyComparer.cs b/src/Arcus/Comparers/DefaultAddressFamilyComparer.cs index f3519e6..2ee90a6 100644 --- a/src/Arcus/Comparers/DefaultAddressFamilyComparer.cs +++ b/src/Arcus/Comparers/DefaultAddressFamilyComparer.cs @@ -1,19 +1,17 @@ -using System; -using System.Collections.Generic; -using System.Net.Sockets; +using System.Net.Sockets; namespace Arcus.Comparers { /// - /// Default - /// Executes the of method + /// Default for . + /// Delegates to the implementation of . /// - public class DefaultAddressFamilyComparer : Comparer + public sealed class DefaultAddressFamilyComparer : Comparer { /// - /// Default instance of using + /// Default singleton instance. /// - public static readonly DefaultAddressFamilyComparer Instance = new DefaultAddressFamilyComparer(); + public static readonly DefaultAddressFamilyComparer Instance = new(); /// public override int Compare(AddressFamily x, AddressFamily y) diff --git a/src/Arcus/Comparers/DefaultIIPAddressRangeComparer.cs b/src/Arcus/Comparers/DefaultIIPAddressRangeComparer.cs index 65c0d22..39811a2 100644 --- a/src/Arcus/Comparers/DefaultIIPAddressRangeComparer.cs +++ b/src/Arcus/Comparers/DefaultIIPAddressRangeComparer.cs @@ -1,19 +1,24 @@ -using System; -using System.Collections.Generic; +using System.Diagnostics; using System.Net; +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif namespace Arcus.Comparers { /// - /// Default - /// Compares by and then by range length ordinal + /// Default for . + /// Compares by first, then by + /// as a tiebreaker. /// - public class DefaultIIPAddressRangeComparer : Comparer +#pragma warning disable S101 // Types should be named in PascalCase + public sealed class DefaultIIPAddressRangeComparer : Comparer +#pragma warning restore S101 // Types should be named in PascalCase { /// - /// Default instance of using + /// Default singleton instance using . /// - public static readonly DefaultIIPAddressRangeComparer Instance = new DefaultIIPAddressRangeComparer(); + public static readonly DefaultIIPAddressRangeComparer Instance = new(); private readonly IComparer _ipAddressComparer; @@ -29,13 +34,20 @@ public DefaultIIPAddressRangeComparer(IComparer ipAddressComparer) /// /// Initializes a new instance of the class. - /// Defaults to use the DefaultIIPAddressComparer /// public DefaultIIPAddressRangeComparer() : this(DefaultIPAddressComparer.Instance) { } /// - public override int Compare(IIPAddressRange x, IIPAddressRange y) + public override int Compare( +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER + [AllowNull] +#endif + IIPAddressRange x, +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER + [AllowNull] +#endif + IIPAddressRange y) { if (ReferenceEquals(x, y)) { @@ -52,6 +64,8 @@ public override int Compare(IIPAddressRange x, IIPAddressRange y) return 1; } + Debug.Assert(x.Head is not null && y.Head is not null, "IIPAddressRange.Head is contractually non-null."); + var headComparison = this._ipAddressComparer.Compare(x.Head, y.Head); return headComparison != 0 ? headComparison : x.Length.CompareTo(y.Length); } diff --git a/src/Arcus/Comparers/DefaultIPAddressComparer.cs b/src/Arcus/Comparers/DefaultIPAddressComparer.cs index c08ad78..4ea74c1 100644 --- a/src/Arcus/Comparers/DefaultIPAddressComparer.cs +++ b/src/Arcus/Comparers/DefaultIPAddressComparer.cs @@ -1,22 +1,21 @@ -using System; -using System.Collections.Generic; -using System.Net; +using System.Net; using System.Net.Sockets; -using Gulliver; +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif namespace Arcus.Comparers { /// - /// Default - /// Compares the then the integer equivalent value of an in - /// ordinal order + /// Default for . + /// Compares by first, then by the numeric value of the address. /// - public class DefaultIPAddressComparer : Comparer + public sealed class DefaultIPAddressComparer : Comparer { /// - /// Default instance of using + /// Default singleton instance using . /// - public static readonly DefaultIPAddressComparer Instance = new DefaultIPAddressComparer(); + public static readonly DefaultIPAddressComparer Instance = new(); private readonly IComparer _addressFamilyComparer; @@ -27,13 +26,12 @@ public class DefaultIPAddressComparer : Comparer /// is . public DefaultIPAddressComparer(IComparer addressFamilyComparer) { - if (addressFamilyComparer == null) + if (addressFamilyComparer is null) { throw new ArgumentNullException(nameof(addressFamilyComparer)); } - this._addressFamilyComparer = - addressFamilyComparer ?? throw new ArgumentNullException(nameof(addressFamilyComparer)); + this._addressFamilyComparer = addressFamilyComparer; } /// @@ -43,7 +41,15 @@ public DefaultIPAddressComparer() : this(DefaultAddressFamilyComparer.Instance) { } /// - public override int Compare(IPAddress x, IPAddress y) + public override int Compare( +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER + [AllowNull] +#endif + IPAddress x, +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER + [AllowNull] +#endif + IPAddress y) { if (ReferenceEquals(x, y)) { @@ -63,7 +69,9 @@ public override int Compare(IPAddress x, IPAddress y) var addressFamilyComparison = this._addressFamilyComparer.Compare(x.AddressFamily, y.AddressFamily); return addressFamilyComparison == 0 - ? ByteArrayUtils.CompareUnsignedBigEndian(x.GetAddressBytes(), y.GetAddressBytes()) + ? BigEndianBitWrapper + .FromBytes(x.GetAddressBytes()) + .CompareTo(BigEndianBitWrapper.FromBytes(y.GetAddressBytes())) : addressFamilyComparison; } } diff --git a/src/Arcus/Comparers/DefaultIPAddressRangeComparer.cs b/src/Arcus/Comparers/DefaultIPAddressRangeComparer.cs deleted file mode 100644 index 9a5d51c..0000000 --- a/src/Arcus/Comparers/DefaultIPAddressRangeComparer.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net; - -namespace Arcus.Comparers -{ - /// - /// Default - /// Compares by and then by range length ordinal - /// - [Obsolete("Use DefaultIIPAddressRangeComparer")] - public class DefaultIPAddressRangeComparer : Comparer - { - private readonly DefaultIIPAddressRangeComparer _comparer; - - /// - /// Initializes a new instance of the class. - /// - /// comparer used to compare - /// is . - public DefaultIPAddressRangeComparer(IComparer ipAddressComparer) - { - if (ipAddressComparer is null) - { - throw new ArgumentNullException(nameof(ipAddressComparer)); - } - - this._comparer = new DefaultIIPAddressRangeComparer(ipAddressComparer); - } - - /// - /// Initializes a new instance of the class. - /// Defaults to use the DefaultIPAddressComparer - /// - public DefaultIPAddressRangeComparer() - : this(DefaultIPAddressComparer.Instance) { } - - /// - public override int Compare(IIPAddressRange x, IIPAddressRange y) - { - return _comparer.Compare(x, y); - } - } -} diff --git a/src/Arcus/Converters/IPAddressConverters.cs b/src/Arcus/Converters/IPAddressConverters.cs index 091dad3..3506441 100644 --- a/src/Arcus/Converters/IPAddressConverters.cs +++ b/src/Arcus/Converters/IPAddressConverters.cs @@ -1,44 +1,50 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Net; +using System.Net; using System.Net.Sockets; -using System.Numerics; -using System.Text.RegularExpressions; using Arcus.Utilities; -using Gulliver; +#if !NET8_0_OR_GREATER +using System.Numerics; +#endif + +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif namespace Arcus.Converters { /// - /// Static utility class containing conversion methods for converting objects into something - /// else + /// Static utility class providing extension methods for converting objects. /// public static class IPAddressConverters { #region integral conversion /// - /// Convert a valid netmask (encoded as ) into a CIDR route prefix - /// Only valid for IPv4 netmasks + /// Converts a valid IPv4 subnet mask to a CIDR route prefix length. /// - /// the netmask to convert - /// the route prefix - /// is . - /// not a valid netmask + /// + /// + /// The netmask must be a contiguous sequence of leading 1-bits per + /// RFC 950 §2. + /// The resulting integer is the CIDR prefix length per + /// RFC 4632 §2. + /// + /// + /// The subnet mask to convert (IPv4 only). + /// The CIDR route prefix length (0-32). + /// is . + /// is not a valid subnet mask. public static int NetmaskToCidrRoutePrefix(this IPAddress netmask) { #region defense - if (netmask == null) + if (netmask is null) { throw new ArgumentNullException(nameof(netmask)); } if (!netmask.IsValidNetMask()) { - throw new InvalidOperationException("not a valid netmask"); + throw new ArgumentException("The provided address is not a valid netmask.", nameof(netmask)); } #endregion // end: defense @@ -67,131 +73,287 @@ public static int NetmaskToCidrRoutePrefix(this IPAddress netmask) #region string conversion /// - /// IPv6 to Base85 (will return empty string for non ipv6 addresses) AKA Ascii85 - /// from RFC 1924 ( http://tools.ietf.org/html/rfc1924 ) - /// - /// The RFC is an April Fools Day Joke, but we implemented it anyhow - /// + /// Converts an IPv6 address to a Base85 (Ascii85) string per RFC 1924. /// - /// the ip address to convert - /// Ascii85/Base85 representation of IPv6 Address, or an empty string on failure + /// + /// + /// Encoding defined in RFC 1924 + /// (an April Fools Day joke, but implemented here regardless). + /// + /// + /// The IPv6 address to convert. + /// Base85 representation, or if the address is not IPv6. +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER + [return: MaybeNull] +#endif public static string ToBase85String(this IPAddress ipAddress) { - if (ipAddress == null || !ipAddress.IsIPv6()) + if (ipAddress?.IsIPv6() is not true) { return null; } - return string.Concat(GetBase85Chars(ipAddress).Reverse()).PadLeft(20, '0'); + const string base85Alphabet = + "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!#$%&()*+-;<=>?@^_`{|}~"; + var chars = new char[20]; + var addressBytes = ipAddress.GetAddressBytes(); -#if NET6_0_OR_GREATER - static -#endif - IEnumerable GetBase85Chars(IPAddress input) +#if NET8_0_OR_GREATER + ulong high = 0; + ulong low = 0; + for (var i = 0; i < 8; i++) { - const string alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!#$%&()*+-;<=>?@^_`{|}~"; - - // get little endian unsigned byte value - var addressBytes = input.GetAddressBytes().Reverse().ToList(); + high = (high << 8) | addressBytes[i]; + } - addressBytes.Add(0x00); + for (var i = 8; i < 16; i++) + { + low = (low << 8) | addressBytes[i]; + } - // either rely on BigInteger, or implement byte based modulus and division - var bigInteger = new BigInteger(addressBytes.ToArray()); + var value128 = new UInt128(high, low); + for (var i = 19; i >= 0; i--) + { + (value128, var remainder) = UInt128.DivRem(value128, 85); + chars[i] = base85Alphabet[(int)remainder]; + } +#else + var leBytes = new byte[17]; // zero-initialized; index 16 stays 0 (sign byte) + for (var i = 0; i < 16; i++) + { + leBytes[i] = addressBytes[15 - i]; + } - do - { - bigInteger = BigInteger.DivRem(bigInteger, 85, out var charIndex); - yield return alphabet[(int)charIndex]; - } while (bigInteger > 0); + var value = new BigInteger(leBytes); + for (var i = 19; i >= 0; i--) + { + value = BigInteger.DivRem(value, 85, out var remainder); + chars[i] = base85Alphabet[(int)remainder]; } +#endif + + return new string(chars); } /// - /// Represent address as dotted quad (if not ipv6 simply return stringified version of input) + /// Converts an IP address to dotted-quad notation (IPv4), or mixed IPv6/IPv4 notation for IPv4-mapped IPv6 addresses. /// + /// + /// + /// IPv4 dotted-quad notation (four decimal octets separated by dots) per + /// RFC 791 §2.3. + /// + /// + /// For IPv6 addresses the first 96 bits (6 hextets) are rendered in compressed IPv6 notation - + /// the longest consecutive run of zero-valued hextets is collapsed to :: per + /// RFC 5952 §4 - followed + /// by the trailing 32 bits as an IPv4 dotted-quad suffix. + /// + /// /// the ip address to convert /// dotted quad version of the given address /// is . +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER + [return: NotNullIfNotNull(nameof(ipAddress))] +#endif public static string ToDottedQuadString(this IPAddress ipAddress) { - if (ipAddress == null) + if (ipAddress?.IsIPv6() is not true) { - return null; + return ipAddress?.ToString(); } - if (!ipAddress.IsIPv6()) +#if NET8_0_OR_GREATER + Span addressBytes = stackalloc byte[16]; + if (!ipAddress.TryWriteBytes(addressBytes, out var bytesWritten) || bytesWritten != 16) { return ipAddress.ToString(); } + Span hextets = stackalloc ushort[6]; +#else + var addressBytes = ipAddress.GetAddressBytes(); + var hextets = new ushort[6]; +#endif + + for (var i = 0; i < 6; i++) + { + hextets[i] = (ushort)((addressBytes[i * 2] << 8) | addressBytes[(i * 2) + 1]); + } - // TODO candidate for clean up / simplification - var bytes = ipAddress.GetAddressBytes(); // get the bytes of the ip address + var (bestStart, bestLen) = FindLongestZeroRun(hextets); - var leadingBytes = bytes.Take(12).ToArray(); // capture the non ipv4 bytes +#if NET8_0_OR_GREATER + Span hexBuffer = stackalloc char[4]; +#endif - var hextets = Enumerable - .Range(0, 6) - .Select(i => - { - var index = i * 2; - return new byte[] { leadingBytes[index + 1], leadingBytes[index], 0x0, 0x0 }; - }) // get bytes in pairs with leading 0's to force unsigned form - .Select(bs => BitConverter.ToInt32(bs, 0)) // convert byte pairs to 16 bit integers - .Select(i => $"{i:x}"); // combine bytes to a hex string + var sb = new System.Text.StringBuilder(45); +#if NET8_0_OR_GREATER + AppendIPv6Prefix(sb, hextets, bestStart, bestLen, hexBuffer); +#else + AppendIPv6Prefix(sb, hextets, bestStart, bestLen); +#endif - var hextetString = string.Join(":", hextets); // join the hextets on a colon + sb.Append(addressBytes[12]); + sb.Append('.'); + sb.Append(addressBytes[13]); + sb.Append('.'); + sb.Append(addressBytes[14]); + sb.Append('.'); + sb.Append(addressBytes[15]); - var longestMatch = new Regex(@"((:|\b)0\b)+") // find 0's surrounded by colons or word breaks - .Matches(hextetString) // match across the hextet string - .Cast() - .Select(match => match.Value) // get the match value - .OrderByDescending(s => - s?.StartsWith("0", StringComparison.OrdinalIgnoreCase) == true ? s.Length + 1 : s.Length - ) // order by length accounting for matches at beginning of string - .FirstOrDefault(); // find the longest span of 0 valued hextets, or null if one does not exist + return sb.ToString(); - if (longestMatch != null) +#if NET8_0_OR_GREATER + static (int BestStart, int BestLen) FindLongestZeroRun(ReadOnlySpan hextets) +#else + static (int BestStart, int BestLen) FindLongestZeroRun(ushort[] hextets) +#endif { - // get the first index of the longest match - var index = hextetString.IndexOf(longestMatch, StringComparison.Ordinal); - hextetString = hextetString.Remove(index, longestMatch.Length).Insert(index, ":"); // replace first occurrence with a ":" char + var bestStart = -1; + var bestLength = 0; + var runStart = -1; + var runLength = 0; + + for (var i = 0; i < 6; i++) + { + if (hextets[i] == 0) + { + if (runStart < 0) + { + runStart = i; + } + + runLength++; + if (runLength > bestLength) + { + bestStart = runStart; + bestLength = runLength; + } + } + else + { + runStart = -1; + runLength = 0; + } + } + + return (bestStart, bestLength); } - var followingBytes = bytes.Skip(12).ToArray(); // capture IPv4 bytes (last 4) - var ipv4Address = new IPAddress(followingBytes).ToString(); +#if NET8_0_OR_GREATER + static void AppendIPv6Prefix( + System.Text.StringBuilder sb, + ReadOnlySpan hextets, + int bestStart, + int bestLen, + Span hexBuffer + ) +#else + static void AppendIPv6Prefix(System.Text.StringBuilder sb, ushort[] hextets, int bestStart, int bestLen) +#endif + { + if (bestLen <= 0) + { +#if NET8_0_OR_GREATER + AppendHextetRange(sb, hextets, 0, 6, hexBuffer); +#else + AppendHextetRange(sb, hextets, 0, 6); +#endif + sb.Append(':'); + return; + } + +#if NET8_0_OR_GREATER + AppendHextetRange(sb, hextets, 0, bestStart, hexBuffer); +#else + AppendHextetRange(sb, hextets, 0, bestStart); +#endif + sb.Append("::"); + + var afterRunEnd = bestStart + bestLen; +#if NET8_0_OR_GREATER + AppendHextetRange(sb, hextets, afterRunEnd, 6, hexBuffer); +#else + AppendHextetRange(sb, hextets, afterRunEnd, 6); +#endif + + if (afterRunEnd < 6) + { + sb.Append(':'); + } + } - return hextetString + ":" + ipv4Address; +#if NET8_0_OR_GREATER + static void AppendHextetRange( + System.Text.StringBuilder sb, + ReadOnlySpan hextets, + int from, + int to, + Span hexBuffer + ) +#else + static void AppendHextetRange(System.Text.StringBuilder sb, ushort[] hextets, int from, int to) +#endif + { + for (var i = from; i < to; i++) + { + if (i > from) + { + sb.Append(':'); + } +#if NET8_0_OR_GREATER + hextets[i].TryFormat(hexBuffer, out var charsWritten, "x"); + sb.Append(hexBuffer[..charsWritten]); +#else + sb.Append(hextets[i].ToString("x")); +#endif + } + } } /// - /// Short hex value + /// Converts an IP address to an uppercase hexadecimal string with no separators. /// - /// the ip address to convert - /// Hex version of the given IP Address - /// is . + /// The address to convert. + /// Hex string (e.g., "C0A80101" for 192.168.1.1), or if input is . +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER + [return: NotNullIfNotNull(nameof(ipAddress))] +#endif public static string ToHexString(this IPAddress ipAddress) { - return ipAddress?.GetAddressBytes().ToString("HC", CultureInfo.InvariantCulture); + return ipAddress == null ? null : BigEndianBitWrapper.FromBytes(ipAddress.GetAddressBytes()).ToHexString(); } /// - /// Convert an to a numeric representation + /// Converts an IP address to its decimal numeric string representation. /// - /// The ip address to convert - /// an integral representation of the IP address - /// is . + /// The address to convert. + /// Decimal string of the unsigned integer value, or if input is . +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER + [return: NotNullIfNotNull(nameof(ipAddress))] +#endif public static string ToNumericString(this IPAddress ipAddress) { - return ipAddress?.GetAddressBytes().ToString("IBE", CultureInfo.InvariantCulture); + return ipAddress == null ? null : BigEndianBitWrapper.FromBytes(ipAddress.GetAddressBytes()).ToDecimalString(); } /// - /// Convert to uncompressed IPv4/IPv6, adding zeros or expanding '::' where appropriate + /// Converts an IP address to its fully-expanded (uncompressed) string form. /// - /// the address to expand - /// the expanded for of IPv4/IPv6, or ToString() otherwise - /// is . + /// + /// + /// For IPv6, produces the fully-expanded form (no :: compression, all hextets zero-padded to 4 digits), + /// which is the inverse of the compressed form defined in + /// RFC 5952 §4. + /// For IPv4, produces three-digit zero-padded dotted-quad per + /// RFC 791 §2.3. + /// + /// + /// The address to expand. + /// The expanded string form, or if input is . +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER + [return: NotNullIfNotNull(nameof(ipAddress))] +#endif public static string ToUncompressedString(this IPAddress ipAddress) { if (ipAddress == null) @@ -199,19 +361,16 @@ public static string ToUncompressedString(this IPAddress ipAddress) return null; } - switch (ipAddress.AddressFamily) + return ipAddress.AddressFamily switch { - case AddressFamily.InterNetwork: - return IPv4ToString(); - case AddressFamily.InterNetworkV6: - return IPv6ToString(); - default: - return ipAddress.ToString(); // all else treat as to string - } + AddressFamily.InterNetwork => IPv4ToString(), + AddressFamily.InterNetworkV6 => IPv6ToString(), + _ => ipAddress.ToString(), // all else treat as to string + }; string IPv4ToString() { - var octets = ipAddress.GetAddressBytes().Select(b => $"{b:D3}"); + var octets = ipAddress.GetAddressBytes().Select(octet => $"{octet:D3}"); return string.Join(".", octets); // join padded strings with '.' character } @@ -219,13 +378,33 @@ string IPv4ToString() string IPv6ToString() { var addressBytes = ipAddress.GetAddressBytes(); +#if NET8_0_OR_GREATER + const string hexDigits = "0123456789abcdef"; + Span chars = stackalloc char[(IPAddressUtilities.IPv6HextetCount * 5) - 1]; + var pos = 0; + for (var i = 0; i < IPAddressUtilities.IPv6HextetCount; i++) + { + if (i > 0) + { + chars[pos++] = ':'; + } + var hiByte = addressBytes[i * 2]; + var loByte = addressBytes[(i * 2) + 1]; + chars[pos++] = hexDigits[hiByte >> 4]; + chars[pos++] = hexDigits[hiByte & 0x0F]; + chars[pos++] = hexDigits[loByte >> 4]; + chars[pos++] = hexDigits[loByte & 0x0F]; + } + return new string(chars); +#else var hextets = Enumerable .Range(0, IPAddressUtilities.IPv6HextetCount) - .Select(i => i * 2) - .Select(i => $"{addressBytes[i]:x2}{addressBytes[i + 1]:x2}"); + .Select(hextetIndex => hextetIndex * 2) + .Select(byteOffset => $"{addressBytes[byteOffset]:x2}{addressBytes[byteOffset + 1]:x2}"); return string.Join(":", hextets); +#endif } } diff --git a/src/Arcus/IIPAddressRange.cs b/src/Arcus/IIPAddressRange.cs index 8baf422..9316bd6 100644 --- a/src/Arcus/IIPAddressRange.cs +++ b/src/Arcus/IIPAddressRange.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Net; +using System.Net; using System.Net.Sockets; using System.Numerics; @@ -11,89 +9,101 @@ namespace Arcus /// A range must contain a head (the first address) and a tail (the last address) inclusive /// The tail should NEVER appear numerically previous to a head /// + /// + /// + /// Covers both the IPv4 32-bit address space per + /// RFC 791 §2.3 and the IPv6 128-bit address + /// space per RFC 4291 §2.1. + /// uses because the IPv6 space (2128 + /// addresses) exceeds . + /// + /// public interface IIPAddressRange : IFormattable, IEnumerable { - // TODO future versions of IIPAddressRange should not directly implement IEnumerable and instead a spawn an IEnumerable on demand + /// + /// Returns an of the values in this range, from + /// to , up to the cap defined by MaxEnumerationExponent. + /// + /// An of values in the range. + IEnumerable ToIPAddresses(); /// - /// Gets the address family of the Address Range - /// Typically Internetwork or InternetworkV6 + /// Gets the address family of the range. /// /// - /// The address family of the Address Range - /// Typically Internetwork or InternetworkV6 + /// for IPv4 ranges, + /// for IPv6 ranges. /// AddressFamily AddressFamily { get; } /// - /// Gets the length of a + /// Gets the number of addresses in this range. /// /// - /// The length of a + /// Total count of addresses from to , inclusive. + /// Uses because IPv6 ranges (2128) exceed + /// . /// + /// + /// + /// Typed as because the IPv6 address space contains 2128 + /// addresses per RFC 4291 §2.1, + /// which exceeds . + /// + /// BigInteger Length { get; } /// - /// Gets the head of a + /// Gets the first (numerically lowest) address in the range. /// - /// - /// The head of a - /// + /// The first in the range, inclusive. IPAddress Head { get; } /// - /// Gets the tail of a + /// Gets the last (numerically highest) address in the range. /// - /// - /// The tail of a - /// + /// The last in the range, inclusive. IPAddress Tail { get; } /// - /// Gets a value indicating whether if the subnet describes a single ip address + /// Gets a value indicating whether this range contains exactly one address. /// - /// - /// if the subnet describes a single ip address - /// + /// if equals . bool IsSingleIP { get; } /// - /// Gets a value indicating whether determine if IIPAddress Range is IPv4 + /// Gets a value indicating whether the range spans only IPv4 addresses. /// - /// - /// Determine if IIPAddress Range is IPv4 - /// + /// if the range uses the IPv4 address family. bool IsIPv4 { get; } /// - /// Gets a value indicating whether determine if IIPAddress Range is IPv6 + /// Gets a value indicating whether the range spans only IPv6 addresses. /// - /// - /// Determine if IIPAddress Range is IPv6 - /// + /// if the range uses the IPv6 address family. bool IsIPv6 { get; } /// - /// Try to get the length as an + /// Attempts to get the range length as an . /// - /// the length of the AddressRange if it is possible to fit into a . Otherwise -1 - /// true if the length is less than or equal to + /// When successful, the range length; otherwise -1. + /// if the length fits in an . bool TryGetLength(out int length); /// - /// Try to get length as an + /// Attempts to get the range length as an . /// - /// the length of the AddressRange if it is possible to fit into a . Otherwise -1 - /// true if the length is less than or equal to + /// When successful, the range length; otherwise -1. + /// if the length fits in a . bool TryGetLength(out long length); #region Deconstructors /// - /// Deconstruct to Head and Tail addresses + /// Deconstructs the range into its and addresses. /// - /// the head - /// the tail + /// Receives the head . + /// Receives the tail . void Deconstruct(out IPAddress head, out IPAddress tail); #endregion // end: Deconstructors @@ -101,49 +111,50 @@ public interface IIPAddressRange : IFormattable, IEnumerable #region Set Operations /// - /// determine if a contains another + /// Determines whether this range contains another range in full. /// - /// the secondary operand - /// true if the is contained within + /// The range to test. + /// if is wholly contained within this range. bool Contains(IIPAddressRange addressRange); /// - /// determine if a contains an + /// Determines whether this range contains a specific address. /// - /// the secondary operand - /// true if the is contained within + /// The address to test. + /// if is within this range. bool Contains(IPAddress address); #region Ovelap and Touches /// - /// Return if the head of this is within the range of + /// Determines whether the of this range lies within . /// - /// the secondary operand - /// if the head of this is within the range of + /// The range to test against. + /// if this range's head is contained by . bool HeadOverlappedBy(IIPAddressRange addressRange); /// - /// Return if the tail of this is within the range of + /// Determines whether the of this range lies within . /// - /// the secondary operand - /// if the tail of this is within the range of + /// The range to test against. + /// if this range's tail is contained by . bool TailOverlappedBy(IIPAddressRange addressRange); /// - /// return if this overlaps that (totally contained within, or contains either the head or the - /// tail) + /// Determines whether this range overlaps another range. /// - /// the secondary operand - /// if overlaps + /// The range to test. + /// if the two ranges share any address in common. bool Overlaps(IIPAddressRange addressRange); /// - /// if tail of one item is consecutively in order of head of other item - /// eg in sequence with no gaps in between + /// Determines whether this range touches another range without overlap. /// - /// the secondary operand - /// if is mathematically consecutive + /// The range to test. + /// + /// if the ranges are consecutive with no gap between them + /// (e.g., this range's tail immediately precedes the other range's head). + /// bool Touches(IIPAddressRange addressRange); #endregion // end: Ovelap and Touches @@ -153,27 +164,27 @@ public interface IIPAddressRange : IFormattable, IEnumerable #region Contains Any/All Public/Private Addresses /// - /// Determines if the range contains any private addresses + /// Determines whether the range contains any private (RFC 1918) addresses. /// - /// if, and only if, the range contains any private addresses + /// if any address in this range is a private address. bool ContainsAnyPrivateAddresses(); /// - /// Determines if the range contains all private addresses + /// Determines whether every address in the range is a private (RFC 1918) address. /// - /// if, and only if, the range contains all private addresses + /// if all addresses in this range are private addresses. bool ContainsAllPrivateAddresses(); /// - /// Determines if the range contains any public addresses + /// Determines whether the range contains any public (non-private) addresses. /// - /// if, and only if, the range contains any public addresses + /// if any address in this range is a public address. bool ContainsAnyPublicAddresses(); /// - /// Determines if the range contains all public addresses + /// Determines whether every address in the range is a public (non-private) address. /// - /// if, and only if, the range contains all public addresses + /// if all addresses in this range are public addresses. bool ContainsAllPublicAddresses(); #endregion end: Contains Any/All Public/Private Addresses diff --git a/src/Arcus/IPAddressRange.Factory.cs b/src/Arcus/IPAddressRange.Factory.cs new file mode 100644 index 0000000..fe6f30a --- /dev/null +++ b/src/Arcus/IPAddressRange.Factory.cs @@ -0,0 +1,275 @@ +using Arcus.Math; +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif + +namespace Arcus +{ + /// + /// static factory methods + /// + public partial class IPAddressRange + { + #region static methods + + /// + /// Attempts to collapse overlapping or adjacent ranges into the minimum number of contiguous ranges. + /// + /// The ranges to collapse. + /// The collapsed ranges. + /// The maximum enumeration exponent (0-128). + /// if the ranges were collapsed successfully. + public static bool TryCollapseAll( + IEnumerable ranges, + out IEnumerable result, + int maxEnumerationExponent = DefaultMaxEnumerationExponent + ) + { + if (ranges is null) + { + result = []; + return true; + } + + var rangeList = ranges.ToList(); + + // item null check + if (rangeList.Contains(null)) + { + result = []; + return false; + } + + // no ranges provided + if (rangeList.Count == 0) + { + result = []; + return true; // assume success + } + + // all families don't match match + if (rangeList.Any(range => range.AddressFamily != rangeList[0].AddressFamily)) + { + result = []; + return false; + } + + // sort range list, has to be done post validation check, as invalid cannot be sorted + rangeList.Sort(); + + var resultList = new List + { + rangeList[0], // start with first item in the sorted list of ranges + }; + + // iterate over items in range list, no need to iterate over first as it is included by default + foreach (var range in rangeList.Skip(1)) + { + var last = resultList[resultList.Count - 1]; // take end of result to process over (is first in first iteration) + + // can be assumed that assume that the range head is greater than or equal to last head because everything is ordered + + // if TryMerge succeeded then overlap exists, merge overlap and re-assign to end of results + if (TryMerge(last, range, out var merge, maxEnumerationExponent)) + { + resultList[resultList.Count - 1] = merge; // overwrite with merged values + continue; + } + + // could not merge, simply add to end and iterate to next + resultList.Add(range); + } + + result = resultList; + return true; + } + + /// + /// Rebuild the initial range as an of ranges excluding the excluded ranges. + /// Excluded ranges are expected to each be sub-ranges of the initial range. + /// + /// the initial to exclude from + /// + /// the various to exclude from the + /// + /// + /// the resulting + /// the maximum enumeration exponent + /// true on success + /// + /// + /// A return value of indicates an error condition (null input, address-family mismatch, + /// or null elements in ), not an empty result set. A + /// return with an empty means the exclusions cover the entire + /// . + /// + /// + /// Family-maximum boundary: when an exclusion ends at the family maximum address + /// (e.g., 255.255.255.255 for IPv4 or ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff for IPv6), + /// no trailing segment can be produced after it. The method returns with only the + /// leading segment in (which may itself be empty if the exclusion also covers + /// the start of the range). + /// + /// + /// Family-minimum boundary: when an exclusion starts at the family minimum address + /// (e.g., 0.0.0.0 for IPv4 or :: for IPv6), no leading segment can be produced before it. + /// The method returns with only the trailing segment in + /// (which may itself be empty if the exclusion also covers the end of the range). + /// + /// + /// A non-overlapping exclusion in causes the method + /// to return rather than throwing. + /// + /// + public static bool TryExcludeAll( + IPAddressRange initialRange, + IEnumerable excludedRanges, + out IEnumerable result, + int maxEnumerationExponent = DefaultMaxEnumerationExponent + ) + { + if (initialRange is null || excludedRanges is null) + { + result = []; + return false; + } + + // Materialize once: copying ensures we do not sort the caller's original collection. + var excludedList = excludedRanges.ToList(); + + if (excludedList.Any(range => range is null || range.AddressFamily != initialRange.AddressFamily)) + { + result = []; + return false; + } + + if (excludedList.Count == 0) + { + result = [new IPAddressRange(initialRange.Head, initialRange.Tail, maxEnumerationExponent)]; + return true; + } + + var resultList = new List { new(initialRange.Head, initialRange.Tail, maxEnumerationExponent) }; + + // Exclusions are processed in ascending order. Because each exclusion is to the right of + // all previous ones, it can only ever affect the rightmost not-yet-trimmed segment - + // earlier segments are entirely left of the current exclusion and are permanently settled. + excludedList.Sort(); + foreach (var exclusion in excludedList) + { + var lastIndex = resultList.Count - 1; + var (error, done, segments) = ApplyExclusion(resultList[lastIndex], exclusion); + + if (error) + { + result = []; + return false; + } + + resultList.RemoveAt(lastIndex); + resultList.AddRange(segments); + + // done=true means the exclusion consumed the segment's tail boundary. Any remaining + // exclusions start at or after the current one's head, so they cannot produce + // additional output - short-circuit to avoid redundant work. + if (done) + { + break; + } + } + + result = resultList; + return true; + + (bool Error, bool Done, IPAddressRange[] Segments) ApplyExclusion(IPAddressRange segment, IPAddressRange exclusion) + { + // exclusion covers the entire segment + if (exclusion.Contains(segment)) + { + return (false, true, []); + } + + // exclusion covers the tail; no trailing portion can exist after this point + if (exclusion.Contains(segment.Tail)) + { + return (false, true, LeadingFragment(segment, exclusion)); + } + + // exclusion covers the head; advance the segment's head past the exclusion's tail + if (exclusion.Contains(segment.Head)) + { + var trailing = TrailingFragment(segment, exclusion); + return (false, trailing.Length == 0, trailing); + } + + // Non-overlapping exclusion: signal an error to the caller instead of throwing. + // Per the documented contract, an exclusion that does not intersect the initial range + // causes TryExcludeAll to return false rather than failing at runtime. + if (!segment.Overlaps(exclusion)) + { + return (true, false, []); + } + + // Strictly interior exclusion: split into leading and trailing pieces. + // The collection-expression spread is bounded: at most one leading and one trailing + // fragment (<=2 elements), so there is no unbounded allocation concern here. + return (false, false, [.. LeadingFragment(segment, exclusion), .. TrailingFragment(segment, exclusion)]); + } + + // The boundary guards IsAtMin() and IsAtMax() below must remain synchronized with + // IPAddressMath.Increment's underflow/overflow detection: when the exclusion's head + // sits at the family minimum (or its tail sits at the family maximum), the + // Increment(-1)/Increment() step would otherwise underflow/overflow and produce + // an out-of-range address. The guards short-circuit to an empty fragment instead. + IPAddressRange[] LeadingFragment(IPAddressRange segment, IPAddressRange exclusion) => + exclusion.Head.IsAtMin() + ? [] + : [new IPAddressRange(segment.Head, exclusion.Head.Increment(-1), maxEnumerationExponent)]; + + IPAddressRange[] TrailingFragment(IPAddressRange segment, IPAddressRange exclusion) => + exclusion.Tail.IsAtMax() + ? [] + : [new IPAddressRange(exclusion.Tail.Increment(), segment.Tail, maxEnumerationExponent)]; + } + + /// + /// Attempts to merge two touching or overlapping address ranges into a single range. + /// + /// The left range. + /// The right range. + /// The merged on success. + /// The maximum enumeration exponent (0-128). + /// if the ranges were merged successfully. + public static bool TryMerge( + IPAddressRange left, + IPAddressRange right, +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER + [NotNullWhen(true)] +#endif + out IPAddressRange mergedRange, + int maxEnumerationExponent = DefaultMaxEnumerationExponent + ) + { + if (left is null || right is null || left.AddressFamily != right.AddressFamily) + { + mergedRange = null; + return false; + } + + // overlap or touch occurs + if (left.Overlaps(right) || left.Touches(right)) + { + var newHead = IPAddressMath.Min(left.Head, right.Head); + var newTail = IPAddressMath.Max(left.Tail, right.Tail); + mergedRange = new IPAddressRange(newHead, newTail, maxEnumerationExponent); + + return true; + } + + mergedRange = null; + return false; + } + + #endregion // end: static methods + } +} diff --git a/src/Arcus/IPAddressRange.IComparable.cs b/src/Arcus/IPAddressRange.IComparable.cs new file mode 100644 index 0000000..707c73b --- /dev/null +++ b/src/Arcus/IPAddressRange.IComparable.cs @@ -0,0 +1,61 @@ +using Arcus.Comparers; +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif + +namespace Arcus +{ + /// + /// implementation of + /// + public partial class IPAddressRange : IComparable, IComparable + { + #region From Interface IComparable + + /// + public int CompareTo( +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER + [AllowNull] +#endif + object obj) + { + if (obj is null) + { + return 1; + } + + if (obj is IPAddressRange other) + { + return CompareTo(other); + } + + throw new ArgumentException("Object is not an IPAddressRange"); + } + + #endregion + + #region From Interface IComparable + + /// + /// + /// Comparison delegates to . Direct + /// head-address comparison could reduce allocations + /// if performance becomes a concern. + /// + public int CompareTo( +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER + [AllowNull] +#endif + IPAddressRange other) + { + if (other is null) + { + return 1; + } + + return DefaultIIPAddressRangeComparer.Instance.Compare(this, other); + } + + #endregion + } +} diff --git a/src/Arcus/IPAddressRange.IEquatable.cs b/src/Arcus/IPAddressRange.IEquatable.cs new file mode 100644 index 0000000..2a1b6a3 --- /dev/null +++ b/src/Arcus/IPAddressRange.IEquatable.cs @@ -0,0 +1,42 @@ +namespace Arcus +{ + /// + /// implementation of + /// + public partial class IPAddressRange : IEquatable + { + #region From Interface IEquatable + + /// + public virtual bool Equals(IPAddressRange other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return this.Head.Equals(other.Head) && this.Tail.Equals(other.Tail); + } + + #endregion + + /// + public override bool Equals(object obj) + { + if (obj is IPAddressRange other) + { + return Equals(other); + } + + return false; + } + + /// + public override int GetHashCode() => base.GetHashCode(); + } +} diff --git a/src/Arcus/IPAddressRange.ISerializable.cs b/src/Arcus/IPAddressRange.ISerializable.cs new file mode 100644 index 0000000..3b6a249 --- /dev/null +++ b/src/Arcus/IPAddressRange.ISerializable.cs @@ -0,0 +1,31 @@ +#if NETSTANDARD2_0 +using System.Runtime.Serialization; + +namespace Arcus +{ + /// + /// implementation of + /// + public partial class IPAddressRange : ISerializable + { + #region From Interface ISerializable + + /// + /// is + public virtual void GetObjectData(SerializationInfo info, StreamingContext context) + { + if (info is null) + { + throw new ArgumentNullException(nameof(info)); + } + + info.AddValue(nameof(Head), Head.GetAddressBytes()); + info.AddValue(nameof(Tail), Tail.GetAddressBytes()); + info.AddValue("SerVersion", 2); + info.AddValue(nameof(this.MaxEnumerationExponent), this.MaxEnumerationExponent); + } + + #endregion + } +} +#endif diff --git a/src/Arcus/IPAddressRange.Operators.cs b/src/Arcus/IPAddressRange.Operators.cs new file mode 100644 index 0000000..1394f11 --- /dev/null +++ b/src/Arcus/IPAddressRange.Operators.cs @@ -0,0 +1,89 @@ +namespace Arcus +{ + /// + /// operators + /// + public partial class IPAddressRange + { + #region operators + + /// + /// Determines whether two instances are equal. + /// + /// The first instance. + /// The second instance. + /// if the instances are equal; otherwise, . + public static bool operator ==(IPAddressRange left, IPAddressRange right) + { + if (ReferenceEquals(left, right)) + { + return true; + } + + if (left is null || right is null) + { + return false; + } + + return left.Equals(right); + } + + /// + /// Determines whether two instances are not equal. + /// + /// The first instance. + /// The second instance. + /// if the instances are not equal; otherwise, . + public static bool operator !=(IPAddressRange left, IPAddressRange right) => !(left == right); + + /// + /// Determines whether one is strictly less than another. + /// + /// The first instance. + /// The second instance. + /// if is less than . + public static bool operator <(IPAddressRange left, IPAddressRange right) + { + if (left is null) + { + return right is not null; + } + + return left.CompareTo(right) < 0; + } + + /// + /// Determines whether one is strictly greater than another. + /// + /// The first instance. + /// The second instance. + /// if is greater than . + public static bool operator >(IPAddressRange left, IPAddressRange right) + { + if (left is null) + { + return false; + } + + return left.CompareTo(right) > 0; + } + + /// + /// Determines whether one is less than or equal to another. + /// + /// The first instance. + /// The second instance. + /// if is less than or equal to . + public static bool operator <=(IPAddressRange left, IPAddressRange right) => left < right || left == right; + + /// + /// Determines whether one is greater than or equal to another. + /// + /// The first instance. + /// The second instance. + /// if is greater than or equal to . + public static bool operator >=(IPAddressRange left, IPAddressRange right) => left > right || left == right; + + #endregion operators + } +} diff --git a/src/Arcus/IPAddressRange.cs b/src/Arcus/IPAddressRange.cs index 5729660..0a9df74 100644 --- a/src/Arcus/IPAddressRange.cs +++ b/src/Arcus/IPAddressRange.cs @@ -1,204 +1,36 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using System.Diagnostics; using System.Net; +#if NETSTANDARD2_0 using System.Runtime.Serialization; -using Arcus.Comparers; -using Arcus.Math; +#endif namespace Arcus { /// - /// A basic implementation of a IIPAddressRange us to represent an inclusive range of arbitrary IP Addresses of the + /// A basic implementation of a IIPAddressRange used to represent an inclusive range of arbitrary IP Addresses of the /// same address family /// - [Serializable] - public class IPAddressRange - : AbstractIPAddressRange, -#if NETSTANDARD2_0 - ISerializable, -#endif - IEquatable, - IComparable, - IComparable + /// + /// + /// Represents an arbitrary inclusive range of addresses within the IPv4 32-bit address space + /// (RFC 791 §2.3) or the IPv6 128-bit address + /// space (RFC 4291 §2.1). Unlike + /// , ranges are not constrained to power-of-two boundaries or valid network addresses. + /// + /// + [DebuggerDisplay("{Head} - {Tail} ({Length})")] + [System.Serializable] + public partial class IPAddressRange : AbstractIPAddressRange { -#if NETSTANDARD2_0 - #region From Interface ISerializable - - /// - /// is - public void GetObjectData(SerializationInfo info, StreamingContext context) - { - if (info is null) - { - throw new ArgumentNullException(nameof(info)); - } - - info.AddValue(nameof(Head), Head.GetAddressBytes()); - info.AddValue(nameof(Tail), Tail.GetAddressBytes()); - } - - #endregion -#endif - - #region From Interface IComparable - - /// - public int CompareTo(object obj) - { - if (obj is null) - { - return 1; - } - - if (obj is IPAddressRange other) - { - return CompareTo(other); - } - - throw new ArgumentException("Object is not an IPAddressRange"); - } - - #endregion - - #region From Interface IComparable - - /// - public int CompareTo(IPAddressRange other) - { - if (other is null) - { - return 1; - } - - return DefaultIIPAddressRangeComparer.Instance.Compare(this, other); - } - - #endregion - - #region From Interface IEquatable - - /// - public virtual bool Equals(IPAddressRange other) - { - if (other is null) - { - return false; - } - - if (ReferenceEquals(this, other)) - { - return true; - } - - return DefaultIIPAddressRangeComparer.Instance.Compare(this, other) == 0; - } - - #endregion - - /// - public override bool Equals(object obj) - { - if (obj is IPAddressRange other) - { - return Equals(other); - } - - return false; - } - - /// - public override int GetHashCode() => HashCode.Combine(Head, Tail); - - #region operators - - /// - /// Determines whether two instances are equal. - /// - /// The first instance. - /// The second instance. - /// true if both instances are equal; otherwise, false. - public static bool operator ==(IPAddressRange left, IPAddressRange right) - { - if (ReferenceEquals(left, right)) - { - return true; - } - - if (left is null || right is null) - { - return false; - } - - return left.Equals(right); - } - - /// - /// Determines whether two instances are not equal. - /// - /// The first instance. - /// The second instance. - /// true if the instances are not equal; otherwise, false. - public static bool operator !=(IPAddressRange left, IPAddressRange right) => !(left == right); - - /// - /// Compares two instances to determine if the first is less than the second. - /// - /// The first instance. - /// The second instance. - /// true if the first instance is less than the second; otherwise, false. - public static bool operator <(IPAddressRange left, IPAddressRange right) - { - if (left is null) - { - return !(right is null); // null is less than any non-null instance - } - - return left.CompareTo(right) < 0; - } - - /// - /// Compares two instances to determine if the first is greater than the second. - /// - /// The first instance. - /// The second instance. - /// true if the first instance is greater than the second; otherwise, false. - public static bool operator >(IPAddressRange left, IPAddressRange right) - { - if (left is null) - { - return false; - } - - return left.CompareTo(right) > 0; - } - - /// - /// Compares two instances to determine if the first is less than or equal to the second. - /// - /// The first instance. - /// The second instance. - /// true if the first instance is less than or equal to the second; otherwise, false. - public static bool operator <=(IPAddressRange left, IPAddressRange right) => left < right || left == right; - - /// - /// Compares two instances to determine if the first is greater than or equal to the second. - /// - /// The first instance. - /// The second instance. - /// true if the first instance is greater than or equal to the second; otherwise, false. - public static bool operator >=(IPAddressRange left, IPAddressRange right) => left > right || left == right; - - #endregion operators - #region Ctor /// /// Initializes a new instance of the class. /// /// the - public IPAddressRange(IPAddress address) - : base(address, address) + /// the maximum enumeration exponent (0-128, default 12) + public IPAddressRange(IPAddress address, int maxEnumerationExponent = DefaultMaxEnumerationExponent) + : base(address, address, maxEnumerationExponent) { // nothing more to do } @@ -208,219 +40,42 @@ public IPAddressRange(IPAddress address) /// /// head /// tail - public IPAddressRange(IPAddress head, IPAddress tail) - : base(head, tail) + /// the maximum enumeration exponent (0-128, default 12) + public IPAddressRange(IPAddress head, IPAddress tail, int maxEnumerationExponent = DefaultMaxEnumerationExponent) + : base(head, tail, maxEnumerationExponent) { // nothing more to do } +#if NETSTANDARD2_0 /// Initializes a new instance of the class. /// serialization info /// serialization context - /// is + /// is protected IPAddressRange(SerializationInfo info, StreamingContext context) : this( new IPAddress( - (byte[])(info ?? throw new ArgumentNullException(nameof(info))).GetValue(nameof(Head), typeof(byte[])) + (byte[]) + (info ?? throw new System.ArgumentNullException(nameof(info))).GetValue(nameof(Head), typeof(byte[])) ), - new IPAddress((byte[])info.GetValue(nameof(Tail), typeof(byte[]))) + new IPAddress((byte[])info.GetValue(nameof(Tail), typeof(byte[]))), + DeserializeMaxEnumerationExponent(info) ) { } - #endregion // end: Ctor - - #region static methods - - /// - /// Attempt collapse the given input of ranges into fewer ranges thus optimizing - /// Ranges that overlap, or butt against each other may be collapsed into a single range - /// - /// ranges to collapse - /// resulting ranges post collapse - /// true on success - public static bool TryCollapseAll(IEnumerable ranges, out IEnumerable result) + private static int DeserializeMaxEnumerationExponent(SerializationInfo info) { - var rangeList = (ranges ?? Enumerable.Empty()).ToList(); - - // item null check - if (rangeList.Contains(null)) + try { - result = Enumerable.Empty(); - return false; + return info.GetInt32(nameof(MaxEnumerationExponent)); } - - // no ranges provided - if (!rangeList.Any()) // no ranges + catch (SerializationException) { - result = Enumerable.Empty(); - return true; // assume success + // Old format (version 1): fall back to default exponent + return DefaultMaxEnumerationExponent; } - - // all families don't match match - if (rangeList.Any(r => r.AddressFamily != rangeList[0].AddressFamily)) - { - result = Enumerable.Empty(); - return false; - } - - // sort range list, has to be done post validation check, as invalid cannot be sorted - rangeList = rangeList.OrderBy(r => r).ToList(); - - var resultList = new List - { - rangeList[0], // start with first item in the sorted list of ranges - }; - - // iterate over items in range list, no need to iterate over first as it is included by default - foreach (var range in rangeList.Skip(1)) - { - var last = resultList[resultList.Count - 1]; // take end of result to process over (is first in first iteration) - - // can be assumed that assume that the range head is greater than or equal to last head because everything is ordered - - // if TryMerge succeeded then overlap exists, merge overlap and re-assign to end of results - if (TryMerge(last, range, out var merge)) - { - resultList[resultList.Count - 1] = merge; // overwrite with merged values - continue; - } - - // could not merge, simply add to end and iterate to next - resultList.Add(range); - } - - result = resultList; - return true; - } - - /// - /// Rebuild the initial range as an of ranges excluding the excluded ranges - /// excluded ranges are expected to each be sub ranges of the initial range - /// an attempt will be made to TryCollapseAll the excluded - /// - /// the initial to exclude from - /// - /// the various to exclude from the - /// - /// - /// the resulting - /// unexpected invalid operation. - /// true on success - public static bool TryExcludeAll( - IPAddressRange initialRange, - IEnumerable excludedRanges, - out IEnumerable result - ) - { - // TODO the logical flow here is confusing, see if it can be cleaned up a bit - - if (initialRange is null || excludedRanges is null) - { - result = Enumerable.Empty(); - return false; - } - - var excludedRangesList = excludedRanges as IList ?? excludedRanges.ToList(); - - // item null check - if (excludedRangesList.Any(r => r is null)) - { - result = Enumerable.Empty(); - return false; - } - - // no rangers to exclude, out copy of original - if (!excludedRangesList.Any()) - { - result = new List { new IPAddressRange(initialRange.Head, initialRange.Tail) }; - - return true; - } - - // all families don't match match - if (excludedRangesList.Any(r => r.AddressFamily != initialRange.AddressFamily)) - { - result = Enumerable.Empty(); - return false; - } - - // results is initialized with a *copy* of initialRange - var resultList = new List { new IPAddressRange(initialRange.Head, initialRange.Tail) }; - - foreach (var exclusion in excludedRangesList) - { - var last = resultList[resultList.Count - 1]; - var lastIndex = resultList.Count - 1; - - if (exclusion.Contains(last)) - { - resultList.RemoveAt(lastIndex); - break; // can't loop any more - } - - if (exclusion.Contains(initialRange.Tail)) - { - var head = resultList[lastIndex].Head; - var tail = exclusion.Head.Increment(-1); - resultList[lastIndex] = new IPAddressRange(head, tail); - continue; // can't loop any more - } - - // exclusion contains head - if (exclusion.Contains(last.Head)) - { - // push head one point beyond tail of exclusion - var head = exclusion.Tail.Increment(); - var tail = resultList[lastIndex].Tail; - resultList[lastIndex] = new IPAddressRange(head, tail); - continue; // next iteration - } - - // exclusion is within last, carve exclusion to two pieces - if (!last.Overlaps(exclusion)) - { - throw new InvalidOperationException("An unexpected overlap check operation occurred"); // should not have made it here - } - - resultList[lastIndex] = new IPAddressRange(resultList[lastIndex].Head, exclusion.Head.Increment(-1)); - - // carve out a piece at starting at exclusion head, ending at exclusion tail - // thus meaning one less than head at previous, and one more at head at next - resultList.Add(new IPAddressRange(exclusion.Tail.Increment(), initialRange.Tail)); - } - - result = resultList; - return true; - } - - /// - /// Merge two touching or overlapping address ranges - /// - /// the left operand - /// the right operand - /// the resulting - /// true on success - public static bool TryMerge(IPAddressRange left, IPAddressRange right, out IPAddressRange mergedRange) - { - if (left is null || right is null || left.AddressFamily != right.AddressFamily) - { - mergedRange = null; - return false; - } - - // overlap or touch occurs - if (left.Overlaps(right) || left.Touches(right)) - { - var newHead = IPAddressMath.Min(left.Head, right.Head); - var newTail = IPAddressMath.Max(left.Tail, right.Tail); - mergedRange = new IPAddressRange(newHead, newTail); - - return true; - } - - mergedRange = null; - return false; } +#endif - #endregion // end: static methods + #endregion // end: Ctor } } diff --git a/src/Arcus/MacAddress.cs b/src/Arcus/MacAddress.cs deleted file mode 100644 index 3cbf0d8..0000000 --- a/src/Arcus/MacAddress.cs +++ /dev/null @@ -1,632 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Runtime.Serialization; -using System.Text.RegularExpressions; -using Gulliver; - -namespace Arcus -{ - /// Representation of a 48-bit MAC Address (EUI-48 & MAC-48) - /// - /// - /// see IEEE "Guidelines for Use of Extended Unique Identifier (EUI), Organizationally Unique Identifier (OUI), and - /// Company ID (CID)": - /// https://standards.ieee.org/content/dam/ieee-standards/standards/web/documents/tutorials/eui.pdf - /// - /// - [Obsolete( - "Candidate for removal in future major version. This, or something like it, may be more appropriate in a separate package" - )] - [Serializable] - public class MacAddress : -#if NETSTANDARD2_0 - ISerializable, -#endif - IEquatable, IComparable, IComparable, IFormattable - { - /// - /// MAC Address Regular Expression pattern for matching: - /// - /// - /// - /// IEEE 802 format for printing EUI-48 & MAC-48 addresses in six groups of two hexadecimal digits, - /// separated by a dash ("-") - /// - /// - /// - /// six groups of two hexadecimal digits separated by colons (":") - /// - /// - /// six groups of two hexadecimal digits separated by spaces (" ") - /// - /// - /// 12 hexadecimal digits with no separation - /// - /// - /// Cisco three groups of four hexadecimal digits separated by dots (".") - /// - /// - /// - /// - /// this regular expression pattern is expected to be used in matches ignoring case - /// - public const string AllFormatMacAddressPattern = - @"^(?:[0-9A-F]{2}([-: ]?))(?:[0-9A-F]{2}\1){4}[0-9A-F]{2}$|^(?:[0-9A-F]{4}\.){2}[0-9A-F]{4}$|^(?:[0-9A-F]{3}\.){3}[0-9A-F]{3}$"; - - /// - /// MAC Address Regular Expression pattern for matching the "common" six groups of two uppercase hexadecimal digits - /// separated by colons(":") - /// - /// - /// this regular expression pattern is expected to be used in matches ignoring case - /// - public const string CommonFormatMacAddressPattern = @"^(?:[\dA-F]{2}:){5}[\dA-F]{2}$"; - - /// A MAC Address that represents the default or case. Equal to FF:FF:FF:FF:FF:FF. - public static readonly MacAddress DefaultMacAddress = new MacAddress(Enumerable.Repeat((byte)0xFF, 6)); - - /// - /// MAC Address Regular Expression matching - /// - /// - /// - /// IEEE 802 format for printing EUI-48 & MAC-48 addresses in six groups of two hexadecimal digits, - /// separated by a dash ("-") - /// - /// - /// - /// six groups of two hexadecimal digits separated by colons (":") - /// - /// - /// six groups of two hexadecimal digits separated by spaces (" ") - /// - /// - /// 12 hexadecimal digits with no separation - /// - /// - /// Cisco three groups of four hexadecimal digits separated by dots (".") - /// - /// - /// - /// - /// see for pattern. - /// - public static readonly Regex AllFormatMacAddressRegularExpression = new Regex( - AllFormatMacAddressPattern, - RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase - ); - - /// - /// MAC Address Regular Expression for matching the "common" six groups of two hexadecimal digits separated by colons - /// (":"). uses - /// - /// - /// see for pattern. - /// - public static readonly Regex CommonFormatMacAddressRegularExpression = new Regex( - CommonFormatMacAddressPattern, - RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase - ); - - /// The inner byte representation fo the MAC Address - private readonly byte[] _address; - - /// Gets a value indicating whether checks if the MAC Address is unicast - /// returns if the MAC Address is unicast - public bool IsUnicast => (byte)(this._address[0] & 1) == 0; - - /// Gets a value indicating whether checks if the MAC Address is multicast - /// returns if the MAC Address is multicast - public bool IsMulticast => (byte)(this._address[0] & 1) != 0; - - /// Gets a value indicating whether checks if the MAC Address is globally unique (OUI enforced) - /// returns if the MAC Address is globally unique - public bool IsGloballyUnique => (byte)(this._address[0] & (1 << 1)) == 0; - - /// Gets a value indicating whether checks if the MAC Address is locally administered - /// returns if the MAC Address is locally administered - public bool IsLocallyAdministered => (byte)(this._address[0] & (1 << 1)) != 0; - - /// Gets a value indicating whether checks if the MAC Address is the EUI-48 default - /// returns if all bits of the MAC Address are set - public bool IsDefault => this.Equals(DefaultMacAddress); - - /// Gets a value indicating whether checks if the MAC Address is "unusable" - /// returns if all the OUI bits of the MAC Address are unset - public bool IsUnusable => this._address.Take(3).Any(b => b != 0); - - #region From Interface IComparable - - /// - public int CompareTo(object obj) - { - if (ReferenceEquals(null, obj)) - { - return 1; - } - else if (ReferenceEquals(this, obj)) - { - return 0; - } - else if (obj is MacAddress other) - { - return this.CompareTo(other); - } - - throw new ArgumentException($"Object must be of type {nameof(MacAddress)}"); - } - - #endregion - - #region From Interface IComparable - - /// - public int CompareTo(MacAddress other) - { - return ReferenceEquals(null, other) ? 1 : ByteArrayUtils.CompareUnsignedBigEndian(this._address, other._address); - } - - #endregion - - #region From Interface IEquatable - - /// - public bool Equals(MacAddress other) - { - return !ReferenceEquals(null, other) - && ( - ReferenceEquals(this, other) || ByteArrayUtils.CompareUnsignedBigEndian(this._address, other._address) == 0 - ); - } - - #endregion - - #region From Interface IFormattable - - /// - /// - /// Express as a string - /// The following formats are provided - /// - /// - /// g, empty string, or - /// - /// General format of uppercase hexadecimal encoded bytes separated by colons. eg - /// AA:BB:CC:DD:EE:FF - /// - /// - /// - /// H - /// formats as contiguous upper case hexadecimal digits. eg AABBCCDDEEFF - /// - /// - /// h - /// formats as contiguous lower case hexadecimal digits. eg aabbccddeeff - /// - /// - /// c - /// - /// Cisco three groups of four uppercase hexadecimal digits separated by dots format. eg - /// AAAA.BBBB.CCCC - /// - /// - /// - /// s - /// hexadecimal encoded bytes separated by a space character. eg AA BB CC DD EE FF - /// - /// - /// d - /// hexadecimal encoded bytes separated by a dash character. eg AA-BB-CC-DD-EE-FF - /// - /// - /// i formatted as a big-endian integer value - /// - /// - /// - /// the format specifier - /// the format provider - /// a byte array represented as a string - /// Invalid base conversion. - /// provided format is not supported. - public string ToString(string format, IFormatProvider formatProvider) - { - if (formatProvider == null) - { - formatProvider = CultureInfo.InvariantCulture; - } - - switch (format?.Trim()) - { - case null: // general formats - case "": - case "g": - return DelimitedBaseConverter(this._address, 16, ":").ToUpperInvariant(); - case "x": // lower hexadecimal - return DelimitedBaseConverter(this._address, 16); - case "X": // upper hexadecimal contiguous - return DelimitedBaseConverter(this._address, 16).ToUpperInvariant(); - case "c": // Cisco - return AsCisco(); - case "s": // space delimited - return DelimitedBaseConverter(this._address, 16, " ").ToUpperInvariant(); - case "i": // integer value - return this._address.ToString("I", formatProvider); - case "d": // dash delimited - return DelimitedBaseConverter(this._address, 16, "-").ToUpperInvariant(); - default: - throw new FormatException($"The \"{format}\" format string is not supported."); - } - - string AsCisco() - { - return string.Join( - ".", - Enumerable - .Range(0, 3) - .Select(i => - { - var j = i * 2; - return $"{this._address[j]:X2}{this._address[j + 1]:X2}"; - }) - ); - } - - string DelimitedBaseConverter( - byte[] input, - int @base, - string delimiter = "", - int? paddingWidth = null, - char paddingChar = '0' - ) - { - if (!new[] { 2, 8, 10, 16 }.Contains(@base)) - { - throw new ArgumentException( - $"{nameof(@base)} must be 2, 8, 10, or 16 (binary, octal, decimal, or hexadecimal respectively)", - nameof(@base) - ); - } - - if (paddingWidth.HasValue && paddingWidth.Value < 0) - { - throw new ArgumentException($"{nameof(paddingWidth)} may not be negative", nameof(paddingWidth)); - } - - // use padding width if defined, otherwise use a width of 8 for binary, 3 for octal and decimal, and 2 for hexadecimal - var width = - paddingWidth - ?? ( - @base == 2 ? 8 - : @base == 8 || @base == 10 ? 3 - : 2 - ); - - return string.Join( - delimiter ?? string.Empty, - input.Select(b => Convert.ToString(b, @base).PadLeft(width, paddingChar)) - ); - } - } - - #endregion - -#if NETSTANDARD2_0 - #region From Interface ISerializable - - /// - /// is - public void GetObjectData(SerializationInfo info, StreamingContext context) - { - if (info == null) - { - throw new ArgumentNullException(nameof(info)); - } - - info.AddValue(nameof(this._address), this._address); - } - - #endregion -#endif - - /// Gets a copy of the underlying big-endian bytes of the MAC Address - /// The bytes of the MAC Address - public byte[] GetAddressBytes() - { - return this._address.ToArray(); - } - - /// Gets the Organizationally Unique Identifier (OUI) of the address - /// A copy of the first 3-bytes (24-bits) of the MAC Address - public byte[] GetOuiBytes() - { - return this._address.Take(3).ToArray(); - } - - /// Gets the Company ID (CID) of the address - /// A copy of the last 3-bytes (24-bits) of the MAC Address - public byte[] GetCidBytes() - { - return this._address.Skip(3).Take(3).ToArray(); - } - - /// - public override string ToString() - { - return this.ToString("g", CultureInfo.InvariantCulture); - } - - /// - public override bool Equals(object obj) - { - return !ReferenceEquals(null, obj) - && (ReferenceEquals(this, obj) || (obj.GetType() == GetType() && this.Equals((MacAddress)obj))); - } - - /// - public override int GetHashCode() => - HashCode.Combine(_address[0], _address[1], _address[2], _address[3], _address[4], _address[5]); - - #region Ctors - - /// Initializes a new instance of the class. - /// serialization info - /// serialization context - /// is - protected MacAddress(SerializationInfo info, StreamingContext context) - : this((byte[])(info ?? throw new ArgumentNullException(nameof(info))).GetValue(nameof(_address), typeof(byte[]))) - { } - - /// Initializes a new instance of the class. - /// a collection of bytes to be copied as the byte of the MAC Address - /// must be exactly 6 bytes long - /// is - public MacAddress(IEnumerable bytes) - { - if (bytes is null) - { - throw new ArgumentNullException(nameof(bytes)); - } - - var address = bytes.ToArray(); // explicit copy - - if (address.Length != 6) - { - throw new ArgumentException("must be exactly 6 bytes long", nameof(bytes)); - } - - this._address = address; - } - - #endregion end: Ctors - - #region factory - - /// Attempt to extract hex characters from string and attempt to identify exactly 6 hextets to make a MAC Address - /// the string input - /// a mac address - /// could not create 6 hextets - /// A time-out occurred - private static MacAddress ExtractHextetsAndCreateMacAddress(string input) - { - // match and gather all hex characters - const string hexCharactersPattern = @"([\dA-F]+)+"; - var matchValues = new Regex( - hexCharactersPattern, - RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase - ); - - // remove all non hexadecimal characters from the input, upper case the result - var matches = matchValues.Matches(input).OfType().Select(match => match.Value); - - var join = string.Concat(matches); - - if (join.Length != 12) - { - throw new ArgumentException("expecting 6 hextets", nameof(input)); - } - - var bytes = Enumerable - .Range(0, 6) - .Select(i => join.Substring(2 * i, 2)) - .Select(sb => byte.Parse(sb, NumberStyles.HexNumber, CultureInfo.InvariantCulture)); - - return new MacAddress(bytes); - } - - #region Parse - - #region Parse string - - /// - /// Parse a string input, given one of the following formats, in order to create a MAC Address - /// - /// - /// - /// IEEE 802 format for printing EUI-48 & MAC-48 addresses in six groups of two hexadecimal digits, - /// separated by a dash ("-") - /// - /// - /// - /// six groups of two hexadecimal digits separated by colons (":") - /// - /// - /// six groups of two hexadecimal digits separated by spaces (" ") - /// - /// - /// 12 hexadecimal digits with no separation - /// - /// - /// Cisco three groups of four hexadecimal digits separated by dots (".") - /// - /// - /// - /// the input to parse - /// a - /// is or whitespace. - /// A time-out occurred - public static MacAddress Parse(string input) - { - if (string.IsNullOrWhiteSpace(input)) - { - throw new ArgumentException("Value cannot be null or whitespace.", nameof(input)); - } - - if (!AllFormatMacAddressRegularExpression.IsMatch(input)) - { - throw new ArgumentException("does not match known MAC Address formats", nameof(input)); - } - - return ExtractHextetsAndCreateMacAddress(input); - } - - /// - /// Try to parse a string input, given one of the following formats, in order to create a MAC Address - /// - /// - /// - /// IEEE 802 format for printing EUI-48 & MAC-48 addresses in six groups of two hexadecimal digits, - /// separated by a dash ("-") - /// - /// - /// - /// six groups of two hexadecimal digits separated by colons (":") - /// - /// - /// six groups of two hexadecimal digits separated by spaces (" ") - /// - /// - /// 12 hexadecimal digits with no separation - /// - /// - /// Cisco three groups of four hexadecimal digits separated by dots (".") - /// - /// - /// - /// the input to parse - /// a created on successful parse - /// if, and only if, the parse was successful - public static bool TryParse(string input, out MacAddress macAddress) - { - try - { - macAddress = Parse(input); - return true; - } - catch // explicitly catching all for the sake of standard bool Try*(out) pattern - { - macAddress = null; - return false; - } - } - - #endregion end: Parse string - - #region ParseAny string - - /// Parse a string disregarding all non hexadecimal digits in order to create a MAC Address - /// the input to parse - /// a - /// is or whitespace. - /// - /// A time-out occurred. For more information about time-outs, see the Remarks - /// section. - /// - public static MacAddress ParseAny(string input) - { - if (string.IsNullOrWhiteSpace(input)) - { - throw new ArgumentException("Value cannot be null or whitespace.", nameof(input)); - } - - return ExtractHextetsAndCreateMacAddress(input); - } - - /// Try to parse a string disregarding all non hexadecimal digits in order to create a MAC Address - /// the input to parse - /// a created on successful parse - /// if the parse was successful - public static bool TryParseAny(string input, out MacAddress macAddress) - { - try - { - macAddress = ParseAny(input); - return true; - } - catch // explicitly catching all for the sake of standard bool Try*(out) pattern - { - macAddress = null; - return false; - } - } - - #endregion end: ParseAny - - #endregion end: Parse - - #endregion end: factory - - #region operators - - /// Check equality of and - /// the left operand - /// the right operand - /// if, and only if, both and are equal - public static bool operator ==(MacAddress left, MacAddress right) - { - return Equals(left, right); - } - - /// Check inequality of and - /// the left operand - /// the right operand - /// if, and only if, both and are not equal - public static bool operator !=(MacAddress left, MacAddress right) - { - return !Equals(left, right); - } - - /// Check if is less than - /// the left operand - /// the right operand - /// if, and only if, is less than - public static bool operator <(MacAddress left, MacAddress right) - { - return Comparer.Default.Compare(left, right) < 0; - } - - /// Check if is greater than - /// the left operand - /// the right operand - /// if, and only if, is greater than - public static bool operator >(MacAddress left, MacAddress right) - { - return Comparer.Default.Compare(left, right) > 0; - } - - /// Check if is less than or equal to - /// the left operand - /// the right operand - /// - /// if, and only if, is less than or equal to - /// - public static bool operator <=(MacAddress left, MacAddress right) - { - return Comparer.Default.Compare(left, right) <= 0; - } - - /// Check if is greater than or equal to - /// the left operand - /// the right operand - /// - /// if, and only if, is greater than or equal to - /// - public static bool operator >=(MacAddress left, MacAddress right) - { - return Comparer.Default.Compare(left, right) >= 0; - } - - #endregion end: operators - } -} diff --git a/src/Arcus/Math/IPAddressMath.cs b/src/Arcus/Math/IPAddressMath.cs index 93b4690..0482e01 100644 --- a/src/Arcus/Math/IPAddressMath.cs +++ b/src/Arcus/Math/IPAddressMath.cs @@ -1,34 +1,60 @@ -using System; -using System.Linq; -using System.Net; +using System.Net; +using System.Numerics; using Arcus.Utilities; -using Gulliver; using static System.Net.Sockets.AddressFamily; +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif namespace Arcus.Math { /// /// Static utility class containing mathematical methods on objects /// + /// + /// + /// IPv4 address arithmetic operates within the 32-bit unsigned address space per + /// RFC 791 §2.3. + /// IPv6 address arithmetic operates within the 128-bit unsigned address space per + /// RFC 4291 §2.1. + /// Overflow or underflow beyond the respective address space boundaries throws + /// . + /// + /// public static class IPAddressMath { #region basic arithmetic operations /// - /// Increment IPv4 or IPv6 value + /// Increments or decrements an IPv4 or IPv6 address by a delta. /// - /// the ip address to affect - /// the increment value, may be negative - /// the incremented ip address - /// could not increment input - /// Increment caused address underflow - /// Increment caused address overflow + /// + /// + /// For IPv4, overflow beyond 255.255.255.255 (upper bound per + /// RFC 791 §2.3) throws + /// . + /// For IPv6, overflow beyond ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff (upper bound per + /// RFC 4291 §2.1) throws + /// . + /// + /// + /// The IP address to adjust. + /// The signed increment value; negative for decrement. + /// The adjusted IP address. + /// + /// + /// is a signed 64-bit integer (). + /// For IPv6 ranges exceeding 2^63 - 1, callers must issue multiple + /// increment calls; there is no -delta overload. + /// + /// + /// Increment would overflow or underflow the address space. /// is . public static IPAddress Increment(this IPAddress input, long delta = 1) { #region defense - if (input == null) + if (input is null) { throw new ArgumentNullException(nameof(input)); } @@ -48,30 +74,29 @@ public static IPAddress Increment(this IPAddress input, long delta = 1) return input; } - if (!ByteArrayUtils.TrySumBigEndian(input.GetAddressBytes(), delta, out var byteResult)) - { - throw new InvalidOperationException("could not increment address"); - } - - var addressByteWidth = input.IsIPv4() ? IPAddressUtilities.IPv4ByteCount : IPAddressUtilities.IPv6ByteCount; - - if (byteResult.Length > addressByteWidth) + var wrapper = BigEndianBitWrapper.FromBytes(input.GetAddressBytes()); + if (!wrapper.TryAdd(delta, out var result)) { - throw new InvalidOperationException("increment would overflow maximum size of ip address"); + throw new InvalidOperationException( + delta > 0 ? "increment would overflow maximum size of ip address" : "could not increment address" + ); } - var paddedBytes = byteResult.PadBigEndianMostSignificantBytes(addressByteWidth); - return new IPAddress(paddedBytes); + return new IPAddress(result.ToBytes()); } /// - /// Try to increment the given address + /// Attempts to increment an IPv4 or IPv6 address, returning instead of throwing on overflow or underflow. /// - /// the to increment - /// the resulting post increment - /// the amount to increment by - /// true on success + /// The IP address to adjust. + /// The resulting , or on failure. + /// The signed increment value; negative for decrement. + /// if the increment succeeded. +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER + public static bool TryIncrement(IPAddress input, [NotNullWhen(true)] out IPAddress address, long delta = 1) +#else public static bool TryIncrement(IPAddress input, out IPAddress address, long delta = 1) +#endif { if (input == null) { @@ -84,7 +109,7 @@ public static bool TryIncrement(IPAddress input, out IPAddress address, long del address = input.Increment(delta); return true; } - catch + catch (Exception ex) when (ex is ArgumentException or InvalidOperationException) { address = null; return false; @@ -96,27 +121,27 @@ public static bool TryIncrement(IPAddress input, out IPAddress address, long del #region comparisons /// - /// Is Equal + /// Determines whether two instances have equal numeric values. /// - /// first operand - /// second operand - /// true if is logically equal to + /// The first address to compare. + /// The second address to compare. + /// if the addresses are equal. public static bool IsEqualTo(this IPAddress left, IPAddress right) { - return ReferenceEquals(left, right) || (!ReferenceEquals(left, null) && left.Equals(right)); + return ReferenceEquals(left, right) || (left?.Equals(right) == true); } /// - /// Greater Than + /// Determines whether one is numerically greater than another. /// - /// first operand - /// second operand - /// true if is logically greater than + /// The first address to compare. + /// The second address to compare. + /// if is greater than . public static bool IsGreaterThan(this IPAddress left, IPAddress right) { if ( ReferenceEquals(left, right) - || (!ReferenceEquals(left, null) && left.Equals(right)) + || (left?.Equals(right) == true) || left == null || right == null || left.AddressFamily != right.AddressFamily @@ -125,97 +150,133 @@ public static bool IsGreaterThan(this IPAddress left, IPAddress right) return false; } - return ByteArrayUtils.CompareUnsignedBigEndian(left.GetAddressBytes(), right.GetAddressBytes()) > 0; + return BigEndianBitWrapper + .FromBytes(left.GetAddressBytes()) + .CompareTo(BigEndianBitWrapper.FromBytes(right.GetAddressBytes())) > 0; } /// - /// Greater Than or equal + /// Determines whether one is numerically greater than or equal to another. /// - /// first operand - /// second operand - /// true if is logically greater than or equal to + /// The first address to compare. + /// The second address to compare. + /// if is greater than or equal to . public static bool IsGreaterThanOrEqualTo(this IPAddress left, IPAddress right) { - return ReferenceEquals(left, right) - || (!ReferenceEquals(left, null) && left.Equals(right)) - || ( - left?.AddressFamily == right?.AddressFamily - && ByteArrayUtils.CompareUnsignedBigEndian(left?.GetAddressBytes(), right?.GetAddressBytes()) >= 0 - ); + if (ReferenceEquals(left, right)) + { + return true; + } + + if (left == null || right == null || left.AddressFamily != right.AddressFamily) + { + return false; + } + + if (left.Equals(right)) + { + return true; + } + + return BigEndianBitWrapper + .FromBytes(left.GetAddressBytes()) + .CompareTo(BigEndianBitWrapper.FromBytes(right.GetAddressBytes())) >= 0; } /// - /// Less Than + /// Determines whether one is numerically less than another. /// - /// first operand - /// second operand - /// true if is logically less than - /// Address families must be InterNetwork or InternetworkV6 + /// The first address to compare. + /// The second address to compare. + /// if is less than . + /// or is . + /// Address families must match. public static bool IsLessThan(this IPAddress left, IPAddress right) { - if (ReferenceEquals(left, right) || left == null || right == null || left.AddressFamily != right.AddressFamily) + if ( + ReferenceEquals(left, right) + || (left?.Equals(right) == true) + || left == null + || right == null + || left.AddressFamily != right.AddressFamily + ) { return false; } - return ByteArrayUtils.CompareUnsignedBigEndian(left.GetAddressBytes(), right.GetAddressBytes()) < 0; + return BigEndianBitWrapper + .FromBytes(left.GetAddressBytes()) + .CompareTo(BigEndianBitWrapper.FromBytes(right.GetAddressBytes())) < 0; } /// - /// Less Than or equal + /// Determines whether one is numerically less than or equal to another. /// - /// first operand - /// second operand - /// true if is logically less than or equal to + /// The first address to compare. + /// The second address to compare. + /// if is less than or equal to . public static bool IsLessThanOrEqualTo(this IPAddress left, IPAddress right) { - return ReferenceEquals(left, right) - || (!ReferenceEquals(left, null) && left.Equals(right)) - || ( - left?.AddressFamily == right?.AddressFamily - && ByteArrayUtils.CompareUnsignedBigEndian(left?.GetAddressBytes(), right?.GetAddressBytes()) <= 0 - ); + if (ReferenceEquals(left, right)) + { + return true; + } + + if (left == null || right == null || left.AddressFamily != right.AddressFamily) + { + return false; + } + + if (left.Equals(right)) + { + return true; + } + + return BigEndianBitWrapper + .FromBytes(left.GetAddressBytes()) + .CompareTo(BigEndianBitWrapper.FromBytes(right.GetAddressBytes())) <= 0; } /// - /// Determine if the occurs numerically between the given high and low IP addresses - /// Inclusivity contingent on inclusive bit + /// Determines whether an address falls within a numeric range, inclusive by default. /// - /// IP address to test - /// low value - /// high value - /// true if bounds are inclusive (defaults to true) - /// is . - /// is . - /// is . - /// true if is between and + /// The address to test. + /// The lower bound of the range. + /// The upper bound of the range. + /// to include the bounds (default). + /// if is between and . + /// Any argument is . + /// Address families must match. + /// is greater than . public static bool IsBetween(this IPAddress input, IPAddress low, IPAddress high, bool inclusive = true) { #region defense - if (input == null) + if (input is null) { throw new ArgumentNullException(nameof(input)); } - if (low == null) + if (low is null) { throw new ArgumentNullException(nameof(low)); } - if (high == null) + if (high is null) { throw new ArgumentNullException(nameof(high)); } if (low.AddressFamily != high.AddressFamily || input.AddressFamily != low.AddressFamily) { - throw new InvalidOperationException("address families do not match"); + throw new ArgumentException("address families do not match"); } var lowAddressBytes = low.GetAddressBytes(); var highAddressBytes = high.GetAddressBytes(); - if (ByteArrayUtils.CompareUnsignedBigEndian(lowAddressBytes, highAddressBytes) > 0) + var lowWrap = BigEndianBitWrapper.FromBytes(lowAddressBytes); + var highWrap = BigEndianBitWrapper.FromBytes(highAddressBytes); + if (lowWrap.CompareTo(highWrap) > 0) { throw new InvalidOperationException($"{nameof(low)} must not be greater than {nameof(high)}"); } @@ -227,30 +288,28 @@ public static bool IsBetween(this IPAddress input, IPAddress low, IPAddress high return inclusive; } - var inputBytes = input.GetAddressBytes(); - return ByteArrayUtils.CompareUnsignedBigEndian(inputBytes, lowAddressBytes) > 0 - && ByteArrayUtils.CompareUnsignedBigEndian(inputBytes, highAddressBytes) < 0; + var inputWrap = BigEndianBitWrapper.FromBytes(input.GetAddressBytes()); + return inputWrap.CompareTo(lowWrap) > 0 && inputWrap.CompareTo(highWrap) < 0; } /// - /// Get Maximum (based on bytes) + /// Returns the numerically greater of two values. /// - /// first operand - /// second operand - /// the largest of the two operands - /// is . - /// is . - /// Address families must match + /// The first address. + /// The second address. + /// The larger of the two addresses. + /// Either argument is . + /// Address families must match. public static IPAddress Max(IPAddress left, IPAddress right) { #region defense - if (left == null) + if (left is null) { throw new ArgumentNullException(nameof(left)); } - if (right == null) + if (right is null) { throw new ArgumentNullException(nameof(right)); } @@ -266,24 +325,23 @@ public static IPAddress Max(IPAddress left, IPAddress right) } /// - /// Get Minimum IPAddress (based on bytes) + /// Returns the numerically lesser of two values. /// - /// first operand - /// second operand - /// the smallest of the two operands - /// is . - /// is . - /// Address families must match + /// The first address. + /// The second address. + /// The smaller of the two addresses. + /// Either argument is . + /// Address families must match. public static IPAddress Min(IPAddress left, IPAddress right) { #region defense - if (left == null) + if (left is null) { throw new ArgumentNullException(nameof(left)); } - if (right == null) + if (right is null) { throw new ArgumentNullException(nameof(right)); } @@ -303,17 +361,25 @@ public static IPAddress Min(IPAddress left, IPAddress right) #region limit deduction /// - /// determine if IP address is at maximum value + /// Determines whether an address is at the maximum value for its address family. /// - /// the IP Address to test - /// true if the address is the maximum value - /// Address families must be InterNetwork or InternetworkV6 + /// + /// + /// IPv4 maximum is 255.255.255.255 per + /// RFC 791 §2.3. + /// IPv6 maximum is ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff per + /// RFC 4291 §2.1. + /// + /// + /// The address to test. + /// if the address is the maximum value for its family. /// is . + /// Unsupported address family. public static bool IsAtMax(this IPAddress address) { #region defense - if (address == null) + if (address is null) { throw new ArgumentNullException(nameof(address)); } @@ -332,17 +398,25 @@ public static bool IsAtMax(this IPAddress address) } /// - /// determine if IP address is at minimum value + /// Determines whether an address is at the minimum value for its address family. /// - /// the IP Address to test - /// true if the address is the minimum value - /// Address families must be InterNetwork or InternetworkV6 + /// + /// + /// IPv4 minimum is 0.0.0.0 per + /// RFC 791 §2.3. + /// IPv6 minimum is :: per + /// RFC 4291 §2.1. + /// + /// + /// The address to test. + /// if the address is the minimum value for its family. /// is . + /// Unsupported address family. public static bool IsAtMin(this IPAddress address) { #region defense - if (address == null) + if (address is null) { throw new ArgumentNullException(nameof(address)); } diff --git a/src/Arcus/Subnet.Factory.cs b/src/Arcus/Subnet.Factory.cs new file mode 100644 index 0000000..94e77d9 --- /dev/null +++ b/src/Arcus/Subnet.Factory.cs @@ -0,0 +1,1116 @@ +using System.Net; +using System.Text.RegularExpressions; +using Arcus.Converters; +using Arcus.Math; +using Arcus.Utilities; +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif + +namespace Arcus +{ + /// + /// static factory methods + /// + public partial class Subnet + { + private const string CouldNotInstantiateSubnetMessage = "could not instantiate subnet"; + + /// + /// Regex pattern that matches valid partial IPv4 octet strings (1-4 dot-separated octets, each 0-255). + /// Applied with . + /// + public const string Ipv4OctetPartialPattern = + @"^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){0,2}(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)?)?$"; + + /// + /// Regex pattern matching the rough shape of a subnet string: an address part followed by an optional slash-prefixed integer. + /// Applied with and . + /// + public const string RoughSubnetStringPattern = @"^([\da-fA-F:.]+)(?:/(-?[\d]+))?$"; + +#if !NET7_0_OR_GREATER + private static readonly Regex IPv4OctetPartialRegex = new( + Ipv4OctetPartialPattern, + RegexOptions.Compiled | RegexOptions.CultureInvariant + ); +#else + private static Regex IPv4OctetPartialRegex => GetIPv4OctetPartialRegex(); + + [GeneratedRegex(Ipv4OctetPartialPattern, RegexOptions.CultureInvariant)] + private static partial Regex GetIPv4OctetPartialRegex(); +#endif + +#if !NET7_0_OR_GREATER + private static readonly Regex RoughSubnetRegex = new( + RoughSubnetStringPattern, + RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase + ); +#else + private static Regex RoughSubnetRegex => GetRoughSubnetRegex(); + + [GeneratedRegex(RoughSubnetStringPattern, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase)] + private static partial Regex GetRoughSubnetRegex(); +#endif + + #region static factory methods + + #region FromNetMask + + /// + /// Creates a subnet from an IP address and subnet mask. + /// + /// + /// + /// The netmask must be a valid IPv4 subnet mask (contiguous leading 1-bits) per + /// RFC 950 §2. + /// IPv4 only; use for IPv6. + /// + /// + /// The IP address. + /// The subnet mask. + /// The maximum enumeration exponent (0-128). + /// The created . + /// is . + /// is . + /// is not IPv4. + /// is not a valid subnet mask. + public static Subnet FromNetMask( + IPAddress address, + IPAddress netmask, + int maxEnumerationExponent = DefaultMaxEnumerationExponent + ) + { + if (address is null) + { + throw new ArgumentNullException(nameof(address)); + } + + if (netmask is null) + { + throw new ArgumentNullException(nameof(netmask)); + } + + if (!address.IsIPv4()) + { + throw new ArgumentException($"{nameof(address)} must be IPv4", nameof(address)); + } + + if (!netmask.IsValidNetMask()) + { + throw new ArgumentException($"{nameof(netmask)} must be a valid netmask", nameof(netmask)); + } + + try + { + return new Subnet(address, netmask.NetmaskToCidrRoutePrefix(), maxEnumerationExponent); + } + catch (Exception e) + { + throw new InvalidOperationException(CouldNotInstantiateSubnetMessage, e); + } + } + + /// + /// Attempts to create a subnet from an IP address and subnet mask. + /// + /// The IP address. + /// The subnet mask. + /// The created , or on failure. + /// The maximum enumeration exponent (0-128). + /// if the subnet was created successfully. +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER + public static bool TryFromNetMask( + IPAddress address, + IPAddress netmask, + [NotNullWhen(true)] out Subnet subnet, + int maxEnumerationExponent = DefaultMaxEnumerationExponent + ) +#else + public static bool TryFromNetMask( + IPAddress address, + IPAddress netmask, + out Subnet subnet, + int maxEnumerationExponent = DefaultMaxEnumerationExponent + ) +#endif + { + try + { + subnet = FromNetMask(address, netmask, maxEnumerationExponent); + return true; + } + catch + { + subnet = null; + return false; + } + } + + #endregion // end: FromNetMask + + /// + /// Creates the smallest subnet that contains both addresses encoded as byte arrays. + /// + /// The lower address as a byte array. + /// The higher address as a byte array. + /// The maximum enumeration exponent (0-128). + /// The created . + public static Subnet FromBytes( + byte[] lowAddressBytes, + byte[] highAddressBytes, + int maxEnumerationExponent = DefaultMaxEnumerationExponent + ) + { + if (lowAddressBytes is null) + { + throw new ArgumentNullException(nameof(lowAddressBytes)); + } + + if (highAddressBytes is null) + { + throw new ArgumentNullException(nameof(highAddressBytes)); + } + + IPAddress lowAddress; + try + { + lowAddress = new IPAddress(lowAddressBytes); + } + catch (ArgumentException e) + { + throw new ArgumentException( + $"could not convert {nameof(lowAddressBytes)} to an IPAddress", + nameof(lowAddressBytes), + e + ); + } + + IPAddress highAddress; + try + { + highAddress = new IPAddress(highAddressBytes); + } + catch (ArgumentException e) + { + throw new ArgumentException( + $"could not convert {nameof(highAddressBytes)} to an IPAddress", + nameof(highAddressBytes), + e + ); + } + + try + { + return new Subnet(lowAddress, highAddress, maxEnumerationExponent); + } + catch (Exception e) + { + throw new InvalidOperationException(CouldNotInstantiateSubnetMessage, e); + } + } + + /// + /// Attempts to create the smallest subnet containing both addresses encoded as byte arrays. + /// + /// The lower address as a byte array. + /// The higher address as a byte array. + /// The created , or on failure. + /// The maximum enumeration exponent (0-128). + /// if the subnet was created successfully. +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER + public static bool TryFromBytes( + byte[] lowAddressBytes, + byte[] highAddressBytes, + [NotNullWhen(true)] out Subnet subnet, + int maxEnumerationExponent = DefaultMaxEnumerationExponent + ) +#else + public static bool TryFromBytes( + byte[] lowAddressBytes, + byte[] highAddressBytes, + out Subnet subnet, + int maxEnumerationExponent = DefaultMaxEnumerationExponent + ) +#endif + { + try + { + subnet = FromBytes(lowAddressBytes, highAddressBytes, maxEnumerationExponent); + return true; + } + catch + { + subnet = null; + return false; + } + } + + /// + /// Creates a single-IP subnet from an with a custom max enumeration exponent. + /// + /// the ip address + /// the maximum enumeration exponent (0-128); enumeration yields at most 2maxEnumerationExponent addresses + /// A representing a single address. + public static Subnet FromIPAddress(IPAddress address, int maxEnumerationExponent = DefaultMaxEnumerationExponent) + { + if (address is null) + { + throw new ArgumentNullException(nameof(address)); + } + + return new Subnet(address, address, maxEnumerationExponent); + } + + #region Parse / TryParse + + /// + /// Parses a subnet from a CIDR-notation string. + /// + /// + /// + /// Accepts CIDR notation (a.b.c.d/n or addr::x/n) as defined in + /// RFC 4632 §2. + /// If no prefix is provided, a host route (/32 for IPv4, /128 for IPv6) is assumed. + /// + /// + /// The string to parse. + /// The parsed . + /// is . + /// is empty or whitespace. + /// The string could not be parsed as a valid subnet. + public static Subnet Parse(string subnetString) + { + subnetString = subnetString?.Trim(); + + if (subnetString is null) + { + throw new ArgumentNullException(nameof(subnetString)); + } + + if (string.IsNullOrWhiteSpace(subnetString)) + { + throw new ArgumentException("a non empty value is expected", nameof(subnetString)); + } + + var matches = RoughSubnetRegex.Matches(subnetString); + + if (matches.Count != 1) + { + throw new FormatException("unexpected format"); + } + + var match = matches[0]; + + var addressString = match.Groups[1].Value; + var routePrefixString = match.Groups[2].Value; + + if (!IPAddress.TryParse(addressString, out var address)) // attempt to parse IP address portion + { + throw new FormatException($"cannot parse ip address \"{addressString}\""); + } + + int routingPrefix; + if (!string.IsNullOrWhiteSpace(routePrefixString)) // attempt to parse routing prefix in there is a match + { + if (!int.TryParse(routePrefixString, out routingPrefix)) + { + throw new FormatException($"cannot parse routing prefix \"{routePrefixString}\""); + } + + if (routingPrefix < 0) + { + throw new FormatException("Routing prefix must be non-negative."); + } + } + else // no routing prefix match, assume it is a single address + { + routingPrefix = address.IsIPv4() ? IPAddressUtilities.IPv4BitCount : IPAddressUtilities.IPv6BitCount; + } + + if (address.IsIPv4() && routingPrefix > IPAddressUtilities.IPv4BitCount) + { + throw new ArgumentException("routing prefix must be 32 or less for an IPv4 subnet", nameof(subnetString)); + } + + if (address.IsIPv6() && routingPrefix > IPAddressUtilities.IPv6BitCount) + { + throw new ArgumentException("routing prefix must be 128 or less for an IPv6 subnet", nameof(subnetString)); + } + + try + { + return new Subnet(address, routingPrefix); + } + catch (Exception e) + { + throw new FormatException(CouldNotInstantiateSubnetMessage, e); + } + } + + /// + /// Parses a subnet from an address string and a CIDR prefix length. + /// + /// + /// + /// Parses a subnet from an address string and a separate CIDR prefix integer per + /// RFC 4632 §2. + /// + /// + /// The address string to parse. + /// The CIDR routing prefix. + /// The maximum enumeration exponent (0-128). + /// The parsed . + /// is . + /// could not be parsed as an . + /// has an unsupported address family. + /// is out of range for the address family. + public static Subnet Parse( + string addressString, + int routingPrefix, + int maxEnumerationExponent = DefaultMaxEnumerationExponent + ) + { + if (addressString is null) + { + throw new ArgumentNullException(nameof(addressString)); + } + + if (!IPAddress.TryParse(addressString, out var address)) + { + throw new FormatException($"could not parse {nameof(addressString)} value \"{addressString}\""); + } + + if (!IPAddressUtilities.ValidAddressFamilies.Contains(address.AddressFamily)) + { + throw new ArgumentException( + $"must have an address family equal to {string.Join(", ", IPAddressUtilities.ValidAddressFamilies)}", + nameof(addressString) + ); + } + + if (routingPrefix < 0) + { + throw new ArgumentOutOfRangeException(nameof(routingPrefix)); + } + + if (address.IsIPv4() && routingPrefix > IPAddressUtilities.IPv4BitCount) + { + throw new ArgumentOutOfRangeException(nameof(routingPrefix)); + } + + if (address.IsIPv6() && routingPrefix > IPAddressUtilities.IPv6BitCount) + { + throw new ArgumentOutOfRangeException(nameof(routingPrefix)); + } + + try + { + return new Subnet(address, routingPrefix, maxEnumerationExponent); + } + catch (Exception e) + { + throw new FormatException(CouldNotInstantiateSubnetMessage, e); + } + } + + /// + /// Parses a subnet from two address strings (low and high bounds). + /// + /// The low address string. + /// The high address string. + /// The maximum enumeration exponent (0-128). + /// The parsed . + public static Subnet Parse( + string lowAddressString, + string highAddressString, + int maxEnumerationExponent = DefaultMaxEnumerationExponent + ) + { + if (lowAddressString is null) + { + throw new ArgumentNullException(nameof(lowAddressString)); + } + + if (highAddressString is null) + { + throw new ArgumentNullException(nameof(highAddressString)); + } + + if (!IPAddress.TryParse(lowAddressString, out var lowAddress)) + { + throw new FormatException($"could not parse {nameof(lowAddressString)} value \"{lowAddressString}\""); + } + + if (!IPAddress.TryParse(highAddressString, out var highAddress)) + { + throw new FormatException($"could not parse {nameof(highAddressString)} value \"{highAddressString}\""); + } + + if (lowAddress.AddressFamily != highAddress.AddressFamily) + { + throw new ArgumentException( + $"{nameof(lowAddressString)} and {nameof(highAddressString)} must have matching address families" + ); + } + + if (!IPAddressUtilities.ValidAddressFamilies.Contains(lowAddress.AddressFamily)) + { + throw new ArgumentException( + $"{nameof(lowAddressString)} must have an address family equal to {string.Join(", ", IPAddressUtilities.ValidAddressFamilies)}", + nameof(lowAddressString) + ); + } + + if (!IPAddressUtilities.ValidAddressFamilies.Contains(highAddress.AddressFamily)) + { + throw new ArgumentException( + $"{nameof(highAddressString)} must have an address family equal to {string.Join(", ", IPAddressUtilities.ValidAddressFamilies)}", + nameof(highAddressString) + ); + } + + if (!highAddress.IsGreaterThanOrEqualTo(lowAddress)) + { + throw new InvalidOperationException( + $"{nameof(highAddressString)} must be greater or equal to {nameof(lowAddressString)}" + ); + } + + try + { + return new Subnet(lowAddress, highAddress, maxEnumerationExponent); + } + catch (Exception e) + { + throw new FormatException(CouldNotInstantiateSubnetMessage, e); + } + } + + /// + /// Attempts to parse a subnet from a CIDR-notation string. + /// + /// The string to parse. + /// The parsed , or on failure. + /// if parsing succeeded. + public static bool TryParse(string subnetString, +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER + [NotNullWhen(true)] +#endif + out Subnet subnet) + { + subnetString = subnetString?.Trim(); + + try + { + subnet = Parse(subnetString); + return true; + } + catch + { + subnet = null; + return false; + } + } + + /// + /// Attempts to parse a subnet from an address string and CIDR prefix length. + /// + /// The address string to parse. + /// The CIDR routing prefix. + /// The parsed , or on failure. + /// The maximum enumeration exponent (0-128). + /// if parsing succeeded. +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER + public static bool TryParse( + string addressString, + int routingPrefix, + [NotNullWhen(true)] out Subnet subnet, + int maxEnumerationExponent = DefaultMaxEnumerationExponent + ) +#else + public static bool TryParse( + string addressString, + int routingPrefix, + out Subnet subnet, + int maxEnumerationExponent = DefaultMaxEnumerationExponent + ) +#endif + { + try + { + subnet = Parse(addressString, routingPrefix, maxEnumerationExponent); + return true; + } + catch + { + subnet = null; + return false; + } + } + + /// + /// Attempts to parse a subnet from two address strings (low and high bounds). + /// + /// The low address string. + /// The high address string. + /// The parsed , or on failure. + /// The maximum enumeration exponent (0-128). + /// if parsing succeeded. +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER + public static bool TryParse( + string lowAddressString, + string highAddressString, + [NotNullWhen(true)] out Subnet subnet, + int maxEnumerationExponent = DefaultMaxEnumerationExponent + ) +#else + public static bool TryParse( + string lowAddressString, + string highAddressString, + out Subnet subnet, + int maxEnumerationExponent = DefaultMaxEnumerationExponent + ) +#endif + { + try + { + subnet = Parse(lowAddressString, highAddressString, maxEnumerationExponent); + + return true; + } + catch + { + subnet = null; + return false; + } + } + + #region From Partial + + /// + /// Attempts to convert a partial IPv4 address into a subnet. + /// + /// + /// + /// Each supplied octet contributes 8 bits to the routing prefix per + /// RFC 791 §2.3. + /// For example, "10.0" produces a /16 subnet, "192.168.1" produces a /24 subnet. + /// + /// + /// The partial IP address string (e.g., "10.0", "192.168"). + /// The created , or on failure. + /// The maximum enumeration exponent (0-128). + /// if the partial address was parsed successfully. + public static bool TryIPv4FromPartial( + string input, +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER + [NotNullWhen(true)] +#endif + out Subnet subnet, + int maxEnumerationExponent = DefaultMaxEnumerationExponent + ) + { + if (string.IsNullOrWhiteSpace(input) || !IPv4OctetPartialRegex.IsMatch(input)) + { + subnet = null; + return false; + } + + input = input.TrimEnd('.'); + var octetCount = input.Count(ch => ch == '.') + 1; + var addressString = input + string.Concat(Enumerable.Repeat(".0", IPAddressUtilities.IPv4OctetCount - octetCount)); + + try + { + subnet = Parse(addressString, octetCount * 8, maxEnumerationExponent); + return true; + } + catch + { + subnet = null; + return false; + } + } + + /// + /// Given a partial IPv6 address (with or without CIDR notation), enumerate all possible + /// valid subnets the input could represent, ordered from most-specific to broadest + /// (descending prefix length). + /// + /// + /// + /// This method handles several distinct input patterns, each delegated to a dedicated + /// handler. See the individual handler documentation for details. + /// + /// + /// Input patternExample + /// Exact CIDR"2001:db8::/32" - returned as-is. + /// Bare hex word"2001" or "abba" - treated as a single hextet with implicit ::. + /// Colon-separated partial"2001:db8::", "2001:db8:", "2001:db8:0:0:0:0:" - all possible prefix-length expansions. + /// Bracketed"[2001:db8::1]" - brackets are stripped before processing. + /// Whitespace-wrapped" 2001:db8:: " - leading/trailing whitespace is trimmed. + /// + /// + /// The permutation-based disambiguation treats each specified hextet as part of the + /// routing prefix. For each possible count of zero hextets the :: collapse + /// could expand to (0 through the remaining hextet slots), a subnet is generated. + /// Results are returned in descending prefix length order so the most specific + /// (tightest) match appears first. + /// + /// + /// For example, "2001:db8::" has 2 specified hextets and one :: collapse. + /// The collapse can expand to consume 0 through 5 hextet positions, producing + /// subnets at prefix lengths /128, /112, /96, /80, /64, /48, /32. + /// + /// + /// IPv6 colon-hex notation with :: zero-group collapse per + /// RFC 4291 §2.2. + /// + /// + /// This method is because the permutation-based + /// disambiguation is a very specific behaviour that may not match the caller's + /// intent. It is likely to be replaced by more explicit factory methods. + /// + /// + /// + /// A partial IPv6 address string. The following forms are accepted: + /// + /// + /// Exact CIDR notation - "2001:db8::/32", "::/0". + /// + /// + /// Fully-qualified address - "2001:db8::1", "2001:db8:0:0:0:0:0:1". + /// + /// + /// Partial hextets with trailing :: - "2001:db8::", "abba::". + /// + /// + /// Partial hextets with trailing : - "2001:db8:", "2001:db8:0:0:0:0:". + /// + /// + /// Bare hex word (no colons) - "2001", "abba". + /// + /// + /// URL-bracketed - "[2001:db8::1]". + /// + /// + /// Leading/trailing whitespace is automatically trimmed. + /// + /// + /// When this method returns , a collection of all possible + /// matching subnets ordered from most-specific (largest prefix) to broadest (smallest + /// prefix); when , an empty collection. + /// + /// the maximum enumeration exponent + /// when at least one valid subnet interpretation was found. +#pragma warning disable S1133 // Do not forget to remove this deprecated code someday + [Obsolete( + "the needs for this method are very specialized and may not be what the developer is expecting; this is likely to be replaced by a host of other more explicit and useful methods" + )] +#pragma warning restore S1133 + public static bool TryIPv6FromPartial( + string input, + out IEnumerable subnets, + int maxEnumerationExponent = DefaultMaxEnumerationExponent + ) + { + subnets = []; + + if (string.IsNullOrWhiteSpace(input)) + { + return false; + } + + input = NormalizeIPv6PartialInput(input); + + if (!IsPotentiallyValidIPv6Partial(input)) + { + return false; + } + + if (TryParseExactCidr(input, out subnets)) + { + return true; + } + + if (ContainsOnlyHexChars(input)) + { + return TryParseBareHextet(input, out subnets, maxEnumerationExponent); + } + + return TryParseAmbiguousHextets(input, out subnets, maxEnumerationExponent); + } + + /// + /// Normalizes a raw user input string for partial IPv6 parsing. + /// Trims leading/trailing whitespace and removes URL-style square brackets. + /// + /// The raw input string (must not be null). + /// The normalized input string. + /// + /// + /// Non-developer users may inadvertently include whitespace or wrap the + /// address in brackets (e.g. from copying a URL). Normalizing upfront + /// prevents these from causing spurious parse failures. + /// + /// + private static string NormalizeIPv6PartialInput(string input) + { + input = input.Trim(); + + if (input.Length >= 2 && input[0] == '[' && input[input.Length - 1] == ']') + { + input = input.Substring(1, input.Length - 2).Trim(); + } + + return input; + } + + /// + /// Fast validation to reject inputs that cannot possibly be a valid IPv6 partial. + /// + /// The normalized input string. + /// if the input could be a valid IPv6 partial. + /// + /// Rejects: + /// + /// Lone ":" - meaningless. + /// Triple-colon ":::" - invalid syntax. + /// Multiple discrete "::" occurrences - RFC 4291 §2.2 allows at most one zero-collapse. + /// More than 8 non-empty hextets - exceeds IPv6 address width. + /// 8+ hextets with trailing colon - over-full; no room for a collapse. + /// + /// Does NOT validate individual hextet values (hex format is checked by downstream parsers). + /// + private static bool IsPotentiallyValidIPv6Partial(string input) + { + if (input.Equals(":", StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + if (input.Contains(":::")) + { + return false; + } + + if (DoubleColonsAppearsMultipleTimes(input)) + { + return false; + } + + var nonEmptyHextetCount = input.Split([':'], StringSplitOptions.RemoveEmptyEntries).Length; + + if (nonEmptyHextetCount > IPAddressUtilities.IPv6HextetCount) + { + return false; + } + + return nonEmptyHextetCount < IPAddressUtilities.IPv6HextetCount + || !input.EndsWith(":", StringComparison.OrdinalIgnoreCase); + } + + /// + /// Checks if a string contains only hexadecimal characters (0-9, a-f, A-F). + /// + /// The string to check. + /// if the string is non-empty and contains only hex chars. + /// + /// Used to detect bare hextet inputs like "2001" or "abba" + /// that lack colons entirely. Without this check, such inputs would be + /// misinterpreted by IPAddress.TryParse as IPv4 addresses + /// (e.g. "2001" is parsed as decimal 2001 = 0.0.7.209). + /// + /// This helper is intentionally permissive (any length of hex), allowing + /// future input patterns to flow through to the permutation engine. + /// + /// + private static bool ContainsOnlyHexChars(string input) + { + if (string.IsNullOrEmpty(input)) + { + return false; + } + + return input.All(ch => ch is >= '0' and <= '9' or >= 'a' and <= 'f' or >= 'A' and <= 'F'); + } + + /// + /// Attempts to parse the input as an exact IPv6 CIDR subnet. + /// + /// The normalized input string. + /// + /// When successful, a single-element collection with the parsed subnet; + /// when unsuccessful, an empty collection. + /// + /// if the input is a valid IPv6 CIDR subnet. + /// + /// Handles inputs containing a / character, e.g. "2001:db8::/32" + /// or "::/0". The input must not end with / (missing prefix) + /// and must not be an IPv4 CIDR (filtered via Subnet.IsIPv6). + /// + /// This is the least-ambiguous input pattern - the user explicitly states + /// the route prefix length. A single result is returned. + /// + /// + private static bool TryParseExactCidr(string input, out IEnumerable subnets) + { + subnets = []; + + if (!input.Contains('/')) + { + return false; + } + +#if NETSTANDARD2_0 + if (input.EndsWith("/")) +#else + if (input.EndsWith('/')) +#endif + { + return false; + } + + if (TryParse(input, out var subnet) && subnet.IsIPv6) + { + subnets = [subnet]; + return true; + } + + return false; + } + + /// + /// Attempts to parse a bare hex word (no colons) as a single-hextet IPv6 partial. + /// + /// A string containing only hexadecimal characters. + /// + /// When successful, all possible subnet interpretations for the hextet, + /// ordered from most-specific to broadest; when unsuccessful, an empty collection. + /// + /// the maximum enumeration exponent + /// when the bare hextet produced at least one valid subnet. + /// + /// + /// A bare hex word like "2001" is treated as a single IPv6 hextet + /// with an implicit :: collapse appended. The method delegates to + /// which generates all valid prefix-length + /// expansions. + /// + /// + /// This handler exists because IPAddress.TryParse would + /// misinterpret bare hex strings as IPv4 addresses (e.g. "2001" as + /// decimal 2001 = 0.0.7.209). + /// + /// + /// Future: This method is a strong candidate for public exposure + /// (e.g. TryParseBareHextet) for users who explicitly want to resolve + /// a single hex value to all covering subnets. + /// + /// + private static bool TryParseBareHextet( + string input, + out IEnumerable subnets, + int exponent = DefaultMaxEnumerationExponent + ) + { + return ExpandHextetPermutations(input, out subnets, exponent); + } + + /// + /// Attempts to parse a colon-separated partial IPv6 string and enumerate all + /// possible subnet interpretations. + /// + /// The normalized input string (must contain colons). + /// + /// When successful, all possible subnet interpretations in descending prefix + /// length order; when unsuccessful, an empty collection. + /// + /// the maximum enumeration exponent + /// when at least one valid subnet was found. + /// + /// + /// Uses a three-stage fallback to coerce the input into a form that + /// IPAddress.TryParse can accept: + /// + /// + /// Parse the input as-is (succeeds for valid addresses like "2001:db8::1"). + /// If input contains "::", trim trailing colons and retry + /// (handles edge cases where :: is followed by extra colons). + /// Append "::" to the colon-trimmed input + /// (handles incomplete forms like "2001:db8:""2001:db8::"). + /// + /// + /// On success, delegates to . + /// + /// + /// Future: This method is a strong candidate for public exposure + /// (e.g. TryParseAmbiguousHextets) for users who want to resolve + /// colon-separated partials without the CIDR or bare-hexet paths. + /// + /// + private static bool TryParseAmbiguousHextets( + string input, + out IEnumerable subnets, + int exponent = DefaultMaxEnumerationExponent + ) + { + subnets = []; + + if ( + !( + IPAddress.TryParse(input, out var address) + || (input.Contains("::") && IPAddress.TryParse(input.TrimEnd(':'), out address)) + || IPAddress.TryParse(input.TrimEnd(':') + "::", out address) + ) || !address.IsIPv6() + ) + { + return false; + } + + return ExpandHextetPermutations(input, out subnets, exponent); + } + + /// + /// Core permutation engine. Given a partial IPv6 address string, finds or synthesises + /// the :: collapse point and generates all valid prefix-length interpretations. + /// + /// A colon-separated partial IPv6 address. + /// + /// All possible subnet interpretations in descending prefix length order + /// (most-specific first), or an empty collection on failure. + /// + /// the maximum enumeration exponent + /// when at least one valid subnet was generated. + /// + /// + /// The algorithm: + /// + /// + /// Split the colon-trimmed input on ':' to obtain individual hextets. + /// An empty-string entry marks the :: collapse position. + /// If no collapse is found and fewer than 8 hextets are present, an implicit + /// collapse is added at the end (the partial is assumed to omit trailing zeros). + /// For each possible expansion of the collapse (replacing it with 0 through the + /// remaining slots), construct a full address and compute the route prefix as + /// (expandedHextets) * 16. + /// + /// + /// Results are emitted in descending prefix length order so the + /// most specific (largest prefix) match appears first in the output collection. + /// + /// + /// Example: input "2001:db8::". + /// Hextets after trim: ["2001", "db8"], no collapse → add implicit end collapse: + /// ["2001", "db8", ""]. Three hextets → 7 permutations: + /// + /// + /// PermutationAddressPrefix + /// 62001:db8:0:0:0:0:0:0/128 + /// 52001:db8:0:0:0:0:0::/112 + /// ......... + /// 02001:db8::/32 + /// + /// + private static bool ExpandHextetPermutations( + string input, + out IEnumerable subnets, + int exponent = DefaultMaxEnumerationExponent + ) + { + subnets = []; + + var trimmedPartial = input.TrimEnd(':'); + + var hextets = trimmedPartial.Split([':'], StringSplitOptions.None).ToList(); + + var collapseIndex = -1; + for (var i = 0; i < hextets.Count; i++) + { + if (string.IsNullOrWhiteSpace(hextets[i])) + { + collapseIndex = i; + break; + } + } + + if (collapseIndex == -1 && hextets.Count == 8) + { + subnets = [Parse(input, IPAddressUtilities.IPv6BitCount, exponent)]; + return true; + } + + if (collapseIndex == -1) + { + hextets.Add(string.Empty); + collapseIndex = hextets.Count - 1; + } + + var hextetCount = hextets.Count; + var permutationLimit = (IPAddressUtilities.IPv6HextetCount + 1) - hextetCount; + var subnetsList = new List(permutationLimit + 1); + var hextetsCopy = new List(hextetCount + permutationLimit); + + // Iterate in DESCENDING order so the most-specific (largest prefix) result + // appears first - this matches user expectations when scanning the output list. + for (var permutation = permutationLimit; permutation >= 0; permutation--) + { + hextetsCopy.Clear(); + hextetsCopy.AddRange(hextets); + hextetsCopy.RemoveAt(collapseIndex); + for (var j = 0; j < permutation; j++) + { + hextetsCopy.Insert(collapseIndex, "0"); + } + + var addressString = string.Join(":", hextetsCopy); + + if ( + !IPAddress.TryParse(addressString + "::", out var subnetAddress) + && !IPAddress.TryParse(addressString, out subnetAddress) + ) + { + return false; + } + + var routePrefix = ((permutation + hextetCount) - 1) * 16; + subnetsList.Add(new Subnet(subnetAddress, routePrefix, exponent)); + } + + subnets = subnetsList; + return true; + } + + /// + /// Checks if a string contains multiple discrete "::" substrings, + /// which is syntactically invalid in IPv6 (RFC 4291 §2.2 allows at most one + /// zero-group collapse). + /// + /// The string to check. + /// + /// if two or more independent "::" occurrences + /// are found. + /// + /// + /// + /// Note that ":::" is caught earlier by + /// via . However, inputs like "::1::2" + /// pass the triple-colon check while having two discrete "::" occurrences + /// that must be rejected. + /// + /// + private static bool DoubleColonsAppearsMultipleTimes(string input) + { + const string colons = "::"; + var firstIndex = input.IndexOf(colons, StringComparison.Ordinal); + return firstIndex >= 0 && input.IndexOf(colons, firstIndex + colons.Length, StringComparison.Ordinal) >= 0; + } + + #endregion // end: From Partial + + #endregion // end: Parse / TryParse + + #endregion // end: Static Factory Methods + } +} diff --git a/src/Arcus/Subnet.IComparable.cs b/src/Arcus/Subnet.IComparable.cs new file mode 100644 index 0000000..5d23a2b --- /dev/null +++ b/src/Arcus/Subnet.IComparable.cs @@ -0,0 +1,48 @@ +using Arcus.Comparers; +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif + +namespace Arcus +{ + /// + /// implementation of and + /// + public partial class Subnet : IComparable, IComparable + { + /// + public int CompareTo( +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER + [AllowNull] +#endif + object obj) + { + if (obj is null) + { + return 1; + } + + if (obj is Subnet other) + { + return CompareTo(other); + } + + throw new ArgumentException("Object is not a Subnet"); + } + + /// + public int CompareTo( +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER + [AllowNull] +#endif + Subnet other) + { + if (other is null) + { + return 1; + } + + return DefaultIIPAddressRangeComparer.Instance.Compare(this, other); + } + } +} diff --git a/src/Arcus/Subnet.IEquatable.cs b/src/Arcus/Subnet.IEquatable.cs new file mode 100644 index 0000000..ab230b8 --- /dev/null +++ b/src/Arcus/Subnet.IEquatable.cs @@ -0,0 +1,38 @@ +namespace Arcus +{ + /// + /// implementation of + /// + public partial class Subnet : IEquatable + { + /// + public virtual bool Equals(Subnet other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return this.Head.Equals(other.Head) && this.Tail.Equals(other.Tail); + } + + /// + public override bool Equals(object obj) + { + if (obj is Subnet other) + { + return Equals(other); + } + + return false; + } + + /// + public override int GetHashCode() => base.GetHashCode(); + } +} \ No newline at end of file diff --git a/src/Arcus/Subnet.ISerializable.cs b/src/Arcus/Subnet.ISerializable.cs new file mode 100644 index 0000000..f18a9c5 --- /dev/null +++ b/src/Arcus/Subnet.ISerializable.cs @@ -0,0 +1,46 @@ +#if NETSTANDARD2_0 +using System.Runtime.Serialization; + +namespace Arcus +{ + /// + /// implementation of + /// + public partial class Subnet : ISerializable + { + /// + /// The serialization format version written by . + /// Deserialization rejects data carrying a different version. + /// + /// + /// + /// Version 3: writes (Head) instead of the + /// previously-written (Tail). Head is the + /// semantically correct seed value for reconstruction; the broadcast address + /// was an implementation artefact of the legacy CtorFactory. + /// + /// + internal const int SerializationFormatVersion = 3; + + /// + /// is + public virtual void GetObjectData(SerializationInfo info, StreamingContext context) + { + if (info is null) + { + throw new ArgumentNullException(nameof(info)); + } + + info.AddValue(nameof(this.NetworkPrefixAddress), this.NetworkPrefixAddress.GetAddressBytes()); + info.AddValue(nameof(this.RoutingPrefix), this.RoutingPrefix); + info.AddValue("SerVersion", SerializationFormatVersion); + info.AddValue(nameof(this.MaxEnumerationExponent), this.MaxEnumerationExponent); + } + + /// Reconstructs a from serialization info - test affordance. + /// serialization info populated by (or hand-built for version tests). + /// The deserialized . + internal static Subnet DeserializeFromInfo(SerializationInfo info) => new(DeserializeSubnet(info)); + } +} +#endif diff --git a/src/Arcus/Subnet.Operators.cs b/src/Arcus/Subnet.Operators.cs new file mode 100644 index 0000000..0cd19ab --- /dev/null +++ b/src/Arcus/Subnet.Operators.cs @@ -0,0 +1,85 @@ +namespace Arcus +{ + /// + /// operators + /// + public partial class Subnet + { + /// + /// Determines whether two instances are equal. + /// + /// The first instance. + /// The second instance. + /// if the instances are equal; otherwise, . + public static bool operator ==(Subnet left, Subnet right) + { + if (ReferenceEquals(left, right)) + { + return true; + } + + if (left is null || right is null) + { + return false; + } + + return left.Equals(right); + } + + /// + /// Determines whether two instances are not equal. + /// + /// The first instance. + /// The second instance. + /// if the instances are not equal; otherwise, . + public static bool operator !=(Subnet left, Subnet right) => !(left == right); + + /// + /// Determines whether one is strictly less than another. + /// + /// The first instance. + /// The second instance. + /// if is less than . + public static bool operator <(Subnet left, Subnet right) + { + if (left is null) + { + return right is not null; + } + + return left.CompareTo(right) < 0; + } + + /// + /// Determines whether one is strictly greater than another. + /// + /// The first instance. + /// The second instance. + /// if is greater than . + public static bool operator >(Subnet left, Subnet right) + { + if (left is null) + { + return false; + } + + return left.CompareTo(right) > 0; + } + + /// + /// Determines whether one is less than or equal to another. + /// + /// The first instance. + /// The second instance. + /// if is less than or equal to . + public static bool operator <=(Subnet left, Subnet right) => !(left > right); + + /// + /// Determines whether one is greater than or equal to another. + /// + /// The first instance. + /// The second instance. + /// if is greater than or equal to . + public static bool operator >=(Subnet left, Subnet right) => !(left < right); + } +} diff --git a/src/Arcus/Subnet.cs b/src/Arcus/Subnet.cs index 57fe020..786ec4e 100644 --- a/src/Arcus/Subnet.cs +++ b/src/Arcus/Subnet.cs @@ -1,169 +1,145 @@ -using System; -using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; -using System.Linq; using System.Net; -using System.Net.Sockets; using System.Numerics; -using System.Runtime.Serialization; -using System.Text.RegularExpressions; -using Arcus.Comparers; -using Arcus.Converters; using Arcus.Math; using Arcus.Utilities; -using Gulliver; +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif + +#if NETSTANDARD2_0 +using System.Runtime.Serialization; +#endif namespace Arcus { /// /// An IPv4 or IPv6 subnetwork representation - the work horse and original intention of the Arcus library /// + /// + /// + /// IPv4 addresses are 32-bit unsigned integers per + /// RFC 791 §2.3 (routing prefix bounded [0, 32]). + /// IPv6 addresses are 128-bit unsigned integers per + /// RFC 4291 §2.1 (routing prefix bounded [0, 128]). + /// Subnet mask semantics are defined in + /// RFC 950 §2. + /// CIDR prefix notation (address/prefix-length) is defined in + /// RFC 4632 §2. + /// + /// + [DebuggerDisplay("{ToString()}")] [Serializable] - public class Subnet : AbstractIPAddressRange, -#if NETSTANDARD2_0 - ISerializable, -#endif - IEquatable, IComparable, IComparable + public partial class Subnet : AbstractIPAddressRange { - /// - /// Pattern that passes on valid IPv4 octet partials - /// - private const string Ipv4OctetPartialPattern = - @"^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){0,2}(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)?)?$"; - - /// - /// Pattern for rough shape of a subnet string - /// - private const string RoughSubnetStringPattern = @"^([\da-fA-F:.]+)(?:/([\d]+))?$"; - - /// - /// Regex that passes on valid IPv4 octet partials - /// - private static readonly Regex IPv4OctetPartialRegex = new Regex( - Ipv4OctetPartialPattern, - RegexOptions.Compiled | RegexOptions.CultureInvariant - ); - - /// - /// Regex for rough shape of a subnet string - /// - private static readonly Regex RoughSubnetRegex = new Regex( - RoughSubnetStringPattern, - RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase - ); - /// /// Gets the number of usable addresses in the subnet (ignores Broadcast and Network addresses) /// /// /// the number of usable addresses in the subnet (ignores Broadcast and Network addresses) /// + /// + /// + /// Excludes the network address and broadcast address as defined in + /// RFC 950 §2. + /// + /// + /// /31 subnets return 0 usable hosts per RFC 950. + /// RFC 3021 allows /31 + /// for point-to-point links; this library follows RFC 950 for usable host count. + /// + /// public BigInteger UsableHostAddressCount => Length >= 2 ? Length - 2 : 0; /// - /// Gets the broadcast of the subnet (highest order ip address) + /// Gets the broadcast address of the subnet. /// - /// - /// the broadcast of the subnet (highest order ip address) - /// + /// The highest address in the subnet (all host bits set), equivalent to . + /// + /// + /// The highest address in the subnet (all host bits set) per + /// RFC 950 §2. + /// + /// public IPAddress BroadcastAddress => Tail; /// - /// Gets the calculated Netmask of the subnet, only valid for IPv4 based subnets will be on IPv6 - /// subnets + /// Gets the subnet mask for this subnet. /// /// - /// the calculated Netmask of the subnet, only valid for IPv4 based subnets will be on IPv6 - /// subnets + /// The subnet mask as an ; for IPv6 subnets + /// (IPv6 uses prefix length only, per RFC 4291). /// + /// + /// + /// A valid subnet mask is a contiguous sequence of leading 1-bits followed by 0-bits per + /// RFC 950 §2. + /// IPv4 only; for IPv6 subnets. + /// + /// public IPAddress Netmask { get; } /// - /// Gets the network prefix of the subnet (lowest order IP address) + /// Gets the network prefix address of the subnet. /// - /// - /// the network prefix of the subnet (lowest order IP address) - /// + /// The lowest address in the subnet (all host bits cleared), equivalent to . + /// + /// + /// The network address derived by AND-ing the IP address with the subnet mask per + /// RFC 4632 §2. + /// + /// public IPAddress NetworkPrefixAddress => Head; /// - /// Gets the routing prefix used to specify the ip address + /// Gets the CIDR routing prefix length. /// /// - /// the routing prefix used to specify the ip address + /// The prefix length in CIDR notation. Valid range is [0, 32] for IPv4 and [0, 128] for IPv6 + /// per RFC 4632 §2. /// + /// + /// + /// The prefix length in CIDR notation per + /// RFC 4632 §2. + /// Valid range is [0, 32] for IPv4 and [0, 128] for IPv6. + /// + /// public int RoutingPrefix { get; } - #region From Interface IComparable - - /// - public int CompareTo(object obj) - { - if (obj is null) - { - return 1; - } - - if (obj is Subnet other) - { - return CompareTo(other); - } - - throw new ArgumentException("Object is not an Subnet"); - } - - #endregion - - #region From Interface IComparable - - /// - public int CompareTo(Subnet other) - { - if (other is null) - { - return 1; - } - - return DefaultIIPAddressRangeComparer.Instance.Compare(this, other); - } - - #endregion - #region Formatting /// - public override string ToString(string format, IFormatProvider formatProvider) + /// + /// + /// The g/G (and default) format produces canonical CIDR notation (address/prefix-length) + /// per RFC 4632 §2. + /// + /// + public override string ToString( +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER + [AllowNull] +#endif + string format, +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER + [AllowNull] +#endif + IFormatProvider formatProvider) { - if (formatProvider is null) - { - formatProvider = CultureInfo.InvariantCulture; - } + formatProvider ??= CultureInfo.InvariantCulture; - switch (format?.Trim()) + return format?.Trim() switch { // unspecified - case null: - case "": - - // general formats - case "g": - case "G": - return $"{this.NetworkPrefixAddress}/{this.RoutingPrefix}"; - + null or { Length: 0 } or "g" or "G" => $"{this.NetworkPrefixAddress}/{this.RoutingPrefix}", // "friendly" formats - case "f": - case "F": - return IsSingleIP ? $"{this.NetworkPrefixAddress}" : $"{this.NetworkPrefixAddress}/{this.RoutingPrefix}"; - + "f" or "F" => IsSingleIP ? $"{this.NetworkPrefixAddress}" : $"{this.NetworkPrefixAddress}/{this.RoutingPrefix}", // range formats - case "r": - case "R": - return $"{this.NetworkPrefixAddress} - {this.BroadcastAddress}"; - + "r" or "R" => $"{this.NetworkPrefixAddress} - {this.BroadcastAddress}", // delegate to base - default: - return base.ToString(format, formatProvider); - } + _ => base.ToString(format, formatProvider), + }; } #endregion // end: Formatting @@ -177,12 +153,16 @@ public override string ToString(string format, IFormatProvider formatProvider) /// true if the passed subnet is contained within this public bool Contains(Subnet subnet) { - return subnet != null && Contains(subnet.NetworkPrefixAddress) && Contains(subnet.BroadcastAddress); + return Contains((IIPAddressRange)subnet); } /// /// check if the given subnets overlaps this /// + /// + /// For CIDR subnets, two subnets are either disjoint or one fully contains the other. + /// This method exploits that property for an efficient check. + /// /// the subnet to check the overlap of /// true if there is an overlap public bool Overlaps(Subnet subnet) @@ -201,121 +181,60 @@ public bool Overlaps(Subnet subnet) /// (lower and higher bounds) but this is not necessary. /// Addresses *MUST* be the same address family (either Internetwork or InternetworkV6) /// + /// + /// + /// Constructs the smallest subnet (largest routing prefix) that contains both addresses per + /// RFC 4632 §2. + /// + /// /// a address to be contained within the subnet /// another address to be contained within the subnet - public Subnet(IPAddress lowAddress, IPAddress highAddress) - : base(CtorFactory(lowAddress, highAddress)) - { - var result = NormalizeAndCreateNetMask(Head, Tail); // TODO the execution of this call is logically redundant as it is used in the call of the base constructor - this.RoutingPrefix = result.Prefix; - this.Netmask = IsIPv4 ? result.Mask : null; // only set netmask for IPv4 based subnets - } - - private static AddressTuple CtorFactory(IPAddress lowAddress, IPAddress highAddress) - { - #region Defense - - if (lowAddress is null) - { - throw new ArgumentNullException(nameof(lowAddress)); - } - - if (highAddress is null) - { - throw new ArgumentNullException(nameof(highAddress)); - } - - if (lowAddress.AddressFamily != highAddress.AddressFamily) - { - throw new ArgumentException( - $"{nameof(lowAddress)} and {nameof(highAddress)} must have matching address families" - ); - } - - if (!IPAddressUtilities.ValidAddressFamilies.Contains(lowAddress.AddressFamily)) - { - throw new ArgumentException( - $"{nameof(lowAddress)} must have an address family equal to {string.Join(", ", IPAddressUtilities.ValidAddressFamilies)}", - nameof(lowAddress) - ); - } - - if (!IPAddressUtilities.ValidAddressFamilies.Contains(highAddress.AddressFamily)) - { - throw new ArgumentException( - $"{nameof(highAddress)} must have an address family equal to {string.Join(", ", IPAddressUtilities.ValidAddressFamilies)}", - nameof(highAddress) - ); - } - - if (!highAddress.IsGreaterThanOrEqualTo(lowAddress)) - { - throw new InvalidOperationException($"{nameof(highAddress)} must be greater or equal to {nameof(lowAddress)}"); - } - - #endregion // end: Defense - - var result = NormalizeAndCreateNetMask(lowAddress, highAddress); - return new AddressTuple(result.Head, result.Tail); - } + /// the maximum enumeration exponent (0-128); enumeration yields at most 2maxEnumerationExponent addresses + public Subnet(IPAddress lowAddress, IPAddress highAddress, int maxEnumerationExponent = DefaultMaxEnumerationExponent) + : this(CtorFactory(lowAddress, highAddress, maxEnumerationExponent)) { } /// /// Initializes a new instance of the class. /// + /// + /// + /// Constructs a subnet from an address and CIDR prefix length per + /// RFC 4632 §2. + /// The network prefix address is derived by AND-ing with the prefix mask. + /// + /// /// the ip address /// the routing prefix + /// the maximum enumeration exponent (0-128, default 12) /// IP Address must be IPv4 or IPv6 /// Routing prefix is out of range - public Subnet(IPAddress address, int routingPrefix) - : base(CtorFactory(address, routingPrefix)) - { - var result = NormalizeAndCreateNetMask(Head, routingPrefix); // TODO the execution of this call is logically redundant as it is used in the call of the base constructor - this.RoutingPrefix = routingPrefix; - this.Netmask = IsIPv4 ? result.Mask : null; // only set netmask for IPv4 based subnets - } + public Subnet(IPAddress address, int routingPrefix, int maxEnumerationExponent = DefaultMaxEnumerationExponent) + : this(CtorFactory(address, routingPrefix, maxEnumerationExponent)) { } - private static AddressTuple CtorFactory(IPAddress address, int routingPrefix) + /// + /// Initializes a new instance of the class. + /// Private constructor that receives pre-computed Head/Tail, netmask, routing prefix, and maxEnumerationExponent. + /// Avoids redundant calls to . + /// + private Subnet(CtorFactoryResult result) + : base(result.Tuple, result.MaxEnumerationExponent) { - #region Defense - - if (address is null) - { - throw new ArgumentNullException(nameof(address)); - } - - if (!IPAddressUtilities.ValidAddressFamilies.Contains(address.AddressFamily)) - { - throw new ArgumentException( - $"{nameof(address)} must have an address family equal to {string.Join(", ", IPAddressUtilities.ValidAddressFamilies)}", - nameof(address) - ); - } - - if (routingPrefix < 0) - { - throw new ArgumentOutOfRangeException(nameof(routingPrefix)); - } - - if (address.IsIPv4() && routingPrefix > IPAddressUtilities.IPv4BitCount) - { - throw new ArgumentOutOfRangeException(nameof(routingPrefix)); - } - - if (address.IsIPv6() && routingPrefix > IPAddressUtilities.IPv6BitCount) - { - throw new ArgumentOutOfRangeException(nameof(routingPrefix)); - } - - #endregion // end: Defense - - var result = NormalizeAndCreateNetMask(address, routingPrefix); - return new AddressTuple(result.Head, result.Tail); + this.RoutingPrefix = result.RoutingPrefix; + this.Netmask = IsIPv4 ? result.Netmask : null; } /// /// Initializes a new instance of the class. /// contains only a single ip address /// + /// + /// + /// Creates a host route: /32 for IPv4 per + /// RFC 791 §2.3, + /// or /128 for IPv6 per + /// RFC 4291 §2.1. + /// + /// /// the ip address public Subnet(IPAddress address) : base(address, address) @@ -332,755 +251,245 @@ public Subnet(IPAddress address) this.Netmask = new IPAddress(netmaskBytes); } +#if NETSTANDARD2_0 /// Initializes a new instance of the class. /// serialization info /// serialization context /// is protected Subnet(SerializationInfo info, StreamingContext context) - : this( - new IPAddress( - (byte[]) - (info ?? throw new ArgumentNullException(nameof(info))).GetValue( - nameof(BroadcastAddress), - typeof(byte[]) - ) - ), - (int)info.GetValue(nameof(RoutingPrefix), typeof(int)) - ) { } - - #endregion // end: Ctor - - #region static factory methods - - #region FromNetMask - - /// - /// Create a subnet from an IP Address and netmask - /// - /// the ip address - /// the net mask - /// The created subnet - /// ipAddress - /// netmask - /// the given IP Address is not IPv4 - /// the given netmask is invalid - public static Subnet FromNetMask(IPAddress address, IPAddress netmask) - { - if (address is null) - { - throw new ArgumentNullException(nameof(address)); - } - - if (netmask is null) - { - throw new ArgumentNullException(nameof(netmask)); - } - - if (!address.IsIPv4()) - { - throw new ArgumentException($"{nameof(address)} must be IPv4", nameof(address)); - } - - if (!netmask.IsValidNetMask()) - { - throw new ArgumentException($"{nameof(netmask)} must be IPv4", nameof(netmask)); - } + : this(DeserializeSubnet(info)) { } - try - { - return new Subnet(address, netmask.NetmaskToCidrRoutePrefix()); - } - catch (Exception e) - { - throw new Exception("could not instantiate subnet", e); - } - } - - /// - /// Try to create a subnet from an IP Address and netmask - /// - /// the ip address - /// the net mask - /// the created subnet or on failure - /// on success - public static bool TryFromNetMask(IPAddress address, IPAddress netmask, out Subnet subnet) + private static CtorFactoryResult DeserializeSubnet(SerializationInfo info) { - try - { - subnet = FromNetMask(address, netmask); - return true; - } - catch - { - subnet = null; - return false; - } - } - - #endregion // end: FromNetMask - - /// - /// Construct the smallest possible subnet that would contain both IP addresses encoded as bytes typically the address - /// specified are the Network and Broadcast addresses (lower and higher bounds) but this is not necessary. Addresses - /// *MUST* be the same address family (either Internetwork or InternetworkV6) - /// - /// the lower address array - /// the high address array - /// The created - public static Subnet FromBytes(byte[] lowAddressBytes, byte[] highAddressBytes) - { - if (lowAddressBytes is null) - { - throw new ArgumentNullException(nameof(lowAddressBytes)); - } - - if (highAddressBytes is null) + if (info is null) { - throw new ArgumentNullException(nameof(highAddressBytes)); + throw new ArgumentNullException(nameof(info)); } - IPAddress lowAddress; + int serVersion; try { - lowAddress = new IPAddress(lowAddressBytes); + serVersion = info.GetInt32("SerVersion"); } - catch (ArgumentException e) + catch (SerializationException) { - throw new ArgumentException( - $"could not convert {nameof(lowAddressBytes)} to an IPAddress", - nameof(lowAddressBytes), - e - ); + // No version stamp: pre-versioning (v1) or v2 (BroadcastAddress) legacy data. + serVersion = 0; } - IPAddress highAddress; - try - { - highAddress = new IPAddress(highAddressBytes); - } - catch (ArgumentException e) + if (serVersion != SerializationFormatVersion) { - throw new ArgumentException( - $"could not convert {nameof(highAddressBytes)} to an IPAddress", - nameof(highAddressBytes), - e + throw new SerializationException( + $"Unsupported Subnet serialization version {serVersion}. Expected {SerializationFormatVersion}." ); } - try - { - return new Subnet(lowAddress, highAddress); - } - catch (Exception e) - { - throw new Exception("could not instantiate subnet", e); - } - } + var networkAddressBytes = (byte[])info.GetValue(nameof(NetworkPrefixAddress), typeof(byte[])); - /// - /// Construct the smallest possible subnet that would contain both IP addresses encoded as bytes typically the address - /// specified are the Network and Broadcast addresses (lower and higher bounds) but this is not necessary. Addresses - /// *MUST* be the same address family (either Internetwork or InternetworkV6) - /// - /// the lower address array - /// the high address array - /// the created subnet or on failure - /// on success - public static bool TryFromBytes(byte[] lowAddressBytes, byte[] highAddressBytes, out Subnet subnet) - { - try - { - subnet = FromBytes(lowAddressBytes, highAddressBytes); - return true; - } - catch + if (networkAddressBytes is null) { - subnet = null; - return false; + throw new SerializationException($"Serialization info does not contain '{nameof(NetworkPrefixAddress)}'."); } - } - #region Parse / TryParse + var networkAddress = new IPAddress(networkAddressBytes); + var routingPrefix = info.GetInt32(nameof(RoutingPrefix)); + var maxEnumerationExponent = info.GetInt32(nameof(MaxEnumerationExponent)); - #region subnet string - - /// - /// Unsafe parsing of a string into a subnet - /// - /// the string to parse a subnet from - /// the parsed subnet - /// could not parse input - public static Subnet Parse(string subnetString) - { - if (subnetString is null) + if (!IPAddressUtilities.ValidAddressFamilies.Contains(networkAddress.AddressFamily)) { - throw new ArgumentNullException(nameof(subnetString)); - } - - if (string.IsNullOrWhiteSpace(subnetString)) - { - throw new ArgumentException("a non empty value is expected", nameof(subnetString)); - } - - var matches = RoughSubnetRegex.Matches(subnetString); - - if (matches.Count != 1) - { - throw new FormatException("unexpected format"); - } - - var match = matches[0]; - - var addressString = match.Groups[1].Value; - var routePrefixString = match.Groups[2].Value; - - if (!IPAddress.TryParse(addressString, out var address)) // attempt to parse IP address portion - { - throw new FormatException($"cannot parse ip address \"{addressString}\""); - } - - int routingPrefix; - if (!string.IsNullOrWhiteSpace(routePrefixString)) // attempt to parse routing prefix in there is a match - { - if (!int.TryParse(routePrefixString, out routingPrefix)) - { - throw new FormatException($"cannot parse routing prefix \"{routePrefixString}\""); - } - } - else // no routing prefix match, assume it is a single address - { - routingPrefix = address.IsIPv4() ? IPAddressUtilities.IPv4BitCount : IPAddressUtilities.IPv6BitCount; - } - - if (address.IsIPv4() && routingPrefix > IPAddressUtilities.IPv4BitCount) - { - throw new ArgumentException("routing prefix must be 32 or less for an IPv4 subnet", nameof(subnetString)); - } - - if (address.IsIPv6() && routingPrefix > IPAddressUtilities.IPv6BitCount) - { - throw new ArgumentException("routing prefix must be 128 or less for an IPv6 subnet", nameof(subnetString)); - } - - try - { - return new Subnet(address, routingPrefix); - } - catch (Exception e) - { - throw new Exception("could not instantiate subnet", e); - } - } - - /// - /// Attempt to parse a string into a subnet - /// - /// the string to parse - /// the created subnet or on failure - /// on success - public static bool TryParse(string subnetString, out Subnet subnet) - { - try - { - subnet = Parse(subnetString); - return true; - } - catch - { - subnet = null; - return false; - } - } - - #endregion // end: subnet string - - #region address, routing prefix - - /// - /// Unsafe parsing of a string address and routing prefix into a subnet - /// - /// the address string - /// the subnet routing prefix - /// - /// has an invalid - /// - /// is . - /// is less than 0 - /// - /// is out of range of the provided - /// - /// - /// The parsed - public static Subnet Parse(string addressString, int routingPrefix) - { - if (addressString is null) - { - throw new ArgumentNullException(nameof(addressString)); - } - - if (!IPAddress.TryParse(addressString, out var address)) - { - throw new FormatException($"could not parse {nameof(addressString)} value \"{addressString}\""); - } - - if (!IPAddressUtilities.ValidAddressFamilies.Contains(address.AddressFamily)) - { - throw new ArgumentException( - $"must have an address family equal to {string.Join(", ", IPAddressUtilities.ValidAddressFamilies)}", - nameof(addressString) + throw new SerializationException( + $"Deserialized '{nameof(NetworkPrefixAddress)}' has unsupported address family '{networkAddress.AddressFamily}'." ); } - if (routingPrefix < 0) - { - throw new ArgumentOutOfRangeException(nameof(routingPrefix)); - } - - if (address.IsIPv4() && routingPrefix > IPAddressUtilities.IPv4BitCount) - { - throw new ArgumentOutOfRangeException(nameof(routingPrefix)); - } - - if (address.IsIPv6() && routingPrefix > IPAddressUtilities.IPv6BitCount) - { - throw new ArgumentOutOfRangeException(nameof(routingPrefix)); - } + var maxPrefixLength = networkAddress.IsIPv4() ? IPAddressUtilities.IPv4BitCount : IPAddressUtilities.IPv6BitCount; - try - { - return new Subnet(address, routingPrefix); - } - catch (Exception e) + if (routingPrefix < 0 || routingPrefix > maxPrefixLength) { - throw new Exception("could not instantiate subnet", e); + throw new SerializationException( + $"Deserialized '{nameof(RoutingPrefix)}' value {routingPrefix} is out of range [0, {maxPrefixLength}]." + ); } - } - /// - /// Try to parse as an for a Subnet with the given routing - /// prefix - /// - /// the address string - /// the subnet routing prefix - /// the created subnet or on failure - /// on success - public static bool TryParse(string addressString, int routingPrefix, out Subnet subnet) - { - try - { - subnet = Parse(addressString, routingPrefix); - return true; - } - catch - { - subnet = null; - return false; - } + return CtorFactory(networkAddress, routingPrefix, maxEnumerationExponent); } +#endif - #endregion // end: address, routing prefix - - #region string string - - /// - /// Unsafe parsing of two string as a new subnet - /// - /// the low address string - /// the high address string - /// The parsed - public static Subnet Parse(string lowAddressString, string highAddressString) + private static CtorFactoryResult CtorFactory( + IPAddress lowAddress, + IPAddress highAddress, + int maxEnumerationExponent = DefaultMaxEnumerationExponent + ) { - if (lowAddressString is null) - { - throw new ArgumentNullException(nameof(lowAddressString)); - } - - if (highAddressString is null) - { - throw new ArgumentNullException(nameof(highAddressString)); - } + #region Defense - if (!IPAddress.TryParse(lowAddressString, out var lowAddress)) + if (lowAddress is null) { - throw new FormatException($"could not parse {nameof(lowAddressString)} value \"{lowAddressString}\""); + throw new ArgumentNullException(nameof(lowAddress)); } - if (!IPAddress.TryParse(highAddressString, out var highAddress)) + if (highAddress is null) { - throw new FormatException($"could not parse {nameof(highAddressString)} value \"{highAddressString}\""); + throw new ArgumentNullException(nameof(highAddress)); } if (lowAddress.AddressFamily != highAddress.AddressFamily) { throw new ArgumentException( - $"{nameof(lowAddressString)} and {nameof(highAddressString)} must have matching address families" + $"{nameof(lowAddress)} and {nameof(highAddress)} must have matching address families" ); } if (!IPAddressUtilities.ValidAddressFamilies.Contains(lowAddress.AddressFamily)) { throw new ArgumentException( - $"{nameof(lowAddressString)} must have an address family equal to {string.Join(", ", IPAddressUtilities.ValidAddressFamilies)}", - nameof(lowAddressString) + $"{nameof(lowAddress)} must have an address family equal to {string.Join(", ", IPAddressUtilities.ValidAddressFamilies)}", + nameof(lowAddress) ); } if (!IPAddressUtilities.ValidAddressFamilies.Contains(highAddress.AddressFamily)) { throw new ArgumentException( - $"{nameof(highAddressString)} must have an address family equal to {string.Join(", ", IPAddressUtilities.ValidAddressFamilies)}", - nameof(highAddressString) + $"{nameof(highAddress)} must have an address family equal to {string.Join(", ", IPAddressUtilities.ValidAddressFamilies)}", + nameof(highAddress) ); } if (!highAddress.IsGreaterThanOrEqualTo(lowAddress)) { - throw new InvalidOperationException( - $"{nameof(highAddressString)} must be greater or equal to {nameof(lowAddressString)}" - ); - } - - try - { - return new Subnet(lowAddress, highAddress); - } - catch (Exception e) - { - throw new Exception("could not instantiate subnet", e); - } - } - - /// - /// Try parse two strings as addresses - /// - /// the low address string - /// the high address string - /// the created subnet or on failure - /// on success - public static bool TryParse(string lowAddressString, string highAddressString, out Subnet subnet) - { - try - { - subnet = Parse(lowAddressString, highAddressString); - - return true; - } - catch - { - subnet = null; - return false; - } - } - - #endregion // end: string string - - #region From Partial - - /// - /// Try to convert a partial IPv4 address into a subnet based on found provided partial octets - /// - /// the partial IP address to parse - /// the subnet created - /// true on success - public static bool TryIPv4FromPartial(string input, out Subnet subnet) - { - if (string.IsNullOrWhiteSpace(input) || !IPv4OctetPartialRegex.IsMatch(input)) - { - subnet = null; - return false; - } - - input = input.TrimEnd('.'); - var octetCount = input.Count(c => c == '.') + 1; - var addressString = input + string.Concat(Enumerable.Repeat(".0", IPAddressUtilities.IPv4OctetCount - octetCount)); - - try - { - subnet = Parse(addressString, octetCount * 8); - return true; - } - catch - { - subnet = null; - return false; + throw new InvalidOperationException($"{nameof(highAddress)} must be greater or equal to {nameof(lowAddress)}"); } - } - /// - /// Given a IPv6 cidr-like or IPv6 like string build a collection of all possible valid subnets that could be intended - /// by the input - /// - /// the partial ipv6 cidr or address - /// a collection of all possible matching subnets on success, or an empty collection on failure - /// on success - [Obsolete( - "the needs for this method are very specialized and may not be what the developer is expecting; this is likely to be replaced by a host of other more explicit and useful methods" - )] - public static bool TryIPv6FromPartial(string input, out IEnumerable subnets) - { - // try to discover possible garbage or incomplete input - int hextetCount; - if ( - string.IsNullOrWhiteSpace(input) - || input.Equals(":", StringComparison.OrdinalIgnoreCase) - || input.Contains(":::") - || DoubleColonsAppearsMultipleTimes(input) - || (hextetCount = input.Split(new[] { ':' }, StringSplitOptions.RemoveEmptyEntries).Length) - > IPAddressUtilities.IPv6HextetCount - || ( - hextetCount >= IPAddressUtilities.IPv6HextetCount && input.EndsWith(":", StringComparison.OrdinalIgnoreCase) - ) - ) // too many hextets - { - subnets = Enumerable.Empty(); - return false; - } + #endregion // end: Defense - // parseable as a well defined IPv6 subnet already - if ( - input.Contains('/') - && !input.EndsWith("/", StringComparison.Ordinal) - && TryParse(input, out var subnet) - && subnet.IsIPv6 - ) + if (lowAddress.IsIPv4()) { - subnets = new[] { subnet }; - return true; + var result = NormalizeAndCreateNetMask(lowAddress, highAddress); + return new CtorFactoryResult( + new AddressTuple(result.Head, result.Tail), + result.Mask, + result.Prefix, + maxEnumerationExponent + ); } - // TODO this could probably be done more cleanly, and provide a more expected result + var headBytes = lowAddress.GetAddressBytes(); + var tailBytes = highAddress.GetAddressBytes(); + var prefix = CalculateRoutingPrefix(headBytes, tailBytes); + var headWrap = BigEndianBitWrapper.FromBytes(headBytes); + var maskWrap = BigEndianBitWrapper.CreateMask(headWrap.ByteWidth, prefix); + return new CtorFactoryResult( + new AddressTuple( + new IPAddress((headWrap & maskWrap).ToBytes()), + new IPAddress((headWrap | ~maskWrap).ToBytes()) + ), + null, + prefix, + maxEnumerationExponent + ); - // a IPv6 cidr partial is provided - if ( - ( - IPAddress.TryParse(input, out var address) // treat as a complete address, may contain a '::' or not - || (input.Contains("::") && IPAddress.TryParse(input.TrimEnd(':'), out address)) - || IPAddress.TryParse(input.TrimEnd(':') + "::", out address) - ) && address.IsIPv6() - ) // no collapse, but incomplete address + static int CalculateRoutingPrefix(byte[] hb, byte[] tb) { - var trimmedPartial = input.TrimEnd(':'); // remove trailing ":" or "::" if exists - - // break up entry on hextets, an empty hextet implies a collapse - var hextets = trimmedPartial - .Split(new[] { ':' }, StringSplitOptions.None) // DO NOT remove empty splits - .ToList(); - - // should contain an empty, pump the first with appropriate values - - // get the index of the collapse - var collapse = hextets - .Select((value, index) => new { value, index }) - .FirstOrDefault(pair => string.IsNullOrWhiteSpace(pair.value)); - - int collapseIndex; - if (collapse is null && hextets.Count == 8) // no collapse - fully fledged address, all 8 hextets present - { - subnets = new[] { Parse(input, IPAddressUtilities.IPv6BitCount) }; - return true; - } - - // introduce a collapse at the end if one does not exist - if (collapse is null) // not fully fledged, add a collapse to the end - { - hextets.Add(string.Empty); - collapseIndex = hextets.Count - 1; - } - else // a collapse is present - { - collapseIndex = collapse.index; - } - - var subnetsList = new List(); - - hextetCount = hextets.Count; - var permutationLimit = (IPAddressUtilities.IPv6HextetCount + 1) - hextetCount; // number of permutations of IP partial, every hextet available removes a permutation - for (var permutation = 0; permutation <= permutationLimit; permutation++) + var bitCount = hb.Length * 8; + for (var i = 0; i < bitCount; i++) { - var hextetsCopy = hextets.ToList(); - hextetsCopy.RemoveAt(collapseIndex); // collapsed empty item - hextetsCopy.InsertRange(collapseIndex, Enumerable.Repeat("0", permutation)); // add zeros as appropriate - - var addressString = string.Join(":", hextetsCopy); // re join string - - if ( - !IPAddress.TryParse(addressString + "::", out var subnetAddress) - && !IPAddress.TryParse(addressString, out subnetAddress) - ) + var byteIndex = i / 8; + var bitmask = (byte)(0x80 >> (i % 8)); + if ((hb[byteIndex] & bitmask) != (tb[byteIndex] & bitmask)) { - subnets = Enumerable.Empty(); - return false; + return i; } - - var routePrefix = ((permutation + hextetCount) - 1) * 16; - subnetsList.Add(new Subnet(subnetAddress, routePrefix)); } - subnets = subnetsList; - return true; - } - - // fail; could not parse anything useful from the input - subnets = Enumerable.Empty(); - return false; - - // checks input for multiple occurrences of discrete "::" substrings -#if NET6_0_OR_GREATER - static -#endif - bool DoubleColonsAppearsMultipleTimes(string @in) - { - const string colons = "::"; - int firstIndex; - - return @in.Length >= 4 - && (firstIndex = @in.IndexOf(colons, StringComparison.Ordinal)) != -1 - && @in.Substring(firstIndex + 2, @in.Length - firstIndex - 2).IndexOf(colons, StringComparison.Ordinal) - != -1; + return bitCount; } } - #endregion // end: From Partial - - #endregion // end: Parse / TryParse - - #endregion // end: Static Factory Methods - - /// - public virtual bool Equals(Subnet other) + private static CtorFactoryResult CtorFactory( + IPAddress address, + int routingPrefix, + int maxEnumerationExponent = DefaultMaxEnumerationExponent + ) { - if (other is null) - { - return false; - } + #region Defense - if (ReferenceEquals(this, other)) + if (address is null) { - return true; + throw new ArgumentNullException(nameof(address)); } - return DefaultIIPAddressRangeComparer.Instance.Compare(this, other) == 0; - } - - /// - public override bool Equals(object obj) - { - if (obj is Subnet other) + if (!IPAddressUtilities.ValidAddressFamilies.Contains(address.AddressFamily)) { - return Equals(other); + throw new ArgumentException( + $"{nameof(address)} must have an address family equal to {string.Join(", ", IPAddressUtilities.ValidAddressFamilies)}", + nameof(address) + ); } - return false; - } - - /// - public override int GetHashCode() => HashCode.Combine(AddressFamily, Head, RoutingPrefix); - -#if NETSTANDARD2_0 - #region From Interface ISerializable - - /// - /// is - public void GetObjectData(SerializationInfo info, StreamingContext context) - { - if (info is null) + if (routingPrefix < 0) { - throw new ArgumentNullException(nameof(info)); + throw new ArgumentOutOfRangeException(nameof(routingPrefix)); } - info.AddValue(nameof(this.BroadcastAddress), this.BroadcastAddress.GetAddressBytes()); - info.AddValue(nameof(this.RoutingPrefix), this.RoutingPrefix); - } - - #endregion -#endif - - #region operators - - /// - /// Determines whether two instances are equal. - /// - /// The first instance. - /// The second instance. - /// true if both instances are equal; otherwise, false. - public static bool operator ==(Subnet left, Subnet right) - { - if (ReferenceEquals(left, right)) + if (address.IsIPv4() && routingPrefix > IPAddressUtilities.IPv4BitCount) { - return true; + throw new ArgumentOutOfRangeException(nameof(routingPrefix)); } - if (left is null || right is null) + if (address.IsIPv6() && routingPrefix > IPAddressUtilities.IPv6BitCount) { - return false; + throw new ArgumentOutOfRangeException(nameof(routingPrefix)); } - return left.Equals(right); - } - - /// - /// Determines whether two instances are not equal. - /// - /// The first instance. - /// The second instance. - /// true if the instances are not equal; otherwise, false. - public static bool operator !=(Subnet left, Subnet right) => !(left == right); + #endregion // end: Defense - /// - /// Compares two instances to determine if the first is less than the second. - /// - /// The first instance. - /// The second instance. - /// true if the first instance is less than the second; otherwise, false. - public static bool operator <(Subnet left, Subnet right) - { - if (left is null) + if (address.IsIPv4()) { - return !(right is null); // null is less than any non-null instance + var result = NormalizeAndCreateNetMask(address, routingPrefix); + return new CtorFactoryResult( + new AddressTuple(result.Head, result.Tail), + result.Mask, + routingPrefix, + maxEnumerationExponent + ); } - return left.CompareTo(right) < 0; + var headWrap = BigEndianBitWrapper.FromBytes(address.GetAddressBytes()); + var maskWrap = BigEndianBitWrapper.CreateMask(headWrap.ByteWidth, routingPrefix); + return new CtorFactoryResult( + new AddressTuple( + new IPAddress((headWrap & maskWrap).ToBytes()), + new IPAddress((headWrap | ~maskWrap).ToBytes()) + ), + null, + routingPrefix, + maxEnumerationExponent + ); } - /// - /// Compares two instances to determine if the first is greater than the second. - /// - /// The first instance. - /// The second instance. - /// true if the first instance is greater than the second; otherwise, false. - public static bool operator >(Subnet left, Subnet right) + private readonly struct CtorFactoryResult( + AddressTuple tuple, + IPAddress netmask, + int routingPrefix, + int maxEnumerationExponent + ) { - if (left is null) - { - return false; - } - - return left.CompareTo(right) > 0; + public AddressTuple Tuple { get; } = tuple; + public IPAddress Netmask { get; } = netmask; + public int RoutingPrefix { get; } = routingPrefix; + public int MaxEnumerationExponent { get; } = maxEnumerationExponent; } - /// - /// Compares two instances to determine if the first is less than or equal to the second. - /// - /// The first instance. - /// The second instance. - /// true if the first instance is less than or equal to the second; otherwise, false. - public static bool operator <=(Subnet left, Subnet right) => left < right || left == right; - - /// - /// Compares two instances to determine if the first is greater than or equal to the second. - /// - /// The first instance. - /// The second instance. - /// true if the first instance is greater than or equal to the second; otherwise, false. - public static bool operator >=(Subnet left, Subnet right) => left > right || left == right; - - #endregion operators + #endregion // end: Ctor - #region Static metods, may be appropriate for extracting + #region Static methods, may be appropriate for extracting private readonly struct AddressMaskAndPrefixTuple { +#pragma warning disable S5766 // Data validated upstream in DeserializeSubnet -> CtorFactory chain public AddressMaskAndPrefixTuple(IPAddress head, IPAddress tail, IPAddress mask, int prefix) +#pragma warning restore S5766 { this.Head = head ?? throw new ArgumentNullException(nameof(head)); this.Tail = tail ?? throw new ArgumentNullException(nameof(tail)); @@ -1095,39 +504,64 @@ public AddressMaskAndPrefixTuple(IPAddress head, IPAddress tail, IPAddress mask, } /// - /// Gets head + /// Gets the network prefix address. /// public IPAddress Head { get; } /// - /// Gets tail + /// Gets the broadcast address. /// public IPAddress Tail { get; } /// - /// Gets mask + /// Gets the subnet mask. /// public IPAddress Mask { get; } /// - /// Gets prefix + /// Gets the CIDR routing prefix length. /// public int Prefix { get; } } + private readonly struct AddressAndMaskTuple(IPAddress head, IPAddress tail, IPAddress mask) + { + /// + /// Gets the network prefix address. + /// + public IPAddress Head { get; } = head ?? throw new ArgumentNullException(nameof(head)); + + /// + /// Gets the broadcast address. + /// + public IPAddress Tail { get; } = tail ?? throw new ArgumentNullException(nameof(tail)); + + /// + /// Gets the subnet mask. + /// + public IPAddress Mask { get; } = mask ?? throw new ArgumentNullException(nameof(mask)); + } + private static AddressMaskAndPrefixTuple NormalizeAndCreateNetMask(IPAddress head, IPAddress tail) { var headBytes = head.GetAddressBytes(); var tailBytes = tail.GetAddressBytes(); var routingPrefix = CalculateRoutingPrefix(headBytes, tailBytes); - var result = NormalizeAndCreateNetMask(head, routingPrefix); + + // Reuse the already-fetched headBytes instead of re-calling head.GetAddressBytes() + // via the (IPAddress, int) overload. This avoids a redundant allocation on the IPv4 + // hot path; the AND/OR/mask computation is identical to NormalizeAndCreateNetMask(IPAddress, int). + var headWrap = BigEndianBitWrapper.FromBytes(headBytes); + var maskWrap = BigEndianBitWrapper.CreateMask(headWrap.ByteWidth, routingPrefix); + var result = new AddressAndMaskTuple( + new IPAddress((headWrap & maskWrap).ToBytes()), + new IPAddress((headWrap | ~maskWrap).ToBytes()), + new IPAddress(maskWrap.ToBytes()) + ); return new AddressMaskAndPrefixTuple(result.Head, result.Tail, result.Mask, routingPrefix); -#if NET6_0_OR_GREATER - static -#endif - int CalculateRoutingPrefix(byte[] hb, byte[] tb) + static int CalculateRoutingPrefix(byte[] hb, byte[] tb) { var bitCount = hb.Length * 8; // 8 bits per byte @@ -1146,46 +580,15 @@ int CalculateRoutingPrefix(byte[] hb, byte[] tb) } } - private readonly struct AddressAndMaskTuple - { - public AddressAndMaskTuple(IPAddress head, IPAddress tail, IPAddress mask) - { - this.Head = head ?? throw new ArgumentNullException(nameof(head)); - this.Tail = tail ?? throw new ArgumentNullException(nameof(tail)); - this.Mask = mask ?? throw new ArgumentNullException(nameof(mask)); - } - - /// - /// Gets head - /// - public IPAddress Head { get; } - - /// - /// Gets tail - /// - public IPAddress Tail { get; } - - /// - /// Gets mask - /// - public IPAddress Mask { get; } - } - private static AddressAndMaskTuple NormalizeAndCreateNetMask(IPAddress head, int routingPrefix) { - var headBytes = head.GetAddressBytes(); - var addressByteLength = headBytes.Length; - - var maskBytes = Enumerable - .Repeat((byte)0xFF, addressByteLength) - .ToArray() - .ShiftBitsLeft((addressByteLength * 8) - routingPrefix); - - var newHead = new IPAddress(ByteArrayUtils.BitwiseAndBigEndian(headBytes, maskBytes)); - var newTail = new IPAddress(ByteArrayUtils.BitwiseOrBigEndian(headBytes, ByteArrayUtils.BitwiseNot(maskBytes))); - var netmask = new IPAddress(maskBytes); - - return new AddressAndMaskTuple(newHead, newTail, netmask); + var headWrap = BigEndianBitWrapper.FromBytes(head.GetAddressBytes()); + var maskWrap = BigEndianBitWrapper.CreateMask(headWrap.ByteWidth, routingPrefix); + return new AddressAndMaskTuple( + new IPAddress((headWrap & maskWrap).ToBytes()), + new IPAddress((headWrap | ~maskWrap).ToBytes()), + new IPAddress(maskWrap.ToBytes()) + ); } #endregion // end: Static metods, may be appropriate for extracting @@ -1197,7 +600,7 @@ private static AddressAndMaskTuple NormalizeAndCreateNetMask(IPAddress head, int /// /// the subnet /// the subnet - /// the subnet , will be for non IPv6 subnets + /// the subnet , will be for IPv6 subnets /// the subnet public void Deconstruct( out IPAddress networkPrefixAddress, diff --git a/src/Arcus/Utilities/IPAddressUtilities.cs b/src/Arcus/Utilities/IPAddressUtilities.cs index f7b4238..cf1e2c6 100644 --- a/src/Arcus/Utilities/IPAddressUtilities.cs +++ b/src/Arcus/Utilities/IPAddressUtilities.cs @@ -1,97 +1,150 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; +using System.Net; using System.Net.Sockets; using System.Text.RegularExpressions; -using Gulliver; +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif namespace Arcus.Utilities { /// /// Static utility class containing miscellaneous operations for objects /// - public static class IPAddressUtilities + /// + /// + /// IPv4 constants are derived from the 32-bit address definition in + /// RFC 791 §2.3. + /// IPv6 constants are derived from the 128-bit address definition in + /// RFC 4291 §2.1. + /// + /// + public static partial class IPAddressUtilities { private const int BitsPerByte = 8; - private const string DottedQuadLeadingZerosPattern = @"(?<=^|\.)0+(?!\.|$)"; - private const string DottedQuadRegularExpressionPattern = @"^[0-9]{1,3}(\.[0-9]{1,3}){3}$"; // checks dotted quad format (does not verify validity of address) - private const string HexLikePattern = "^[0-9a-f]*$"; /// - /// number of bits in an IPv4 address + /// Regex pattern matching leading zeros in each dotted-quad octet. + /// Lookbehind (?<=^|\.) anchors to start-of-string or a dot; lookahead (?!\.|$) preserves lone-zero octets. + /// Applied with . + /// + public const string DottedQuadLeadingZerosPattern = @"(?<=^|\.)0+(?!\.|$)"; + + /// + /// Regex pattern checking dotted-quad format: four groups of 1-3 digits separated by dots. + /// Does not verify address validity (octet value range). + /// Applied with . + /// + public const string DottedQuadRegularExpressionPattern = @"^[0-9]{1,3}(\.[0-9]{1,3}){3}$"; + + /// + /// Regex pattern matching strings composed entirely of hexadecimal digits (0-9, a-f; allows empty string). + /// Applied with and . + /// + public const string HexLikePattern = "^[0-9a-f]*$"; + + /// + /// Bit width of an IPv4 address (32). /// public const int IPv4BitCount = 32; /// - /// number of bytes in an IPv4 address (4) + /// Byte width of an IPv4 address (4). /// public const int IPv4ByteCount = IPv4BitCount / BitsPerByte; /// - /// number of octets in an IPv4 address + /// Number of octets in an IPv4 dotted-quad address (4). /// public const int IPv4OctetCount = 4; /// - /// number of bits in an IPv6 address + /// Bit width of an IPv6 address (128). /// public const int IPv6BitCount = 128; /// - /// number of bytes in an IPv6 address (16) + /// Byte width of an IPv6 address (16). /// public const int IPv6ByteCount = IPv6BitCount / BitsPerByte; /// - /// number of hextets in an IPv6 address + /// Number of hextets in an IPv6 colon-hex address (8). /// + /// + /// + /// IPv6 colon-hex notation groups 128 bits into 8 16-bit hextets per + /// RFC 4291 §2.2. + /// + /// public const int IPv6HextetCount = 8; - private static readonly Regex HexLikeRegularExpression = new Regex( +#if !NET7_0_OR_GREATER + private static readonly Regex HexLikeRegularExpression = new( HexLikePattern, RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant ); - private static readonly Regex DottedQuadLeadingZerosRegularExpression = new Regex( +#else + private static Regex HexLikeRegularExpression => GetHexLikeRegularExpression(); + + [GeneratedRegex(HexLikePattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)] + private static partial Regex GetHexLikeRegularExpression(); +#endif + +#if !NET7_0_OR_GREATER + private static readonly Regex DottedQuadLeadingZerosRegularExpression = new( DottedQuadLeadingZerosPattern, RegexOptions.Compiled | RegexOptions.CultureInvariant ); - private static readonly Regex DottedQuadStringRegularExpression = new Regex( +#else + private static Regex DottedQuadLeadingZerosRegularExpression => GetDottedQuadLeadingZerosRegularExpression(); + + [GeneratedRegex(DottedQuadLeadingZerosPattern, RegexOptions.CultureInvariant)] + private static partial Regex GetDottedQuadLeadingZerosRegularExpression(); +#endif + +#if !NET7_0_OR_GREATER + private static readonly Regex DottedQuadStringRegularExpression = new( DottedQuadRegularExpressionPattern, RegexOptions.Compiled | RegexOptions.CultureInvariant ); +#else + private static Regex DottedQuadStringRegularExpression => GetDottedQuadStringRegularExpression(); + + [GeneratedRegex(DottedQuadRegularExpressionPattern, RegexOptions.CultureInvariant)] + private static partial Regex GetDottedQuadStringRegularExpression(); +#endif /// - /// Maximum IPv4 Address value (255.255.255.255) + /// Maximum IPv4 address: 255.255.255.255. /// - public static readonly IPAddress IPv4MaxAddress = new IPAddress(uint.MaxValue); + public static readonly IPAddress IPv4MaxAddress = new(uint.MaxValue); /// - /// Minimum IPv4 Address value (0.0.0.0) + /// Minimum IPv4 address: 0.0.0.0. /// - public static readonly IPAddress IPv4MinAddress = new IPAddress(0); + public static readonly IPAddress IPv4MinAddress = new(0); /// - /// Maximum IPv6 value (ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff) + /// Maximum IPv6 address: ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff. /// - public static readonly IPAddress IPv6MaxAddress = new IPAddress( - new byte[] { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }, + public static readonly IPAddress IPv6MaxAddress = new( + [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff], 0L ); /// - /// Standard valid + /// The set of address families supported by Arcus. /// - public static readonly IReadOnlyCollection ValidAddressFamilies = new List - { + public static readonly AddressFamily[] ValidAddressFamilies = + [ AddressFamily.InterNetwork, AddressFamily.InterNetworkV6, - }.AsReadOnly(); + ]; /// - /// Minimum IPv6 value (::) + /// Minimum IPv6 address: :: (all zeros). /// - public static readonly IPAddress IPv6MinAddress = new IPAddress(new byte[IPv6ByteCount], 0L); + public static readonly IPAddress IPv6MinAddress = new(new byte[IPv6ByteCount], 0L); #region Address Max / Minimum @@ -103,15 +156,12 @@ public static class IPAddressUtilities /// when is or when public static IPAddress MaxIPAddress(this AddressFamily addressFamily) { - switch (addressFamily) + return addressFamily switch { - case AddressFamily.InterNetwork: - return IPv4MaxAddress; - case AddressFamily.InterNetworkV6: - return IPv6MaxAddress; - default: - throw new ArgumentException($"Unsupported address family \"{addressFamily}\"", nameof(addressFamily)); - } + AddressFamily.InterNetwork => IPv4MaxAddress, + AddressFamily.InterNetworkV6 => IPv6MaxAddress, + _ => throw new ArgumentException($"Unsupported address family \"{addressFamily}\"", nameof(addressFamily)), + }; } /// @@ -122,15 +172,12 @@ public static IPAddress MaxIPAddress(this AddressFamily addressFamily) /// when is or when public static IPAddress MinIPAddress(this AddressFamily addressFamily) { - switch (addressFamily) + return addressFamily switch { - case AddressFamily.InterNetwork: - return IPv4MinAddress; - case AddressFamily.InterNetworkV6: - return IPv6MinAddress; - default: - throw new ArgumentException($"Unsupported address family \"{addressFamily}\"", nameof(addressFamily)); - } + AddressFamily.InterNetwork => IPv4MinAddress, + AddressFamily.InterNetworkV6 => IPv6MinAddress, + _ => throw new ArgumentException($"Unsupported address family \"{addressFamily}\"", nameof(addressFamily)), + }; } #endregion // end: Address Max / Minimum @@ -138,20 +185,20 @@ public static IPAddress MinIPAddress(this AddressFamily addressFamily) #region address family detection /// - /// Test if address is IPv4 + /// Determines whether the address is an IPv4 address. /// - /// the IPAddress to test - /// true if ipv4 + /// The address to test. + /// if the address is IPv4. public static bool IsIPv4(this IPAddress ipAddress) { return ipAddress != null && ipAddress.AddressFamily == AddressFamily.InterNetwork; } /// - /// Test if address is IPv6 + /// Determines whether the address is an IPv6 address. /// - /// the IPAddress to test - /// true if ipv6 + /// The address to test. + /// if the address is IPv6. public static bool IsIPv6(this IPAddress ipAddress) { return ipAddress != null && ipAddress.AddressFamily == AddressFamily.InterNetworkV6; @@ -162,29 +209,59 @@ public static bool IsIPv6(this IPAddress ipAddress) #region address format detection /// - /// Check if address is IPv4 mapped to IPv6 - /// in accordance to rfc 4291 (https://tools.ietf.org/html/rfc4291) - 2.5.5.2. IPv4-Mapped IPv6 Address + /// Determines whether the address is an IPv4-mapped IPv6 address. /// + /// + /// + /// IPv4-Mapped IPv6 Address format per + /// RFC 4291 §2.5.5.2: + /// 80 zero bits, 16 one-bits (0xFFFF), followed by the 32-bit IPv4 address. + /// + /// /// the IPAddress to test /// true if is an IPv4 address mapped to IPv6 /// is . public static bool IsIPv4MappedIPv6(this IPAddress ipAddress) { - if (ipAddress == null || !ipAddress.IsIPv6()) + if (ipAddress?.IsIPv6() != true) { return false; } var addressBytes = ipAddress.GetAddressBytes(); - return addressBytes.Take(10).All(b => b == 0x00) && addressBytes[10] == 0xff && addressBytes[11] == 0xff; +#if NET8_0_OR_GREATER + return addressBytes[11] == 0xff + && addressBytes[10] == 0xff + && addressBytes[0] == 0x00 + && addressBytes[1] == 0x00 + && addressBytes[2] == 0x00 + && addressBytes[3] == 0x00 + && addressBytes[4] == 0x00 + && addressBytes[5] == 0x00 + && addressBytes[6] == 0x00 + && addressBytes[7] == 0x00 + && addressBytes[8] == 0x00 + && addressBytes[9] == 0x00; +#else + return addressBytes.Take(10).All(byteValue => byteValue == 0x00) + && addressBytes[10] == 0xff + && addressBytes[11] == 0xff; +#endif } /// - /// Determine if the given is a valid net mask + /// Determines whether the given is a valid IPv4 subnet mask. /// - /// the netmask to test - /// true if the given input is a valid netmask + /// + /// + /// A valid subnet mask is a contiguous sequence of leading 1-bits followed by 0-bits per + /// RFC 950 §2. + /// IPv4 only; returns for IPv6 addresses. + /// + /// + /// The mask to test. + /// if the address is a valid IPv4 subnet mask. public static bool IsValidNetMask(this IPAddress netmask) { if (netmask == null || netmask.AddressFamily != AddressFamily.InterNetwork) @@ -220,16 +297,18 @@ public static bool IsValidNetMask(this IPAddress netmask) #region hex parsing /// - /// Attempt to parse a hex input string as an IP Address of the given family + /// Parses a hex string (optionally prefixed with "0x") as an IP address. /// - /// hex input - /// address family - /// IP Address, or if parse fails + /// Hex input. + /// Address family. + /// + /// IP Address, or if parse fails. + /// public static IPAddress ParseFromHexString(string input, AddressFamily addressFamily) { #region defense - if (input == null) + if (input is null) { throw new ArgumentNullException(nameof(input)); } @@ -249,42 +328,59 @@ public static IPAddress ParseFromHexString(string input, AddressFamily addressFa #endregion // end: defense - // ignore "0x" prefix, and trim most significant 0s + // Ignore "0x" prefix and trim most significant zeros. var byteString = ( - input.StartsWith("0x", StringComparison.OrdinalIgnoreCase) ? input.Substring(2, input.Length - 2) : input + input.StartsWith("0x", StringComparison.OrdinalIgnoreCase) ? input.Substring(2) : input ).TrimStart('0'); - // if the byte string has an odd number of characters provide a single significant 0 + // Preserve a single "0" when the input was all zeros (TrimStart would otherwise empty the string). + if (byteString.Length == 0) + { + byteString = "0"; + } + + // If the byte string has an odd number of characters, provide a single significant 0. if (byteString.Length % 2 != 0) { - byteString = "0" + byteString; // ensure string has an even number of characters, prefix with MSB 0 + byteString = "0" + byteString; } - // fail if the string is composed of non-valid hex characters + // Fail if the string contains non-hex characters. if (!HexLikeRegularExpression.IsMatch(byteString)) { throw new ArgumentException($"{nameof(input)} is in an unexpected format", nameof(input)); } - // for each i%2, convert i, i+1 into a byte, reverse, and convert into an array +#if NET8_0_OR_GREATER + var bytes = Convert.FromHexString(byteString); + return Parse(bytes, addressFamily); +#else + // Convert each pair of hex characters into a byte. var byteArray = Enumerable .Range(0, byteString.Length / 2) - .Select(i => i * 2) - .Select(i => Convert.ToByte(byteString.Substring(i, 2), 16)) + .Select(bytePairIndex => Convert.ToByte(byteString.Substring(bytePairIndex * 2, 2), 16)) .ToArray(); return Parse(byteArray, addressFamily); +#endif } /// - /// Try to parse an IPv4 or IPv6 from string containing hex numbers (possibly prefixed by - /// "0x") + /// Attempts to parse a hex string as an IP address. /// - /// the string to attempt to parse - /// the desired address family - /// the address that was parsed - /// true on success + /// The hex string (optionally prefixed with "0x"). + /// The desired address family. + /// The parsed , or on failure. + /// if parsing succeeded. +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER + public static bool TryParseFromHexString( + string input, + AddressFamily addressFamily, + [NotNullWhen(true)] out IPAddress address + ) +#else public static bool TryParseFromHexString(string input, AddressFamily addressFamily, out IPAddress address) +#endif { if (input == null) { @@ -309,19 +405,22 @@ public static bool TryParseFromHexString(string input, AddressFamily addressFami #region octal parsing /// - /// Converts an IP address string to an instance ignoring leading zeros (octal - /// notation) of dotted quad format. + /// Parses an IP address string, stripping leading zeros from dotted-quad octets to avoid octal interpretation. /// - /// - /// A string that contains an IP address in dotted-quad notation for IPv4 and in colon-hexadecimal - /// notation for IPv6 - /// - /// An instance + /// + /// + /// IPv4 dotted-quad notation uses four decimal octets per + /// RFC 791 §2.3. + /// Leading zeros in an octet are stripped before parsing to avoid unintended octal interpretation. + /// + /// + /// The IP address string (dotted-quad for IPv4, colon-hex for IPv6). + /// The parsed . public static IPAddress ParseIgnoreOctalInIPv4(string input) { #region defense - if (input == null) + if (input is null) { throw new ArgumentNullException(nameof(input)); } @@ -342,14 +441,16 @@ public static IPAddress ParseIgnoreOctalInIPv4(string input) } /// - /// Converts an IP address string to an instance ignoring leading zeros (octal - /// notation) of dotted - /// quad format. + /// Attempts to parse an IP address string, stripping leading zeros from dotted-quad octets. /// - /// The string to validate. - /// The version of the string. - /// true if ipString is a valid IP address; otherwise, false. - public static bool TryParseIgnoreOctalInIPv4(string input, out IPAddress address) + /// The IP address string. + /// The parsed , or on failure. + /// if parsing succeeded. + public static bool TryParseIgnoreOctalInIPv4(string input, +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER + [NotNullWhen(true)] +#endif + out IPAddress address) { if (input == null) { @@ -374,30 +475,24 @@ public static bool TryParseIgnoreOctalInIPv4(string input, out IPAddress address #region Parse byte[] /// - /// Convert bytes (big endian) to an IP Address providing 0x00 MSB bytes as needed + /// Parses a big-endian byte array into an , zero-padding the MSB side as needed. /// - /// the big endian IPAddress - /// the desired address family - /// The parsed + /// The big-endian byte array. + /// The target address family. + /// The parsed . public static IPAddress Parse(byte[] input, AddressFamily addressFamily) { - if (input == null) + if (input is null) { throw new ArgumentNullException(nameof(input)); } - int expectedByteCount; - switch (addressFamily) + var expectedByteCount = addressFamily switch { - case AddressFamily.InterNetwork: - expectedByteCount = IPv4ByteCount; - break; - case AddressFamily.InterNetworkV6: - expectedByteCount = IPv6ByteCount; - break; - default: - throw new ArgumentOutOfRangeException(nameof(addressFamily)); - } + AddressFamily.InterNetwork => IPv4ByteCount, + AddressFamily.InterNetworkV6 => IPv6ByteCount, + _ => throw new ArgumentOutOfRangeException(nameof(addressFamily)), + }; if (input.Length > expectedByteCount) { @@ -407,18 +502,28 @@ public static IPAddress Parse(byte[] input, AddressFamily addressFamily) ); } - return new IPAddress(input.PadBigEndianMostSignificantBytes(expectedByteCount)); + return new IPAddress(BigEndianBitWrapper.FromBytes(input, expectedByteCount).ToBytes()); } /// - /// Attempt to convert bytes (big endian) to IP address providing 0x00 MSB bytes as needed + /// Attempts to parse a big-endian byte array into an . /// - /// the big endian IPAddress - /// the desired address family - /// the address on success - /// true on success + /// The big-endian byte array. + /// The target address family. + /// The parsed , or on failure. + /// if parsing succeeded. +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER + public static bool TryParse(byte[] input, AddressFamily addressFamily, [NotNullWhen(true)] out IPAddress address) +#else public static bool TryParse(byte[] input, AddressFamily addressFamily, out IPAddress address) +#endif { + if (input is null) + { + address = null; + return false; + } + try { address = Parse(input, addressFamily); @@ -440,6 +545,12 @@ public static bool TryParse(byte[] input, AddressFamily addressFamily, out IPAdd /// /// Determines if an is a private address. /// + /// + /// + /// Private address blocks (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fd00::/8) + /// are defined in RFC 1918 §3. + /// + /// /// the input address /// if, and only if, the is private. /// is diff --git a/src/Arcus/Utilities/SubnetUtilities.cs b/src/Arcus/Utilities/SubnetUtilities.cs index b639074..a36cfdc 100644 --- a/src/Arcus/Utilities/SubnetUtilities.cs +++ b/src/Arcus/Utilities/SubnetUtilities.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; +using System.Net; using Arcus.Math; namespace Arcus.Utilities @@ -11,50 +8,105 @@ namespace Arcus.Utilities /// public static class SubnetUtilities { + private static readonly Lazy> PrivateRanges = new(() => new[] + { + // IPv4 RFC 1918 +#pragma warning disable S1313 // IP addresses should not be hardcoded + Subnet.Parse("10.0.0.0", 8), + Subnet.Parse("172.16.0.0", 12), + Subnet.Parse("192.168.0.0", 16), + // IPv6 RFC 4193 + Subnet.Parse("fd00::", 8), +#pragma warning restore S1313 // IP addresses should not be hardcoded + }.ToList().AsReadOnly()); + + private static readonly Lazy> LinkLocalRanges = new(() => new[] + { + // RFC 3927 +#pragma warning disable S1313 // IP addresses should not be hardcoded + Subnet.Parse("169.254.0.0", 16), + // RFC 4291 + Subnet.Parse("fe80::", 10), +#pragma warning restore S1313 // IP addresses should not be hardcoded + }.ToList().AsReadOnly()); + /// - /// A collection of all known private IP Address ranges + /// Gets a collection of all known private IP Address ranges. /// - public static IReadOnlyList PrivateIPAddressRangesList = new[] - { - // IPv4 RFC 1918 - Subnet.Parse("10.0.0.0", 8), - Subnet.Parse("172.16.0.0", 12), - Subnet.Parse("192.168.0.0", 16), - // IPv6 RFC 4193 - Subnet.Parse("fd00::", 8), - }.ToList().AsReadOnly(); + /// A of the RFC 1918 / RFC 4193 private ranges. + /// + /// + /// Contains the four RFC-defined private/ULA address blocks: + /// 10.0.0.0/8, 172.16.0.0/12, and 192.168.0.0/16 per + /// RFC 1918 §3; + /// and fd00::/8 (IPv6 Unique Local Addresses) per + /// RFC 4193 §8. + /// + /// + /// Breaking change (lazy): This member is now a lazily-initialized static + /// property backed by . Prior to this version it was a + /// field whose reference could be reassigned + /// by external code. Code that assigned to + /// SubnetUtilities.PrivateIPAddressRangesList = ... will no longer compile. + /// The contract already prevented mutation of + /// the list contents; this change extends that guarantee to the backing reference + /// and defers construction until first access. + /// + /// + public static IReadOnlyList PrivateIPAddressRangesList => PrivateRanges.Value; /// - /// A collection of all known Link Local IP Address ranges + /// Gets a collection of all known Link Local IP Address ranges. /// - public static IReadOnlyList LinkLocalIPAddressRangesList = new[] - { - // RFC 3927 - Subnet.Parse("169.254.0.0", 16), - // RFC 4291 - Subnet.Parse("fe80::", 10), - }.ToList().AsReadOnly(); + /// A of the RFC 3927 / RFC 4291 link-local ranges. + /// + /// + /// Contains 169.254.0.0/16 (IPv4 link-local) per + /// RFC 3927 §2.1 + /// and fe80::/10 (IPv6 link-local) per + /// RFC 4291 §2.4. + /// + /// + /// Breaking change (lazy): This member is now a lazily-initialized static + /// property backed by . Prior to this version it was a + /// field whose reference could be reassigned + /// by external code. Code that assigned to + /// SubnetUtilities.LinkLocalIPAddressRangesList = ... will no longer compile. + /// + /// + public static IReadOnlyList LinkLocalIPAddressRangesList => LinkLocalRanges.Value; /// - /// Get The fewest consecutive subnets that would fill the range between the given addresses (inclusive) + /// Computes the fewest consecutive CIDR subnets that exactly cover the range from to (inclusive). /// - /// lowest order IP Address - /// highest order IP Address - /// an enumerable of Subnet - /// is . - /// is . - /// Address families must match - /// Address families must be InterNetwork or InternetworkV6 - public static IEnumerable FewestConsecutiveSubnetsFor(IPAddress left, IPAddress right) + /// + /// + /// Uses a binary-tree decomposition algorithm: the range is progressively divided + /// into the largest possible CIDR blocks. Each increment of the prefix length halves + /// the block size (2max−n addresses) per + /// RFC 4632 §2. + /// + /// + /// The lower bound of the range. + /// The upper bound of the range. + /// The maximum enumeration exponent (0-128). + /// An enumerable of values covering the range. + /// or is . + /// Address families must match. + public static IEnumerable FewestConsecutiveSubnetsFor( + IPAddress left, + IPAddress right, + int maxEnumerationExponent = AbstractIPAddressRange.DefaultMaxEnumerationExponent + ) { #region defense - if (left == null) + if (left is null) { throw new ArgumentNullException(nameof(left)); } - if (right == null) + if (right is null) { throw new ArgumentNullException(nameof(right)); } @@ -85,15 +137,26 @@ public static IEnumerable FewestConsecutiveSubnetsFor(IPAddress left, IP var minHead = IPAddressMath.Min(left, right); var maxTail = IPAddressMath.Max(left, right); - return FilledSubnets(minHead, maxTail, new Subnet(minHead, maxTail)); + return FilledSubnets( + minHead, + maxTail, + new Subnet(minHead, maxTail, maxEnumerationExponent), + maxEnumerationExponent + ); // recursive function call // Works by verifying that passed subnet isn't bounded by head, tail IP Addresses // if not breaks subnet in half and recursively tests, building in essence a binary tree of testable subnet paths -#if NET6_0_OR_GREATER - static -#endif - IEnumerable FilledSubnets(IPAddress head, IPAddress tail, Subnet subnet) + // + // Implemented as a yield-return iterator rather than a recursive Concat chain. The previous + // `return a.Concat(b)` form produced an O(N) iterator-wrapper depth — each level wrapped its + // predecessor — so enumerating the full result was O(N^2) (one Concat MoveNext per emitted + // element, multiplied across the recursion depth). Yielding directly lets the compiler emit a + // single state machine per branch with O(1) amortized work per element and no intermediate + // Concat allocations, while preserving the exact same subnet sequence (ascending by + // NetworkPrefixAddress, no gaps, no overlaps). + + static IEnumerable FilledSubnets(IPAddress head, IPAddress tail, Subnet subnet, int exponent) { var networkPrefixAddress = subnet.NetworkPrefixAddress; var broadcastAddress = subnet.BroadcastAddress; @@ -101,7 +164,8 @@ IEnumerable FilledSubnets(IPAddress head, IPAddress tail, Subnet subnet) // the given subnet is the perfect size for the head/tail (not papa bear, not mama bear, but just right with baby bear) if (networkPrefixAddress.IsGreaterThanOrEqualTo(head) && broadcastAddress.IsLessThanOrEqualTo(tail)) { - return new[] { subnet }; + yield return subnet; + yield break; } // increasing the route prefix by 1 creates a subnet of half the initial size (due 2^(max-n) route prefix sizing) @@ -113,11 +177,11 @@ IEnumerable FilledSubnets(IPAddress head, IPAddress tail, Subnet subnet) || (subnet.IsIPv4 && nextSmallestRoutePrefix > IPAddressUtilities.IPv4BitCount) ) { - return Enumerable.Empty(); // no subnets to be found here, stop investigating branch of tree + yield break; // no subnets to be found here, stop investigating branch of tree } // build head subnet - var headSubnet = new Subnet(networkPrefixAddress, nextSmallestRoutePrefix); + var headSubnet = new Subnet(networkPrefixAddress, nextSmallestRoutePrefix, exponent); // use the next address after the end of the head subnet as the first address for the tail subnet if (!IPAddressMath.TryIncrement(headSubnet.BroadcastAddress, out var tailStartingAddress)) @@ -125,46 +189,106 @@ IEnumerable FilledSubnets(IPAddress head, IPAddress tail, Subnet subnet) throw new InvalidOperationException($"unable to increment {headSubnet.BroadcastAddress}"); } - var tailSubnet = new Subnet(tailStartingAddress, nextSmallestRoutePrefix); + var tailSubnet = new Subnet(tailStartingAddress, nextSmallestRoutePrefix, exponent); // break into binary search tree, searching both head subnet and tail subnet for ownership of head and tail ip - return FilledSubnets(head, tail, headSubnet).Concat(FilledSubnets(head, tail, tailSubnet)); + foreach (var s in FilledSubnets(head, tail, headSubnet, exponent)) + { + yield return s; + } + + foreach (var s in FilledSubnets(head, tail, tailSubnet, exponent)) + { + yield return s; + } } } /// - /// Return the largest subnet (smallest route prefix value) - /// if more than one "largest" return is not predictable beyond that one will be returned - /// Consider usage of DefaultSubnetComparer + /// Return the largest subnet (smallest routing prefix value). + /// When multiple subnets tie for largest, which one is returned is not guaranteed beyond that one will be returned; + /// consider for a stable sort. /// + /// + /// + /// A smaller routing prefix means a larger address block per + /// RFC 4632 §2 + /// (block size = 2max−prefix). + /// + /// /// the subnets to search - /// - /// The first largest subnet by routing prefix, or if no to - /// choose from - /// + /// The first largest subnet by routing prefix. + /// is . + /// contains no elements. + /// contains a element. public static Subnet LargestSubnet(IEnumerable subnets) { - var enumerable = (subnets ?? Enumerable.Empty()).ToList(); - - return !enumerable.Any() - ? null - : enumerable.Where(s => s != null).Aggregate((s1, s2) => s1.RoutingPrefix < s2.RoutingPrefix ? s1 : s2); + return SelectSubnet(subnets, (candidate, current) => candidate.RoutingPrefix < current.RoutingPrefix); } /// - /// Return the smallest subnet (largest route prefix value) - /// if more than one "smallest" return is not predictable beyond that one will be returned - /// Consider usage of DefaultSubnetComparer + /// Return the smallest subnet (largest routing prefix value). + /// When multiple subnets tie for smallest, which one is returned is not guaranteed beyond that one will be returned; + /// consider for a stable sort. /// - /// the list of subnets - /// The first smallest subnet by routing prefix, or null if no subnets to choose from + /// + /// + /// A larger routing prefix means a smaller address block per + /// RFC 4632 §2 + /// (block size = 2max−prefix). + /// + /// + /// the subnets to search + /// The first smallest subnet by routing prefix. + /// is . + /// contains no elements. + /// contains a element. public static Subnet SmallestSubnet(IEnumerable subnets) { - var enumerable = (subnets ?? Enumerable.Empty()).ToList(); + return SelectSubnet(subnets, (candidate, current) => candidate.RoutingPrefix > current.RoutingPrefix); + } + + /// Iterates and returns the element for which never returns when compared against a later element. + /// the subnets to search; must not be or empty, and must contain no elements. + /// + /// A predicate of the form (candidate, currentBest) that returns when + /// candidate should displace currentBest as the running winner. + /// + /// The winning subnet after a single pass. + /// is . + /// contains no elements. + /// contains a element. + private static Subnet SelectSubnet(IEnumerable subnets, Func shouldReplace) + { + if (subnets is null) + { + throw new ArgumentNullException(nameof(subnets)); + } + + using var enumerator = subnets.GetEnumerator(); + + if (!enumerator.MoveNext()) + { + throw new InvalidOperationException("Sequence contains no elements."); + } + + var best = + enumerator.Current + ?? throw new ArgumentException("The collection cannot contain null elements.", nameof(subnets)); + + while (enumerator.MoveNext()) + { + var candidate = + enumerator.Current + ?? throw new ArgumentException("The collection cannot contain null elements.", nameof(subnets)); + + if (shouldReplace(candidate, best)) + { + best = candidate; + } + } - return !enumerable.Any() - ? null - : enumerable.Where(s => s != null).Aggregate((s1, s2) => s1.RoutingPrefix > s2.RoutingPrefix ? s1 : s2); + return best; } } } diff --git a/src/Arcus/packages.lock.json b/src/Arcus/packages.lock.json index ebd3062..3ec1029 100644 --- a/src/Arcus/packages.lock.json +++ b/src/Arcus/packages.lock.json @@ -1,19 +1,7 @@ { - "version": 1, + "version": 2, "dependencies": { ".NETStandard,Version=v2.0": { - "AsyncFixer": { - "type": "Direct", - "requested": "[1.6.0, )", - "resolved": "1.6.0", - "contentHash": "/Xfs9H3UMfEv64cwT+C/JrTRp4w08BmPuFbj0ageadCHpx6rxYJxAU2C6sEqRFG22xmGk5cX9ewzoiiehWVHOw==" - }, - "Gulliver": { - "type": "Direct", - "requested": "[3.0.0, )", - "resolved": "3.0.0", - "contentHash": "J232PI4ccScfLKt59UzyHBRZLcAJpyJ+N/at1fFF/XxBKwjl8idDVPkMU3FZkwaaU/e3Ert8/EnGLg5CxmQ8dQ==" - }, "Microsoft.Bcl.HashCode": { "type": "Direct", "requested": "[6.0.0, )", @@ -22,19 +10,20 @@ }, "Microsoft.SourceLink.GitHub": { "type": "Direct", - "requested": "[8.0.0, )", - "resolved": "8.0.0", - "contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==", + "requested": "[10.0.300, )", + "resolved": "10.0.300", + "contentHash": "QzCtLkXVb3l4IxcpvJCbzUwMLihAmLN6vVLjQGSzYSF8d2dvXxqJAZk83RV3gYnp2egz8jRMgSR2woY3vOahTA==", "dependencies": { - "Microsoft.Build.Tasks.Git": "8.0.0", - "Microsoft.SourceLink.Common": "8.0.0" + "Microsoft.Build.Tasks.Git": "10.0.300", + "Microsoft.SourceLink.Common": "10.0.300", + "System.IO.Hashing": "10.0.8" } }, "Microsoft.VisualStudio.Threading.Analyzers": { "type": "Direct", - "requested": "[17.14.15, )", - "resolved": "17.14.15", - "contentHash": "mXQPJsbuUD2ydq4/ffd8h8tSOFCXec+2xJOVNCvXjuMOq/+5EKHq3D2m2MC2+nUaXeFMSt66VS/J4HdKBixgcw==" + "requested": "[18.7.23, )", + "resolved": "18.7.23", + "contentHash": "dyj6z8m+LFjpeH69hdnciCcRCmD0YgMlvuQEywiEshjqj/blgQ+PuWX8Vd5gkWQFoBN4e+TFxx6DvGNSv/gIKg==" }, "NETStandard.Library": { "type": "Direct", @@ -47,27 +36,27 @@ }, "Roslynator.Analyzers": { "type": "Direct", - "requested": "[4.14.1, )", - "resolved": "4.14.1", - "contentHash": "yMSjze/xMYDF6PCE60/ULWx0tttNyKAndw2KijNxbKil0FX8nvDeEneDZGma8Uifk17RlfZqIXxf1mmBmhRHjg==" + "requested": "[4.15.0, )", + "resolved": "4.15.0", + "contentHash": "E8fu71Y+vzhniJKy1K13Rzwm8Vjcj7CDFBpflqncKUZfc3SzuF0kMBiyMmgXNPkjWTdE3Z+30HlGgYjHONZY/Q==" }, "Roslynator.CodeAnalysis.Analyzers": { "type": "Direct", - "requested": "[4.14.1, )", - "resolved": "4.14.1", - "contentHash": "axessewcgSLn+UAtKyMKPgXpzIldnivynjRe9qsRjrVtrxjctDpFDEjAkcbpDjhR0ccm8/+ihOna60wnXJ/66A==" + "requested": "[4.15.0, )", + "resolved": "4.15.0", + "contentHash": "XPY6vqHG8jLJ1S0SGzLDtyuDjmkcIAGuLRrsG3SlAuyAOlwnVfA1glgIy6YqXutoi7hwd1fqZC2W9zoMntuZNg==" }, "Roslynator.Formatting.Analyzers": { "type": "Direct", - "requested": "[4.14.1, )", - "resolved": "4.14.1", - "contentHash": "hALZsyELJXhj7WQw7Q7nLI7sELre8AlQIMyGxXpmpK+iBqShPcWHuJMGjIsTMa/bC8Q5xyQ4iFfRtm0bP1zisQ==" + "requested": "[4.15.0, )", + "resolved": "4.15.0", + "contentHash": "/LvSJGJbrx+OYC2GePmiAA9fRTfz37R/oaHC2j8YK/j9lE8ykZESCGTMVGqXF5woc6RbzfJlpYkzeG6fAV6hFQ==" }, "SonarAnalyzer.CSharp": { "type": "Direct", - "requested": "[10.15.0.120848, )", - "resolved": "10.15.0.120848", - "contentHash": "1hM3HVRl5jdC/ZBDu+G7CCYLXRGe/QaP01Zy+c9ETPhY7lWD8g8HiefY6sGaH0T3CJ4wAy0/waGgQTh0TYy0oQ==" + "requested": "[10.28.0.143324, )", + "resolved": "10.28.0.143324", + "contentHash": "ZnmYHFiQw2Aph2ft9c7UqVrqe79aZR5l7oeG59+sgrSAO9J159meglP1Zo34+Mcw4etIUTC9btYnP0Ar/rQIyg==" }, "StyleCop.Analyzers": { "type": "Direct", @@ -77,8 +66,11 @@ }, "Microsoft.Build.Tasks.Git": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ==" + "resolved": "10.0.300", + "contentHash": "P0kaQwVZx4xIUe2FtrLyBadYNXuAljttJUPvjBYRuHhPE8L77L42KakLDkaADRiUrGspoLcMwayjrbQhYTr0zA==", + "dependencies": { + "System.IO.Hashing": "10.0.8" + } }, "Microsoft.NETCore.Platforms": { "type": "Transitive", @@ -87,62 +79,85 @@ }, "Microsoft.SourceLink.Common": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw==" + "resolved": "10.0.300", + "contentHash": "0jlkXaUGjYlWTIVPve5MftjKHnT3SlAtq9BCLV4J9IjdPrxV/+4rMlBSjfr1khG8/GC6KGojjola8E1VvWF0qQ==" + }, + "System.Buffers": { + "type": "Transitive", + "resolved": "4.6.1", + "contentHash": "N8GXpmiLMtljq7gwvyS+1QvKT/W2J8sNAvx+HVg4NGmsG/H+2k/y9QI23auLJRterrzCiDH+IWAw4V/GPwsMlw==" + }, + "System.IO.Hashing": { + "type": "Transitive", + "resolved": "10.0.8", + "contentHash": "+dJsbPJ3FyUbTZNplFj0RCKePFizmv6ewDV46JE9q/IVH4c3xTCftHfHelLsAKf0jryIPqgMb5GpS0x7TAY3mg==", + "dependencies": { + "System.Buffers": "4.6.1", + "System.Memory": "4.6.3" + } + }, + "System.Memory": { + "type": "Transitive", + "resolved": "4.6.3", + "contentHash": "qdcDOgnFZY40+Q9876JUHnlHu7bosOHX8XISRoH94fwk6hgaeQGSgfZd8srWRZNt5bV9ZW2TljcegDNxsf+96A==", + "dependencies": { + "System.Buffers": "4.6.1", + "System.Numerics.Vectors": "4.6.1", + "System.Runtime.CompilerServices.Unsafe": "6.1.2" + } + }, + "System.Numerics.Vectors": { + "type": "Transitive", + "resolved": "4.6.1", + "contentHash": "sQxefTnhagrhoq2ReR0D/6K0zJcr9Hrd6kikeXsA1I8kOCboTavcUC4r7TSfpKFeE163uMuxZcyfO1mGO3EN8Q==" + }, + "System.Runtime.CompilerServices.Unsafe": { + "type": "Transitive", + "resolved": "6.1.2", + "contentHash": "2hBr6zdbIBTDE3EhK7NSVNdX58uTK6iHW/P/Axmm9sl1xoGSLqDvMtpecn226TNwHByFokYwJmt/aQQNlO5CRw==" } }, "net10.0": { - "AsyncFixer": { - "type": "Direct", - "requested": "[1.6.0, )", - "resolved": "1.6.0", - "contentHash": "/Xfs9H3UMfEv64cwT+C/JrTRp4w08BmPuFbj0ageadCHpx6rxYJxAU2C6sEqRFG22xmGk5cX9ewzoiiehWVHOw==" - }, - "Gulliver": { - "type": "Direct", - "requested": "[3.0.0, )", - "resolved": "3.0.0", - "contentHash": "J232PI4ccScfLKt59UzyHBRZLcAJpyJ+N/at1fFF/XxBKwjl8idDVPkMU3FZkwaaU/e3Ert8/EnGLg5CxmQ8dQ==" - }, "Microsoft.SourceLink.GitHub": { "type": "Direct", - "requested": "[8.0.0, )", - "resolved": "8.0.0", - "contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==", + "requested": "[10.0.300, )", + "resolved": "10.0.300", + "contentHash": "QzCtLkXVb3l4IxcpvJCbzUwMLihAmLN6vVLjQGSzYSF8d2dvXxqJAZk83RV3gYnp2egz8jRMgSR2woY3vOahTA==", "dependencies": { - "Microsoft.Build.Tasks.Git": "8.0.0", - "Microsoft.SourceLink.Common": "8.0.0" + "Microsoft.Build.Tasks.Git": "10.0.300", + "Microsoft.SourceLink.Common": "10.0.300", + "System.IO.Hashing": "10.0.8" } }, "Microsoft.VisualStudio.Threading.Analyzers": { "type": "Direct", - "requested": "[17.14.15, )", - "resolved": "17.14.15", - "contentHash": "mXQPJsbuUD2ydq4/ffd8h8tSOFCXec+2xJOVNCvXjuMOq/+5EKHq3D2m2MC2+nUaXeFMSt66VS/J4HdKBixgcw==" + "requested": "[18.7.23, )", + "resolved": "18.7.23", + "contentHash": "dyj6z8m+LFjpeH69hdnciCcRCmD0YgMlvuQEywiEshjqj/blgQ+PuWX8Vd5gkWQFoBN4e+TFxx6DvGNSv/gIKg==" }, "Roslynator.Analyzers": { "type": "Direct", - "requested": "[4.14.1, )", - "resolved": "4.14.1", - "contentHash": "yMSjze/xMYDF6PCE60/ULWx0tttNyKAndw2KijNxbKil0FX8nvDeEneDZGma8Uifk17RlfZqIXxf1mmBmhRHjg==" + "requested": "[4.15.0, )", + "resolved": "4.15.0", + "contentHash": "E8fu71Y+vzhniJKy1K13Rzwm8Vjcj7CDFBpflqncKUZfc3SzuF0kMBiyMmgXNPkjWTdE3Z+30HlGgYjHONZY/Q==" }, "Roslynator.CodeAnalysis.Analyzers": { "type": "Direct", - "requested": "[4.14.1, )", - "resolved": "4.14.1", - "contentHash": "axessewcgSLn+UAtKyMKPgXpzIldnivynjRe9qsRjrVtrxjctDpFDEjAkcbpDjhR0ccm8/+ihOna60wnXJ/66A==" + "requested": "[4.15.0, )", + "resolved": "4.15.0", + "contentHash": "XPY6vqHG8jLJ1S0SGzLDtyuDjmkcIAGuLRrsG3SlAuyAOlwnVfA1glgIy6YqXutoi7hwd1fqZC2W9zoMntuZNg==" }, "Roslynator.Formatting.Analyzers": { "type": "Direct", - "requested": "[4.14.1, )", - "resolved": "4.14.1", - "contentHash": "hALZsyELJXhj7WQw7Q7nLI7sELre8AlQIMyGxXpmpK+iBqShPcWHuJMGjIsTMa/bC8Q5xyQ4iFfRtm0bP1zisQ==" + "requested": "[4.15.0, )", + "resolved": "4.15.0", + "contentHash": "/LvSJGJbrx+OYC2GePmiAA9fRTfz37R/oaHC2j8YK/j9lE8ykZESCGTMVGqXF5woc6RbzfJlpYkzeG6fAV6hFQ==" }, "SonarAnalyzer.CSharp": { "type": "Direct", - "requested": "[10.15.0.120848, )", - "resolved": "10.15.0.120848", - "contentHash": "1hM3HVRl5jdC/ZBDu+G7CCYLXRGe/QaP01Zy+c9ETPhY7lWD8g8HiefY6sGaH0T3CJ4wAy0/waGgQTh0TYy0oQ==" + "requested": "[10.28.0.143324, )", + "resolved": "10.28.0.143324", + "contentHash": "ZnmYHFiQw2Aph2ft9c7UqVrqe79aZR5l7oeG59+sgrSAO9J159meglP1Zo34+Mcw4etIUTC9btYnP0Ar/rQIyg==" }, "StyleCop.Analyzers": { "type": "Direct", @@ -152,67 +167,64 @@ }, "Microsoft.Build.Tasks.Git": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ==" + "resolved": "10.0.300", + "contentHash": "P0kaQwVZx4xIUe2FtrLyBadYNXuAljttJUPvjBYRuHhPE8L77L42KakLDkaADRiUrGspoLcMwayjrbQhYTr0zA==", + "dependencies": { + "System.IO.Hashing": "10.0.8" + } }, "Microsoft.SourceLink.Common": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw==" + "resolved": "10.0.300", + "contentHash": "0jlkXaUGjYlWTIVPve5MftjKHnT3SlAtq9BCLV4J9IjdPrxV/+4rMlBSjfr1khG8/GC6KGojjola8E1VvWF0qQ==" + }, + "System.IO.Hashing": { + "type": "Transitive", + "resolved": "10.0.8", + "contentHash": "+dJsbPJ3FyUbTZNplFj0RCKePFizmv6ewDV46JE9q/IVH4c3xTCftHfHelLsAKf0jryIPqgMb5GpS0x7TAY3mg==" } }, "net8.0": { - "AsyncFixer": { - "type": "Direct", - "requested": "[1.6.0, )", - "resolved": "1.6.0", - "contentHash": "/Xfs9H3UMfEv64cwT+C/JrTRp4w08BmPuFbj0ageadCHpx6rxYJxAU2C6sEqRFG22xmGk5cX9ewzoiiehWVHOw==" - }, - "Gulliver": { - "type": "Direct", - "requested": "[3.0.0, )", - "resolved": "3.0.0", - "contentHash": "J232PI4ccScfLKt59UzyHBRZLcAJpyJ+N/at1fFF/XxBKwjl8idDVPkMU3FZkwaaU/e3Ert8/EnGLg5CxmQ8dQ==" - }, "Microsoft.SourceLink.GitHub": { "type": "Direct", - "requested": "[8.0.0, )", - "resolved": "8.0.0", - "contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==", + "requested": "[10.0.300, )", + "resolved": "10.0.300", + "contentHash": "QzCtLkXVb3l4IxcpvJCbzUwMLihAmLN6vVLjQGSzYSF8d2dvXxqJAZk83RV3gYnp2egz8jRMgSR2woY3vOahTA==", "dependencies": { - "Microsoft.Build.Tasks.Git": "8.0.0", - "Microsoft.SourceLink.Common": "8.0.0" + "Microsoft.Build.Tasks.Git": "10.0.300", + "Microsoft.SourceLink.Common": "10.0.300", + "System.IO.Hashing": "10.0.8" } }, "Microsoft.VisualStudio.Threading.Analyzers": { "type": "Direct", - "requested": "[17.14.15, )", - "resolved": "17.14.15", - "contentHash": "mXQPJsbuUD2ydq4/ffd8h8tSOFCXec+2xJOVNCvXjuMOq/+5EKHq3D2m2MC2+nUaXeFMSt66VS/J4HdKBixgcw==" + "requested": "[18.7.23, )", + "resolved": "18.7.23", + "contentHash": "dyj6z8m+LFjpeH69hdnciCcRCmD0YgMlvuQEywiEshjqj/blgQ+PuWX8Vd5gkWQFoBN4e+TFxx6DvGNSv/gIKg==" }, "Roslynator.Analyzers": { "type": "Direct", - "requested": "[4.14.1, )", - "resolved": "4.14.1", - "contentHash": "yMSjze/xMYDF6PCE60/ULWx0tttNyKAndw2KijNxbKil0FX8nvDeEneDZGma8Uifk17RlfZqIXxf1mmBmhRHjg==" + "requested": "[4.15.0, )", + "resolved": "4.15.0", + "contentHash": "E8fu71Y+vzhniJKy1K13Rzwm8Vjcj7CDFBpflqncKUZfc3SzuF0kMBiyMmgXNPkjWTdE3Z+30HlGgYjHONZY/Q==" }, "Roslynator.CodeAnalysis.Analyzers": { "type": "Direct", - "requested": "[4.14.1, )", - "resolved": "4.14.1", - "contentHash": "axessewcgSLn+UAtKyMKPgXpzIldnivynjRe9qsRjrVtrxjctDpFDEjAkcbpDjhR0ccm8/+ihOna60wnXJ/66A==" + "requested": "[4.15.0, )", + "resolved": "4.15.0", + "contentHash": "XPY6vqHG8jLJ1S0SGzLDtyuDjmkcIAGuLRrsG3SlAuyAOlwnVfA1glgIy6YqXutoi7hwd1fqZC2W9zoMntuZNg==" }, "Roslynator.Formatting.Analyzers": { "type": "Direct", - "requested": "[4.14.1, )", - "resolved": "4.14.1", - "contentHash": "hALZsyELJXhj7WQw7Q7nLI7sELre8AlQIMyGxXpmpK+iBqShPcWHuJMGjIsTMa/bC8Q5xyQ4iFfRtm0bP1zisQ==" + "requested": "[4.15.0, )", + "resolved": "4.15.0", + "contentHash": "/LvSJGJbrx+OYC2GePmiAA9fRTfz37R/oaHC2j8YK/j9lE8ykZESCGTMVGqXF5woc6RbzfJlpYkzeG6fAV6hFQ==" }, "SonarAnalyzer.CSharp": { "type": "Direct", - "requested": "[10.15.0.120848, )", - "resolved": "10.15.0.120848", - "contentHash": "1hM3HVRl5jdC/ZBDu+G7CCYLXRGe/QaP01Zy+c9ETPhY7lWD8g8HiefY6sGaH0T3CJ4wAy0/waGgQTh0TYy0oQ==" + "requested": "[10.28.0.143324, )", + "resolved": "10.28.0.143324", + "contentHash": "ZnmYHFiQw2Aph2ft9c7UqVrqe79aZR5l7oeG59+sgrSAO9J159meglP1Zo34+Mcw4etIUTC9btYnP0Ar/rQIyg==" }, "StyleCop.Analyzers": { "type": "Direct", @@ -222,67 +234,64 @@ }, "Microsoft.Build.Tasks.Git": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ==" + "resolved": "10.0.300", + "contentHash": "P0kaQwVZx4xIUe2FtrLyBadYNXuAljttJUPvjBYRuHhPE8L77L42KakLDkaADRiUrGspoLcMwayjrbQhYTr0zA==", + "dependencies": { + "System.IO.Hashing": "10.0.8" + } }, "Microsoft.SourceLink.Common": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw==" + "resolved": "10.0.300", + "contentHash": "0jlkXaUGjYlWTIVPve5MftjKHnT3SlAtq9BCLV4J9IjdPrxV/+4rMlBSjfr1khG8/GC6KGojjola8E1VvWF0qQ==" + }, + "System.IO.Hashing": { + "type": "Transitive", + "resolved": "10.0.8", + "contentHash": "+dJsbPJ3FyUbTZNplFj0RCKePFizmv6ewDV46JE9q/IVH4c3xTCftHfHelLsAKf0jryIPqgMb5GpS0x7TAY3mg==" } }, "net9.0": { - "AsyncFixer": { - "type": "Direct", - "requested": "[1.6.0, )", - "resolved": "1.6.0", - "contentHash": "/Xfs9H3UMfEv64cwT+C/JrTRp4w08BmPuFbj0ageadCHpx6rxYJxAU2C6sEqRFG22xmGk5cX9ewzoiiehWVHOw==" - }, - "Gulliver": { - "type": "Direct", - "requested": "[3.0.0, )", - "resolved": "3.0.0", - "contentHash": "J232PI4ccScfLKt59UzyHBRZLcAJpyJ+N/at1fFF/XxBKwjl8idDVPkMU3FZkwaaU/e3Ert8/EnGLg5CxmQ8dQ==" - }, "Microsoft.SourceLink.GitHub": { "type": "Direct", - "requested": "[8.0.0, )", - "resolved": "8.0.0", - "contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==", + "requested": "[10.0.300, )", + "resolved": "10.0.300", + "contentHash": "QzCtLkXVb3l4IxcpvJCbzUwMLihAmLN6vVLjQGSzYSF8d2dvXxqJAZk83RV3gYnp2egz8jRMgSR2woY3vOahTA==", "dependencies": { - "Microsoft.Build.Tasks.Git": "8.0.0", - "Microsoft.SourceLink.Common": "8.0.0" + "Microsoft.Build.Tasks.Git": "10.0.300", + "Microsoft.SourceLink.Common": "10.0.300", + "System.IO.Hashing": "10.0.8" } }, "Microsoft.VisualStudio.Threading.Analyzers": { "type": "Direct", - "requested": "[17.14.15, )", - "resolved": "17.14.15", - "contentHash": "mXQPJsbuUD2ydq4/ffd8h8tSOFCXec+2xJOVNCvXjuMOq/+5EKHq3D2m2MC2+nUaXeFMSt66VS/J4HdKBixgcw==" + "requested": "[18.7.23, )", + "resolved": "18.7.23", + "contentHash": "dyj6z8m+LFjpeH69hdnciCcRCmD0YgMlvuQEywiEshjqj/blgQ+PuWX8Vd5gkWQFoBN4e+TFxx6DvGNSv/gIKg==" }, "Roslynator.Analyzers": { "type": "Direct", - "requested": "[4.14.1, )", - "resolved": "4.14.1", - "contentHash": "yMSjze/xMYDF6PCE60/ULWx0tttNyKAndw2KijNxbKil0FX8nvDeEneDZGma8Uifk17RlfZqIXxf1mmBmhRHjg==" + "requested": "[4.15.0, )", + "resolved": "4.15.0", + "contentHash": "E8fu71Y+vzhniJKy1K13Rzwm8Vjcj7CDFBpflqncKUZfc3SzuF0kMBiyMmgXNPkjWTdE3Z+30HlGgYjHONZY/Q==" }, "Roslynator.CodeAnalysis.Analyzers": { "type": "Direct", - "requested": "[4.14.1, )", - "resolved": "4.14.1", - "contentHash": "axessewcgSLn+UAtKyMKPgXpzIldnivynjRe9qsRjrVtrxjctDpFDEjAkcbpDjhR0ccm8/+ihOna60wnXJ/66A==" + "requested": "[4.15.0, )", + "resolved": "4.15.0", + "contentHash": "XPY6vqHG8jLJ1S0SGzLDtyuDjmkcIAGuLRrsG3SlAuyAOlwnVfA1glgIy6YqXutoi7hwd1fqZC2W9zoMntuZNg==" }, "Roslynator.Formatting.Analyzers": { "type": "Direct", - "requested": "[4.14.1, )", - "resolved": "4.14.1", - "contentHash": "hALZsyELJXhj7WQw7Q7nLI7sELre8AlQIMyGxXpmpK+iBqShPcWHuJMGjIsTMa/bC8Q5xyQ4iFfRtm0bP1zisQ==" + "requested": "[4.15.0, )", + "resolved": "4.15.0", + "contentHash": "/LvSJGJbrx+OYC2GePmiAA9fRTfz37R/oaHC2j8YK/j9lE8ykZESCGTMVGqXF5woc6RbzfJlpYkzeG6fAV6hFQ==" }, "SonarAnalyzer.CSharp": { "type": "Direct", - "requested": "[10.15.0.120848, )", - "resolved": "10.15.0.120848", - "contentHash": "1hM3HVRl5jdC/ZBDu+G7CCYLXRGe/QaP01Zy+c9ETPhY7lWD8g8HiefY6sGaH0T3CJ4wAy0/waGgQTh0TYy0oQ==" + "requested": "[10.28.0.143324, )", + "resolved": "10.28.0.143324", + "contentHash": "ZnmYHFiQw2Aph2ft9c7UqVrqe79aZR5l7oeG59+sgrSAO9J159meglP1Zo34+Mcw4etIUTC9btYnP0Ar/rQIyg==" }, "StyleCop.Analyzers": { "type": "Direct", @@ -292,13 +301,21 @@ }, "Microsoft.Build.Tasks.Git": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ==" + "resolved": "10.0.300", + "contentHash": "P0kaQwVZx4xIUe2FtrLyBadYNXuAljttJUPvjBYRuHhPE8L77L42KakLDkaADRiUrGspoLcMwayjrbQhYTr0zA==", + "dependencies": { + "System.IO.Hashing": "10.0.8" + } }, "Microsoft.SourceLink.Common": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw==" + "resolved": "10.0.300", + "contentHash": "0jlkXaUGjYlWTIVPve5MftjKHnT3SlAtq9BCLV4J9IjdPrxV/+4rMlBSjfr1khG8/GC6KGojjola8E1VvWF0qQ==" + }, + "System.IO.Hashing": { + "type": "Transitive", + "resolved": "10.0.8", + "contentHash": "+dJsbPJ3FyUbTZNplFj0RCKePFizmv6ewDV46JE9q/IVH4c3xTCftHfHelLsAKf0jryIPqgMb5GpS0x7TAY3mg==" } } } diff --git a/src/Directory.Build.props b/src/Directory.Build.props new file mode 100644 index 0000000..27e4b96 --- /dev/null +++ b/src/Directory.Build.props @@ -0,0 +1,30 @@ + + + net10.0;net9.0;net8.0;net48 + 14.0 + enable + true + true + + + + Sandia National Laboratories + Apache-2.0 + https://github.com/sandialabs/Arcus.git + true + true + + + + true + latest + true + + false + + + + + true + + diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets new file mode 100644 index 0000000..4ca4765 --- /dev/null +++ b/src/Directory.Build.targets @@ -0,0 +1,17 @@ + + + + + + false + true + + + + false + Exe + + + diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props new file mode 100644 index 0000000..2250cb6 --- /dev/null +++ b/src/Directory.Packages.props @@ -0,0 +1,33 @@ + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/run-benchmarks.ps1 b/src/run-benchmarks.ps1 new file mode 100644 index 0000000..50191c0 --- /dev/null +++ b/src/run-benchmarks.ps1 @@ -0,0 +1,34 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + Run Arcus benchmarks and export results to src\results\. + +.DESCRIPTION + Builds the benchmark project in Release mode and runs all benchmarks. + Results land in src\results\\ as JSON and Markdown files. + Commit that directory when you want to record a progression snapshot. + +.PARAMETER AdditionalArgs + Extra arguments forwarded to BenchmarkDotNet (e.g. "--filter *Subnet*"). + +.EXAMPLE + .\src\run-benchmarks.ps1 + .\src\run-benchmarks.ps1 -AdditionalArgs "--filter","*Subnet*" +#> + +param( + [string[]]$AdditionalArgs = @() +) + +$ErrorActionPreference = "Stop" + +$ScriptDir = $PSScriptRoot + +dotnet run --project "$ScriptDir\Arcus.Benchmarks" ` + -c Release ` + -- --artifacts "$ScriptDir\results" --exporters json markdown @AdditionalArgs + +if ($LASTEXITCODE -ne 0) { + Write-Host "Benchmarks failed with exit code $LASTEXITCODE" -ForegroundColor Red + exit $LASTEXITCODE +} diff --git a/src/run-benchmarks.sh b/src/run-benchmarks.sh new file mode 100644 index 0000000..7e836b8 --- /dev/null +++ b/src/run-benchmarks.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# Run Arcus benchmarks and export results to src/results/. +# +# Usage: +# ./src/run-benchmarks.sh # run all benchmarks +# ./src/run-benchmarks.sh -- --filter *Subnet* # run a subset +# +# Results land in src/results// as JSON and Markdown files. +# Commit that directory when you want to record a progression snapshot. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +dotnet run --project "${SCRIPT_DIR}/Arcus.Benchmarks" \ + -c Release \ + -- --artifacts "${SCRIPT_DIR}/results" --exporters json markdown "$@" diff --git a/src/stylecop.json b/stylecop.json similarity index 100% rename from src/stylecop.json rename to stylecop.json