├── .github ├── FUNDING.yml └── workflows │ ├── docs.disabled_yml │ ├── test-apple.yml │ └── test.yml ├── .gitignore ├── .swift-format ├── Benchmarks ├── .gitignore ├── Package.swift ├── Sources │ └── WebURLBenchmark │ │ ├── ComponentSetters.swift │ │ ├── Constructor+HTTP.swift │ │ ├── FoundationCompat.swift │ │ ├── PathComponents.swift │ │ ├── PercentEncoding.swift │ │ ├── SampleURLs.swift │ │ └── main.swift ├── compare.py └── results │ ├── .gitignore │ └── put_results_here_so_you_dont_commit_them ├── Fuzzers ├── .gitignore ├── Corpora │ └── put_corpus_in_a_subdir_here ├── Package.swift ├── README.md ├── Seeding │ └── generate_corpus_parse-reparse.swift ├── Sources │ ├── foundation-to-web │ │ └── main.swift │ ├── url-parse-reparse │ │ └── main.swift │ ├── web-foundation-roundtrip │ │ └── main.swift │ └── web-to-foundation │ │ └── main.swift ├── corpus-to-test.swift ├── fuzz-foundation-roundtrip.sh ├── fuzz-ftow.sh ├── fuzz-parser.sh ├── fuzz-wtof.sh └── url.dict ├── LICENSE ├── NOTICE ├── Package.swift ├── Package@swift-5.5.swift ├── README.md ├── Sources ├── IDNA │ ├── Generated │ │ ├── MappingData.swift │ │ └── ValidationData.swift │ ├── IDNA.swift │ ├── NFC.swift │ └── Punycode.swift ├── UnicodeDataStructures │ ├── ParseNPrint │ │ ├── IDNAMappingDatabase.swift │ │ ├── IDNAValidationDatabase.swift │ │ ├── ParsingHelpers.swift │ │ ├── Printing.swift │ │ └── Utils.swift │ └── Shared │ │ ├── CodePointDatabase.swift │ │ ├── IDNA │ │ ├── IDNAMappingDataSchema.swift │ │ └── IDNAValidationDataSchema.swift │ │ ├── IndexedTable.swift │ │ └── SegmentedLine.swift ├── WebURL │ ├── DeprecatedAPIs.swift │ ├── IPAddress.swift │ ├── Parser │ │ ├── Parser+Host.swift │ │ ├── Parser+Path.swift │ │ ├── Parser+StringUtils.swift │ │ ├── Parser.swift │ │ ├── URLWriter.swift │ │ ├── ValidationError.swift │ │ └── WebURL+Component.swift │ ├── PercentEncoding.swift │ ├── SPIs.swift │ ├── URLStorage+Setters.swift │ ├── URLStorage.swift │ ├── URLStructure.swift │ ├── Util │ │ ├── ASCII+LazyTextTransformations.swift │ │ ├── ASCII.swift │ │ ├── BidirectionalCollection+suffix.swift │ │ ├── BidirectionalCollection+trim.swift │ │ ├── BitTwiddling.swift │ │ ├── Collection+longestRange.swift │ │ ├── Either.swift │ │ ├── Errors.swift │ │ ├── FastCollectionAlgorithms.swift │ │ ├── Integers.swift │ │ ├── ManagedArrayBuffer.swift │ │ ├── MutableCollection+pathUtils.swift │ │ ├── Pointers.swift │ │ ├── StaticMember.swift │ │ ├── StringAdditions.swift │ │ ├── UnsafeBuffer+ReplaceSubrange.swift │ │ └── UnsafeSmallStack.swift │ ├── WebURL+Domain.swift │ ├── WebURL+FilePaths.swift │ ├── WebURL+FormParameters.swift │ ├── WebURL+Host.swift │ ├── WebURL+JSModel.swift │ ├── WebURL+Origin.swift │ ├── WebURL+PathComponents.swift │ ├── WebURL+Scheme.swift │ ├── WebURL+UTF8View.swift │ ├── WebURL.docc │ │ ├── Deprecated.md │ │ ├── FoundationInterop.md │ │ ├── PercentEncoding.md │ │ ├── WebURL.md │ │ └── WebURLStruct.md │ └── WebURL.swift ├── WebURLFoundationExtras │ ├── Extensions │ │ ├── DataAndString.swift │ │ ├── URLRequestResponse.swift │ │ └── URLSession.swift │ ├── FoundationToWebURL.swift │ ├── Util │ │ ├── ConversionError.swift │ │ ├── EvenFasterCollectionAlgorithms.swift │ │ └── StringAdditions.swift │ └── WebURLToFoundation.swift ├── WebURLSystemExtras │ └── WebURL+FilePaths+System.swift └── WebURLTestSupport │ ├── CollectionUtils.swift │ ├── IPAddressUtils.swift │ ├── JSONHelpers.swift │ ├── Reports │ ├── SimpleDescription.swift │ └── SimpleTestReport.swift │ ├── TestFiles.swift │ ├── TestFilesData │ ├── .gitattributes │ ├── IdnaTestV2.txt │ ├── additional_constructor_tests.json │ ├── additional_setters_tests.json │ ├── file_url_path_tests.json │ ├── setters_tests.json │ ├── toascii.json │ └── urltestdata.json │ ├── TestSuite.swift │ ├── TestSuites │ ├── FilePathTests+WebURLReportHarness.swift │ ├── FilePathTests.swift │ ├── UTS46Conformance+WebURLIDNAReportHarness.swift │ ├── UTS46Conformance.swift │ ├── WPTConstructorTest+WebURLReportHarness.swift │ ├── WPTConstructorTest.swift │ ├── WPTSetterTest+WebURLReportHarness.swift │ ├── WPTSetterTest.swift │ ├── WPTToASCIITest+WebURLReportHarness.swift │ └── WPTToASCIITest.swift │ └── URLValues.swift └── Tests ├── IDNATests ├── PunycodeTests.swift ├── SimpleToUnicodeTests.swift ├── UTS46ConformanceTests.swift └── Utils.swift ├── LinuxMain.swift ├── UnicodeDataStructuresTests ├── GenerateData │ ├── GenerateData.swift │ └── TableDefinitions │ │ ├── .gitattributes │ │ ├── DerivedBidiClass.txt │ │ ├── DerivedJoiningType.txt │ │ └── IdnaMappingTable.txt ├── IndexedTableTests.swift └── SegmentedLineTests.swift ├── WebURLDeprecatedAPITests └── DeprecatedAPITests.swift ├── WebURLFoundationEndToEndTests ├── LocalServer.swift └── URLSessionTests.swift ├── WebURLFoundationExtrasTests ├── Extensions │ ├── DataAndStringExtensions.swift │ └── URLRequestResponseExtensions.swift ├── URLConversion │ ├── FoundationToWebTests.disabled_swift │ ├── FuzzCorpus_foundation_to_web.swift │ ├── FuzzCorpus_web_to_foundation.swift │ ├── Resources │ │ └── NSURLTestData.plist │ └── WebToFoundationTests.swift └── _ReportGeneratingTestCase.swift ├── WebURLSystemExtrasTests ├── SystemFilePathTests.swift └── Utils.swift └── WebURLTests ├── ASCIITests.swift ├── CollectionExtensionsTests.swift ├── DomainTests.swift ├── FilePathTests.swift ├── FormParametersTests.swift ├── HostTests.swift ├── IPv4AddressTests.swift ├── IPv6AddressTests.swift ├── ManagedArrayBufferTests.swift ├── MutableCollectionExtensionsTests.swift ├── OtherURLTests.swift ├── OtherUtilitiesTests.swift ├── PathComponentsTests.swift ├── PercentEncodingTests.swift ├── SchemeKindTests.swift ├── Utils.swift ├── WebPlatformTests.swift ├── WebURLPercentEncodingUtilsTests.swift ├── WebURLTests.swift └── _ReportGeneratingTestCase.swift /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [karwa] 4 | -------------------------------------------------------------------------------- /.github/workflows/docs.disabled_yml: -------------------------------------------------------------------------------- 1 | name: Generate Documentation 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | # 1. Checkout the package (we need a full clone in order to get tags). 13 | # This must be the root folder for 'github-pages-deploy-action' to work. 14 | - name: Checkout Package 15 | uses: actions/checkout@v2 16 | with: 17 | fetch-depth: 0 18 | 19 | # 2. Download and Install Swift 5.5.1 20 | - name: Download Swift 5.5.1 21 | run: wget -q https://download.swift.org/swift-5.5.1-release/ubuntu2004/swift-5.5.1-RELEASE/swift-5.5.1-RELEASE-ubuntu20.04.tar.gz 22 | - name: Extract Swift 5.5.1 23 | run: tar xzf swift-5.5.1-RELEASE-ubuntu20.04.tar.gz 24 | - name: Add Swift toolchain to PATH 25 | run: | 26 | echo "$GITHUB_WORKSPACE/swift-5.5.1-RELEASE-ubuntu20.04/usr/bin" >> $GITHUB_PATH 27 | 28 | # 3. Checkout and build Swift-DocC (TODO: Remove once docc is included in the toolchain DL) 29 | - name: Checkout swift-docc 30 | uses: actions/checkout@v2 31 | with: 32 | repository: karwa/swift-docc 33 | ref: main 34 | path: swift-docc 35 | - name: Cache DocC 36 | id: cache-docc 37 | uses: actions/cache@v2 38 | with: 39 | key: swift-url-docc-build 40 | path: swift-docc/.build 41 | - name: Build swift-docc 42 | if: ${{ !steps.cache-docc.outputs.cache-hit }} 43 | run: | 44 | cd swift-docc; swift build --product docc -c release; cd .. 45 | 46 | # 4. Checkout and build custom Swift-DocC-Render 47 | - name: Checkout swift-docc-render 48 | uses: actions/checkout@v2 49 | with: 50 | repository: karwa/swift-docc-render 51 | ref: main 52 | path: swift-docc-render 53 | - name: Build swift-docc-render 54 | run: | 55 | cd swift-docc-render; npm install && npm run build; cd .. 56 | 57 | # 5. Checkout the gh-pages branch (we want to add to what is already there) 58 | - name: Checkout gh-pages Branch 59 | uses: actions/checkout@v2 60 | with: 61 | ref: gh-pages 62 | path: docs-out 63 | 64 | # 6. Generate docs for main branch, replacing the existing docs for main. 65 | - name: (main) Clear existing docs 66 | run: rm -rf docs-out/.git && rm -rf docs-out/main && mkdir -p docs-out/main 67 | - name: (main) Generate SymbolGraph 68 | run: | 69 | mkdir -p .build/symbol-graphs && swift build --target WebURL -Xswiftc -emit-symbol-graph -Xswiftc -emit-symbol-graph-dir -Xswiftc .build/symbol-graphs 70 | - name: (main) Build Documentation 71 | run: | 72 | export DOCC_HTML_DIR="$(pwd)/swift-docc-render/dist" && swift-docc/.build/release/docc convert Sources/WebURL/WebURL.docc --fallback-display-name WebURL --fallback-bundle-identifier com.karwa.WebURL --fallback-bundle-version 0.0.0 --additional-symbol-graph-dir .build/symbol-graphs --transform-for-static-hosting --hosting-base-path /swift-url/main --output-path docs-out/main 73 | 74 | # 7. Find out if documentation has already been generated for the latest tag. 75 | - name: (latest-tag) Find the Latest Tag 76 | run: | 77 | echo "latest-tag=$(git tag -l | tail -1)" >> $GITHUB_ENV 78 | - name: (latest-tag) Check if Documentation Exists 79 | run: | 80 | if [ -d "docs-out/${{ env.latest-tag }}" ]; then echo "latest-tag-has-docs=1" >> "$GITHUB_ENV"; fi 81 | 82 | # 8. If there is no documentation for the latest tag, build it. 83 | - name: (latest-tag) Checkout Package 84 | if: ${{ !env.latest-tag-has-docs }} 85 | uses: actions/checkout@v2 86 | with: 87 | ref: ${{ env.latest-tag }} 88 | path: latest-tag 89 | clean: false 90 | - name: (latest tag) Generate SymbolGraph 91 | if: ${{ !env.latest-tag-has-docs }} 92 | run: | 93 | pushd latest-tag && mkdir -p .build/symbol-graphs && swift build --target WebURL -Xswiftc -emit-symbol-graph -Xswiftc -emit-symbol-graph-dir -Xswiftc .build/symbol-graphs && popd 94 | - name: (latest tag) Create Output Directory 95 | if: ${{ !env.latest-tag-has-docs }} 96 | run: | 97 | mkdir -p docs-out/${{ env.latest-tag }} 98 | - name: (latest tag) Build Documentation 99 | if: ${{ !env.latest-tag-has-docs }} 100 | run: | 101 | pushd latest-tag && export DOCC_HTML_DIR="$(pwd)/../swift-docc-render/dist" && ../swift-docc/.build/release/docc convert Sources/WebURL/WebURL.docc --fallback-display-name WebURL --fallback-bundle-identifier com.karwa.WebURL --fallback-bundle-version ${{ env.latest-tag }} --additional-symbol-graph-dir .build/symbol-graphs --transform-for-static-hosting --hosting-base-path /swift-url/${{ env.latest-tag }} --output-path ../docs-out/${{ env.latest-tag }} && popd 102 | - name: (latest tag) Update symlink 103 | if: ${{ !env.latest-tag-has-docs }} 104 | run: | 105 | pushd docs-out && ln -sfn ${{ env.latest-tag }} latest && popd 106 | 107 | # 9. Publish the result 108 | - name: Fix permissions 109 | run: 'sudo chown --recursive $USER docs-out' 110 | - name: Publish documentation to GitHub Pages 111 | uses: JamesIves/github-pages-deploy-action@4.1.7 112 | with: 113 | branch: gh-pages 114 | folder: docs-out 115 | single-commit: true -------------------------------------------------------------------------------- /.github/workflows/test-apple.yml: -------------------------------------------------------------------------------- 1 | name: Swift package tests (Apple) 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | test-macos12: 11 | strategy: 12 | matrix: 13 | xcode-version: 14 | - 13.2.1 # Swift 5.5.3 15 | - 13.4.1 # Swift 5.6.3 16 | - 14.2 # Swift 5.7.2 17 | runs-on: macos-12 18 | continue-on-error: ${{ matrix.xcode-version == '13.2.1' || matrix.xcode-version == '13.4.1' }} # Package resolution can be flaky. 19 | steps: 20 | - uses: actions/checkout@v3 21 | - name: Build 22 | run: | 23 | env DEVELOPER_DIR="/Applications/Xcode_${{ matrix.xcode-version }}.app" xcrun xcodebuild -resolvePackageDependencies -clonedSourcePackagesDirPath "$PWD/.dependencies" 24 | env DEVELOPER_DIR="/Applications/Xcode_${{ matrix.xcode-version }}.app" xcrun xcodebuild -IDEClonedSourcePackagesDirPathOverride="$PWD/.dependencies" -derivedDataPath "$PWD/.derivedData-${{ matrix.xcode-version }}" -scheme "swift-url-Package" -destination "generic/platform=macos" build 25 | - name: Run tests 26 | run: | 27 | env SWIFT_URL_REPORT_PATH="/private/var/tmp/swift-url-report/" DEVELOPER_DIR="/Applications/Xcode_${{ matrix.xcode-version }}.app" xcrun xcodebuild -IDEClonedSourcePackagesDirPathOverride="$PWD/.dependencies" -derivedDataPath "$PWD/.derivedData-${{ matrix.xcode-version }}" -scheme "swift-url-Package" -destination "platform=macos" -retry-tests-on-failure test 28 | - name: Upload report files 29 | uses: actions/upload-artifact@v3 30 | with: 31 | name: test-reports-macos-xcode-${{ matrix.xcode-version }} 32 | path: /private/var/tmp/swift-url-report/* 33 | if-no-files-found: warn 34 | 35 | build-only: 36 | strategy: 37 | matrix: 38 | target-os: [macos, ios, tvos, watchos] 39 | runs-on: macos-12 40 | steps: 41 | - uses: actions/checkout@v3 42 | - name: Build 43 | run: | 44 | xcrun xcodebuild -IDEClonedSourcePackagesDirPathOverride="$PWD/.dependencies" -derivedDataPath "$PWD/.derivedData-${{ matrix.target-os }}" -scheme "swift-url-Package" -destination "generic/platform=${{ matrix.target-os }}" build 45 | - name: Build benchmarks 46 | if: ${{ matrix.target-os == 'macos' }} 47 | run: | 48 | cd Benchmarks 49 | swift build -v 50 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Swift package tests 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | test: 11 | strategy: 12 | matrix: 13 | swift-version: [5.3.3, 5.4.3, 5.5.3, 5.6.3, 5.7.2] 14 | host-os: [ubuntu-20.04, windows-latest] 15 | exclude: 16 | - host-os: windows-latest 17 | swift-version: 5.3.3 18 | - host-os: windows-latest 19 | swift-version: 5.4.3 20 | - host-os: windows-latest 21 | swift-version: 5.5.3 22 | - host-os: windows-latest 23 | swift-version: 5.6.3 24 | continue-on-error: ${{ matrix.host-os == 'Windows' }} 25 | runs-on: ${{ matrix.host-os }} 26 | steps: 27 | - uses: actions/checkout@v3 28 | # 1. Install Swift. 29 | - name: (Windows) Install Swift ${{ matrix.swift-version }} 30 | if: ${{ runner.os == 'Windows' }} 31 | uses: compnerd/gha-setup-swift@main 32 | with: 33 | branch: swift-${{ matrix.swift-version }}-release 34 | tag: ${{ matrix.swift-version }}-RELEASE 35 | - name: (Windows) Set Swift command 36 | if: ${{ runner.os == 'Windows' }} 37 | run: echo "swift_cmd=swift" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append 38 | - name: (Linux) Install Swift ${{ matrix.swift-version }} 39 | if: ${{ runner.os == 'Linux' }} 40 | run: | 41 | wget -q https://download.swift.org/swift-${{ matrix.swift-version }}-release/ubuntu2004/swift-${{ matrix.swift-version }}-RELEASE/swift-${{ matrix.swift-version }}-RELEASE-ubuntu20.04.tar.gz 42 | tar xzf swift-${{ matrix.swift-version }}-RELEASE-ubuntu20.04.tar.gz 43 | rm swift-${{ matrix.swift-version }}-RELEASE-ubuntu20.04.tar.gz 44 | echo "$GITHUB_WORKSPACE/swift-${{ matrix.swift-version }}-RELEASE-ubuntu20.04/usr/bin" >> $GITHUB_PATH 45 | echo "swift_cmd=swift" >> $GITHUB_ENV 46 | # macOS builds are tested using a different matrix, 47 | # consisting of Xcode versions rather than swift toolchain versions. 48 | # This step exists as a fallback, in case that other matrix ends up not being reliable enough. 49 | - name: (Mac) Install Swift ${{ matrix.swift-version }} 50 | if: ${{ runner.os == 'macOS' }} 51 | run: | 52 | wget -q https://download.swift.org/swift-${{ matrix.swift-version }}-release/xcode/swift-${{ matrix.swift-version }}-RELEASE/swift-${{ matrix.swift-version }}-RELEASE-osx.pkg 53 | sudo installer -pkg swift-${{ matrix.swift-version }}-RELEASE-osx.pkg -target LocalSystem 54 | rm swift-${{ matrix.swift-version }}-RELEASE-osx.pkg 55 | echo "swift_cmd=xcrun -toolchain swift swift" >> $GITHUB_ENV 56 | - name: Check installation 57 | run: ${{ env.swift_cmd }} --version 58 | # 2. Build the package. 59 | - name: Build 60 | run: ${{ env.swift_cmd }} build -v 61 | # 3. Run the tests. 62 | - name: (Windows) Set test reports directory 63 | if: ${{ runner.os == 'Windows' }} 64 | run: echo "SWIFT_URL_REPORT_PATH=C:\tmp\swift-url-report" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append 65 | - name: (Linux, Mac) Set test reports directory 66 | if: ${{ runner.os == 'Linux' || runner.os == 'macOS' }} 67 | run: echo "SWIFT_URL_REPORT_PATH=/tmp/swift-url-report/" >> $GITHUB_ENV 68 | - name: Run tests 69 | run: ${{ env.swift_cmd }} test --enable-test-discovery -v 70 | # 4. Check that the Benchmarks sub-package builds. 71 | - name: Build benchmarks 72 | if: ${{ runner.os == 'Linux' || runner.os == 'macOS' }} 73 | run: | 74 | cd Benchmarks 75 | ${{ env.swift_cmd }} build -v 76 | # 5. Done. Report results. 77 | - name: Upload report files 78 | uses: actions/upload-artifact@v3 79 | if: always() 80 | with: 81 | name: test-reports-${{ matrix.host-os }}-${{ matrix.swift-version }} 82 | path: ${{ env.SWIFT_URL_REPORT_PATH }}/* 83 | if-no-files-found: warn -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /.swiftpm 4 | /Packages 5 | /*.xcodeproj 6 | xcuserdata/ 7 | Package.resolved 8 | .docc-build/ 9 | .vscode/ -------------------------------------------------------------------------------- /.swift-format: -------------------------------------------------------------------------------- 1 | { 2 | "lineLength" : 120, 3 | "maximumBlankLines" : 2, 4 | 5 | "blankLineBetweenMembers" : { 6 | "ignoreSingleLineProperties" : true 7 | }, 8 | "indentation" : { 9 | "spaces" : 2 10 | }, 11 | "indentConditionalCompilationBlocks" : true, 12 | "lineBreakBeforeControlFlowKeywords" : false, 13 | "lineBreakBeforeEachArgument" : false, 14 | "respectsExistingLineBreaks" : true, 15 | "rules" : { 16 | "AllPublicDeclarationsHaveDocumentation" : true, 17 | "AlwaysUseLowerCamelCase" : true, 18 | "AmbiguousTrailingClosureOverload" : true, 19 | "BeginDocumentationCommentWithOneLineSummary" : true, 20 | "BlankLineBetweenMembers" : true, 21 | "CaseIndentLevelEqualsSwitch" : true, 22 | "DoNotUseSemicolons" : true, 23 | "DontRepeatTypeInStaticProperties" : true, 24 | "FullyIndirectEnum" : true, 25 | "GroupNumericLiterals" : true, 26 | "IdentifiersMustBeASCII" : true, 27 | "MultiLineTrailingCommas" : true, 28 | "NeverForceUnwrap" : true, 29 | "NeverUseForceTry" : true, 30 | "NeverUseImplicitlyUnwrappedOptionals" : true, 31 | "NoAccessLevelOnExtensionDeclaration" : true, 32 | "NoBlockComments" : true, 33 | "NoCasesWithOnlyFallthrough" : true, 34 | "NoEmptyTrailingClosureParentheses" : true, 35 | "NoLabelsInCasePatterns" : true, 36 | "NoLeadingUnderscores" : true, 37 | "NoParensAroundConditions" : true, 38 | "NoVoidReturnOnFunctionSignature" : true, 39 | "OneCasePerLine" : true, 40 | "OneVariableDeclarationPerLine" : true, 41 | "OnlyOneTrailingClosureArgument" : true, 42 | "OrderedImports" : true, 43 | "ReturnVoidInsteadOfEmptyTuple" : true, 44 | "UseEnumForNamespacing" : true, 45 | "UseLetInEveryBoundCaseVariable" : true, 46 | "UseShorthandTypeNames" : false, 47 | "UseSingleLinePropertyGetter" : true, 48 | "UseSynthesizedInitializer" : true, 49 | "UseTripleSlashForDocumentationComments" : true, 50 | "ValidateDocumentationComments" : true 51 | }, 52 | "tabWidth" : 8, 53 | "version" : 1 54 | } 55 | -------------------------------------------------------------------------------- /Benchmarks/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /.swiftpm 4 | /Packages 5 | /*.xcodeproj 6 | xcuserdata/ 7 | Package.resolved -------------------------------------------------------------------------------- /Benchmarks/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | // Copyright The swift-url Contributors. 5 | // 6 | // Licensed under the Apache License, Version 2.0 (the "License"); 7 | // you may not use this file except in compliance with the License. 8 | // You may obtain a copy of the License at 9 | // 10 | // http://www.apache.org/licenses/LICENSE-2.0 11 | // 12 | // Unless required by applicable law or agreed to in writing, software 13 | // distributed under the License is distributed on an "AS IS" BASIS, 14 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | // See the License for the specific language governing permissions and 16 | // limitations under the License. 17 | 18 | import PackageDescription 19 | 20 | let package = Package( 21 | name: "swift-url-benchmark", 22 | products: [ 23 | .executable(name: "WebURLBenchmark", targets: ["WebURLBenchmark"]) 24 | ], 25 | dependencies: [ 26 | .package(name: "Benchmark", url: "https://github.com/google/swift-benchmark", from: "0.1.0"), 27 | .package(name: "swift-url", path: ".."), 28 | ], 29 | targets: [ 30 | .target( 31 | name: "WebURLBenchmark", 32 | dependencies: [ 33 | .product(name: "WebURL", package: "swift-url"), 34 | .product(name: "WebURLFoundationExtras", package: "swift-url"), 35 | .product(name: "Benchmark", package: "Benchmark") 36 | ] 37 | ) 38 | ] 39 | ) 40 | -------------------------------------------------------------------------------- /Benchmarks/Sources/WebURLBenchmark/FoundationCompat.swift: -------------------------------------------------------------------------------- 1 | // Copyright The swift-url Contributors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Benchmark 16 | import Foundation 17 | import WebURL 18 | import WebURLFoundationExtras 19 | 20 | /// Benchmarks of WebURL-Foundation interoperability features. 21 | /// 22 | internal enum FoundationCompat {} 23 | 24 | 25 | // -------------------------------------------- 26 | // MARK: - NSURLToWeb 27 | // -------------------------------------------- 28 | 29 | 30 | extension FoundationCompat { 31 | 32 | /// Benchmarks the `WebURL.init?(Foundation.URL)` constructor. 33 | /// 34 | internal static var NSURLToWeb: BenchmarkSuite { 35 | BenchmarkSuite(name: "FoundationCompat.NSURLToWeb") { suite in 36 | 37 | let AverageHTTPURLs_Foundation = SampleURLs.AverageHTTPURLs.map { URL(string: $0)! } 38 | 39 | suite.benchmark("AverageURLs") { state in 40 | blackHole(AverageHTTPURLs_Foundation) 41 | try state.measure { 42 | for url in AverageHTTPURLs_Foundation { 43 | blackHole(WebURL(url)) 44 | } 45 | } 46 | } 47 | 48 | let ExoticIPv4URLs_Foundation = SampleURLs.ExoticIPv4URLs.map { URL(string: $0)! } 49 | 50 | suite.benchmark("IPv4") { state in 51 | blackHole(ExoticIPv4URLs_Foundation) 52 | try state.measure { 53 | for url in ExoticIPv4URLs_Foundation { 54 | blackHole(WebURL(url)) 55 | } 56 | } 57 | } 58 | 59 | let IPv6URLs_Foundation = SampleURLs.IPv6URLs.map { URL(string: $0)! } 60 | 61 | suite.benchmark("IPv6") { state in 62 | blackHole(IPv6URLs_Foundation) 63 | try state.measure { 64 | for url in IPv6URLs_Foundation { 65 | blackHole(WebURL(url)) 66 | } 67 | } 68 | } 69 | 70 | // End of suite. 71 | } 72 | } 73 | } 74 | 75 | 76 | // -------------------------------------------- 77 | // MARK: - WebToNSURL 78 | // -------------------------------------------- 79 | 80 | 81 | extension FoundationCompat { 82 | 83 | /// Benchmarks the `Foundation.URL.init?(WebURL)` constructor. 84 | /// 85 | internal static var WebToNSURL: BenchmarkSuite { 86 | BenchmarkSuite(name: "FoundationCompat.WebToNSURL") { suite in 87 | 88 | let AverageHTTPURLs_Web = SampleURLs.AverageHTTPURLs.map { WebURL($0)! } 89 | 90 | suite.benchmark("AverageURLs") { state in 91 | blackHole(AverageHTTPURLs_Web) 92 | try state.measure { 93 | for url in AverageHTTPURLs_Web { 94 | blackHole(URL(url)) 95 | } 96 | } 97 | } 98 | 99 | let ExoticIPv4URLs_Web = SampleURLs.ExoticIPv4URLs.map { WebURL($0)! } 100 | 101 | suite.benchmark("IPv4") { state in 102 | blackHole(ExoticIPv4URLs_Web) 103 | try state.measure { 104 | for url in ExoticIPv4URLs_Web { 105 | blackHole(URL(url)) 106 | } 107 | } 108 | } 109 | 110 | let IPv6URLs_Web = SampleURLs.IPv6URLs.map { WebURL($0)! } 111 | 112 | suite.benchmark("IPv6") { state in 113 | blackHole(IPv6URLs_Web) 114 | try state.measure { 115 | for url in IPv6URLs_Web { 116 | blackHole(URL(url)) 117 | } 118 | } 119 | } 120 | 121 | // End of suite. 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /Benchmarks/Sources/WebURLBenchmark/PathComponents.swift: -------------------------------------------------------------------------------- 1 | // Copyright The swift-url Contributors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Benchmark 16 | import WebURL 17 | 18 | /// Benchmarks the WebURL `.pathComponents` view. 19 | /// 20 | let PathComponents = BenchmarkSuite(name: "PathComponents") { suite in 21 | 22 | // Iteration. 23 | 24 | suite.benchmark("Iteration.Small.Forwards") { state in 25 | var url = WebURL("http://example.com/hello/this/a/path/with/a/couple/of/components")! 26 | try state.measure { 27 | for component in url.pathComponents { 28 | blackHole(component) 29 | } 30 | } 31 | blackHole(url) 32 | } 33 | 34 | suite.benchmark("Iteration.Small.Reverse") { state in 35 | var url = WebURL("http://example.com/hello/this/a/path/with/a/couple/of/components")! 36 | try state.measure { 37 | for component in url.pathComponents.reversed() as ReversedCollection { 38 | blackHole(component) 39 | } 40 | } 41 | blackHole(url) 42 | } 43 | 44 | suite.benchmark("Iteration.Long.Forwards") { state in 45 | var url = WebURL("http://example.com/hello/this/a/path/with/a/couple/of/components/hello/this/a/path/with/a/couple/of/components/hello/this/a/path/with/a/couple/of/components/hello/this/a/path/with/a/couple/of/components//hello/this/a/path/with/a/couple/of/components")! 46 | try state.measure { 47 | for component in url.pathComponents { 48 | blackHole(component) 49 | } 50 | } 51 | blackHole(url) 52 | } 53 | 54 | suite.benchmark("Iteration.Long.Reverse") { state in 55 | var url = WebURL("http://example.com/hello/this/a/path/with/a/couple/of/components/hello/this/a/path/with/a/couple/of/components/hello/this/a/path/with/a/couple/of/components/hello/this/a/path/with/a/couple/of/components//hello/this/a/path/with/a/couple/of/components")! 56 | try state.measure { 57 | for component in url.pathComponents.reversed() as ReversedCollection { 58 | blackHole(component) 59 | } 60 | } 61 | blackHole(url) 62 | } 63 | 64 | // Append. 65 | 66 | suite.benchmark("Append.Single") { state in 67 | var url = WebURL("http://example.com/")! 68 | try state.measure { 69 | url.pathComponents.append("foo") 70 | } 71 | blackHole(url) 72 | } 73 | 74 | suite.benchmark("Append.Multiple") { state in 75 | var url = WebURL("http://example.com/")! 76 | try state.measure { 77 | url.pathComponents += ["foo", "bar", "baz"] 78 | } 79 | blackHole(url) 80 | } 81 | 82 | // RemoveLast. 83 | 84 | suite.benchmark("RemoveLast.Single") { state in 85 | var url = WebURL("http://example.com/foo/bar/baz")! 86 | try state.measure { 87 | url.pathComponents.removeLast() 88 | } 89 | blackHole(url) 90 | } 91 | 92 | suite.benchmark("RemoveLast.Multiple") { state in 93 | var url = WebURL("http://example.com/foo/bar/baz/qux")! 94 | try state.measure { 95 | url.pathComponents.removeLast(3) 96 | } 97 | blackHole(url) 98 | } 99 | 100 | // ReplaceSubrange. 101 | 102 | suite.benchmark("ReplaceSubrange.Shrink") { state in 103 | var url = WebURL("http://example.com/foo/bar/baz/qux/")! 104 | let start = url.pathComponents.index(url.pathComponents.startIndex, offsetBy: 1) 105 | let end = url.pathComponents.index(start, offsetBy: 2) 106 | try state.measure { 107 | url.pathComponents.replaceSubrange(start..(_ x: T) { 20 | @_optimize(none) 21 | func assumePointeeIsRead(_ x: UnsafeRawPointer) {} 22 | withUnsafePointer(to: x) { assumePointeeIsRead($0) } 23 | } 24 | 25 | // Benchmark plan: 26 | // - Non-special versions of SpecialNonFile tests. 27 | // - Cannot-be-a-base URLs 28 | // - file: URLs 29 | 30 | Benchmark.main([ 31 | Constructor.HTTP, 32 | ComponentSetters, 33 | PathComponents, 34 | PercentEncoding, 35 | FoundationCompat.NSURLToWeb, 36 | FoundationCompat.WebToNSURL, 37 | ]) 38 | -------------------------------------------------------------------------------- /Benchmarks/results/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | -------------------------------------------------------------------------------- /Benchmarks/results/put_results_here_so_you_dont_commit_them: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karwa/swift-url/9306a962396a50d7d88e924afcd7ec67226763db/Benchmarks/results/put_results_here_so_you_dont_commit_them -------------------------------------------------------------------------------- /Fuzzers/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /.swiftpm 4 | /Packages 5 | /*.xcodeproj 6 | xcuserdata/ 7 | Package.resolved 8 | 9 | /Corpora/ 10 | -------------------------------------------------------------------------------- /Fuzzers/Corpora/put_corpus_in_a_subdir_here: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karwa/swift-url/9306a962396a50d7d88e924afcd7ec67226763db/Fuzzers/Corpora/put_corpus_in_a_subdir_here -------------------------------------------------------------------------------- /Fuzzers/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | 3 | // Copyright The swift-url Contributors. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | import PackageDescription 18 | 19 | let package = Package( 20 | name: "swift-url-fuzzers", 21 | products: [ 22 | // Parses a single input as a URL string. 23 | // Valid URLs are re-parsed to ensure parsing/serialization is idempotent. 24 | .executable(name: "url-parse-reparse", targets: ["url-parse-reparse"]), 25 | 26 | // Parses a single input as a Swift String, then a Foundation URL. 27 | // If successful, converts the URL to a WebURL and checks for semantic equivalence. 28 | .executable(name: "foundation-to-web", targets: ["foundation-to-web"]), 29 | 30 | // Parses a single input as a URL string. 31 | // If successful, converts the URL to a Foundation URL and checks for semantic equivalence. 32 | .executable(name: "web-to-foundation", targets: ["web-to-foundation"]), 33 | 34 | // Parses a single input as a URL string and uses '.encodedForFoundation' to add percent-encoding. 35 | // The URL is then converted to a Foundation URL. If successful, it must round-trip back to WebURL 36 | // and the round-tripped URL must be exactly the same as the (encoded) original. 37 | .executable(name: "web-foundation-roundtrip", targets: ["web-foundation-roundtrip"]), 38 | ], 39 | dependencies: [ 40 | .package(name: "swift-url", path: "..") 41 | ], 42 | targets: [ 43 | .target( 44 | name: "url-parse-reparse", 45 | dependencies: [.product(name: "WebURL", package: "swift-url")], 46 | swiftSettings: [.unsafeFlags(["-parse-as-library", "-sanitize=fuzzer,address"])] 47 | ), 48 | .target( 49 | name: "foundation-to-web", 50 | dependencies: [ 51 | .product(name: "WebURL", package: "swift-url"), 52 | .product(name: "WebURLFoundationExtras", package: "swift-url"), 53 | ], 54 | swiftSettings: [.unsafeFlags(["-parse-as-library", "-sanitize=fuzzer,address"])] 55 | ), 56 | .target( 57 | name: "web-to-foundation", 58 | dependencies: [ 59 | .product(name: "WebURL", package: "swift-url"), 60 | .product(name: "WebURLFoundationExtras", package: "swift-url"), 61 | ], 62 | swiftSettings: [.unsafeFlags(["-parse-as-library", "-sanitize=fuzzer,address"])] 63 | ), 64 | .target( 65 | name: "web-foundation-roundtrip", 66 | dependencies: [ 67 | .product(name: "WebURL", package: "swift-url"), 68 | .product(name: "WebURLFoundationExtras", package: "swift-url"), 69 | ], 70 | swiftSettings: [.unsafeFlags(["-parse-as-library", "-sanitize=fuzzer,address"])] 71 | ), 72 | ] 73 | ) 74 | -------------------------------------------------------------------------------- /Fuzzers/README.md: -------------------------------------------------------------------------------- 1 | # Fuzzing WebURL 2 | 3 | Fuzzing is a way of testing an API with generated data, and can help discover bugs in scenarios that developers wouldn't think to test. 4 | For more information on fuzzing, see [google/fuzzing: why fuzz?](https://github.com/google/fuzzing/blob/master/docs/why-fuzz.md). 5 | 6 | WebURL uses Swift's built-in support for LLVM's `libfuzzer` to support these kinds of tests on our API. Note that the version of Swift 7 | included with Apple's SDK does *not* include this support. In order to fuzz on an Apple platform, a toolchain from 8 | [swift.org](https://swift.org/download/) must first be installed. 9 | 10 | Currently, the following fuzzers are available: 11 | 12 | - `url-parse-reparse` 13 | 14 | Parses some bytes generated by the fuzzer (regardless if it is valid UTF-8), and if successful, re-parses the URL's serialized 15 | representation and checks that the result is exactly the same. This ensures that the parser behaves itself (and won't crash or attempt 16 | an out-of-bounds access, even if you literally give it random bytes to parse), and validates one of the [goals](https://url.spec.whatwg.org/#goals) 17 | of the URL living standard, that a successfully parsed URL will not change no matter how many times it is serialized to-/parsed from a string. 18 | 19 | To build and run this fuzzer, use the `fuzz-parser` script: 20 | ``` 21 | ./fuzz-parser.sh -max_len=20 -dict=url.dict 22 | ``` 23 | 24 | - `foundation-to-web` 25 | 26 | Parses some bytes generated by the fuzzer as a Swift String, then as a Foundation URL. 27 | If successful, the URL is converted to a WebURL and the results are checked for semantic equivalence. 28 | This has been incredibly helpful in developing Foundation interoperability, and fuzz-testing Foundation against WebURL's implementation. 29 | 30 | To build and run this fuzzer, use the `fuzz-ftow` script: 31 | ``` 32 | ./fuzz-ftow.sh -jobs=4 -max_len=16 -dict=url.dict Corpora/foundation-to-web 33 | ``` 34 | 35 | - `web-to-foundation` 36 | 37 | Parses some bytes generated by the fuzzer as a WebURL, and if successful, attempts to convert it to a Foundation URL. 38 | If conversion is also successful, the results are checked for semantic equivalence. 39 | 40 | To build and run this fuzzer, use the `fuzz-wtof` script: 41 | ``` 42 | ./fuzz-wtof.sh -jobs=4 -max_len=16 -dict=url.dict Corpora/web-to-foundation 43 | ``` 44 | 45 | - `web-foundation-roundtrip` 46 | 47 | Parses some bytes generated by the fuzzer as a WebURL, and encodes it for Foundation using the `.encodedForFoundation` property. 48 | The result if then converted to a Foundation URL. If successful, the Foundation URL must round-trip back to a WebURL, 49 | and that WebURL must be identical to the URL returned by `.encodedForFoundation`. 50 | 51 | To build and run this fuzzer, use the `fuzz-foundation-roundtrip` script: 52 | ``` 53 | ./fuzz-foundation-roundtrip.sh -jobs=4 -max_len=16 -dict=url.dict Corpora/foundation-roundtrip 54 | ``` 55 | 56 | Any arguments provided to the `fuzz-*.sh` scripts will be forwarded to the fuzzer executable. 57 | Use the `-help=1` argument for more information about the supported arguments. 58 | 59 | Eventually, the goal is to also add fuzzers for the `resolve` function (parsing a relative URL against a base URL), and as many setters and 60 | mutating APIs as is practical. Ideally, they would be run continuously as the code changes to detect any bugs that may be introduced, 61 | but in practice they are each given ~12 hours of exercise every 4-6 weeks, or when particularly large code changes are made. 62 | That's good for a couple hundred million iterations, and if there are issues, it tends to find them fairly quickly. 63 | 64 | ## Corpora 65 | 66 | Fuzzing is greatly helped by the presence of a corpus - a collection of API inputs which exercise as many unique code paths as possible. 67 | The fuzzer is able to discover these inputs by itself and store them in a directory you specify. Storing the corpus allows you to stop 68 | and restart the fuzzer while keeping the coverage discovered by previous runs. 69 | 70 | To specify where the fuzzer should store its corpus, supply a directory path as an argument. If the directory already contains a corpus, 71 | fuzzing will be resumed using that data. The following command generates a corpus at "Corpora/parse-reparse": 72 | 73 | ``` 74 | ./fuzz-parser.sh -max_len=20 -dict=url.dict Corpora/parse-reparse 75 | ``` 76 | 77 | (Note: the contents of the "Corpora" directory are ignored by git, so you can safely store corpora there without worrying that you might commit them). 78 | 79 | However, it is usually better _not_ to start from nothing; it will take the fuzzer a long time to discover a good set of inputs. 80 | Generating a seed corpus allows you to give the fuzzer a bit of a head-start, and greatly reduces the time required to achieve good coverage. 81 | 82 | The "Seeding" directory contains scripts which you can use to generate a seed corpus. You can edit these scripts, add some URL strings 83 | (possibly copied from the benchmark or test suites, or anything else you think of), and then run them to create a directory of sample 84 | inputs for the fuzzer to use when building its own corpus. For example: 85 | 86 | ``` 87 | ./Seeding/generate_corpus_parse-reparse.swift 88 | 89 | mkdir -p Corpora/parse-reparse 90 | ./fuzz-parser.sh -max_len=20 -dict=url.dict Corpora/parse-reparse Seeding/parse-reparse 91 | ``` 92 | 93 | The first line will invoke a swift script, which generates a corpus using the URL strings contained in the script, and stores the results in 94 | "Seeding/parse-reparse". We then supply that corpus to the fuzzer _after_ its own corpus directory; it will analyse the seed inputs, 95 | and any interesting code-paths it discovers will inform how it creates/adds to the corpus in the "Corpora/parse-reparse" directory. 96 | -------------------------------------------------------------------------------- /Fuzzers/Seeding/generate_corpus_parse-reparse.swift: -------------------------------------------------------------------------------- 1 | #!/usr/bin/swift 2 | 3 | // Copyright The swift-url Contributors. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | import Foundation 18 | 19 | let seed_urls: [String] = [ 20 | // Add URLs for the seed corpus here. 21 | #"http://a/b/c?d#e"#, 22 | ] 23 | 24 | let dirName = "parse-reparse" 25 | let fileManager = FileManager() 26 | 27 | guard !fileManager.fileExists(atPath: dirName) else { 28 | fatalError("Unable to generate seed corpus - directory '\(dirName)' already exists") 29 | } 30 | try fileManager.createDirectory(atPath: dirName, withIntermediateDirectories: false) 31 | 32 | for (i, url) in seed_urls.enumerated() { 33 | guard fileManager.createFile(atPath: "\(dirName)/\(i)", contents: Data(url.utf8)) else { 34 | print("Unable to create seed file - \(i)") 35 | break 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Fuzzers/Sources/foundation-to-web/main.swift: -------------------------------------------------------------------------------- 1 | // Copyright The swift-url Contributors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Foundation 16 | import WebURL 17 | import WebURLFoundationExtras 18 | 19 | @_cdecl("LLVMFuzzerTestOneInput") 20 | public func foundation_to_web(_ start: UnsafePointer, _ count: Int) -> CInt { 21 | 22 | let bytes = UnsafeBufferPointer(start: start, count: count) 23 | 24 | // 1. Parse the bytes generated by the fuzzer as a Swift String, then as a Foundation URL. 25 | 26 | let input = String(decoding: bytes, as: UTF8.self) 27 | 28 | guard let foundationURL = URL(string: input) else { 29 | return 0 // Fuzzer did not produce a URL. Try again. 30 | } 31 | 32 | // 2. Convert the Foundation URL to a WebURL. 33 | 34 | guard let webURL = WebURL(foundationURL) else { 35 | return 0 // WebURL didn't convert the URL. That's fine. 36 | } 37 | 38 | // 3. Perform a full equivalence check of every component. 39 | 40 | var foundationURLString = foundationURL.absoluteString 41 | let areEquivalent = foundationURLString.withUTF8 { foundationStringUTF8 in 42 | WebURL._SPIs._checkEquivalence_f2w(webURL, foundationURL, foundationString: foundationStringUTF8, shortcuts: false) 43 | } 44 | guard areEquivalent else { 45 | fatalError( 46 | """ 47 | URLs are not equivalent: 48 | - Input string: \(input) 49 | - Foundation.URL: \(foundationURL) 50 | - WebURL: \(webURL) 51 | """ 52 | ) 53 | } 54 | 55 | // All checks passed. 56 | return 0 57 | } 58 | -------------------------------------------------------------------------------- /Fuzzers/Sources/url-parse-reparse/main.swift: -------------------------------------------------------------------------------- 1 | // Copyright The swift-url Contributors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import WebURL 16 | 17 | @_cdecl("LLVMFuzzerTestOneInput") 18 | public func url_parse_reparse(_ start: UnsafePointer, _ count: Int) -> CInt { 19 | 20 | let bytes = UnsafeBufferPointer(start: start, count: count) 21 | 22 | // 1. Parse the bytes generated by the fuzzer. 23 | 24 | guard let url = WebURL(utf8: bytes) else { 25 | return 0 // Fuzzer did not produce a URL. Try again. 26 | } 27 | 28 | // 2. Reparse the result. 29 | 30 | guard let reparsed = WebURL(utf8: url.utf8) else { 31 | fatalError( 32 | """ 33 | Failed to reparse URL: \(url) 34 | """ 35 | ) 36 | } 37 | 38 | // 3. Check that the reparsed URL is exactly the same as the original. 39 | 40 | guard reparsed.utf8.elementsEqual(url.utf8) else { 41 | fatalError( 42 | """ 43 | Reparsed URL has different code-units: 44 | - Original: \(url) 45 | - Reparsed: \(reparsed) 46 | """ 47 | ) 48 | } 49 | guard reparsed._spis._describesSameStructure(as: url) else { 50 | fatalError( 51 | """ 52 | URLStructures are not equivalent: 53 | - Original: \(url) 54 | - Reparsed: \(reparsed) 55 | """ 56 | ) 57 | } 58 | 59 | // All checks passed. 60 | return 0 61 | } 62 | -------------------------------------------------------------------------------- /Fuzzers/Sources/web-foundation-roundtrip/main.swift: -------------------------------------------------------------------------------- 1 | // Copyright The swift-url Contributors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Foundation 16 | import WebURL 17 | import WebURLFoundationExtras 18 | 19 | @_cdecl("LLVMFuzzerTestOneInput") 20 | public func web_foundation_roundtrip(_ start: UnsafePointer, _ count: Int) -> CInt { 21 | 22 | let bytes = UnsafeBufferPointer(start: start, count: count) 23 | 24 | // 1. Parse the bytes generated by the fuzzer, and encode the URL for Foundation conversion. 25 | 26 | guard let webURL = WebURL(utf8: bytes)?.encodedForFoundation else { 27 | return 0 // Fuzzer did not produce a URL. Try again. 28 | } 29 | 30 | // 2. Convert the encoded WebURL to a Foundation URL. 31 | 32 | guard let foundationURL = URL(webURL, addPercentEncoding: true) else { 33 | return 0 // Couldn't convert the URL. That's fine. 34 | } 35 | 36 | // 3. Check that the Foundation URL can be converted back to a WebURL. 37 | 38 | guard let roundtripURL = WebURL(foundationURL) else { 39 | fatalError( 40 | """ 41 | URL failed to round-trip: 42 | - WebURL: \(webURL) 43 | - Foundation.URL: \(foundationURL) 44 | """ 45 | ) 46 | } 47 | 48 | // 4. Check that the round-tripped WebURL is exactly the same as the original encoded WebURL. 49 | 50 | guard roundtripURL == webURL, roundtripURL._spis._describesSameStructure(as: webURL) else { 51 | fatalError( 52 | """ 53 | Round-tripped URL is not the same as the original: 54 | - WebURL: \(webURL) 55 | - Foundation.URL: \(foundationURL) 56 | - WebURL (2): \(roundtripURL) 57 | """ 58 | ) 59 | } 60 | 61 | // All checks passed. 62 | return 0 63 | } 64 | -------------------------------------------------------------------------------- /Fuzzers/Sources/web-to-foundation/main.swift: -------------------------------------------------------------------------------- 1 | // Copyright The swift-url Contributors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Foundation 16 | import WebURL 17 | import WebURLFoundationExtras 18 | 19 | @_cdecl("LLVMFuzzerTestOneInput") 20 | public func web_to_foundation(_ start: UnsafePointer, _ count: Int) -> CInt { 21 | 22 | let bytes = UnsafeBufferPointer(start: start, count: count) 23 | 24 | // 1. Parse the bytes generated by the fuzzer. 25 | 26 | guard let webURL = WebURL(utf8: bytes) else { 27 | return 0 // Fuzzer did not produce a URL. Try again. 28 | } 29 | 30 | // 2. Convert the WebURL to a Foundation URL. 31 | 32 | guard let foundationURL = URL(webURL, addPercentEncoding: true) else { 33 | return 0 // Couldn't convert the URL. That's fine. 34 | } 35 | 36 | // 3. Perform a full, strict equivalence check of every component against the encoded original WebURL. 37 | 38 | let encodedWebURL = webURL.encodedForFoundation 39 | var foundationURLString = foundationURL.absoluteString 40 | let areEquivalent = foundationURLString.withUTF8 { 41 | WebURL._SPIs._checkEquivalence_w2f(encodedWebURL, foundationURL, foundationString: $0, shortcuts: false) 42 | } 43 | guard areEquivalent else { 44 | fatalError( 45 | """ 46 | URLs are not equivalent: 47 | - Input string: \(String(decoding: bytes, as: UTF8.self)) 48 | - WebURL: \(webURL) 49 | - Encoded WebURL: \(encodedWebURL) 50 | - Foundation.URL: \(foundationURL) 51 | """ 52 | ) 53 | } 54 | 55 | // All checks passed. 56 | return 0 57 | } 58 | -------------------------------------------------------------------------------- /Fuzzers/corpus-to-test.swift: -------------------------------------------------------------------------------- 1 | #! /usr/bin/swift 2 | 3 | // Generates a Swift source file with the contents of a fuzzing corpus. 4 | // The source file can be used by tests, or executed as a script to recreate the corpus. 5 | 6 | import Foundation 7 | 8 | // 1. Parse command-line args. 9 | // Example usage: ./corpus-to-test.swift Corpora/foundation-to-web foundation_to_web 16 ../Tests/WebURLFoundationExtrasTests/FuzzCorpus_foundation_to_web.swift 10 | 11 | var cmdargs_it = CommandLine.arguments.makeIterator() 12 | guard let _ = cmdargs_it.next() else { 13 | fatalError("First entry should be the script's name") 14 | } 15 | guard let _corpusDir = cmdargs_it.next() else { 16 | fatalError("❌ First argument should be the corpus directory") 17 | } 18 | let corpusDir = URL(fileURLWithPath: _corpusDir) 19 | guard let corpusName = cmdargs_it.next() else { 20 | fatalError("❌ Second argument should be the corpus name. Must be a valid Swift identifier.") 21 | } 22 | guard let _maxLength = cmdargs_it.next(), let maxLength = Int(_maxLength), maxLength > 0 else { 23 | fatalError("❌ Third argument should be the maximum length of each entry in the corpus") 24 | } 25 | guard let outputFile = cmdargs_it.next() else { 26 | fatalError("❌ Fourth argument should be the output file name") 27 | } 28 | guard cmdargs_it.next() == nil else { 29 | fatalError("❌ Too many arguments") 30 | } 31 | 32 | // 2. Gather the files in the corpus directory and produce a sorted list. 33 | 34 | var corpusEntries = [URL]() 35 | do { 36 | let enumerator = FileManager().enumerator(at: corpusDir, includingPropertiesForKeys: nil)! 37 | while let next = enumerator.nextObject() as? URL { 38 | corpusEntries.append(next) 39 | } 40 | corpusEntries.sort(by: { 41 | // Restored corpus entries have numeric names, whilst new/reduced entries have UUID names. 42 | // Sort by count first, so restored entries remain at the top of the list. 43 | let lhs = $0.lastPathComponent 44 | let rhs = $1.lastPathComponent 45 | guard lhs.utf8.count == rhs.utf8.count else { 46 | return lhs.utf8.count < rhs.utf8.count 47 | } 48 | return lhs < rhs 49 | }) 50 | } 51 | 52 | // 3. Generate the output file. 53 | 54 | var output = 55 | """ 56 | // Copyright The swift-url Contributors. 57 | // 58 | // Licensed under the Apache License, Version 2.0 (the "License"); 59 | // you may not use this file except in compliance with the License. 60 | // You may obtain a copy of the License at 61 | // 62 | // http://www.apache.org/licenses/LICENSE-2.0 63 | // 64 | // Unless required by applicable law or agreed to in writing, software 65 | // distributed under the License is distributed on an "AS IS" BASIS, 66 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 67 | // See the License for the specific language governing permissions and 68 | // limitations under the License. 69 | 70 | 71 | // -------------------------------------------------------------- 72 | // DO NOT EDIT - THIS FILE WAS GENERATED BY CORPUS-TO-TEST.SWIFT 73 | // -------------------------------------------------------------- 74 | // Generated at: \(Date()) 75 | // Entries: \(corpusEntries.count) 76 | // Max length: \(maxLength) 77 | 78 | 79 | // swift-format-ignore 80 | internal let corpus_\(corpusName): [[UInt8]] = [\n 81 | """ 82 | 83 | for testDataURL in corpusEntries { 84 | let testData = try Data(contentsOf: testDataURL) 85 | // Generate an array literal containing the bytes. 86 | var arrayLiteralString = testData.reduce(into: "[") { str, byte in 87 | str += "0x" + (byte < 0x10 ? "0" : "") + String(byte, radix: 16, uppercase: true) + ", " 88 | } 89 | arrayLiteralString.removeLast(2) 90 | arrayLiteralString.append("]") 91 | // Include the string representation as a comment, but strip it of null bytes and newlines. 92 | var stringRepresentation = String(decoding: testData, as: UTF8.self) 93 | stringRepresentation.removeAll(where: { $0.asciiValue == 0 || $0.isNewline }) 94 | output += " \(arrayLiteralString), // \"\(stringRepresentation)\"\n" 95 | } 96 | 97 | output += 98 | #""" 99 | ] 100 | 101 | import Foundation 102 | 103 | // swift-format-ignore 104 | private func recreateCorpus_\#(corpusName)(_ outputDir: URL) throws { 105 | try? FileManager().removeItem(at: outputDir) 106 | try FileManager().createDirectory(at: outputDir, withIntermediateDirectories: true, attributes: nil) 107 | var entryNumber = 0 108 | for entry in corpus_\#(corpusName) { 109 | let destinationURL = outputDir.appendingPathComponent("\(entryNumber)", isDirectory: false) 110 | try Data(entry).write(to: destinationURL) 111 | entryNumber += 1 112 | } 113 | } 114 | 115 | // To recreate the corpus, uncomment this line and run the file using the interpreter (e.g. 'swift {this-file}') 116 | // try! recreateCorpus_\#(corpusName)(URL(fileURLWithPath: "recreated_corpus_\#(corpusName)")) 117 | 118 | """# 119 | 120 | let outputFileURL = URL(fileURLWithPath: outputFile) 121 | try! output.write(to: outputFileURL, atomically: false, encoding: .utf8) 122 | -------------------------------------------------------------------------------- /Fuzzers/fuzz-foundation-roundtrip.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright The swift-url Contributors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | if [ "$(uname)" == "Darwin" ]; then 18 | # Apple's SDK toolchains do not include fuzzer support, but swift.org toolchains do. 19 | xcrun -toolchain swift swift build -Xswiftc -sanitize=fuzzer,address -Xswiftc -g 20 | else 21 | swift build -Xswiftc -sanitize=fuzzer,address -Xswiftc -g 22 | fi 23 | 24 | ./.build/debug/web-foundation-roundtrip "$@" 25 | -------------------------------------------------------------------------------- /Fuzzers/fuzz-ftow.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright The swift-url Contributors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | if [ "$(uname)" == "Darwin" ]; then 18 | # Apple's SDK toolchains do not include fuzzer support, but swift.org toolchains do. 19 | xcrun -toolchain swift swift build -Xswiftc -sanitize=fuzzer,address -Xswiftc -g 20 | else 21 | swift build -Xswiftc -sanitize=fuzzer,address -Xswiftc -g 22 | fi 23 | 24 | ./.build/debug/foundation-to-web "$@" 25 | -------------------------------------------------------------------------------- /Fuzzers/fuzz-parser.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright The swift-url Contributors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | if [ "$(uname)" == "Darwin" ]; then 18 | # Apple's SDK toolchains do not include fuzzer support, but swift.org toolchains do. 19 | xcrun -toolchain swift swift build -Xswiftc -sanitize=fuzzer,address -Xswiftc -g 20 | else 21 | swift build -Xswiftc -sanitize=fuzzer,address -Xswiftc -g 22 | fi 23 | 24 | ./.build/debug/url-parse-reparse "$@" 25 | -------------------------------------------------------------------------------- /Fuzzers/fuzz-wtof.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright The swift-url Contributors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | if [ "$(uname)" == "Darwin" ]; then 18 | # Apple's SDK toolchains do not include fuzzer support, but swift.org toolchains do. 19 | xcrun -toolchain swift swift build -Xswiftc -sanitize=fuzzer,address -Xswiftc -g 20 | else 21 | swift build -Xswiftc -sanitize=fuzzer,address -Xswiftc -g 22 | fi 23 | 24 | ./.build/debug/web-to-foundation "$@" 25 | -------------------------------------------------------------------------------- /Fuzzers/url.dict: -------------------------------------------------------------------------------- 1 | # Copyright The swift-url Contributors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # -------------------------------------------- 15 | # A dictionary of tokens to aid fuzzers of URL parsers. 16 | # -------------------------------------------- 17 | 18 | # Schemes 19 | "http" 20 | "https" 21 | "ws" 22 | "wss" 23 | "ftp" 24 | "file" 25 | "foo" 26 | 27 | # Component delimiters 28 | "@" 29 | "%40" 30 | ":" 31 | "%3A" 32 | "?" 33 | "%3F" 34 | "#" 35 | "%23" 36 | "[" 37 | "%5B" 38 | "]" 39 | "%5D" 40 | 41 | # Slashes 42 | "/" 43 | "%2F" 44 | "\\" 45 | "%5C" 46 | "//" 47 | "%2F%2F" 48 | "\\\\" 49 | "%5C%5C" 50 | 51 | # Dots 52 | "." 53 | "%2E" 54 | ".." 55 | "%2E%2E" 56 | 57 | # Percent sign 58 | "%" 59 | "%25" 60 | 61 | # Windows drive letters 62 | "C:" 63 | "C|" 64 | "/C|/.." 65 | 66 | # Known Ports 67 | ":21" 68 | ":80" 69 | ":443" 70 | 71 | # Other parts 72 | ".com" 73 | "%72" 74 | "%80" 75 | "%00" 76 | "%FF" 77 | "%82" 78 | ";" 79 | "%3B" 80 | 81 | # Hex Numbers 82 | "0x00" 83 | "0x12" 84 | "0x80" 85 | "0xAB" 86 | "0xFF" 87 | 88 | # Tabs, Newlines, Spaces 89 | "\x09" 90 | "\x0A" 91 | "\x0D" 92 | "\x20" 93 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | swift-url (WebURL) 2 | Copyright Karl Wagner, and the swift-url Contributors. 3 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | 3 | // Copyright The swift-url Contributors. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | import PackageDescription 18 | 19 | let package = Package( 20 | name: "swift-url", 21 | products: [ 22 | 23 | // 🧩 Core functionality. 24 | // The WebURL type. You definitely want this. 25 | .library(name: "WebURL", targets: ["WebURL"]), 26 | 27 | // 🔗 Integration with swift-system. 28 | // Adds WebURL <-> FilePath conversions. 29 | .library(name: "WebURLSystemExtras", targets: ["WebURLSystemExtras"]), 30 | 31 | // 🔗 Integration with Foundation. 32 | // Adds WebURL <-> Foundation.URL conversions, and URLSession integration. 33 | .library(name: "WebURLFoundationExtras", targets: ["WebURLFoundationExtras"]), 34 | 35 | // 🧰 Support Libraries (internal use only). 36 | // ========================================= 37 | // These libraries expose some convenient hooks for testing, benchmarking, and other tools 38 | // - either in this repo or at . 39 | .library(name: "_WebURLIDNA", targets: ["IDNA"]), 40 | .library(name: "_WebURLTestSupport", targets: ["WebURLTestSupport"]), 41 | 42 | ], 43 | dependencies: [ 44 | 45 | // 🔗 Integrations. 46 | // ================ 47 | // WebURLSystemExtras supports swift-system 1.0+. 48 | .package(url: "https://github.com/apple/swift-system.git", .upToNextMajor(from: "1.0.0")), 49 | 50 | // 🧪 Test-Only Dependencies. 51 | // ========================== 52 | // Checkit - Exercises for stdlib protocol conformances. 53 | .package(name: "Checkit", url: "https://github.com/karwa/swift-checkit.git", from: "0.0.2"), 54 | 55 | ], 56 | targets: [ 57 | 58 | // 🗺 Unicode and IDNA. 59 | // ==================== 60 | 61 | .target( 62 | name: "UnicodeDataStructures", 63 | swiftSettings: [.define("WEBURL_UNICODE_PARSE_N_PRINT", .when(configuration: .debug))] 64 | ), 65 | .testTarget( 66 | name: "UnicodeDataStructuresTests", 67 | dependencies: ["UnicodeDataStructures", "Checkit"], 68 | resources: [.copy("GenerateData/TableDefinitions")] 69 | ), 70 | 71 | .target( 72 | name: "IDNA", 73 | dependencies: ["UnicodeDataStructures"] 74 | ), 75 | .testTarget( 76 | name: "IDNATests", 77 | dependencies: ["IDNA", "WebURLTestSupport"] 78 | ), 79 | 80 | // 🌐 WebURL. 81 | // ========== 82 | 83 | .target( 84 | name: "WebURL", 85 | dependencies: ["IDNA"], 86 | exclude: ["WebURL.docc"] 87 | ), 88 | .target( 89 | name: "WebURLTestSupport", 90 | dependencies: ["WebURL", "IDNA"], 91 | resources: [.copy("TestFilesData")] 92 | ), 93 | .testTarget( 94 | name: "WebURLTests", 95 | dependencies: ["WebURL", "WebURLTestSupport", "Checkit"] 96 | ), 97 | .testTarget( 98 | name: "WebURLDeprecatedAPITests", 99 | dependencies: ["WebURL"] 100 | ), 101 | 102 | // 🔗 WebURLSystemExtras. 103 | // ====================== 104 | 105 | .target( 106 | name: "WebURLSystemExtras", 107 | dependencies: ["WebURL", .product(name: "SystemPackage", package: "swift-system")] 108 | ), 109 | .testTarget( 110 | name: "WebURLSystemExtrasTests", 111 | dependencies: ["WebURLSystemExtras", "WebURL", .product(name: "SystemPackage", package: "swift-system")] 112 | ), 113 | 114 | // 🔗 WebURLFoundationExtras. 115 | // ========================== 116 | 117 | .target( 118 | name: "WebURLFoundationExtras", 119 | dependencies: ["WebURL"] 120 | ), 121 | .testTarget( 122 | name: "WebURLFoundationExtrasTests", 123 | dependencies: ["WebURLFoundationExtras", "WebURLTestSupport", "WebURL"], 124 | resources: [.copy("URLConversion/Resources")] 125 | ), 126 | .testTarget( 127 | name: "WebURLFoundationEndToEndTests", 128 | dependencies: ["WebURLFoundationExtras", "WebURL"] 129 | ), 130 | ] 131 | ) 132 | -------------------------------------------------------------------------------- /Package@swift-5.5.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.5 2 | 3 | // Copyright The swift-url Contributors. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | import PackageDescription 18 | 19 | let package = Package( 20 | name: "swift-url", 21 | products: [ 22 | 23 | // 🧩 Core functionality. 24 | // The WebURL type. You definitely want this. 25 | .library(name: "WebURL", targets: ["WebURL"]), 26 | 27 | // 🔗 Integration with swift-system. 28 | // Adds WebURL <-> FilePath conversions. 29 | .library(name: "WebURLSystemExtras", targets: ["WebURLSystemExtras"]), 30 | 31 | // 🔗 Integration with Foundation. 32 | // Adds WebURL <-> Foundation.URL conversions, and URLSession integration. 33 | .library(name: "WebURLFoundationExtras", targets: ["WebURLFoundationExtras"]), 34 | 35 | // 🧰 Support Libraries (internal use only). 36 | // ========================================= 37 | // These libraries expose some convenient hooks for testing, benchmarking, and other tools 38 | // - either in this repo or at . 39 | .library(name: "_WebURLIDNA", targets: ["IDNA"]), 40 | .library(name: "_WebURLTestSupport", targets: ["WebURLTestSupport"]), 41 | 42 | ], 43 | dependencies: [ 44 | 45 | // 🔗 Integrations. 46 | // ================ 47 | // WebURLSystemExtras supports swift-system 1.0+. 48 | .package(url: "https://github.com/apple/swift-system.git", .upToNextMajor(from: "1.0.0")), 49 | 50 | // 🧪 Test-Only Dependencies. 51 | // ========================== 52 | // Checkit - Exercises for stdlib protocol conformances. 53 | .package(name: "Checkit", url: "https://github.com/karwa/swift-checkit.git", from: "0.0.2"), 54 | 55 | ], 56 | targets: [ 57 | 58 | // 🗺 Unicode and IDNA. 59 | // ==================== 60 | 61 | .target( 62 | name: "UnicodeDataStructures", 63 | swiftSettings: [.define("WEBURL_UNICODE_PARSE_N_PRINT", .when(configuration: .debug))] 64 | ), 65 | .testTarget( 66 | name: "UnicodeDataStructuresTests", 67 | dependencies: ["UnicodeDataStructures", "Checkit"], 68 | resources: [.copy("GenerateData/TableDefinitions")] 69 | ), 70 | 71 | .target( 72 | name: "IDNA", 73 | dependencies: ["UnicodeDataStructures"] 74 | ), 75 | .testTarget( 76 | name: "IDNATests", 77 | dependencies: ["IDNA", "WebURLTestSupport"] 78 | ), 79 | 80 | // 🌐 WebURL. 81 | // ========== 82 | 83 | .target( 84 | name: "WebURL", 85 | dependencies: ["IDNA"] 86 | ), 87 | .target( 88 | name: "WebURLTestSupport", 89 | dependencies: ["WebURL", "IDNA"], 90 | resources: [.copy("TestFilesData")] 91 | ), 92 | .testTarget( 93 | name: "WebURLTests", 94 | dependencies: ["WebURL", "WebURLTestSupport", "Checkit"] 95 | ), 96 | .testTarget( 97 | name: "WebURLDeprecatedAPITests", 98 | dependencies: ["WebURL"] 99 | ), 100 | 101 | // 🔗 WebURLSystemExtras. 102 | // ====================== 103 | 104 | .target( 105 | name: "WebURLSystemExtras", 106 | dependencies: ["WebURL", .product(name: "SystemPackage", package: "swift-system")] 107 | ), 108 | .testTarget( 109 | name: "WebURLSystemExtrasTests", 110 | dependencies: ["WebURLSystemExtras", "WebURL", .product(name: "SystemPackage", package: "swift-system")] 111 | ), 112 | 113 | // 🔗 WebURLFoundationExtras. 114 | // ========================== 115 | 116 | .target( 117 | name: "WebURLFoundationExtras", 118 | dependencies: ["WebURL"] 119 | ), 120 | .testTarget( 121 | name: "WebURLFoundationExtrasTests", 122 | dependencies: ["WebURLFoundationExtras", "WebURLTestSupport", "WebURL"], 123 | resources: [.copy("URLConversion/Resources")] 124 | ), 125 | .testTarget( 126 | name: "WebURLFoundationEndToEndTests", 127 | dependencies: ["WebURLFoundationExtras", "WebURL"] 128 | ), 129 | ] 130 | ) 131 | -------------------------------------------------------------------------------- /Sources/IDNA/NFC.swift: -------------------------------------------------------------------------------- 1 | // Copyright The swift-url Contributors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #if WEBURL_IDNA_USE_STDLIB_NFC 16 | 17 | @_spi(_Unicode) import Swift 18 | 19 | @usableFromInline 20 | internal typealias NFCIterator = AnyIterator 21 | 22 | @inlinable 23 | internal func isNFC(_ scalars: C) -> Bool where C.Element == Unicode.Scalar { 24 | toNFC(String(scalars)).elementsEqual(scalars) 25 | } 26 | 27 | @usableFromInline 28 | internal func toNFC(_ string: String) -> NFCIterator { 29 | if #available(macOS 9999, *) { 30 | return AnyIterator(string._nfc.makeIterator()) 31 | } else { 32 | fatalError() 33 | } 34 | } 35 | 36 | #else 37 | 38 | import Foundation 39 | 40 | @usableFromInline 41 | internal typealias NFCIterator = String.UnicodeScalarView.Iterator 42 | 43 | @inlinable 44 | internal func isNFC(_ scalars: C) -> Bool where C.Element == Unicode.Scalar { 45 | IteratorSequence(toNFC(String(scalars))).elementsEqual(scalars) 46 | } 47 | 48 | @inlinable 49 | internal func toNFC(_ string: String) -> NFCIterator { 50 | string.precomposedStringWithCanonicalMapping.unicodeScalars.makeIterator() 51 | } 52 | 53 | #endif 54 | 55 | // Why is this not in the standard library? 56 | extension String { 57 | 58 | @inlinable 59 | internal init(_ scalars: C) where C: Collection, C.Element == Unicode.Scalar { 60 | // Unicode.Scalar is a UInt32: 61 | // https://github.com/apple/swift/blob/0c67ce64874d83b2d4f8d73b899ee58f2a75527f/stdlib/public/core/UnicodeScalar.swift#L38 62 | // Again, would be cool if the standard library would offer this interface, 63 | // so we wouldn't need to do... what I'm about to do. 64 | let _contiguousResult = scalars.withContiguousStorageIfAvailable { ptr in 65 | ptr.withMemoryRebound(to: UInt32.self) { utf32 in 66 | String(decoding: utf32, as: UTF32.self) 67 | } 68 | } 69 | if let contiguousResult = _contiguousResult { 70 | self = contiguousResult 71 | return 72 | } 73 | self = "" 74 | self.unicodeScalars.replaceSubrange(Range(uncheckedBounds: (endIndex, endIndex)), with: scalars) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Sources/UnicodeDataStructures/ParseNPrint/ParsingHelpers.swift: -------------------------------------------------------------------------------- 1 | // Copyright The swift-url Contributors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #if WEBURL_UNICODE_PARSE_N_PRINT 16 | 17 | /// Helper utilities for parsing Unicode data files. 18 | /// 19 | internal enum ParsingHelpers {} 20 | 21 | extension ParsingHelpers { 22 | 23 | /// A mapping of code-points to a single property value. 24 | /// 25 | /// This value can be parsed from a line of various Unicode data tables. 26 | /// 27 | internal struct CodePointsToProperty where Property: RawRepresentable, Property.RawValue == String { 28 | 29 | internal var codePoints: ClosedRange 30 | internal var value: Property 31 | 32 | /// Parses a value from a line of a Unicode data table. 33 | /// 34 | /// The range of code-points should be specified as a hexadecimal integer or range of integers, 35 | /// separated from the property value by a semicolon. Spaces will be trimmed from both values. 36 | /// 37 | /// If the line contains a comment (as many Unicode data tables do), it must be stripped before parsing. 38 | /// For example, valid inputs for parsing the value `Bidi_Class`, which has values `"L"` and `"AN"`, would be: 39 | /// 40 | /// ``` 41 | /// 1F20..1F45 ; L 42 | /// 1F48..1F4D ; L 43 | /// 1F50..1F57 ; L 44 | /// 1F59 ; L 45 | /// 1F5B ; L 46 | /// 1F5D ; L 47 | /// 0660..0669 ; AN 48 | /// 066B..066C ; AN 49 | /// 06DD ; AN 50 | /// 0890..0891 ; AN 51 | /// ``` 52 | /// 53 | internal init?(parsing tableData: Substring) { 54 | let components = tableData.split(separator: ";").map { $0.trimmingSpaces } 55 | guard components.count == 2 else { return nil } 56 | codePoints: do { 57 | let stringValue = components[0] 58 | guard let codePoints = parseCodePointRange(stringValue) else { return nil } 59 | self.codePoints = codePoints 60 | } 61 | value: do { 62 | let stringValue = components[1] 63 | guard let value = Property(rawValue: String(stringValue)) else { return nil } 64 | self.value = value 65 | } 66 | // Done. 67 | } 68 | } 69 | } 70 | 71 | extension ParsingHelpers { 72 | 73 | /// Parses a codepoint range, as typically expressed in Unicode data tables. 74 | /// 75 | /// Accepted formats are: 76 | /// 77 | /// - Single 32-bit integer in hex notation (case insensitive) 78 | /// - Two 32-bit integers in hex notation (case insensitive), separated by two dots (".."). 79 | /// 80 | /// For example: `"004AE"`, or `"0061..0071"`. 81 | /// 82 | /// The codepoints are validated as being \<= `0x10\_FFFF`, but may not be valid scalar values. 83 | /// 84 | internal static func parseCodePointRange(_ stringValue: Substring) -> ClosedRange? { 85 | 86 | if let singleValue = UInt32(stringValue, radix: 16) { 87 | return singleValue <= 0x10_FFFF ? singleValue...singleValue : nil 88 | } else { 89 | guard 90 | stringValue.contains("."), 91 | let rangeStart = UInt32(stringValue.prefix(while: { $0 != "." }), radix: 16), rangeStart <= 0x10_FFFF, 92 | let rangeEnd = UInt32(stringValue.suffix(while: { $0 != "." }), radix: 16), rangeEnd <= 0x10_FFFF 93 | else { 94 | return nil 95 | } 96 | return rangeStart...rangeEnd 97 | } 98 | } 99 | } 100 | 101 | #endif // WEBURL_UNICODE_PARSE_N_PRINT 102 | -------------------------------------------------------------------------------- /Sources/UnicodeDataStructures/ParseNPrint/Printing.swift: -------------------------------------------------------------------------------- 1 | // Copyright The swift-url Contributors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #if WEBURL_UNICODE_PARSE_N_PRINT 16 | 17 | // -------------------------------------------- 18 | // MARK: - Formatting Integers 19 | // -------------------------------------------- 20 | 21 | 22 | @usableFromInline 23 | internal enum HexStringFormat { 24 | /// Format without any leading zeroes, e.g. `0x2`. 25 | case minimal 26 | /// Format which includes all leading zeroes in a fixed-width integer, e.g. `0x00CE`. 27 | case fullWidth 28 | /// Format which pads values with enough zeroes to reach a minimum length. 29 | case padded(toLength: Int) 30 | } 31 | 32 | extension FixedWidthInteger { 33 | 34 | @usableFromInline 35 | internal func hexString(format: HexStringFormat = .fullWidth) -> String { 36 | "0x" + unprefixedHexString(format: format) 37 | } 38 | 39 | @usableFromInline 40 | internal func unprefixedHexString(format: HexStringFormat) -> String { 41 | let minimal = String(self, radix: 16, uppercase: true) 42 | switch format { 43 | case .minimal: 44 | return minimal 45 | case .fullWidth: 46 | return minimal.leftPadding(toLength: Self.bitWidth / 4, withPad: "0") 47 | case .padded(toLength: let minLength): 48 | if minimal.count < minLength { 49 | return minimal.leftPadding(toLength: minLength, withPad: "0") 50 | } else { 51 | return minimal 52 | } 53 | } 54 | } 55 | } 56 | 57 | 58 | // -------------------------------------------- 59 | // MARK: - Array Literals 60 | // -------------------------------------------- 61 | 62 | 63 | internal func printArrayLiteral( 64 | name: String, elementType: String, data: Data, 65 | columns: Int = 8, formatter: (Data.Element) -> String = { String(describing: $0) }, 66 | includeSizeComment: Bool = true, 67 | to output: inout OutputStream 68 | ) where Data: Sequence, OutputStream: TextOutputStream { 69 | 70 | output.write( 71 | #""" 72 | // swift-format-ignore 73 | @usableFromInline 74 | internal let \#(name): [\#(elementType)] = [ 75 | """# 76 | ) 77 | output.write("\n") 78 | 79 | var totalCount = 0 80 | 81 | var it = data.makeIterator() 82 | var row = [Data.Element]() 83 | row.reserveCapacity(columns) 84 | 85 | func populateNextRow() { 86 | row.removeAll(keepingCapacity: true) 87 | while row.count < columns, let nextColumn = it.next() { row.append(nextColumn) } 88 | } 89 | populateNextRow() 90 | 91 | let rowLeading = " " 92 | let rowTrailing = ",\n" 93 | while !row.isEmpty { 94 | output.write(rowLeading) 95 | for i in 0.. Bool) -> SubSequence { 29 | var i = endIndex 30 | while i > startIndex { 31 | let beforeI = index(before: i) 32 | guard predicate(self[beforeI]) else { return self[i.. String { 50 | precondition(count <= newLength, "newLength must be greater than or equal to string length") 51 | return String(repeating: character, count: newLength - count) + self 52 | } 53 | } 54 | 55 | 56 | // -------------------------------------------- 57 | // MARK: - Converting SegmentedLine <-> CodePointDatabase 58 | // -------------------------------------------- 59 | 60 | 61 | extension SegmentedLine where Bound == UInt32 { 62 | 63 | func generateTable( 64 | _: Schema.Type, 65 | mapAsciiValue: (Value) -> Schema.ASCIIData, 66 | mapUnicodeValue: (Value) -> Schema.UnicodeData 67 | ) -> CodePointDatabase where Schema: CodePointDatabase_Schema { 68 | precondition(self.bounds.lowerBound == 0) 69 | precondition(self.bounds.upperBound == 0x11_0000) 70 | 71 | var builder = CodePointDatabase.Builder() 72 | for (range, value) in segments { 73 | for asciiCodePoint in range.clamped(to: 0..<0x80) { 74 | builder.appendAscii(mapAsciiValue(value), for: asciiCodePoint) 75 | } 76 | guard range.upperBound > 0x80 else { continue } 77 | builder.appendUnicode(mapUnicodeValue(value), for: ClosedRange(range.clamped(to: 0x80..<0x110000))) 78 | } 79 | return builder.finalize() 80 | } 81 | } 82 | 83 | // CodePointDatabase -> SegmentedLine (unused right now) 84 | // 85 | //extension CodePointDatabase { 86 | // 87 | // static func toRangeTableData( 88 | // offset: UInt32, _ input: SplitTable 89 | // ) -> [(UInt32, Schema.UnicodeData)] { 90 | // var result = [(UInt32, Schema.UnicodeData)]() 91 | // result.reserveCapacity(input.codePointTable.count) 92 | // for i in 0..) -> Void) { 101 | // 102 | // // FIXME: ASCII? 103 | // 104 | // for (n, bmpSubArray) in _bmpData.enumerated() { 105 | // let start = UInt32(bmpSubArray.codePointTable.first!) 106 | // let end = start + 0x00_1000 107 | // let table = RangeTable(_upperBound: end, _data: Self.toRangeTableData(offset: 0, bmpSubArray)) 108 | // body(table) 109 | // } 110 | // for (n, nonbmpArray) in _nonbmpData.enumerated() { 111 | // let start = UInt32((n + 1) * 0x1_0000) 112 | // let end = start + 0x01_0000 113 | // let table = RangeTable(_upperBound: end, _data: Self.toRangeTableData(offset: start, nonbmpArray)) 114 | // body(table) 115 | // } 116 | // } 117 | //} 118 | 119 | #endif // WEBURL_UNICODE_PARSE_N_PRINT 120 | -------------------------------------------------------------------------------- /Sources/UnicodeDataStructures/Shared/IDNA/IDNAValidationDataSchema.swift: -------------------------------------------------------------------------------- 1 | // Copyright The swift-url Contributors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /// Data used to validate domain labels in IDNA. 16 | /// 17 | /// Validity criteria are described by [UTS#46](https://unicode.org/reports/tr46/#Validity_Criteria). 18 | /// 19 | public struct IDNAValidationData: CodePointDatabase_Schema { 20 | public typealias ASCIIData = ValidationFlags 21 | public typealias UnicodeData = ValidationFlags 22 | } 23 | 24 | extension IDNAValidationData { 25 | 26 | public struct ValidationFlags: HasRawStorage, Equatable { 27 | 28 | public typealias RawStorage = UInt8 29 | 30 | public var storage: UInt8 31 | 32 | @inlinable 33 | public init(storage: UInt8) { 34 | self.storage = storage 35 | } 36 | 37 | // Reading and writing properties. 38 | 39 | @inlinable 40 | public var bidiInfo: BidiInfo { 41 | // Bidi_Class gets the low 3 bits. 42 | get { BidiInfo(value: storage & 0b0000_0111) } 43 | set { storage = (storage & ~0b0000_0111) | newValue.value } 44 | } 45 | 46 | @inlinable 47 | public var joiningType: JoiningType { 48 | // Joining_Type gets the next 3 bits. 49 | get { JoiningType(value: (storage & 0b0011_1000) &>> 3) } 50 | set { storage = (storage & ~0b0011_1000) | (newValue.value &<< 3) } 51 | } 52 | 53 | @inlinable 54 | public var isVirama: Bool { 55 | // The next bit marks the 61 code-points with CCC=virama. 56 | // Having this here means we can avoid a lookup in the standard library's data tables. 57 | get { (storage & 0b0100_0000) != 0 } 58 | set { storage = newValue ? (storage | 0b0100_0000) : (storage & ~0b0100_0000) } 59 | } 60 | 61 | @inlinable 62 | public var isMark: Bool { 63 | // The final bit is to mark the ~2000 code-points with General_Category=Mark. 64 | // Having this here means we can avoid a lookup in the standard library's data tables. 65 | get { (storage & 0b1000_0000) != 0 } 66 | set { storage = newValue ? (storage | 0b1000_0000) : (storage & ~0b1000_0000) } 67 | } 68 | 69 | // Creation. 70 | 71 | @inlinable 72 | internal init(bidiInfo: BidiInfo, joiningType: JoiningType, isVirama: Bool, isMark: Bool) { 73 | self.storage = 0 74 | self.bidiInfo = bidiInfo 75 | self.joiningType = joiningType 76 | self.isVirama = isVirama 77 | self.isMark = isMark 78 | } 79 | } 80 | } 81 | 82 | extension IDNAValidationData.ValidationFlags { 83 | 84 | // swift-format-ignore 85 | public struct BidiInfo: Equatable { 86 | 87 | @inlinable public static var L: Self { Self(value: 0) } 88 | @inlinable public static var RorAL: Self { Self(value: 1) } 89 | @inlinable public static var AN: Self { Self(value: 2) } 90 | @inlinable public static var EN: Self { Self(value: 3) } 91 | @inlinable public static var ESorCSorETorONorBN: Self { Self(value: 4) } 92 | @inlinable public static var NSM: Self { Self(value: 5) } 93 | @inlinable public static var disallowed: Self { Self(value: 6) } 94 | 95 | @usableFromInline 96 | internal var value: UInt8 97 | 98 | @inlinable 99 | init(value: UInt8) { 100 | self.value = value 101 | } 102 | 103 | @inlinable 104 | public static func == (lhs: Self, rhs: Self) -> Bool { 105 | lhs.value == rhs.value 106 | } 107 | } 108 | 109 | // swift-format-ignore 110 | public struct JoiningType: Equatable { 111 | 112 | @inlinable public static var other: Self { Self(value: 0) } 113 | @inlinable public static var T: Self { Self(value: 1) } 114 | @inlinable public static var D: Self { Self(value: 2) } 115 | @inlinable public static var L: Self { Self(value: 3) } 116 | @inlinable public static var R: Self { Self(value: 4) } 117 | 118 | @usableFromInline 119 | internal var value: UInt8 120 | 121 | @inlinable 122 | init(value: UInt8) { 123 | self.value = value 124 | } 125 | 126 | @inlinable 127 | public static func == (lhs: Self, rhs: Self) -> Bool { 128 | lhs.value == rhs.value 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /Sources/WebURL/DeprecatedAPIs.swift: -------------------------------------------------------------------------------- 1 | // Copyright The swift-url Contributors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // This file is a place for deprecated APIs to live until they are eventually removed. 16 | // Even if blank, it is useful to keep this file for history purposes, 17 | // so it doesn't keep being created and deleted all the time. 18 | 19 | // Currently, there are no deprecated APIs. 20 | -------------------------------------------------------------------------------- /Sources/WebURL/Parser/WebURL+Component.swift: -------------------------------------------------------------------------------- 1 | // Copyright The swift-url Contributors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | extension WebURL { 16 | 17 | // swift-format-ignore 18 | /// A value representing a component in a URL. 19 | /// 20 | /// Each component has a unique bit set on its `rawValue`, making it suitable for use in bit-sets. 21 | /// 22 | /// - seealso: `WebURL.ComponentSet`. 23 | /// 24 | @usableFromInline 25 | internal struct Component: Equatable { 26 | 27 | @usableFromInline 28 | internal let rawValue: UInt8 29 | 30 | @inlinable 31 | internal init(_unchecked rawValue: UInt8) { 32 | self.rawValue = rawValue 33 | } 34 | 35 | @inlinable internal static var scheme: Self { Self(_unchecked: 1 << 0) } 36 | @inlinable internal static var username: Self { Self(_unchecked: 1 << 1) } 37 | @inlinable internal static var password: Self { Self(_unchecked: 1 << 2) } 38 | @inlinable internal static var hostname: Self { Self(_unchecked: 1 << 3) } 39 | @inlinable internal static var port: Self { Self(_unchecked: 1 << 4) } 40 | @inlinable internal static var path: Self { Self(_unchecked: 1 << 5) } 41 | @inlinable internal static var query: Self { Self(_unchecked: 1 << 6) } 42 | @inlinable internal static var fragment: Self { Self(_unchecked: 1 << 7) } 43 | } 44 | } 45 | 46 | extension WebURL { 47 | 48 | /// An efficient set of `WebURL.Component` values. 49 | /// 50 | @usableFromInline 51 | internal struct ComponentSet: Equatable, ExpressibleByArrayLiteral { 52 | 53 | @usableFromInline 54 | internal var _rawValue: UInt8 55 | 56 | @inlinable 57 | internal init(arrayLiteral elements: Component...) { 58 | self._rawValue = elements.reduce(into: 0) { $0 |= $1.rawValue } 59 | } 60 | 61 | @inlinable 62 | internal subscript(component: Component) -> Bool { 63 | get { contains(component) } 64 | set { newValue ? insert(component) : remove(component) } 65 | } 66 | 67 | /// Inserts a component in to the set. 68 | /// 69 | @inlinable 70 | internal mutating func insert(_ newMember: Component) { 71 | _rawValue |= newMember.rawValue 72 | } 73 | 74 | /// Removes a component from the set. 75 | /// 76 | @inlinable 77 | internal mutating func remove(_ newMember: Component) { 78 | _rawValue &= ~newMember.rawValue 79 | } 80 | 81 | /// Whether or not the given component is a member of this set. 82 | /// 83 | @inlinable 84 | internal func contains(_ member: Component) -> Bool { 85 | return (_rawValue & member.rawValue) != 0 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Sources/WebURL/Util/BidirectionalCollection+suffix.swift: -------------------------------------------------------------------------------- 1 | // Copyright The swift-url Contributors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | extension BidirectionalCollection { 16 | 17 | /// Returns the slice of this collection's trailing elements which match the given predicate. 18 | /// 19 | /// If no elements match the predicate, the returned slice is empty, from `endIndex.. Bool) -> SubSequence { 23 | var i = endIndex 24 | while i > startIndex { 25 | let beforeI = index(before: i) 26 | guard predicate(self[beforeI]) else { return self[i.. Bool) rethrows -> SubSequence { 27 | var sliceStart = startIndex 28 | var sliceEnd = endIndex 29 | // Consume elements from the front. 30 | while sliceStart < sliceEnd, try predicate(self[sliceStart]) { 31 | formIndex(after: &sliceStart) 32 | } 33 | // Consume elements from the back only if the element at the "before" index matches the predicate. 34 | while sliceStart < sliceEnd { 35 | let idxBeforeSliceEnd = index(before: sliceEnd) 36 | guard try predicate(self[idxBeforeSliceEnd]) else { 37 | return self[sliceStart.. Important: 42 | /// > This function is **not** bounds-checked (since 8-byte chunks are loaded directly from the `baseAddress`, 43 | /// > rather than via the Collection interface), although of course it only reads data within the buffer's bounds. 44 | /// > The reason it lives on `UnsafeBoundsCheckedBufferPointer` is because unsigned indexes allow for 45 | /// > better performance and code-size. 46 | /// 47 | @inlinable @inline(__always) // mask must be constant-folded. 48 | internal func uncheckedFastContains(_ element: UInt8) -> Bool { 49 | let mask = UInt64(repeatingByte: element) 50 | return _uncheckedFastContains(element: element, mask: mask) 51 | } 52 | 53 | @inlinable 54 | internal func _uncheckedFastContains(element: UInt8, mask: UInt64) -> Bool { 55 | var i = startIndex 56 | while distance(from: i, to: endIndex) >= 8 { 57 | // Load 8 bytes from the source. 58 | var eightBytes = UnsafeRawPointer( 59 | self.baseAddress.unsafelyUnwrapped.advanced(by: Int(bitPattern: i)) 60 | ).loadUnaligned(as: UInt64.self) 61 | // XOR every byte with the element we're searching for. 62 | // If there are any matches, we'll get a zero byte in that position. 63 | eightBytes ^= mask 64 | // Use bit-twiddling to detect if any bytes were zero. 65 | // https://graphics.stanford.edu/~seander/bithacks.html#ValueInWord 66 | let found = (eightBytes &- 0x0101_0101_0101_0101) & (~eightBytes & 0x8080_8080_8080_8080) 67 | if found != 0 { return true } 68 | i &+= 8 69 | } 70 | while i < endIndex { 71 | if self[i] == element { return true } 72 | i &+= 1 73 | } 74 | return false 75 | } 76 | } 77 | 78 | extension UnsafeBoundsCheckedBufferPointer where Element == UInt8 { 79 | 80 | /// Whether or not the buffer contains an ASCII horizontal tab (0x09), line feed (0x0A), 81 | /// or carriage return (0x0D) code-unit. 82 | /// 83 | /// This implementation is able to search chunks of 8 bytes at a time, using only 5 instructions per chunk. 84 | /// 85 | /// > Important: 86 | /// > This function is **not** bounds-checked (since 8-byte chunks are loaded directly from the `baseAddress`, 87 | /// > rather than via the Collection interface), although of course it only reads data within the buffer's bounds. 88 | /// > The reason it lives on `UnsafeBoundsCheckedBufferPointer` is because unsigned indexes allow for 89 | /// > better performance and code-size. 90 | /// 91 | @inlinable 92 | internal func uncheckedFastContainsTabOrCROrLF() -> Bool { 93 | var i = startIndex 94 | while distance(from: i, to: endIndex) >= 8 { 95 | // Load 8 bytes from the source. 96 | let eightBytes = UnsafeRawPointer( 97 | self.baseAddress.unsafelyUnwrapped.advanced(by: Int(bitPattern: i)) 98 | ).loadUnaligned(as: UInt64.self) 99 | 100 | // Check for line feeds first; we're more likely to find one than a tab or carriage return. 101 | var bytesForLF = eightBytes 102 | bytesForLF ^= UInt64(repeatingByte: ASCII.lineFeed.codePoint) 103 | var found = (bytesForLF &- 0x0101_0101_0101_0101) & (~bytesForLF & 0x8080_8080_8080_8080) 104 | if found != 0 { return true } 105 | 106 | // Check for tabs (0x09, 0b0000_1001) and carriage returns (0x0D, 0b0000_1101). 107 | // These differ by one bit, so mask it out (turns carriage returns in to tabs), then look for tabs. 108 | var bytesForTCR = eightBytes 109 | bytesForTCR &= UInt64(repeatingByte: 0b1111_1011) 110 | bytesForTCR ^= UInt64(repeatingByte: 0b0000_1001) 111 | found = (bytesForTCR &- 0x0101_0101_0101_0101) & (~bytesForTCR & 0x8080_8080_8080_8080) 112 | if found != 0 { return true } 113 | 114 | i &+= 8 115 | } 116 | while i < endIndex { 117 | let byte = self[i] 118 | if byte == ASCII.lineFeed.codePoint || (byte & 0b1111_1011) == 0b0000_1001 { return true } 119 | i &+= 1 120 | } 121 | return false 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /Sources/WebURL/Util/Collection+longestRange.swift: -------------------------------------------------------------------------------- 1 | // Copyright The swift-url Contributors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | extension Collection { 16 | 17 | /// Returns the longest subrange of elements satisfying the given predicate. 18 | /// 19 | /// In the case of a tie, the range closest to the start of the Collection is returned. 20 | /// If no elements match the predicate, the returned range is empty and the returned length is 0. 21 | /// 22 | /// - parameters: 23 | /// - predicate: The condition which elements should match. 24 | /// - returns: A tuple containing the longest subrange matching the predicate, 25 | /// as well as how many elements are contained within that range. 26 | /// 27 | @inlinable 28 | internal func longestSubrange(satisfying predicate: (Element) throws -> Bool) rethrows 29 | -> (subrange: Range, length: Int) 30 | { 31 | var idx = startIndex 32 | var longest: (Range, length: Int) = (idx.. longest.length { longest = (current.start.. longest.length { 46 | longest = (current.start.. (subrange: Range, length: Int) { 71 | return longestSubrange { $0 == value } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Sources/WebURL/Util/Either.swift: -------------------------------------------------------------------------------- 1 | // Copyright The swift-url Contributors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /// A container holding a value which may be of 2 possible types. 16 | /// 17 | @usableFromInline 18 | internal enum Either { 19 | case left(Left) 20 | case right(Right) 21 | } 22 | 23 | extension Either { 24 | 25 | @inlinable 26 | internal func map( 27 | left transformLeft: (Left) -> NewLeft, 28 | right transformRight: (Right) -> NewRight 29 | ) -> Either { 30 | switch self { 31 | case .left(let value): return .left(transformLeft(value)) 32 | case .right(let value): return .right(transformRight(value)) 33 | } 34 | } 35 | } 36 | 37 | extension Either where Left == Right { 38 | 39 | /// Returns the value held by this container. 40 | /// 41 | @inlinable 42 | internal func get() -> Left { 43 | switch self { 44 | case .left(let value): return value 45 | case .right(let value): return value 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/WebURL/Util/Errors.swift: -------------------------------------------------------------------------------- 1 | // Copyright The swift-url Contributors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | extension Result where Success == Void { 16 | 17 | @inlinable 18 | internal static var success: Self { 19 | .success(()) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/WebURL/Util/FastCollectionAlgorithms.swift: -------------------------------------------------------------------------------- 1 | // Copyright The swift-url Contributors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | extension Collection { 16 | 17 | /// Returns the index of the first element to match the given predicate, 18 | /// or `nil` if no elements match. 19 | /// 20 | @inlinable 21 | internal func fastFirstIndex(where predicate: (Element) -> Bool) -> Index? { 22 | fastFirstIndex(from: startIndex, to: endIndex, where: predicate) 23 | } 24 | 25 | /// Returns the index of the first element to match the given predicate within the given indexes, 26 | /// or `nil` if no elements in the range match. 27 | /// 28 | /// This function is equivalent to `self[start.. Bool) -> Index? { 33 | // This function should be re-evaluated once the following compiler/language enhancements ship: 34 | // - OSSA modules: https://forums.swift.org/t/arc-overhead-when-wrapping-a-class-in-a-struct-which-is-only-borrowed/62037/8 35 | // - borrow 'self' for non-mutating functions: https://forums.swift.org/t/pitch-dont-copy-self-when-calling-non-mutating-methods/61707 36 | var idx = start 37 | while idx < end { 38 | if predicate(self[idx]) { return idx } 39 | formIndex(after: &idx) 40 | } 41 | return nil 42 | } 43 | 44 | /// Returns a subsequence of elements which match the given predicate, starting at `startIndex`. 45 | /// 46 | @inlinable 47 | internal func fastPrefix(where predicate: (Element) -> Bool) -> SubSequence { 48 | if let endOfPrefix = fastFirstIndex(where: { !predicate($0) }) { 49 | return self[Range(uncheckedBounds: (startIndex, endOfPrefix))] 50 | } 51 | return self[Range(uncheckedBounds: (startIndex, endIndex))] 52 | } 53 | 54 | /// Returns a subsequence of elements formed by discarding elements at the front which match the given predicate. 55 | /// 56 | @inlinable 57 | internal func fastDrop(while predicate: (Element) -> Bool) -> SubSequence { 58 | if let firstToKeep = fastFirstIndex(where: { !predicate($0) }) { 59 | return self[Range(uncheckedBounds: (firstToKeep, endIndex))] 60 | } 61 | return self[Range(uncheckedBounds: (endIndex, endIndex))] 62 | } 63 | 64 | /// Whether all elements of the collection satisfy the given predicate. 65 | /// If the collection is empty, returns `true`. 66 | /// 67 | @inlinable 68 | internal func fastAllSatisfy(_ predicate: (Element) -> Bool) -> Bool { 69 | fastFirstIndex(where: { !predicate($0) }) == nil 70 | } 71 | } 72 | 73 | extension Collection where Element: Equatable { 74 | 75 | @inlinable @inline(__always) // The closure should be inlined. 76 | internal func fastFirstIndex(of element: Element) -> Index? { 77 | fastFirstIndex(where: { $0 == element }) 78 | } 79 | } 80 | 81 | extension Collection where SubSequence == Self { 82 | 83 | /// Removes and returns the first element of this collection, if it is not empty. 84 | /// 85 | @inlinable 86 | internal mutating func fastPopFirst() -> Element? { 87 | guard startIndex < endIndex else { return nil } 88 | let element = self[startIndex] 89 | self = self[Range(uncheckedBounds: (index(after: startIndex), endIndex))] 90 | return element 91 | } 92 | } 93 | 94 | extension BidirectionalCollection { 95 | 96 | /// Returns the index closest to `endIndex` whose element matches the given predicate. 97 | /// 98 | /// The difference between this implementation and the one in the standard library is 99 | /// that this uses `>` and `<` rather than `==` and `!=` to compare indexes, 100 | /// which allows bounds-checking to be more thoroughly eliminated. 101 | /// 102 | @inlinable 103 | internal func fastLastIndex(where predicate: (Element) -> Bool) -> Index? { 104 | var i = endIndex 105 | while i > startIndex { 106 | formIndex(before: &i) 107 | // Since endIndex > startIndex, and 'i' starts from endIndex and decrements if > startIndex, 108 | // it will never underflow. Thus 'i < endIndex' must always be true. 109 | // The compiler isn't smart enough to prove that, so it won't eliminate the bounds-check. 110 | // It's better to add the (certainly predictable) branch rather than bork our codegen with a trap. 111 | if i < endIndex, predicate(self[i]) { return i } 112 | } 113 | return nil 114 | } 115 | } 116 | 117 | extension BidirectionalCollection where Element: Equatable { 118 | 119 | /// Returns the index closest to `endIndex` whose element is equal to the given element. 120 | /// 121 | /// The difference between this implementation and the one in the standard library is 122 | /// that this uses `>` and `<` rather than `==` and `!=` to compare indexes, 123 | /// which allows bounds-checking to be more thoroughly eliminated. 124 | /// 125 | @inlinable @inline(__always) // The closure should be inlined. 126 | internal func fastLastIndex(of element: Element) -> Index? { 127 | fastLastIndex(where: { $0 == element }) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /Sources/WebURL/Util/Integers.swift: -------------------------------------------------------------------------------- 1 | // Copyright The swift-url Contributors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | extension FixedWidthInteger { 16 | 17 | /// Returns the result of `(self - x) + y`, or `nil` if the calculation overflows. 18 | /// 19 | @inlinable 20 | internal func subtracting(_ x: Self, adding y: Self) -> Self? { 21 | var (result, overflow) = self.subtractingReportingOverflow(x) 22 | guard !overflow else { return nil } 23 | (result, overflow) = result.addingReportingOverflow(y) 24 | guard !overflow else { return nil } 25 | return result 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/WebURL/Util/StaticMember.swift: -------------------------------------------------------------------------------- 1 | // Copyright The swift-url Contributors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /// A utility which allows emulating static members on protocol types. 16 | /// 17 | /// In Swift 5.5+, this functionality has been superseded by [SE-0299 - Extending Static Member Lookup in Generic Contexts][SE-0299]. 18 | /// This struct survives as an implementation detail, allowing APIs to use the syntax enabled by SE-0299 while remaining compatible with older toolchains. 19 | /// 20 | /// Please **never write this type name directly**. You may use it in APIs published by this library (e.g. `.percentEncoded(using: .pathSet)`), 21 | /// but you should never write the name `_StaticMember`: never declare a variable using this type, never add extensions to it, never write new APIs using it, etc. 22 | /// Forget you ever found it and move on. 23 | /// 24 | /// One day, when this library is able to require a minimum 5.5 toolchain, this type will be removed. 25 | /// As long as you never write this type's name directly, that removal will be a source-compatible change. 26 | /// 27 | /// [SE-0299]: https://github.com/apple/swift-evolution/blob/main/proposals/0299-extend-generic-static-member-lookup.md 28 | /// 29 | public struct _StaticMember { 30 | 31 | @usableFromInline 32 | internal var base: Base 33 | 34 | @inlinable 35 | internal init(_ base: Base) { self.base = base } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/WebURL/Util/UnsafeSmallStack.swift: -------------------------------------------------------------------------------- 1 | // Copyright The swift-url Contributors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /// A fixed-capacity stack view over unowned memory. 16 | /// 17 | /// This type allows for a variable `count` Collection view over a region of memory. 18 | /// Append items using the `push` method, and remove them using `pop`. Appending more elements 19 | /// than the stack has capacity for, or popping elements when the stack is empty, will trigger a runtime error. 20 | /// 21 | /// This type is unsafe, as it does not own the memory used to store elements, and it is your responsibility 22 | /// to ensure that its lifetime does not exceed that of the memory it points to. 23 | /// 24 | @usableFromInline 25 | internal struct UnsafeSmallStack { 26 | 27 | @usableFromInline 28 | internal var _storage: UnsafeMutablePointer 29 | 30 | @usableFromInline 31 | internal var _capacity: UInt8 32 | 33 | @usableFromInline 34 | internal var _count: UInt8 35 | 36 | /// Creates an `UnsafeSmallStack` over the given region of memory, containing instances of `Optional`. 37 | /// The entire capacity must be initialized - e.g. to `Optional.none`/`nil`. 38 | /// 39 | @inlinable 40 | internal init(ptr: UnsafeMutablePointer, capacity: UInt8) { 41 | self._storage = ptr 42 | self._capacity = capacity 43 | self._count = 0 44 | } 45 | } 46 | 47 | extension UnsafeSmallStack: RandomAccessCollection { 48 | 49 | @usableFromInline 50 | internal typealias Index = UInt8 51 | 52 | @inlinable 53 | internal var startIndex: UInt8 { 0 } 54 | 55 | @inlinable 56 | internal var endIndex: UInt8 { _count } 57 | 58 | @inlinable 59 | internal var count: Int { Int(_count) } 60 | 61 | @inlinable 62 | internal func index(after i: UInt8) -> UInt8 { i &+ 1 } 63 | 64 | @inlinable 65 | internal func index(before i: UInt8) -> UInt8 { i &- 1 } 66 | 67 | @inlinable 68 | internal subscript(position: UInt8) -> Element { 69 | precondition(position < _count) 70 | return (_storage + Int(position)).pointee.unsafelyUnwrapped 71 | } 72 | } 73 | 74 | extension UnsafeSmallStack { 75 | 76 | /// Appends an item to the end of the stack. 77 | /// If the stack has no more capacity, a runtime error is triggered. 78 | /// 79 | @inlinable 80 | internal mutating func push(_ newElement: Element) { 81 | precondition(_count < _capacity) 82 | (_storage + Int(_count)).pointee = newElement 83 | _count &+= 1 84 | } 85 | 86 | /// Removes the last item from the stack. 87 | /// If the stack is empty, a runtime error is triggered. 88 | /// 89 | @inlinable 90 | internal mutating func pop(_ newElement: Element) { 91 | precondition(_count > 0) 92 | (_storage + Int(_count)).pointee = nil 93 | _count &-= 1 94 | } 95 | 96 | @inlinable 97 | internal static func += (_ stack: inout Self, _ newElement: Element) { 98 | stack.push(newElement) 99 | } 100 | } 101 | 102 | /// Executes `body`, passing in an `UnsafeSmallStack` with capacity for 2 elements of the given type. 103 | /// 104 | /// - important: The lifetime of the `UnsafeSmallStack` must not escape the duration of `body`. 105 | /// 106 | @inlinable @inline(__always) 107 | func withUnsafeSmallStack_2( 108 | of: Element.Type, _ body: (inout UnsafeSmallStack) -> Result 109 | ) -> Result { 110 | var storage: (Element?, Element?) = (nil, nil) 111 | return withUnsafeMutableBufferPointerToElements(tuple: &storage) { 112 | var arrayView = UnsafeSmallStack(ptr: $0.baseAddress.unsafelyUnwrapped, capacity: 2) 113 | return body(&arrayView) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /Sources/WebURL/WebURL.docc/Deprecated.md: -------------------------------------------------------------------------------- 1 | # Deprecated APIs 2 | 3 | These APIs will be removed in a future version of WebURL. 4 | 5 | > Note: 6 | > 7 | > Currently, there are no deprecated APIs. 8 | > 9 | > Should there be a need to deprecate any APIs in the future, a notice here will describe 10 | > how to update your applications. Deprecated APIs will be removed at the next SemVer-major release 11 | > (e.g. `1.x.y` -> `2.x.y`). 12 | -------------------------------------------------------------------------------- /Sources/WebURL/WebURL.docc/WebURL.md: -------------------------------------------------------------------------------- 1 | # ``WebURL`` 2 | 3 | A new URL type for Swift. 4 | 5 | ## Overview 6 | 7 | WebURL provides support for creating and interpreting URLs according to the latest [industry standard][whatwg-url]. 8 | The API is more expressive than Swift's current `URL` type and more comprehensive, yet also simpler, 9 | with more intuitive semantics. Integration libraries mean that it works seamlessly with Foundation, 10 | swift-system, and other libraries which use the current URL type. 11 | 12 | URLs are universal identifiers, and while most of us probably think of websites (HTTP URLs) or other network services, 13 | they can be used for anything, including local applications and databases. With a better URL library, 14 | you can _do more_ with URLs, in a way that is more obvious and robust, and compatible with existing tooling. 15 | 16 | [whatwg-url]: https://url.spec.whatwg.org/ 17 | 18 | ➡️ **Visit the ``WebURL/WebURL`` type to get started.** 19 | 20 | ## Topics 21 | 22 | ### Creating or Modifying URLs 23 | 24 | - ``WebURL/WebURL`` 25 | - 26 | 27 | ### Foundation Interoperability 28 | 29 | - 30 | 31 | ### Network Hosts 32 | 33 | - ``WebURL/WebURL/Domain`` 34 | - ``IPv4Address`` 35 | - ``IPv6Address`` 36 | 37 | ### Deprecated APIs 38 | 39 | - 40 | -------------------------------------------------------------------------------- /Sources/WebURLFoundationExtras/Extensions/DataAndString.swift: -------------------------------------------------------------------------------- 1 | // Copyright The swift-url Contributors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Note: Some documentation comments are copied/adapted from Foundation, 16 | // and are (c) Apple Inc and the Swift project authors. 17 | 18 | import Foundation 19 | import WebURL 20 | 21 | #if canImport(FoundationNetworking) 22 | import FoundationNetworking 23 | #endif 24 | 25 | // -------------------------------- 26 | // MARK: - Data 27 | // -------------------------------- 28 | 29 | 30 | extension Data { 31 | 32 | /// Foundation APIs which can be stubbed for integration tests. 33 | /// 34 | internal struct _ExtendedFunctions { 35 | internal var contentsOfURL: (URL, Data.ReadingOptions) throws -> Data 36 | internal var writeToURL: (Data, URL, Data.WritingOptions) throws -> Void 37 | 38 | internal static let `default` = _ExtendedFunctions( 39 | contentsOfURL: { try Data(contentsOf: $0, options: $1) }, 40 | writeToURL: { try $0.write(to: $1, options: $2) } 41 | ) 42 | } 43 | 44 | #if DEBUG 45 | internal static var extendedFunctions = _ExtendedFunctions.default 46 | #else 47 | internal static let extendedFunctions = _ExtendedFunctions.default 48 | #endif 49 | 50 | /// Produces a `Data` by loading the contents referred to by a URL. 51 | /// 52 | /// - parameters: 53 | /// - url: The location of the data to load. 54 | /// - options: Options for loading the data. Default value is `[]`. 55 | /// 56 | public init(contentsOf url: WebURL, options: Data.ReadingOptions = []) throws { 57 | self = try Data.extendedFunctions.contentsOfURL(try URL(convertOrThrow: url), options) 58 | } 59 | 60 | /// Writes the contents of the data buffer to a location. 61 | /// 62 | /// - parameters: 63 | /// - url: The location to write the data into. 64 | /// - options: Options for writing the data. Default value is `[]`. 65 | /// 66 | public func write(to url: WebURL, options: Data.WritingOptions = []) throws { 67 | try Data.extendedFunctions.writeToURL(self, try URL(convertOrThrow: url), options) 68 | } 69 | } 70 | 71 | 72 | // -------------------------------- 73 | // MARK: - String 74 | // -------------------------------- 75 | 76 | 77 | extension String { 78 | 79 | /// Foundation APIs which can be stubbed for integration tests. 80 | /// 81 | internal struct _ExtendedFunctions { 82 | internal var contentsOfURL: (URL) throws -> String 83 | internal var contentsOfURLWithEncoding: (URL, String.Encoding) throws -> String 84 | internal var contentsOfURLWithUsedEncoding: (URL, inout String.Encoding) throws -> String 85 | internal var writeToURL: (String, URL, Bool, String.Encoding) throws -> Void 86 | 87 | internal static let `default` = _ExtendedFunctions( 88 | contentsOfURL: { try String(contentsOf: $0) }, 89 | contentsOfURLWithEncoding: { try String(contentsOf: $0, encoding: $1) }, 90 | contentsOfURLWithUsedEncoding: { try String(contentsOf: $0, usedEncoding: &$1) }, 91 | writeToURL: { try $0.write(to: $1, atomically: $2, encoding: $3) } 92 | ) 93 | } 94 | 95 | #if DEBUG 96 | internal static var extendedFunctions = _ExtendedFunctions.default 97 | #else 98 | internal static let extendedFunctions = _ExtendedFunctions.default 99 | #endif 100 | 101 | /// Produces a `String` by loading data from a given URL and attempting to infer its encoding. 102 | /// 103 | /// - parameters: 104 | /// - url: The location of the data to load. 105 | /// - encoding: The encoding to use to interpret the loaded data as a string. 106 | /// 107 | public init(contentsOf url: WebURL) throws { 108 | self = try String.extendedFunctions.contentsOfURL(try URL(convertOrThrow: url)) 109 | } 110 | 111 | /// Produces a `String` by loading data from a given URL and interpreting it using a given encoding. 112 | /// 113 | /// - parameters: 114 | /// - url: The location of the data to load. 115 | /// - encoding: The encoding to use to interpret the loaded data as a string. 116 | /// 117 | public init(contentsOf url: WebURL, encoding: String.Encoding) throws { 118 | self = try String.extendedFunctions.contentsOfURLWithEncoding(try URL(convertOrThrow: url), encoding) 119 | } 120 | 121 | /// Produces a `String` by loading data from a given URL, inferring its encoding, and returning the encoding 122 | /// which was inferred. 123 | /// 124 | /// - parameters: 125 | /// - url: The location of the data to load. 126 | /// - usedEncoding: The encoding which was used to interpret the loaded data as a string. 127 | /// 128 | public init(contentsOf url: WebURL, usedEncoding: inout String.Encoding) throws { 129 | self = try String.extendedFunctions.contentsOfURLWithUsedEncoding(try URL(convertOrThrow: url), &usedEncoding) 130 | } 131 | 132 | /// Writes the contents of the string to a location, using a specified encoding. 133 | /// 134 | /// - parameters: 135 | /// - url: The location to write the string into. 136 | /// - atomically: If `true`, the string is first written to an auxiliary file, which replaces the original 137 | /// file when writing is complete. 138 | /// - encoding: The encoding to write the string as. 139 | /// 140 | public func write(to url: WebURL, atomically useAuxiliaryFile: Bool, encoding: String.Encoding) throws { 141 | try String.extendedFunctions.writeToURL(self, try URL(convertOrThrow: url), useAuxiliaryFile, encoding) 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /Sources/WebURLFoundationExtras/Util/ConversionError.swift: -------------------------------------------------------------------------------- 1 | // Copyright The swift-url Contributors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Foundation 16 | import WebURL 17 | 18 | /// An Error which is thrown when a `WebURL` value could not be converted to a `Foundation.URL`. 19 | /// 20 | internal struct WebURLToFoundationConversionError: Error, CustomStringConvertible { 21 | 22 | /// The `WebURL` which could not be converted. 23 | internal var url: WebURL 24 | 25 | internal init(_ url: WebURL) { 26 | self.url = url 27 | } 28 | 29 | internal var description: String { 30 | "Failed to convert WebURL to Foundation.URL: \(url)." 31 | } 32 | } 33 | 34 | extension URL { 35 | 36 | /// Converts the given `WebURL` to a `Foundation.URL`, or throws a `WebURLToFoundationConversionError`. 37 | /// 38 | internal init(convertOrThrow url: WebURL, addPercentEncoding: Bool = true) throws { 39 | guard let convertedURL = URL(url, addPercentEncoding: addPercentEncoding) else { 40 | throw WebURLToFoundationConversionError(url) 41 | } 42 | self = convertedURL 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/WebURLFoundationExtras/Util/EvenFasterCollectionAlgorithms.swift: -------------------------------------------------------------------------------- 1 | // Copyright The swift-url Contributors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import func Foundation.memcmp 16 | import struct WebURL.IPv4Address 17 | import struct WebURL.IPv6Address 18 | 19 | // -------------------------------------------- 20 | // MARK: - FastElementsEqual 21 | // -------------------------------------------- 22 | // Optimizes to a memcmp when both sequences are contiguous. The pure-Swift version is big and slow. 23 | // https://bugs.swift.org/browse/SR-15712 24 | 25 | 26 | extension Sequence where Element == UInt8 { 27 | 28 | @inline(__always) // wCSIA. 29 | internal func fastElementsEqual(_ other: Other) -> Bool where Other: Sequence, Other.Element == UInt8 { 30 | let contiguousResult = withContiguousStorageIfAvailable { selfBuffer in 31 | other.withContiguousStorageIfAvailable { otherBuffer -> Bool in 32 | guard selfBuffer.count == otherBuffer.count else { 33 | return false 34 | } 35 | guard selfBuffer.count > 0 else { 36 | return true 37 | } 38 | return memcmp(selfBuffer.baseAddress!, otherBuffer.baseAddress!, selfBuffer.count) == 0 39 | } 40 | } 41 | switch contiguousResult { 42 | case .some(.some(let result)): 43 | return result 44 | default: 45 | return elementsEqual(other) 46 | } 47 | } 48 | } 49 | 50 | 51 | // -------------------------------------------- 52 | // MARK: - IPAddress FastEquals 53 | // -------------------------------------------- 54 | // Uses memcmp to compare IP addresses. The pure-Swift version is big and slow. 55 | // Technically not a collection algorithm, but in the same spirit as fastElementsEqual above. 56 | // https://bugs.swift.org/browse/SR-15712 57 | 58 | 59 | internal func fastEquals(_ lhs: IPv4Address, _ rhs: IPv4Address) -> Bool { 60 | withUnsafeBytes(of: lhs.octets) { lhsBytes in 61 | withUnsafeBytes(of: rhs.octets) { rhsBytes in 62 | memcmp(lhsBytes.baseAddress!, rhsBytes.baseAddress!, 4) == 0 63 | } 64 | } 65 | } 66 | 67 | internal func fastEquals(_ lhs: IPv6Address, _ rhs: IPv6Address) -> Bool { 68 | withUnsafeBytes(of: lhs.octets) { lhsBytes in 69 | withUnsafeBytes(of: rhs.octets) { rhsBytes in 70 | memcmp(lhsBytes.baseAddress!, rhsBytes.baseAddress!, 16) == 0 71 | } 72 | } 73 | } 74 | 75 | 76 | // -------------------------------------------- 77 | // MARK: - FastContains 78 | // -------------------------------------------- 79 | // From WebURL/Util/Pointers.swift, WebURL/Util/BitTwiddling.swift 80 | 81 | 82 | extension Sequence where Element == UInt8 { 83 | 84 | /// Whether or not this sequence contains the given byte. 85 | /// 86 | /// If the sequence has contiguous storage, this optimizes to a fast, chunked search. 87 | /// 88 | @inlinable @inline(__always) 89 | internal func fastContains(_ element: Element) -> Bool { 90 | // Hoist mask calculation out of wCSIA to ensure it is constant-folded, even if wCSIA isn't inlined. 91 | let mask = UInt64(repeatingByte: element) 92 | return withContiguousStorageIfAvailable { $0.__fastContains(element: element, mask: mask) } ?? contains(element) 93 | } 94 | } 95 | 96 | #if swift(<5.9) 97 | extension UnsafeRawPointer { 98 | 99 | /// Returns a new instance of the given type, constructed from the raw memory at the specified offset. 100 | /// 101 | /// The memory at this pointer plus offset must be initialized to `T` or another type 102 | /// that is layout compatible with `T`. It does not need to be aligned for access to `T`. 103 | /// 104 | @inlinable @inline(__always) 105 | internal func loadUnaligned(fromByteOffset offset: Int = 0, as: T.Type) -> T where T: FixedWidthInteger { 106 | assert(_isPOD(T.self)) 107 | var val: T = 0 108 | withUnsafeMutableBytes(of: &val) { 109 | $0.copyMemory(from: UnsafeRawBufferPointer(start: self, count: T.bitWidth / 8)) 110 | } 111 | return val 112 | } 113 | } 114 | #endif 115 | 116 | extension UInt64 { 117 | 118 | /// Creates an 8-byte integer, each of which is equal to the given byte. 119 | /// 120 | @inlinable @inline(__always) 121 | internal init(repeatingByte byte: UInt8) { 122 | self = 0 123 | withUnsafeMutableBytes(of: &self) { 124 | $0[0] = byte 125 | $0[1] = byte 126 | $0[2] = byte 127 | $0[3] = byte 128 | $0[4] = byte 129 | $0[5] = byte 130 | $0[6] = byte 131 | $0[7] = byte 132 | } 133 | } 134 | } 135 | 136 | extension UnsafeBufferPointer where Element == UInt8 { 137 | 138 | /// Whether or not the buffer contains the given byte. 139 | /// 140 | /// This implementation compares chunks of 8 bytes at a time, 141 | /// using only 4 instructions per chunk of 8 bytes. 142 | /// 143 | @inlinable 144 | internal func __fastContains(element: UInt8, mask: UInt64) -> Bool { 145 | var i = startIndex 146 | while distance(from: i, to: endIndex) >= 8 { 147 | // Load 8 bytes from the source. 148 | var eightBytes = UnsafeRawPointer( 149 | self.baseAddress.unsafelyUnwrapped.advanced(by: i) 150 | ).loadUnaligned(as: UInt64.self) 151 | // XOR every byte with the element we're searching for. 152 | // If there are any matches, we'll get a zero byte in that position. 153 | eightBytes ^= mask 154 | // Use bit-twiddling to detect if any bytes were zero. 155 | // https://graphics.stanford.edu/~seander/bithacks.html#ValueInWord 156 | let found = (eightBytes &- 0x0101_0101_0101_0101) & (~eightBytes & 0x8080_8080_8080_8080) 157 | if found != 0 { return true } 158 | i &+= 8 159 | } 160 | while i < endIndex { 161 | if self[i] == element { return true } 162 | i &+= 1 163 | } 164 | return false 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /Sources/WebURLFoundationExtras/Util/StringAdditions.swift: -------------------------------------------------------------------------------- 1 | // Copyright The swift-url Contributors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // -------------------------------------------- 16 | // MARK: - Contiguous UTF-8 17 | // -------------------------------------------- 18 | // Copied from WebURL/Util/StringAdditions.swift 19 | 20 | 21 | extension StringProtocol { 22 | 23 | /// Calls `body` with this string's contents in a contiguous UTF-8 buffer. 24 | /// 25 | /// - If this is already a native String/Substring, its contiguous storage will be used directly. 26 | /// - Otherwise, it will be copied to contiguous storage. 27 | /// 28 | @inlinable @inline(__always) 29 | internal func _withContiguousUTF8(_ body: (UnsafeBufferPointer) throws -> Result) rethrows -> Result { 30 | if let resultWithExistingStorage = try utf8.withContiguousStorageIfAvailable(body) { 31 | return resultWithExistingStorage 32 | } 33 | var copy = String(self) 34 | return try copy.withUTF8(body) 35 | } 36 | } 37 | 38 | extension Optional where Wrapped: StringProtocol { 39 | 40 | /// Calls `body` with this string's contents in a contiguous UTF-8 buffer. 41 | /// 42 | /// - If this value is `nil`, `body` is invoked with `nil`. 43 | /// - If this is already a native String/Substring, its contiguous storage will be used directly. 44 | /// - Otherwise, it will be copied to contiguous storage. 45 | /// 46 | @inlinable @inline(__always) 47 | internal func _withContiguousUTF8(_ body: (UnsafeBufferPointer?) throws -> Result) rethrows -> Result { 48 | switch self { 49 | case .some(let string): return try string._withContiguousUTF8(body) 50 | case .none: return try body(nil) 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/WebURLTestSupport/CollectionUtils.swift: -------------------------------------------------------------------------------- 1 | // Copyright The swift-url Contributors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | extension BidirectionalCollection where Element == UInt8 { 16 | 17 | /// Returns a slice of this collection, trimmed of any ASCII spaces from the start and end. 18 | /// 19 | var trimmingASCIISpaces: SubSequence { 20 | let firstNonSpace = firstIndex(where: { $0 != UInt8(ascii: " ") }) ?? endIndex 21 | let lastNonSpace = lastIndex(where: { $0 != UInt8(ascii: " ") }).map { index(after: $0) } ?? endIndex 22 | return self[firstNonSpace.. = [SectionHeaderOr] 25 | 26 | /// An object which is either an `Element` or a section header. 27 | /// 28 | /// When decoding, data which fails to be decode as an `Element` and contains a `__section__` key will be considered a section marker. 29 | /// This is useful for organizing large JSON arrays, such as a list of test-cases. 30 | /// 31 | public enum SectionHeaderOr { 32 | case sectionHeader(String) 33 | case element(Element) 34 | } 35 | 36 | extension SectionHeaderOr: Codable where Element: Codable { 37 | 38 | enum SectionCodingKeys: String, CodingKey { 39 | case section = "__section__" 40 | } 41 | 42 | public init(from decoder: Decoder) throws { 43 | do { 44 | let element = try Element(from: decoder) 45 | self = .element(element) 46 | } catch { 47 | let elementDecodingError = error 48 | do { 49 | let sectionObject = try decoder.container(keyedBy: SectionCodingKeys.self) 50 | let sectionName = try sectionObject.decode(String.self, forKey: .section) 51 | self = .sectionHeader(sectionName) 52 | } catch { 53 | throw elementDecodingError 54 | } 55 | } 56 | } 57 | 58 | public func encode(to encoder: Encoder) throws { 59 | switch self { 60 | case .sectionHeader(let name): 61 | var container = encoder.container(keyedBy: SectionCodingKeys.self) 62 | try container.encode(name, forKey: .section) 63 | case .element(let element): 64 | try element.encode(to: encoder) 65 | } 66 | } 67 | } 68 | 69 | extension SectionHeaderOr: Equatable where Element: Equatable {} 70 | extension SectionHeaderOr: Hashable where Element: Hashable {} 71 | 72 | 73 | // -------------------------------------------- 74 | // MARK: - Encoded 'Result' 75 | // -------------------------------------------- 76 | 77 | 78 | /// An object which, like the standard library's `Result`, represents the result of an operation which may fail. 79 | /// 80 | /// It is different from the standard library's `Result` in that the failure condition may not be an `Error` (it can, for instance, be a `String`), 81 | /// and by having a custom `Codable` representation. When decoding, if data fails to be decoded as a `Value` and includes a `failure-reason` key, 82 | /// the data for that key will be decoded as the `Failure` type. 83 | /// 84 | public enum EncodedResult { 85 | case success(Value) 86 | case failure(Failure) 87 | } 88 | 89 | extension EncodedResult: Codable where Value: Codable, Failure: Codable { 90 | 91 | enum FailureCaseCodingKeys: String, CodingKey { 92 | case reason = "failure-reason" 93 | } 94 | 95 | public init(from decoder: Decoder) throws { 96 | do { 97 | self = .success(try decoder.singleValueContainer().decode(Value.self)) 98 | } catch { 99 | let valueDecodingError = error 100 | do { 101 | let failureObject = try decoder.container(keyedBy: FailureCaseCodingKeys.self) 102 | self = .failure(try failureObject.decode(Failure.self, forKey: .reason)) 103 | } catch { 104 | throw valueDecodingError 105 | } 106 | } 107 | } 108 | 109 | public func encode(to encoder: Encoder) throws { 110 | switch self { 111 | case .success(let successResult): 112 | var container = encoder.singleValueContainer() 113 | try container.encode(successResult) 114 | case .failure(let reason): 115 | var container = encoder.container(keyedBy: FailureCaseCodingKeys.self) 116 | try container.encode(reason, forKey: .reason) 117 | } 118 | } 119 | } 120 | 121 | extension EncodedResult: Equatable where Value: Equatable, Failure: Equatable {} 122 | extension EncodedResult: Hashable where Value: Hashable, Failure: Hashable {} 123 | 124 | extension EncodedResult where Failure: Error { 125 | 126 | public init(_ other: Result) { 127 | switch other { 128 | case .success(let value): 129 | self = .success(value) 130 | case .failure(let error): 131 | self = .failure(error) 132 | } 133 | } 134 | } 135 | 136 | extension Result { 137 | 138 | public init(_ other: EncodedResult) { 139 | switch other { 140 | case .success(let value): 141 | self = .success(value) 142 | case .failure(let error): 143 | self = .failure(error) 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /Sources/WebURLTestSupport/Reports/SimpleDescription.swift: -------------------------------------------------------------------------------- 1 | // Copyright The swift-url Contributors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /// Returns a simple structural description of a value. 16 | /// 17 | /// It's a bit rough, but the output is a bit easier to read in test reports than the result of `String(reflecting:)` or `String(describing:)`, 18 | /// and is less boilerplate-y than writing custom formatters for everything you want to log. 19 | /// 20 | internal func describe(_ value: T, maxLevel: Int = 8) -> String { 21 | 22 | let mirror = Mirror(reflecting: value) 23 | 24 | // Scalars. 25 | if mirror.children.isEmpty { 26 | return String(describing: value) 27 | // Optionals. 28 | } else if let optional = value as? OptionalProtocol { 29 | if let unwrapped = optional.asOptional { 30 | return describe(unwrapped) 31 | } else { 32 | return "" 33 | } 34 | } 35 | // Complex structures. 36 | var string = "{\n" 37 | for (label, value) in mirror.children { 38 | string += " - " + "\(label ?? "_"): \(_describeChild(value, level: 2, maxLevel: maxLevel))\n" 39 | } 40 | string += "}" 41 | return string 42 | } 43 | 44 | private func _describeChild(_ value: T, level: Int, maxLevel: Int) -> String { 45 | 46 | let mirror = Mirror(reflecting: value) 47 | 48 | // Scalars/depth limit. 49 | if mirror.children.isEmpty || level == maxLevel { 50 | return String(describing: value) 51 | // Optionals. 52 | } else if let optional = value as? OptionalProtocol { 53 | if let unwrapped = optional.asOptional { 54 | return _describeChild(unwrapped, level: level, maxLevel: maxLevel) 55 | } else { 56 | return "" 57 | } 58 | } 59 | // Complex structures. No braces around these because it's just too much noise. 60 | var string = "\n" 61 | for (label, value) in mirror.children { 62 | string += String(repeating: " ", count: level) 63 | string += "- \(label ?? "_"): \(_describeChild(value, level: level + 1, maxLevel: maxLevel))\n" 64 | } 65 | string.removeLast() 66 | return string 67 | } 68 | 69 | private protocol OptionalProtocol { 70 | var asOptional: Any? { get } 71 | } 72 | 73 | extension Optional: OptionalProtocol { 74 | fileprivate var asOptional: Any? { 75 | self 76 | } 77 | } 78 | 79 | extension CustomStringConvertible where Self: RawRepresentable, Self.RawValue == String { 80 | public var description: String { 81 | rawValue 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Sources/WebURLTestSupport/TestFiles.swift: -------------------------------------------------------------------------------- 1 | // Copyright The swift-url Contributors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Foundation 16 | 17 | /// A database of test cases. 18 | /// 19 | /// Load a database using either 20 | /// - ``loadBinaryTestFile(_:)``, to load the file as binary data, or 21 | /// - ``loadTestFile(_:as:)``, providing to decode the file from JSON using Codable. 22 | /// 23 | public enum TestFile: String, Equatable, Hashable, Codable { 24 | case WPTURLConstructorTests = "urltestdata" 25 | case WebURLAdditionalConstructorTests = "additional_constructor_tests" 26 | case WPTURLSetterTests = "setters_tests" 27 | case WebURLAdditionalSetterTests = "additional_setters_tests" 28 | case WPTToASCIITests = "toascii" 29 | case FilePathTests = "file_url_path_tests" 30 | case IdnaTest = "IdnaTestV2" 31 | 32 | fileprivate var fileExtension: String { 33 | if case .IdnaTest = self { return "txt" } 34 | return "json" 35 | } 36 | } 37 | 38 | internal enum LoadTestFileError: Error { 39 | case failedToLoad(TestFile, error: Error?) 40 | case failedToDecode(TestFile, error: Error) 41 | } 42 | 43 | /// Loads the given test database as binary data. 44 | /// 45 | /// If the file cannot be loaded, an error is thrown. 46 | /// 47 | public func loadBinaryTestFile(_ file: TestFile) throws -> Data { 48 | let fileData: Data 49 | #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) 50 | let url = Bundle.module.url(forResource: "TestFilesData/\(file.rawValue)", withExtension: file.fileExtension)! 51 | fileData = try Result { 52 | try Data(contentsOf: url) 53 | }.mapError { 54 | LoadTestFileError.failedToLoad(file, error: $0) 55 | }.get() 56 | #else 57 | // SwiftPM resources don't appear to work on other platforms, so just... load it from the repository (sigh). 58 | var path = #filePath 59 | path.removeLast("TestFiles.swift".utf8.count) 60 | path += "TestFilesData/\(file.rawValue).\(file.fileExtension)" 61 | guard let data = FileManager.default.contents(atPath: path) else { 62 | throw LoadTestFileError.failedToLoad(file, error: nil) 63 | } 64 | fileData = data 65 | #endif 66 | return fileData 67 | } 68 | 69 | /// Loads and decodes the given test database. 70 | /// 71 | /// If the file cannot be loaded or decoded, an error is thrown. 72 | /// 73 | public func loadTestFile(_ file: TestFile, as: T.Type) throws -> T { 74 | 75 | let fileData = try loadBinaryTestFile(file) 76 | return try Result { 77 | try JSONDecoder().decode(T.self, from: fileData) 78 | }.mapError { 79 | LoadTestFileError.failedToDecode(file, error: $0) 80 | }.get() 81 | } 82 | -------------------------------------------------------------------------------- /Sources/WebURLTestSupport/TestFilesData/.gitattributes: -------------------------------------------------------------------------------- 1 | # Do not interfere with line endings 2 | *.txt binary 3 | -------------------------------------------------------------------------------- /Sources/WebURLTestSupport/TestFilesData/additional_setters_tests.json: -------------------------------------------------------------------------------- 1 | { 2 | "comment": [ 3 | "## Tests for setters of https://url.spec.whatwg.org/#urlutils-members", 4 | "", 5 | "This file contains a JSON object.", 6 | "Other than 'comment', each key is an attribute of the `URL` interface", 7 | "defined in WHATWG’s URL Standard.", 8 | "The values are arrays of test case objects for that attribute.", 9 | "", 10 | "To run a test case for the attribute `attr`:", 11 | "", 12 | "* Create a new `URL` object with the value for the 'href' key", 13 | " the constructor single parameter. (Without a base URL.)", 14 | " This must not throw.", 15 | "* Set the attribute `attr` to (invoke its setter with)", 16 | " with the value of for 'new_value' key.", 17 | "* The value for the 'expected' key is another object.", 18 | " For each `key` / `value` pair of that object,", 19 | " get the attribute `key` (invoke its getter).", 20 | " The returned string must be equal to `value`.", 21 | "", 22 | "Note: the 'href' setter is already covered by urltestdata.json." 23 | ], 24 | "protocol": [ 25 | { 26 | "comment": "Tabs and newlines are ignored", 27 | "href": "a://example.net", 28 | "new_value": "\u0009\u0009fo\u000Ao", 29 | "expected": { 30 | "href": "foo://example.net", 31 | "protocol": "foo:" 32 | } 33 | } 34 | ], 35 | "username": [ 36 | { 37 | "comment": "Tabs and newlines are not ignored", 38 | "href": "http://example.com/", 39 | "new_value": "\u0009us\u000Aer", 40 | "expected": { 41 | "href": "http://%09us%0Aer@example.com/", 42 | "username": "%09us%0Aer" 43 | } 44 | } 45 | ], 46 | "password": [ 47 | { 48 | "comment": "Tabs and newlines are not ignored", 49 | "href": "http://example.com/", 50 | "new_value": "pa\u0009\u000Ass\u0009", 51 | "expected": { 52 | "href": "http://:pa%09%0Ass%09@example.com/", 53 | "password": "pa%09%0Ass%09" 54 | } 55 | } 56 | ], 57 | "port": [ 58 | { 59 | "comment": "Tabs and newlines are ignored", 60 | "href": "http://example.com/", 61 | "new_value": "\u00099\u000A\u00090", 62 | "expected": { 63 | "href": "http://example.com:90/", 64 | "port": "90" 65 | } 66 | } 67 | ], 68 | "pathname": [ 69 | { 70 | "comment": "Tabs and newlines are ignored", 71 | "href": "http://example.com/", 72 | "new_value": "\u0009/\u000Ahel\u0009lo\u0009/\u0009\u0009world/\u0009.\u0009.", 73 | "expected": { 74 | "href": "http://example.com/hello/", 75 | "pathname": "/hello/" 76 | } 77 | } 78 | ], 79 | "host": [], 80 | "hostname": [], 81 | "search": [ 82 | { 83 | "comment": "Adding a query to a URL with opaque path does not throw an error", 84 | "href": "opaque:somepath", 85 | "new_value": "q=test", 86 | "expected": { 87 | "href": "opaque:somepath?q=test", 88 | "search": "?q=test" 89 | } 90 | }, 91 | { 92 | "comment": "Removing a query on a URL with opaque path does not throw an error", 93 | "href": "opaque:somepath?q=test", 94 | "new_value": "", 95 | "expected": { 96 | "href": "opaque:somepath", 97 | "search": "" 98 | } 99 | }, 100 | { 101 | "comment": "Modifying a query on a URL with opaque path does not throw an error", 102 | "href": "opaque:somepath?q=test", 103 | "new_value": "foo=bar", 104 | "expected": { 105 | "href": "opaque:somepath?foo=bar", 106 | "search": "?foo=bar" 107 | } 108 | } 109 | ], 110 | "hash": [] 111 | } 112 | -------------------------------------------------------------------------------- /Sources/WebURLTestSupport/TestFilesData/toascii.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "comment": "Label with hyphens in 3rd and 4th position", 4 | "input": "aa--", 5 | "output": "aa--" 6 | }, 7 | { 8 | "input": "a†--", 9 | "output": "xn--a---kp0a" 10 | }, 11 | { 12 | "input": "ab--c", 13 | "output": "ab--c" 14 | }, 15 | { 16 | "comment": "Label with leading hyphen", 17 | "input": "-x", 18 | "output": "-x" 19 | }, 20 | { 21 | "input": "-†", 22 | "output": "xn----xhn" 23 | }, 24 | { 25 | "input": "-x.xn--zca", 26 | "output": "-x.xn--zca" 27 | }, 28 | { 29 | "input": "-x.ß", 30 | "output": "-x.xn--zca" 31 | }, 32 | { 33 | "comment": "Label with trailing hyphen", 34 | "input": "x-.xn--zca", 35 | "output": "x-.xn--zca" 36 | }, 37 | { 38 | "input": "x-.ß", 39 | "output": "x-.xn--zca" 40 | }, 41 | { 42 | "comment": "Empty labels", 43 | "input": "x..xn--zca", 44 | "output": "x..xn--zca" 45 | }, 46 | { 47 | "input": "x..ß", 48 | "output": "x..xn--zca" 49 | }, 50 | { 51 | "comment": "Invalid Punycode", 52 | "input": "xn--a", 53 | "output": null 54 | }, 55 | { 56 | "input": "xn--a.xn--zca", 57 | "output": null 58 | }, 59 | { 60 | "input": "xn--a.ß", 61 | "output": null 62 | }, 63 | { 64 | "comment": "Invalid Punycode (contains non-ASCII character)", 65 | "input": "xn--tešla", 66 | "output": null 67 | }, 68 | { 69 | "comment": "Valid Punycode", 70 | "input": "xn--zca.xn--zca", 71 | "output": "xn--zca.xn--zca" 72 | }, 73 | { 74 | "comment": "Mixed", 75 | "input": "xn--zca.ß", 76 | "output": "xn--zca.xn--zca" 77 | }, 78 | { 79 | "input": "ab--c.xn--zca", 80 | "output": "ab--c.xn--zca" 81 | }, 82 | { 83 | "input": "ab--c.ß", 84 | "output": "ab--c.xn--zca" 85 | }, 86 | { 87 | "comment": "CheckJoiners is true", 88 | "input": "\u200D.example", 89 | "output": null 90 | }, 91 | { 92 | "input": "xn--1ug.example", 93 | "output": null 94 | }, 95 | { 96 | "comment": "CheckBidi is true", 97 | "input": "يa", 98 | "output": null 99 | }, 100 | { 101 | "input": "xn--a-yoc", 102 | "output": null 103 | }, 104 | { 105 | "comment": "processing_option is Nontransitional_Processing", 106 | "input": "ශ්‍රී", 107 | "output": "xn--10cl1a0b660p" 108 | }, 109 | { 110 | "input": "نامه‌ای", 111 | "output": "xn--mgba3gch31f060k" 112 | }, 113 | { 114 | "comment": "U+FFFD", 115 | "input": "\uFFFD.com", 116 | "output": null 117 | }, 118 | { 119 | "comment": "U+FFFD character encoded in Punycode", 120 | "input": "xn--zn7c.com", 121 | "output": null 122 | }, 123 | { 124 | "comment": "Label longer than 63 code points", 125 | "input": "x01234567890123456789012345678901234567890123456789012345678901x", 126 | "output": "x01234567890123456789012345678901234567890123456789012345678901x" 127 | }, 128 | { 129 | "input": "x01234567890123456789012345678901234567890123456789012345678901†", 130 | "output": "xn--x01234567890123456789012345678901234567890123456789012345678901-6963b" 131 | }, 132 | { 133 | "input": "x01234567890123456789012345678901234567890123456789012345678901x.xn--zca", 134 | "output": "x01234567890123456789012345678901234567890123456789012345678901x.xn--zca" 135 | }, 136 | { 137 | "input": "x01234567890123456789012345678901234567890123456789012345678901x.ß", 138 | "output": "x01234567890123456789012345678901234567890123456789012345678901x.xn--zca" 139 | }, 140 | { 141 | "comment": "Domain excluding TLD longer than 253 code points", 142 | "input": "01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.0123456789012345678901234567890123456789012345678.x", 143 | "output": "01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.0123456789012345678901234567890123456789012345678.x" 144 | }, 145 | { 146 | "input": "01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.0123456789012345678901234567890123456789012345678.xn--zca", 147 | "output": "01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.0123456789012345678901234567890123456789012345678.xn--zca" 148 | }, 149 | { 150 | "input": "01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.0123456789012345678901234567890123456789012345678.ß", 151 | "output": "01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.0123456789012345678901234567890123456789012345678.xn--zca" 152 | }, 153 | { 154 | "comment": "IDNA ignored code points", 155 | "input": "a\u00ADb", 156 | "output": "ab" 157 | }, 158 | { 159 | "input": "a%C2%ADb", 160 | "output": "ab" 161 | }, 162 | { 163 | "comment": "Empty host after domain to ASCII", 164 | "input": "\u00AD", 165 | "output": null 166 | }, 167 | { 168 | "input": "%C2%AD", 169 | "output": null 170 | }, 171 | { 172 | "input": "xn--", 173 | "output": null 174 | } 175 | ] 176 | -------------------------------------------------------------------------------- /Sources/WebURLTestSupport/TestSuite.swift: -------------------------------------------------------------------------------- 1 | // Copyright The swift-url Contributors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /// A description of a test suite. 16 | /// 17 | /// This groups a series of related types, which can be used to simplify some boilerplate code. 18 | /// 19 | public protocol TestSuite { 20 | 21 | /// A testcase. A set of input data and expected results. 22 | /// 23 | associatedtype TestCase: Hashable 24 | 25 | /// The set of possible failures that may occur when testing a particular `TestCase`. 26 | /// 27 | associatedtype TestFailure: Hashable 28 | 29 | /// Additional information captured while testing a particular `TestCase`. 30 | /// 31 | associatedtype CapturedData: Hashable = Never 32 | 33 | // Note: 34 | // Ideally, we'd have the harness here as an "associated protocol", and then we could build 35 | // some more functionality, such as adding a `runTest(TestCase, using: H)` requirement. 36 | // 37 | // I've tried a bunch of stuff, like open classes, but anything I add in protocol extensions on 'TestSuite' 38 | // won't get dynamic dispatch. We're really quite limited in what we can express, so this can't be much more 39 | // than a bag-o-types. 40 | } 41 | 42 | /// The result of testing a particular `TestCase` from the given `Suite`. 43 | /// 44 | /// This includes the case that was tested, any failures that occurred, and any additional captured data. 45 | /// 46 | public struct TestResult { 47 | 48 | /// The number of cases that have been tested prior to this one. 49 | /// 50 | public var testNumber: Int 51 | 52 | /// The `TestCase` that was tested. 53 | /// 54 | public var testCase: Suite.TestCase 55 | 56 | /// The failures that were encountered while testing `testCase`. If empty, the test did not encounter any unexpected results. 57 | /// 58 | public var failures: Set 59 | 60 | /// Any additional captured data gathered while running the test. 61 | /// 62 | public var captures: Suite.CapturedData? 63 | 64 | public init(testNumber: Int, testCase: Suite.TestCase) { 65 | self.testNumber = testNumber 66 | self.testCase = testCase 67 | self.captures = nil 68 | self.failures = [] 69 | } 70 | } 71 | 72 | extension TestResult: Equatable, Hashable {} 73 | 74 | /// An object which is able to run test-cases from a `TestSuite` and collect the results. 75 | /// 76 | /// Each `TestSuite` adds its own protocol which refines this one, adding requirements for the functionality it needs, 77 | /// and implements the `_runTestCase` method using those requirements. Conformers should implement those additional requirements, 78 | /// as well as `reportTestResult` and (optionally) `markSection`, but **not** `_runTestCase`. 79 | /// 80 | public protocol TestHarnessProtocol { 81 | 82 | associatedtype Suite: TestSuite 83 | 84 | /// Private method which is implemented by protocols which refine `TestHarnessProtocol`. 85 | /// Unless you are defining a new `TestSuite`, **DO NOT IMPLEMENT THIS**. 86 | /// 87 | func _runTestCase(_ testcase: Suite.TestCase, _ result: inout TestResult) 88 | 89 | /// Report the result of running a test from the suite. 90 | /// 91 | mutating func reportTestResult(_ result: TestResult) 92 | 93 | /// Optional method which marks the start of a new section of tests. 94 | /// All results reported after this method is called are considered as belonging to tests in this section. 95 | /// 96 | mutating func markSection(_ name: String) 97 | } 98 | 99 | extension TestHarnessProtocol { 100 | 101 | public mutating func markSection(_ name: String) { 102 | // Optional. 103 | } 104 | 105 | public mutating func runTests(_ tests: [Suite.TestCase]) { 106 | var index = 0 107 | for testcase in tests { 108 | var result = TestResult(testNumber: index, testCase: testcase) 109 | _runTestCase(testcase, &result) 110 | reportTestResult(result) 111 | index += 1 112 | } 113 | } 114 | 115 | public mutating func runTests(_ tests: FlatSectionedArray) { 116 | var index = 0 117 | for sectionOrTestcase in tests { 118 | switch sectionOrTestcase { 119 | case .sectionHeader(let name): 120 | markSection(name) 121 | case .element(let testcase): 122 | var result = TestResult(testNumber: index, testCase: testcase) 123 | _runTestCase(testcase, &result) 124 | reportTestResult(result) 125 | index += 1 126 | } 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /Sources/WebURLTestSupport/TestSuites/FilePathTests+WebURLReportHarness.swift: -------------------------------------------------------------------------------- 1 | // Copyright The swift-url Contributors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import WebURL 16 | 17 | // -------------------------------------------- 18 | // MARK: - FilePathToURLTests 19 | // -------------------------------------------- 20 | 21 | 22 | extension FilePathToURLTests { 23 | 24 | public struct WebURLReportHarness { 25 | public private(set) var report = SimpleTestReport() 26 | public private(set) var entriesSeen = 0 27 | public let expectedFailures: Set 28 | 29 | public init(expectedFailures: Set = []) { 30 | self.expectedFailures = expectedFailures 31 | } 32 | } 33 | } 34 | 35 | extension FilePathToURLTests.WebURLReportHarness: FilePathToURLTests.Harness { 36 | 37 | public func filePathToURL( 38 | _ path: String, format: FilePathFormat 39 | ) -> Result { 40 | Result { try WebURL(filePath: path, format: format).serialized() } 41 | .mapError { Self.errorToFailureReason($0 as! URLFromFilePathError) } 42 | } 43 | 44 | private static func errorToFailureReason(_ error: URLFromFilePathError) -> FilePathToURLTests.FailureReason { 45 | switch error { 46 | case .emptyInput: return .emptyInput 47 | case .nullBytes: return .nullBytes 48 | case .relativePath: return .relativePath 49 | case .upwardsTraversal: return .upwardsTraversal 50 | case .invalidHostname: return .invalidHostname 51 | case .invalidPath: return .invalidPath 52 | case .unsupportedWin32NamespacedPath: return .unsupportedWin32NamespacedPath 53 | case .transcodingFailure: fatalError("WebURL.fromBinaryFilePath should not be transcoding anything") 54 | default: fatalError("Unknown error") 55 | } 56 | } 57 | 58 | public func parseSerializedURL(_ serializedURL: String) -> String? { 59 | WebURL(serializedURL)?.serialized() 60 | } 61 | 62 | public mutating func markSection(_ name: String) { 63 | report.markSection(name) 64 | } 65 | 66 | public mutating func reportTestResult(_ result: TestResult) { 67 | entriesSeen += 1 68 | report.performTest { reporter in 69 | if expectedFailures.contains(result.testNumber) { reporter.expectedResult = .fail } 70 | reporter.reportTestResult(result) 71 | } 72 | } 73 | } 74 | 75 | 76 | // -------------------------------------------- 77 | // MARK: - URLToFilePathTests 78 | // -------------------------------------------- 79 | 80 | 81 | extension URLToFilePathTests { 82 | 83 | public struct WebURLReportHarness { 84 | public private(set) var report = SimpleTestReport() 85 | public private(set) var entriesSeen = 0 86 | public let expectedFailures: Set 87 | 88 | public init(expectedFailures: Set = []) { 89 | self.expectedFailures = expectedFailures 90 | } 91 | } 92 | } 93 | 94 | extension URLToFilePathTests.WebURLReportHarness: URLToFilePathTests.Harness { 95 | 96 | public func parseSerializedURL(_ string: String) -> WebURL? { 97 | WebURL(string) 98 | } 99 | 100 | public func urlToFilePath( 101 | _ url: WebURL, format: FilePathFormat 102 | ) -> Result { 103 | Result { 104 | String(decoding: try WebURL.binaryFilePath(from: url, format: format, nullTerminated: false), as: UTF8.self) 105 | }.mapError { Self.errorToFailureReason($0 as! FilePathFromURLError) } 106 | } 107 | 108 | private static func errorToFailureReason(_ error: FilePathFromURLError) -> URLToFilePathTests.FailureReason { 109 | switch error { 110 | case .notAFileURL: return .notAFileURL 111 | case .encodedNullBytes: return .encodedNullByte 112 | case .encodedPathSeparator: return .encodedSeparator 113 | case .unsupportedNonLocalFile: return .unsupportedNonLocalFile 114 | case .unsupportedHostname: return .unsupportedHostname 115 | case .windowsPathIsNotFullyQualified: return .relativePath 116 | case .transcodingFailure: fatalError("WebURL.binaryFilePath should not be transcoding anything") 117 | default: fatalError("Unknown error") 118 | } 119 | } 120 | 121 | public mutating func markSection(_ name: String) { 122 | report.markSection(name) 123 | } 124 | 125 | public mutating func reportTestResult(_ result: TestResult) { 126 | entriesSeen += 1 127 | report.performTest { reporter in 128 | if expectedFailures.contains(result.testNumber) { reporter.expectedResult = .fail } 129 | reporter.reportTestResult(result) 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /Sources/WebURLTestSupport/TestSuites/UTS46Conformance+WebURLIDNAReportHarness.swift: -------------------------------------------------------------------------------- 1 | // Copyright The swift-url Contributors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import IDNA 16 | 17 | extension UTS46Conformance { 18 | 19 | public struct WebURLIDNAReportHarness { 20 | public private(set) var report = SimpleTestReport() 21 | public private(set) var reportedResultCount = 0 22 | public let std3Tests: Set 23 | 24 | public init(std3Tests: Set = []) { 25 | self.std3Tests = std3Tests 26 | } 27 | } 28 | } 29 | 30 | extension UTS46Conformance.WebURLIDNAReportHarness: UTS46Conformance.Harness { 31 | 32 | // ToUnicode. 33 | 34 | // swift-format-ignore 35 | public static var ToUnicode_NonApplicableValidationSteps: Set = [ 36 | .V2, .V3, // CheckHyphens 37 | .NV8, // IDNA2008 extras 38 | .X3, .X4_2, // Thrown by older versions of the standard. 39 | ] 40 | 41 | public func toUnicode( 42 | utf8: UTF8Bytes 43 | ) -> String? where UTF8Bytes: Collection, UTF8Bytes.Element == UInt8 { 44 | var result = "" 45 | let success = IDNA.toUnicode(utf8: utf8) { label, dot in 46 | result.unicodeScalars += label 47 | if dot { result += "." } 48 | return true 49 | } 50 | return success ? result : nil 51 | } 52 | 53 | // ToAsciiN. 54 | 55 | // swift-format-ignore 56 | public static var ToAsciiN_NonApplicableValidationSteps: Set = [ 57 | .A4_1, .A4_2, // VerifyDnsLength 58 | .V2, .V3, // CheckHyphens 59 | .NV8, // IDNA2008 extras 60 | .X3, .X4_2, // Thrown by older versions of the standard. 61 | ] 62 | 63 | public func toAsciiN( 64 | utf8: UTF8Bytes 65 | ) -> String? where UTF8Bytes: Collection, UTF8Bytes.Element == UInt8 { 66 | var result = [UInt8]() 67 | let success = IDNA.toASCII(utf8: utf8) { result.append($0) } 68 | return success ? String(decoding: result, as: UTF8.self) : nil 69 | } 70 | 71 | // Reporting. 72 | 73 | public mutating func markSection(_ name: String) { 74 | report.markSection(name) 75 | } 76 | 77 | public mutating func reportTestResult(_ result: TestResult) { 78 | reportedResultCount += 1 79 | report.performTest { reporter in 80 | 81 | // From UTS46: 82 | // 83 | // > A conformance testing file (IdnaTestV2.txt) is provided for each version of Unicode 84 | // > starting with Unicode 6.0, in versioned directories under [IDNA-Table]. 85 | // > 👉 ** It only provides test cases for UseSTD3ASCIIRules=true. ** 👈 86 | // https://unicode.org/reports/tr46/#Conformance_Testing 87 | // 88 | // - The URL Standard uses UseSTD3ASCIIRules=false. 89 | // - We ^want^ to test UseSTD3ASCIIRules=false, because that's the code-path WebURL will actually take. 90 | // - Our implementation supports setting UseSTD3ASCIIRules=true, but only in toASCII, 91 | // and we might remove it one day. 92 | // 93 | // So how do we test UseSTD3ASCIIRules=false? 94 | // 95 | // The spec functions always produce an output string, sometimes with an accompanying set of errors. 96 | // Our implementation should always return the same string as the spec/test file, but where it expects us 97 | // to also return an STD3-related validation error, we won't return that error. 98 | // Result is the same; we're just more lenient about some particular scalars and don't consider them an "error". 99 | 100 | if std3Tests.contains(result.testNumber) { 101 | 102 | // 1. The testcase expects the function to produce a validation error 103 | // due to a disallowed codepoint in the input (P1/V6). 104 | let expectedFailures = UTS46Conformance.FailableValidationSteps.parse(result.testCase.toAsciiN.status) 105 | assert(expectedFailures.contains(where: { $0 == .P1 || $0 == .V6 })) 106 | 107 | // 2. But neither toUnicode nor toASCII actually produced an error for this input. 108 | assert(result.failures == [.toUnicode, .toAsciiN]) 109 | assert(result.captures!.toAsciiNResult != nil) 110 | assert(result.captures!.toUnicodeResult != nil) 111 | 112 | // 3. The output we produced matches what was expected, besides the lack of error. 113 | // And it should be an exact match - the same scalars, same code-units. Not just "canonically equivalent". 114 | assert(result.captures!.toUnicodeResult?.utf8.elementsEqual(result.testCase.toUnicode.result) == true) 115 | assert(result.captures!.toAsciiNResult?.utf8.elementsEqual(result.testCase.toAsciiN.result) == true) 116 | 117 | // 4. Also, toASCII with (beStrict = true) does indeed fail with an error. 118 | let successInStrictMode = IDNA.toASCII(utf8: result.testCase.source, beStrict: true) { _ in } 119 | assert(!successInStrictMode) 120 | 121 | // 5. Sure enough, if we examine the _expected_ toUnicode result, 122 | // we find a scalar which we have marked as `disallowed_STD3_valid`. 123 | let expectedToUnicode = String(decoding: result.testCase.toUnicode.result, as: UTF8.self) 124 | assert(expectedToUnicode.unicodeScalars.contains(where: { IDNA._SPIs._isDisallowed_STD3_valid($0) })) 125 | 126 | // X. If all of these checks passed, we appear to be producing the correct result 127 | // for an implementation where UseSTD3ASCIIRules=false. 128 | reporter.expectedResult = .fail 129 | } 130 | reporter.reportTestResult(result) 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /Sources/WebURLTestSupport/TestSuites/WPTConstructorTest+WebURLReportHarness.swift: -------------------------------------------------------------------------------- 1 | // Copyright The swift-url Contributors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import WebURL 16 | 17 | extension WPTConstructorTest { 18 | 19 | /// A harness for running a series of `WPTConstructorTest`s with the `WebURL` parser, 20 | /// and accumulating the results in a `SimpleTestReport`. 21 | /// 22 | public struct WebURLReportHarness { 23 | public private(set) var report = SimpleTestReport() 24 | public private(set) var reportedResultCount = 0 25 | public let expectedFailures: Set 26 | 27 | public init(expectedFailures: Set = []) { 28 | self.expectedFailures = expectedFailures 29 | } 30 | } 31 | } 32 | 33 | extension WPTConstructorTest.WebURLReportHarness: WPTConstructorTest.Harness { 34 | 35 | public func parseURL(_ input: String, base: String?) -> URLValues? { 36 | return WebURL.JSModel(input, base: base)?.urlValues 37 | } 38 | 39 | public mutating func markSection(_ name: String) { 40 | report.markSection(name) 41 | } 42 | 43 | public mutating func reportTestResult(_ result: TestResult) { 44 | reportedResultCount += 1 45 | report.performTest { reporter in 46 | if expectedFailures.contains(result.testNumber) { reporter.expectedResult = .fail } 47 | reporter.reportTestResult(result) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/WebURLTestSupport/TestSuites/WPTSetterTest+WebURLReportHarness.swift: -------------------------------------------------------------------------------- 1 | // Copyright The swift-url Contributors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import WebURL 16 | 17 | extension WPTSetterTest { 18 | 19 | /// A harness for running a series of WPT URL setter tets with the `WebURL` JS model and accumulating the results in a `SimpleTestReport`. 20 | /// 21 | public struct WebURLReportHarness { 22 | public private(set) var report = SimpleTestReport() 23 | public private(set) var reportedResultCount = 0 24 | 25 | public init() {} 26 | } 27 | } 28 | 29 | extension WPTSetterTest.WebURLReportHarness: WPTSetterTest.Harness { 30 | 31 | public typealias URLType = WebURL.JSModel 32 | 33 | public func parseURL(_ input: String) -> URLType? { 34 | WebURL(input)?.jsModel 35 | } 36 | 37 | public func setValue(_ newValue: String, forProperty property: URLModelProperty, on url: inout URLType) { 38 | switch property { 39 | case .href: 40 | url.href = newValue 41 | case .protocol: 42 | url.scheme = newValue 43 | case .username: 44 | url.username = newValue 45 | case .password: 46 | url.password = newValue 47 | case .hostname: 48 | url.hostname = newValue 49 | case .port: 50 | url.port = newValue 51 | case .pathname: 52 | url.pathname = newValue 53 | case .search: 54 | url.search = newValue 55 | case .hash: 56 | url.hash = newValue 57 | case .origin: 58 | assertionFailure("The URL Standard does not allow setting the origin directly") 59 | case .host: 60 | break // 'host' setter is not implemented. 61 | } 62 | } 63 | 64 | public func urlValues(_ url: URLType) -> URLValues { 65 | url.urlValues 66 | } 67 | 68 | public mutating func markSection(_ name: String) { 69 | report.markSection(name) 70 | } 71 | 72 | public mutating func reportTestResult(_ result: TestResult) { 73 | reportedResultCount += 1 74 | if case .host = result.testCase.property { 75 | // 'host' setter is not implemented. Can't be XFAIL-ed because some setters are no-ops and actually pass. 76 | report.skipTest() 77 | } else { 78 | report.performTest { reporter in reporter.reportTestResult(result) } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Sources/WebURLTestSupport/TestSuites/WPTToASCIITest+WebURLReportHarness.swift: -------------------------------------------------------------------------------- 1 | // Copyright The swift-url Contributors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import WebURL 16 | 17 | extension WPTToASCIITest { 18 | 19 | /// A harness for running a series of `WPTToASCIITest`s with the `WebURL` parser, 20 | /// and accumulating the results in a `SimpleTestReport`. 21 | /// 22 | public struct WebURLReportHarness { 23 | public private(set) var report = SimpleTestReport() 24 | public private(set) var reportedResultCount = 0 25 | public let expectedFailures: Set 26 | 27 | public init(expectedFailures: Set = []) { 28 | self.expectedFailures = expectedFailures 29 | } 30 | } 31 | } 32 | 33 | extension WPTToASCIITest.WebURLReportHarness: WPTToASCIITest.Harness { 34 | 35 | public typealias ParsedURL = WebURL 36 | 37 | public func getPropertyValues(_ url: WebURL) -> URLValues { 38 | url.jsModel.urlValues 39 | } 40 | 41 | public func parseURL(_ input: String) -> WebURL? { 42 | WebURL(input) 43 | } 44 | 45 | public func setHostname(_ url: inout WebURL, to newValue: String) { 46 | url.jsModel.hostname = newValue 47 | } 48 | 49 | public mutating func markSection(_ name: String) { 50 | report.markSection(name) 51 | } 52 | 53 | public mutating func reportTestResult(_ result: TestResult) { 54 | reportedResultCount += 1 55 | report.performTest { reporter in 56 | if expectedFailures.contains(result.testNumber) { reporter.expectedResult = .fail } 57 | reporter.reportTestResult(result) 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Sources/WebURLTestSupport/URLValues.swift: -------------------------------------------------------------------------------- 1 | // Copyright The swift-url Contributors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import WebURL 16 | 17 | /// A property exposed by the WHATWG URL model. 18 | /// 19 | public enum URLModelProperty: String, CaseIterable, Equatable, Hashable, CodingKey { 20 | case href = "href" 21 | case origin = "origin" 22 | case `protocol` = "protocol" 23 | case username = "username" 24 | case password = "password" 25 | case host = "host" 26 | case hostname = "hostname" 27 | case port = "port" 28 | case pathname = "pathname" 29 | case search = "search" 30 | case hash = "hash" 31 | 32 | public var name: String { 33 | stringValue 34 | } 35 | } 36 | 37 | /// A storage type for the properties exposed in the WHATWG URL model. 38 | /// 39 | public struct URLValues: Equatable, Hashable { 40 | private var href: String 41 | private var `protocol`: String 42 | private var username: String 43 | private var password: String 44 | private var host: String 45 | private var hostname: String 46 | private var port: String 47 | private var pathname: String 48 | private var search: String 49 | private var hash: String 50 | // Unfortunately, the WPT constructor tests often omit the origin 😐. 51 | // "The origin key may be missing. In that case, the API’s origin attribute is not tested." 52 | public var origin: String? 53 | 54 | public subscript(property: URLModelProperty) -> String? { 55 | get { 56 | switch property { 57 | case .href: return href 58 | case .origin: return origin 59 | case .protocol: return self.protocol 60 | case .username: return username 61 | case .password: return password 62 | case .host: return host 63 | case .hostname: return hostname 64 | case .port: return port 65 | case .pathname: return pathname 66 | case .search: return search 67 | case .hash: return hash 68 | } 69 | } 70 | } 71 | 72 | public init( 73 | href: String, origin: String?, protocol: String, username: String, password: String, host: String, 74 | hostname: String, port: String, pathname: String, search: String, hash: String 75 | ) { 76 | self.href = href 77 | self.origin = origin 78 | self.protocol = `protocol` 79 | self.username = username 80 | self.password = password 81 | self.host = host 82 | self.hostname = hostname 83 | self.port = port 84 | self.pathname = pathname 85 | self.search = search 86 | self.hash = hash 87 | } 88 | } 89 | 90 | extension URLValues: CustomStringConvertible { 91 | 92 | public var description: String { 93 | return """ 94 | { 95 | .href: \(href) 96 | .origin: \(origin ?? "") 97 | .protocol: \(`protocol`) 98 | .username: \(username) 99 | .password: \(password) 100 | .host: \(host) 101 | .hostname: \(hostname) 102 | .port: \(port) 103 | .pathname: \(pathname) 104 | .search: \(search) 105 | .hash: \(hash) 106 | } 107 | """ 108 | } 109 | } 110 | 111 | 112 | extension WebURL.JSModel { 113 | public var urlValues: URLValues { 114 | return .init( 115 | href: href, origin: origin, protocol: scheme, 116 | username: username, password: password, 117 | host: host, hostname: hostname, port: port, 118 | pathname: pathname, search: search, hash: hash 119 | ) 120 | } 121 | } 122 | 123 | 124 | extension URLValues { 125 | 126 | public static func diff(_ lhs: URLValues?, _ rhs: URLValues?) -> [URLModelProperty] { 127 | switch (lhs, rhs) { 128 | case (.none, .none): return [] 129 | case (.some, .none), (.none, .some): return URLModelProperty.allCases 130 | case (.some(let lhs), .some(let rhs)): return rhs.allMismatchingURLProperties(comparedWith: lhs) 131 | } 132 | } 133 | 134 | /// The properties which must always be present, and always be tested. 135 | private static var minimumPropertiesToDiff: [URLModelProperty] { 136 | return [ 137 | .href, 138 | .protocol, 139 | .username, .password, 140 | .hostname, .port, .host, 141 | .pathname, .search, .hash, 142 | ] 143 | } 144 | 145 | func allMismatchingURLProperties(comparedWith other: URLValues) -> [URLModelProperty] { 146 | var results = [URLModelProperty]() 147 | for property in Self.minimumPropertiesToDiff { 148 | if self[property] != other[property] { 149 | results.append(property) 150 | } 151 | } 152 | if let origin = self.origin, let otherOrigin = other.origin, origin != otherOrigin { 153 | results.append(.origin) 154 | } 155 | return results 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /Tests/IDNATests/UTS46ConformanceTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright The swift-url Contributors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import WebURLTestSupport 16 | import XCTest 17 | 18 | class UTS46ConformanceTests: ReportGeneratingTestCase {} 19 | 20 | extension UTS46ConformanceTests { 21 | 22 | func testConformance() throws { 23 | let lines = Array(try loadBinaryTestFile(.IdnaTest)).split(separator: UInt8(ascii: "\n")) 24 | var harness = WebURLTestSupport.UTS46Conformance.WebURLIDNAReportHarness(std3Tests: [ 25 | 26 | // From UTS46: 27 | // 28 | // > A conformance testing file (IdnaTestV2.txt) is provided for each version of Unicode 29 | // > starting with Unicode 6.0, in versioned directories under [IDNA-Table]. 30 | // > 👉 ** It only provides test cases for UseSTD3ASCIIRules=true. ** 👈 31 | // https://unicode.org/reports/tr46/#Conformance_Testing 32 | // 33 | // - The URL Standard uses UseSTD3ASCIIRules=false. 34 | // - We ^want^ to test UseSTD3ASCIIRules=false, because that's the code-path WebURL will actually take. 35 | // - Our implementation supports setting UseSTD3ASCIIRules=true, but only in toASCII, 36 | // and we might remove it one day. 37 | // 38 | // The test-cases listed here are all rely on UseSTD3ASCIIRules=true. 39 | // The harness puts them through some additional checks to make sure that we produce the correct result. 40 | 41 | 425, 461, 1756, 1817, 1818, 1819, 1890, 1891, 42 | 1892, 1893, 1894, 1895, 1896, 1897, 1898, 1899, 43 | 1920, 2216, 2356, 2529, 2852, 2853, 2854, 3029, 44 | 3030, 3031, 3032, 3033, 3034, 3035, 3036, 3037, 45 | 3038, 3039, 3040, 3041, 3042, 3043, 3044, 3045, 46 | 3046, 3047, 3082, 3083, 3084, 3085, 3086, 3283, 47 | 3284, 3285, 3286, 3287, 3288, 3289, 3290, 3291, 48 | 3292, 3293, 3294, 3295, 3296, 3297, 3298, 3299, 49 | 3300, 3550, 3678, 4002, 4003, 4006, 4007, 4011, 50 | 4012, 4578, 4774, 4775, 4776, 4777, 4778, 4990, 51 | 5226, 5227, 5228, 5325, 5326, 5327, 5328, 5329, 52 | 5330, 5331, 5332, 5333, 5334, 5335, 5336, 5337, 53 | 5338, 5339, 5340, 5341, 5342, 6120, 6122, 6125, 54 | 6126, 6128, 6130, 6133, 6135, 6220, 6224, 55 | ]) 56 | harness.runTests(lines) 57 | XCTAssertEqual(harness.reportedResultCount, 6235, "Unexpected number of tests executed.") 58 | XCTAssertFalse(harness.report.hasUnexpectedResults, "Test failed") 59 | 60 | let reportURL = fileURLForReport(named: "weburl-idna-uts46-tests") 61 | try harness.report.generateReport().write(to: reportURL, atomically: false, encoding: .utf8) 62 | print("✏️ Report written to: \(reportURL)") 63 | } 64 | } 65 | 66 | class ReportGeneratingTestCase: XCTestCase { 67 | 68 | private static let reportDir = ProcessInfo.processInfo.environment["SWIFT_URL_REPORT_PATH"] ?? NSTemporaryDirectory() 69 | 70 | override class func setUp() { 71 | try? FileManager.default.createDirectory(atPath: reportDir, withIntermediateDirectories: true, attributes: nil) 72 | } 73 | 74 | func fileURLForReport(named reportName: String) -> URL { 75 | URL(fileURLWithPath: ReportGeneratingTestCase.reportDir).appendingPathComponent(reportName) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Tests/IDNATests/Utils.swift: -------------------------------------------------------------------------------- 1 | // Copyright The swift-url Contributors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import XCTest 16 | 17 | /// Asserts that two sequences contain the same elements in the same order. 18 | /// 19 | func XCTAssertEqualElements( 20 | _ left: Left, _ right: Right, file: StaticString = #file, line: UInt = #line 21 | ) where Left.Element == Right.Element, Left.Element: Equatable { 22 | XCTAssertTrue(left.elementsEqual(right), file: file, line: line) 23 | } 24 | 25 | /// Asserts that two sequences contain the same elements in the same order. 26 | /// 27 | func XCTAssertEqualElements( 28 | _ left: Left?, _ right: Right, file: StaticString = #file, line: UInt = #line 29 | ) where Left.Element == Right.Element, Left.Element: Equatable { 30 | guard let left = left else { 31 | XCTFail("nil is not equal to \(right)", file: file, line: line) 32 | return 33 | } 34 | XCTAssertTrue(left.elementsEqual(right), file: file, line: line) 35 | } 36 | 37 | /// A String containing all 128 ASCII characters (`0..<128`), in order. 38 | /// 39 | let stringWithEveryASCIICharacter: String = { 40 | let asciiChars: Range = 0..<128 41 | let str = String(asciiChars.lazy.map { Character(UnicodeScalar($0)) }) 42 | precondition(str.utf8.elementsEqual(asciiChars)) 43 | return str 44 | }() 45 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | // Copyright The swift-url Contributors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #error(""" 16 | ----------------------------------------------------- 17 | Please test with `swift test --enable-test-discovery` 18 | ----------------------------------------------------- 19 | """) 20 | -------------------------------------------------------------------------------- /Tests/UnicodeDataStructuresTests/GenerateData/GenerateData.swift: -------------------------------------------------------------------------------- 1 | // Copyright The swift-url Contributors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | // ------------------------------------ 16 | // 17 | // This is the script which generates Sources/IDNA/Generated/{MappingData/ValidationData}.swift 18 | // It is written as a unit test, because for many reasons that's the easiest way to share code 19 | // between the build-time/run-time portions of the UnicodeDataStructures library. 20 | // 21 | // To regenerate the database, set the build-time condition in the test method to 'true' 22 | // and run the test. It'll write the files to a temporary folder and print the location to stdout. 23 | // 24 | // ------------------------------------ 25 | 26 | import Foundation 27 | import UnicodeDataStructures 28 | import XCTest 29 | 30 | fileprivate func URLOfUnicodeDataFile(named name: String) -> URL? { 31 | Bundle.module.url(forResource: "TableDefinitions/\(name)", withExtension: "txt") 32 | } 33 | 34 | fileprivate let SourceFileHeader = 35 | #""" 36 | // Copyright The swift-url Contributors. 37 | // 38 | // Licensed under the Apache License, Version 2.0 (the "License"); 39 | // you may not use this file except in compliance with the License. 40 | // You may obtain a copy of the License at 41 | // 42 | // http://www.apache.org/licenses/LICENSE-2.0 43 | // 44 | // Unless required by applicable law or agreed to in writing, software 45 | // distributed under the License is distributed on an "AS IS" BASIS, 46 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 47 | // See the License for the specific language governing permissions and 48 | // limitations under the License. 49 | 50 | // --------------------------------------------- 51 | // DO NOT MODIFY 52 | // --------------------------------------------- 53 | // Generated by WebURL/Tests/UnicodeDataStructureTests/GenerateData 54 | 55 | """# + "\n" 56 | 57 | final class GenerateData: XCTestCase {} 58 | 59 | extension GenerateData { 60 | 61 | func testGeneratingIDNAMappingAndValidationData() throws { 62 | 63 | #if false // Set to true to write the files to disk. 64 | 65 | let outputDir = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) 66 | .appendingPathComponent("IDNA", isDirectory: true) 67 | try FileManager.default.createDirectory(at: outputDir, withIntermediateDirectories: true, attributes: nil) 68 | 69 | func writeFile(name: String, contents: String) throws { 70 | let destination = outputDir.appendingPathComponent(name, isDirectory: false) 71 | try Data(contents.utf8).write(to: destination) 72 | print("✏️ Wrote file to: \(destination)") 73 | } 74 | 75 | #else 76 | 77 | func writeFile(name: String, contents: String) throws { 78 | // No-op. Just check that we can generate the file. 79 | } 80 | 81 | #endif 82 | 83 | // Mapping data. 84 | let mappingDataFile = try String(contentsOf: URLOfUnicodeDataFile(named: "IdnaMappingTable")!) 85 | let mappingDB = IDNAMappingDatabase(parsing: mappingDataFile) 86 | do { 87 | let swiftSourceFile = SourceFileHeader + mappingDB.printAsSwiftSourceCode(name: "_idna_map") 88 | try writeFile(name: "MappingData.swift", contents: swiftSourceFile) 89 | } 90 | 91 | // Validation data. 92 | do { 93 | let derivedBidiClassTxt = try String(contentsOf: URLOfUnicodeDataFile(named: "DerivedBidiClass")!) 94 | let derivedJoiningTypeTxt = try String(contentsOf: URLOfUnicodeDataFile(named: "DerivedJoiningType")!) 95 | let db = IDNAValidationDatabase( 96 | mappingDB: mappingDB, 97 | derivedBidiClassTxt: derivedBidiClassTxt, 98 | derivedJoiningTypeTxt: derivedJoiningTypeTxt 99 | ) 100 | let swiftSourceFile = SourceFileHeader + db.printAsSwiftSourceCode(name: "_idna_validate") 101 | try writeFile(name: "ValidationData.swift", contents: swiftSourceFile) 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Tests/UnicodeDataStructuresTests/GenerateData/TableDefinitions/.gitattributes: -------------------------------------------------------------------------------- 1 | # Do not interfere with line endings 2 | *.txt binary 3 | -------------------------------------------------------------------------------- /Tests/WebURLDeprecatedAPITests/DeprecatedAPITests.swift: -------------------------------------------------------------------------------- 1 | // Copyright The swift-url Contributors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // There is a separate test target for deprecated APIs, so we can test them 16 | // without adding a bunch of compile-time warnings to the regular test target. 17 | // Even if blank, it is useful to keep this file for history purposes, 18 | // so the test target and its folder don't keep being created and deleted all the time. 19 | 20 | // Currently, there are no deprecated APIs. 21 | -------------------------------------------------------------------------------- /Tests/WebURLFoundationExtrasTests/_ReportGeneratingTestCase.swift: -------------------------------------------------------------------------------- 1 | // Copyright The swift-url Contributors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Foundation 16 | import XCTest 17 | 18 | class ReportGeneratingTestCase: XCTestCase { 19 | 20 | private static let reportDir = ProcessInfo.processInfo.environment["SWIFT_URL_REPORT_PATH"] ?? NSTemporaryDirectory() 21 | 22 | override class func setUp() { 23 | try? FileManager.default.createDirectory(atPath: reportDir, withIntermediateDirectories: true, attributes: nil) 24 | } 25 | 26 | func fileURLForReport(named reportName: String) -> URL { 27 | URL(fileURLWithPath: ReportGeneratingTestCase.reportDir).appendingPathComponent(reportName) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Tests/WebURLSystemExtrasTests/Utils.swift: -------------------------------------------------------------------------------- 1 | // Copyright The swift-url Contributors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import XCTest 16 | 17 | @testable import WebURL 18 | 19 | /// Asserts that two sequences contain the same elements in the same order. 20 | /// 21 | func XCTAssertEqualElements( 22 | _ left: Left, _ right: Right, file: StaticString = #file, line: UInt = #line 23 | ) where Left.Element == Right.Element, Left.Element: Equatable { 24 | XCTAssertTrue(left.elementsEqual(right), file: file, line: line) 25 | } 26 | 27 | /// Asserts that a closure throws a particular error. 28 | /// 29 | func XCTAssertThrowsSpecific( 30 | _ expectedError: E, file: StaticString = #file, line: UInt = #line, _ body: () throws -> Void 31 | ) where E: Error, E: Equatable { 32 | do { 33 | try body() 34 | XCTFail("Expected an error to be thrown") 35 | } catch let error as E { 36 | XCTAssertEqual(error, expectedError) 37 | } catch { 38 | XCTFail("Unexpected error \(error)") 39 | } 40 | } 41 | 42 | 43 | // -------------------------------------------- 44 | // MARK: - WebURL test utilities 45 | // -------------------------------------------- 46 | 47 | 48 | /// Checks that the given URL returns precisely the same value when its serialized representation is re-parsed. 49 | /// 50 | func XCTAssertURLIsIdempotent(_ url: WebURL) { 51 | var serialized = url.serialized() 52 | serialized.makeContiguousUTF8() 53 | guard let reparsed = WebURL(serialized) else { 54 | XCTFail("Failed to reparse URL string: \(serialized)") 55 | return 56 | } 57 | // Check that the URLStructure (i.e. code-unit offsets, flags, etc) are the same. 58 | XCTAssertTrue(url.storage.structure.describesSameStructure(as: reparsed.storage.structure)) 59 | // Check that the code-units are the same. 60 | XCTAssertEqualElements(url.utf8, reparsed.utf8) 61 | // Triple check: check that the serialized representations are the same. 62 | XCTAssertEqual(serialized, reparsed.serialized()) 63 | } 64 | 65 | /// Checks the component values of the given URL. Any components not specified are checked to have a `nil` value. 66 | /// 67 | func XCTAssertURLComponents( 68 | _ url: WebURL, scheme: String, username: String? = nil, password: String? = nil, hostname: String? = nil, 69 | port: Int? = nil, path: String, query: String? = nil, fragment: String? = nil 70 | ) { 71 | XCTAssertEqual(url.scheme, scheme) 72 | XCTAssertEqual(url.username, username) 73 | XCTAssertEqual(url.password, password) 74 | XCTAssertEqual(url.hostname, hostname) 75 | XCTAssertEqual(url.port, port) 76 | XCTAssertEqual(url.path, path) 77 | XCTAssertEqual(url.query, query) 78 | XCTAssertEqual(url.fragment, fragment) 79 | } 80 | -------------------------------------------------------------------------------- /Tests/WebURLTests/OtherURLTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright The swift-url Contributors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import XCTest 16 | 17 | @testable import WebURL 18 | 19 | final class OtherURLTests: XCTestCase {} 20 | 21 | // This file contains tests which document "questionable behaviour" in the URL Standard. 22 | // Web compatibility means these things might never change, but it may also be worth raising them 23 | // with the WHATWG at some point. In any case, it's worth adding a test so we don't forget about them 24 | // and don't mistakenly think they are bugs with WebURL. 25 | 26 | extension OtherURLTests { 27 | 28 | func testPathSetterBackslash() { 29 | 30 | // Backslashes are not percent-encoded in non-special URLs, 31 | // so copying the path to a special URL can cause it to be interpreted differently. 32 | do { 33 | var specialURL = WebURL("https://example.com/")! 34 | XCTAssertEqual(specialURL.serialized(), "https://example.com/") 35 | XCTAssertEqual(specialURL.path, "/") 36 | XCTAssertEqual(specialURL.pathComponents.count, 1) 37 | 38 | let nonSpecialURL = WebURL(#"foo://example.com/baz\qux"#)! 39 | XCTAssertEqual(nonSpecialURL.serialized(), #"foo://example.com/baz\qux"#) 40 | XCTAssertEqual(nonSpecialURL.path, #"/baz\qux"#) 41 | XCTAssertEqual(nonSpecialURL.pathComponents.count, 1) 42 | 43 | specialURL.path = nonSpecialURL.path 44 | XCTAssertEqual(specialURL.serialized(), "https://example.com/baz/qux") 45 | XCTAssertEqual(specialURL.path, "/baz/qux") 46 | XCTAssertEqual(specialURL.pathComponents.count, 2) 47 | } 48 | 49 | // Our pathComponents view happens to work around this. 50 | // (covered already in PathComponentsTests.testReplaceSubrange_Slashes, .testAppendContents, etc) 51 | do { 52 | var specialURL = WebURL("https://example.com/")! 53 | XCTAssertEqual(specialURL.serialized(), "https://example.com/") 54 | XCTAssertEqual(specialURL.path, "/") 55 | XCTAssertEqual(specialURL.pathComponents.count, 1) 56 | 57 | let nonSpecialURL = WebURL(#"foo://example.com/baz\qux"#)! 58 | XCTAssertEqual(nonSpecialURL.serialized(), #"foo://example.com/baz\qux"#) 59 | XCTAssertEqual(nonSpecialURL.path, #"/baz\qux"#) 60 | XCTAssertEqual(nonSpecialURL.pathComponents.count, 1) 61 | 62 | specialURL.pathComponents.append(contentsOf: nonSpecialURL.pathComponents) 63 | XCTAssertEqual(specialURL.serialized(), "https://example.com/baz%5Cqux") 64 | XCTAssertEqual(specialURL.path, "/baz%5Cqux") 65 | XCTAssertEqual(specialURL.pathComponents.count, 1) 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Tests/WebURLTests/SchemeKindTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright The swift-url Contributors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import XCTest 16 | 17 | @testable import WebURL 18 | 19 | /// Tests for `WebURL.SchemeKind`. 20 | /// 21 | final class SchemeKindTests: XCTestCase { 22 | 23 | func testParser() { 24 | 25 | let testData: [(String, WebURL.SchemeKind, Bool)] = [ 26 | ("ftp", .ftp, true), 27 | ("file", .file, true), 28 | ("http", .http, true), 29 | ("https", .https, true), 30 | ("ws", .ws, true), 31 | ("wss", .wss, true), 32 | ("foo", .other, false), 33 | ("✌️", .other, false), 34 | ] 35 | for (name, expectedSchemeKind, expectedIsSpecial) in testData { 36 | XCTAssertEqual(WebURL.SchemeKind(parsing: name.utf8), expectedSchemeKind) 37 | XCTAssertEqual(expectedSchemeKind.isSpecial, expectedIsSpecial) 38 | // The parser should not allow any trailing content. 39 | let nameWithSchemeTerminator = name + ":" 40 | XCTAssertEqual(WebURL.SchemeKind(parsing: nameWithSchemeTerminator.utf8), .other) 41 | let nameWithASCII = name + "x" 42 | XCTAssertEqual(WebURL.SchemeKind(parsing: nameWithASCII.utf8), .other) 43 | let nameWithNonASCII = name + "✌️" 44 | XCTAssertEqual(WebURL.SchemeKind(parsing: nameWithNonASCII.utf8), .other) 45 | } 46 | XCTAssertEqual(WebURL.SchemeKind(parsing: "".utf8), .other) 47 | XCTAssertEqual(WebURL.SchemeKind(parsing: "\n".utf8), .other) 48 | } 49 | 50 | func testDefaultPorts() { 51 | 52 | let testData: [(WebURL.SchemeKind, UInt16?)] = [ 53 | (.ftp, 21), 54 | (.http, 80), 55 | (.https, 443), 56 | (.ws, 80), 57 | (.wss, 443), 58 | (.file, nil), 59 | (.other, nil), 60 | ] 61 | for (schemeKind, expectedDefaultPort) in testData { 62 | XCTAssertEqual(schemeKind.defaultPort, expectedDefaultPort) 63 | if let defaultPort = expectedDefaultPort { 64 | // isDefaultPortString should only return 'true' for the literal port number as an ASCII string. 65 | let serialized = String(defaultPort) 66 | XCTAssertTrue(schemeKind.isDefaultPort(utf8: serialized.utf8)) 67 | XCTAssertFalse(schemeKind.isDefaultPort(utf8: (":" + serialized).utf8)) 68 | XCTAssertFalse(schemeKind.isDefaultPort(utf8: (serialized + "0").utf8)) 69 | XCTAssertFalse(schemeKind.isDefaultPort(utf8: (serialized + "\n").utf8)) 70 | XCTAssertFalse(schemeKind.isDefaultPort(utf8: (serialized + "🦩").utf8)) 71 | // Schemes with default ports are special. 72 | XCTAssertTrue(schemeKind.isSpecial) 73 | } else { 74 | // If there is no default port, everything should return 'false'. 75 | XCTAssertFalse(schemeKind.isDefaultPort(utf8: "80".utf8)) 76 | XCTAssertFalse(schemeKind.isDefaultPort(utf8: ":80".utf8)) 77 | XCTAssertFalse(schemeKind.isDefaultPort(utf8: ":80\n".utf8)) 78 | XCTAssertFalse(schemeKind.isDefaultPort(utf8: "🦩".utf8)) 79 | } 80 | XCTAssertFalse(schemeKind.isDefaultPort(utf8: "".utf8)) 81 | XCTAssertFalse(schemeKind.isDefaultPort(utf8: "\n".utf8)) 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Tests/WebURLTests/WebPlatformTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright The swift-url Contributors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import WebURLTestSupport 16 | import XCTest 17 | 18 | @testable import WebURL 19 | 20 | final class WebPlatformTests: ReportGeneratingTestCase {} 21 | 22 | // -------------------------------------------- 23 | // MARK: - URL Constructor 24 | // -------------------------------------------- 25 | // https://github.com/web-platform-tests/wpt/blob/master/url/resources/urltestdata.json 26 | // at version 2a64dae4641fbd61bd4257df460e188f425b492e 27 | // Adjusted to remove an invalid surrogate pair which Foundation's JSON parser refuses to parse. 28 | 29 | 30 | extension WebPlatformTests { 31 | 32 | func testURLConstructor() throws { 33 | let testFile = try loadTestFile(.WPTURLConstructorTests, as: WPTConstructorTest.TestFile.self) 34 | assert( 35 | testFile.count == 839, 36 | "Incorrect number of test cases. If you updated the test list, be sure to update the expected failure indexes" 37 | ) 38 | 39 | var harness = WPTConstructorTest.WebURLReportHarness() 40 | harness.runTests(testFile) 41 | XCTAssertEqual(harness.reportedResultCount, 737, "Unexpected number of tests executed.") 42 | XCTAssertFalse(harness.report.hasUnexpectedResults, "Test failed") 43 | 44 | let reportURL = fileURLForReport(named: "weburl_constructor_wpt.txt") 45 | try harness.report.generateReport().write(to: reportURL, atomically: false, encoding: .utf8) 46 | print("ℹ️ Report written to \(reportURL)") 47 | } 48 | 49 | func testURLConstructor_additional() throws { 50 | let testFile = try loadTestFile(.WebURLAdditionalConstructorTests, as: WPTConstructorTest.TestFile.self) 51 | var harness = WPTConstructorTest.WebURLReportHarness() 52 | harness.runTests(testFile) 53 | XCTAssert(harness.reportedResultCount > 0, "Failed to execute any tests") 54 | XCTAssertFalse(harness.report.hasUnexpectedResults, "Test failed") 55 | 56 | let reportURL = fileURLForReport(named: "weburl_constructor_more.txt") 57 | try harness.report.generateReport().write(to: reportURL, atomically: false, encoding: .utf8) 58 | print("ℹ️ Report written to \(reportURL)") 59 | } 60 | } 61 | 62 | 63 | // -------------------------------------------- 64 | // MARK: - Setters 65 | // -------------------------------------------- 66 | // https://github.com/web-platform-tests/wpt/blob/master/url/resources/setters_tests.json 67 | // at version 77d54aa9e0405f737987b59331f3584e3e1c26f9 68 | 69 | 70 | extension WebPlatformTests { 71 | 72 | func testURLSetters() throws { 73 | let testFile = try loadTestFile(.WPTURLSetterTests, as: WPTSetterTest.TestFile.self) 74 | var harness = WPTSetterTest.WebURLReportHarness() 75 | harness.runTests(testFile) 76 | XCTAssert(harness.reportedResultCount > 0, "Failed to execute any tests") 77 | XCTAssertFalse(harness.report.hasUnexpectedResults, "Test failed") 78 | 79 | let reportURL = fileURLForReport(named: "weburl_setters_wpt.txt") 80 | try harness.report.generateReport().write(to: reportURL, atomically: false, encoding: .utf8) 81 | print("ℹ️ Report written to \(reportURL)") 82 | } 83 | 84 | func testURLSetters_additional() throws { 85 | let testFile = try loadTestFile(.WebURLAdditionalSetterTests, as: WPTSetterTest.TestFile.self) 86 | var harness = WPTSetterTest.WebURLReportHarness() 87 | harness.runTests(testFile) 88 | XCTAssert(harness.reportedResultCount > 0, "Failed to execute any tests") 89 | XCTAssertFalse(harness.report.hasUnexpectedResults, "Test failed") 90 | 91 | let reportURL = fileURLForReport(named: "weburl_setters_more.txt") 92 | try harness.report.generateReport().write(to: reportURL, atomically: false, encoding: .utf8) 93 | print("ℹ️ Report written to \(reportURL)") 94 | } 95 | } 96 | 97 | 98 | // -------------------------------------------- 99 | // MARK: - ToASCII 100 | // -------------------------------------------- 101 | // https://github.com/web-platform-tests/wpt/blob/master/url/resources/toascii.json 102 | // at version b772ca18865a09f3307440a9a756cb08fc0028a6 103 | // Adjusted to delete line 2 (the comment) 104 | 105 | 106 | extension WebPlatformTests { 107 | 108 | func testToASCII() throws { 109 | let testFile = try loadTestFile(.WPTToASCIITests, as: WPTToASCIITest.TestFile.self) 110 | assert( 111 | testFile.count == 39, 112 | "Incorrect number of test cases. If you updated the test list, be sure to update the expected failure indexes" 113 | ) 114 | 115 | var harness = WPTToASCIITest.WebURLReportHarness() 116 | harness.runTests(testFile) 117 | XCTAssertEqual(harness.reportedResultCount, 39, "Unexpected number of tests executed.") 118 | XCTAssertFalse(harness.report.hasUnexpectedResults, "Test failed") 119 | 120 | let reportURL = fileURLForReport(named: "weburl_toascii_wpt.txt") 121 | try harness.report.generateReport().write(to: reportURL, atomically: false, encoding: .utf8) 122 | print("ℹ️ Report written to \(reportURL)") 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /Tests/WebURLTests/_ReportGeneratingTestCase.swift: -------------------------------------------------------------------------------- 1 | // Copyright The swift-url Contributors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Foundation 16 | import XCTest 17 | 18 | class ReportGeneratingTestCase: XCTestCase { 19 | 20 | private static let reportDir = ProcessInfo.processInfo.environment["SWIFT_URL_REPORT_PATH"] ?? NSTemporaryDirectory() 21 | 22 | override class func setUp() { 23 | try? FileManager.default.createDirectory(atPath: reportDir, withIntermediateDirectories: true, attributes: nil) 24 | } 25 | 26 | func fileURLForReport(named reportName: String) -> URL { 27 | URL(fileURLWithPath: ReportGeneratingTestCase.reportDir).appendingPathComponent(reportName) 28 | } 29 | } 30 | --------------------------------------------------------------------------------