Repository: Comcast/ip4s Branch: main Commit: 64cc655f5d75 Files: 70 Total size: 224.8 KB Directory structure: gitextract_slxgux55/ ├── .git-blame-ignore-revs ├── .github/ │ └── workflows/ │ ├── ci.yml │ └── clean.yml ├── .gitignore ├── .sbtopts ├── .scalafmt.conf ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING ├── LICENSE ├── NOTICE ├── README.md ├── build.sbt ├── docs/ │ ├── guide.md │ └── src/ │ └── guide.md ├── js/ │ └── src/ │ └── main/ │ └── scala/ │ └── com/ │ └── comcast/ │ └── ip4s/ │ ├── DnsPlatform.scala │ ├── IpAddressPlatform.scala │ ├── NetworkInterfacePlatform.scala │ ├── NetworkInterfacesPlatform.scala │ ├── SocketAddressPlatform.scala │ ├── UnknownHostException.scala │ └── ip4splatform.scala ├── jvm-native/ │ └── src/ │ └── main/ │ └── scala/ │ └── com/ │ └── comcast/ │ └── ip4s/ │ ├── DnsPlatform.scala │ ├── IpAddressPlatform.scala │ ├── NetworkInterfacePlatform.scala │ ├── NetworkInterfacesPlatform.scala │ ├── SocketAddressPlatform.scala │ └── ip4splatform.scala ├── project/ │ ├── build.properties │ └── plugins.sbt ├── shared/ │ └── src/ │ └── main/ │ ├── scala/ │ │ └── com/ │ │ └── comcast/ │ │ └── ip4s/ │ │ ├── Cidr.scala │ │ ├── Dns.scala │ │ ├── GenSocketAddress.scala │ │ ├── Host.scala │ │ ├── MacAddress.scala │ │ ├── Multicast.scala │ │ ├── MulticastJoin.scala │ │ ├── MulticastSocketAddress.scala │ │ ├── NetworkInterface.scala │ │ ├── NetworkInterfaces.scala │ │ ├── Port.scala │ │ ├── SocketAddress.scala │ │ └── UnixSocketAddress.scala │ ├── scala-2/ │ │ ├── Literals.scala │ │ └── package.scala │ ├── scala-2.12/ │ │ └── com/ │ │ └── comcast/ │ │ └── ip4s/ │ │ └── CollectionCompat.scala │ ├── scala-2.13/ │ │ └── com/ │ │ └── comcast/ │ │ └── ip4s/ │ │ └── CollectionCompat.scala │ └── scala-3/ │ ├── Literals.scala │ ├── com/ │ │ └── comcast/ │ │ └── ip4s/ │ │ └── CollectionCompat.scala │ └── package.scala └── test-kit/ ├── js/ │ └── src/ │ └── test/ │ └── scala/ │ └── com/ │ └── comcast/ │ └── ip4s/ │ └── TestPlatform.scala ├── jvm/ │ └── src/ │ └── test/ │ └── scala/ │ └── com/ │ └── comcast/ │ └── ip4s/ │ ├── Ipv6AddressJvmTest.scala │ └── TestPlatform.scala ├── native/ │ └── src/ │ └── test/ │ └── scala/ │ └── com/ │ └── comcast/ │ └── ip4s/ │ └── TestPlatform.scala └── shared/ └── src/ ├── main/ │ └── scala/ │ └── com/ │ └── comcast/ │ └── ip4s/ │ └── Arbitraries.scala └── test/ └── scala/ └── com/ └── comcast/ └── ip4s/ ├── BaseTestSuite.scala ├── CidrStrictTest.scala ├── CidrTest.scala ├── DnsTest.scala ├── Examples.scala ├── HostnameTest.scala ├── IDNTest.scala ├── IdnaSuite.scala ├── Ipv4AddressTest.scala ├── Ipv6AddressTest.scala ├── MacAddressTest.scala ├── MulticastSocketAddressTest.scala ├── MulticastTest.scala ├── NetworkInterfacesTest.scala ├── PortTest.scala └── SocketAddressTest.scala ================================================ FILE CONTENTS ================================================ ================================================ FILE: .git-blame-ignore-revs ================================================ # Scala Steward: Reformat with scalafmt 3.9.7 5ccb66cf1c2c22e7c4aeb4c73fec92b874e6d296 ================================================ FILE: .github/workflows/ci.yml ================================================ # This file was automatically generated by sbt-github-actions using the # githubWorkflowGenerate task. You should add and commit this file to # your git repository. It goes without saying that you shouldn't edit # this file by hand! Instead, if you wish to make changes, you should # change your sbt build configuration to revise the workflow description # to meet your needs, then regenerate this file. name: Continuous Integration on: pull_request: branches: ['**', '!update/**', '!pr/**'] push: branches: ['**', '!update/**', '!pr/**'] tags: [v*] env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} concurrency: group: ${{ github.workflow }} @ ${{ github.ref }} cancel-in-progress: true jobs: build: name: Test strategy: matrix: os: [ubuntu-22.04] scala: [2.12, 2.13, 3] java: [temurin@8] project: [rootJS, rootJVM, rootNative] runs-on: ${{ matrix.os }} timeout-minutes: 60 steps: - name: Checkout current branch (full) uses: actions/checkout@v6 with: fetch-depth: 0 - name: Setup sbt uses: sbt/setup-sbt@v1 - name: Setup Java (temurin@8) id: setup-java-temurin-8 if: matrix.java == 'temurin@8' uses: actions/setup-java@v5 with: distribution: temurin java-version: 8 cache: sbt - name: sbt update if: matrix.java == 'temurin@8' && steps.setup-java-temurin-8.outputs.cache-hit == 'false' run: sbt +update - name: Check that workflows are up to date run: sbt githubWorkflowCheck - name: Check headers and formatting if: matrix.java == 'temurin@8' && matrix.os == 'ubuntu-22.04' run: sbt 'project ${{ matrix.project }}' '++ ${{ matrix.scala }}' headerCheckAll scalafmtCheckAll 'project /' scalafmtSbtCheck - name: scalaJSLink if: matrix.project == 'rootJS' run: sbt 'project ${{ matrix.project }}' '++ ${{ matrix.scala }}' Test/scalaJSLinkerResult - name: nativeLink if: matrix.project == 'rootNative' run: sbt 'project ${{ matrix.project }}' '++ ${{ matrix.scala }}' Test/nativeLink - name: Test run: sbt 'project ${{ matrix.project }}' '++ ${{ matrix.scala }}' test - name: Check binary compatibility if: matrix.java == 'temurin@8' && matrix.os == 'ubuntu-22.04' run: sbt 'project ${{ matrix.project }}' '++ ${{ matrix.scala }}' mimaReportBinaryIssues - name: Generate API documentation if: matrix.java == 'temurin@8' && matrix.os == 'ubuntu-22.04' run: sbt 'project ${{ matrix.project }}' '++ ${{ matrix.scala }}' doc - name: Make target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') run: mkdir -p native/target test-kit/js/target js/target test-kit/native/target test-kit/jvm/target jvm/target project/target - name: Compress target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') run: tar cf targets.tar native/target test-kit/js/target js/target test-kit/native/target test-kit/jvm/target jvm/target project/target - name: Upload target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') uses: actions/upload-artifact@v5 with: name: target-${{ matrix.os }}-${{ matrix.java }}-${{ matrix.scala }}-${{ matrix.project }} path: targets.tar publish: name: Publish Artifacts needs: [build] if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') strategy: matrix: os: [ubuntu-22.04] java: [temurin@8] runs-on: ${{ matrix.os }} steps: - name: Checkout current branch (full) uses: actions/checkout@v6 with: fetch-depth: 0 - name: Setup sbt uses: sbt/setup-sbt@v1 - name: Setup Java (temurin@8) id: setup-java-temurin-8 if: matrix.java == 'temurin@8' uses: actions/setup-java@v5 with: distribution: temurin java-version: 8 cache: sbt - name: sbt update if: matrix.java == 'temurin@8' && steps.setup-java-temurin-8.outputs.cache-hit == 'false' run: sbt +update - name: Download target directories (2.12, rootJS) uses: actions/download-artifact@v6 with: name: target-${{ matrix.os }}-${{ matrix.java }}-2.12-rootJS - name: Inflate target directories (2.12, rootJS) run: | tar xf targets.tar rm targets.tar - name: Download target directories (2.12, rootJVM) uses: actions/download-artifact@v6 with: name: target-${{ matrix.os }}-${{ matrix.java }}-2.12-rootJVM - name: Inflate target directories (2.12, rootJVM) run: | tar xf targets.tar rm targets.tar - name: Download target directories (2.12, rootNative) uses: actions/download-artifact@v6 with: name: target-${{ matrix.os }}-${{ matrix.java }}-2.12-rootNative - name: Inflate target directories (2.12, rootNative) run: | tar xf targets.tar rm targets.tar - name: Download target directories (2.13, rootJS) uses: actions/download-artifact@v6 with: name: target-${{ matrix.os }}-${{ matrix.java }}-2.13-rootJS - name: Inflate target directories (2.13, rootJS) run: | tar xf targets.tar rm targets.tar - name: Download target directories (2.13, rootJVM) uses: actions/download-artifact@v6 with: name: target-${{ matrix.os }}-${{ matrix.java }}-2.13-rootJVM - name: Inflate target directories (2.13, rootJVM) run: | tar xf targets.tar rm targets.tar - name: Download target directories (2.13, rootNative) uses: actions/download-artifact@v6 with: name: target-${{ matrix.os }}-${{ matrix.java }}-2.13-rootNative - name: Inflate target directories (2.13, rootNative) run: | tar xf targets.tar rm targets.tar - name: Download target directories (3, rootJS) uses: actions/download-artifact@v6 with: name: target-${{ matrix.os }}-${{ matrix.java }}-3-rootJS - name: Inflate target directories (3, rootJS) run: | tar xf targets.tar rm targets.tar - name: Download target directories (3, rootJVM) uses: actions/download-artifact@v6 with: name: target-${{ matrix.os }}-${{ matrix.java }}-3-rootJVM - name: Inflate target directories (3, rootJVM) run: | tar xf targets.tar rm targets.tar - name: Download target directories (3, rootNative) uses: actions/download-artifact@v6 with: name: target-${{ matrix.os }}-${{ matrix.java }}-3-rootNative - name: Inflate target directories (3, rootNative) run: | tar xf targets.tar rm targets.tar - name: Import signing key if: env.PGP_SECRET != '' && env.PGP_PASSPHRASE == '' env: PGP_SECRET: ${{ secrets.PGP_SECRET }} PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} run: echo $PGP_SECRET | base64 -d -i - | gpg --import - name: Import signing key and strip passphrase if: env.PGP_SECRET != '' && env.PGP_PASSPHRASE != '' env: PGP_SECRET: ${{ secrets.PGP_SECRET }} PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} run: | echo "$PGP_SECRET" | base64 -d -i - > /tmp/signing-key.gpg echo "$PGP_PASSPHRASE" | gpg --pinentry-mode loopback --passphrase-fd 0 --import /tmp/signing-key.gpg (echo "$PGP_PASSPHRASE"; echo; echo) | gpg --command-fd 0 --pinentry-mode loopback --change-passphrase $(gpg --list-secret-keys --with-colons 2> /dev/null | grep '^sec:' | cut --delimiter ':' --fields 5 | tail -n 1) - name: Publish env: SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} SONATYPE_CREDENTIAL_HOST: ${{ secrets.SONATYPE_CREDENTIAL_HOST }} run: sbt tlCiRelease dependency-submission: name: Submit Dependencies if: github.event.repository.fork == false && github.event_name != 'pull_request' strategy: matrix: os: [ubuntu-22.04] java: [temurin@8] runs-on: ${{ matrix.os }} steps: - name: Checkout current branch (full) uses: actions/checkout@v6 with: fetch-depth: 0 - name: Setup sbt uses: sbt/setup-sbt@v1 - name: Setup Java (temurin@8) id: setup-java-temurin-8 if: matrix.java == 'temurin@8' uses: actions/setup-java@v5 with: distribution: temurin java-version: 8 cache: sbt - name: sbt update if: matrix.java == 'temurin@8' && steps.setup-java-temurin-8.outputs.cache-hit == 'false' run: sbt +update - name: Submit Dependencies uses: scalacenter/sbt-dependency-submission@v2 with: modules-ignore: rootjs_2.12 rootjs_2.13 rootjs_3 rootjvm_2.12 rootjvm_2.13 rootjvm_3 rootnative_2.12 rootnative_2.13 rootnative_3 configs-ignore: test scala-tool scala-doc-tool test-internal ================================================ FILE: .github/workflows/clean.yml ================================================ # This file was automatically generated by sbt-github-actions using the # githubWorkflowGenerate task. You should add and commit this file to # your git repository. It goes without saying that you shouldn't edit # this file by hand! Instead, if you wish to make changes, you should # change your sbt build configuration to revise the workflow description # to meet your needs, then regenerate this file. name: Clean on: push jobs: delete-artifacts: name: Delete Artifacts runs-on: ubuntu-latest env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Delete artifacts run: | # Customize those three lines with your repository and credentials: REPO=${GITHUB_API_URL}/repos/${{ github.repository }} # A shortcut to call GitHub API. ghapi() { curl --silent --location --user _:$GITHUB_TOKEN "$@"; } # A temporary file which receives HTTP response headers. TMPFILE=/tmp/tmp.$$ # An associative array, key: artifact name, value: number of artifacts of that name. declare -A ARTCOUNT # Process all artifacts on this repository, loop on returned "pages". URL=$REPO/actions/artifacts while [[ -n "$URL" ]]; do # Get current page, get response headers in a temporary file. JSON=$(ghapi --dump-header $TMPFILE "$URL") # Get URL of next page. Will be empty if we are at the last page. URL=$(grep '^Link:' "$TMPFILE" | tr ',' '\n' | grep 'rel="next"' | head -1 | sed -e 's/.*.*//') rm -f $TMPFILE # Number of artifacts on this page: COUNT=$(( $(jq <<<$JSON -r '.artifacts | length') )) # Loop on all artifacts on this page. for ((i=0; $i < $COUNT; i++)); do # Get name of artifact and count instances of this name. name=$(jq <<<$JSON -r ".artifacts[$i].name?") ARTCOUNT[$name]=$(( $(( ${ARTCOUNT[$name]} )) + 1)) id=$(jq <<<$JSON -r ".artifacts[$i].id?") size=$(( $(jq <<<$JSON -r ".artifacts[$i].size_in_bytes?") )) printf "Deleting '%s' #%d, %'d bytes\n" $name ${ARTCOUNT[$name]} $size ghapi -X DELETE $REPO/actions/artifacts/$id done done ================================================ FILE: .gitignore ================================================ target .classpath .project .settings/ .worksheet/ .cache .tags .idea .idea_modules/ .DS_Store *.swp .metaserver .bloop .metals .vscode project/metals.sbt project/project .bsp ================================================ FILE: .sbtopts ================================================ -J-Xms2g -J-Xmx4g -J-XX:MaxMetaspaceSize=512m ================================================ FILE: .scalafmt.conf ================================================ version = "3.10.7" maxColumn: 120 runner.dialect = scala213source3 project.layout = StandardConvention rewrite.scala3.convertToNewSyntax = true rewrite.scala3.newSyntax.control = false runner.dialectOverride.allowUnderscoreAsTypePlaceholder = false runner.dialectOverride.allowSignificantIndentation = false runner.dialectOverride.allowStarWildcardImport = false docstrings.wrap = "no" ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Code of Conduct We are committed to providing a friendly, safe and welcoming environment for all, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, nationality, or other such characteristics. Everyone is expected to follow the [Scala Code of Conduct] when discussing the project on the available communication channels. If you are being harassed, please contact us immediately so that we can support you. ## Moderation Any questions, concerns, or moderation requests please contact a member of the project. - [Michael Pilquist](mailto:mpilquist@gmail.com) [Scala Code of Conduct]: https://www.scala-lang.org/conduct/ ================================================ FILE: CONTRIBUTING ================================================ If you would like to contribute code to this project you can do so through GitHub by forking the repository and sending a pull request. Before Comcast merges your code into the project you must sign the [Comcast Contributor License Agreement (CLA)](https://gist.github.com/ComcastOSS/a7b8933dd8e368535378cda25c92d19a). If you haven't previously signed a Comcast CLA, you'll automatically be asked to when you open a pull request. Alternatively, we can send you a PDF that you can sign and scan back to us. Please create a new GitHub issue to request a PDF version of the CLA. ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: NOTICE ================================================ IP Addresses for Scala and Scala.js Copyright 2018 Comcast Cable Communications Management, LLC Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. This product includes software developed at Comcast (http://www.comcast.com/). This product includes a dependency on and sources derived from [punycode.js](https://github.com/mathiasbynens/punycode.js), which is available under the following MIT license. ``` Copyright Mathias Bynens Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` This product includes sources derived from [libuv](https://github.com/libuv/libuv/), which is available under the following license. ``` Copyright (c) 2015-present libuv project contributors. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` ================================================ FILE: README.md ================================================ [![Published Artifact](https://img.shields.io/maven-central/v/com.comcast/ip4s-core_3.svg)](http://search.maven.org/#search%7Cga%7C1%7Cip4s-core_3) [![javadoc](https://javadoc.io/badge2/com.comcast/ip4s-core_3/javadoc.svg)](https://javadoc.io/doc/com.comcast/ip4s-core_3) ip4s: IP Addresses for Scala, Scala.js & Scala Native ===================================================== This project defines immutable, safe data structures for describing IP addresses, multicast joins, socket addresses and similar IP & network related data types. There are two defining characteristics of this project that make it different from other similar projects: - all data types are immutable and every function/method is referentially transparent (e.g., no accidental DNS lookups by calling `InetAddress.getByName(...)`) - published for Scala, Scala.js and Scala Native See the [guide](docs/guide.md) and [ScalaDoc](https://javadoc.io/doc/com.comcast/ip4s-core_3) for more details. ## Getting Binaries This library is published on Maven Central under group id `com.comcast` and artifact id `ip4s-core_${scalaBinaryVersion}`. Add the following to your SBT build: ```scala libraryDependencies += "com.comcast" %% "ip4s-core" % "version" ``` ## Interop As of 1.4, ip4s depends on cats and provides type class instances directly in data type companion objects. For Scalaz support, we recommend [shims](https://github.com/djspiewak/shims). ## Copyright and License This project is made available under the [Apache License, Version 2.0](LICENSE). Copyright information can be found in [NOTICE](NOTICE). ## Code of Conduct See the [Code of Conduct](CODE_OF_CONDUCT.md). ================================================ FILE: build.sbt ================================================ import com.typesafe.tools.mima.core._ ThisBuild / tlBaseVersion := "3.7" ThisBuild / organization := "com.comcast" ThisBuild / organizationName := "Comcast Cable Communications Management, LLC" ThisBuild / startYear := Some(2018) ThisBuild / developers ++= List( tlGitHubDev("mpilquist", "Michael Pilquist"), tlGitHubDev("matthughes", "Matt Hughes"), tlGitHubDev("nequissimus", "Tim Steinbach") ) ThisBuild / githubWorkflowJavaVersions := Seq(JavaSpec.temurin("8")) ThisBuild / crossScalaVersions := List("2.12.21", "2.13.18", "3.3.7") ThisBuild / tlVersionIntroduced := Map("3" -> "3.0.3") ThisBuild / doctestTestFramework := DoctestTestFramework.ScalaCheck ThisBuild / initialCommands := "import com.comcast.ip4s._" ThisBuild / mimaBinaryIssueFilters ++= Seq( ProblemFilters.exclude[ReversedMissingMethodProblem]("com.comcast.ip4s.IpAddress.toDefaultString"), // #553 ProblemFilters.exclude[DirectMissingMethodProblem]("com.comcast.ip4s.Ipv6Address.toInetAddress"), ProblemFilters.exclude[ReversedMissingMethodProblem]("com.comcast.ip4s.Dns.*"), // sealed trait // Scala 3 (erroneously?) considered Multicast/SourceSpecificMulticast as sum types ProblemFilters.exclude[DirectMissingMethodProblem]("com.comcast.ip4s.Multicast.ordinal"), ProblemFilters.exclude[MissingTypesProblem]("com.comcast.ip4s.Multicast$"), ProblemFilters.exclude[DirectMissingMethodProblem]("com.comcast.ip4s.SourceSpecificMulticast.ordinal"), ProblemFilters.exclude[MissingTypesProblem]("com.comcast.ip4s.SourceSpecificMulticast$"), ProblemFilters.exclude[ReversedMissingMethodProblem]("com.comcast.ip4s.IpAddress.isPrivate"), // #562 ProblemFilters.exclude[ReversedMissingMethodProblem]("com.comcast.ip4s.IpAddress.isLoopback"), ProblemFilters.exclude[ReversedMissingMethodProblem]("com.comcast.ip4s.IpAddress.isLinkLocal"), // Removed JS-specifc Punycode bindings ProblemFilters.exclude[MissingClassProblem]("com.comcast.ip4s.Punycode"), ProblemFilters.exclude[MissingClassProblem]("com.comcast.ip4s.Punycode$"), ProblemFilters.exclude[MissingTypesProblem]("com.comcast.ip4s.IDN$"), ProblemFilters.exclude[MissingClassProblem]("com.comcast.ip4s.IDNCompanionPlatform") ) lazy val root = tlCrossRootProject.aggregate(core, testKit) lazy val testKit = crossProject(JVMPlatform, JSPlatform, NativePlatform) .in(file("./test-kit")) .settings(commonSettings) .settings( name := "ip4s-test-kit" ) .settings(mimaPreviousArtifacts := Set.empty) .settings( libraryDependencies ++= Seq( "org.scalacheck" %%% "scalacheck" % "1.19.0", "org.scalameta" %%% "munit-scalacheck" % "1.2.0" % Test, "org.typelevel" %%% "munit-cats-effect" % "2.2.0" % Test ) ) .jvmSettings( libraryDependencies += "com.google.guava" % "guava" % "33.5.0-jre" % "test" ) .dependsOn(core % "compile->compile") lazy val testKitJVM = testKit.jvm lazy val testKitJS = testKit.js .disablePlugins(DoctestPlugin) .enablePlugins(ScalaJSBundlerPlugin) lazy val testKitNative = testKit.js .disablePlugins(DoctestPlugin) .settings(commonNativeSettings) lazy val core = crossProject(JVMPlatform, JSPlatform, NativePlatform) .in(file(".")) .settings(commonSettings) .settings( name := "ip4s-core", libraryDependencies ++= { if (tlIsScala3.value) Nil else List("org.scala-lang" % "scala-reflect" % scalaVersion.value % "provided") }, scalacOptions := scalacOptions.value.filterNot(_ == "-source:3.0-migration"), Compile / doc / scalacOptions ++= (if (scalaVersion.value.startsWith("2.")) Seq("-nowarn") else Nil) ) .settings( libraryDependencies ++= Seq( "org.typelevel" %%% "literally" % "1.2.0", "org.typelevel" %%% "cats-core" % "2.13.0", "org.typelevel" %%% "cats-effect" % "3.7.0", "org.typelevel" %%% "idna4s-core" % "0.1.0", "org.scalacheck" %%% "scalacheck" % "1.19.0" % Test ) ) lazy val coreJVM = core.jvm.settings( doctestIgnoreRegex := Some(".*Literals.scala") ) lazy val coreJS = core.js .disablePlugins(DoctestPlugin) .enablePlugins(ScalaJSBundlerPlugin) .settings( scalaJSLinkerConfig ~= (_.withModuleKind(ModuleKind.CommonJSModule)) ) lazy val coreNative = core.native .disablePlugins(DoctestPlugin) .settings(commonNativeSettings) lazy val docs = project .in(file("docs")) .enablePlugins(MdocPlugin) .dependsOn(coreJVM) .settings( mdocIn := baseDirectory.value / "src", mdocOut := baseDirectory.value / "../docs", githubWorkflowArtifactUpload := false, libraryDependencies += "org.typelevel" %%% "cats-effect" % "3.7.0" ) lazy val commonSettings = Seq( Compile / unmanagedResources ++= { val base = baseDirectory.value / ".." (base / "NOTICE") +: (base / "LICENSE") +: (base / "CONTRIBUTING") +: ((base / "licenses") * "LICENSE_*").get } ) lazy val commonNativeSettings = Seq( tlVersionIntroduced := List("2.12", "2.13", "3").map(_ -> "3.8.0").toMap ) ================================================ FILE: docs/guide.md ================================================ ip4s: IP Addresses for Scala & Scala.js ======================================= This is the guide for IP Addresses for Scala & Scala.js. This library provides the package `com.comcast.ip4s`, which contains all types. It is a small package and it is often useful to import all of its contents via `import com.comcast.ip4s._` -- doing so will enable some syntax. # IP Addresses The `IpAddress` type represents either an IPv4 address or an IPv6 address. The primary mechanism to construct an `IpAddress` is `IpAddress.fromString`, which converts a string to an `Option[IpAddress]`. You can also construct an `IpAddress` from a byte array of either 4 bytes or 16 bytes. ```scala import com.comcast.ip4s.IpAddress val home = IpAddress.fromString("127.0.0.1") // home: Option[IpAddress] = Some(127.0.0.1) val home6 = IpAddress.fromString("::1") // home6: Option[IpAddress] = Some(::1) ``` The `toString` method on `IpAddress` renders the IP in dotted-decimal notation if it is a V4 address and condensed string notation if it is a V6 address. The `toBytes` method converts the IP into a 4 or 16 element byte array. There are a few more methods on `IpAddress` that we'll look at later but not many more -- the API is small. Sometimes it is useful to explicitly require an IPv4 or IPv6 address -- for example, when modelling the configuration of a device that requires an IPv6 address. This can be accomplished by using the `Ipv4Address` or `Ipv6Address` types, both of which are subtypes of `IpAddress`. We can construct these types directly via methods on their companions: ```scala import com.comcast.ip4s.{Ipv4Address, Ipv6Address} val explicitV4Home = Ipv4Address.fromString("127.0.0.1") // explicitV4Home: Option[Ipv4Address] = Some(127.0.0.1) val explicitV6Home = Ipv6Address.fromString("::1") // explicitV6Home: Option[Ipv6Address] = Some(::1) ``` Because `Ipv4Address` and `Ipv6Address` are subtypes of `IpAddress`, we can pattern match on an `IpAddress` or use the `fold` method: ```scala import com.comcast.ip4s.{Ipv4Address, Ipv6Address} val homeIsV4 = home.get match { case _: Ipv4Address => true case _: Ipv6Address => false } // homeIsV4: Boolean = true val home6IsV4 = home6.get.fold(_ => true, _ => false) // home6IsV4: Boolean = false ``` ## IP Literals In the previous examples, all of the IP addresses were wrapped in an `Option`. When a string is statically (i.e. at compile time) known to be a valid IP address, we can avoid the `Option` entirely by using IP address string interpolators. ```scala import com.comcast.ip4s._ val home = ip"127.0.0.1" // home: IpAddress = 127.0.0.1 val home4 = ipv4"127.0.0.1" // home4: Ipv4Address = 127.0.0.1 val home6 = ipv6"::1" // home6: Ipv6Address = ::1 ``` The `ip` interpolator returns an `IpAddress`, the `ipv4` interpolator returns an `Ipv4Address`, and the `ipv6` interpolator returns an `Ipv6Address`. If the string is not a valid IP of the requested type, the expression will fail to compile. ## IPv6 String Formats IPv6 addresses have a number of special string formats. The default format (what's returned by `toString`) adheres to [RFC5952](https://tools.ietf.org/html/rfc5952) -- e.g., maximal use of `::` to condense string length. If instead you want a string that does not use `::` and expresses each hextet as 4 characters, call `.toUncondensedString`. Note that the `toString` method never outputs a mixed string consisting of both V6 hextets and a dotted decimal V4 address. For example, the address consisting of 12 0 bytes followed by 127, 0, 0, 1 is rendered as `::7f00:1` instead of `::127.0.0.1`. `Ipv6Address.fromString` and `IpAddress.fromString` can parse all of these formats. ```scala import com.comcast.ip4s._ val home = ipv6"::7f00:1" // home: Ipv6Address = ::7f00:1 val homeLong = home.toUncondensedString // homeLong: String = 0000:0000:0000:0000:0000:0000:7f00:0001 val homeMixed = home.toMixedString // homeMixed: String = ::127.0.0.1 val parsedHomeLong = Ipv6Address.fromString(homeLong) // parsedHomeLong: Option[Ipv6Address] = Some(::7f00:1) val parsedHomeMixed = Ipv6Address.fromString(homeMixed) // parsedHomeMixed: Option[Ipv6Address] = Some(::7f00:1) ``` ## Ordering IP addresses have a defined ordering, making them sortable. Note when comparing an IPv4 address to an IPv6 address, the V4 address is converted to a V6 address by left padding with 0 bytes (aka, a "compatible" V4-in-V6 address). ```scala val ips = List(ipv4"10.1.1.1", ipv4"10.1.2.0", ipv4"10.1.0.0", ipv6"::1", ipv6"ff3b::") // ips: List[IpAddress] = List(10.1.1.1, 10.1.2.0, 10.1.0.0, ::1, ff3b::) val sorted = ips.sorted // sorted: List[IpAddress] = List(::1, 10.1.0.0, 10.1.1.1, 10.1.2.0, ff3b::) ``` ## JVM Integration When compiling for the JVM, the various IP address classes have a `toInetAddress` method which returns a `java.net.InetAddress`, allowing easy integration with libraries that use `InetAddress`. ```scala val homeIA = ip"127.0.0.1".toInetAddress // homeIA: InetAddress = /127.0.0.1 val home4IA = ipv4"127.0.0.1".toInetAddress // home4IA: Inet4Address = /127.0.0.1 val home6IA = ipv6"::1".toInetAddress // home6IA: InetAddress = /0:0:0:0:0:0:0:1 ``` # Multicast Both IPv4 and IPv6 have reserved address ranges for multicast and smaller reserved ranges for source specific multicast. The address types in this library are aware of these ranges: ```scala val ips = List(ip"127.0.0.1", ip"224.10.10.10", ip"232.11.11.11", ip"::1", ip"ff00::10", ip"ff3b::11") // ips: List[IpAddress] = List(127.0.0.1, 224.10.10.10, 232.11.11.11, ::1, ff00::10, ff3b::11) val multicastIps = ips.filter(_.isMulticast) // multicastIps: List[IpAddress] = List(224.10.10.10, 232.11.11.11, ff00::10, ff3b::11) val ssmIps = ips.filter(_.isSourceSpecificMulticast) // ssmIps: List[IpAddress] = List(232.11.11.11, ff3b::11) ``` ## Multicast Witnesses Often, especially when modelling configuration of systems, you need a type that indicates an address is a valid multicast or source specific multicast address. That's provided by the `Multicast` and `SourceSpecificMulticast` types. These roughly look like: ```scala sealed trait Multicast[A <: IpAddress] { def address: A } sealed trait SourceSpecificMulticast[A <: IpAddress] extends Multicast[A] ``` These wrappers serve as type level witnesses that the wrapped address is a valid multicast / source specific multicast address. These types are parameterized by the type of IP address -- either `IpAddress`, `Ipv4Address`, or `Ipv6Address`, to allow for accurate domain modeling at the type level. For example, if an application only supported IPv6 source specific multicast, we can use `SourceSpecificMulticast[Ipv6Address]`. To construct instances of `Multicast[A]` and `SourceSpecificMulticast[A]`, we can use the `asMulticast` and `asSourceSpecificMulticast` methods on `IpAddress`: ```scala val multicastIps = ips.flatMap(_.asMulticast) // multicastIps: List[Multicast[IpAddress]] = List(224.10.10.10, 232.11.11.11, ff00::10, ff3b::11) val ssmIps = ips.flatMap(_.asSourceSpecificMulticast) // ssmIps: List[Strict[IpAddress]] = List(232.11.11.11, ff3b::11) ``` It's common for source specific multicast to be used with group addresses outside the designated source specific multicast address range. To support such cases, use `asSourceSpecificMulticastLenient`: ```scala val lenient = ips.flatMap(_.asSourceSpecificMulticastLenient) // lenient: List[SourceSpecificMulticast[IpAddress]] = List(224.10.10.10, 232.11.11.11, ff00::10, ff3b::11) ``` Additionally, the `SourceSpecificMulticast.Strict[A]` type provides the guarantee that the wrapped address is in the RFC defined source specific range. ## Multicast Literals There are string interpolators for constructing multicast and source specific multicast address from literal strings, similar to the `ip`, `ipv4`, and `ipv6` interpolators. The multicast interpolators are: |Interpolator|Description|Result Type|Example| |------------|-----------|-----------|-------| |`mip`|Multicast IP|`Multicast[IpAddress]`|`mip"224.10.10.10"`| |`mipv4`|V4 Multicast IP|`Multicast[Ipv4Address]`|`mipv4"224.10.10.10"`| |`mipv6`|V6 Multicast IP|`Multicast[Ipv6Address]`|`mipv6"ff3b::10"`| |`ssmip`|Source Specific Multicast IP|`SourceSpecificMulticast[IpAddress]`|`ssmip"224.10.10.10"`| |`ssmipv4`|V4 Source Specific Multicast IP|`SourceSpecificMulticast[Ipv4Address]`|`ssmipv4"224.10.10.10"`| |`ssmipv6`|V6 Source Specific Multicast IP|`SourceSpecificMulticast[Ipv6Address]`|`ssmipv6"ff3b::10"`| ## Multicast Joins The `MulticastJoin` type models a request to join a multicast group. There are two types of joins -- any source joins and source specific joins. An any source join is specified by supplying a multicast IP address whereas a source specific join is specified by supplying a source IP address and a source specific multicast address. In both cases, the multicast IP address is referred to as the group. This is modeled roughly as: ```scala sealed trait MulticastJoin[A <: IpAddress] case class AnySourceMulticastJoin[A <: IpAddress](group: Multicast[A]) extends MulticastJoin[A] case class SourceSpecificMulticastJoin[A <: IpAddress](source: A, group: SourceSpecificMulticast[A]) extends MulticastJoin[A] ``` `MulticastJoin` and its subtypes are parameterized by the address type in order to optionally constrain the join to V4 or V6 addresses. The `AnySourceMulticastJoin` and `SourceSpecificMulticastJoin` types are exposed, instead of being kept as an implementation detail (or data constructor), for a similar reason -- to allow modelling where a type or function wants a very specific type like `SourceSpecificMulticastJoin[Ipv6Address]` while supporting other scnenarios that want something much more general like a `MulticastJoin[IpAddress]`. To construct a `MulticastJoin`, we can use the `asm` and `ssm` methods in the `MulticastJoin` companion. ```scala val j1 = MulticastJoin.ssm(ipv4"10.11.12.13", ssmipv4"232.1.2.3") // j1: MulticastJoin[Ipv4Address] = 10.11.12.13@232.1.2.3 val j2 = MulticastJoin.ssm(ipv4"10.11.12.13", ipv4"232.1.2.3".asSourceSpecificMulticast.get) // j2: MulticastJoin[Ipv4Address] = 10.11.12.13@232.1.2.3 val j3 = MulticastJoin.asm(mipv6"ff3b::10") // j3: MulticastJoin[Ipv6Address] = ff3b::10 ``` # CIDR CIDR (classless inter-domain routing) addresses are a compact representation of an IP address and a routing prefix. They are expressed as an IP followed by `/prefixLength` where `prefixLength` represents the number of bits of the start of the address that define the routing prefix. For example, the CIDR `10.123.45.67/8` represents the IP `10.123.45.67` and the routing prefix of `255.0.0.0`. The `Cidr` type represents CIDR addresses. It's parameterized by the type of IP address you are working with -- either `IpAddress`, `Ipv4Address`, or `Ipv6Address`. ```scala val x = Cidr(ip"10.123.45.67", 8) // x: Cidr[IpAddress] = 10.123.45.67/8 val y = Cidr(ipv4"10.123.45.67", 8) // y: Cidr[Ipv4Address] = 10.123.45.67/8 val z = Cidr(ipv6"ff3b::10", 16) // z: Cidr[Ipv6Address] = ff3b::10/16 ``` A shortand for constructing a `Cidr` is available as a method on `IpAddress`: ```scala val x = ip"10.123.45.67" / 8 // x: Cidr[IpAddress] = 10.123.45.67/8 val y = ipv4"10.123.45.67" / 8 // y: Cidr[Ipv4Address] = 10.123.45.67/8 val z = ipv6"ff3b::10" / 16 // z: Cidr[Ipv6Address] = ff3b::10/16 ``` The `Cidr` object provides mechanisms for parsing CIDR strings as well: ```scala val parsedX = Cidr.fromString("10.123.45.67/8") // parsedX: Option[Cidr[IpAddress]] = Some(10.123.45.67/8) val parsedY = Cidr.fromString4("10.123.45.67/8") // parsedY: Option[Cidr[Ipv4Address]] = Some(10.123.45.67/8) val parsedZ = Cidr.fromString6("ff3b::10/16") // parsedZ: Option[Cidr[Ipv6Address]] = Some(ff3b::10/16) ``` Given a `Cidr[A]`, we can ask various things about the routing prefix: ```scala val prefixX = x.prefix // prefixX: IpAddress = 10.0.0.0 val prefixZ = z.prefix // prefixZ: Ipv6Address = ff3b:: val maskX = x.mask // maskX: IpAddress = 255.0.0.0 val maskZ = z.mask // maskZ: Ipv6Address = ffff:: val doesXContainHome = x.contains(home) // doesXContainHome: Boolean = false val doesXContainSuccessor = x.contains(x.address.next) // doesXContainSuccessor: Boolean = true val lastAddressInX = x.last // lastAddressInX: IpAddress = 10.255.255.255 ``` # Socket Addresses A socket address is an IP address and a TCP/UDP port number. This is roughly modeled as: ```scala case class SocketAddress[+A <: IpAddress](ip: A, port: Port) ``` Like we saw with `CIDR` and `MulticastJoin`, `SocketAddress` is polymorphic in address type, allowing expression of constraints like a socket address with an IPv6 IP. `SocketAddress` can be converted to and from a string representation, where V6 addresses are surrounded by square brackets. ```scala val s = SocketAddress(ipv4"127.0.0.1", port"5555") // s: SocketAddress[Ipv4Address] = 127.0.0.1:5555 val s1 = SocketAddress.fromString(s.toString) // s1: Option[SocketAddress[Host]] = Some(127.0.0.1:5555) val t = SocketAddress(ipv6"::1", port"5555") // t: SocketAddress[Ipv6Address] = [::1]:5555 val t1 = SocketAddress.fromString6(t.toString) // t1: Option[SocketAddress[Ipv6Address]] = Some([::1]:5555) ``` On the JVM, a `SocketAddress` can be converted to a `java.net.InetSocketAddress` via the `toInetSocketAddress` method. ```scala val u = t.toInetSocketAddress // u: InetSocketAddress = /[0:0:0:0:0:0:0:1]:5555 ``` ## Multicast Socket Addresses Similarly, a multicast socket address is a multicast join and a UDP port number. It is defined polymorphically in both the IP address type and the join type (general join, any source join, or source specific join). For example, compare the types of `s` and `t`: ```scala import com.comcast.ip4s._ val s = MulticastSocketAddress(SourceSpecificMulticastJoin(ipv4"10.10.10.10", ssmipv4"232.10.11.12"), port"5555") // s: MulticastSocketAddress[SourceSpecificMulticastJoin, Ipv4Address] = 10.10.10.10@232.10.11.12:5555 val t = MulticastSocketAddress(MulticastJoin.ssm(ip"10.10.10.10", ssmip"232.10.11.12"), port"5555") // t: MulticastSocketAddress[MulticastJoin, IpAddress] = 10.10.10.10@232.10.11.12:5555 ``` # Hostnames The `Hostname` type models an RFC1123 compliant hostname -- limited to 253 total characters, labels separated by periods, and each label consisting of ASCII letters and digits and dashes, not beginning or ending in a dash, and not exceeding 63 characters. ```scala val home = Hostname.fromString("localhost") // home: Option[Hostname] = Some(localhost) val ls = home.map(_.labels) // ls: Option[List[Label]] = Some(List(localhost)) val comcast = host"comcast.com" // comcast: Hostname = comcast.com val cs = comcast.labels // cs: List[Label] = List(comcast, com) ``` ## Hostname Resolution On the JVM, hostnames can be resolved to IP addresses via `resolve` and `resolveAll`: ```scala import com.comcast.ip4s._ import cats.effect.IO, cats.effect.unsafe.implicits.global val home = host"localhost" // home: Hostname = localhost val homeIp = home.resolve[IO] // homeIp: IO[IpAddress] = IO(...) homeIp.unsafeRunSync() // res4: IpAddress = 127.0.0.1 val homeIps = home.resolveAll[IO] // homeIps: IO[List[IpAddress]] = IO(...) homeIps.unsafeRunSync() // res5: List[IpAddress] = List(127.0.0.1, ::1) ``` # Internationalized Domain Names RFC1123 hostnames are limited to ASCII characters. The `IDN` type provides a way to represent Unicode hostnames. ```scala val unicodeComcast = idn"comcast。com" // unicodeComcast: IDN = comcast。com unicodeComcast.hostname // res6: Hostname = comcast.com val emojiRegistrar = idn"i❤.ws" // emojiRegistrar: IDN = i❤.ws emojiRegistrar.hostname // res7: Hostname = xn--i-7iq.ws ``` # Hosts The `Host` type is a common supertype of `IpAddress`, `Hostname`, and `IDN`. ```scala val hosts = List(ip"127.0.0.255", home, emojiRegistrar) // hosts: List[Host] = List(127.0.0.255, localhost, i❤.ws) import cats.syntax.all._ val hostIps = hosts.traverse(_.resolve[IO]).unsafeRunSync() // hostIps: List[IpAddress] = List(127.0.0.255, 127.0.0.1, 132.148.137.119) ``` ================================================ FILE: docs/src/guide.md ================================================ ip4s: IP Addresses for Scala & Scala.js ======================================= This is the guide for IP Addresses for Scala & Scala.js. This library provides the package `com.comcast.ip4s`, which contains all types. It is a small package and it is often useful to import all of its contents via `import com.comcast.ip4s._` -- doing so will enable some syntax. # IP Addresses The `IpAddress` type represents either an IPv4 address or an IPv6 address. The primary mechanism to construct an `IpAddress` is `IpAddress.fromString`, which converts a string to an `Option[IpAddress]`. You can also construct an `IpAddress` from a byte array of either 4 bytes or 16 bytes. ```scala mdoc:to-string import com.comcast.ip4s.IpAddress val home = IpAddress.fromString("127.0.0.1") val home6 = IpAddress.fromString("::1") ``` The `toString` method on `IpAddress` renders the IP in dotted-decimal notation if it is a V4 address and condensed string notation if it is a V6 address. The `toBytes` method converts the IP into a 4 or 16 element byte array. There are a few more methods on `IpAddress` that we'll look at later but not many more -- the API is small. Sometimes it is useful to explicitly require an IPv4 or IPv6 address -- for example, when modelling the configuration of a device that requires an IPv6 address. This can be accomplished by using the `Ipv4Address` or `Ipv6Address` types, both of which are subtypes of `IpAddress`. We can construct these types directly via methods on their companions: ```scala mdoc:to-string import com.comcast.ip4s.{Ipv4Address, Ipv6Address} val explicitV4Home = Ipv4Address.fromString("127.0.0.1") val explicitV6Home = Ipv6Address.fromString("::1") ``` Because `Ipv4Address` and `Ipv6Address` are subtypes of `IpAddress`, we can pattern match on an `IpAddress` or use the `fold` method: ```scala mdoc:to-string import com.comcast.ip4s.{Ipv4Address, Ipv6Address} val homeIsV4 = home.get match { case _: Ipv4Address => true case _: Ipv6Address => false } val home6IsV4 = home6.get.fold(_ => true, _ => false) ``` ## IP Literals In the previous examples, all of the IP addresses were wrapped in an `Option`. When a string is statically (i.e. at compile time) known to be a valid IP address, we can avoid the `Option` entirely by using IP address string interpolators. ```scala mdoc:reset:to-string import com.comcast.ip4s._ val home = ip"127.0.0.1" val home4 = ipv4"127.0.0.1" val home6 = ipv6"::1" ``` The `ip` interpolator returns an `IpAddress`, the `ipv4` interpolator returns an `Ipv4Address`, and the `ipv6` interpolator returns an `Ipv6Address`. If the string is not a valid IP of the requested type, the expression will fail to compile. ## IPv6 String Formats IPv6 addresses have a number of special string formats. The default format (what's returned by `toString`) adheres to [RFC5952](https://tools.ietf.org/html/rfc5952) -- e.g., maximal use of `::` to condense string length. If instead you want a string that does not use `::` and expresses each hextet as 4 characters, call `.toUncondensedString`. Note that the `toString` method never outputs a mixed string consisting of both V6 hextets and a dotted decimal V4 address. For example, the address consisting of 12 0 bytes followed by 127, 0, 0, 1 is rendered as `::7f00:1` instead of `::127.0.0.1`. `Ipv6Address.fromString` and `IpAddress.fromString` can parse all of these formats. ```scala mdoc:reset:to-string import com.comcast.ip4s._ val home = ipv6"::7f00:1" val homeLong = home.toUncondensedString val homeMixed = home.toMixedString val parsedHomeLong = Ipv6Address.fromString(homeLong) val parsedHomeMixed = Ipv6Address.fromString(homeMixed) ``` ## Ordering IP addresses have a defined ordering, making them sortable. Note when comparing an IPv4 address to an IPv6 address, the V4 address is converted to a V6 address by left padding with 0 bytes (aka, a "compatible" V4-in-V6 address). ```scala mdoc:nest:to-string val ips = List(ipv4"10.1.1.1", ipv4"10.1.2.0", ipv4"10.1.0.0", ipv6"::1", ipv6"ff3b::") val sorted = ips.sorted ``` ## JVM Integration When compiling for the JVM, the various IP address classes have a `toInetAddress` method which returns a `java.net.InetAddress`, allowing easy integration with libraries that use `InetAddress`. ```scala mdoc:to-string val homeIA = ip"127.0.0.1".toInetAddress val home4IA = ipv4"127.0.0.1".toInetAddress val home6IA = ipv6"::1".toInetAddress ``` # Multicast Both IPv4 and IPv6 have reserved address ranges for multicast and smaller reserved ranges for source specific multicast. The address types in this library are aware of these ranges: ```scala mdoc:nest:to-string val ips = List(ip"127.0.0.1", ip"224.10.10.10", ip"232.11.11.11", ip"::1", ip"ff00::10", ip"ff3b::11") val multicastIps = ips.filter(_.isMulticast) val ssmIps = ips.filter(_.isSourceSpecificMulticast) ``` ## Multicast Witnesses Often, especially when modelling configuration of systems, you need a type that indicates an address is a valid multicast or source specific multicast address. That's provided by the `Multicast` and `SourceSpecificMulticast` types. These roughly look like: ```scala mdoc:to-string sealed trait Multicast[A <: IpAddress] { def address: A } sealed trait SourceSpecificMulticast[A <: IpAddress] extends Multicast[A] ``` These wrappers serve as type level witnesses that the wrapped address is a valid multicast / source specific multicast address. These types are parameterized by the type of IP address -- either `IpAddress`, `Ipv4Address`, or `Ipv6Address`, to allow for accurate domain modeling at the type level. For example, if an application only supported IPv6 source specific multicast, we can use `SourceSpecificMulticast[Ipv6Address]`. To construct instances of `Multicast[A]` and `SourceSpecificMulticast[A]`, we can use the `asMulticast` and `asSourceSpecificMulticast` methods on `IpAddress`: ```scala mdoc:nest:to-string val multicastIps = ips.flatMap(_.asMulticast) val ssmIps = ips.flatMap(_.asSourceSpecificMulticast) ``` It's common for source specific multicast to be used with group addresses outside the designated source specific multicast address range. To support such cases, use `asSourceSpecificMulticastLenient`: ```scala mdoc:nest:to-string val lenient = ips.flatMap(_.asSourceSpecificMulticastLenient) ``` Additionally, the `SourceSpecificMulticast.Strict[A]` type provides the guarantee that the wrapped address is in the RFC defined source specific range. ## Multicast Literals There are string interpolators for constructing multicast and source specific multicast address from literal strings, similar to the `ip`, `ipv4`, and `ipv6` interpolators. The multicast interpolators are: |Interpolator|Description|Result Type|Example| |------------|-----------|-----------|-------| |`mip`|Multicast IP|`Multicast[IpAddress]`|`mip"224.10.10.10"`| |`mipv4`|V4 Multicast IP|`Multicast[Ipv4Address]`|`mipv4"224.10.10.10"`| |`mipv6`|V6 Multicast IP|`Multicast[Ipv6Address]`|`mipv6"ff3b::10"`| |`ssmip`|Source Specific Multicast IP|`SourceSpecificMulticast[IpAddress]`|`ssmip"224.10.10.10"`| |`ssmipv4`|V4 Source Specific Multicast IP|`SourceSpecificMulticast[Ipv4Address]`|`ssmipv4"224.10.10.10"`| |`ssmipv6`|V6 Source Specific Multicast IP|`SourceSpecificMulticast[Ipv6Address]`|`ssmipv6"ff3b::10"`| ## Multicast Joins The `MulticastJoin` type models a request to join a multicast group. There are two types of joins -- any source joins and source specific joins. An any source join is specified by supplying a multicast IP address whereas a source specific join is specified by supplying a source IP address and a source specific multicast address. In both cases, the multicast IP address is referred to as the group. This is modeled roughly as: ```scala mdoc:to-string sealed trait MulticastJoin[A <: IpAddress] case class AnySourceMulticastJoin[A <: IpAddress](group: Multicast[A]) extends MulticastJoin[A] case class SourceSpecificMulticastJoin[A <: IpAddress](source: A, group: SourceSpecificMulticast[A]) extends MulticastJoin[A] ``` `MulticastJoin` and its subtypes are parameterized by the address type in order to optionally constrain the join to V4 or V6 addresses. The `AnySourceMulticastJoin` and `SourceSpecificMulticastJoin` types are exposed, instead of being kept as an implementation detail (or data constructor), for a similar reason -- to allow modelling where a type or function wants a very specific type like `SourceSpecificMulticastJoin[Ipv6Address]` while supporting other scnenarios that want something much more general like a `MulticastJoin[IpAddress]`. To construct a `MulticastJoin`, we can use the `asm` and `ssm` methods in the `MulticastJoin` companion. ```scala mdoc:to-string val j1 = MulticastJoin.ssm(ipv4"10.11.12.13", ssmipv4"232.1.2.3") val j2 = MulticastJoin.ssm(ipv4"10.11.12.13", ipv4"232.1.2.3".asSourceSpecificMulticast.get) val j3 = MulticastJoin.asm(mipv6"ff3b::10") ``` # CIDR CIDR (classless inter-domain routing) addresses are a compact representation of an IP address and a routing prefix. They are expressed as an IP followed by `/prefixLength` where `prefixLength` represents the number of bits of the start of the address that define the routing prefix. For example, the CIDR `10.123.45.67/8` represents the IP `10.123.45.67` and the routing prefix of `255.0.0.0`. The `Cidr` type represents CIDR addresses. It's parameterized by the type of IP address you are working with -- either `IpAddress`, `Ipv4Address`, or `Ipv6Address`. ```scala mdoc:nest:to-string val x = Cidr(ip"10.123.45.67", 8) val y = Cidr(ipv4"10.123.45.67", 8) val z = Cidr(ipv6"ff3b::10", 16) ``` A shortand for constructing a `Cidr` is available as a method on `IpAddress`: ```scala mdoc:nest:to-string val x = ip"10.123.45.67" / 8 val y = ipv4"10.123.45.67" / 8 val z = ipv6"ff3b::10" / 16 ``` The `Cidr` object provides mechanisms for parsing CIDR strings as well: ```scala mdoc:nest:to-string val parsedX = Cidr.fromString("10.123.45.67/8") val parsedY = Cidr.fromString4("10.123.45.67/8") val parsedZ = Cidr.fromString6("ff3b::10/16") ``` Given a `Cidr[A]`, we can ask various things about the routing prefix: ```scala mdoc:nest:to-string val prefixX = x.prefix val prefixZ = z.prefix val maskX = x.mask val maskZ = z.mask val doesXContainHome = x.contains(home) val doesXContainSuccessor = x.contains(x.address.next) val lastAddressInX = x.last ``` # Socket Addresses A socket address is an IP address and a TCP/UDP port number. This is roughly modeled as: ```scala case class SocketAddress[+A <: IpAddress](ip: A, port: Port) ``` Like we saw with `CIDR` and `MulticastJoin`, `SocketAddress` is polymorphic in address type, allowing expression of constraints like a socket address with an IPv6 IP. `SocketAddress` can be converted to and from a string representation, where V6 addresses are surrounded by square brackets. ```scala mdoc:nest:to-string val s = SocketAddress(ipv4"127.0.0.1", port"5555") val s1 = SocketAddress.fromString(s.toString) val t = SocketAddress(ipv6"::1", port"5555") val t1 = SocketAddress.fromString6(t.toString) ``` On the JVM, a `SocketAddress` can be converted to a `java.net.InetSocketAddress` via the `toInetSocketAddress` method. ```scala mdoc:nest:to-string val u = t.toInetSocketAddress ``` ## Multicast Socket Addresses Similarly, a multicast socket address is a multicast join and a UDP port number. It is defined polymorphically in both the IP address type and the join type (general join, any source join, or source specific join). For example, compare the types of `s` and `t`: ```scala mdoc:reset:to-string import com.comcast.ip4s._ val s = MulticastSocketAddress(SourceSpecificMulticastJoin(ipv4"10.10.10.10", ssmipv4"232.10.11.12"), port"5555") val t = MulticastSocketAddress(MulticastJoin.ssm(ip"10.10.10.10", ssmip"232.10.11.12"), port"5555") ``` # Hostnames The `Hostname` type models an RFC1123 compliant hostname -- limited to 253 total characters, labels separated by periods, and each label consisting of ASCII letters and digits and dashes, not beginning or ending in a dash, and not exceeding 63 characters. ```scala mdoc:nest:to-string val home = Hostname.fromString("localhost") val ls = home.map(_.labels) val comcast = host"comcast.com" val cs = comcast.labels ``` ## Hostname Resolution On the JVM, hostnames can be resolved to IP addresses via `resolve` and `resolveAll`: ```scala mdoc:reset:to-string import com.comcast.ip4s._ import cats.effect.IO, cats.effect.unsafe.implicits.global val home = host"localhost" val homeIp = home.resolve[IO] homeIp.unsafeRunSync() val homeIps = home.resolveAll[IO] homeIps.unsafeRunSync() ``` # Internationalized Domain Names RFC1123 hostnames are limited to ASCII characters. The `IDN` type provides a way to represent Unicode hostnames. ```scala mdoc:to-string val unicodeComcast = idn"comcast。com" unicodeComcast.hostname val emojiRegistrar = idn"i❤.ws" emojiRegistrar.hostname ``` # Hosts The `Host` type is a common supertype of `IpAddress`, `Hostname`, and `IDN`. ```scala mdoc:to-string val hosts = List(ip"127.0.0.255", home, emojiRegistrar) import cats.syntax.all._ val hostIps = hosts.traverse(_.resolve[IO]).unsafeRunSync() ``` ================================================ FILE: js/src/main/scala/com/comcast/ip4s/DnsPlatform.scala ================================================ /* * Copyright 2018 Comcast Cable Communications Management, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.comcast.ip4s import cats.effect.kernel.Async import cats.syntax.all._ import scala.scalajs.js import scala.scalajs.js._ import scala.scalajs.js.annotation.JSImport import org.typelevel.scalaccompat.annotation._ private[ip4s] trait DnsCompanionPlatform { def forAsync[F[_]](implicit F: Async[F]): Dns[F] = new UnsealedDns[F] { def resolve(hostname: Hostname): F[IpAddress] = F.fromPromise(F.delay(dnsPromises.lookup(hostname.toString, LookupOptions(all = false)))) .flatMap { address => IpAddress .fromString(address.asInstanceOf[LookupResult].address) .liftTo[F](new RuntimeException("Node.js returned invalid IP address")) } .adaptError { case ex @ js.JavaScriptException(error: js.Error) if error.message.contains("ENOTFOUND") => new JavaScriptUnknownHostException(s"$hostname: Name or service not known", ex) } def resolveOption(hostname: Hostname): F[Option[IpAddress]] = resolve(hostname).map(_.some).recover { case _: UnknownHostException => None } def resolveAll(hostname: Hostname): F[List[IpAddress]] = F.fromPromise(F.delay(dnsPromises.lookup(hostname.toString, LookupOptions(all = true)))) .flatMap { addresses => addresses .asInstanceOf[js.Array[LookupResult]] .toList .traverse { address => IpAddress .fromString(address.address) .liftTo[F](new RuntimeException("Node.js returned invalid IP address")) } } .recover { case js.JavaScriptException(error: js.Error) if error.message.contains("ENOTFOUND") => Nil } def reverse(address: IpAddress): F[Hostname] = reverseAllOrError(address).flatMap(_.headOption.liftTo(new UnknownHostException(address.toString))) def reverseOption(address: IpAddress): F[Option[Hostname]] = reverseAll(address).map(_.headOption) def reverseAll(address: IpAddress): F[List[Hostname]] = reverseAllOrError(address).recover { case _: UnknownHostException => Nil } private def reverseAllOrError(address: IpAddress): F[List[Hostname]] = F.fromPromise(F.delay(dnsPromises.reverse(address.toString))) .flatMap { hostnames => hostnames.toList.traverse { hostname => Hostname .fromString(hostname) .liftTo[F](new RuntimeException("Node.js returned invalid hostname")) } } .adaptError { case ex @ js.JavaScriptException(error: js.Error) if error.message.contains("ENOTFOUND") => new JavaScriptUnknownHostException(address.toString, ex) } def loopback: F[IpAddress] = resolve(Hostname.fromString("localhost").get) } } @js.native @JSImport("dns", "promises") @nowarn212("cat=unused") private[ip4s] object dnsPromises extends js.Any { def lookup(hostname: String, options: LookupOptions): js.Promise[LookupResult | js.Array[LookupResult]] = js.native def reverse(ip: String): js.Promise[js.Array[String]] = js.native } private[ip4s] sealed trait LookupOptions extends js.Object object LookupOptions { def apply(all: Boolean): LookupOptions = js.Dynamic.literal(all = all).asInstanceOf[LookupOptions] } @js.native private[ip4s] sealed trait LookupResult extends js.Object { def address: String = js.native def family: Int = js.native } ================================================ FILE: js/src/main/scala/com/comcast/ip4s/IpAddressPlatform.scala ================================================ /* * Copyright 2018 Comcast Cable Communications Management, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.comcast.ip4s private[ip4s] trait IpAddressPlatform private[ip4s] trait IpAddressCompanionPlatform private[ip4s] trait Ipv4AddressPlatform extends IpAddressPlatform private[ip4s] trait Ipv4AddressCompanionPlatform private[ip4s] trait Ipv6AddressPlatform extends IpAddressPlatform private[ip4s] trait Ipv6AddressCompanionPlatform ================================================ FILE: js/src/main/scala/com/comcast/ip4s/NetworkInterfacePlatform.scala ================================================ /* * Copyright 2018 Comcast Cable Communications Management, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.comcast.ip4s private[ip4s] trait NetworkInterfaceCompanionPlatform ================================================ FILE: js/src/main/scala/com/comcast/ip4s/NetworkInterfacesPlatform.scala ================================================ /* * Copyright 2018 Comcast Cable Communications Management, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.comcast.ip4s import cats.effect.Sync import scala.scalajs.js import scala.scalajs.js.annotation.JSImport private[ip4s] trait NetworkInterfacesCompanionPlatform { def forSync[F[_]](implicit F: Sync[F]): NetworkInterfaces[F] = new NetworkInterfaces.SyncNetworkInterfaces[F] { def getAll: F[Map[String, NetworkInterface]] = F.blocking { val dict = osFacade.networkInterfaces() var result = collection.immutable.ListMap.empty[String, NetworkInterface] dict.keys.foreach(k => result = result + (k -> fromNetworkInterfaceInfo(k, dict(k)))) result } private def fromNetworkInterfaceInfo( name: String, nia: js.Array[osFacade.NetworkInterfaceInfo] ): NetworkInterface = NetworkInterface( name, name, MacAddress.fromString(nia.head.mac).filterNot(_.toLong == 0), nia.view.map { ni => val cidr = Cidr.fromString(ni.cidr).get cidr.address match { case v6: Ipv6Address => if (ni.scopeid == 0) cidr else Cidr(v6.withScopeId(ni.scopeid.toString), cidr.prefixBits) case _ => cidr } }.toList, nia.head.internal, true ) } } private[ip4s] object osFacade { @js.native @JSImport("os", "networkInterfaces") def networkInterfaces(): js.Dictionary[js.Array[NetworkInterfaceInfo]] = js.native @js.native trait NetworkInterfaceInfo extends js.Object { def mac: String = js.native def cidr: String = js.native def scopeid: Int = js.native def internal: Boolean = js.native } } ================================================ FILE: js/src/main/scala/com/comcast/ip4s/SocketAddressPlatform.scala ================================================ /* * Copyright 2018 Comcast Cable Communications Management, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.comcast.ip4s private[ip4s] trait SocketAddressPlatform[+A <: Host] private[ip4s] trait SocketAddressCompanionPlatform ================================================ FILE: js/src/main/scala/com/comcast/ip4s/UnknownHostException.scala ================================================ /* * Copyright 2018 Comcast Cable Communications Management, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.comcast.ip4s import java.io.IOException import scala.scalajs.js import scala.util.control.NoStackTrace class UnknownHostException(message: String = null, cause: Throwable = null) extends IOException(message, cause) private[ip4s] class JavaScriptUnknownHostException(message: String, cause: js.JavaScriptException) extends UnknownHostException(message, cause) with NoStackTrace ================================================ FILE: js/src/main/scala/com/comcast/ip4s/ip4splatform.scala ================================================ /* * Copyright 2018 Comcast Cable Communications Management, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.comcast.ip4s private[comcast] trait ip4splatform ================================================ FILE: jvm-native/src/main/scala/com/comcast/ip4s/DnsPlatform.scala ================================================ /* * Copyright 2018 Comcast Cable Communications Management, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.comcast.ip4s import cats.effect.kernel.Async import cats.effect.kernel.Sync import cats.syntax.all._ import java.net.InetAddress private[ip4s] trait DnsCompanionPlatform { def forAsync[F[_]: Async]: Dns[F] = forSync // alias for cross-compiling w/ JS def forSync[F[_]](implicit F: Sync[F]): Dns[F] = new UnsealedDns[F] { def resolve(hostname: Hostname): F[IpAddress] = F.blocking { val addr = InetAddress.getByName(hostname.toString) IpAddress.fromBytes(addr.getAddress).get } def resolveOption(hostname: Hostname): F[Option[IpAddress]] = resolve(hostname).map(_.some).recover { case _: UnknownHostException => None } def resolveAll(hostname: Hostname): F[List[IpAddress]] = F.blocking { try { val addrs = InetAddress.getAllByName(hostname.toString) addrs.toList.flatMap(addr => IpAddress.fromBytes(addr.getAddress)) } catch { case _: UnknownHostException => Nil } } def reverse(address: IpAddress): F[Hostname] = { val inetAddress = address.toInetAddress F.blocking { inetAddress.getCanonicalHostName } flatMap { hn => (if (hn == inetAddress.getHostAddress) None // getCanonicalHostName returns the IP address as a string on failure else Hostname.fromString(hn)) .liftTo[F](new UnknownHostException(address.toString)) } } def reverseOption(address: IpAddress): F[Option[Hostname]] = reverse(address).map(_.some).recover { case _: UnknownHostException => None } def reverseAll(address: IpAddress): F[List[Hostname]] = reverseOption(address).map(_.toList) def loopback: F[IpAddress] = F.blocking(IpAddress.fromInetAddress(InetAddress.getByName(null))) } } ================================================ FILE: jvm-native/src/main/scala/com/comcast/ip4s/IpAddressPlatform.scala ================================================ /* * Copyright 2018 Comcast Cable Communications Management, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.comcast.ip4s import java.net.{InetAddress, Inet4Address, Inet6Address} private[ip4s] trait IpAddressPlatform { /** Converts this address to a `java.net.InetAddress`. Note this method only exists on the JVM. */ def toInetAddress: InetAddress } private[ip4s] trait IpAddressCompanionPlatform { /** Converts the supplied `InetAddress` to an `IpAddress`. */ def fromInetAddress(address: InetAddress): IpAddress = address match { case v4: Inet4Address => Ipv4Address.fromInet4Address(v4) case v6: Inet6Address => Ipv6Address.fromInet6Address(v6) case _ => throw new UnsupportedOperationException(s"unsupported address type: $address") } } private[ip4s] trait Ipv4AddressPlatform extends IpAddressPlatform { protected val bytes: Array[Byte] override def toInetAddress: Inet4Address = InetAddress.getByAddress(bytes).asInstanceOf[Inet4Address] } private[ip4s] trait Ipv4AddressCompanionPlatform { /** Converts the supplied `Inet4Address` to an `Ipv4Address`. */ def fromInet4Address(address: Inet4Address): Ipv4Address = Ipv4Address.fromBytes(address.getAddress).get } private[ip4s] trait Ipv6AddressPlatform extends IpAddressPlatform { protected val bytes: Array[Byte] override def toInetAddress: InetAddress = InetAddress.getByAddress(bytes) } private[ip4s] trait Ipv6AddressCompanionPlatform { /** Converts the supplied `Inet6Address` to an `Ipv6Address`. */ def fromInet6Address(address: Inet6Address): Ipv6Address = { val scopeId = address.getScopeId val unscoped = Ipv6Address.fromBytes(address.getAddress).get if (scopeId == 0) unscoped else unscoped.withScopeId(scopeId.toString) } } ================================================ FILE: jvm-native/src/main/scala/com/comcast/ip4s/NetworkInterfacePlatform.scala ================================================ /* * Copyright 2018 Comcast Cable Communications Management, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.comcast.ip4s import java.net.{InterfaceAddress as JInterfaceAddress, NetworkInterface as JNetworkInterface} import Ip4sCollectionCompat.* private[ip4s] trait NetworkInterfaceCompanionPlatform { def fromJava(jni: JNetworkInterface): NetworkInterface = NetworkInterface( jni.getName, jni.getDisplayName, Option(jni.getHardwareAddress).flatMap(MacAddress.fromBytes), jni.getInterfaceAddresses.asScala.toList.map(fromJavaInterfaceAddress), jni.isLoopback, jni.isUp ) private def fromJavaInterfaceAddress(ifa: JInterfaceAddress): Cidr[IpAddress] = Cidr(IpAddress.fromInetAddress(ifa.getAddress), ifa.getNetworkPrefixLength.toInt) } ================================================ FILE: jvm-native/src/main/scala/com/comcast/ip4s/NetworkInterfacesPlatform.scala ================================================ /* * Copyright 2018 Comcast Cable Communications Management, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.comcast.ip4s import cats.effect.Sync import java.net.NetworkInterface as JNetworkInterface import Ip4sCollectionCompat.* private[ip4s] trait NetworkInterfacesCompanionPlatform { def forSync[F[_]](implicit F: Sync[F]): NetworkInterfaces[F] = new NetworkInterfaces.SyncNetworkInterfaces[F] { def getAll: F[Map[String, NetworkInterface]] = F.blocking { collection.immutable.ListMap.empty[String, NetworkInterface] ++ JNetworkInterface.getNetworkInterfaces.asScala.toList.map(jni => jni.getName -> NetworkInterface.fromJava(jni) ) } } } ================================================ FILE: jvm-native/src/main/scala/com/comcast/ip4s/SocketAddressPlatform.scala ================================================ /* * Copyright 2018 Comcast Cable Communications Management, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.comcast.ip4s import cats.Applicative import java.net.InetSocketAddress private[ip4s] trait SocketAddressPlatform[+A <: Host] { val host: A val port: Port def toInetSocketAddress(implicit ev: A <:< IpAddress): InetSocketAddress = new InetSocketAddress(host.toInetAddress, port.value) } private[ip4s] trait SocketAddressCompanionPlatform { // bincompat @deprecated("resolve is now defined on SocketAddress", "3.2.1") def ResolveOps(self: SocketAddress[Host]): ResolveOps = new ResolveOps(self) @deprecated("resolve is now defined on SocketAddress", "3.2.1") class ResolveOps(private val self: SocketAddress[Host]) { /** Resolves this `SocketAddress[Hostname]` to a `SocketAddress[IpAddress]`. */ def resolve[F[_]: Dns: Applicative]: F[SocketAddress[IpAddress]] = self.resolve } /** Converts an `InetSocketAddress` to a `SocketAddress[IpAddress]`. */ def fromInetSocketAddress(address: InetSocketAddress): SocketAddress[IpAddress] = SocketAddress(IpAddress.fromInetAddress(address.getAddress), Port.fromInt(address.getPort).get) } ================================================ FILE: jvm-native/src/main/scala/com/comcast/ip4s/ip4splatform.scala ================================================ /* * Copyright 2018 Comcast Cable Communications Management, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.comcast.ip4s private[comcast] trait ip4splatform { type UnknownHostException = java.net.UnknownHostException } ================================================ FILE: project/build.properties ================================================ sbt.version=1.12.5 ================================================ FILE: project/plugins.sbt ================================================ addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.8.5") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.20.2") addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.10") addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.3.2") addSbtPlugin("ch.epfl.scala" % "sbt-scalajs-bundler" % "0.21.1") addSbtPlugin("io.github.sbt-doctest" % "sbt-doctest" % "0.12.4") addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.8.2") libraryDependencySchemes += "com.lihaoyi" %% "geny" % VersionScheme.Always ================================================ FILE: shared/src/main/scala/com/comcast/ip4s/Cidr.scala ================================================ /* * Copyright 2018 Comcast Cable Communications Management, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.comcast.ip4s import scala.util.Try import scala.util.hashing.MurmurHash3 import cats.{Order, Show} import com.comcast.ip4s.Cidr.Strict /** Classless Inter-Domain Routing address, which represents an IP address and its routing prefix. * * @param address * IP address for which this CIDR refers to * @param prefixBits * number of leading 1s in the routing mask */ sealed class Cidr[+A <: IpAddress] protected (val address: A, val prefixBits: Int) extends Product with Serializable { def copy[AA >: A <: IpAddress](address: AA = this.address, prefixBits: Int = this.prefixBits): Cidr[AA] = Cidr[AA](address, prefixBits) /** Returns a normalized cidr range, where the address is truncated to the prefix, so that the returned range * is (and prints as) a spec-valid cidr range, with no bits outside the routing mask set. * * @return a normalized cidr range * * @example {{{ * scala> val raw = Cidr(ipv4"10.11.12.13", 8) * raw: Cidr[Ipv4Address] = 10.11.12.13/8 * scala> raw.normalized * res0: Cidr.Strict[Ipv4Address] = 10.0.0.0/8 * }}} */ def normalized: Strict[A] = Cidr.Strict(this) /** Returns the routing mask. * * @example {{{ * scala> Cidr(ipv4"10.11.12.13", 8).mask * res0: Ipv4Address = 255.0.0.0 * scala> Cidr(ipv6"2001:db8:abcd:12::", 96).mask * res1: Ipv6Address = ffff:ffff:ffff:ffff:ffff:ffff:: * }}} */ def mask: A = address.transform(_ => Ipv4Address.mask(prefixBits), _ => Ipv6Address.mask(prefixBits)) /** Returns the routing prefix. * * Note: the routing prefix also serves as the first address in the range described by this CIDR. * * @example {{{ * scala> Cidr(ipv4"10.11.12.13", 8).prefix * res0: Ipv4Address = 10.0.0.0 * scala> Cidr(ipv6"2001:db8:abcd:12::", 96).prefix * res1: Ipv6Address = 2001:db8:abcd:12:: * scala> Cidr(ipv6"2001:db8:abcd:12::", 32).prefix * res2: Ipv6Address = 2001:db8:: * }}} */ def prefix: A = address.transform(_.masked(Ipv4Address.mask(prefixBits)), _.masked(Ipv6Address.mask(prefixBits))) /** Returns the last address in the range described by this CIDR. * * @example {{{ * scala> Cidr(ipv4"10.11.12.13", 8).last * res0: Ipv4Address = 10.255.255.255 * scala> Cidr(ipv6"2001:db8:abcd:12::", 96).last * res1: Ipv6Address = 2001:db8:abcd:12::ffff:ffff * scala> Cidr(ipv6"2001:db8:abcd:12::", 32).last * res2: Ipv6Address = 2001:db8:ffff:ffff:ffff:ffff:ffff:ffff * }}} */ def last: A = address.transform(_.maskedLast(Ipv4Address.mask(prefixBits)), _.maskedLast(Ipv6Address.mask(prefixBits))) /** Returns the number of addresses in the range described by this CIDR. */ def totalIps: BigInt = { BigInt(1) << (address.fold(_ => 32, _ => 128) - prefixBits) } /** Returns a predicate which tests if the supplied address is in the range described by this CIDR. * * @example {{{ * scala> Cidr(ipv4"10.11.12.13", 8).contains(ipv4"10.100.100.100") * res0: Boolean = true * scala> Cidr(ipv4"10.11.12.13", 8).contains(ipv4"11.100.100.100") * res1: Boolean = false * scala> val x = Cidr(ipv6"2001:db8:abcd:12::", 96).contains * scala> x(ipv6"2001:db8:abcd:12::5") * res2: Boolean = true * scala> x(ipv6"2001:db8::") * res3: Boolean = false * }}} */ def contains[AA >: A <: IpAddress]: AA => Boolean = { val start = prefix val end = last a => a >= start && a <= end } /** Returns an iterator over all addresses in the range described by this CIDR. * @return */ def addresses: Iterator[IpAddress] = { val start: IpAddress = prefix val end: IpAddress = last Iterator.iterate(start)(_.next).takeWhile(_ <= end) } override def toString: String = s"$address/$prefixBits" override def hashCode: Int = MurmurHash3.productHash(this, productPrefix.hashCode) override def equals(other: Any): Boolean = other match { case that: Cidr[?] => address == that.address && prefixBits == that.prefixBits case _ => false } override def canEqual(other: Any): Boolean = other.isInstanceOf[Cidr[?]] override def productArity: Int = 2 override def productElement(n: Int): Any = n match { case 0 => address case 1 => prefixBits case _ => throw new IndexOutOfBoundsException } } object Cidr { /** A normalized cidr range, of which the address is identical to the prefix. * * This means the address will never have any bits set outside the prefix. For example, a range * such as 192.168.0.1/31 is not allowed. */ final class Strict[+A <: IpAddress] private (address: A, prefixBits: Int) extends Cidr[A](address, prefixBits) { override def normalized: this.type = this override def prefix = address } object Strict { def apply[A <: IpAddress](cidr: Cidr[A]): Cidr.Strict[A] = cidr match { case already: Strict[?] => already.asInstanceOf[Strict[A]] case _ => new Cidr.Strict(cidr.prefix, cidr.prefixBits) } } /** Constructs a CIDR from the supplied IP address and prefix bit count. Note if `prefixBits` is less than 0, the * built `Cidr` will have `prefixBits` set to 0. Similarly, if `prefixBits` is greater than the bit length of the * address, it will be set to the bit length of the address. */ def apply[A <: IpAddress](address: A, prefixBits: Int): Cidr[A] = { val maxPrefixBits = address.fold(_ => 32, _ => 128) val b = if (prefixBits < 0) 0 else if (prefixBits > maxPrefixBits) maxPrefixBits else prefixBits new Cidr(address, b) } /** Constructs a CIDR from the supplied IP address and netmask. The number of leading 1 bits in the netmask are used * as the prefix bits for the CIDR. */ def fromIpAndMask[A <: IpAddress](address: A, mask: A): Cidr[A] = address / mask.prefixBits /** Constructs a CIDR from a string of the form `ip/prefixBits`. */ def fromString(value: String): Option[Cidr[IpAddress]] = fromStringGeneral(value, IpAddress.fromString) /** Constructs a CIDR from a string of the form `ipv4/prefixBits`. */ def fromString4(value: String): Option[Cidr[Ipv4Address]] = fromStringGeneral(value, Ipv4Address.fromString) /** Constructs a CIDR from a string of the form `ipv6/prefixBits`. */ def fromString6(value: String): Option[Cidr[Ipv6Address]] = fromStringGeneral(value, Ipv6Address.fromString) private val CidrPattern = """([^/]+)/(\d+)""".r private def fromStringGeneral[A <: IpAddress](value: String, parseAddress: String => Option[A]): Option[Cidr[A]] = value match { case CidrPattern(addrStr, prefixBitsStr) => for { addr <- parseAddress(addrStr) maxPrefixBits = addr.fold(_ => 32, _ => 128) prefixBits <- Try(prefixBitsStr.toInt).toOption if prefixBits >= 0 && prefixBits <= maxPrefixBits } yield Cidr(addr, prefixBits) case _ => None } implicit def order[A <: IpAddress]: Order[Cidr[A]] = Order.fromOrdering(Cidr.ordering[A]) implicit def ordering[A <: IpAddress]: Ordering[Cidr[A]] = Ordering.by(x => (x.address, x.prefixBits)) implicit def show[A <: IpAddress]: Show[Cidr[A]] = Show.fromToString[Cidr[A]] def unapply[A <: IpAddress](c: Cidr[A]): Option[(A, Int)] = Some((c.address, c.prefixBits)) } ================================================ FILE: shared/src/main/scala/com/comcast/ip4s/Dns.scala ================================================ /* * Copyright 2018 Comcast Cable Communications Management, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.comcast.ip4s import cats.~> import cats.Functor import cats.data.EitherT import cats.data.Kleisli import cats.effect.IO /** Capability for an effect `F[_]` which can do DNS lookups. * * An instance is available for any effect which has a `Sync` instance on JVM and `Async` on Node.js. */ sealed trait Dns[F[_]] { outer => /** Resolves the supplied hostname to an ip address using the platform DNS resolver. * * If the hostname cannot be resolved, the effect fails with an `UnknownHostException`. */ def resolve(hostname: Hostname): F[IpAddress] /** Resolves the supplied hostname to an ip address using the platform DNS resolver. * * If the hostname cannot be resolved, a `None` is returned. */ def resolveOption(hostname: Hostname): F[Option[IpAddress]] /** Resolves the supplied hostname to all ip addresses known to the platform DNS resolver. * * If the hostname cannot be resolved, an empty list is returned. */ def resolveAll(hostname: Hostname): F[List[IpAddress]] /** Reverses the supplied address to a hostname using the platform DNS resolver. * * If the address cannot be reversed, the effect fails with an `UnknownHostException`. */ def reverse(address: IpAddress): F[Hostname] /** Reverses the supplied address to a hostname using the platform DNS resolver. * * If the address cannot be reversed, a `None` is returned. */ def reverseOption(address: IpAddress): F[Option[Hostname]] /** Reverses the supplied address to all hostnames known to the platform DNS resolver. * * If the address cannot be reversed, an empty list is returned. */ def reverseAll(address: IpAddress): F[List[Hostname]] /** Gets an IP address representing the loopback interface. */ def loopback: F[IpAddress] /** Translates effect type from `F` to `G` using the supplied `FunctionK` */ final def mapK[G[_]](fk: F ~> G): Dns[G] = new Dns[G] { def resolve(hostname: Hostname) = fk(outer.resolve(hostname)) def resolveOption(hostname: Hostname) = fk(outer.resolveOption(hostname)) def resolveAll(hostname: Hostname) = fk(outer.resolveAll(hostname)) def reverse(address: IpAddress) = fk(outer.reverse(address)) def reverseOption(address: IpAddress) = fk(outer.reverseOption(address)) def reverseAll(address: IpAddress) = fk(outer.reverseAll(address)) def loopback = fk(outer.loopback) } } private[ip4s] trait UnsealedDns[F[_]] extends Dns[F] object Dns extends DnsCompanionPlatform { def apply[F[_]](implicit F: Dns[F]): F.type = F implicit def forIO: Dns[IO] = forAsync[IO] implicit def forEitherT[F[_]: Functor, A](implicit dns: Dns[F]): Dns[EitherT[F, A, *]] = dns.mapK(EitherT.liftK) implicit def forKleisli[F[_], A](implicit dns: Dns[F]): Dns[Kleisli[F, A, *]] = dns.mapK(Kleisli.liftK) } ================================================ FILE: shared/src/main/scala/com/comcast/ip4s/GenSocketAddress.scala ================================================ /* * Copyright 2018 Comcast Cable Communications Management, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.comcast.ip4s /** Supertype for types that specify address information for opening a socket. * * There are two built-in subtypes: * - [[SocketAddress]] - which specifies address and port info for IPv4/IPv6 sockets * - [[UnixSocketAddress]] - which specifies the path to a unix domain socket * * This trait is left open for extension, allowing other address types to be defined. * When using this trait, pattern match on the supported subtypes. */ abstract class GenSocketAddress private[ip4s] () { /** Downcasts this address to a `SocketAddress[IpAddress]`. * * @throws UnsupportedOperationException if this address is not a `SocketAddress[IpAddress]` */ def asIpUnsafe: SocketAddress[IpAddress] = this match { case addr @ SocketAddress(_: IpAddress, _) => addr.asInstanceOf[SocketAddress[IpAddress]] case other => throw new UnsupportedOperationException(s"asIpUnsafe only supported on SocketAddress[IpAddress]: $other") } } ================================================ FILE: shared/src/main/scala/com/comcast/ip4s/Host.scala ================================================ /* * Copyright 2018 Comcast Cable Communications Management, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.comcast.ip4s import cats.{Applicative, ApplicativeThrow, Order, Show} import cats.syntax.all._ import org.typelevel.idna4s.core.bootstring.Bootstring import org.typelevel.idna4s.core.bootstring.BootstringParams.PunycodeParams import scala.util.hashing.MurmurHash3 // annoying bincompat shim private[ip4s] trait HostPlatform /** ADT representing either an `IpAddress`, `Hostname`, or `IDN`. */ sealed trait Host extends HostPlatform with Ordered[Host] { def compare(that: Host): Int = this match { case x: Ipv4Address => that match { case y: Ipv4Address => IpAddress.compareBytes(x, y) case y: Ipv6Address => IpAddress.compareBytes(x.toCompatV6, y) case _ => -1 } case x: Ipv6Address => that match { case y: Ipv4Address => IpAddress.compareBytes(x, y.toCompatV6) case y: Ipv6Address => IpAddress.compareBytes(x, y) case _ => -1 } case x: Hostname => that match { case _: Ipv4Address => 1 case _: Ipv6Address => 1 case y: Hostname => x.toString.compare(y.toString) case y: IDN => x.toString.compare(y.hostname.toString) } case x: IDN => that match { case _: Ipv4Address => 1 case _: Ipv6Address => 1 case y: Hostname => x.hostname.toString.compare(y.toString) case y: IDN => x.hostname.toString.compare(y.hostname.toString) } } /** Resolves this host to an ip address using the platform DNS resolver. * * If the host cannot be resolved, the effect fails with a `com.comcast.ip4s.UnknownHostException`. */ def resolve[F[_]: Dns: Applicative]: F[IpAddress] = this match { case ip: IpAddress => Applicative[F].pure(ip) case hostname: Hostname => Dns[F].resolve(hostname) case idn: IDN => Dns[F].resolve(idn.hostname) } /** Resolves this host to an ip address using the platform DNS resolver. * * If the host cannot be resolved, a `None` is returned. */ def resolveOption[F[_]: Dns: ApplicativeThrow]: F[Option[IpAddress]] = resolve[F].map(Some(_): Option[IpAddress]).recover { case _: UnknownHostException => None } /** Resolves this host to all ip addresses known to the platform DNS resolver. * * If the host cannot be resolved, an empty list is returned. */ def resolveAll[F[_]: Dns: Applicative]: F[List[IpAddress]] = this match { case ip: IpAddress => Applicative[F].pure(List(ip)) case hostname: Hostname => Dns[F].resolveAll(hostname) case idn: IDN => Dns[F].resolveAll(idn.hostname) } } object Host { def fromString(string: String): Option[Host] = IpAddress.fromString(string) orElse Hostname.fromString(string) orElse IDN.fromString(string) implicit def show: Show[Host] = Show.fromToString[Host] implicit def order: Order[Host] = Order.fromComparable[Host] implicit def ordering: Ordering[Host] = _.compare(_) } /** RFC1123 compliant hostname. * * A hostname contains one or more labels, where each label consists of letters A-Z, a-z, digits 0-9, or a dash. A * label may not start or end in a dash and may not exceed 63 characters in length. Labels are separated by periods and * the overall hostname must not exceed 253 characters in length. */ final class Hostname private (val labels: List[Hostname.Label], override val toString: String) extends Host { /** Converts this hostname to lower case. */ def normalized: Hostname = new Hostname(labels.map(l => new Hostname.Label(l.toString.toLowerCase)), toString.toLowerCase) override def hashCode: Int = MurmurHash3.stringHash(toString, "Hostname".hashCode) override def equals(other: Any): Boolean = other match { case that: Hostname => toString == that.toString case _ => false } } object Hostname { /** Label component of a hostname. * * A label consists of letters A-Z, a-z, digits 0-9, or a dash. A label may not start or end in a dash and may not * exceed 63 characters in length. */ final class Label private[Hostname] (override val toString: String) extends Serializable with Ordered[Label] { def compare(that: Label): Int = toString.compare(that.toString) override def hashCode: Int = MurmurHash3.stringHash(toString, "Label".hashCode) override def equals(other: Any): Boolean = other match { case that: Label => toString == that.toString case _ => false } } private val Pattern = """[a-zA-Z0-9](?:[a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*""".r /** Constructs a `Hostname` from a string. */ def fromString(value: String): Option[Hostname] = value.size match { case 0 => None case i if i > 253 => None case _ => value match { case Pattern(_*) => val labels = value .split('.') .iterator .map(new Label(_)) .toList if (labels.isEmpty) None else Option(new Hostname(labels, value)) case _ => None } } } sealed trait IpVersion object IpVersion { case object V4 extends IpVersion case object V6 extends IpVersion } /** Immutable and safe representation of an IP address, either V4 or V6. * * ===Construction=== * * `IpAddress` instances are constructed in a few different ways: * - via `IpAddress("127.0.0.1")`, which parses a string representation of the IP and returns an `Option[IpAddress]` * - via `IpAddress.fromBytes(arr)`, which returns an IP address if the supplied array is either exactly 4 bytes or * exactly 16 bytes * - via literal syntax like `ip"127.0.0.1"`, which returns an `IpAddress` and fails to *compile* if the IP is * invalid. * * ===Type Hierarchy=== * * There are two subtypes of `IpAddress` -- [[Ipv4Address]] and [[Ipv6Address]]. Each of these subtypes have a richer * API than `IpAddress` and it is often useful to use those types directly, for example if your use case requires a V6 * address. It is safe to pattern match on `IpAddress` to access `Ipv4Address` or `Ipv6Address` directly, or * alternatively, you can use [[fold]]. * * ===JVM Specific API=== * * If using `IpAddress` on the JVM, you can call `toInetAddress` to convert the address to a `java.net.InetAddress`, * for use with networking libraries. This method does not exist on the Scala.js version. */ sealed abstract class IpAddress extends IpAddressPlatform with Host with Serializable { protected val bytes: Array[Byte] /** Converts this address to a network order byte array of either 4 or 16 bytes. */ def toBytes: Array[Byte] = bytes.clone /** Converts this address to a value of type `A` using the supplied functions. */ def fold[A](v4: Ipv4Address => A, v6: Ipv6Address => A): A /** Maps a type-preserving function across this IP address. */ def transform(v4: Ipv4Address => Ipv4Address, v6: Ipv6Address => Ipv6Address): this.type /** Returns true if this address is an IPv4 address. */ def isV4: Boolean = fold(_ => true, _ => false) /** Returns true if this address is an IPv6 address. */ def isV6: Boolean = fold(_ => false, _ => true) /** Returns true if this address is either 0.0.0.0 or ::. */ def isWildcard: Boolean = fold(_ == Ipv4Address.Wildcard, _ == Ipv6Address.Wildcard) /** Returns true if this address is in the multicast range. */ def isMulticast: Boolean /** Converts this address to a multicast address, as long as it is in the multicast address range. */ def asMulticast: Option[Multicast[this.type]] = Multicast.fromIpAddress(this) /** Returns true if this address is in the source specific multicast range. */ def isSourceSpecificMulticast: Boolean /** Converts this address to a source specific multicast address, as long as it is in the source specific multicast * address range. */ def asSourceSpecificMulticast: Option[SourceSpecificMulticast.Strict[this.type]] = SourceSpecificMulticast.fromIpAddress(this) /** Like `asSourceSpecificMulticast` but allows multicast addresses outside the source specific range. */ def asSourceSpecificMulticastLenient: Option[SourceSpecificMulticast[this.type]] = SourceSpecificMulticast.fromIpAddressLenient(this) /** Returns true if this address is a loopback address. */ def isLoopback: Boolean /** Returns true if this address is a link local address. */ def isLinkLocal: Boolean /** Returns true if this address is in a private range. */ def isPrivate: Boolean /** Narrows this address to an Ipv4Address if that is the underlying type. */ def asIpv4: Option[Ipv4Address] = collapseMappedV4.fold(Some(_), _ => None) /** Narrows this address to an Ipv6Address if that is the underlying type. */ def asIpv6: Option[Ipv6Address] = fold(_ => None, Some(_)) /** Returns the version of this address. */ def version: IpVersion = fold(_ => IpVersion.V4, _ => IpVersion.V6) /** Returns true if this address is a V6 address containing a mapped V4 address. */ def isMappedV4: Boolean = fold(_ => false, Ipv6Address.MappedV4Block.contains) /** If this address is an IPv4 mapped IPv6 address, converts to an IPv4 address, otherwise returns this. */ def collapseMappedV4: IpAddress = fold(identity, v6 => if (v6.isMappedV4) IpAddress.fromBytes(v6.toBytes.takeRight(4)).get else v6) /** Constructs a [[Cidr]] address from this address. */ def /(prefixBits: Int): Cidr[this.type] = Cidr(this, prefixBits) /** Returns the number of leading ones in this address. When this address is a netmask, the result can be used to * create a CIDR. * * @example {{{ * scala> val address = ipv4"192.168.1.25" * scala> val netmask = ipv4"255.255.0.0" * scala> (address / netmask.prefixBits): Cidr[Ipv4Address] * res0: Cidr[Ipv4Address] = 192.168.1.25/16 * }}} */ def prefixBits: Int = { import java.lang.Long.numberOfLeadingZeros fold( m => numberOfLeadingZeros(~(m.toLong | 0xffffffff00000000L)) - 32, m => { val upper = (m.toBigInt >> 64).toLong val upperNum = numberOfLeadingZeros(~upper) if (upperNum == 64) upperNum + numberOfLeadingZeros(~(m.toBigInt.toLong)) else upperNum } ) } /** Gets the IP address after this address, with overflow from the maximum value to the minimum value. */ def next: IpAddress /** Gets the IP address before this address, with underflow from minimum value to the maximum value. */ def previous: IpAddress /** Returns the default/common representation of this address. */ protected[this] def toDefaultString: String /** Converts this address to a string form that is compatible for use in a URI per RFC3986 (namely, IPv6 addresses are * rendered in condensed form and surrounded by brackets). */ def toUriString: String /** Returns the default/common representation of this address. For IPv4 * addresses, returns the dotted decimal representation of the address. For * IPv6 addresses, returns the condensed string representation of the array * per RFC5952. * * This method returns the same result as [[`toUriString`]] except that IPv6 * addresses are not surrounded by brackets. */ final override def toString: String = toDefaultString override def equals(other: Any): Boolean = other match { case that: IpAddress => java.util.Arrays.equals(bytes, that.bytes) case _ => false } override def hashCode: Int = java.util.Arrays.hashCode(bytes) } object IpAddress extends IpAddressCompanionPlatform { /** Parses an IP address from a string, either in dotted decimal notation or in RFC4291 notation. */ def fromString(value: String): Option[IpAddress] = Ipv4Address.fromString(value) orElse Ipv6Address.fromString(value) /** Constructs an IP address from either a 4-element byte array or a 16-element byte array. Any other size array * results in a `None`. */ def fromBytes(bytes: Array[Byte]): Option[IpAddress] = Ipv4Address.fromBytes(bytes) orElse Ipv6Address.fromBytes(bytes) /** Gets an IP address repesenting the loopback interface. */ def loopback[F[_]](implicit F: Dns[F]): F[IpAddress] = F.loopback private[ip4s] def compareBytes(x: IpAddress, y: IpAddress): Int = { var i, result = 0 val xb = x.bytes val yb = y.bytes val sz = xb.length while (i < sz && result == 0) { result = Integer.compare(xb(i) & 0xff, yb(i) & 0xff) i += 1 } result } implicit def order[A <: IpAddress]: Order[A] = Order.fromOrdering(IpAddress.ordering[A]) implicit def ordering[A <: IpAddress]: Ordering[A] = _.compare(_) } /** Representation of an IPv4 address that works on both the JVM and Scala.js. */ final class Ipv4Address private (protected val bytes: Array[Byte]) extends IpAddress with Ipv4AddressPlatform { override def fold[A](v4: Ipv4Address => A, v6: Ipv6Address => A): A = v4(this) override def transform(v4: Ipv4Address => Ipv4Address, v6: Ipv6Address => Ipv6Address): this.type = v4(this).asInstanceOf[this.type] /** Returns the dotted decimal representation of this address. */ override protected[this] def toDefaultString: String = s"${bytes(0) & 0xff}.${bytes(1) & 0xff}.${bytes(2) & 0xff}.${bytes(3) & 0xff}" override def toUriString: String = toDefaultString /** Gets the IPv4 address after this address, with overflow from `255.255.255.255` to `0.0.0.0`. */ override def next: Ipv4Address = Ipv4Address.fromLong(toLong + 1) /** Gets the IPv4 address before this address, with underflow from `0.0.0.0` to `255.255.255.255`. */ override def previous: Ipv4Address = Ipv4Address.fromLong(toLong - 1) /** Converts this address to a 32-bit unsigned integer. */ def toLong: Long = { val bs = bytes var result = 0L for (i <- 0 until bs.size) { result = (result << 8) | (0x0ff & bs(i)) } result } override def isMulticast: Boolean = this >= Ipv4Address.MulticastRangeStart && this <= Ipv4Address.MulticastRangeEnd override def isSourceSpecificMulticast: Boolean = this >= Ipv4Address.SourceSpecificMulticastRangeStart && this <= Ipv4Address.SourceSpecificMulticastRangeEnd override def isLoopback: Boolean = Ipv4Address.Classes.Loopback.contains(this) override def isLinkLocal: Boolean = Ipv4Address.Classes.LinkLocal.contains(this) override def isPrivate: Boolean = Ipv4Address.Classes.Private.A.contains(this) || Ipv4Address.Classes.Private.B.contains(this) || Ipv4Address.Classes.Private.C.contains(this) /** Converts this V4 address to a compat V6 address, where the first 12 bytes are all zero and the last 4 bytes * contain the bytes of this V4 address. */ def toCompatV6: Ipv6Address = { val compat = new Array[Byte](16) compat(12) = bytes(0) compat(13) = bytes(1) compat(14) = bytes(2) compat(15) = bytes(3) Ipv6Address.fromBytes(compat).get } /** Converts this V4 address to a mapped V6 address, where the first 10 bytes are all zero, the next two bytes are * `ff`, and the last 4 bytes contain the bytes of this V4 address. */ def toMappedV6: Ipv6Address = { val mapped = new Array[Byte](16) mapped(10) = 255.toByte mapped(11) = 255.toByte mapped(12) = bytes(0) mapped(13) = bytes(1) mapped(14) = bytes(2) mapped(15) = bytes(3) Ipv6Address.fromBytes(mapped).get } /** Applies the supplied mask to this address. * * @example {{{ * scala> ipv4"192.168.29.1".masked(ipv4"255.255.0.0") * res0: Ipv4Address = 192.168.0.0 * }}} */ def masked(mask: Ipv4Address): Ipv4Address = Ipv4Address.fromLong(toLong & mask.toLong) /** Computes the last address in the network identified by applying the supplied mask to this address. * * @example {{{ * scala> ipv4"192.168.29.1".maskedLast(ipv4"255.255.0.0") * res0: Ipv4Address = 192.168.255.255 * }}} */ def maskedLast(mask: Ipv4Address): Ipv4Address = Ipv4Address.fromLong(toLong & mask.toLong | ~mask.toLong) } object Ipv4Address extends Ipv4AddressCompanionPlatform { /** Wildcard IPv4 address - 0.0.0.0 */ val Wildcard: Ipv4Address = fromBytes(0, 0, 0, 0) /** First IP address in the IPv4 multicast range. */ val MulticastRangeStart: Ipv4Address = fromBytes(224, 0, 0, 0) /** Last IP address in the IPv4 multicast range. */ val MulticastRangeEnd: Ipv4Address = fromBytes(239, 255, 255, 255) /** First IP address in the IPv4 source specific multicast range. */ val SourceSpecificMulticastRangeStart: Ipv4Address = fromBytes(232, 0, 0, 0) /** Last IP address in the IPv4 source specific multicast range. */ val SourceSpecificMulticastRangeEnd: Ipv4Address = fromBytes(232, 255, 255, 255) /** IPv4 address classes represented as CIDRs. */ object Classes { /** Class A: 0.0.0.0 - 127.255.255.255 */ val A: Cidr[Ipv4Address] = Cidr(fromBytes(0, 0, 0, 0), 1) /** Class B: 128.0.0.0 - 191.255.255.255 */ val B: Cidr[Ipv4Address] = Cidr(fromBytes(128, 0, 0, 0), 2) /** Class C: 192.0.0.0 - 223.255.255.255 */ val C: Cidr[Ipv4Address] = Cidr(fromBytes(192, 0, 0, 0), 3) /** Class D: 224.0.0.0 - 239.255.255.255 */ val D: Cidr[Ipv4Address] = Cidr(fromBytes(224, 0, 0, 0), 4) /** Class E: 240.0.0.0 - 255.255.255.255 */ val E: Cidr[Ipv4Address] = Cidr(fromBytes(240, 0, 0, 0), 5) /** Private address ranges. */ object Private { /** Class A: 10.0.0.0 - 10.255.255.255 */ val A: Cidr[Ipv4Address] = Cidr(fromBytes(10, 0, 0, 0), 8) /** Class B: 172.16.0.0 - 172.31.255.255 */ val B: Cidr[Ipv4Address] = Cidr(fromBytes(172, 16, 0, 0), 12) /** Class A: 192.168.0.0 - 192.168.255.255 */ val C: Cidr[Ipv4Address] = Cidr(fromBytes(192, 168, 0, 0), 16) } /** Loopback: 127.0.0.0 - 127.255.255.255. */ val Loopback: Cidr[Ipv4Address] = Cidr(fromBytes(127, 0, 0, 0), 8) /** Link local: 169.254.0.0 - 169.254.255.255. */ val LinkLocal: Cidr[Ipv4Address] = Cidr(fromBytes(169, 254, 0, 0), 16) } /** Parses an IPv4 address from a dotted-decimal string, returning `None` if the string is not a valid IPv4 address. */ def fromString(value: String): Option[Ipv4Address] = { val trimmed = value.trim val fields = trimmed.split('.') if (fields.length == 4) { val bytes = new Array[Byte](4) var idx = 0 var result: Option[Ipv4Address] = null while (idx < bytes.length && (result eq null)) { try { val value = fields(idx).toInt if (value >= 0 && value < 256) bytes(idx) = value.toByte else result = None idx += 1 } catch { case _: NumberFormatException => result = None } } if (result eq null) Some(unsafeFromBytes(bytes)) else result } else None } /** Constructs an IPv4 address from a 4-element byte array. Returns `Some` when array is exactly 4-bytes and `None` * otherwise. */ def fromBytes(bytes: Array[Byte]): Option[Ipv4Address] = if (bytes.size == 4) Some(unsafeFromBytes(bytes.clone)) else None private def unsafeFromBytes(bytes: Array[Byte]): Ipv4Address = new Ipv4Address(bytes) /** Constructs an address from the specified 4 bytes. * * Each byte is represented as an `Int` to avoid having to manually call `.toByte` on each value -- the `toByte` call * is done inside this function. */ def fromBytes(a: Int, b: Int, c: Int, d: Int): Ipv4Address = { val bytes = new Array[Byte](4) bytes(0) = a.toByte bytes(1) = b.toByte bytes(2) = c.toByte bytes(3) = d.toByte unsafeFromBytes(bytes) } /** Constructs an IPv4 address from a `Long`, using the lower 32-bits. */ def fromLong(value: Long): Ipv4Address = { val bytes = new Array[Byte](4) var rem = value for (i <- 3 to 0 by -1) { bytes(i) = (rem & 0x0ff).toByte rem = rem >> 8 } unsafeFromBytes(bytes) } /** Computes a mask by setting the first / left-most `n` bits high. * * @example {{{ * scala> Ipv4Address.mask(16) * res0: Ipv4Address = 255.255.0.0 * }}} */ def mask(bits: Int): Ipv4Address = { val b = if (bits < 0) 0 else if (bits > 32) 32 else bits Ipv4Address.fromLong(if (b == 32) -1L else ~(-1 >>> b).toLong) } } /** Representation of an IPv6 address that works on both the JVM and Scala.js. */ final class Ipv6Address private (protected val bytes: Array[Byte], val scopeId: Option[String]) extends IpAddress with Ipv6AddressPlatform { override def fold[A](v4: Ipv4Address => A, v6: Ipv6Address => A): A = v6(this) override def transform(v4: Ipv4Address => Ipv4Address, v6: Ipv6Address => Ipv6Address): this.type = v6(this).asInstanceOf[this.type] /** Returns the condensed string representation of the array per RFC5952. */ override protected[this] def toDefaultString: String = { val fields: Array[Int] = new Array[Int](8) var condensing = false var condensedStart, maxCondensedStart = -1 var condensedLength, maxCondensedLength = 0 var idx = 0 while (idx < 8) { val j = 2 * idx val field = ((0x0ff & bytes(j)) << 8) | (0x0ff & bytes(j + 1)) fields(idx) = field if (field == 0) { if (!condensing) { condensing = true condensedStart = idx condensedLength = 0 } condensedLength += 1 } else { condensing = false } if (condensedLength > maxCondensedLength) { maxCondensedLength = condensedLength maxCondensedStart = condensedStart } idx += 1 } if (maxCondensedLength == 1) maxCondensedStart = -1 val str = new StringBuilder idx = 0 while (idx < 8) { if (idx == maxCondensedStart) { str.append("::") idx += maxCondensedLength } else { val hextet = Integer.toString(fields(idx), 16) str.append(hextet) idx += 1 if (idx < 8 && idx != maxCondensedStart) str.append(":") } } scopeId.foreach(sid => str.append("%").append(sid)) str.toString } /** Returns an uncondensed string representation of the array. */ def toUncondensedString: String = { val str = new StringBuilder var idx = 0 val bytes = toBytes while (idx < 16) { val field = ((bytes(idx) & 0xff) << 8) | (bytes(idx + 1) & 0xff) val hextet = f"$field%04x" str.append(hextet) idx += 2 if (idx < 15) str.append(":") } scopeId.foreach(sid => str.append("%").append(sid)) str.toString } /** Converts this address to a string of form `x:x:x:x:x:x:a.b.c.d` where each `x` represents 16-bits and `a.b.c.d` is * IPv4 dotted decimal notation. Consecutive 0 `x` fields are condensed with `::`. * * For example, the IPv4 address `127.0.0.1` can be converted to a compatible IPv6 address via * [[Ipv4Address#toCompatV6]], which is represented as the string `::7f00:1` and the mixed string `::127.0.0.1`. * * Similarly, `127.0.0.1` can be converted to a mapped V6 address via [[Ipv4Address#toMappedV6]], resulting in * `::ffff:7f00:1` and the mixed string `::ffff:127.0.0.1`. * * This format is described in RFC4291 section 2.2.3. * * @example {{{ * scala> ipv6"::7f00:1".toMixedString * res0: String = ::127.0.0.1 * scala> ipv6"ff3b:1234::ffab:7f00:1".toMixedString * res0: String = ff3b:1234::ffab:127.0.0.1 * }}} */ def toMixedString: String = { val bytes = toBytes val v4 = Ipv4Address.fromBytes(bytes.slice(12, 16)).get bytes(12) = 0 bytes(13) = 1 bytes(14) = 0 bytes(15) = 1 val s = Ipv6Address.unsafeFromBytes(bytes).toString val prefix = s.slice(0, s.size - 3) prefix + v4.toString } override def toUriString: String = s"[$toDefaultString]" /** Gets the IPv6 address after this address, with overflow from `ffff:ffff:....:ffff` to `::`. */ override def next: Ipv6Address = Ipv6Address.fromBigInt(toBigInt + 1) /** Gets the IPv6 address before this address, with underflow from `::` to `ffff:ffff:....:ffff`. */ override def previous: Ipv6Address = Ipv6Address.fromBigInt(toBigInt - 1) /** Converts this address to a 128-bit unsigned integer. */ def toBigInt: BigInt = { val bs = bytes var result = BigInt(0) for (i <- 0 until bs.size) { result = (result << 8) | (0x0ff & bs(i)) } result } override def isMulticast: Boolean = this >= Ipv6Address.MulticastRangeStart && this <= Ipv6Address.MulticastRangeEnd override def isSourceSpecificMulticast: Boolean = this >= Ipv6Address.SourceSpecificMulticastRangeStart && this <= Ipv6Address.SourceSpecificMulticastRangeEnd override def isLoopback: Boolean = this == Ipv6Address.Loopback || (isMappedV4 && collapseMappedV4.isLoopback) override def isLinkLocal: Boolean = Ipv6Address.LinkLocalBlock.contains(this) || (isMappedV4 && collapseMappedV4.isLinkLocal) override def isPrivate: Boolean = Ipv6Address.UniqueLocalBlock.contains(this) || (isMappedV4 && collapseMappedV4.isPrivate) /** Applies the supplied mask to this address. * * @example {{{ * scala> ipv6"ff3b::1".masked(ipv6"fff0::") * res0: Ipv6Address = ff30:: * }}} */ def masked(mask: Ipv6Address): Ipv6Address = Ipv6Address.fromBigInt(toBigInt & mask.toBigInt) /** Computes the last address in the network identified by applying the supplied mask to this address. * * @example {{{ * scala> ipv6"ff3b::1".maskedLast(ipv6"fff0::") * res0: Ipv6Address = ff3f:ffff:ffff:ffff:ffff:ffff:ffff:ffff * }}} */ def maskedLast(mask: Ipv6Address): Ipv6Address = Ipv6Address.fromBigInt(toBigInt & mask.toBigInt | ~mask.toBigInt) /** Returns a new address with the supplied scope id. */ def withScopeId(scopeId: String): Ipv6Address = new Ipv6Address(bytes, Some(scopeId)) /** Returns a new address with the scope id, if defined, removed. */ def withoutScopeId: Ipv6Address = new Ipv6Address(bytes, None) override def equals(other: Any): Boolean = other match { case that: Ipv6Address => java.util.Arrays.equals(bytes, that.bytes) && scopeId == that.scopeId case _ => false } override def hashCode: Int = { val hash = java.util.Arrays.hashCode(bytes) scopeId match { case None => hash case Some(s) => val h = MurmurHash3.mix(hash, s.##) MurmurHash3.finalizeHash(h, 2) } } } object Ipv6Address extends Ipv6AddressCompanionPlatform { /** Wildcard IPv6 address - 0.0.0.0 */ val Wildcard: Ipv6Address = fromBytes(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) /** First IP address in the IPv6 multicast range. */ val MulticastRangeStart: Ipv6Address = fromBytes(255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) /** Last IP address in the IPv6 multicast range. */ val MulticastRangeEnd: Ipv6Address = fromBytes(255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255) /** First IP address in the IPv6 source specific multicast range. */ val SourceSpecificMulticastRangeStart: Ipv6Address = fromBytes(255, 48, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) /** Last IP address in the IPv6 source specific multicast range. */ val SourceSpecificMulticastRangeEnd: Ipv6Address = fromBytes(255, 63, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255) /** CIDR which defines the mapped IPv4 address block (https://datatracker.ietf.org/doc/html/rfc4291#section-2.5.5.2). */ val MappedV4Block: Cidr[Ipv6Address] = Cidr(Ipv6Address.fromBytes(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0), 96) /** Alias for ::1. */ val Loopback: Ipv6Address = Ipv6Address.fromBytes(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1) /** CIDR which defines the unique local address block. */ val UniqueLocalBlock: Cidr[Ipv6Address] = Cidr(Ipv6Address.fromBytes(0xfc, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), 7) /** CIDR which defines the linked scope unicast address block. */ val LinkLocalBlock: Cidr[Ipv6Address] = Cidr(Ipv6Address.fromBytes(0xfe, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), 10) /** Parses an IPv6 address from a string in RFC4291 notation, returning `None` if the string is not a valid IPv6 * address. */ def fromString(value: String): Option[Ipv6Address] = { val scopeSeparator = value.indexOf('%') if (scopeSeparator < 0) { fromNonMixedString(value) orElse fromMixedString(value) } else { val addrPart = value.substring(0, scopeSeparator) val scopePart = value.substring(scopeSeparator + 1) val addr = fromNonMixedString(addrPart) orElse fromMixedString(addrPart) addr.map(_.withScopeId(scopePart)) } } private def fromNonMixedString(value: String): Option[Ipv6Address] = { var prefix: List[Int] = Nil var beforeCondenser = true var suffix: List[Int] = Nil val trimmed = value.trim() var result: Option[Ipv6Address] = null var idx = 0 var fieldStart = 0 while (idx < trimmed.length && (result eq null)) { val c = trimmed(idx) if ((c == ':' && idx > 0) || idx == trimmed.length - 1) { val field = trimmed.substring(fieldStart, if (c == ':') idx else idx + 1) if (field.size > 4) { result = None } else { try { val fieldValue = Integer.parseInt(field, 16) if (beforeCondenser) prefix = fieldValue :: prefix else suffix = fieldValue :: suffix } catch { case _: NumberFormatException => result = None } } } if (c == ':') { if (idx == trimmed.length - 1) { // String terminated in a single colon result = None } else if (trimmed(idx + 1) == ':') { beforeCondenser = false idx += 1 } fieldStart = idx + 1 } idx += 1 } // If after traversing all fields, there was no condenser, then fail unless there are exactly 8 fields if (beforeCondenser && (prefix.size + suffix.size) != 8) result = None if (result ne null) { result } else if (prefix.isEmpty && suffix.isEmpty && (trimmed.isEmpty || trimmed != "::")) { None } else { val bytes = new Array[Byte](16) idx = 0 val prefixSize = prefix.size val suffixSize = suffix.size val numCondensedZeroes = 8 - (prefixSize + suffixSize) if (numCondensedZeroes < 0) { None } else { var prefixIdx = prefixSize - 1 while (prefixIdx >= 0) { val value = prefix(prefixIdx) bytes(idx) = (value >> 8).toByte bytes(idx + 1) = value.toByte prefixIdx -= 1 idx += 2 } idx += numCondensedZeroes * 2 var suffixIdx = suffixSize - 1 while (suffixIdx >= 0) { val value = suffix(suffixIdx) bytes(idx) = (value >> 8).toByte bytes(idx + 1) = value.toByte suffixIdx -= 1 idx += 2 } Some(unsafeFromBytes(bytes)) } } } private val MixedStringFormat = """([:a-fA-F0-9]+:)(\d+\.\d+\.\d+\.\d+)""".r private def fromMixedString(value: String): Option[Ipv6Address] = value match { case MixedStringFormat(prefix, v4Str) => for { pfx <- fromNonMixedString(prefix + "0:0") v4 <- Ipv4Address.fromString(v4Str) } yield { val bytes = pfx.toBytes val v4bytes = v4.toBytes bytes(12) = v4bytes(0) bytes(13) = v4bytes(1) bytes(14) = v4bytes(2) bytes(15) = v4bytes(3) unsafeFromBytes(bytes) } case _ => None } /** Constructs an IPv6 address from a 16-element byte array. Returns `Some` when array is exactly 16-bytes and `None` * otherwise. */ def fromBytes(bytes: Array[Byte]): Option[Ipv6Address] = if (bytes.size == 16) Some(unsafeFromBytes(bytes.clone)) else None private def unsafeFromBytes(bytes: Array[Byte]): Ipv6Address = new Ipv6Address(bytes, None) /** Constructs an address from the specified 16 bytes. * * Each byte is represented as an `Int` to avoid having to manually call `.toByte` on each value -- the `toByte` call * is done inside this function. */ def fromBytes( b0: Int, b1: Int, b2: Int, b3: Int, b4: Int, b5: Int, b6: Int, b7: Int, b8: Int, b9: Int, b10: Int, b11: Int, b12: Int, b13: Int, b14: Int, b15: Int ): Ipv6Address = { val bytes = new Array[Byte](16) bytes(0) = b0.toByte bytes(1) = b1.toByte bytes(2) = b2.toByte bytes(3) = b3.toByte bytes(4) = b4.toByte bytes(5) = b5.toByte bytes(6) = b6.toByte bytes(7) = b7.toByte bytes(8) = b8.toByte bytes(9) = b9.toByte bytes(10) = b10.toByte bytes(11) = b11.toByte bytes(12) = b12.toByte bytes(13) = b13.toByte bytes(14) = b14.toByte bytes(15) = b15.toByte unsafeFromBytes(bytes) } /** Constructs an IPv6 address from a `BigInt`, using the lower 128-bits. */ def fromBigInt(value: BigInt): Ipv6Address = { val bytes = new Array[Byte](16) var rem = value for (i <- 15 to 0 by -1) { bytes(i) = (rem & 0x0ff).toByte rem = rem >> 8 } unsafeFromBytes(bytes) } /** Computes a mask by setting the first / left-most `n` bits high. * * @example {{{ * scala> Ipv6Address.mask(32) * res0: Ipv6Address = ffff:ffff:: * }}} */ def mask(bits: Int): Ipv6Address = { val b = if (bits < 0) 0 else if (bits > 128) 128 else bits Ipv6Address .fromBigInt { if (b == 128) (BigInt(-1L) << 64) | BigInt(-1L) else if (b < 64) BigInt(~(-1L >>> b)) << 64 else (BigInt(-1L) << 64) | BigInt(~(-1L >>> (b - 64))) } } } /** Internationalized domain name, as specified by RFC3490 and RFC5891. * * This type models internationalized hostnames. An IDN provides unicode labels, a unicode string form, and an ASCII * hostname form. * * A well formed IDN consists of one or more labels separated by dots. Each label may contain unicode characters as * long as the converted ASCII form meets the requirements of RFC1123 (e.g., 63 or fewer characters and no leading or * trailing dash). A dot is represented as an ASCII period or one of the unicode dots: full stop, ideographic full * stop, fullwidth full stop, halfwidth ideographic full stop. * * The `toString` method returns the IDN in the form in which it was constructed. Sometimes it is useful to normalize * the IDN -- converting all dots to an ASCII period and converting all labels to lowercase. * * Note: equality and comparison of IDNs is case-sensitive. Consider comparing normalized toString values for a more * lenient notion of equality. */ final class IDN private (val labels: List[IDN.Label], val hostname: Hostname, override val toString: String) extends Host { /** Converts this IDN to lower case and replaces dots with ASCII periods. */ def normalized: IDN = { val newLabels = labels.map(l => new IDN.Label(l.toString.toLowerCase)) new IDN(newLabels, hostname.normalized, newLabels.toList.mkString(".")) } override def hashCode: Int = MurmurHash3.stringHash(toString, "IDN".hashCode) override def equals(other: Any): Boolean = other match { case that: IDN => toString == that.toString case _ => false } } object IDN { /** Label component of an IDN. */ final class Label private[IDN] (override val toString: String) extends Serializable with Ordered[Label] { def compare(that: Label): Int = toString.compare(that.toString) override def hashCode: Int = MurmurHash3.stringHash(toString, "Label".hashCode) override def equals(other: Any): Boolean = other match { case that: Label => toString == that.toString case _ => false } } private val DotPattern = "[\\.\u002e\u3002\uff0e\uff61]" private val DotPatternR = DotPattern.r.pattern /** Constructs a `IDN` from a string. */ def fromString(value: String): Option[IDN] = value.size match { case 0 => None case _ => val labels = value .split(DotPattern) .iterator .map(new Label(_)) .toList Option(labels).filterNot(_.isEmpty).flatMap { ls => val hostname = toAscii(value).flatMap(Hostname.fromString) hostname.map(h => new IDN(ls, h, value)) } } /** Converts the supplied (ASCII) hostname in to an IDN. */ def fromHostname(hostname: Hostname): IDN = { val labels = hostname.labels.map(l => new Label(toUnicode(l.toString))) new IDN(labels, hostname, labels.toList.mkString(".")) } private[ip4s] def toAscii(value: String): Option[String] = DotPatternR .split(value, -1) .toList .traverse { label => if (label.forall(_ < 128)) Some(label) else Bootstring.encodeRaw(PunycodeParams)(label).toOption.map("xn--" + _) } .map(_.mkString(".")) private[ip4s] def toUnicode(value: String): String = DotPatternR .split(value, -1) .toList .traverse { label => if (label.startsWith("xn--")) Bootstring.decodeRaw(PunycodeParams)(label.substring(4, label.length)) else Right(label) } .map(_.mkString(".")) .fold(e => throw new IllegalArgumentException(e), identity[String](_)) implicit val show: Show[IDN] = Show.fromToString[IDN] } ================================================ FILE: shared/src/main/scala/com/comcast/ip4s/MacAddress.scala ================================================ /* * Copyright 2018 Comcast Cable Communications Management, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.comcast.ip4s import scala.util.control.NonFatal import cats.{Order, Show} /** 6-byte MAC address. */ final class MacAddress private (private val bytes: Array[Byte]) extends Ordered[MacAddress] with Serializable { def toBytes: Array[Byte] = bytes.clone def toLong: Long = { val bs = bytes var result = 0L for (i <- 0 until bs.size) { result = (result << 8) | (0x0ff & bs(i)) } result } override def compare(that: MacAddress): Int = { var i, result = 0 val tb = that.bytes val sz = bytes.length while (i < sz && result == 0) { result = Integer.compare(bytes(i) & 0xff, tb(i) & 0xff) i += 1 } result } override def equals(other: Any): Boolean = other match { case that: MacAddress => java.util.Arrays.equals(bytes, that.bytes) case _ => false } override def hashCode: Int = java.util.Arrays.hashCode(bytes) override def toString: String = bytes.map(b => f"${0xff & b}%02x").mkString("", ":", "") } object MacAddress { /** Constructs a `MacAddress` from a 6-element byte array. Returns `Some` when array is exactly 6-bytes and `None` * otherwise. */ def fromBytes(bytes: Array[Byte]): Option[MacAddress] = { if (bytes.length == 6) Some(new MacAddress(bytes)) else None } /** Constructs a `MacAddress` from the specified 6 bytes. * * Each byte is represented as an `Int` to avoid having to manually call `.toByte` on each value -- the `toByte` call * is done inside this function. */ def fromBytes( b0: Int, b1: Int, b2: Int, b3: Int, b4: Int, b5: Int ): MacAddress = { val bytes = new Array[Byte](6) bytes(0) = b0.toByte bytes(1) = b1.toByte bytes(2) = b2.toByte bytes(3) = b3.toByte bytes(4) = b4.toByte bytes(5) = b5.toByte new MacAddress(bytes) } /** Constructs a `MacAddress` from a `Long`, using the lower 48-bits. */ def fromLong(value: Long): MacAddress = { val bytes = new Array[Byte](6) var rem = value for (i <- 5 to 0 by -1) { bytes(i) = (rem & 0x0ff).toByte rem = rem >> 8 } new MacAddress(bytes) } /** Parses a `MacAddress` from a string, returning `None` if the string is not a valid mac. */ def fromString(value: String): Option[MacAddress] = { val trimmed = value.trim val fields = trimmed.split(':') if (fields.length == 6) { val result = new Array[Byte](6) var i = 0 while (i < result.length) { val field = fields(i) if (field.size == 2) { try { result(i) = (0xff & Integer.parseInt(field, 16)).toByte i += 1 } catch { case NonFatal(_) => return None } } else return None } Some(new MacAddress(result)) } else None } implicit val order: Order[MacAddress] = Order.fromComparable[MacAddress] implicit val show: Show[MacAddress] = Show.fromToString[MacAddress] } ================================================ FILE: shared/src/main/scala/com/comcast/ip4s/Multicast.scala ================================================ /* * Copyright 2018 Comcast Cable Communications Management, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.comcast.ip4s import cats.{Order, Show} /** Witness that the wrapped address of type `A` is a multicast address. * * An instance of `Multicast` is typically created by either calling `Multicast.apply` or by using the `asMulticast` * method on `IpAddress`. */ sealed trait Multicast[+A <: IpAddress] extends Product with Serializable { def address: A } object Multicast { private case class DefaultMulticast[+A <: IpAddress](address: A) extends Multicast[A] { override def toString: String = address.toString override def equals(that: Any): Boolean = that match { case m: Multicast[?] => address == m.address case _ => false } override def hashCode: Int = address.hashCode } /** Constructs a multicast IP address. Returns `None` is the supplied address is not in the valid multicast range. */ def fromIpAddress[A <: IpAddress](address: A): Option[Multicast[A]] = if (address.isSourceSpecificMulticast) Some(SourceSpecificMulticast.unsafeCreate(address)) else if (address.isMulticast) Some(DefaultMulticast(address)) else None implicit def order[J[x <: IpAddress] <: Multicast[x], A <: IpAddress]: Order[J[A]] = Order.fromOrdering(Multicast.ordering[J, A]) implicit def ordering[J[x <: IpAddress] <: Multicast[x], A <: IpAddress]: Ordering[J[A]] = Ordering.by(_.address) implicit def show[J[x <: IpAddress] <: Multicast[x], A <: IpAddress]: Show[J[A]] = Show.fromToString[J[A]] } /** Witnesses that the wrapped address of type `A` is a source specific multicast address. * * An instance of `SourceSpecificMulticast` is typically created by either calling `Multicast.apply` or by using * `asSourceSpecificMulticast` and `asSourceSpecificMulticastLenient` methods on `IpAddress`. */ sealed trait SourceSpecificMulticast[+A <: IpAddress] extends Multicast[A] { /** Ensures the referenced address is in the RFC defined source specific address range. */ def strict: Option[SourceSpecificMulticast.Strict[A]] = if (address.isSourceSpecificMulticast) Some(SourceSpecificMulticast.unsafeCreateStrict(address)) else None override def toString: String = address.toString } object SourceSpecificMulticast { /** Indicates the address is within the RFC defined source specific multicast range. */ trait Strict[+A <: IpAddress] extends SourceSpecificMulticast[A] private case class DefaultSourceSpecificMulticast[+A <: IpAddress](address: A) extends SourceSpecificMulticast[A] { override def toString: String = address.toString override def equals(that: Any): Boolean = that match { case m: Multicast[?] => address == m.address case _ => false } override def hashCode: Int = address.hashCode } /** Constructs a source specific multicast IP address. Returns `None` if the supplied address is not in the valid * source specific multicast range. */ def fromIpAddress[A <: IpAddress](address: A): Option[SourceSpecificMulticast.Strict[A]] = if (address.isSourceSpecificMulticast) Some(unsafeCreateStrict(address)) else None /** Constructs a source specific multicast IP address. Unlike `fromIpAddress`, multicast addresses outside the RFC * defined source specific range are allowed. */ def fromIpAddressLenient[A <: IpAddress](address: A): Option[SourceSpecificMulticast[A]] = if (address.isMulticast) Some(unsafeCreate(address)) else None private[ip4s] def unsafeCreate[A <: IpAddress](address: A): SourceSpecificMulticast[A] = DefaultSourceSpecificMulticast(address) private[ip4s] def unsafeCreateStrict[A <: IpAddress](address: A): SourceSpecificMulticast.Strict[A] = new DefaultSourceSpecificMulticast(address) with Strict[A] implicit def ordering[A <: IpAddress]: Ordering[SourceSpecificMulticast[A]] = Multicast.ordering[SourceSpecificMulticast, A] } ================================================ FILE: shared/src/main/scala/com/comcast/ip4s/MulticastJoin.scala ================================================ /* * Copyright 2018 Comcast Cable Communications Management, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.comcast.ip4s import cats.{Order, Show} /** Represents a join of a multicast group. * * This is represented as an ADT consisting of two constructors, [[AnySourceMulticastJoin]] and * [[SourceSpecificMulticastJoin]]. These constructors are provided as top level types to allow domain modeling where a * specific join type is required. The address type is parameterized for a similar reason -- to allow domain modeling * where a specific address type is required. */ sealed abstract class MulticastJoin[+A <: IpAddress] extends Product with Serializable { /** Converts this join to a value of type `A` using the supplied functions. */ def fold[B](asm: AnySourceMulticastJoin[A] => B, ssm: SourceSpecificMulticastJoin[A] => B): B = this match { case a: AnySourceMulticastJoin[A] => asm(a) case a: SourceSpecificMulticastJoin[A] => ssm(a) } /** Narrows to an `AnySourceMulticastJoin`. */ def asAsm: Option[AnySourceMulticastJoin[A]] = fold(Some(_), _ => None) /** Narrows to a `SourceSpecificMulticastJoin`. */ def asSsm: Option[SourceSpecificMulticastJoin[A]] = fold(_ => None, Some(_)) /** Returns the source address and group address. If this join is an any-source join, `None` is returned for the * source. Otherwise, this join is a source specific join and `Some(src)` is returned for the source. */ def sourceAndGroup: (Option[A], Multicast[A]) = fold(asm => (None, asm.group), ssm => (Some(ssm.source), ssm.group)) override def toString: String = fold(asm => asm.group.toString, ssm => s"${ssm.source}@${ssm.group}") } object MulticastJoin { /** Constructs an `AnySourceMulticastJoin[A]`. */ def asm[A <: IpAddress](group: Multicast[A]): MulticastJoin[A] = AnySourceMulticastJoin(group) /** Constructs a `SourceSpecificMulticastJoin[A]`. */ def ssm[A <: IpAddress](source: A, group: SourceSpecificMulticast[A]): MulticastJoin[A] = SourceSpecificMulticastJoin(source, group) def fromString(value: String): Option[MulticastJoin[IpAddress]] = fromStringGeneric(value, IpAddress.fromString) def fromString4(value: String): Option[MulticastJoin[Ipv4Address]] = fromStringGeneric(value, Ipv4Address.fromString) def fromString6(value: String): Option[MulticastJoin[Ipv6Address]] = fromStringGeneric(value, Ipv6Address.fromString) private val Pattern = """(?:([^@]+)@)?(.+)""".r private[ip4s] def fromStringGeneric[A <: IpAddress]( value: String, parse: String => Option[A] ): Option[MulticastJoin[A]] = value match { case Pattern(sourceStr, groupStr) => Option(sourceStr) match { case Some(sourceStr) => for { source <- parse(sourceStr) group <- parse(groupStr).flatMap(_.asSourceSpecificMulticastLenient) } yield ssm(source, group) case None => for { group <- parse(groupStr).flatMap(_.asSourceSpecificMulticastLenient) } yield asm(group) } case _ => None } implicit def order[J[x <: IpAddress] <: MulticastJoin[x], A <: IpAddress]: Order[J[A]] = Order.fromOrdering(MulticastJoin.ordering[J, A]) implicit def ordering[J[x <: IpAddress] <: MulticastJoin[x], A <: IpAddress]: Ordering[J[A]] = Ordering.by(_.sourceAndGroup) implicit def show[J[x <: IpAddress] <: MulticastJoin[x], A <: IpAddress]: Show[J[A]] = Show.fromToString[J[A]] } /** Multicast join to a group without a source filter. */ final case class AnySourceMulticastJoin[+A <: IpAddress](group: Multicast[A]) extends MulticastJoin[A] object AnySourceMulticastJoin { def fromString(value: String): Option[AnySourceMulticastJoin[IpAddress]] = MulticastJoin.fromStringGeneric(value, IpAddress.fromString).flatMap(_.asAsm) def fromString4(value: String): Option[AnySourceMulticastJoin[Ipv4Address]] = MulticastJoin.fromStringGeneric(value, Ipv4Address.fromString).flatMap(_.asAsm) def fromString6(value: String): Option[AnySourceMulticastJoin[Ipv6Address]] = MulticastJoin.fromStringGeneric(value, Ipv6Address.fromString).flatMap(_.asAsm) } /** Multicast join to a group from the specified source. */ final case class SourceSpecificMulticastJoin[+A <: IpAddress](source: A, group: SourceSpecificMulticast[A]) extends MulticastJoin[A] object SourceSpecificMulticastJoin { def fromString(value: String): Option[SourceSpecificMulticastJoin[IpAddress]] = MulticastJoin.fromStringGeneric(value, IpAddress.fromString).flatMap(_.asSsm) def fromString4(value: String): Option[SourceSpecificMulticastJoin[Ipv4Address]] = MulticastJoin.fromStringGeneric(value, Ipv4Address.fromString).flatMap(_.asSsm) def fromString6(value: String): Option[SourceSpecificMulticastJoin[Ipv6Address]] = MulticastJoin.fromStringGeneric(value, Ipv6Address.fromString).flatMap(_.asSsm) } ================================================ FILE: shared/src/main/scala/com/comcast/ip4s/MulticastSocketAddress.scala ================================================ /* * Copyright 2018 Comcast Cable Communications Management, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.comcast.ip4s import cats.{Order, Show} /** A multicast join of the specified type and a port number. Used to describe UDP join of a multicast group. */ final case class MulticastSocketAddress[J[+x <: IpAddress] <: MulticastJoin[x], +A <: IpAddress]( join: J[A], port: Port ) { /** Narrows join to an `AnySourceMulticastJoin`. */ def asAsm: Option[MulticastSocketAddress[AnySourceMulticastJoin, A]] = join.asAsm.map(j => MulticastSocketAddress(j, port)) /** Narrows join to a `SourceSpecificMulticastJoin`. */ def asSsm: Option[MulticastSocketAddress[SourceSpecificMulticastJoin, A]] = join.asSsm.map(j => MulticastSocketAddress(j, port)) override def toString: String = { val (source, group) = join.sourceAndGroup group.address.fold( _ => s"$join:$port", _ => source match { case None => s"[${group.address}]:$port" case Some(src) => s"[$src]@[${group.address}]:$port" } ) } } object MulticastSocketAddress { def fromString(value: String): Option[MulticastSocketAddress[MulticastJoin, IpAddress]] = fromString4(value) orElse fromString6(value) private val V4Pattern = """(?:([^@]+)@)?([^:]+):(\d+)""".r def fromString4(value: String): Option[MulticastSocketAddress[MulticastJoin, Ipv4Address]] = fromStringGeneric(value, V4Pattern, Ipv4Address.fromString) private val V6Pattern = """(?:\[([^\]]+)\]@)?\[([^\]]+)\]:(\d+)""".r def fromString6(value: String): Option[MulticastSocketAddress[MulticastJoin, Ipv6Address]] = fromStringGeneric(value, V6Pattern, Ipv6Address.fromString) def anySourceFromString(value: String): Option[MulticastSocketAddress[AnySourceMulticastJoin, IpAddress]] = anySourceFromString4(value) orElse anySourceFromString6(value) def anySourceFromString6(value: String): Option[MulticastSocketAddress[AnySourceMulticastJoin, Ipv6Address]] = fromString6(value).flatMap(_.asAsm) def anySourceFromString4(value: String): Option[MulticastSocketAddress[AnySourceMulticastJoin, Ipv4Address]] = fromString4(value).flatMap(_.asAsm) def sourceSpecificFromString(value: String): Option[MulticastSocketAddress[SourceSpecificMulticastJoin, IpAddress]] = sourceSpecificFromString4(value) orElse sourceSpecificFromString6(value) def sourceSpecificFromString6( value: String ): Option[MulticastSocketAddress[SourceSpecificMulticastJoin, Ipv6Address]] = fromString6(value).flatMap(_.asSsm) def sourceSpecificFromString4( value: String ): Option[MulticastSocketAddress[SourceSpecificMulticastJoin, Ipv4Address]] = fromString4(value).flatMap(_.asSsm) private def fromStringGeneric[A <: IpAddress]( value: String, pattern: util.matching.Regex, parse: String => Option[A] ): Option[MulticastSocketAddress[MulticastJoin, A]] = { val Pattern = pattern value match { case Pattern(sourceStr, groupStr, portStr) => Option(sourceStr) match { case Some(sourceStr) => for { source <- parse(sourceStr) group <- parse(groupStr).flatMap(_.asSourceSpecificMulticastLenient) port <- Port.fromString(portStr) } yield MulticastSocketAddress(MulticastJoin.ssm(source, group), port) case None => for { group <- parse(groupStr).flatMap(_.asMulticast) port <- Port.fromString(portStr) } yield MulticastSocketAddress(MulticastJoin.asm(group), port) } case _ => None } } implicit def order[J[+x <: IpAddress] <: MulticastJoin[x], A <: IpAddress]: Order[MulticastSocketAddress[J, A]] = Order.fromOrdering(MulticastSocketAddress.ordering[J, A]) implicit def ordering[J[+x <: IpAddress] <: MulticastJoin[x], A <: IpAddress] : Ordering[MulticastSocketAddress[J, A]] = Ordering.by(x => (x.join, x.port)) implicit def show[J[+x <: IpAddress] <: MulticastJoin[x], A <: IpAddress]: Show[MulticastSocketAddress[J, A]] = Show.fromToString[MulticastSocketAddress[J, A]] } ================================================ FILE: shared/src/main/scala/com/comcast/ip4s/NetworkInterface.scala ================================================ /* * Copyright 2018 Comcast Cable Communications Management, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.comcast.ip4s /** Description of a network interface. * * Note this is an immutable description, representing a snapshot of the state of the network interface at the time the operating system was queried. * * To get an instance of `NetworkInterface`, use `NetworkInterfaces[F].getAll`. */ sealed trait NetworkInterface { /** Unique name for the interface. */ def name: String /** Descriptive name of the interface, suitable for use on user interfaces. */ def displayName: String /** MAC address of the interface, if available. */ def macAddress: Option[MacAddress] /** IP addresses associated with the interface. */ def addresses: List[Cidr[IpAddress]] /** True if the interface is a loopback interface. */ def isLoopback: Boolean /** True if the interface is up/active. */ def isUp: Boolean } object NetworkInterface extends NetworkInterfaceCompanionPlatform { def apply( name: String, displayName: String, macAddress: Option[MacAddress], addresses: List[Cidr[IpAddress]], isLoopback: Boolean, isUp: Boolean ): NetworkInterface = DefaultNetworkInterface(name, displayName, macAddress, addresses, isLoopback, isUp) private case class DefaultNetworkInterface( name: String, displayName: String, macAddress: Option[MacAddress], addresses: List[Cidr[IpAddress]], isLoopback: Boolean, isUp: Boolean ) extends NetworkInterface { override def toString: String = s"NetworkInterface($name, $displayName, $macAddress, $addresses, $isLoopback, $isUp)" } } ================================================ FILE: shared/src/main/scala/com/comcast/ip4s/NetworkInterfaces.scala ================================================ /* * Copyright 2018 Comcast Cable Communications Management, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.comcast.ip4s import cats.effect.{IO, Sync} import cats.syntax.all.* /** Capability for an effect `F[_]` which can query for local network interfaces. * * An instance is available for any effect which has a `Sync` instance. */ sealed trait NetworkInterfaces[F[_]] { /** Gets a map of network interfaces by name. */ def getAll: F[Map[String, NetworkInterface]] /** Gets the network interface with the specified name. */ def getByName(name: String): F[Option[NetworkInterface]] /** Gets the network interface with the specified address. */ def getByAddress(address: IpAddress): F[Option[NetworkInterface]] /** Gets the network interfaces with the specified MAC address. */ def getByMacAddress(address: MacAddress): F[List[NetworkInterface]] } object NetworkInterfaces extends NetworkInterfacesCompanionPlatform { private[ip4s] abstract class SyncNetworkInterfaces[F[_]: Sync] extends NetworkInterfaces[F] { def getByName(name: String): F[Option[NetworkInterface]] = getAll.map(_.get(name)) def getByAddress(address: IpAddress): F[Option[NetworkInterface]] = getAll.map { all => all.values.collectFirst { case iface if iface.addresses.exists(_.address == address) => iface } } def getByMacAddress(address: MacAddress): F[List[NetworkInterface]] = getAll.map { all => all.values.collect { case iface if iface.macAddress == Some(address) => iface }.toList } } def apply[F[_]](implicit F: NetworkInterfaces[F]): F.type = F implicit def forIO: NetworkInterfaces[IO] = forSync[IO] } ================================================ FILE: shared/src/main/scala/com/comcast/ip4s/Port.scala ================================================ /* * Copyright 2018 Comcast Cable Communications Management, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.comcast.ip4s import scala.util.Try import scala.util.hashing.MurmurHash3 import cats.{Order, Show} /** TCP or UDP port number. */ final class Port private (val value: Int) extends Product with Serializable with Ordered[Port] { def copy(value: Int): Option[Port] = Port.fromInt(value) def compare(that: Port): Int = value.compare(that.value) override def toString: String = value.toString override def hashCode: Int = MurmurHash3.productHash(this, productPrefix.hashCode) override def equals(other: Any): Boolean = other match { case that: Port => value == that.value case _ => false } override def canEqual(other: Any): Boolean = other.isInstanceOf[Port] override def productArity: Int = 1 override def productElement(n: Int): Any = if (n == 0) value else throw new IndexOutOfBoundsException } object Port { final val MinValue = 0 final val MaxValue = 65535 final val Wildcard: Port = new Port(0) def fromInt(value: Int): Option[Port] = if (value >= MinValue && value <= MaxValue) Some(new Port(value)) else None def fromString(value: String): Option[Port] = Try(value.toInt).toOption.flatMap(fromInt) def unapply(p: Port): Option[Int] = Some(p.value) implicit val order: Order[Port] = Order.fromComparable[Port] implicit val show: Show[Port] = Show.fromToString[Port] } ================================================ FILE: shared/src/main/scala/com/comcast/ip4s/SocketAddress.scala ================================================ /* * Copyright 2018 Comcast Cable Communications Management, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.comcast.ip4s import cats.{Applicative, Order, Show} import cats.syntax.all._ /** An IP address of the specified type and a port number. Used to describe the source or destination of a socket. */ final case class SocketAddress[+A <: Host](host: A, port: Port) extends GenSocketAddress with SocketAddressPlatform[A] { override def toString: String = host match { case _: Ipv6Address => s"[$host]:$port" case _ => s"$host:$port" } /** Resolves this `SocketAddress[Hostname]` to a `SocketAddress[IpAddress]`. */ final def resolve[F[_]: Dns: Applicative]: F[SocketAddress[IpAddress]] = host.resolve.map(SocketAddress(_, port)) } object SocketAddress extends SocketAddressCompanionPlatform { /** Alias for 0.0.0.0:0. */ val Wildcard: SocketAddress[Host] = SocketAddress(Ipv4Address.Wildcard, Port.Wildcard) /** Alias for 0.0.0.0:port. */ def port(port: Port): SocketAddress[Host] = SocketAddress(Ipv4Address.Wildcard, port) def fromString(value: String): Option[SocketAddress[Host]] = fromStringIp(value) orElse fromStringHostname(value) orElse fromStringIDN(value) def fromStringIp(value: String): Option[SocketAddress[IpAddress]] = fromString4(value) orElse fromString6(value) private val UnescapedPattern = """([^:]+):(\d+)""".r def fromString4(value: String): Option[SocketAddress[Ipv4Address]] = value match { case UnescapedPattern(ip, port) => for { addr <- Ipv4Address.fromString(ip) prt <- Port.fromString(port) } yield SocketAddress(addr, prt) case _ => None } private val V6Pattern = """\[(.+)\]:(\d+)""".r def fromString6(value: String): Option[SocketAddress[Ipv6Address]] = value match { case V6Pattern(ip, port) => for { addr <- Ipv6Address.fromString(ip) prt <- Port.fromString(port) } yield SocketAddress(addr, prt) case _ => None } def fromStringHostname(value: String): Option[SocketAddress[Hostname]] = value match { case UnescapedPattern(s, port) => for { hostname <- Hostname.fromString(s) prt <- Port.fromString(port) } yield SocketAddress(hostname, prt) case _ => None } def fromStringIDN(value: String): Option[SocketAddress[IDN]] = value match { case UnescapedPattern(s, port) => for { idn <- IDN.fromString(s) prt <- Port.fromString(port) } yield SocketAddress(idn, prt) case _ => None } implicit def order[A <: Host]: Order[SocketAddress[A]] = Order.fromOrdering(SocketAddress.ordering[A]) implicit def ordering[A <: Host]: Ordering[SocketAddress[A]] = Ordering.by(x => (x.host: Host, x.port)) implicit def show[A <: Host]: Show[SocketAddress[A]] = Show.fromToString[SocketAddress[A]] } ================================================ FILE: shared/src/main/scala/com/comcast/ip4s/UnixSocketAddress.scala ================================================ /* * Copyright 2018 Comcast Cable Communications Management, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.comcast.ip4s import cats.{Order, Show} final case class UnixSocketAddress(path: String) extends GenSocketAddress with Ordered[UnixSocketAddress] { override def toString: String = path override def compare(that: UnixSocketAddress): Int = path.compare(that.path) } object UnixSocketAddress { implicit val order: Order[UnixSocketAddress] = Order.fromComparable[UnixSocketAddress] implicit val show: Show[UnixSocketAddress] = Show.fromToString[UnixSocketAddress] } ================================================ FILE: shared/src/main/scala-2/Literals.scala ================================================ /* * Copyright 2018 Comcast Cable Communications Management, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.comcast.ip4s import org.typelevel.literally.Literally /** Macros that support literal string interpolators. */ object Literals { object ip extends Literally[IpAddress] { def validate(c: Context)(s: String): Either[String, c.Expr[IpAddress]] = { import c.universe._ IpAddress.fromString(s) match { case Some(_) => Right(c.Expr(q"_root_.com.comcast.ip4s.IpAddress.fromString($s).get")) case None => Left("invalid IP address") } } def make(c: Context)(args: c.Expr[Any]*): c.Expr[IpAddress] = apply(c)(args*) } object ipv4 extends Literally[Ipv4Address] { def validate(c: Context)(s: String): Either[String, c.Expr[Ipv4Address]] = { import c.universe._ Ipv4Address.fromString(s) match { case Some(_) => Right(c.Expr(q"_root_.com.comcast.ip4s.Ipv4Address.fromString($s).get")) case None => Left("invalid IPv4 address") } } def make(c: Context)(args: c.Expr[Any]*): c.Expr[Ipv4Address] = apply(c)(args*) } object ipv6 extends Literally[Ipv6Address] { def validate(c: Context)(s: String): Either[String, c.Expr[Ipv6Address]] = { import c.universe._ Ipv6Address.fromString(s) match { case Some(_) => Right(c.Expr(q"_root_.com.comcast.ip4s.Ipv6Address.fromString($s).get")) case None => Left("invalid IPv6 address") } } def make(c: Context)(args: c.Expr[Any]*): c.Expr[Ipv6Address] = apply(c)(args*) } object mip extends Literally[Multicast[IpAddress]] { def validate(c: Context)(s: String): Either[String, c.Expr[Multicast[IpAddress]]] = { import c.universe._ IpAddress.fromString(s).flatMap(_.asMulticast) match { case Some(_) => Right(c.Expr(q"_root_.com.comcast.ip4s.IpAddress.fromString($s).get.asMulticast.get")) case None => Left("invalid IP multicast address") } } def make(c: Context)(args: c.Expr[Any]*): c.Expr[Multicast[IpAddress]] = apply(c)(args*) } object mipv4 extends Literally[Multicast[Ipv4Address]] { def validate(c: Context)(s: String): Either[String, c.Expr[Multicast[Ipv4Address]]] = { import c.universe._ Ipv4Address.fromString(s).flatMap(_.asMulticast) match { case Some(_) => Right(c.Expr(q"_root_.com.comcast.ip4s.Ipv4Address.fromString($s).get.asMulticast.get")) case None => Left("invalid IPv4 multicast address") } } def make(c: Context)(args: c.Expr[Any]*): c.Expr[Multicast[Ipv4Address]] = apply(c)(args*) } object mipv6 extends Literally[Multicast[Ipv6Address]] { def validate(c: Context)(s: String): Either[String, c.Expr[Multicast[Ipv6Address]]] = { import c.universe._ Ipv6Address.fromString(s).flatMap(_.asMulticast) match { case Some(_) => Right(c.Expr(q"_root_.com.comcast.ip4s.Ipv6Address.fromString($s).get.asMulticast.get")) case None => Left("invalid IPv6 multicast address") } } def make(c: Context)(args: c.Expr[Any]*): c.Expr[Multicast[Ipv6Address]] = apply(c)(args*) } object ssmip extends Literally[SourceSpecificMulticast.Strict[IpAddress]] { def validate(c: Context)(s: String): Either[String, c.Expr[SourceSpecificMulticast.Strict[IpAddress]]] = { import c.universe._ IpAddress.fromString(s).flatMap(_.asSourceSpecificMulticast) match { case Some(_) => Right(c.Expr(q"_root_.com.comcast.ip4s.IpAddress.fromString($s).get.asSourceSpecificMulticast.get")) case None => Left("invalid source specific IP multicast address") } } def make(c: Context)(args: c.Expr[Any]*): c.Expr[SourceSpecificMulticast.Strict[IpAddress]] = apply(c)(args*) } object ssmipv4 extends Literally[SourceSpecificMulticast.Strict[Ipv4Address]] { def validate(c: Context)(s: String): Either[String, c.Expr[SourceSpecificMulticast.Strict[Ipv4Address]]] = { import c.universe._ Ipv4Address.fromString(s).flatMap(_.asSourceSpecificMulticast) match { case Some(_) => Right(c.Expr(q"_root_.com.comcast.ip4s.Ipv4Address.fromString($s).get.asSourceSpecificMulticast.get")) case None => Left("invalid source specific IPv4 multicast address") } } def make(c: Context)(args: c.Expr[Any]*): c.Expr[SourceSpecificMulticast.Strict[Ipv4Address]] = apply(c)(args*) } object ssmipv6 extends Literally[SourceSpecificMulticast.Strict[Ipv6Address]] { def validate(c: Context)(s: String): Either[String, c.Expr[SourceSpecificMulticast.Strict[Ipv6Address]]] = { import c.universe._ Ipv6Address.fromString(s).flatMap(_.asSourceSpecificMulticast) match { case Some(_) => Right(c.Expr(q"_root_.com.comcast.ip4s.Ipv6Address.fromString($s).get.asSourceSpecificMulticast.get")) case None => Left("invalid source specific IPv6 multicast address") } } def make(c: Context)(args: c.Expr[Any]*): c.Expr[SourceSpecificMulticast.Strict[Ipv6Address]] = apply(c)(args*) } object port extends Literally[Port] { def validate(c: Context)(s: String): Either[String, c.Expr[Port]] = { import c.universe._ scala.util.Try(s.toInt).toOption.flatMap(Port.fromInt) match { case Some(_) => Right(c.Expr(q"_root_.com.comcast.ip4s.Port.fromInt($s.toInt).get")) case None => Left("invalid port") } } def make(c: Context)(args: c.Expr[Any]*): c.Expr[Port] = apply(c)(args*) } object hostname extends Literally[Hostname] { def validate(c: Context)(s: String): Either[String, c.Expr[Hostname]] = { import c.universe._ Hostname.fromString(s) match { case Some(_) => Right(c.Expr(q"_root_.com.comcast.ip4s.Hostname.fromString($s).get")) case None => Left("invalid hostname") } } def make(c: Context)(args: c.Expr[Any]*): c.Expr[Hostname] = apply(c)(args*) } object idn extends Literally[IDN] { def validate(c: Context)(s: String): Either[String, c.Expr[IDN]] = { import c.universe._ IDN.fromString(s) match { case Some(_) => Right(c.Expr(q"_root_.com.comcast.ip4s.IDN.fromString($s).get")) case None => Left("invalid IDN") } } def make(c: Context)(args: c.Expr[Any]*): c.Expr[IDN] = apply(c)(args*) } } ================================================ FILE: shared/src/main/scala-2/package.scala ================================================ /* * Copyright 2018 Comcast Cable Communications Management, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.comcast package object ip4s extends ip4splatform { final implicit class IpLiteralSyntax(val sc: StringContext) extends AnyVal { def ip(args: Any*): IpAddress = macro Literals.ip.make def ipv4(args: Any*): Ipv4Address = macro Literals.ipv4.make def ipv6(args: Any*): Ipv6Address = macro Literals.ipv6.make def mip(args: Any*): Multicast[IpAddress] = macro Literals.mip.make def mipv4(args: Any*): Multicast[Ipv4Address] = macro Literals.mipv4.make def mipv6(args: Any*): Multicast[Ipv6Address] = macro Literals.mipv6.make def ssmip(args: Any*): SourceSpecificMulticast.Strict[IpAddress] = macro Literals.ssmip.make def ssmipv4(args: Any*): SourceSpecificMulticast.Strict[Ipv4Address] = macro Literals.ssmipv4.make def ssmipv6(args: Any*): SourceSpecificMulticast.Strict[Ipv6Address] = macro Literals.ssmipv6.make def port(args: Any*): Port = macro Literals.port.make def host(args: Any*): Hostname = macro Literals.hostname.make def idn(args: Any*): IDN = macro Literals.idn.make } } ================================================ FILE: shared/src/main/scala-2.12/com/comcast/ip4s/CollectionCompat.scala ================================================ /* * Copyright 2018 Comcast Cable Communications Management, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.comcast.ip4s import scala.collection.JavaConverters._ private[ip4s] object Ip4sCollectionCompat { implicit class JIterableOps[A](private val self: java.lang.Iterable[A]) extends AnyVal { def asScala: Iterable[A] = iterableAsScalaIterable(self) } implicit class ListOps[A](private val self: List[A]) extends AnyVal { def asJava: java.util.List[A] = seqAsJavaList(self) } implicit class EnumerationOps[A](private val self: java.util.Enumeration[A]) extends AnyVal { def asScala: Iterator[A] = enumerationAsScalaIterator(self) } } ================================================ FILE: shared/src/main/scala-2.13/com/comcast/ip4s/CollectionCompat.scala ================================================ /* * Copyright 2018 Comcast Cable Communications Management, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.comcast.ip4s import scala.jdk.CollectionConverters._ private[ip4s] object Ip4sCollectionCompat { implicit class JIterableOps[A](private val self: java.lang.Iterable[A]) extends AnyVal { def asScala: Iterable[A] = IterableHasAsScala(self).asScala } implicit class ListOps[A](private val self: List[A]) extends AnyVal { def asJava: java.util.List[A] = SeqHasAsJava(self).asJava } implicit class EnumerationOps[A](private val self: java.util.Enumeration[A]) extends AnyVal { def asScala: Iterator[A] = EnumerationHasAsScala(self).asScala } } ================================================ FILE: shared/src/main/scala-3/Literals.scala ================================================ /* * Copyright 2018 Comcast Cable Communications Management, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.comcast.ip4s import org.typelevel.literally.Literally extension (inline ctx: StringContext) inline def ip(inline args: Any*): IpAddress = ${ Literals.ip('ctx, 'args) } inline def ipv4(inline args: Any*): Ipv4Address = ${ Literals.ipv4('ctx, 'args) } inline def ipv6(inline args: Any*): Ipv6Address = ${ Literals.ipv6('ctx, 'args) } inline def mip(inline args: Any*): Multicast[IpAddress] = ${ Literals.mip('ctx, 'args) } inline def mipv4(inline args: Any*): Multicast[Ipv4Address] = ${ Literals.mipv4('ctx, 'args) } inline def mipv6(inline args: Any*): Multicast[Ipv6Address] = ${ Literals.mipv6('ctx, 'args) } inline def ssmip(inline args: Any*): SourceSpecificMulticast.Strict[IpAddress] = ${ Literals.ssmip('ctx, 'args) } inline def ssmipv4(inline args: Any*): SourceSpecificMulticast.Strict[Ipv4Address] = ${ Literals.ssmipv4('ctx, 'args) } inline def ssmipv6(inline args: Any*): SourceSpecificMulticast.Strict[Ipv6Address] = ${ Literals.ssmipv6('ctx, 'args) } inline def port(inline args: Any*): Port = ${ Literals.port('ctx, 'args) } inline def host(inline args: Any*): Hostname = ${ Literals.host('ctx, 'args) } inline def idn(inline args: Any*): IDN = ${ Literals.idn('ctx, 'args) } object Literals: object ip extends Literally[IpAddress]: def validate(s: String)(using Quotes) = IpAddress.fromString(s) match case Some(_) => Right('{ _root_.com.comcast.ip4s.IpAddress.fromString(${ Expr(s) }).get }) case None => Left("Invalid IP address") object ipv4 extends Literally[Ipv4Address]: def validate(s: String)(using Quotes) = Ipv4Address.fromString(s) match case Some(_) => Right('{ _root_.com.comcast.ip4s.Ipv4Address.fromString(${ Expr(s) }).get }) case None => Left("Invalid IPv4 address") object ipv6 extends Literally[Ipv6Address]: def validate(s: String)(using Quotes) = Ipv6Address.fromString(s) match case Some(_) => Right('{ _root_.com.comcast.ip4s.Ipv6Address.fromString(${ Expr(s) }).get }) case None => Left("Invalid IPv6 address") object mip extends Literally[Multicast[IpAddress]]: def validate(s: String)(using Quotes) = IpAddress.fromString(s).flatMap(_.asMulticast) match case Some(_) => Right('{ _root_.com.comcast.ip4s.IpAddress.fromString(${ Expr(s) }).get.asMulticast.get }) case None => Left("Invalid IP multicast address") object mipv4 extends Literally[Multicast[Ipv4Address]]: def validate(s: String)(using Quotes) = Ipv4Address.fromString(s).flatMap(_.asMulticast) match case Some(_) => Right('{ _root_.com.comcast.ip4s.Ipv4Address.fromString(${ Expr(s) }).get.asMulticast.get }) case None => Left("Invalid IPv4 multicast address") object mipv6 extends Literally[Multicast[Ipv6Address]]: def validate(s: String)(using Quotes) = Ipv6Address.fromString(s).flatMap(_.asMulticast) match case Some(_) => Right('{ _root_.com.comcast.ip4s.Ipv6Address.fromString(${ Expr(s) }).get.asMulticast.get }) case None => Left("Invalid IPv6 multicast address") object ssmip extends Literally[SourceSpecificMulticast.Strict[IpAddress]]: def validate(s: String)(using Quotes) = IpAddress.fromString(s).flatMap(_.asSourceSpecificMulticast) match case Some(_) => Right('{ _root_.com.comcast.ip4s.IpAddress.fromString(${ Expr(s) }).get.asSourceSpecificMulticast.get }) case None => Left("Invalid source specific IP multicast address") object ssmipv4 extends Literally[SourceSpecificMulticast.Strict[Ipv4Address]]: def validate(s: String)(using Quotes) = Ipv4Address.fromString(s).flatMap(_.asSourceSpecificMulticast) match case Some(_) => Right('{ _root_.com.comcast.ip4s.Ipv4Address.fromString(${ Expr(s) }).get.asSourceSpecificMulticast.get }) case None => Left("Invalid source specific IPv4 multicast address") object ssmipv6 extends Literally[SourceSpecificMulticast.Strict[Ipv6Address]]: def validate(s: String)(using Quotes) = Ipv6Address.fromString(s).flatMap(_.asSourceSpecificMulticast) match case Some(_) => Right('{ _root_.com.comcast.ip4s.Ipv6Address.fromString(${ Expr(s) }).get.asSourceSpecificMulticast.get }) case None => Left("Invalid source specific IPv6 multicast address") object port extends Literally[Port]: def validate(s: String)(using Quotes) = s.toIntOption.flatMap(Port.fromInt) match case Some(_) => Right('{ _root_.com.comcast.ip4s.Port.fromInt(${ Expr(s.toInt) }).get }) case None => Left("Invalid port") object host extends Literally[Hostname]: def validate(s: String)(using Quotes) = Hostname.fromString(s) match case Some(_) => Right('{ _root_.com.comcast.ip4s.Hostname.fromString(${ Expr(s) }).get }) case None => Left("Invalid hostname") object idn extends Literally[IDN]: def validate(s: String)(using Quotes) = IDN.fromString(s) match case Some(_) => Right('{ _root_.com.comcast.ip4s.IDN.fromString(${ Expr(s) }).get }) case None => Left("Invalid IDN") ================================================ FILE: shared/src/main/scala-3/com/comcast/ip4s/CollectionCompat.scala ================================================ /* * Copyright 2018 Comcast Cable Communications Management, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.comcast.ip4s import scala.jdk.CollectionConverters.* private[ip4s] object Ip4sCollectionCompat { implicit class JIterableOps[A](private val self: java.lang.Iterable[A]) extends AnyVal { def asScala: Iterable[A] = IterableHasAsScala(self).asScala } implicit class ListOps[A](private val self: List[A]) extends AnyVal { def asJava: java.util.List[A] = SeqHasAsJava(self).asJava } implicit class EnumerationOps[A](private val self: java.util.Enumeration[A]) extends AnyVal { def asScala: Iterator[A] = EnumerationHasAsScala(self).asScala } } ================================================ FILE: shared/src/main/scala-3/package.scala ================================================ /* * Copyright 2018 Comcast Cable Communications Management, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.comcast package object ip4s extends ip4splatform ================================================ FILE: test-kit/js/src/test/scala/com/comcast/ip4s/TestPlatform.scala ================================================ /* * Copyright 2018 Comcast Cable Communications Management, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.comcast.ip4s object TestPlatform { def isJVM = false def isJS = true def isNative = false } ================================================ FILE: test-kit/jvm/src/test/scala/com/comcast/ip4s/Ipv6AddressJvmTest.scala ================================================ /* * Copyright 2018 Comcast Cable Communications Management, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.comcast.ip4s import com.google.common.net.InetAddresses import java.net.{InetAddress, Inet6Address} import org.scalacheck.{Arbitrary, Gen, Test} import org.scalacheck.Prop.forAll import Arbitraries._ class Ipv6AddressJvmTest extends BaseTestSuite { override protected def scalaCheckTestParameters: Test.Parameters = super.scalaCheckTestParameters.withMinSuccessfulTests(10000) test("to string - roundtrips through strings") { forAll(Gen.listOfN(16, Arbitrary.arbitrary[Byte])) { bytesList => if (bytesList.size == 16) { val bytes = bytesList.toArray val str = InetAddresses.toAddrString(InetAddress.getByAddress(bytes)) assertEquals(Ipv6Address.fromString(str).map(_.toString), Some(str)) } } } test("to string - follows RFC5952") { forAll(Gen.listOfN(16, Arbitrary.arbitrary[Byte])) { bytesList => if (bytesList.size == 16) { val bytes = bytesList.toArray val expected = InetAddresses.toAddrString(InetAddress.getByAddress(bytes)) assertEquals(Ipv6Address.fromBytes(bytes).map(_.toString), Some(expected)) } } } test("support conversion to Inet6Address") { forAll { (ip: Ipv6Address) => assert(ip.toInetAddress.isInstanceOf[Inet6Address]) } } test("toInetAddress with IPv4 mapped addresses") { assertEquals(ip"::ffff:f:f".toInetAddress, InetAddress.getByName("::ffff:f:f")) } } ================================================ FILE: test-kit/jvm/src/test/scala/com/comcast/ip4s/TestPlatform.scala ================================================ /* * Copyright 2018 Comcast Cable Communications Management, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.comcast.ip4s object TestPlatform { def isJVM = true def isJS = false def isNative = false } ================================================ FILE: test-kit/native/src/test/scala/com/comcast/ip4s/TestPlatform.scala ================================================ /* * Copyright 2018 Comcast Cable Communications Management, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.comcast.ip4s object TestPlatform { def isJVM = false def isJS = false def isNative = true } ================================================ FILE: test-kit/shared/src/main/scala/com/comcast/ip4s/Arbitraries.scala ================================================ /* * Copyright 2018 Comcast Cable Communications Management, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.comcast.ip4s import org.scalacheck.{Arbitrary, Gen} object Arbitraries { val ipv4Generator: Gen[Ipv4Address] = for { bytes <- Gen.listOfN(4, Arbitrary.arbitrary[Byte]) } yield Ipv4Address.fromBytes(bytes.toArray).get implicit val ipv4Arbitrary: Arbitrary[Ipv4Address] = Arbitrary(ipv4Generator) val ipv6Generator: Gen[Ipv6Address] = for { bytes <- Gen.listOfN(16, Arbitrary.arbitrary[Byte]) } yield Ipv6Address.fromBytes(bytes.toArray).get implicit val ipv6Arbitrary: Arbitrary[Ipv6Address] = Arbitrary(ipv6Generator) val ipGenerator: Gen[IpAddress] = Gen.oneOf(ipv4Generator, ipv6Generator) implicit val ipArbitrary: Arbitrary[IpAddress] = Arbitrary(ipGenerator) def cidrGenerator[A <: IpAddress](genIp: Gen[A]): Gen[Cidr[A]] = for { ip <- genIp bitLength = ip.fold(_ => 32, _ => 128) prefix <- Gen.chooseNum(0, bitLength) } yield ip / prefix implicit def cidrArbitrary[A <: IpAddress](implicit arbIp: Arbitrary[A]): Arbitrary[Cidr[A]] = Arbitrary(cidrGenerator(arbIp.arbitrary)) implicit def cidrStrictArbitrary[A <: IpAddress](implicit arbIp: Arbitrary[A]): Arbitrary[Cidr.Strict[A]] = Arbitrary( cidrGenerator(arbIp.arbitrary).map(_.normalized) ) val portGenerator: Gen[Port] = Gen.chooseNum(0, 65535).map(Port.fromInt(_).get) implicit val portArbitrary: Arbitrary[Port] = Arbitrary(portGenerator) def socketAddressGenerator[A <: IpAddress](genIp: Gen[A], genPort: Gen[Port]): Gen[SocketAddress[A]] = for { ip <- genIp port <- genPort } yield SocketAddress(ip, port) implicit def socketAddressArbitrary[A <: IpAddress](implicit arbIp: Arbitrary[A], arbPort: Arbitrary[Port] ): Arbitrary[SocketAddress[A]] = Arbitrary(socketAddressGenerator(arbIp.arbitrary, arbPort.arbitrary)) val multicastGenerator4: Gen[Multicast[Ipv4Address]] = for { ip <- ipv4Generator } yield Ipv4Address.fromLong(ip.toLong & ~(15 << 28) | (14 << 28)).asMulticast.get implicit val multicastArbitrary4: Arbitrary[Multicast[Ipv4Address]] = Arbitrary(multicastGenerator4) val multicastGenerator6: Gen[Multicast[Ipv6Address]] = for { ip <- ipv6Generator } yield Ipv6Address.fromBigInt(ip.toBigInt & ~(BigInt(255) << 120) | (BigInt(255) << 120)).asMulticast.get implicit val multicastArbitrary6: Arbitrary[Multicast[Ipv6Address]] = Arbitrary(multicastGenerator6) val multicastGenerator: Gen[Multicast[IpAddress]] = Gen.oneOf(multicastGenerator4, multicastGenerator6) implicit val multicastArbitrary: Arbitrary[Multicast[IpAddress]] = Arbitrary(multicastGenerator) def multicastJoinGenerator[A <: IpAddress](genSource: Gen[A], genGroup: Gen[Multicast[A]]): Gen[MulticastJoin[A]] = genGroup.flatMap { group => group.address.asSourceSpecificMulticast match { case Some(grp) => genSource.filter(_.getClass == grp.address.getClass).flatMap(src => MulticastJoin.ssm(src, grp)) case None => MulticastJoin.asm(group) } } implicit def multicastJoinArbitrary[A <: IpAddress](implicit arbSource: Arbitrary[A], arbGroup: Arbitrary[Multicast[A]] ): Arbitrary[MulticastJoin[A]] = Arbitrary(multicastJoinGenerator(arbSource.arbitrary, arbGroup.arbitrary)) def multicastSocketAddressGenerator[A <: IpAddress]( genJoin: Gen[MulticastJoin[A]], genPort: Gen[Port] ): Gen[MulticastSocketAddress[MulticastJoin, A]] = for { join <- genJoin port <- genPort } yield MulticastSocketAddress(join, port) implicit def multicastSocketAddressArbitrary[A <: IpAddress](implicit arbJoin: Arbitrary[MulticastJoin[A]], arbPort: Arbitrary[Port] ): Arbitrary[MulticastSocketAddress[MulticastJoin, A]] = Arbitrary(multicastSocketAddressGenerator(arbJoin.arbitrary, arbPort.arbitrary)) val hostnameGenerator: Gen[Hostname] = { val genLabel: Gen[String] = for { first <- Gen.alphaNumChar middleLen <- Gen.chooseNum(0, 61) middle <- Gen.listOfN(middleLen, Gen.oneOf(Gen.alphaNumChar, Gen.const('-'))).map(_.mkString) last <- if (middleLen > 0) Gen.alphaNumChar.map(Some(_)) else Gen.option(Gen.alphaNumChar) } yield first.toString + middle + last.fold("")(_.toString) for { numLabels <- Gen.chooseNum(1, 5) labels <- Gen.listOfN(numLabels, genLabel) if labels.foldLeft(0)(_ + _.size) < (253 - (numLabels - 1)) } yield Hostname.fromString(labels.mkString(".")).get } implicit val hostnameArbitrary: Arbitrary[Hostname] = Arbitrary(hostnameGenerator) lazy val idnGenerator: Gen[IDN] = { val genChar: Gen[Char] = Gen.oneOf(Gen.alphaNumChar, Gen.const('δ'), Gen.const('π'), Gen.const('θ')) val genLabel: Gen[String] = for { first <- genChar middleLen <- Gen.chooseNum(0, 61) middle <- Gen.listOfN(middleLen, Gen.oneOf(genChar, Gen.const('-'))).map(_.mkString) last <- if (middleLen > 0) genChar.map(Some(_)) else Gen.option(genChar) str = first.toString + middle + last.fold("")(_.toString) if IDN.toAscii(str).size < 64 } yield str for { numLabels <- Gen.chooseNum(1, 5) labels <- Gen.listOfN(numLabels, genLabel) dot <- Gen.oneOf('.', '\u002e', '\u3002', '\uff0e', '\uff61') idn = IDN.fromString(labels.mkString(dot.toString)) if idn.isDefined } yield idn.get } implicit def idnArbitrary: Arbitrary[IDN] = Arbitrary(idnGenerator) val macAddressGenerator: Gen[MacAddress] = for { bytes <- Gen.listOfN(6, Arbitrary.arbitrary[Byte]) } yield MacAddress.fromBytes(bytes.toArray).get implicit val macAddressArbitrary: Arbitrary[MacAddress] = Arbitrary(macAddressGenerator) } ================================================ FILE: test-kit/shared/src/test/scala/com/comcast/ip4s/BaseTestSuite.scala ================================================ /* * Copyright 2018 Comcast Cable Communications Management, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.comcast.ip4s import munit.ScalaCheckSuite import org.scalacheck.Test abstract class BaseTestSuite extends ScalaCheckSuite { override protected def scalaCheckTestParameters: Test.Parameters = super.scalaCheckTestParameters.withMinSuccessfulTests(5000) protected def group(name: String)(thunk: => Unit): Unit = { val countBefore = munitTestsBuffer.size val _ = thunk val countAfter = munitTestsBuffer.size val countRegistered = countAfter - countBefore val registered = munitTestsBuffer.toList.drop(countBefore) (0 until countRegistered).foreach(_ => munitTestsBuffer.remove(countBefore)) registered.foreach(t => munitTestsBuffer += t.withName(s"$name - ${t.name}")) } } ================================================ FILE: test-kit/shared/src/test/scala/com/comcast/ip4s/CidrStrictTest.scala ================================================ /* * Copyright 2018 Comcast Cable Communications Management, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.comcast.ip4s import org.scalacheck.Prop.forAll import Arbitraries._ class CidrStrictTest extends BaseTestSuite { property("prefix and address are identical") { forAll { (cidr: Cidr.Strict[IpAddress]) => assertEquals(cidr.address, cidr.prefix) } } } ================================================ FILE: test-kit/shared/src/test/scala/com/comcast/ip4s/CidrTest.scala ================================================ /* * Copyright 2018 Comcast Cable Communications Management, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.comcast.ip4s import org.scalacheck.Prop.forAll import Arbitraries._ class CidrTest extends BaseTestSuite { property("roundtrip through string") { forAll { (cidr: Cidr[IpAddress]) => assertEquals(Cidr.fromString(cidr.toString), Some(cidr)) } } property("fromIpAndMask") { forAll { (ip: IpAddress, prefixBits0: Int) => val max = ip.fold(_ => 32, _ => 128) val prefixBits = ((prefixBits0 % max).abs + 1) val maskInt = BigInt(-1) << (max - prefixBits) val mask = ip.fold(_ => Ipv4Address.fromLong(maskInt.toLong & 0xffffffff), _ => Ipv6Address.fromBigInt(maskInt)) assertEquals(Cidr.fromIpAndMask(ip, mask), Cidr(ip, prefixBits)) } } property("parsing from string: only masks with a valid length return a CIDR") { forAll { (ip: IpAddress, prefixBits: Int) => val cidr = Cidr.fromString(s"$ip/$prefixBits") val max = ip.fold(_ => 32, _ => 128) assertEquals(cidr.isDefined, (prefixBits <= max && prefixBits >= 0)) } } test("addresses should return iterator") { val cidr = Cidr(ipv4"10.0.0.0", 8) val addresses = cidr.addresses assertEquals(addresses.size, 16777216) } } ================================================ FILE: test-kit/shared/src/test/scala/com/comcast/ip4s/DnsTest.scala ================================================ /* * Copyright 2018 Comcast Cable Communications Management, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.comcast.ip4s import cats.effect.IO import cats.syntax.all._ import munit.CatsEffectSuite class DnsTest extends CatsEffectSuite { test("resolve/reverseAll round-trip") { for { hostname <- Hostname.fromString("comcast.com").liftTo[IO](new NoSuchElementException) address <- Dns[IO].resolve(hostname) hostnames <- Dns[IO].reverseAll(address) _ <- IO(assert(hostnames.nonEmpty)) reversedAddress <- hostnames.traverse(Dns[IO].resolve) } yield assert(reversedAddress.forall(_ == address)) } test("resolveAll/reverse round-trip") { for { hostname <- Hostname.fromString("comcast.com").liftTo[IO](new NoSuchElementException) addresses <- Dns[IO].resolveAll(hostname) _ <- IO(assert(addresses.nonEmpty)) hostnames <- addresses.traverse(Dns[IO].reverse) reversedAddresses <- hostnames.traverse(Dns[IO].resolve) } yield assertEquals(Set(addresses), Set(reversedAddresses)) } test("loopback") { val loopbacks = Set(ip"127.0.0.1", ip"::1") Dns[IO].loopback.flatMap { loopback => IO(assert(loopbacks.contains(clue(loopback)))) } } test("resolve unknown host") { (Dns[IO].resolve(host"not.example.com") >> IO.raiseError(new AssertionError("Did not raise `UnknownHostException`"))).recover { case ex: UnknownHostException => val msg = ex.getMessage val expected = List( "not.example.com: Name or service not known", "not.example.com: nodename nor servname provided, or not known", "not.example.com: No address associated with hostname" ) assert(expected.contains(msg), msg) } } test("reverse unknown ip") { Dns[IO].reverse(ip"240.0.0.0").interceptMessage[UnknownHostException]("240.0.0.0") } } ================================================ FILE: test-kit/shared/src/test/scala/com/comcast/ip4s/Examples.scala ================================================ /* * Copyright 2018 Comcast Cable Communications Management, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.comcast.ip4s object Examples { val localhost4: Ipv4Address = ipv4"127.0.0.1" val localhost6: Ipv6Address = ipv6"::1" val ssmv4: SourceSpecificMulticastJoin[Ipv4Address] = SourceSpecificMulticastJoin(localhost4, ssmipv4"232.10.10.10") val ssmv4WithPort: MulticastSocketAddress[SourceSpecificMulticastJoin, Ipv4Address] = MulticastSocketAddress(ssmv4, port"5555") val asmv4: AnySourceMulticastJoin[Ipv4Address] = AnySourceMulticastJoin(ssmipv4"232.10.10.10") val asmv4WithPort: MulticastSocketAddress[AnySourceMulticastJoin, Ipv4Address] = MulticastSocketAddress(asmv4, port"5555") } ================================================ FILE: test-kit/shared/src/test/scala/com/comcast/ip4s/HostnameTest.scala ================================================ /* * Copyright 2018 Comcast Cable Communications Management, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.comcast.ip4s import org.scalacheck.Prop.forAll import Arbitraries._ class HostnameTest extends BaseTestSuite { test("roundtrip through string") { forAll { (h: Hostname) => assertEquals(Hostname.fromString(h.toString), Some(h)) } } test("allow access to labels") { forAll { (h: Hostname) => assertEquals(Hostname.fromString(h.labels.toList.mkString(".")), Some(h)) } } test("require overall length be less than 254 chars") { forAll { (h: Hostname) => val hstr = h.toString val h2 = hstr + "." + hstr val expected = if (h2.length > 253) None else Some(Hostname.fromString(h2).get) assertEquals(Hostname.fromString(h2), expected) } } test("require labels be less than 64 chars") { forAll { (h: Hostname) => val hstr = h.toString val suffix = new String(Array.fill(63)(hstr.last)) val tooLong = hstr + suffix assertEquals(Hostname.fromString(tooLong), None) } } test("disallow labels that end in a dash") { forAll { (h: Hostname) => val hstr = h.toString val disallowed = hstr + "-" assertEquals(Hostname.fromString(disallowed), None) } } test("disallow labels that start with a dash") { forAll { (h: Hostname) => val hstr = h.toString val disallowed = "-" + hstr assertEquals(Hostname.fromString(disallowed), None) } } } ================================================ FILE: test-kit/shared/src/test/scala/com/comcast/ip4s/IDNTest.scala ================================================ /* * Copyright 2018 Comcast Cable Communications Management, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.comcast.ip4s import org.scalacheck.Prop.forAll import Arbitraries._ class IDNTest extends BaseTestSuite { test("support any hostname") { forAll { (h: Hostname) => val representable = h.labels.forall(l => !l.toString.toLowerCase.startsWith("xn--")) if (representable) { val i = IDN.fromHostname(h) assertEquals(i.hostname, h) assertEquals(IDN.fromString(h.toString), Some(i)) } } } test("roundtrip through string") { forAll { (i: IDN) => assertEquals(IDN.fromString(i.toString), Some(i)) } } test("allow access to labels") { forAll { (i: IDN) => assertEquals(IDN.fromString(i.labels.toList.mkString(".")).map(_.labels), Some(i.labels)) } } test("require overall ascii length be less than 254 chars") { forAll { (i: IDN) => val istr = i.toString val i2 = istr + "." + istr val expected = if (i.hostname.toString.length > (253 / 2)) None else Some(IDN.fromString(i2).get) assertEquals(IDN.fromString(i2), expected) } } test("require labels be less than 64 ascii chars") { forAll { (i: IDN) => val str = i.toString val suffix = new String(Array.fill(63)(str.last)) val tooLong = str + suffix assertEquals(IDN.fromString(tooLong), None) } } test("disallow labels that end in a dash") { forAll { (i: IDN) => val str = i.toString // Note: simply appending a dash to final label doesn't guarantee the ASCII encoded label ends with a dash val disallowed = str + ".a-" assertEquals(IDN.fromString(disallowed), None) } } test("disallow labels that start with a dash") { forAll { (i: IDN) => val str = i.toString val disallowed = "-a." + str assertEquals(IDN.fromString(disallowed), None) } } test("support normalization") { forAll { (i: IDN) => assertEquals(i.normalized, IDN.fromString(i.labels.map(_.toString.toLowerCase).toList.mkString(".")).get) } } } ================================================ FILE: test-kit/shared/src/test/scala/com/comcast/ip4s/IdnaSuite.scala ================================================ /* * Copyright 2018 Comcast Cable Communications Management, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // https://github.com/libuv/libuv/blob/97dcdb1926f6aca43171e1614338bcef067abd59/test/test-idna.c /* Copyright The libuv project and contributors. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. */ package com.comcast.ip4s class IdnaSuite extends BaseTestSuite { case class TestCase(decoded: String, encoded: Option[String]) object TestCase { def apply(decoded: String, encoded: String): TestCase = apply(decoded, Some(encoded)) } val testData = List( // No conversion TestCase("", ""), TestCase(".", "."), TestCase(".com", ".com"), TestCase("example", "example"), TestCase("example-", "example-"), TestCase("straße.de", "xn--strae-oqa.de"), // Test cases adapted from punycode.js. Most are from RFC 3492. TestCase("foo.bar", "foo.bar"), TestCase("mañana.com", "xn--maana-pta.com"), TestCase("example.com.", "example.com."), TestCase("bücher.com", "xn--bcher-kva.com"), TestCase("café.com", "xn--caf-dma.com"), TestCase("café.café.com", "xn--caf-dma.xn--caf-dma.com"), TestCase("☃-⌘.com", "xn----dqo34k.com"), TestCase("퐀☃-⌘.com", "xn----dqo34kn65z.com"), TestCase("💩.la", "xn--ls8h.la"), TestCase("mañana.com", "xn--maana-pta.com"), TestCase("mañana。com", "xn--maana-pta.com"), TestCase("mañana.com", "xn--maana-pta.com"), TestCase("mañana。com", "xn--maana-pta.com"), TestCase("ü", "xn--tda"), TestCase(".ü", ".xn--tda"), TestCase("ü.ü", "xn--tda.xn--tda"), TestCase("ü.ü.", "xn--tda.xn--tda."), TestCase("üëäö♥", "xn--4can8av2009b"), TestCase( "Willst du die Blüthe des frühen, die Früchte des späteren Jahres", "xn--Willst du die Blthe des frhen, die Frchte des spteren Jahres-x9e96lkal" ), TestCase("ليهمابتكلموشعربي؟", "xn--egbpdaj6bu4bxfgehfvwxn"), TestCase("他们为什么不说中文", "xn--ihqwcrb4cv8a8dqg056pqjye"), TestCase("他們爲什麽不說中文", "xn--ihqwctvzc91f659drss3x8bo0yb"), TestCase("Pročprostěnemluvíčesky", "xn--Proprostnemluvesky-uyb24dma41a"), TestCase("למההםפשוטלאמדבריםעברית", "xn--4dbcagdahymbxekheh6e0a7fei0b"), TestCase("यहलोगहिन्दीक्योंनहींबोलसकतेहैं", "xn--i1baa7eci9glrd9b2ae1bj0hfcgg6iyaf8o0a1dig0cd"), TestCase("なぜみんな日本語を話してくれないのか", "xn--n8jok5ay5dzabd5bym9f0cm5685rrjetr6pdxa"), TestCase("세계의모든사람들이한국어를이해한다면얼마나좋을까", "xn--989aomsvi5e83db1d2a355cv1e0vak1dwrv93d5xbh15a0dt30a5jpsd879ccm6fea98c"), TestCase("почемужеонинеговорятпорусски", "xn--b1abfaaepdrnnbgefbadotcwatmq2g4l"), TestCase("PorquénopuedensimplementehablarenEspañol", "xn--PorqunopuedensimplementehablarenEspaol-fmd56a"), TestCase("TạisaohọkhôngthểchỉnóitiếngViệt", "xn--TisaohkhngthchnitingVit-kjcr8268qyxafd2f1b9g"), TestCase("3年B組金八先生", "xn--3B-ww4c5e180e575a65lsy2b"), TestCase("安室奈美恵-with-SUPER-MONKEYS", "xn---with-SUPER-MONKEYS-pc58ag80a8qai00g7n9n"), TestCase("Hello-Another-Way-それぞれの場所", "xn--Hello-Another-Way--fc4qua05auwb3674vfr0b"), TestCase("ひとつ屋根の下2", "xn--2-u9tlzr9756bt3uc0v"), TestCase("MajiでKoiする5秒前", "xn--MajiKoi5-783gue6qz075azm5e"), TestCase("パフィーdeルンバ", "xn--de-jg4avhby1noc0d"), TestCase("そのスピードで", "xn--d9juau41awczczp"), TestCase("-> $1.00 <-", "-> $1.00 <-"), // Test cases from https://unicode.org/reports/tr46/ TestCase("faß.de", "xn--fa-hia.de"), TestCase("βόλος.com", "xn--nxasmm1c.com"), TestCase("ශ්‍රී.com", "xn--10cl1a0b660p.com"), TestCase("نامه‌ای.com", "xn--mgba3gch31f060k.com") ) group("toAscii") { testData.foreach { case TestCase(decoded, encoded) => test(decoded) { assertEquals(IDN.toAscii(decoded), encoded) } } } group("toUnicode") { testData.foreach { case TestCase(decoded, Some(encoded)) => test(encoded) { assertEquals(IDN.toUnicode(encoded), decoded.replaceAll("[\u002e\u3002\uff0e\uff61]", ".")) } case _ => () } } } ================================================ FILE: test-kit/shared/src/test/scala/com/comcast/ip4s/Ipv4AddressTest.scala ================================================ /* * Copyright 2018 Comcast Cable Communications Management, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.comcast.ip4s import org.scalacheck.{Arbitrary, Gen} import org.scalacheck.Prop.forAll import Arbitraries._ class Ipv4AddressTest extends BaseTestSuite { test("parsing from string - does not parse the empty string") { assertEquals(Ipv4Address.fromString(""), None) } test("parsing from string - does not parse white space string") { assertEquals(Ipv4Address.fromString(" "), None) } test("roundtrip through string") { forAll(Gen.listOfN(4, Arbitrary.arbitrary[Byte])) { bytesList => if (bytesList.size == 4) { val bytes = bytesList.toArray val addr = Ipv4Address.fromBytes(bytes).get assertEquals(Ipv4Address.fromString(addr.toString), Some(addr)) } } } test("roundtrip through long") { forAll(Gen.listOfN(4, Arbitrary.arbitrary[Byte])) { bytesList => if (bytesList.size == 4) { val bytes = bytesList.toArray val addr = Ipv4Address.fromBytes(bytes).get assertEquals(Ipv4Address.fromLong(addr.toLong), addr) } } } test("support ordering") { forAll { (left: Ipv4Address, right: Ipv4Address) => val longCompare = left.toLong.compare(right.toLong) val result = Ordering[Ipv4Address].compare(left, right) assertEquals(result, longCompare) } } test("support computing next IP") { assertEquals(Ipv4Address.fromString("255.255.255.255").map(_.next), Ipv4Address.fromString("0.0.0.0")) forAll { (ip: Ipv4Address) => assertEquals(ip.next, Ipv4Address.fromLong(ip.toLong + 1)) } } test("support computing previous IP") { assertEquals(Ipv4Address.fromString("0.0.0.0").map(_.previous), Ipv4Address.fromString("255.255.255.255")) forAll { (ip: Ipv4Address) => assertEquals(ip.previous, Ipv4Address.fromLong(ip.toLong - 1)) } } test("isPrivate") { assert(!ipv4"10.0.0.0".previous.isPrivate) assert(ipv4"10.0.0.0".isPrivate) assert(ipv4"10.255.255.255".isPrivate) assert(!ipv4"10.255.255.255".next.isPrivate) assert(!ipv4"172.16.0.0".previous.isPrivate) assert(ipv4"172.16.0.0".isPrivate) assert(ipv4"172.31.255.255".isPrivate) assert(!ipv4"172.31.255.255".next.isPrivate) assert(!ipv4"192.168.0.0".previous.isPrivate) assert(ipv4"192.168.0.0".isPrivate) assert(ipv4"192.168.255.255".isPrivate) assert(!ipv4"192.168.255.255".next.isPrivate) } test("isLoopback") { assert(ipv4"127.0.0.1".isLoopback) assert(ipv4"127.255.255.255".isLoopback) assert(!ipv4"128.0.0.0".isLoopback) } test("isLinkLocal") { assert(!ipv4"127.0.0.1".isLinkLocal) assert(ipv4"169.254.0.0".isLinkLocal) assert(ipv4"169.254.255.255".isLinkLocal) assert(!ipv4"169.254.255.255".next.isLinkLocal) } } ================================================ FILE: test-kit/shared/src/test/scala/com/comcast/ip4s/Ipv6AddressTest.scala ================================================ /* * Copyright 2018 Comcast Cable Communications Management, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.comcast.ip4s import org.scalacheck.{Arbitrary, Gen} import org.scalacheck.Prop.forAll import Arbitraries._ class Ipv6AddressTest extends BaseTestSuite { test("parsing from string - does not parse the empty string") { assertEquals(Ipv6Address.fromString(""), None) } test("parsing from string - does not parse white space string") { assertEquals(Ipv6Address.fromString(" "), None) } test("parsing from string - does not parse a single :") { assertEquals(Ipv6Address.fromString(":"), None) assertEquals(Ipv6Address.fromString(" : "), None) } test("parsing from string - does not parse invalid number of sections in ipv6") { assertEquals(Ipv6Address.fromString("::1:1:1:1:1:1:2:1:3"), None) assertEquals(Ipv6Address.fromString("1:1:1:1:1:1:2:1:3"), None) // https://github.com/Comcast/ip4s/issues/662 assertEquals(Ipv6Address.fromString("18ec:cb0e:5795:a7df:6c7d:902c:9bd4"), None) assertEquals(Ipv6Address.fromString("18ec:cb0e"), None) assertEquals(Ipv6Address.fromString("18ec:"), None) } test("parsing from string - does parse ::") { assertEquals(Ipv6Address.fromString("::").isDefined, true) assertEquals(Ipv6Address.fromString(" :: ").isDefined, true) } test("parsing from string - supports mixed strings") { forAll { (v4: Ipv4Address) => assertEquals(Ipv6Address.fromString("::" + v4), Some(v4.toCompatV6)) assertEquals(Ipv6Address.fromString("::ffff:" + v4), Some(v4.toMappedV6)) } } test("parsing from string - does not misinterpret hosts") { assertEquals(Ipv6Address.fromString("db"), None) } test("support converting to uncondensed string form") { forAll(Gen.listOfN(16, Arbitrary.arbitrary[Byte])) { bytesList => if (bytesList.size == 16) { val bytes = bytesList.toArray val addr = Ipv6Address.fromBytes(bytes).get assert(addr.toUncondensedString.size == 4 * 8 + 7) } } } test("roundtrip through uncondensed strings") { forAll(Gen.listOfN(16, Arbitrary.arbitrary[Byte])) { bytesList => if (bytesList.size == 16) { val bytes = bytesList.toArray val addr = Ipv6Address.fromBytes(bytes).get assertEquals(Ipv6Address.fromString(addr.toUncondensedString), Some(addr)) } } } test("support converting to mixed string form") { forAll { (v4: Ipv4Address) => assertEquals(v4.toCompatV6.toMixedString, "::" + v4) assertEquals(v4.toMappedV6.toMixedString, "::ffff:" + v4) } } test("roundtrip through mixed strings") { forAll(Gen.listOfN(16, Arbitrary.arbitrary[Byte])) { bytesList => if (bytesList.size == 16) { val bytes = bytesList.toArray val addr = Ipv6Address.fromBytes(bytes).get assertEquals(Ipv6Address.fromString(addr.toMixedString), Some(addr)) } } } test("roundtrip through BigInt") { forAll(Gen.listOfN(16, Arbitrary.arbitrary[Byte])) { bytesList => if (bytesList.size == 16) { val bytes = bytesList.toArray val addr = Ipv6Address.fromBytes(bytes).get assertEquals(Ipv6Address.fromBigInt(addr.toBigInt), addr) } } } test("support ordering") { forAll { (left: Ipv6Address, right: Ipv6Address) => val bigIntCompare = left.toBigInt.compare(right.toBigInt) val result = Ordering[Ipv6Address].compare(left, right) assertEquals(result, bigIntCompare) } } test("support computing next IP") { assertEquals( Ipv6Address.fromString("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff").map(_.next), Ipv6Address.fromString("::") ) forAll { (ip: Ipv6Address) => assertEquals(ip.next, Ipv6Address.fromBigInt(ip.toBigInt + 1)) } } test("support computing previous IP") { assertEquals( Ipv6Address.fromString("::").map(_.previous), Ipv6Address.fromString("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff") ) forAll { (ip: Ipv6Address) => assertEquals(ip.previous, Ipv6Address.fromBigInt(ip.toBigInt - 1)) } } test("converting V4 mapped address") { val addr = ip"::ffff:f:f" assertEquals[Any, Any](addr.getClass, classOf[Ipv6Address]) assertEquals(addr.version, IpVersion.V6) assertEquals(addr.toString, "::ffff:f:f") assertEquals[Any, Any](addr.collapseMappedV4.getClass, classOf[Ipv4Address]) assertEquals[Any, Any](addr.asIpv6, Some(addr)) assertEquals[Any, Any](addr.asIpv4, Some(ip"0.15.0.15")) } test("isPrivate") { assert(!ipv6"fc00::".previous.isPrivate) assert(ipv6"fc00::".isPrivate) assert(ipv6"fe00::".previous.isPrivate) assert(!ipv6"fe00::".isPrivate) // mapped v4 assert(ipv6"::ffff:10.1.1.1".isPrivate) } test("isLoopback") { assert(ipv6"::1".isLoopback) assert(ipv6"::ffff:127.0.0.1".isLoopback) } test("isLinkLocal") { assert(ipv6"fe80::1".isLinkLocal) assert(ipv6"::ffff:169.254.0.0".isLinkLocal) } test("scope ids") { assertEquals(ipv6"fe80::1".scopeId, None) assert(ipv6"fe80::1%en0".isLinkLocal) assertEquals(ipv6"fe80::1%en0".scopeId, Some("en0")) assert(ipv6"fe80::1" != ipv6"fe80::1%en0") assert(ipv6"fe80::1".## != ipv6"fe80::1%en0".##) } } ================================================ FILE: test-kit/shared/src/test/scala/com/comcast/ip4s/MacAddressTest.scala ================================================ /* * Copyright 2018 Comcast Cable Communications Management, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.comcast.ip4s import org.scalacheck.{Arbitrary, Gen, Prop} import Prop.forAll import Arbitraries._ class MacAddressTest extends BaseTestSuite { test("roundtrip through string") { forAll(Gen.listOfN(6, Arbitrary.arbitrary[Byte])) { bytesList => if (bytesList.size == 6) { val bytes = bytesList.toArray val addr = MacAddress.fromBytes(bytes).get assertEquals(MacAddress.fromString(addr.toString), Some(addr)) } } } test("support ordering") { forAll { (left: MacAddress, right: MacAddress) => val longCompare = left.toLong.compare(right.toLong) val result = Ordering[MacAddress].compare(left, right) assertEquals(result, longCompare) } } } ================================================ FILE: test-kit/shared/src/test/scala/com/comcast/ip4s/MulticastSocketAddressTest.scala ================================================ /* * Copyright 2018 Comcast Cable Communications Management, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.comcast.ip4s import org.scalacheck.Prop.forAll import Arbitraries._ class MulticastSocketAddressTest extends BaseTestSuite { test("roundtrip through string") { forAll { (msa: MulticastSocketAddress[MulticastJoin, IpAddress]) => assertEquals(MulticastSocketAddress.fromString(msa.toString), Some(msa)) } } } ================================================ FILE: test-kit/shared/src/test/scala/com/comcast/ip4s/MulticastTest.scala ================================================ /* * Copyright 2018 Comcast Cable Communications Management, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.comcast.ip4s import org.scalacheck.Prop.forAll import Arbitraries._ class MulticastTest extends BaseTestSuite { test("support equality") { forAll { (mip: Multicast[IpAddress]) => assertEquals(mip.address.asMulticast, Some(mip)) mip.address.asSourceSpecificMulticastLenient.foreach(x => assertEquals(mip, x)) mip.address.asSourceSpecificMulticastLenient.foreach(x => assert(x == mip)) } } test("support SSM outside source specific range") { assertEquals(ip"239.10.10.10".asSourceSpecificMulticast, None) assertEquals( ip"239.10.10.10".asSourceSpecificMulticastLenient, Some(SourceSpecificMulticast.unsafeCreate(ip"239.10.10.10")) ) } } ================================================ FILE: test-kit/shared/src/test/scala/com/comcast/ip4s/NetworkInterfacesTest.scala ================================================ /* * Copyright 2018 Comcast Cable Communications Management, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.comcast.ip4s import cats.effect.IO import cats.syntax.all.* import munit.CatsEffectSuite class NetworkInterfacesTest extends CatsEffectSuite { test("getAll") { NetworkInterfaces[IO].getAll.map { nis => assert(nis.nonEmpty) } } test("getByName") { NetworkInterfaces[IO].getAll.flatMap { nis => nis.values.toList.traverse { ni => NetworkInterfaces[IO].getByName(ni.name).map { nni => assertEquals(nni, Some(ni)) } } } } test("getByAddress") { NetworkInterfaces[IO].getAll.flatMap { nis => nis.values.toList.traverse { ni => ni.addresses.traverse { cidr => NetworkInterfaces[IO].getByAddress(cidr.address).map { nni => assertEquals(nni, Some(ni)) } } } } } test("getByMacAddress") { NetworkInterfaces[IO].getAll.flatMap { nis => nis.values.toList.traverse { ni => ni.macAddress.traverse { mac => NetworkInterfaces[IO].getByMacAddress(mac).map { nni => assert(nni.contains(ni)) } } } } } } ================================================ FILE: test-kit/shared/src/test/scala/com/comcast/ip4s/PortTest.scala ================================================ /* * Copyright 2018 Comcast Cable Communications Management, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.comcast.ip4s import org.scalacheck.Prop.forAll import Arbitraries._ class PortTest extends BaseTestSuite { test("roundtrip through string") { forAll { (p: Port) => assertEquals(Port.fromString(p.toString), Some(p)) } } } ================================================ FILE: test-kit/shared/src/test/scala/com/comcast/ip4s/SocketAddressTest.scala ================================================ /* * Copyright 2018 Comcast Cable Communications Management, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.comcast.ip4s import org.scalacheck.Prop.forAll import Arbitraries._ class SocketAddressTest extends BaseTestSuite { test("roundtrip through string") { forAll { (sa: SocketAddress[IpAddress]) => assertEquals(SocketAddress.fromString(sa.toString), Some(sa)) } } }