From 861662836269640d787cc3275a66197524483e62 Mon Sep 17 00:00:00 2001 From: Brian McKeon <135748266+brianjosephmckeon@users.noreply.github.com> Date: Fri, 1 Mar 2024 11:36:52 -0500 Subject: [PATCH] Prepare 1.7.8 release. Reformatted. Relaxed upper bounds on build dependencies. Use new .github workflows. --- .github/CODEOWNERS | 1 + .github/workflows/build.yaml | 11 + .github/workflows/release.yaml | 10 + .gitignore | 1 + CHANGELOG.md | 102 ++ Setup.hs | 2 - cabal.project | 2 + changelog.md | 97 -- fourmolu.yaml | 51 + ip.cabal | 129 +- src/Data/ByteString/Builder/Fixed.hs | 26 +- src/Data/Text/Builder/Common/Compat.hs | 1 + src/Data/Text/Builder/Common/Internal.hs | 60 +- src/Data/Text/Builder/Fixed.hs | 25 +- src/Data/Text/Builder/Variable.hs | 58 +- src/Data/Text/SmallBuilder.hs | 46 +- src/Data/Word/Synthetic/Word12.hs | 241 ++-- src/Net/IP.hs | 256 ++-- src/Net/IPv4.hs | 1359 ++++++++++++---------- src/Net/IPv6.hs | 1034 ++++++++-------- src/Net/Mac.hs | 866 +++++++------- src/Net/Types.hs | 28 +- test/Bench.hs | 167 +-- test/Doctests.hs | 23 +- test/IPv4ByteString1.hs | 75 +- test/IPv4DecodeText1.hs | 37 +- test/IPv4DecodeText2.hs | 37 +- test/IPv4Text1.hs | 32 +- test/IPv4Text2.hs | 26 +- test/IPv4TextVariableBuilder.hs | 25 +- test/Naive.hs | 34 +- test/Net/IPv4Spec.hs | 151 ++- test/Test.hs | 744 +++++++----- 33 files changed, 3189 insertions(+), 2568 deletions(-) create mode 100644 .github/CODEOWNERS create mode 100644 .github/workflows/build.yaml create mode 100644 .github/workflows/release.yaml create mode 100644 CHANGELOG.md delete mode 100644 Setup.hs create mode 100644 cabal.project delete mode 100644 changelog.md create mode 100644 fourmolu.yaml diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..f6c0b22 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +@byteverse/l3c diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 0000000..38c2a4c --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,11 @@ +name: build +on: + pull_request: + branches: + - "*" + +jobs: + call-workflow: + uses: byteverse/.github/.github/workflows/build-matrix.yaml@main + with: + cabal-file: ip.cabal diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..9411962 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,10 @@ +name: release +on: + push: + tags: + - "*" + +jobs: + call-workflow: + uses: byteverse/.github/.github/workflows/release.yaml@main + secrets: inherit diff --git a/.gitignore b/.gitignore index f89b58e..b4f606c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.vscode/ *.aux cabal-dev .cabal-sandbox diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..9bda147 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,102 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) +and this project adheres to the [Haskell Package Versioning Policy](https://pvp.haskell.org/). + +## 1.7.8 -- 2024-03-01 + +* Update package metadata. +* Relaxed upper bounds on dependencies. + +## 1.7.7 -- 2023-08-24 + +* Add `isSubsetOf` for `IPv4` and `IPv6` ranges. + +## 1.7.6 -- 2022-10-07 + +* Bump upper bound on `text` to `< 2.1`. +* Add `Hashable` instances for `IP` and `IPv6`. + +## 1.7.5 -- 2022-07-28 + +* Add `boundedBuilderOctetsBE` and `boundedBuilderOctetsLE` to `Net.IPv4`. +* Make doctests work again. Requires `doctest-0.20` or higher. +* Bump upper bound on `attoparsec` to `< 0.15`. +* Bump upper bound on `hashable` to `< 1.5`. +* Derive `Generic` for `Net.IPv6.IPv6`. +* Bump lower bound on `wide-word` to `>= 0.1.1.2`. +* Add compatibility with GHC 9.2.3. + +## 1.7.4 -- 2021-12-28 + +* Add `decodeUtf8Bytes` to `Net.IP`. +* Fix IPv4 octet overflow bug (#74) + +## 1.7.3 -- 2021-01-22 + +* Export `decodeOctets` from `Net.Mac`. +* Add `encodeShort` to `Net.Mac`. + +## 1.7.2 -- 2020-05-30 +* Fix bug in `doctest` documentation +* Bump upper bound on `aeson`: (< 1.5) -> (< 1.6) + +## 1.7.1 -- 2020-01-22 +* Deprecate 'decodeBytes' in favor of 'decodeOctets'. +* Add `Bytes`-oriented encode and decode functions to `Net.Mac`: + `boundedBuilderUtf8`, `decodeUtf8Bytes`, and `parserUtf8Bytes`. +* Add `parserRangeUtf8Bytes` and `parserRangeUtf8BytesLenient` to + both `Net.IPv4` and `Net.IPv6` modules. + +## 1.7.0 -- 2019-11-05 +* Add `Data` instances for all types. +* Add `Ix` instances for all address types. +* Add missing `ToJSON`/`FromJSON` instances for `IPv6Range`. +* Remove `Num`, `Integral`, and `Real` instances from `IPv6`. +* Remove `Bits` instance for `IPv4Range`. +* Switch to derived `Bits` instance for `IPv4. +* Remove old spec test for IPv4 Bits laws, instead use + quickcheck-classes. +* Bump exclusive upper bound on small-bytearray-builder + +## 1.6.0 -- 2019-09-30 +* Provide decode functions for decoding from `ShortText` and + from `Bytes`. These two are implemented internally using + the same function. +* Dependency on `bytesmith` effectively restricts users to + GHC 8.6 and up. Since GHC 8.8 is about to be released, + this is deemed an acceptable cost. +* Require cabal version 2.2 so that leading commas are accepted + in dependencies lists. + +## 1.5.1 -- 2019-07-29 +* Allow building with primitive-0.7. +* Add more doctests to Net.IP. +* Add to Net.IP: `isIPv4` and `isIPv6`. +* Bump lower bound on primitive from 0.6 to 0.6.4. +* Bump upper bound on hashable from < 1.3. to < 1.4. + +## 1.5.0 -- 2019-03-23 +* Implement `IPv6` using `wide-word`'s `Word128`. (This is a breaking change.) + +## 1.4.2.1 -- 2019-03-18 +* Docfix for `Net.IPv4.toList` + +## 1.4.2 -- 2019-03-14 +* Fix existing `spec` test suite. +* 100% haddock coverage, along with significantly more doctest coverage. +* Add `Net.IPv4.localhost` and `Net.IPv6.localhost`, aliases for `loopback`. + +## 1.4.1 -- 2018-08-19 +* Add `Enum` and `Bounded` instances for `Mac`. +* Add `NFData` instances for all types. + +## 1.4.0 -- 2018-07-18 +* Combine `Net.IPv4` and `Net.IPv4.Range` modules. +* Add `IPv6Range`. +* Drop support for older aeson. +* Add `Enum` instance for `IPv6`. + +## 1.2.1 -- 2018-05-10 +* Added a `Prim` instance for `Mac`. diff --git a/Setup.hs b/Setup.hs deleted file mode 100644 index 9a994af..0000000 --- a/Setup.hs +++ /dev/null @@ -1,2 +0,0 @@ -import Distribution.Simple -main = defaultMain diff --git a/cabal.project b/cabal.project new file mode 100644 index 0000000..7ef286b --- /dev/null +++ b/cabal.project @@ -0,0 +1,2 @@ +packages: . +tests: True diff --git a/changelog.md b/changelog.md deleted file mode 100644 index 3cfdc73..0000000 --- a/changelog.md +++ /dev/null @@ -1,97 +0,0 @@ -# Changelog -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) -and this project adheres to the [Haskell Package Versioning Policy](https://pvp.haskell.org/). - -## [1.7.7] - 2023-08-24 - -- Add `isSubsetOf` for `IPv4` and `IPv6` ranges. - -## [1.7.6] - 2022-10-07 - -- Bump upper bound on `text` to `< 2.1`. -- Add `Hashable` instances for `IP` and `IPv6`. - -## [1.7.5] - 2022-07-28 - -- Add `boundedBuilderOctetsBE` and `boundedBuilderOctetsLE` to `Net.IPv4`. -- Make doctests work again. Requires `doctest-0.20` or higher. -- Bump upper bound on `attoparsec` to `< 0.15`. -- Bump upper bound on `hashable` to `< 1.5`. -- Derive `Generic` for `Net.IPv6.IPv6`. -- Bump lower bound on `wide-word` to `>= 0.1.1.2`. -- Add compatibility with GHC 9.2.3. - -## [1.7.4] - 2021-12-28 - -- Add `decodeUtf8Bytes` to `Net.IP`. -- Fix IPv4 octet overflow bug (#74) - -## [1.7.3] - 2021-01-22 - -- Export `decodeOctets` from `Net.Mac`. -- Add `encodeShort` to `Net.Mac`. - -## [1.7.2] - 2020-05-30 -- Fix bug in `doctest` documentation -- Bump upper bound on `aeson`: (< 1.5) -> (< 1.6) - -## [1.7.1] - 2020-01-22 -- Deprecate 'decodeBytes' in favor of 'decodeOctets'. -- Add `Bytes`-oriented encode and decode functions to `Net.Mac`: - `boundedBuilderUtf8`, `decodeUtf8Bytes`, and `parserUtf8Bytes`. -- Add `parserRangeUtf8Bytes` and `parserRangeUtf8BytesLenient` to - both `Net.IPv4` and `Net.IPv6` modules. - -## [1.7.0] - 2019-11-05 -- Add `Data` instances for all types. -- Add `Ix` instances for all address types. -- Add missing `ToJSON`/`FromJSON` instances for `IPv6Range`. -- Remove `Num`, `Integral`, and `Real` instances from `IPv6`. -- Remove `Bits` instance for `IPv4Range`. -- Switch to derived `Bits` instance for `IPv4. -- Remove old spec test for IPv4 Bits laws, instead use - quickcheck-classes. -- Bump exclusive upper bound on small-bytearray-builder - -## [1.6.0] - 2019-09-30 -- Provide decode functions for decoding from `ShortText` and - from `Bytes`. These two are implemented internally using - the same function. -- Dependency on `bytesmith` effectively restricts users to - GHC 8.6 and up. Since GHC 8.8 is about to be released, - this is deemed an acceptable cost. -- Require cabal version 2.2 so that leading commas are accepted - in dependencies lists. - -## [1.5.1] - 2019-07-29 -- Allow building with primitive-0.7. -- Add more doctests to Net.IP. -- Add to Net.IP: `isIPv4` and `isIPv6`. -- Bump lower bound on primitive from 0.6 to 0.6.4. -- Bump upper bound on hashable from < 1.3. to < 1.4. - -## [1.5.0] - 2019-03-23 -- Implement `IPv6` using `wide-word`'s `Word128`. (This is a breaking change.) - -## [1.4.2.1] - 2019-03-18 -- Docfix for `Net.IPv4.toList` - -## [1.4.2] - 2019-03-14 -- Fix existing `spec` test suite. -- 100% haddock coverage, along with significantly more doctest coverage. -- Add `Net.IPv4.localhost` and `Net.IPv6.localhost`, aliases for `loopback`. - -## [1.4.1] - 2018-08-19 -- Add `Enum` and `Bounded` instances for `Mac`. -- Add `NFData` instances for all types. - -## [1.4.0] - 2018-07-18 -- Combine `Net.IPv4` and `Net.IPv4.Range` modules. -- Add `IPv6Range`. -- Drop support for older aeson. -- Add `Enum` instance for `IPv6`. - -## [1.2.1] - 2018-05-10 -- Added a `Prim` instance for `Mac`. diff --git a/fourmolu.yaml b/fourmolu.yaml new file mode 100644 index 0000000..40cd005 --- /dev/null +++ b/fourmolu.yaml @@ -0,0 +1,51 @@ +# Number of spaces per indentation step +indentation: 2 + +# Max line length for automatic line breaking +column-limit: 200 + +# Styling of arrows in type signatures (choices: trailing, leading, or leading-args) +function-arrows: trailing + +# How to place commas in multi-line lists, records, etc. (choices: leading or trailing) +comma-style: leading + +# Styling of import/export lists (choices: leading, trailing, or diff-friendly) +import-export-style: leading + +# Whether to full-indent or half-indent 'where' bindings past the preceding body +indent-wheres: false + +# Whether to leave a space before an opening record brace +record-brace-space: true + +# Number of spaces between top-level declarations +newlines-between-decls: 1 + +# How to print Haddock comments (choices: single-line, multi-line, or multi-line-compact) +haddock-style: multi-line + +# How to print module docstring +haddock-style-module: null + +# Styling of let blocks (choices: auto, inline, newline, or mixed) +let-style: auto + +# How to align the 'in' keyword with respect to the 'let' keyword (choices: left-align, right-align, or no-space) +in-style: right-align + +# Whether to put parentheses around a single constraint (choices: auto, always, or never) +single-constraint-parens: always + +# Output Unicode syntax (choices: detect, always, or never) +unicode: never + +# Give the programmer more choice on where to insert blank lines +respectful: true + +# Fixity information for operators +fixities: [] + +# Module reexports Fourmolu should know about +reexports: [] + diff --git a/ip.cabal b/ip.cabal index eb30bdb..ccfac60 100644 --- a/ip.cabal +++ b/ip.cabal @@ -1,15 +1,16 @@ -cabal-version: 2.2 -name: ip -version: 1.7.7 -synopsis: Library for IP and MAC addresses -homepage: https://github.com/andrewthad/haskell-ip#readme -license: BSD-3-Clause -license-file: LICENSE -author: Andrew Martin -maintainer: andrew.thaddeus@gmail.com -copyright: 2016 Andrew Martin -category: web -build-type: Simple +cabal-version: 3.0 +name: ip +version: 1.7.8 +synopsis: Library for IP and MAC addresses +homepage: https://github.com/byteverse/haskell-ip +bug-reports: https://github.com/byteverse/haskell-ip/issues +license: BSD-3-Clause +license-file: LICENSE +author: Andrew Martin +maintainer: amartin@layer3com.com +copyright: 2016 Andrew Martin +category: web +build-type: Simple description: The `ip` package provides types and functions for dealing with IPv4 addresses, CIDR blocks, and MAC addresses. We provide instances @@ -29,16 +30,29 @@ description: The following packages are intended to be used with this package: . * `yesod-ip`: Provides orphan instances needed to work with yesod and - persistent. Also, provides a `yesod-form` helper. + persistent. Also, provides a `yesod-form` helper. + +extra-doc-files: + CHANGELOG.md + README.md + +tested-with: GHC ==9.4.8 || ==9.6.3 || ==9.8.1 + +common build-settings + default-language: Haskell2010 + ghc-options: -Wall -Wunused-packages library - hs-source-dirs: src + import: build-settings + hs-source-dirs: src + ghc-options: -O2 exposed-modules: Net.IP Net.IPv4 Net.IPv6 Net.Mac Net.Types + other-modules: Data.ByteString.Builder.Fixed Data.Text.Builder.Common.Compat @@ -46,70 +60,71 @@ library Data.Text.Builder.Fixed Data.Text.Builder.Variable Data.Word.Synthetic.Word12 + build-depends: - , aeson >= 1.0 && < 2.3 - , attoparsec >= 0.13 && < 0.15 - , base >= 4.9 && < 5 - , byteslice >= 0.1.2 && < 0.3 - , bytesmith >= 0.3.9 && < 0.4 - , bytestring >= 0.10.8 && < 0.12 - , deepseq >= 1.4 && < 1.5 - , hashable >= 1.2 && < 1.5 - , natural-arithmetic >= 0.1 && <0.3 - , primitive >= 0.6.4 && < 0.10 - , bytebuild >= 0.3.4 && <0.4 - , text >= 1.2 && < 2.2 - , text-short >= 0.1.3 && < 0.2 - , vector >= 0.11 && < 0.14 - , wide-word >= 0.1.1.2 && < 0.2 - , word-compat >= 0.0.4 && <0.1 - ghc-options: -Wall -O2 - default-language: Haskell2010 + , aeson >=1.0 + , attoparsec >=0.13 + , base >=4.9 && <5 + , bytebuild >=0.3.4 + , byteslice >=0.1.2 + , bytesmith >=0.3.9 + , bytestring >=0.10.8 + , deepseq >=1.4 + , hashable >=1.2 + , natural-arithmetic >=0.1 + , primitive >=0.6.4 + , text >=1.2 + , text-short >=0.1.3 + , vector >=0.11 + , wide-word >=0.1.1.2 + , word-compat >=0.0.4 test-suite test - type: exitcode-stdio-1.0 - hs-source-dirs: test - main-is: Test.hs + import: build-settings + type: exitcode-stdio-1.0 + hs-source-dirs: test + main-is: Test.hs build-depends: - , HUnit - , QuickCheck , attoparsec , base , byteslice , bytestring + , HUnit , ip - , quickcheck-classes >= 0.4.13 && < 0.7.0.0 + , QuickCheck + , quickcheck-classes >=0.4.13 && <0.7.0.0 , tasty , tasty-hunit , tasty-quickcheck , text , text-short , wide-word + other-modules: IPv4ByteString1 IPv4Text1 IPv4Text2 Naive - ghc-options: -Wall -O2 - default-language: Haskell2010 test-suite spec - type: exitcode-stdio-1.0 - hs-source-dirs: test - main-is: Spec.hs + import: build-settings + type: exitcode-stdio-1.0 + hs-source-dirs: test + main-is: Spec.hs build-depends: , base - , hspec >= 2.5.5 + , hspec >=2.5.5 , ip - , wide-word - other-modules: - Net.IPv4Spec - ghc-options: -Wall -O2 - default-language: Haskell2010 - build-tool-depends: hspec-discover:hspec-discover >= 2.5.5 + + other-modules: Net.IPv4Spec + build-tool-depends: hspec-discover:hspec-discover >=2.5.5 benchmark criterion - type: exitcode-stdio-1.0 + import: build-settings + hs-source-dirs: test + main-is: Bench.hs + ghc-options: -O2 + type: exitcode-stdio-1.0 build-depends: , attoparsec , base @@ -118,8 +133,9 @@ benchmark criterion , criterion , ip , primitive - , text , random + , text + other-modules: IPv4ByteString1 IPv4DecodeText1 @@ -127,12 +143,7 @@ benchmark criterion IPv4Text1 IPv4Text2 Naive - ghc-options: -Wall -O2 - default-language: Haskell2010 - hs-source-dirs: test - main-is: Bench.hs source-repository head - type: git - location: https://github.com/andrewthad/haskell-ip - + type: git + location: git://github.com/byteverse/haskell-ip.git diff --git a/src/Data/ByteString/Builder/Fixed.hs b/src/Data/ByteString/Builder/Fixed.hs index d7638c9..d43a9c3 100644 --- a/src/Data/ByteString/Builder/Fixed.hs +++ b/src/Data/ByteString/Builder/Fixed.hs @@ -4,10 +4,9 @@ {-# LANGUAGE MagicHash #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} - {-# OPTIONS_GHC -Wall -funbox-strict-fields #-} -{-| For concatenating fixed-width strings that are only a few +{- | For concatenating fixed-width strings that are only a few characters each, this can be six times faster than the builder that ships with @bytestring@. -} @@ -27,14 +26,14 @@ module Data.ByteString.Builder.Fixed #if !MIN_VERSION_base(4,11,0) import Data.Monoid #endif -import Data.Word -import Data.Word.Synthetic.Word12 (Word12) import Data.Bits +import Data.ByteString.Internal (ByteString (..)) +import Data.ByteString.Short (ShortByteString) import Data.Char (ord) -import Text.Printf -import Data.ByteString.Internal (ByteString(..)) +import Data.Word +import Data.Word.Synthetic.Word12 (Word12) import Foreign -import Data.ByteString.Short (ShortByteString) +import Text.Printf import qualified Data.ByteString as ByteString import qualified Data.ByteString.Char8 as BC8 @@ -109,12 +108,12 @@ word12HexFixedLower = word12HexFixedGeneral False hexValuesWord12Upper :: ShortByteString hexValuesWord12Upper = - SBS.pack $ map (fromIntegral . ord) $ concat $ map (printf "%03X") [0 :: Int ..4095] + SBS.pack $ map (fromIntegral . ord) $ concat $ map (printf "%03X") [0 :: Int .. 4095] {-# NOINLINE hexValuesWord12Upper #-} hexValuesWord12Lower :: ShortByteString hexValuesWord12Lower = - SBS.pack $ map (fromIntegral . ord) $ concat $ map (printf "%03x") [0 :: Int ..4095] + SBS.pack $ map (fromIntegral . ord) $ concat $ map (printf "%03x") [0 :: Int .. 4095] {-# NOINLINE hexValuesWord12Lower #-} word8HexFixedUpper :: Builder Word8 @@ -137,12 +136,12 @@ word8HexFixedGeneral upper = BuilderFunction (BC8.pack "--") $ \i marr w -> do hexValuesWord8Upper :: ShortByteString hexValuesWord8Upper = - SBS.pack $ map (fromIntegral . ord) $ concat $ map (printf "%02X") [0 :: Int ..255] + SBS.pack $ map (fromIntegral . ord) $ concat $ map (printf "%02X") [0 :: Int .. 255] {-# NOINLINE hexValuesWord8Upper #-} hexValuesWord8Lower :: ShortByteString hexValuesWord8Lower = - SBS.pack $ map (fromIntegral . ord) $ concat $ map (printf "%02x") [0 :: Int ..255] + SBS.pack $ map (fromIntegral . ord) $ concat $ map (printf "%02x") [0 :: Int .. 255] {-# NOINLINE hexValuesWord8Lower #-} char8 :: Builder Char @@ -153,8 +152,9 @@ word8 :: Builder Word8 word8 = BuilderFunction (BC8.pack "-") $ \i marr w -> pokeByteOff marr i w {-# INLINE word8 #-} --- | Taken from @Data.ByteString.Internal@. The same warnings --- apply here. +{- | Taken from @Data.ByteString.Internal@. The same warnings + apply here. +-} c2w :: Char -> Word8 c2w = fromIntegral . ord {-# INLINE c2w #-} diff --git a/src/Data/Text/Builder/Common/Compat.hs b/src/Data/Text/Builder/Common/Compat.hs index 65c278b..8887d2f 100644 --- a/src/Data/Text/Builder/Common/Compat.hs +++ b/src/Data/Text/Builder/Common/Compat.hs @@ -1,4 +1,5 @@ {-# LANGUAGE CPP #-} + {- | Compatibility module allowing us to support UTF-16 & UTF-8 versions of the 'text' package. -} diff --git a/src/Data/Text/Builder/Common/Internal.hs b/src/Data/Text/Builder/Common/Internal.hs index 331d6ac..5db8e64 100644 --- a/src/Data/Text/Builder/Common/Internal.hs +++ b/src/Data/Text/Builder/Common/Internal.hs @@ -1,29 +1,31 @@ module Data.Text.Builder.Common.Internal where -import Data.Text (Text) import Control.Monad.ST -import Data.Monoid -import Text.Printf import Data.Char (ord) import Data.Foldable (fold) +import Data.Monoid +import Data.Text (Text) import qualified Data.Text as Text import qualified Data.Text.Array as A import qualified Data.Text.Internal.Unsafe.Char as TC +import Text.Printf --- | This is slower that just pattern matching on the Text data constructor. --- However, it will work with GHCJS. This should only be used in places --- where we know that it will only be evaluated once. +{- | This is slower that just pattern matching on the Text data constructor. + However, it will work with GHCJS. This should only be used in places + where we know that it will only be evaluated once. +-} portableTextArray :: Text -> A.Array portableTextArray = fst . portableUntext {-# INLINE portableTextArray #-} --- | This length is not the character length. It is the length of Word16s --- required by a UTF16 representation. +{- | This length is not the character length. It is the length of Word16s + required by a UTF16 representation. +-} portableTextLength :: Text -> Int portableTextLength = snd . portableUntext {-# INLINE portableTextLength #-} -portableUntext :: Text -> (A.Array,Int) +portableUntext :: Text -> (A.Array, Int) portableUntext t = let str = Text.unpack t Sum len = foldMap (Sum . charUtf16Size) str @@ -31,11 +33,12 @@ portableUntext t = marr <- A.new len writeString marr str return marr - in (arr,len) + in (arr, len) {-# NOINLINE portableUntext #-} writeString :: A.MArray s -> String -> ST s () -writeString marr = go 0 where +writeString marr = go 0 + where go i s = case s of c : cs -> do n <- TC.unsafeWrite marr i c @@ -46,32 +49,41 @@ charUtf16Size :: Char -> Int charUtf16Size c = if ord c < 0x10000 then 1 else 2 hexValuesWord12Upper :: A.Array -hexValuesWord12Upper = portableTextArray $ fold - $ map (Text.pack . printf "%03X") [0 :: Int ..4096] +hexValuesWord12Upper = + portableTextArray $ + fold $ + map (Text.pack . printf "%03X") [0 :: Int .. 4096] {-# NOINLINE hexValuesWord12Upper #-} hexValuesWord12Lower :: A.Array -hexValuesWord12Lower = portableTextArray $ fold - $ map (Text.pack . printf "%03x") [0 :: Int ..4096] +hexValuesWord12Lower = + portableTextArray $ + fold $ + map (Text.pack . printf "%03x") [0 :: Int .. 4096] {-# NOINLINE hexValuesWord12Lower #-} hexValuesWord8Upper :: A.Array -hexValuesWord8Upper = portableTextArray $ fold - $ map (Text.pack . printf "%02X") [0 :: Int ..255] +hexValuesWord8Upper = + portableTextArray $ + fold $ + map (Text.pack . printf "%02X") [0 :: Int .. 255] {-# NOINLINE hexValuesWord8Upper #-} hexValuesWord8Lower :: A.Array -hexValuesWord8Lower = portableTextArray $ fold - $ map (Text.pack . printf "%02x") [0 :: Int ..255] +hexValuesWord8Lower = + portableTextArray $ + fold $ + map (Text.pack . printf "%02x") [0 :: Int .. 255] {-# NOINLINE hexValuesWord8Lower #-} twoDecimalDigits :: A.Array -twoDecimalDigits = portableTextArray - $ foldMap (Text.pack . printf "%02d") [0 :: Int ..99] +twoDecimalDigits = + portableTextArray $ + foldMap (Text.pack . printf "%02d") [0 :: Int .. 99] {-# NOINLINE twoDecimalDigits #-} threeDecimalDigits :: A.Array -threeDecimalDigits = portableTextArray - $ foldMap (Text.pack . printf "%03d") [0 :: Int ..255] +threeDecimalDigits = + portableTextArray $ + foldMap (Text.pack . printf "%03d") [0 :: Int .. 255] {-# NOINLINE threeDecimalDigits #-} - diff --git a/src/Data/Text/Builder/Fixed.hs b/src/Data/Text/Builder/Fixed.hs index cf7bf1b..b6ce5d2 100644 --- a/src/Data/Text/Builder/Fixed.hs +++ b/src/Data/Text/Builder/Fixed.hs @@ -1,12 +1,11 @@ +{-# LANGUAGE BangPatterns #-} {-# LANGUAGE CPP #-} -{-# LANGUAGE RankNTypes #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE BangPatterns #-} - +{-# LANGUAGE RankNTypes #-} {-# OPTIONS_GHC -Wall -funbox-strict-fields #-} -{-| For concatenating fixed-width strings that are only a few +{- | For concatenating fixed-width strings that are only a few characters each, this can be ten times faster than the builder that ships with @text@. -} @@ -28,14 +27,14 @@ import Data.Monoid #endif import Data.Bits import Data.Char (ord) -import Data.Word -import Data.Word.Synthetic.Word12 (Word12) -import Data.Text (Text) import qualified Data.Semigroup as Semigroup +import Data.Text (Text) import qualified Data.Text as Text import qualified Data.Text.Array as A -import qualified Data.Text.Internal as TI import qualified Data.Text.Builder.Common.Internal as I +import qualified Data.Text.Internal as TI +import Data.Word +import Data.Word.Synthetic.Word12 (Word12) data Builder a where BuilderStatic :: Text -> Builder a @@ -75,6 +74,7 @@ contramapBuilder f x = case x of BuilderFunction t g -> BuilderFunction t (\ix marr b -> g ix marr (f b)) {-# INLINE contramapBuilder #-} +{- FOURMOLU_DISABLE -} run :: Builder a -> a -> Text run x = case x of BuilderStatic t -> \_ -> t @@ -92,6 +92,7 @@ run x = case x of A.unsafeFreeze marr in TI.text outArr 0 len {-# INLINE run #-} +{- FOURMOLU_ENABLE -} word8HexFixedUpper :: Builder Word8 word8HexFixedUpper = word8HexFixedGeneral True @@ -112,9 +113,10 @@ word8HexFixedGeneral upper = A.unsafeWrite marr (i + 1) (A.unsafeIndex arr ix2) {-# INLINE word8HexFixedGeneral #-} --- | Characters outside the basic multilingual plane are not handled --- correctly by this function. They will not cause a program to crash; --- instead, the character will have the upper bits masked out. +{- | Characters outside the basic multilingual plane are not handled + correctly by this function. They will not cause a program to crash; + instead, the character will have the upper bits masked out. +-} charBmp :: Builder Char charBmp = BuilderFunction (Text.pack "-") $ \i marr c -> A.unsafeWrite marr i (fromIntegral (ord c)) @@ -138,4 +140,3 @@ word12HexFixedUpper = word12HexFixedGeneral True word12HexFixedLower :: Builder Word12 word12HexFixedLower = word12HexFixedGeneral False {-# INLINE word12HexFixedLower #-} - diff --git a/src/Data/Text/Builder/Variable.hs b/src/Data/Text/Builder/Variable.hs index a6ee817..93a47f0 100644 --- a/src/Data/Text/Builder/Variable.hs +++ b/src/Data/Text/Builder/Variable.hs @@ -1,9 +1,9 @@ -{-# LANGUAGE CPP #-} -{-# LANGUAGE RankNTypes #-} {-# LANGUAGE BangPatterns #-} +{-# LANGUAGE CPP #-} {-# LANGUAGE MultiWayIf #-} +{-# LANGUAGE RankNTypes #-} -{-| This is a builder optimized for concatenating short +{- | This is a builder optimized for concatenating short variable-length strings whose length has a known upper bound. In these cases, this can be up to ten times faster than the builder provided by the @text@ library. However, @@ -23,18 +23,18 @@ module Data.Text.Builder.Variable , word8 ) where -import Data.Word -import Data.Text (Text) -import Data.Text.Builder.Common.Compat (Codepoint) import Control.Monad.ST import Data.Char (ord) -import Data.Vector (Vector) import Data.Maybe (fromMaybe) -import qualified Data.Vector as Vector import qualified Data.Semigroup as Semigroup +import Data.Text (Text) import qualified Data.Text.Array as A +import Data.Text.Builder.Common.Compat (Codepoint) import qualified Data.Text.Builder.Common.Internal as I import qualified Data.Text.Internal as TI +import Data.Vector (Vector) +import qualified Data.Vector as Vector +import Data.Word data Builder a = Builder @@ -60,10 +60,10 @@ instance Monoid (Builder a) where run :: Builder a -> a -> Text run (Builder maxLen f) = \a -> - let (outArr,len) = A.run2 $ do + let (outArr, len) = A.run2 $ do marr <- A.new maxLen finalIx <- f 0 marr a - return (marr,finalIx) + return (marr, finalIx) in TI.text outArr 0 len {-# INLINE run #-} @@ -90,25 +90,27 @@ staticCharBmp c = Builder 1 $ \i marr _ -> do {-# INLINE staticCharBmp #-} word8 :: Builder Word8 -word8 = Builder 3 $ \pos marr w -> if - | w < 10 -> do - A.unsafeWrite marr pos (i2w w) - return (pos + 1) - | w < 100 -> do - let wInt = fromIntegral w - ix = wInt + wInt - A.unsafeWrite marr pos (A.unsafeIndex I.twoDecimalDigits ix) - A.unsafeWrite marr (pos + 1) (A.unsafeIndex I.twoDecimalDigits (ix + 1)) - return (pos + 2) - | otherwise -> do - let wInt = fromIntegral w - ix = wInt + wInt + wInt - A.unsafeWrite marr pos (A.unsafeIndex I.threeDecimalDigits ix) - A.unsafeWrite marr (pos + 1) (A.unsafeIndex I.threeDecimalDigits (ix + 1)) - A.unsafeWrite marr (pos + 2) (A.unsafeIndex I.threeDecimalDigits (ix + 2)) - return (pos + 3) +word8 = Builder 3 $ \pos marr w -> + if + | w < 10 -> do + A.unsafeWrite marr pos (i2w w) + return (pos + 1) + | w < 100 -> do + let wInt = fromIntegral w + ix = wInt + wInt + A.unsafeWrite marr pos (A.unsafeIndex I.twoDecimalDigits ix) + A.unsafeWrite marr (pos + 1) (A.unsafeIndex I.twoDecimalDigits (ix + 1)) + return (pos + 2) + | otherwise -> do + let wInt = fromIntegral w + ix = wInt + wInt + wInt + A.unsafeWrite marr pos (A.unsafeIndex I.threeDecimalDigits ix) + A.unsafeWrite marr (pos + 1) (A.unsafeIndex I.threeDecimalDigits (ix + 1)) + A.unsafeWrite marr (pos + 2) (A.unsafeIndex I.threeDecimalDigits (ix + 2)) + return (pos + 3) {-# INLINE word8 #-} +{- FOURMOLU_DISABLE -} -- This has not yet been tested. _vector :: Text -- ^ Default, used when index is out of range @@ -133,8 +135,8 @@ _vector tDef v = i2w :: Integral a => a -> Codepoint i2w v = asciiZero + fromIntegral v {-# INLINE i2w #-} +{- FOURMOLU_ENABLE -} asciiZero :: Codepoint asciiZero = 48 {-# INLINE asciiZero #-} - diff --git a/src/Data/Text/SmallBuilder.hs b/src/Data/Text/SmallBuilder.hs index 4f8be2d..e1608d6 100644 --- a/src/Data/Text/SmallBuilder.hs +++ b/src/Data/Text/SmallBuilder.hs @@ -1,11 +1,10 @@ -{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE BangPatterns #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE BangPatterns #-} - +{-# LANGUAGE RankNTypes #-} {-# OPTIONS_GHC -Wall -funbox-strict-fields #-} -{-| For concatenating fixed-width strings that are only a few +{- | For concatenating fixed-width strings that are only a few characters each, this can be ten times faster than the builder that ships with @text@. The restriction imposed is that all of the concatenated textual encoding be fixed-width. @@ -22,22 +21,22 @@ module Data.Text.SmallBuilder , word12HexFixedUpper ) where -import Data.Text.Internal (Text(..)) import Control.Monad.ST -import Data.Monoid -import Data.Word import Data.Bits -import Text.Printf (printf) -import Debug.Trace import Data.Char (ord) -import Data.Word.Synthetic (Word12) +import Data.Monoid import qualified Data.Text as Text +import qualified Data.Text.Array as A +import qualified Data.Text.IO as Text +import Data.Text.Internal (Text (..)) import qualified Data.Text.Lazy as LText -import qualified Data.Text.Lazy.IO as LText import qualified Data.Text.Lazy.Builder as TBuilder import qualified Data.Text.Lazy.Builder.Int as TBuilder -import qualified Data.Text.IO as Text -import qualified Data.Text.Array as A +import qualified Data.Text.Lazy.IO as LText +import Data.Word +import Data.Word.Synthetic (Word12) +import Debug.Trace +import Text.Printf (printf) data Builder a where BuilderStatic :: !Text -> Builder a @@ -80,8 +79,9 @@ word8HexFixedUpper :: Builder Word8 word8HexFixedUpper = word8HexFixedGeneral True {-# INLINE word8HexFixedUpper #-} --- | Lowercase fixed-width hexidecimal 'Word8' encoding. The text --- produced is always two characters in length. +{- | Lowercase fixed-width hexidecimal 'Word8' encoding. The text + produced is always two characters in length. +-} word8HexFixedLower :: Builder Word8 word8HexFixedLower = word8HexFixedGeneral False {-# INLINE word8HexFixedLower #-} @@ -96,19 +96,20 @@ word8HexFixedGeneral upper = BuilderFunction (Text.pack "--") $ \i marr w -> do A.unsafeWrite marr (i + 1) (A.unsafeIndex arr ix2) {-# INLINE word8HexFixedGeneral #-} --- | Characters outside the basic multilingual plane are not handled --- correctly by this function. However, they will not cause a program to crash. --- Instead, the character will have the upper bits masked out. +{- | Characters outside the basic multilingual plane are not handled + correctly by this function. However, they will not cause a program to crash. + Instead, the character will have the upper bits masked out. +-} charBmp :: Builder Char charBmp = BuilderFunction (Text.pack "-") $ \i marr c -> A.unsafeWrite marr i (fromIntegral (ord c)) {-# INLINE charBmp #-} hexValuesUpper :: A.Array -hexValuesUpper = let Text arr _ _ = Text.copy $ Text.pack $ concat $ map (printf "%02X") [0 :: Int ..255] in arr +hexValuesUpper = let Text arr _ _ = Text.copy $ Text.pack $ concat $ map (printf "%02X") [0 :: Int .. 255] in arr {-# NOINLINE hexValuesUpper #-} hexValuesLower :: A.Array -hexValuesLower = let Text arr _ _ = Text.copy $ Text.pack $ concat $ map (printf "%02x") [0 :: Int ..255] in arr +hexValuesLower = let Text arr _ _ = Text.copy $ Text.pack $ concat $ map (printf "%02x") [0 :: Int .. 255] in arr {-# NOINLINE hexValuesLower #-} word12HexFixedGeneral :: Bool -> Builder Word12 @@ -130,10 +131,9 @@ word12HexFixedLower = word12HexFixedGeneral False {-# INLINE word12HexFixedLower #-} hexValuesWord12Upper :: A.Array -hexValuesWord12Upper = let Text arr _ _ = Text.copy $ Text.pack $ concat $ map (printf "%03X") [0 :: Int ..4096] in arr +hexValuesWord12Upper = let Text arr _ _ = Text.copy $ Text.pack $ concat $ map (printf "%03X") [0 :: Int .. 4096] in arr {-# NOINLINE hexValuesWord12Upper #-} hexValuesWord12Lower :: A.Array -hexValuesWord12Lower = let Text arr _ _ = Text.copy $ Text.pack $ concat $ map (printf "%03x") [0 :: Int ..4096] in arr +hexValuesWord12Lower = let Text arr _ _ = Text.copy $ Text.pack $ concat $ map (printf "%03x") [0 :: Int .. 4096] in arr {-# NOINLINE hexValuesWord12Lower #-} - diff --git a/src/Data/Word/Synthetic/Word12.hs b/src/Data/Word/Synthetic/Word12.hs index 9824742..d85cde1 100644 --- a/src/Data/Word/Synthetic/Word12.hs +++ b/src/Data/Word/Synthetic/Word12.hs @@ -1,48 +1,47 @@ -{-# LANGUAGE BangPatterns #-} -{-# LANGUAGE CPP #-} -{-# LANGUAGE MagicHash #-} +{-# LANGUAGE BangPatterns #-} +{-# LANGUAGE CPP #-} +{-# LANGUAGE MagicHash #-} +{-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE NoImplicitPrelude #-} -{-# LANGUAGE PatternSynonyms #-} - - --- | --- Module : Data.Word.Word12 --- License : see src/Data/LICENSE --- Stability : experimental --- Portability : non-portable (GHC Extensions) -- Provide a 12-bit unsigned integral type: 'Word12', analagous to Word8, -- Word16, etc. -- -module Data.Word.Synthetic.Word12 ( - -- * Word12 type - Word12(..) - -- * Internal helpers +{- | +Module : Data.Word.Word12 +License : see src/Data/LICENSE +Stability : experimental +Portability : non-portable (GHC Extensions) +-} +module Data.Word.Synthetic.Word12 + ( -- * Word12 type + Word12 (..) + + -- * Internal helpers , narrow12Word# , clz12# , ctz12# , popCnt12# ) - where -import Data.Bits -import Data.Data -import Data.Maybe +import Data.Bits +import Data.Data +import Data.Maybe -import GHC.Arr -import GHC.Base -import GHC.Enum +import GHC.Arr +import GHC.Base +import GHC.Enum #if MIN_VERSION_base(4,15,0) import GHC.Integer (integerToWord, smallInteger) import GHC.Num hiding (integerToWord) #else import GHC.Num #endif -import GHC.Read -import GHC.Real -import GHC.Show +import GHC.Read +import GHC.Real +import GHC.Show import qualified GHC.Word.Compat as Compat @@ -52,7 +51,6 @@ import qualified GHC.Word.Compat as Compat -- must ensure that it holds only values in its logical range. -- | 12-bit unsigned integer type --- data Word12 = W12# Word# deriving (Eq, Ord) word12Type :: DataType @@ -61,9 +59,12 @@ word12Type = mkIntType "Data.Word.Synthetic.Word12.Word12" instance Data Word12 where toConstr x = mkIntegralConstr word12Type x gunfold _ z c = case constrRep c of - (IntConstr x) -> z (fromIntegral x) - _ -> error $ "Data.Data.gunfold: Constructor " ++ show c - ++ " is not of type Word12." + (IntConstr x) -> z (fromIntegral x) + _ -> + error $ + "Data.Data.gunfold: Constructor " + ++ show c + ++ " is not of type Word12." dataTypeOf _ = word12Type -- | narrowings represented as primop 'and#' in GHC. @@ -71,17 +72,14 @@ narrow12Word# :: Word# -> Word# narrow12Word# = and# 0xFFF## -- | count leading zeros --- clz12# :: Word# -> Word# clz12# w# = clz32# (narrow12Word# w#) `minusWord#` 20## -- | count trailing zeros --- ctz12# :: Word# -> Word# ctz12# w# = ctz# w# -- | the number of set bits --- popCnt12# :: Word# -> Word# popCnt12# w# = popCnt# (narrow12Word# w#) @@ -92,134 +90,147 @@ instance Num Word12 where (W12# x#) + (W12# y#) = W12# (narrow12Word# (x# `plusWord#` y#)) (W12# x#) - (W12# y#) = W12# (narrow12Word# (x# `minusWord#` y#)) (W12# x#) * (W12# y#) = W12# (narrow12Word# (x# `timesWord#` y#)) - negate (W12# x#) = W12# (narrow12Word# (int2Word# (negateInt# (word2Int# x#)))) - abs x = x - signum 0 = 0 - signum _ = 1 - fromInteger i = W12# (narrow12Word# (integerToWord i)) + negate (W12# x#) = W12# (narrow12Word# (int2Word# (negateInt# (word2Int# x#)))) + abs x = x + signum 0 = 0 + signum _ = 1 + fromInteger i = W12# (narrow12Word# (integerToWord i)) instance Real Word12 where toRational x = toInteger x % 1 instance Enum Word12 where succ x - | x /= maxBound = x + 1 - | otherwise = succError "Word12" + | x /= maxBound = x + 1 + | otherwise = succError "Word12" pred x - | x /= minBound = x - 1 - | otherwise = predError "Word12" + | x /= minBound = x - 1 + | otherwise = predError "Word12" toEnum i@(I# i#) - | i >= 0 && i <= fromIntegral (maxBound :: Word12) - = W12# (int2Word# i#) - | otherwise = toEnumError "Word12" i (minBound::Word12, maxBound::Word12) + | i >= 0 && i <= fromIntegral (maxBound :: Word12) = + W12# (int2Word# i#) + | otherwise = toEnumError "Word12" i (minBound :: Word12, maxBound :: Word12) fromEnum (W12# x#) = I# (word2Int# x#) - enumFrom = boundedEnumFrom - enumFromThen = boundedEnumFromThen + enumFrom = boundedEnumFrom + enumFromThen = boundedEnumFromThen instance Integral Word12 where quot (W12# x#) y@(W12# y#) - | y /= 0 = W12# (x# `quotWord#` y#) - | otherwise = divZeroError + | y /= 0 = W12# (x# `quotWord#` y#) + | otherwise = divZeroError rem (W12# x#) y@(W12# y#) - | y /= 0 = W12# (x# `remWord#` y#) - | otherwise = divZeroError + | y /= 0 = W12# (x# `remWord#` y#) + | otherwise = divZeroError div (W12# x#) y@(W12# y#) - | y /= 0 = W12# (x# `quotWord#` y#) - | otherwise = divZeroError + | y /= 0 = W12# (x# `quotWord#` y#) + | otherwise = divZeroError mod (W12# x#) y@(W12# y#) - | y /= 0 = W12# (x# `remWord#` y#) - | otherwise = divZeroError + | y /= 0 = W12# (x# `remWord#` y#) + | otherwise = divZeroError quotRem (W12# x#) y@(W12# y#) - | y /= 0 = (W12# (x# `quotWord#` y#), W12# (x# `remWord#` y#)) - | otherwise = divZeroError + | y /= 0 = (W12# (x# `quotWord#` y#), W12# (x# `remWord#` y#)) + | otherwise = divZeroError divMod (W12# x#) y@(W12# y#) - | y /= 0 = (W12# (x# `quotWord#` y#), W12# (x# `remWord#` y#)) - | otherwise = divZeroError - toInteger (W12# x#) = smallInteger (word2Int# x#) + | y /= 0 = (W12# (x# `quotWord#` y#), W12# (x# `remWord#` y#)) + | otherwise = divZeroError + toInteger (W12# x#) = smallInteger (word2Int# x#) instance Bounded Word12 where minBound = 0 maxBound = 0xFFFFFF instance Ix Word12 where - range (m,n) = [m..n] - unsafeIndex (m,_) i = fromIntegral (i - m) - inRange (m,n) i = m <= i && i <= n + range (m, n) = [m .. n] + unsafeIndex (m, _) i = fromIntegral (i - m) + inRange (m, n) i = m <= i && i <= n instance Read Word12 where - readsPrec p s = [(fromIntegral (x::Int), r) | (x, r) <- readsPrec p s] + readsPrec p s = [(fromIntegral (x :: Int), r) | (x, r) <- readsPrec p s] instance Bits Word12 where - {-# INLINE shift #-} - {-# INLINE bit #-} - {-# INLINE testBit #-} - - (W12# x#) .&. (W12# y#) = W12# (x# `and#` y#) - (W12# x#) .|. (W12# y#) = W12# (x# `or#` y#) - (W12# x#) `xor` (W12# y#) = W12# (x# `xor#` y#) - complement (W12# x#) = W12# (x# `xor#` mb#) where !(W12# mb#) = maxBound - (W12# x#) `shift` (I# i#) - | isTrue# (i# >=# 0#) = W12# (narrow12Word# (x# `shiftL#` i#)) - | otherwise = W12# (x# `shiftRL#` negateInt# i#) - (W12# x#) `shiftL` (I# i#) = W12# (narrow12Word# (x# `shiftL#` i#)) - (W12# x#) `unsafeShiftL` (I# i#) = - W12# (narrow12Word# (x# `uncheckedShiftL#` i#)) - (W12# x#) `shiftR` (I# i#) = W12# (x# `shiftRL#` i#) - (W12# x#) `unsafeShiftR` (I# i#) = W12# (x# `uncheckedShiftRL#` i#) - (W12# x#) `rotate` i - | isTrue# (i'# ==# 0#) = W12# x# - | otherwise = W12# (narrow12Word# ((x# `uncheckedShiftL#` i'#) `or#` - (x# `uncheckedShiftRL#` (12# -# i'#)))) - where - !(I# i'#) = i `mod` 12 - bitSizeMaybe i = Just (finiteBitSize i) - bitSize = finiteBitSize - isSigned _ = False - popCount (W12# x#) = I# (word2Int# (popCnt12# x#)) - bit = bitDefault - testBit = testBitDefault + {-# INLINE shift #-} + {-# INLINE bit #-} + {-# INLINE testBit #-} + + (W12# x#) .&. (W12# y#) = W12# (x# `and#` y#) + (W12# x#) .|. (W12# y#) = W12# (x# `or#` y#) + (W12# x#) `xor` (W12# y#) = W12# (x# `xor#` y#) + complement (W12# x#) = W12# (x# `xor#` mb#) where !(W12# mb#) = maxBound + (W12# x#) `shift` (I# i#) + | isTrue# (i# >=# 0#) = W12# (narrow12Word# (x# `shiftL#` i#)) + | otherwise = W12# (x# `shiftRL#` negateInt# i#) + (W12# x#) `shiftL` (I# i#) = W12# (narrow12Word# (x# `shiftL#` i#)) + (W12# x#) `unsafeShiftL` (I# i#) = + W12# (narrow12Word# (x# `uncheckedShiftL#` i#)) + (W12# x#) `shiftR` (I# i#) = W12# (x# `shiftRL#` i#) + (W12# x#) `unsafeShiftR` (I# i#) = W12# (x# `uncheckedShiftRL#` i#) + (W12# x#) `rotate` i + | isTrue# (i'# ==# 0#) = W12# x# + | otherwise = + W12# + ( narrow12Word# + ( (x# `uncheckedShiftL#` i'#) + `or#` (x# `uncheckedShiftRL#` (12# -# i'#)) + ) + ) + where + !(I# i'#) = i `mod` 12 + bitSizeMaybe i = Just (finiteBitSize i) + bitSize = finiteBitSize + isSigned _ = False + popCount (W12# x#) = I# (word2Int# (popCnt12# x#)) + bit = bitDefault + testBit = testBitDefault instance FiniteBits Word12 where - finiteBitSize _ = 12 - countLeadingZeros (W12# x#) = I# (word2Int# (clz12# x#)) - countTrailingZeros (W12# x#) = I# (word2Int# (ctz12# x#)) + finiteBitSize _ = 12 + countLeadingZeros (W12# x#) = I# (word2Int# (clz12# x#)) + countTrailingZeros (W12# x#) = I# (word2Int# (ctz12# x#)) {-# RULES -"fromIntegral/Word8->Word12" fromIntegral = \x -> case x of { Compat.W8# y -> W12# y } -"fromIntegral/Word12->Word12" fromIntegral = id :: Word12 -> Word12 -"fromIntegral/Word12->Integer" fromIntegral = toInteger :: Word12 -> Integer -"fromIntegral/a->Word12" fromIntegral = \x -> case fromIntegral x of W# x# -> W12# (narrow12Word# x#) -"fromIntegral/Word12->a" fromIntegral = \(W12# x#) -> fromIntegral (W# x#) +"fromIntegral/Word8->Word12" fromIntegral = \x -> case x of Compat.W8# y -> W12# y +"fromIntegral/Word12->Word12" fromIntegral = id :: Word12 -> Word12 +"fromIntegral/Word12->Integer" fromIntegral = toInteger :: Word12 -> Integer +"fromIntegral/a->Word12" fromIntegral = \x -> case fromIntegral x of W# x# -> W12# (narrow12Word# x#) +"fromIntegral/Word12->a" fromIntegral = \(W12# x#) -> fromIntegral (W# x#) #-} {-# RULES "properFraction/Float->(Word12,Float)" - properFraction = \x -> - case properFraction x of { - (n, y) -> ((fromIntegral :: Int -> Word12) n, y :: Float) } + properFraction = + \x -> + case properFraction x of + (n, y) -> ((fromIntegral :: Int -> Word12) n, y :: Float) "truncate/Float->Word12" - truncate = (fromIntegral :: Int -> Word12) . (truncate :: Float -> Int) + truncate = + (fromIntegral :: Int -> Word12) . (truncate :: Float -> Int) "floor/Float->Word12" - floor = (fromIntegral :: Int -> Word12) . (floor :: Float -> Int) + floor = + (fromIntegral :: Int -> Word12) . (floor :: Float -> Int) "ceiling/Float->Word12" - ceiling = (fromIntegral :: Int -> Word12) . (ceiling :: Float -> Int) + ceiling = + (fromIntegral :: Int -> Word12) . (ceiling :: Float -> Int) "round/Float->Word12" - round = (fromIntegral :: Int -> Word12) . (round :: Float -> Int) + round = + (fromIntegral :: Int -> Word12) . (round :: Float -> Int) #-} {-# RULES "properFraction/Double->(Word12,Double)" - properFraction = \x -> - case properFraction x of { - (n, y) -> ((fromIntegral :: Int -> Word12) n, y :: Double) } + properFraction = + \x -> + case properFraction x of + (n, y) -> ((fromIntegral :: Int -> Word12) n, y :: Double) "truncate/Double->Word12" - truncate = (fromIntegral :: Int -> Word12) . (truncate :: Double -> Int) + truncate = + (fromIntegral :: Int -> Word12) . (truncate :: Double -> Int) "floor/Double->Word12" - floor = (fromIntegral :: Int -> Word12) . (floor :: Double -> Int) + floor = + (fromIntegral :: Int -> Word12) . (floor :: Double -> Int) "ceiling/Double->Word12" - ceiling = (fromIntegral :: Int -> Word12) . (ceiling :: Double -> Int) + ceiling = + (fromIntegral :: Int -> Word12) . (ceiling :: Double -> Int) "round/Double->Word12" - round = (fromIntegral :: Int -> Word12) . (round :: Double -> Int) + round = + (fromIntegral :: Int -> Word12) . (round :: Double -> Int) #-} - - diff --git a/src/Net/IP.hs b/src/Net/IP.hs index 4198094..a23589d 100644 --- a/src/Net/IP.hs +++ b/src/Net/IP.hs @@ -1,13 +1,12 @@ {-# LANGUAGE BangPatterns #-} -{-# LANGUAGE DeriveDataTypeable #-} -{-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} - {-# OPTIONS_GHC -Wall #-} -{-| An IP data type representing either an IPv4 address or +{- | An IP data type representing either an IPv4 address or an IPv6 address. The user can think of this as though it were a sum type. However, to minimize indirections, it is actually implemented as an 'IPv6' address, with 'IPv4' @@ -30,91 +29,108 @@ >>> decode "A3F5:12:F26::1466:8B91" Just (ipv6 0xa3f5 0x0012 0x0f26 0x0000 0x0000 0x0000 0x1466 0x8b91) -} - module Net.IP ( -- * Pattern Matching case_ , isIPv4 , isIPv6 + -- * Construction , ipv4 , ipv6 , fromIPv4 , fromIPv6 + -- * Textual Conversion + -- ** Text , encode , encodeShort , decode , decodeShort , boundedBuilderUtf8 + -- ** Bytes , decodeUtf8Bytes , parserUtf8Bytes + -- ** Printing , print + -- * Types - , IP(..) + , IP (..) ) where import Control.DeepSeq (NFData) -import Data.Aeson (FromJSON(..),ToJSON(..)) +import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.Bits import Data.Coerce (coerce) import Data.Data (Data) import Data.Hashable (Hashable) import Data.Ix (Ix) import Data.Text (Text) -import Data.WideWord (Word128(..)) -import Data.Word (Word8,Word16) +import Data.Text.Short (ShortText) +import Data.WideWord (Word128 (..)) +import Data.Word (Word16, Word8) import GHC.Generics (Generic) -import Net.IPv4 (IPv4(..)) -import Net.IPv6 (IPv6(..)) -import Prelude hiding (print) +import Net.IPv4 (IPv4 (..)) +import Net.IPv6 (IPv6 (..)) import Text.ParserCombinators.ReadPrec ((+++)) -import Text.Read (Read(..)) -import Data.Text.Short (ShortText) +import Text.Read (Read (..)) +import Prelude hiding (print) import qualified Arithmetic.Lte as Lte import qualified Data.Aeson as Aeson import qualified Data.Bytes as Bytes import qualified Data.Bytes.Builder.Bounded as BB -import qualified Data.Text.IO as TIO import qualified Data.Bytes.Parser as Parser +import qualified Data.Text.IO as TIO import qualified Net.IPv4 as IPv4 import qualified Net.IPv6 as IPv6 --- $setup --- >>> :set -XOverloadedStrings --- >>> import qualified Arithmetic.Nat as Nat - --- | Run a function over an 'IP' depending on its status --- as an 'IPv4' or 'IPv6'. --- --- >>> case_ IPv4.encode IPv6.encode (ipv4 192 168 2 47) --- "192.168.2.47" --- --- >>> addr = ipv6 0x2001 0x0db8 0x0000 0x0000 0x0000 0x0000 0x0000 0x0001 --- >>> case_ IPv4.encode IPv6.encode addr --- "2001:db8::1" +{- $setup +>>> :set -XOverloadedStrings +>>> import qualified Arithmetic.Nat as Nat +-} + +{- | Run a function over an 'IP' depending on its status + as an 'IPv4' or 'IPv6'. + + >>> case_ IPv4.encode IPv6.encode (ipv4 192 168 2 47) + "192.168.2.47" + + >>> addr = ipv6 0x2001 0x0db8 0x0000 0x0000 0x0000 0x0000 0x0000 0x0001 + >>> case_ IPv4.encode IPv6.encode addr + "2001:db8::1" +-} case_ :: (IPv4 -> a) -> (IPv6 -> a) -> IP -> a -- Note: rather than performing the masking operations on the 'Word128', -- we unwrap the 'Word64's, as that's probably a bit more efficient, and -- we might need the lower word anyway. -case_ f g (IP addr@(IPv6 (Word128 w1 w2))) = if w1 == 0 && (0xFFFFFFFF00000000 .&. w2 == 0x0000FFFF00000000) - then f (IPv4 (fromIntegral w2)) - else g addr +case_ f g (IP addr@(IPv6 (Word128 w1 w2))) = + if w1 == 0 && (0xFFFFFFFF00000000 .&. w2 == 0x0000FFFF00000000) + then f (IPv4 (fromIntegral w2)) + else g addr --- | Construct an 'IP' address from the four octets of --- an IPv4 address. +{- | Construct an 'IP' address from the four octets of + an IPv4 address. +-} ipv4 :: Word8 -> Word8 -> Word8 -> Word8 -> IP ipv4 a b c d = fromIPv4 (IPv4.fromOctets a b c d) --- | Construct an 'IP' address from the eight 16-bit --- chunks of an IPv6 address. -ipv6 :: Word16 -> Word16 -> Word16 -> Word16 - -> Word16 -> Word16 -> Word16 -> Word16 - -> IP +{- | Construct an 'IP' address from the eight 16-bit + chunks of an IPv6 address. +-} +ipv6 :: + Word16 -> + Word16 -> + Word16 -> + Word16 -> + Word16 -> + Word16 -> + Word16 -> + Word16 -> + IP ipv6 a b c d e f g h = fromIPv6 (IPv6.fromWord16s a b c d e f g h) -- | Turn an 'IPv4' into an 'IP'. @@ -125,48 +141,53 @@ fromIPv4 (IPv4 w) = IP (IPv6 (Word128 0 (0x0000FFFF00000000 .|. fromIntegral w)) fromIPv6 :: IPv6 -> IP fromIPv6 = IP --- | Encode an 'IP' as 'Text'. --- --- >>> encode (ipv4 10 0 0 25) --- "10.0.0.25" --- --- >>> encode (ipv6 0x3124 0x0 0x0 0xDEAD 0xCAFE 0xFF 0xFE00 0x1) --- "3124::dead:cafe:ff:fe00:1" +{- | Encode an 'IP' as 'Text'. + + >>> encode (ipv4 10 0 0 25) + "10.0.0.25" + + >>> encode (ipv6 0x3124 0x0 0x0 0xDEAD 0xCAFE 0xFF 0xFE00 0x1) + "3124::dead:cafe:ff:fe00:1" +-} encode :: IP -> Text encode = case_ IPv4.encode IPv6.encode --- | Encode an 'IP' as 'ShortText'. --- --- >>> encodeShort (ipv4 10 0 1 26) --- "10.0.1.26" --- --- >>> encodeShort (ipv6 0x3124 0x0 0x0 0xDEAD 0xCAFE 0xFF 0xFE01 0x0000) --- "3124::dead:cafe:ff:fe01:0" +{- | Encode an 'IP' as 'ShortText'. + + >>> encodeShort (ipv4 10 0 1 26) + "10.0.1.26" + + >>> encodeShort (ipv6 0x3124 0x0 0x0 0xDEAD 0xCAFE 0xFF 0xFE01 0x0000) + "3124::dead:cafe:ff:fe01:0" +-} encodeShort :: IP -> ShortText encodeShort = case_ IPv4.encodeShort IPv6.encodeShort --- | Encode an 'IP' as a bounded bytearray builder. --- --- >>> BB.run Nat.constant (boundedBuilderUtf8 (ipv4 192 168 2 14)) --- [0x31, 0x39, 0x32, 0x2e, 0x31, 0x36, 0x38, 0x2e, 0x32, 0x2e, 0x31, 0x34] +{- | Encode an 'IP' as a bounded bytearray builder. + +>>> BB.run Nat.constant (boundedBuilderUtf8 (ipv4 192 168 2 14)) +[0x31, 0x39, 0x32, 0x2e, 0x31, 0x36, 0x38, 0x2e, 0x32, 0x2e, 0x31, 0x34] +-} boundedBuilderUtf8 :: IP -> BB.Builder 39 -boundedBuilderUtf8 = case_ - (\y -> BB.weaken Lte.constant (IPv4.boundedBuilderUtf8 y)) - IPv6.boundedBuilderUtf8 - --- | Decode an 'IP' from 'Text'. --- --- >>> decode "10.0.0.25" --- Just (ipv4 10 0 0 25) --- --- >>> fmap isIPv4 (decode "10.0.0.25") --- Just True --- --- >>> decode "3124::dead:cafe:ff:fe00:1" --- Just (ipv6 0x3124 0x0000 0x0000 0xdead 0xcafe 0x00ff 0xfe00 0x0001) --- --- >>> fmap isIPv6 (decode "3124::dead:cafe:ff:fe00:1") --- Just True +boundedBuilderUtf8 = + case_ + (\y -> BB.weaken Lte.constant (IPv4.boundedBuilderUtf8 y)) + IPv6.boundedBuilderUtf8 + +{- | Decode an 'IP' from 'Text'. + + >>> decode "10.0.0.25" + Just (ipv4 10 0 0 25) + + >>> fmap isIPv4 (decode "10.0.0.25") + Just True + + >>> decode "3124::dead:cafe:ff:fe00:1" + Just (ipv6 0x3124 0x0000 0x0000 0xdead 0xcafe 0x00ff 0xfe00 0x0001) + + >>> fmap isIPv6 (decode "3124::dead:cafe:ff:fe00:1") + Just True +-} decode :: Text -> Maybe IP decode t = case IPv4.decode t of Nothing -> case IPv6.decode t of @@ -174,12 +195,13 @@ decode t = case IPv4.decode t of Just v6 -> Just (fromIPv6 v6) Just v4 -> Just (fromIPv4 v4) --- | Decode an 'IP' from 'ShortText'. --- --- >>> decodeShort "10.0.0.25" --- Just (ipv4 10 0 0 25) --- >>> decodeShort "::dead:cafe" --- Just (ipv6 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0xdead 0xcafe) +{- | Decode an 'IP' from 'ShortText'. + + >>> decodeShort "10.0.0.25" + Just (ipv4 10 0 0 25) + >>> decodeShort "::dead:cafe" + Just (ipv6 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0xdead 0xcafe) +-} decodeShort :: ShortText -> Maybe IP decodeShort t | Just x <- IPv4.decodeShort t = Just (fromIPv4 x) @@ -197,49 +219,52 @@ decodeUtf8Bytes !b = case Parser.parseBytes (parserUtf8Bytes ()) b of parserUtf8Bytes :: e -> Parser.Parser e s IP parserUtf8Bytes e = fmap fromIPv4 (IPv4.parserUtf8Bytes ()) - `Parser.orElse` - coerce (IPv6.parserUtf8Bytes e) - --- | Is the 'IP' an IPv4 address? --- --- >>> isIPv4 (ipv4 10 0 0 25) --- True --- --- >>> isIPv4 (ipv6 0x3124 0x0 0x0 0xDEAD 0xCAFE 0xFF 0xFE00 0x1) --- False + `Parser.orElse` coerce (IPv6.parserUtf8Bytes e) + +{- | Is the 'IP' an IPv4 address? + + >>> isIPv4 (ipv4 10 0 0 25) + True + + >>> isIPv4 (ipv6 0x3124 0x0 0x0 0xDEAD 0xCAFE 0xFF 0xFE00 0x1) + False +-} isIPv4 :: IP -> Bool isIPv4 = case_ (const True) (const False) -{-# inline isIPv4 #-} - --- | Is the 'IP' an IPv6 address? --- --- >>> isIPv6 (ipv4 10 0 0 25) --- False --- --- >>> isIPv6 (ipv6 0x3124 0x0 0x0 0xDEAD 0xCAFE 0xFF 0xFE00 0x1) --- True +{-# INLINE isIPv4 #-} + +{- | Is the 'IP' an IPv6 address? + + >>> isIPv6 (ipv4 10 0 0 25) + False + + >>> isIPv6 (ipv6 0x3124 0x0 0x0 0xDEAD 0xCAFE 0xFF 0xFE00 0x1) + True +-} isIPv6 :: IP -> Bool isIPv6 = case_ (const False) (const True) -{-# inline isIPv6 #-} - --- | Print an 'IP' using the textual encoding. This exists mostly for --- debugging purposes. --- --- >>> print (ipv4 10 0 0 25) --- 10.0.0.25 --- --- >>> print (ipv6 0x3124 0x0 0x0 0xDEAD 0xCAFE 0xFF 0xFE00 0x1) --- 3124::dead:cafe:ff:fe00:1 +{-# INLINE isIPv6 #-} + +{- | Print an 'IP' using the textual encoding. This exists mostly for + debugging purposes. + + >>> print (ipv4 10 0 0 25) + 10.0.0.25 + + >>> print (ipv6 0x3124 0x0 0x0 0xDEAD 0xCAFE 0xFF 0xFE00 0x1) + 3124::dead:cafe:ff:fe00:1 +-} print :: IP -> IO () print = TIO.putStrLn . encode --- | A 32-bit 'IPv4' address or a 128-bit 'IPv6' address. Internally, this --- is just represented as an 'IPv6' address. The functions provided --- in @Net.IP@ help simulate constructing and pattern matching on values --- of this type. All functions and typeclass methods that convert --- 'IP' values to text will display it as an 'IPv4' address if possible. -newtype IP = IP { getIP :: IPv6 } - deriving stock (Eq,Ord,Generic,Ix,Data) +{- | A 32-bit 'IPv4' address or a 128-bit 'IPv6' address. Internally, this + is just represented as an 'IPv6' address. The functions provided + in @Net.IP@ help simulate constructing and pattern matching on values + of this type. All functions and typeclass methods that convert + 'IP' values to text will display it as an 'IPv4' address if possible. +-} +newtype IP = IP {getIP :: IPv6} + deriving stock (Eq, Ord, Generic, Ix, Data) deriving newtype (Hashable) instance NFData IP @@ -257,4 +282,3 @@ instance FromJSON IP where parseJSON = Aeson.withText "IP" $ \t -> case decode t of Nothing -> fail "Could not parse IP address" Just addr -> return addr - diff --git a/src/Net/IPv4.hs b/src/Net/IPv4.hs index 9bb38e2..81cd36c 100644 --- a/src/Net/IPv4.hs +++ b/src/Net/IPv4.hs @@ -1,5 +1,6 @@ {-# LANGUAGE BangPatterns #-} {-# LANGUAGE CPP #-} +{-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} @@ -7,29 +8,31 @@ {-# LANGUAGE MagicHash #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE TypeFamilies #-} -{-# LANGUAGE TypeInType #-} {-# LANGUAGE UnboxedTuples #-} -{-| This module provides the IPv4 data type and functions for working +{- | This module provides the IPv4 data type and functions for working with it. -} - module Net.IPv4 ( -- * Conversion Functions ipv4 , fromOctets , fromTupleOctets , toOctets + -- * Special IP Addresses , any , loopback , localhost , broadcast + -- * Range Predicates , private , reserved , public + -- * Textual Conversion + -- ** Text , encode , decode @@ -38,26 +41,33 @@ module Net.IPv4 , parser , decodeShort , encodeShort + -- ** UTF-8 ByteString , encodeUtf8 , decodeUtf8 , builderUtf8 , parserUtf8 + -- ** UTF-8 Bytes , decodeUtf8Bytes , parserUtf8Bytes , byteArrayBuilderUtf8 , boundedBuilderUtf8 + -- ** Non-textual Bytes , boundedBuilderOctetsBE , boundedBuilderOctetsLE + -- ** String -- $string , encodeString , decodeString + -- ** Printing , print + -- * IPv4 Ranges + -- ** Range functions , range , fromBounds @@ -67,43 +77,51 @@ module Net.IPv4 , member , lowerInclusive , upperInclusive + -- ** Conversion to IPv4 , toList , toGenerator + -- ** Private Ranges , private24 , private20 , private16 + -- ** Textual Conversion + -- *** Text , encodeRange , decodeRange , builderRange , parserRange , printRange + -- ** UTF-8 Bytes , parserRangeUtf8Bytes , parserRangeUtf8BytesLenient + -- * Types - , IPv4(..) + , IPv4 (..) , IPv4# - , IPv4Range(..) + , IPv4Range (..) + -- * Unboxing + -- | These functions are useful for micro-optimizing -- when GHC does a poor job with worker-wrapper. , box , unbox , parserUtf8Bytes# + -- * Interoperability -- $interoperability ) where import Control.DeepSeq (NFData) import Control.Monad -import Control.Monad.ST (ST,runST) -import Data.Aeson (FromJSON(..),ToJSON(..)) -import Data.Aeson (ToJSONKey(..),FromJSONKey(..),ToJSONKeyFunction(..),FromJSONKeyFunction(..)) -import Data.Bits (Bits(..)) +import Control.Monad.ST (ST, runST) +import Data.Aeson (FromJSON (..), FromJSONKey (..), FromJSONKeyFunction (..), ToJSON (..), ToJSONKey (..), ToJSONKeyFunction (..)) +import Data.Bits (Bits (..)) import Data.ByteString (ByteString) import Data.Coerce (coerce) import Data.Data (Data) @@ -113,18 +131,18 @@ import Data.Primitive.Types (Prim) import Data.Text (Text) import Data.Text.Builder.Common.Compat (Codepoint) import Data.Text.Encoding (decodeUtf8') -import Data.Text.Internal (Text(..)) +import Data.Text.Internal (Text (..)) import Data.Text.Short (ShortText) -import Data.Vector.Generic.Mutable (MVector(..)) +import Data.Vector.Generic.Mutable (MVector (..)) import Data.Word -import Foreign.Ptr (Ptr,plusPtr) +import Foreign.Ptr (Ptr, plusPtr) import Foreign.Storable (Storable, poke) import GHC.Exts (Word#) import GHC.Generics (Generic) -import Prelude hiding (any, print, print) -import Text.ParserCombinators.ReadPrec (prec,step) +import Text.ParserCombinators.ReadPrec (prec, step) import Text.Printf (printf) -import Text.Read (Read(..),Lexeme(Ident),lexP,parens) +import Text.Read (Lexeme (Ident), Read (..), lexP, parens) +import Prelude hiding (any, print) import qualified Arithmetic.Nat as Nat import qualified Data.Aeson as Aeson @@ -132,14 +150,14 @@ import qualified Data.Aeson.Types as Aeson import qualified Data.Attoparsec.ByteString.Char8 as AB import qualified Data.Attoparsec.Text as AT import qualified Data.Bits as Bits -import qualified Data.Bytes.Builder.Bounded as BB -import qualified Data.Bytes.Builder as UB import qualified Data.ByteString.Builder as Builder import qualified Data.ByteString.Char8 as BC8 import qualified Data.ByteString.Internal as I -import qualified Data.ByteString.Unsafe as ByteString import qualified Data.ByteString.Short.Internal as BSS +import qualified Data.ByteString.Unsafe as ByteString import qualified Data.Bytes as Bytes +import qualified Data.Bytes.Builder as UB +import qualified Data.Bytes.Builder.Bounded as BB import qualified Data.Bytes.Parser as Parser import qualified Data.Bytes.Parser.Latin as Latin import qualified Data.Char as Char @@ -164,54 +182,59 @@ import qualified GHC.Word.Compat as Compat import qualified Data.Aeson.Key as AesonKey #endif --- $setup --- --- These are here to get doctest's property checking to work --- --- >>> :set -XOverloadedStrings --- >>> import Test.QuickCheck (Arbitrary(..)) --- >>> import Net.IPv4 (getIPv4) --- >>> import qualified Prelude as P --- >>> import qualified Data.Text.IO as T --- >>> import qualified Data.Bytes.Text.Ascii as Ascii --- >>> import qualified Data.Attoparsec.Text as AT --- >>> import qualified Data.ByteString.Builder as Builder --- >>> import qualified Data.Bytes.Builder as UB --- >>> import qualified Data.Attoparsec.ByteString.Char8 as AB --- >>> instance Arbitrary IPv4 where { arbitrary = fmap IPv4 arbitrary } --- >>> instance Arbitrary IPv4.IPv4Range where { arbitrary = IPv4.IPv4Range <$> arbitrary <*> arbitrary } --- >>> import qualified Data.Bytes.Chunks as Chunks - - --- | Create an 'IPv4' address from four octets. The first argument --- is the most significant octet. The last argument is the least --- significant. Since IP addresses are commonly written using dot-decimal --- notation, this is the recommended way to create an IP address. --- Additionally, it is used for the 'Show' and 'Read' instances --- of 'IPv4' to help keep things readable in GHCi. --- --- >>> let addr = IPv4.ipv4 192 168 1 1 --- >>> addr --- ipv4 192 168 1 1 --- >>> getIPv4 addr --- 3232235777 --- +{- $setup + +These are here to get doctest's property checking to work + +>>> :set -XOverloadedStrings +>>> import Test.QuickCheck (Arbitrary(..)) +>>> import Net.IPv4 (getIPv4) +>>> import qualified Prelude as P +>>> import qualified Data.Text.IO as T +>>> import qualified Data.Bytes.Text.Ascii as Ascii +>>> import qualified Data.Attoparsec.Text as AT +>>> import qualified Data.ByteString.Builder as Builder +>>> import qualified Data.Bytes.Builder as UB +>>> import qualified Data.Attoparsec.ByteString.Char8 as AB +>>> instance Arbitrary IPv4 where { arbitrary = fmap IPv4 arbitrary } +>>> instance Arbitrary IPv4.IPv4Range where { arbitrary = IPv4.IPv4Range <$> arbitrary <*> arbitrary } +>>> import qualified Data.Bytes.Chunks as Chunks +-} + +{- | Create an 'IPv4' address from four octets. The first argument + is the most significant octet. The last argument is the least + significant. Since IP addresses are commonly written using dot-decimal + notation, this is the recommended way to create an IP address. + Additionally, it is used for the 'Show' and 'Read' instances + of 'IPv4' to help keep things readable in GHCi. + + >>> let addr = IPv4.ipv4 192 168 1 1 + >>> addr + ipv4 192 168 1 1 + >>> getIPv4 addr + 3232235777 +-} ipv4 :: Word8 -> Word8 -> Word8 -> Word8 -> IPv4 ipv4 = fromOctets -- | An alias for the 'ipv4' smart constructor. fromOctets :: Word8 -> Word8 -> Word8 -> Word8 -> IPv4 -fromOctets a b c d = fromOctets' - (fromIntegral a) (fromIntegral b) (fromIntegral c) (fromIntegral d) +fromOctets a b c d = + fromOctets' + (fromIntegral a) + (fromIntegral b) + (fromIntegral c) + (fromIntegral d) -- | An uncurried variant of 'fromOctets'. -fromTupleOctets :: (Word8,Word8,Word8,Word8) -> IPv4 -fromTupleOctets (a,b,c,d) = fromOctets a b c d +fromTupleOctets :: (Word8, Word8, Word8, Word8) -> IPv4 +fromTupleOctets (a, b, c, d) = fromOctets a b c d --- | Convert an 'IPv4' address into a quadruple of octets. The first --- element in the quadruple is the most significant octet. The last --- element is the least significant octet. -toOctets :: IPv4 -> (Word8,Word8,Word8,Word8) +{- | Convert an 'IPv4' address into a quadruple of octets. The first + element in the quadruple is the most significant octet. The last + element is the least significant octet. +-} +toOctets :: IPv4 -> (Word8, Word8, Word8, Word8) toOctets (IPv4 w) = ( fromIntegral (shiftR w 24) , fromIntegral (shiftR w 16) @@ -219,42 +242,47 @@ toOctets (IPv4 w) = , fromIntegral w ) --- | The IP address representing any host. --- --- >>> IPv4.any --- ipv4 0 0 0 0 +{- | The IP address representing any host. + + >>> IPv4.any + ipv4 0 0 0 0 +-} any :: IPv4 any = IPv4 0 --- | The local loopback IP address. --- --- >>> IPv4.loopback --- ipv4 127 0 0 1 +{- | The local loopback IP address. + + >>> IPv4.loopback + ipv4 127 0 0 1 +-} loopback :: IPv4 loopback = fromOctets 127 0 0 1 --- | A useful and common alias for 'loopback'. --- --- >>> IPv4.localhost --- ipv4 127 0 0 1 +{- | A useful and common alias for 'loopback'. + + >>> IPv4.localhost + ipv4 127 0 0 1 +-} localhost :: IPv4 localhost = loopback --- | The broadcast IP address. --- --- >>> IPv4.broadcast --- ipv4 255 255 255 255 +{- | The broadcast IP address. + + >>> IPv4.broadcast + ipv4 255 255 255 255 +-} broadcast :: IPv4 broadcast = fromOctets 255 255 255 255 --- | Checks to see if the 'IPv4' address belongs to a private --- network. The three private networks that are checked are --- @10.0.0.0/8@, @172.16.0.0/12@, and @192.168.0.0/16@. +{- | Checks to see if the 'IPv4' address belongs to a private +network. The three private networks that are checked are +@10.0.0.0/8@, @172.16.0.0/12@, and @192.168.0.0/16@. +-} private :: IPv4 -> Bool private (IPv4 w) = - mask8 .&. w == p24 - || mask12 .&. w == p20 - || mask16 .&. w == p16 + mask8 .&. w == p24 + || mask12 .&. w == p20 + || mask16 .&. w == p16 ---------------------------------------- -- Note [The implementation of reserved] @@ -272,31 +300,32 @@ private (IPv4 w) = -- range. On the laptop that ran the microbenchmark, this function -- decided the reservedness of 100 random IPv4 addresses in 200ns. --- | Checks to see if the 'IPv4' address belongs to a reserved --- network. This includes the three private networks that 'private' --- checks along with several other ranges that are not used --- on the public Internet. The implementation of this function --- is optimized. +{- | Checks to see if the 'IPv4' address belongs to a reserved +network. This includes the three private networks that 'private' +checks along with several other ranges that are not used +on the public Internet. The implementation of this function +is optimized. +-} reserved :: IPv4 -> Bool reserved !(IPv4 w) = case unsafeShiftR w 29 of 0 -> let a = getIPv4 $ fromOctets' 0 0 0 0 y = getIPv4 $ fromOctets' 10 0 0 0 - in mask8 .&. w == a - || mask8 .&. w == y + in mask8 .&. w == a + || mask8 .&. w == y 1 -> False 2 -> False 3 -> let b = getIPv4 $ fromOctets' 100 64 0 0 c = getIPv4 $ fromOctets' 127 0 0 0 - in mask8 .&. w == c - || mask10 .&. w == b + in mask8 .&. w == c + || mask10 .&. w == b 4 -> False 5 -> let d = getIPv4 $ fromOctets' 169 254 0 0 x = getIPv4 $ fromOctets' 172 16 0 0 in mask12 .&. w == x - || mask16 .&. w == d + || mask16 .&. w == d 6 -> let e = getIPv4 $ fromOctets' 192 0 0 0 f = getIPv4 $ fromOctets' 192 0 2 0 @@ -306,150 +335,169 @@ reserved !(IPv4 w) = case unsafeShiftR w 29 of j = getIPv4 $ fromOctets' 203 0 113 0 z = getIPv4 $ fromOctets' 192 168 0 0 in mask15 .&. w == h - || mask16 .&. w == z - || mask24 .&. w == e - || mask24 .&. w == f - || mask24 .&. w == g - || mask24 .&. w == i - || mask24 .&. w == j + || mask16 .&. w == z + || mask24 .&. w == e + || mask24 .&. w == f + || mask24 .&. w == g + || mask24 .&. w == i + || mask24 .&. w == j _ -> True -mask8,mask12,mask16,mask10,mask24,mask15 :: Word32 -mask8 = 0xFF000000 +mask8, mask12, mask16, mask10, mask24, mask15 :: Word32 +mask8 = 0xFF000000 mask10 = 0xFFC00000 mask12 = 0xFFF00000 mask15 = 0xFFFE0000 mask16 = 0xFFFF0000 mask24 = 0xFFFFFF00 --- | Checks to see if the 'IPv4' address is publicly routable. --- --- prop> IPv4.public x == not (IPv4.reserved x) +{- | Checks to see if the 'IPv4' address is publicly routable. + +prop> IPv4.public x == not (IPv4.reserved x) +-} public :: IPv4 -> Bool public = not . reserved --- | Encode an 'IPv4' address to 'Text' using dot-decimal notation: --- --- >>> T.putStrLn (IPv4.encode (IPv4.ipv4 192 168 2 47)) --- 192.168.2.47 +{- | Encode an 'IPv4' address to 'Text' using dot-decimal notation: + + >>> T.putStrLn (IPv4.encode (IPv4.ipv4 192 168 2 47)) + 192.168.2.47 +-} encode :: IPv4 -> Text encode = toDotDecimalText --- | Decode an 'IPv4' address. --- --- >>> IPv4.decode "192.168.2.47" --- Just (ipv4 192 168 2 47) --- --- >>> IPv4.decode "10.100.256.256" --- Nothing +{- | Decode an 'IPv4' address. + + >>> IPv4.decode "192.168.2.47" + Just (ipv4 192 168 2 47) + + >>> IPv4.decode "10.100.256.256" + Nothing +-} decode :: Text -> Maybe IPv4 decode = decodeIPv4TextMaybe --- | Encode an 'IPv4' address to a text 'TBuilder.Builder'. --- --- >>> IPv4.builder (IPv4.ipv4 192 168 2 47) --- "192.168.2.47" +{- | Encode an 'IPv4' address to a text 'TBuilder.Builder'. + + >>> IPv4.builder (IPv4.ipv4 192 168 2 47) + "192.168.2.47" +-} builder :: IPv4 -> TBuilder.Builder builder = toDotDecimalBuilder --- | Parse an 'IPv4' address using a 'TextRead.Reader'. --- --- >>> IPv4.reader "192.168.2.47" --- Right (ipv4 192 168 2 47,"") --- --- >>> IPv4.reader "192.168.2.470" --- Left "All octets in an IPv4 address must be between 0 and 255" +{- | Parse an 'IPv4' address using a 'TextRead.Reader'. + + >>> IPv4.reader "192.168.2.47" + Right (ipv4 192 168 2 47,"") + + >>> IPv4.reader "192.168.2.470" + Left "All octets in an IPv4 address must be between 0 and 255" +-} reader :: TextRead.Reader IPv4 reader = decodeIPv4TextReader --- | Parse an 'IPv4' address using a 'AT.Parser'. --- --- >>> AT.parseOnly IPv4.parser "192.168.2.47" --- Right (ipv4 192 168 2 47) --- --- >>> AT.parseOnly IPv4.parser "192.168.2.470" --- Left "Failed reading: All octets in an IPv4 address must be between 0 and 255" +{- | Parse an 'IPv4' address using a 'AT.Parser'. + + >>> AT.parseOnly IPv4.parser "192.168.2.47" + Right (ipv4 192 168 2 47) + + >>> AT.parseOnly IPv4.parser "192.168.2.470" + Left "Failed reading: All octets in an IPv4 address must be between 0 and 255" +-} parser :: AT.Parser IPv4 parser = dotDecimalParser --- | Encode an 'IPv4' address to a UTF-8 encoded 'ByteString'. --- --- >>> IPv4.encodeUtf8 (IPv4.ipv4 192 168 2 47) --- "192.168.2.47" +{- | Encode an 'IPv4' address to a UTF-8 encoded 'ByteString'. + + >>> IPv4.encodeUtf8 (IPv4.ipv4 192 168 2 47) + "192.168.2.47" +-} encodeUtf8 :: IPv4 -> ByteString encodeUtf8 = toBSPreAllocated toBSPreAllocated :: IPv4 -> ByteString -toBSPreAllocated (IPv4 !w) = I.unsafeCreateUptoN 15 (\ptr1 -> - do len1 <- writeWord ptr1 w1 - let ptr2 = ptr1 `plusPtr` len1 - poke ptr2 dot - len2 <- writeWord (ptr2 `plusPtr` 1) w2 - let ptr3 = ptr2 `plusPtr` len2 `plusPtr` 1 - poke ptr3 dot - len3 <- writeWord (ptr3 `plusPtr` 1) w3 - let ptr4 = ptr3 `plusPtr` len3 `plusPtr` 1 - poke ptr4 dot - len4 <- writeWord (ptr4 `plusPtr` 1) w4 - return (3 + len1 + len2 + len3 + len4)) - where w1 = fromIntegral $ shiftR w 24 - w2 = fromIntegral $ shiftR w 16 - w3 = fromIntegral $ shiftR w 8 - w4 = fromIntegral w - dot = 46 :: Word8 - writeWord :: Ptr Word8 -> Word8 -> IO Int - writeWord !ptr !word - | word >= 100 = do - let int = fromIntegral word - indx = int + int + int - get3 = fromIntegral . ByteString.unsafeIndex threeDigits - poke ptr (get3 indx) - poke (ptr `plusPtr` 1) (get3 (indx + 1)) - poke (ptr `plusPtr` 2) (get3 (indx + 2)) - return 3 - | word >= 10 = do - let int = fromIntegral word - indx = int + int - get2 = fromIntegral . ByteString.unsafeIndex twoDigits - poke ptr (get2 indx) - poke (ptr `plusPtr` 1) (get2 (indx + 1)) - return 2 - | otherwise = do - poke ptr (word + 48) - return 1 - --- | Decode a UTF8-encoded 'ByteString' into an 'IPv4'. --- --- >>> IPv4.decodeUtf8 "192.168.2.47" --- Just (ipv4 192 168 2 47) --- --- Currently not terribly efficient since the implementation --- re-encodes the argument as UTF-16 text before decoding that --- IPv4 address from that. PRs to fix this are welcome. +toBSPreAllocated (IPv4 !w) = + I.unsafeCreateUptoN + 15 + ( \ptr1 -> + do + len1 <- writeWord ptr1 w1 + let ptr2 = ptr1 `plusPtr` len1 + poke ptr2 dot + len2 <- writeWord (ptr2 `plusPtr` 1) w2 + let ptr3 = ptr2 `plusPtr` len2 `plusPtr` 1 + poke ptr3 dot + len3 <- writeWord (ptr3 `plusPtr` 1) w3 + let ptr4 = ptr3 `plusPtr` len3 `plusPtr` 1 + poke ptr4 dot + len4 <- writeWord (ptr4 `plusPtr` 1) w4 + return (3 + len1 + len2 + len3 + len4) + ) + where + w1 = fromIntegral $ shiftR w 24 + w2 = fromIntegral $ shiftR w 16 + w3 = fromIntegral $ shiftR w 8 + w4 = fromIntegral w + dot = 46 :: Word8 + writeWord :: Ptr Word8 -> Word8 -> IO Int + writeWord !ptr !word + | word >= 100 = do + let int = fromIntegral word + indx = int + int + int + get3 = fromIntegral . ByteString.unsafeIndex threeDigits + poke ptr (get3 indx) + poke (ptr `plusPtr` 1) (get3 (indx + 1)) + poke (ptr `plusPtr` 2) (get3 (indx + 2)) + return 3 + | word >= 10 = do + let int = fromIntegral word + indx = int + int + get2 = fromIntegral . ByteString.unsafeIndex twoDigits + poke ptr (get2 indx) + poke (ptr `plusPtr` 1) (get2 (indx + 1)) + return 2 + | otherwise = do + poke ptr (word + 48) + return 1 + +{- | Decode a UTF8-encoded 'ByteString' into an 'IPv4'. + + >>> IPv4.decodeUtf8 "192.168.2.47" + Just (ipv4 192 168 2 47) + + Currently not terribly efficient since the implementation + re-encodes the argument as UTF-16 text before decoding that + IPv4 address from that. PRs to fix this are welcome. +-} decodeUtf8 :: ByteString -> Maybe IPv4 decodeUtf8 = decode <=< rightToMaybe . decodeUtf8' + -- This (decodeUtf8) should be rewritten to not go through text -- as an intermediary. --- | Decode 'ShortText' as an 'IPv4' address. --- --- >>> IPv4.decodeShort "192.168.3.48" --- Just (ipv4 192 168 3 48) +{- | Decode 'ShortText' as an 'IPv4' address. + + >>> IPv4.decodeShort "192.168.3.48" + Just (ipv4 192 168 3 48) +-} decodeShort :: ShortText -> Maybe IPv4 decodeShort t = decodeUtf8Bytes (Bytes.fromByteArray b) - where b = shortByteStringToByteArray (TS.toShortByteString t) + where + b = shortByteStringToByteArray (TS.toShortByteString t) + +{- | Encode an 'IPv4' address as 'ShortText'. --- | Encode an 'IPv4' address as 'ShortText'. --- --- >>> IPv4.encodeShort (IPv4.ipv4 192 168 5 99) --- "192.168.5.99" + >>> IPv4.encodeShort (IPv4.ipv4 192 168 5 99) + "192.168.5.99" +-} encodeShort :: IPv4 -> ShortText -encodeShort !w = id - $ TS.fromShortByteStringUnsafe - $ byteArrayToShortByteString - $ BB.run Nat.constant - $ boundedBuilderUtf8 - $ w +encodeShort !w = + id $ + TS.fromShortByteStringUnsafe $ + byteArrayToShortByteString $ + BB.run Nat.constant $ + boundedBuilderUtf8 $ + w shortByteStringToByteArray :: BSS.ShortByteString -> PM.ByteArray shortByteStringToByteArray (BSS.SBS x) = PM.ByteArray x @@ -457,10 +505,11 @@ shortByteStringToByteArray (BSS.SBS x) = PM.ByteArray x byteArrayToShortByteString :: PM.ByteArray -> BSS.ShortByteString byteArrayToShortByteString (PM.ByteArray x) = BSS.SBS x --- | Decode UTF-8-encoded 'Bytes' into an 'IPv4' address. --- --- >>> IPv4.decodeUtf8Bytes (Ascii.fromString "127.0.0.1") --- Just (ipv4 127 0 0 1) +{- | Decode UTF-8-encoded 'Bytes' into an 'IPv4' address. + + >>> IPv4.decodeUtf8Bytes (Ascii.fromString "127.0.0.1") + Just (ipv4 127 0 0 1) +-} decodeUtf8Bytes :: Bytes.Bytes -> Maybe IPv4 decodeUtf8Bytes !b = case Parser.parseBytes (parserUtf8Bytes ()) b of Parser.Success (Parser.Slice _ len addr) -> case len of @@ -468,17 +517,18 @@ decodeUtf8Bytes !b = case Parser.parseBytes (parserUtf8Bytes ()) b of _ -> Nothing Parser.Failure _ -> Nothing --- | Parse UTF-8-encoded 'Bytes' as an 'IPv4' address. --- --- >>> Parser.parseBytes (IPv4.parserUtf8Bytes ()) (Ascii.fromString "10.0.1.254") --- Success (Slice {offset = 10, length = 0, value = ipv4 10 0 1 254}) +{- | Parse UTF-8-encoded 'Bytes' as an 'IPv4' address. + + >>> Parser.parseBytes (IPv4.parserUtf8Bytes ()) (Ascii.fromString "10.0.1.254") + Success (Slice {offset = 10, length = 0, value = ipv4 10 0 1 254}) +-} parserUtf8Bytes :: e -> Parser.Parser e s IPv4 -{-# inline parserUtf8Bytes #-} +{-# INLINE parserUtf8Bytes #-} parserUtf8Bytes e = coerce (Parser.boxWord32 (parserUtf8Bytes# e)) -- | Variant of 'parserUtf8Bytes' with unboxed result type. parserUtf8Bytes# :: e -> Parser.Parser e s IPv4# -{-# noinline parserUtf8Bytes# #-} +{-# NOINLINE parserUtf8Bytes# #-} parserUtf8Bytes# e = Parser.unboxWord32 $ do !a <- Latin.decWord8 e Latin.char e '.' @@ -489,16 +539,17 @@ parserUtf8Bytes# e = Parser.unboxWord32 $ do !d <- Latin.decWord8 e pure (getIPv4 (fromOctets a b c d)) --- | Parse UTF-8-encoded 'Bytes' into an 'IPv4Range'. --- This requires the mask to be present. --- --- >>> maybe (putStrLn "nope") IPv4.printRange $ Parser.parseBytesMaybe (IPv4.parserRangeUtf8Bytes ()) (Ascii.fromString "192.168.0.0/16") --- 192.168.0.0/16 --- >>> maybe (putStrLn "nope") IPv4.printRange $ Parser.parseBytesMaybe (IPv4.parserRangeUtf8Bytes ()) (Ascii.fromString "10.10.10.1") --- nope --- --- See 'parserRangeUtf8BytesLenient' for a variant that treats --- a missing mask as a @/32@ mask. +{- | Parse UTF-8-encoded 'Bytes' into an 'IPv4Range'. +This requires the mask to be present. + +>>> maybe (putStrLn "nope") IPv4.printRange $ Parser.parseBytesMaybe (IPv4.parserRangeUtf8Bytes ()) (Ascii.fromString "192.168.0.0/16") +192.168.0.0/16 +>>> maybe (putStrLn "nope") IPv4.printRange $ Parser.parseBytesMaybe (IPv4.parserRangeUtf8Bytes ()) (Ascii.fromString "10.10.10.1") +nope + +See 'parserRangeUtf8BytesLenient' for a variant that treats +a missing mask as a @/32@ mask. +-} parserRangeUtf8Bytes :: e -> Parser.Parser e s IPv4Range parserRangeUtf8Bytes e = do base <- parserUtf8Bytes e @@ -508,17 +559,18 @@ parserRangeUtf8Bytes e = do then Parser.fail e else pure $! normalize (IPv4Range base theMask) --- | Variant of 'parserRangeUtf8Bytes' that allows the mask --- to be omitted. An omitted mask is treated as a @/32@ mask. --- --- >>> maybe (putStrLn "nope") IPv4.printRange $ Parser.parseBytesMaybe (IPv4.parserRangeUtf8BytesLenient ()) (Ascii.fromString "192.168.0.0/16") --- 192.168.0.0/16 --- >>> maybe (putStrLn "nope") IPv4.printRange $ Parser.parseBytesMaybe (IPv4.parserRangeUtf8BytesLenient ()) (Ascii.fromString "10.10.10.1") --- 10.10.10.1/32 +{- | Variant of 'parserRangeUtf8Bytes' that allows the mask +to be omitted. An omitted mask is treated as a @/32@ mask. + +>>> maybe (putStrLn "nope") IPv4.printRange $ Parser.parseBytesMaybe (IPv4.parserRangeUtf8BytesLenient ()) (Ascii.fromString "192.168.0.0/16") +192.168.0.0/16 +>>> maybe (putStrLn "nope") IPv4.printRange $ Parser.parseBytesMaybe (IPv4.parserRangeUtf8BytesLenient ()) (Ascii.fromString "10.10.10.1") +10.10.10.1/32 +-} parserRangeUtf8BytesLenient :: e -> Parser.Parser e s IPv4Range parserRangeUtf8BytesLenient e = do base <- parserUtf8Bytes e - Latin.trySatisfy (=='/') >>= \case + Latin.trySatisfy (== '/') >>= \case True -> do theMask <- Latin.decWord8 e if theMask > 32 @@ -526,110 +578,105 @@ parserRangeUtf8BytesLenient e = do else pure $! normalize (IPv4Range base theMask) False -> pure $! IPv4Range base 32 --- | Encode an 'IPv4' as a bytestring 'Builder.Builder' --- --- >>> Builder.toLazyByteString (IPv4.builderUtf8 (IPv4.fromOctets 192 168 2 12)) --- "192.168.2.12" +{- | Encode an 'IPv4' as a bytestring 'Builder.Builder' + +>>> Builder.toLazyByteString (IPv4.builderUtf8 (IPv4.fromOctets 192 168 2 12)) +"192.168.2.12" +-} builderUtf8 :: IPv4 -> Builder.Builder builderUtf8 = Builder.byteString . encodeUtf8 --- | Encode an 'IPv4' address as a unbounded byte array builder. --- --- >>> Chunks.concat (UB.run 1 (IPv4.byteArrayBuilderUtf8 (IPv4.fromOctets 192 168 2 13))) --- [0x31,0x39,0x32,0x2e,0x31,0x36,0x38,0x2e,0x32,0x2e,0x31,0x33] --- --- Note that period is encoded by UTF-8 as @0x2e@. +{- | Encode an 'IPv4' address as a unbounded byte array builder. + +>>> Chunks.concat (UB.run 1 (IPv4.byteArrayBuilderUtf8 (IPv4.fromOctets 192 168 2 13))) +[0x31,0x39,0x32,0x2e,0x31,0x36,0x38,0x2e,0x32,0x2e,0x31,0x33] + +Note that period is encoded by UTF-8 as @0x2e@. +-} byteArrayBuilderUtf8 :: IPv4 -> UB.Builder byteArrayBuilderUtf8 = UB.fromBounded Nat.constant . boundedBuilderUtf8 --- | Encode an 'IPv4' address as a bounded byte array builder. --- --- >>> BB.run Nat.constant (IPv4.boundedBuilderUtf8 (IPv4.fromOctets 192 168 2 14)) --- [0x31, 0x39, 0x32, 0x2e, 0x31, 0x36, 0x38, 0x2e, 0x32, 0x2e, 0x31, 0x34] --- --- Note that period is encoded by UTF-8 as @0x2e@. +{- | Encode an 'IPv4' address as a bounded byte array builder. + +>>> BB.run Nat.constant (IPv4.boundedBuilderUtf8 (IPv4.fromOctets 192 168 2 14)) +[0x31, 0x39, 0x32, 0x2e, 0x31, 0x36, 0x38, 0x2e, 0x32, 0x2e, 0x31, 0x34] + +Note that period is encoded by UTF-8 as @0x2e@. +-} boundedBuilderUtf8 :: IPv4 -> BB.Builder 15 boundedBuilderUtf8 (IPv4 !w) = BB.word8Dec w1 - `BB.append` - BB.ascii '.' - `BB.append` - BB.word8Dec w2 - `BB.append` - BB.ascii '.' - `BB.append` - BB.word8Dec w3 - `BB.append` - BB.ascii '.' - `BB.append` - BB.word8Dec w4 - where + `BB.append` BB.ascii '.' + `BB.append` BB.word8Dec w2 + `BB.append` BB.ascii '.' + `BB.append` BB.word8Dec w3 + `BB.append` BB.ascii '.' + `BB.append` BB.word8Dec w4 + where w1 = fromIntegral (shiftR w 24) :: Word8 w2 = fromIntegral (shiftR w 16) :: Word8 w3 = fromIntegral (shiftR w 8) :: Word8 w4 = fromIntegral w :: Word8 --- | Encode 'IPv4' address to a sequence a 4 bytes with the first --- byte representing corresponding to the most significant byte in --- the address. --- --- >>> BB.run Nat.constant (IPv4.boundedBuilderOctetsBE (IPv4.fromOctets 0xc0 0xa8 0x02 0x1f)) --- [0xc0, 0xa8, 0x02, 0x1f] +{- | Encode 'IPv4' address to a sequence a 4 bytes with the first +byte representing corresponding to the most significant byte in +the address. + +>>> BB.run Nat.constant (IPv4.boundedBuilderOctetsBE (IPv4.fromOctets 0xc0 0xa8 0x02 0x1f)) +[0xc0, 0xa8, 0x02, 0x1f] +-} boundedBuilderOctetsBE :: IPv4 -> BB.Builder 4 -{-# inline boundedBuilderOctetsBE #-} +{-# INLINE boundedBuilderOctetsBE #-} boundedBuilderOctetsBE (IPv4 !w) = BB.word8 w1 - `BB.append` - BB.word8 w2 - `BB.append` - BB.word8 w3 - `BB.append` - BB.word8 w4 - where + `BB.append` BB.word8 w2 + `BB.append` BB.word8 w3 + `BB.append` BB.word8 w4 + where w1 = fromIntegral (shiftR w 24) :: Word8 w2 = fromIntegral (shiftR w 16) :: Word8 w3 = fromIntegral (shiftR w 8) :: Word8 w4 = fromIntegral w :: Word8 --- | Encode 'IPv4' address to a sequence a 4 bytes with the first --- byte representing corresponding to the least significant byte in --- the address. --- --- >>> BB.run Nat.constant (IPv4.boundedBuilderOctetsLE (IPv4.fromOctets 0xc0 0xa8 0x02 0x1f)) --- [0x1f, 0x02, 0xa8, 0xc0] +{- | Encode 'IPv4' address to a sequence a 4 bytes with the first +byte representing corresponding to the least significant byte in +the address. + +>>> BB.run Nat.constant (IPv4.boundedBuilderOctetsLE (IPv4.fromOctets 0xc0 0xa8 0x02 0x1f)) +[0x1f, 0x02, 0xa8, 0xc0] +-} boundedBuilderOctetsLE :: IPv4 -> BB.Builder 4 -{-# inline boundedBuilderOctetsLE #-} +{-# INLINE boundedBuilderOctetsLE #-} boundedBuilderOctetsLE (IPv4 !w) = BB.word8 w4 - `BB.append` - BB.word8 w3 - `BB.append` - BB.word8 w2 - `BB.append` - BB.word8 w1 - where + `BB.append` BB.word8 w3 + `BB.append` BB.word8 w2 + `BB.append` BB.word8 w1 + where w1 = fromIntegral (shiftR w 24) :: Word8 w2 = fromIntegral (shiftR w 16) :: Word8 w3 = fromIntegral (shiftR w 8) :: Word8 w4 = fromIntegral w :: Word8 --- | Parse an 'IPv4' using a 'AB.Parser'. --- --- >>> AB.parseOnly IPv4.parserUtf8 "192.168.2.47" --- Right (ipv4 192 168 2 47) --- --- >>> AB.parseOnly IPv4.parserUtf8 "192.168.2.470" --- Left "Failed reading: All octets in an ipv4 address must be between 0 and 255" +{- | Parse an 'IPv4' using a 'AB.Parser'. + + >>> AB.parseOnly IPv4.parserUtf8 "192.168.2.47" + Right (ipv4 192 168 2 47) + + >>> AB.parseOnly IPv4.parserUtf8 "192.168.2.470" + Left "Failed reading: All octets in an ipv4 address must be between 0 and 255" +-} parserUtf8 :: AB.Parser IPv4 -parserUtf8 = fromOctets' - <$> (AB.decimal >>= limitSize) - <* AB.char '.' - <*> (AB.decimal >>= limitSize) - <* AB.char '.' - <*> (AB.decimal >>= limitSize) - <* AB.char '.' - <*> (AB.decimal >>= limitSize) - where +parserUtf8 = + fromOctets' + <$> (AB.decimal >>= limitSize) + <* AB.char '.' + <*> (AB.decimal >>= limitSize) + <* AB.char '.' + <*> (AB.decimal >>= limitSize) + <* AB.char '.' + <*> (AB.decimal >>= limitSize) + where limitSize i = if i > 255 then fail "All octets in an ipv4 address must be between 0 and 255" @@ -642,7 +689,6 @@ parserUtf8 = fromOctets' is discouraged unless the end user is working with a library that can only use 'String' to deal with textual data (such as @pandoc@, @hxr@, or @network@). - -} -- | Encode an 'IPv4' as a 'String'. @@ -653,44 +699,46 @@ encodeString = Text.unpack . encode decodeString :: String -> Maybe IPv4 decodeString = decode . Text.pack - --- | Unboxed variant of 'IPv4'. Before GHC 8.10, this is --- implemented as a type synonym. Portable use of this type requires --- treating it as though it were opaque. Use 'box' and 'unbox' to --- convert between this and the lifted 'IPv4'. +{- | Unboxed variant of 'IPv4'. Before GHC 8.10, this is +implemented as a type synonym. Portable use of this type requires +treating it as though it were opaque. Use 'box' and 'unbox' to +convert between this and the lifted 'IPv4'. +-} type IPv4# = Word# -- | Convert an unboxed IPv4 address to a boxed one. box :: IPv4# -> IPv4 -{-# inline box #-} +{-# INLINE box #-} box w = IPv4 (Compat.W32# w) -- | Convert a boxed IPv4 address to an unboxed one. unbox :: IPv4 -> IPv4# -{-# inline unbox #-} +{-# INLINE unbox #-} unbox (IPv4 (Compat.W32# w)) = w --- | A 32-bit Internet Protocol version 4 address. To use this with the --- @network@ library, it is necessary to use @Network.Socket.htonl@ to --- convert the underlying 'Word32' from host byte order to network byte --- order. -newtype IPv4 = IPv4 { getIPv4 :: Word32 } - deriving (Bits.Bits,Bounded,Data,Enum,Eq,Bits.FiniteBits,Generic,Hashable,Ix,Ord,Prim,Storable) +{- | A 32-bit Internet Protocol version 4 address. To use this with the + @network@ library, it is necessary to use @Network.Socket.htonl@ to + convert the underlying 'Word32' from host byte order to network byte + order. +-} +newtype IPv4 = IPv4 {getIPv4 :: Word32} + deriving (Bits.Bits, Bounded, Data, Enum, Eq, Bits.FiniteBits, Generic, Hashable, Ix, Ord, Prim, Storable) instance NFData IPv4 instance Show IPv4 where - showsPrec p addr = showParen (p > 10) - $ showString "ipv4 " - . showsPrec 11 a - . showChar ' ' - . showsPrec 11 b - . showChar ' ' - . showsPrec 11 c - . showChar ' ' - . showsPrec 11 d - where - (a,b,c,d) = toOctets addr + showsPrec p addr = + showParen (p > 10) $ + showString "ipv4 " + . showsPrec 11 a + . showChar ' ' + . showsPrec 11 b + . showChar ' ' + . showsPrec 11 c + . showChar ' ' + . showsPrec 11 d + where + (a, b, c, d) = toOctets addr instance Read IPv4 where readPrec = parens $ prec 10 $ do @@ -759,10 +807,11 @@ instance FromJSON IPv4 where parseJSON = Aeson.withText "IPv4" aesonParser instance ToJSONKey IPv4 where - toJSONKey = ToJSONKeyText - (keyFromText . encode) - (\addr -> Aeson.unsafeToEncoding $ Builder.char7 '"' <> builderUtf8 addr <> Builder.char7 '"') - where + toJSONKey = + ToJSONKeyText + (keyFromText . encode) + (\addr -> Aeson.unsafeToEncoding $ Builder.char7 '"' <> builderUtf8 addr <> Builder.char7 '"') + where #if MIN_VERSION_aeson(2,0,0) keyFromText = AesonKey.fromText #else @@ -784,72 +833,78 @@ aesonParser t = case decode t of decodeIPv4TextMaybe :: Text -> Maybe IPv4 decodeIPv4TextMaybe t = case decodeIPv4TextReader t of Left _ -> Nothing - Right (w,t') -> if Text.null t' - then Just w - else Nothing + Right (w, t') -> + if Text.null t' + then Just w + else Nothing decodeIPv4TextReader :: TextRead.Reader IPv4 decodeIPv4TextReader t1' = do - (a,t2) <- readOctet t1' + (a, t2) <- readOctet t1' t2' <- stripDecimal t2 - (b,t3) <- readOctet t2' + (b, t3) <- readOctet t2' t3' <- stripDecimal t3 - (c,t4) <- readOctet t3' + (c, t4) <- readOctet t3' t4' <- stripDecimal t4 - (d,t5) <- readOctet t4' - Right (fromOctets' a b c d,t5) - --- | Read an IPv4 octet (@0 <= n <= 255@) --- --- The input must begin with at least one decimal digit. Input is consumed --- until a non-digit is reached, the end of the input is reached, or the --- accumulated value exceeds the maximum bound (255). As with --- 'TextRead.decimal', any number of leading zeros are permitted. --- --- Optimizations: --- --- * The 'Char.isDigit' and 'Char.digitToInt' functions are avoided in order --- to avoiding checking the range more than once. This implementation calls --- 'Char.ord' (once) and uses the result for both the range check and the --- calculation. --- * The type of the accumulated value is 'Int', allowing for a single --- 'fromIntegral' call instead of one for each digit. This is possible --- because the maximum bound (255) is sufficiently less than the maximum --- bound of 'Int'. Specifically: @255 * 10 + Char.ord '9' <= maxBound@ --- * This implementation does not make use of @UnboxedTuples@ because the --- @span_@ function is part of the internal API. Additional performance --- could be gained by using this internal API function. + (d, t5) <- readOctet t4' + Right (fromOctets' a b c d, t5) + +{- | Read an IPv4 octet (@0 <= n <= 255@) + +The input must begin with at least one decimal digit. Input is consumed +until a non-digit is reached, the end of the input is reached, or the +accumulated value exceeds the maximum bound (255). As with +'TextRead.decimal', any number of leading zeros are permitted. + +Optimizations: + +* The 'Char.isDigit' and 'Char.digitToInt' functions are avoided in order + to avoiding checking the range more than once. This implementation calls + 'Char.ord' (once) and uses the result for both the range check and the + calculation. +* The type of the accumulated value is 'Int', allowing for a single + 'fromIntegral' call instead of one for each digit. This is possible + because the maximum bound (255) is sufficiently less than the maximum + bound of 'Int'. Specifically: @255 * 10 + Char.ord '9' <= maxBound@ +* This implementation does not make use of @UnboxedTuples@ because the + @span_@ function is part of the internal API. Additional performance + could be gained by using this internal API function. +-} readOctet :: TextRead.Reader Word readOctet t = do let (digits, rest) = Text.span Char.isDigit t when (Text.null digits) $ Left "octet does not start with a digit" case Text.foldr go Just digits 0 of - Just n -> Right (fromIntegral n, rest) + Just n -> Right (fromIntegral n, rest) Nothing -> Left ipOctetSizeErrorMsg - where + where go :: Char -> (Int -> Maybe Int) -> Int -> Maybe Int go !d !f !n = let n' = n * 10 + Char.ord d - 48 - in if n' <= 255 then f n' else Nothing + in if n' <= 255 then f n' else Nothing stripDecimal :: Text -> Either String Text stripDecimal t = case Text.uncons t of Nothing -> Left "expected a dot but input ended instead" - Just (c,tnext) -> if c == '.' - then Right tnext - else Left "expected a dot but found a different character" - --- | This is sort of a misnomer. It takes Word to make --- dotDecimalParser perform better. This is mostly --- for internal use. The arguments must all fit --- in a Word8. + Just (c, tnext) -> + if c == '.' + then Right tnext + else Left "expected a dot but found a different character" + +{- | This is sort of a misnomer. It takes Word to make + dotDecimalParser perform better. This is mostly + for internal use. The arguments must all fit + in a Word8. +-} fromOctets' :: Word -> Word -> Word -> Word -> IPv4 -fromOctets' a b c d = IPv4 $ fromIntegral - ( shiftL a 24 - .|. shiftL b 16 - .|. shiftL c 8 - .|. d - ) +fromOctets' a b c d = + IPv4 $ + fromIntegral + ( shiftL a 24 + .|. shiftL b 16 + .|. shiftL c 8 + .|. d + ) p24 :: Word32 p24 = getIPv4 (fromOctets' 10 0 0 0) @@ -860,18 +915,20 @@ p20 = getIPv4 (fromOctets' 172 16 0 0) p16 :: Word32 p16 = getIPv4 (fromOctets' 192 168 0 0) --- | This does not do an endOfInput check because it is --- reused in the range parser implementation. +{- | This does not do an endOfInput check because it is +reused in the range parser implementation. +-} dotDecimalParser :: AT.Parser IPv4 -dotDecimalParser = fromOctets' - <$> (AT.decimal >>= limitSize) - <* AT.char '.' - <*> (AT.decimal >>= limitSize) - <* AT.char '.' - <*> (AT.decimal >>= limitSize) - <* AT.char '.' - <*> (AT.decimal >>= limitSize) - where +dotDecimalParser = + fromOctets' + <$> (AT.decimal >>= limitSize) + <* AT.char '.' + <*> (AT.decimal >>= limitSize) + <* AT.char '.' + <*> (AT.decimal >>= limitSize) + <* AT.char '.' + <*> (AT.decimal >>= limitSize) + where limitSize i = if i > 255 then fail ipOctetSizeErrorMsg @@ -886,10 +943,11 @@ toDotDecimalText = toTextPreAllocated toDotDecimalBuilder :: IPv4 -> TBuilder.Builder toDotDecimalBuilder = TBuilder.fromText . toTextPreAllocated --- | I think that this function can be improved. Right now, it --- always allocates enough space for a fifteen-character text --- rendering of an IP address. I think that it should be possible --- to do more of the math upfront and allocate less space. +{- | I think that this function can be improved. Right now, it + always allocates enough space for a fifteen-character text + rendering of an IP address. I think that it should be possible + to do more of the math upfront and allocate less space. +-} toTextPreAllocated :: IPv4 -> Text toTextPreAllocated (IPv4 w) = let w1 = 255 .&. unsafeShiftR (fromIntegral w) 24 @@ -898,6 +956,7 @@ toTextPreAllocated (IPv4 w) = w4 = 255 .&. fromIntegral w in toTextPreallocatedPartTwo w1 w2 w3 w4 +{- FOURMOLU_DISABLE -} toTextPreallocatedPartTwo :: Word -> Word -> Word -> Word -> Text toTextPreallocatedPartTwo !w1 !w2 !w3 !w4 = #ifdef ghcjs_HOST_OS @@ -932,6 +991,7 @@ toTextPreallocatedPartTwo !w1 !w2 !w3 !w4 = return (theArr,i4 + n3') in Text arr 0 len #endif +{- FOURMOLU_ENABLE -} twoDigits :: ByteString twoDigits = foldMap (BC8.pack . printf "%02d") $ enumFromTo (0 :: Int) 99 @@ -941,7 +1001,7 @@ threeDigits :: ByteString threeDigits = foldMap (BC8.pack . printf "%03d") $ enumFromTo (0 :: Int) 999 {-# NOINLINE threeDigits #-} -i2w :: Integral a => a -> Codepoint +i2w :: (Integral a) => a -> Codepoint i2w v = zero + fromIntegral v zero :: Codepoint @@ -952,7 +1012,7 @@ putAndCount pos w marr | w < 10 = TArray.unsafeWrite marr pos (i2w w) >> return 1 | w < 100 = write2 pos w >> return 2 | otherwise = write3 pos w >> return 3 - where + where write2 off i0 = do let i = fromIntegral i0; j = i + i TArray.unsafeWrite marr off $ get2 j @@ -987,36 +1047,37 @@ Consequently, it is necessary to convert between the two as follows: These functions are not included with this library since it would require picking up a dependency on @network@. +-} + +{- $setup + +These are here to get doctest's property checking to work. +>>> import qualified Prelude as P +>>> import qualified Data.Text.IO as T +>>> import Net.IPv4 (fromOctets,ipv4) +>>> import Test.QuickCheck (Arbitrary(..)) +>>> instance Arbitrary IPv4 where { arbitrary = fmap IPv4 arbitrary } +>>> instance Arbitrary IPv4Range where { arbitrary = IPv4Range <$> arbitrary <*> arbitrary } -} --- $setup --- --- These are here to get doctest's property checking to work. --- --- >>> import qualified Prelude as P --- >>> import qualified Data.Text.IO as T --- >>> import Net.IPv4 (fromOctets,ipv4) --- >>> import Test.QuickCheck (Arbitrary(..)) --- >>> instance Arbitrary IPv4 where { arbitrary = fmap IPv4 arbitrary } --- >>> instance Arbitrary IPv4Range where { arbitrary = IPv4Range <$> arbitrary <*> arbitrary } --- - --- | Smart constructor for 'IPv4Range'. Ensures the mask is appropriately --- sized and sets masked bits in the 'IPv4' to zero. +{- | Smart constructor for 'IPv4Range'. Ensures the mask is appropriately + sized and sets masked bits in the 'IPv4' to zero. +-} range :: IPv4 -> Word8 -> IPv4Range range addr len = normalize (IPv4Range addr len) --- | Given an inclusive lower and upper ip address, create the smallest --- 'IPv4Range' that contains the two. This is helpful in situations where --- input given as a range like @192.168.16.0-192.168.19.255@ needs to be --- handled. This makes the range broader if it cannot be represented in --- CIDR notation. --- --- >>> IPv4.printRange $ IPv4.fromBounds (IPv4.fromOctets 192 168 16 0) (IPv4.fromOctets 192 168 19 255) --- 192.168.16.0/22 --- >>> IPv4.printRange $ IPv4.fromBounds (IPv4.fromOctets 10 0 5 7) (IPv4.fromOctets 10 0 5 14) --- 10.0.5.0/28 +{- | Given an inclusive lower and upper ip address, create the smallest +'IPv4Range' that contains the two. This is helpful in situations where +input given as a range like @192.168.16.0-192.168.19.255@ needs to be +handled. This makes the range broader if it cannot be represented in +CIDR notation. + +>>> IPv4.printRange $ IPv4.fromBounds (IPv4.fromOctets 192 168 16 0) (IPv4.fromOctets 192 168 19 255) +192.168.16.0/22 +>>> IPv4.printRange $ IPv4.fromBounds (IPv4.fromOctets 10 0 5 7) (IPv4.fromOctets 10 0 5 14) +10.0.5.0/28 +-} fromBounds :: IPv4 -> IPv4 -> IPv4Range fromBounds (IPv4 a) (IPv4 b) = normalize (IPv4Range (IPv4 a) (maskFromBounds a b)) @@ -1024,76 +1085,80 @@ fromBounds (IPv4 a) (IPv4 b) = maskFromBounds :: Word32 -> Word32 -> Word8 maskFromBounds lo hi = fromIntegral (Bits.countLeadingZeros (Bits.xor lo hi)) --- | Checks to see if an 'IPv4' address belongs in the 'IPv4Range'. --- --- >>> let ip = IPv4.fromOctets 10 10 1 92 --- >>> IPv4.contains (IPv4.IPv4Range (IPv4.fromOctets 10 0 0 0) 8) ip --- True --- >>> IPv4.contains (IPv4.IPv4Range (IPv4.fromOctets 10 11 0 0) 16) ip --- False --- --- Typically, element-testing functions are written to take the element --- as the first argument and the set as the second argument. This is intentionally --- written the other way for better performance when iterating over a collection. --- For example, you might test elements in a list for membership like this: --- --- >>> let r = IPv4.IPv4Range (IPv4.fromOctets 10 10 10 6) 31 --- >>> mapM_ (P.print . IPv4.contains r) (take 5 $ iterate succ $ IPv4.fromOctets 10 10 10 5) --- False --- True --- True --- False --- False --- --- The implementation of 'contains' ensures that (with GHC), the bitmask --- creation and range normalization only occur once in the above example. --- They are reused as the list is iterated. +{- | Checks to see if an 'IPv4' address belongs in the 'IPv4Range'. + +>>> let ip = IPv4.fromOctets 10 10 1 92 +>>> IPv4.contains (IPv4.IPv4Range (IPv4.fromOctets 10 0 0 0) 8) ip +True +>>> IPv4.contains (IPv4.IPv4Range (IPv4.fromOctets 10 11 0 0) 16) ip +False + +Typically, element-testing functions are written to take the element +as the first argument and the set as the second argument. This is intentionally +written the other way for better performance when iterating over a collection. +For example, you might test elements in a list for membership like this: + +>>> let r = IPv4.IPv4Range (IPv4.fromOctets 10 10 10 6) 31 +>>> mapM_ (P.print . IPv4.contains r) (take 5 $ iterate succ $ IPv4.fromOctets 10 10 10 5) +False +True +True +False +False + +The implementation of 'contains' ensures that (with GHC), the bitmask +creation and range normalization only occur once in the above example. +They are reused as the list is iterated. +-} contains :: IPv4Range -> IPv4 -> Bool contains (IPv4Range (IPv4 wsubnet) len) = let theMask = mask len wsubnetNormalized = wsubnet .&. theMask in \(IPv4 w) -> (w .&. theMask) == wsubnetNormalized --- | Checks if the first range is a subset of the second range. --- --- >>> IPv4.isSubsetOf (IPv4.IPv4Range (IPv4.fromOctets 192 0 2 128) 25) (IPv4.IPv4Range (IPv4.fromOctets 192 0 2 0) 24) --- True --- >>> IPv4.isSubsetOf (IPv4.IPv4Range (IPv4.fromOctets 192 0 2 0) 30) (IPv4.IPv4Range (IPv4.fromOctets 192 0 2 4) 30) --- False +{- | Checks if the first range is a subset of the second range. + +>>> IPv4.isSubsetOf (IPv4.IPv4Range (IPv4.fromOctets 192 0 2 128) 25) (IPv4.IPv4Range (IPv4.fromOctets 192 0 2 0) 24) +True +>>> IPv4.isSubsetOf (IPv4.IPv4Range (IPv4.fromOctets 192 0 2 0) 30) (IPv4.IPv4Range (IPv4.fromOctets 192 0 2 4) 30) +False +-} isSubsetOf :: IPv4Range -> IPv4Range -> Bool isSubsetOf a b = lowerInclusive a >= lowerInclusive b - && - upperInclusive a <= upperInclusive b + && upperInclusive a <= upperInclusive b mask :: Word8 -> Word32 mask = complement . shiftR 0xffffffff . fromIntegral --- | This is provided to mirror the interface provided by @Data.Set@. It --- behaves just like 'contains' but with flipped arguments. --- --- prop> IPv4.member ip r == IPv4.contains r ip +{- | This is provided to mirror the interface provided by @Data.Set@. It +behaves just like 'contains' but with flipped arguments. + +prop> IPv4.member ip r == IPv4.contains r ip +-} member :: IPv4 -> IPv4Range -> Bool member = flip contains --- | The inclusive lower bound of an 'IPv4Range'. This is conventionally --- understood to be the broadcast address of a subnet. For example: --- --- >>> T.putStrLn $ IPv4.encode $ IPv4.lowerInclusive $ IPv4.IPv4Range (IPv4.ipv4 10 10 1 160) 25 --- 10.10.1.128 --- --- Note that the lower bound of a normalized 'IPv4Range' is simply the --- ip address of the range: --- --- prop> IPv4.lowerInclusive r == IPv4.ipv4RangeBase (IPv4.normalize r) +{- | The inclusive lower bound of an 'IPv4Range'. This is conventionally + understood to be the broadcast address of a subnet. For example: + +>>> T.putStrLn $ IPv4.encode $ IPv4.lowerInclusive $ IPv4.IPv4Range (IPv4.ipv4 10 10 1 160) 25 +10.10.1.128 + +Note that the lower bound of a normalized 'IPv4Range' is simply the +ip address of the range: + +prop> IPv4.lowerInclusive r == IPv4.ipv4RangeBase (IPv4.normalize r) +-} lowerInclusive :: IPv4Range -> IPv4 lowerInclusive (IPv4Range (IPv4 w) len) = IPv4 (w .&. mask len) --- | The inclusive upper bound of an 'IPv4Range'. --- --- >>> T.putStrLn $ IPv4.encode $ IPv4.upperInclusive $ IPv4.IPv4Range (IPv4.ipv4 10 10 1 160) 25 --- 10.10.1.255 +{- | The inclusive upper bound of an 'IPv4Range'. + + >>> T.putStrLn $ IPv4.encode $ IPv4.upperInclusive $ IPv4.IPv4Range (IPv4.ipv4 10 10 1 160) 25 + 10.10.1.255 +-} upperInclusive :: IPv4Range -> IPv4 upperInclusive (IPv4Range (IPv4 w) len) = let theInvertedMask = shiftR 0xffffffff (fromIntegral len) @@ -1105,40 +1170,45 @@ upperInclusive (IPv4Range (IPv4 w) len) = -- addresses. Not exported. countAddrs :: Word8 -> Word64 countAddrs w = - let amountToShift = if w > 32 - then 0 - else 32 - fromIntegral w + let amountToShift = + if w > 32 + then 0 + else 32 - fromIntegral w in shift 1 amountToShift wordSuccessors :: Word64 -> IPv4 -> [IPv4] -wordSuccessors !w (IPv4 !a) = if w > 0 - then IPv4 a : wordSuccessors (w - 1) (IPv4 (a + 1)) - else [] - -wordSuccessorsM :: MonadPlus m => Word64 -> IPv4 -> m IPv4 -wordSuccessorsM = go where - go !w (IPv4 !a) = if w > 0 - then mplus (return (IPv4 a)) (go (w - 1) (IPv4 (a + 1))) - else mzero - --- | Convert an 'IPv4Range' into a list of the 'IPv4' addresses that --- are in it. --- --- >>> let r = IPv4.IPv4Range (IPv4.fromOctets 192 168 1 8) 30 --- >>> mapM_ (T.putStrLn . IPv4.encode) (IPv4.toList r) --- 192.168.1.8 --- 192.168.1.9 --- 192.168.1.10 --- 192.168.1.11 - +wordSuccessors !w (IPv4 !a) = + if w > 0 + then IPv4 a : wordSuccessors (w - 1) (IPv4 (a + 1)) + else [] + +wordSuccessorsM :: (MonadPlus m) => Word64 -> IPv4 -> m IPv4 +wordSuccessorsM = go + where + go !w (IPv4 !a) = + if w > 0 + then mplus (return (IPv4 a)) (go (w - 1) (IPv4 (a + 1))) + else mzero + +{- | Convert an 'IPv4Range' into a list of the 'IPv4' addresses that + are in it. + +>>> let r = IPv4.IPv4Range (IPv4.fromOctets 192 168 1 8) 30 +>>> mapM_ (T.putStrLn . IPv4.encode) (IPv4.toList r) +192.168.1.8 +192.168.1.9 +192.168.1.10 +192.168.1.11 +-} toList :: IPv4Range -> [IPv4] toList (IPv4Range ip len) = let totalAddrs = countAddrs len in wordSuccessors totalAddrs ip --- | A stream-polymorphic generator over an 'IPv4Range'. --- For more information, see . -toGenerator :: MonadPlus m => IPv4Range -> m IPv4 +{- | A stream-polymorphic generator over an 'IPv4Range'. + For more information, see . +-} +toGenerator :: (MonadPlus m) => IPv4Range -> m IPv4 toGenerator (IPv4Range ip len) = let totalAddrs = countAddrs len in wordSuccessorsM totalAddrs ip @@ -1149,88 +1219,96 @@ private24 = IPv4Range (fromOctets 10 0 0 0) 8 -- | The RFC1918 20-bit block. Subnet mask: @172.16.0.0/12@ private20 :: IPv4Range -private20 = IPv4Range (fromOctets 172 16 0 0) 12 +private20 = IPv4Range (fromOctets 172 16 0 0) 12 -- | The RFC1918 16-bit block. Subnet mask: @192.168.0.0/16@ private16 :: IPv4Range private16 = IPv4Range (fromOctets 192 168 0 0) 16 --- | Normalize an 'IPv4Range'. The first result of this is that the --- 'IPv4' inside the 'IPv4Range' is changed so that the insignificant --- bits are zeroed out. For example: --- --- >>> IPv4.printRange $ IPv4.normalize $ IPv4.IPv4Range (IPv4.fromOctets 192 168 1 19) 24 --- 192.168.1.0/24 --- >>> IPv4.printRange $ IPv4.normalize $ IPv4.IPv4Range (IPv4.fromOctets 192 168 1 163) 28 --- 192.168.1.160/28 --- --- The second effect of this is that the mask length is lowered to --- be 32 or smaller. Working with 'IPv4Range's that have not been --- normalized does not cause any issues for this library, although --- other applications may reject such ranges (especially those with --- a mask length above 32). --- --- Note that 'normalize' is idempotent, that is: --- --- prop> IPv4.normalize r == (IPv4.normalize . IPv4.normalize) r +{- | Normalize an 'IPv4Range'. The first result of this is that the +'IPv4' inside the 'IPv4Range' is changed so that the insignificant +bits are zeroed out. For example: + +>>> IPv4.printRange $ IPv4.normalize $ IPv4.IPv4Range (IPv4.fromOctets 192 168 1 19) 24 +192.168.1.0/24 +>>> IPv4.printRange $ IPv4.normalize $ IPv4.IPv4Range (IPv4.fromOctets 192 168 1 163) 28 +192.168.1.160/28 + +The second effect of this is that the mask length is lowered to +be 32 or smaller. Working with 'IPv4Range's that have not been +normalized does not cause any issues for this library, although +other applications may reject such ranges (especially those with +a mask length above 32). + +Note that 'normalize' is idempotent, that is: + +prop> IPv4.normalize r == (IPv4.normalize . IPv4.normalize) r +-} normalize :: IPv4Range -> IPv4Range normalize (IPv4Range (IPv4 w) len) = let len' = min len 32 w' = w .&. mask len' in IPv4Range (IPv4 w') len' --- | Encode an 'IPv4Range' as 'Text'. --- --- >>> IPv4.encodeRange (IPv4.IPv4Range (IPv4.ipv4 172 16 0 0) 12) --- "172.16.0.0/12" +{- | Encode an 'IPv4Range' as 'Text'. + + >>> IPv4.encodeRange (IPv4.IPv4Range (IPv4.ipv4 172 16 0 0) 12) + "172.16.0.0/12" +-} encodeRange :: IPv4Range -> Text encodeRange = rangeToDotDecimalText --- | Decode an 'IPv4Range' from 'Text'. --- --- >>> IPv4.decodeRange "172.16.0.0/12" --- Just (IPv4Range {ipv4RangeBase = ipv4 172 16 0 0, ipv4RangeLength = 12}) --- >>> IPv4.decodeRange "192.168.25.254/16" --- Just (IPv4Range {ipv4RangeBase = ipv4 192 168 0 0, ipv4RangeLength = 16}) +{- | Decode an 'IPv4Range' from 'Text'. + + >>> IPv4.decodeRange "172.16.0.0/12" + Just (IPv4Range {ipv4RangeBase = ipv4 172 16 0 0, ipv4RangeLength = 12}) + >>> IPv4.decodeRange "192.168.25.254/16" + Just (IPv4Range {ipv4RangeBase = ipv4 192 168 0 0, ipv4RangeLength = 16}) +-} decodeRange :: Text -> Maybe IPv4Range decodeRange = rightToMaybe . AT.parseOnly (parserRange <* AT.endOfInput) --- | Encode an 'IPv4Range' to a 'TBuilder.Builder'. --- --- >>> IPv4.builderRange (IPv4.IPv4Range (IPv4.ipv4 172 16 0 0) 12) --- "172.16.0.0/12" +{- | Encode an 'IPv4Range' to a 'TBuilder.Builder'. + + >>> IPv4.builderRange (IPv4.IPv4Range (IPv4.ipv4 172 16 0 0) 12) + "172.16.0.0/12" +-} builderRange :: IPv4Range -> TBuilder.Builder builderRange = rangeToDotDecimalBuilder --- | Parse an 'IPv4Range' using a 'AT.Parser'. --- --- >>> AT.parseOnly IPv4.parserRange "192.168.25.254/16" --- Right (IPv4Range {ipv4RangeBase = ipv4 192 168 0 0, ipv4RangeLength = 16}) +{- | Parse an 'IPv4Range' using a 'AT.Parser'. + + >>> AT.parseOnly IPv4.parserRange "192.168.25.254/16" + Right (IPv4Range {ipv4RangeBase = ipv4 192 168 0 0, ipv4RangeLength = 16}) +-} parserRange :: AT.Parser IPv4Range parserRange = do ip <- parser _ <- AT.char '/' theMask <- AT.decimal >>= limitSize return (normalize (IPv4Range ip theMask)) - where + where limitSize i = if i > 32 then fail "An IP range length must be between 0 and 32" else return i --- | Print an 'IPv4Range'. Helper function that --- exists mostly for testing purposes. +{- | Print an 'IPv4Range'. Helper function that + exists mostly for testing purposes. +-} printRange :: IPv4Range -> IO () printRange = TIO.putStrLn . encodeRange --- | The length should be between 0 and 32. These bounds are inclusive. --- This expectation is not in any way enforced by this library because --- it does not cause errors. A mask length greater than 32 will be --- treated as if it were 32. +{- | The length should be between 0 and 32. These bounds are inclusive. + This expectation is not in any way enforced by this library because + it does not cause errors. A mask length greater than 32 will be + treated as if it were 32. +-} data IPv4Range = IPv4Range - { ipv4RangeBase :: {-# UNPACK #-} !IPv4 + { ipv4RangeBase :: {-# UNPACK #-} !IPv4 , ipv4RangeLength :: {-# UNPACK #-} !Word8 - } deriving (Eq,Ord,Show,Read,Generic,Data) + } + deriving (Eq, Ord, Show, Read, Generic, Data) instance NFData IPv4Range instance Hashable IPv4Range @@ -1244,114 +1322,118 @@ instance FromJSON IPv4Range where Just res -> return res parseJSON _ = mzero -data instance MUVector.MVector s IPv4Range = MV_IPv4Range - !(MUVector.MVector s IPv4) - !(MUVector.MVector s Word8) -data instance UVector.Vector IPv4Range = V_IPv4Range - !(UVector.Vector IPv4) - !(UVector.Vector Word8) +data instance MUVector.MVector s IPv4Range + = MV_IPv4Range + !(MUVector.MVector s IPv4) + !(MUVector.MVector s Word8) +data instance UVector.Vector IPv4Range + = V_IPv4Range + !(UVector.Vector IPv4) + !(UVector.Vector Word8) instance UVector.Unbox IPv4Range instance MGVector.MVector MUVector.MVector IPv4Range where - {-# INLINE basicLength #-} + {-# INLINE basicLength #-} basicLength (MV_IPv4Range as _) = MGVector.basicLength as - {-# INLINE basicUnsafeSlice #-} - basicUnsafeSlice i_ m_ (MV_IPv4Range as bs) - = MV_IPv4Range (MGVector.basicUnsafeSlice i_ m_ as) - (MGVector.basicUnsafeSlice i_ m_ bs) - {-# INLINE basicOverlaps #-} - basicOverlaps (MV_IPv4Range as1 bs1) (MV_IPv4Range as2 bs2) - = MGVector.basicOverlaps as1 as2 - || MGVector.basicOverlaps bs1 bs2 - {-# INLINE basicUnsafeNew #-} - basicUnsafeNew n_ - = do - as <- MGVector.basicUnsafeNew n_ - bs <- MGVector.basicUnsafeNew n_ - return $ MV_IPv4Range as bs - {-# INLINE basicInitialize #-} - basicInitialize (MV_IPv4Range as bs) - = do - MGVector.basicInitialize as - MGVector.basicInitialize bs - {-# INLINE basicUnsafeReplicate #-} - basicUnsafeReplicate n_ (IPv4Range a b) - = do - as <- MGVector.basicUnsafeReplicate n_ a - bs <- MGVector.basicUnsafeReplicate n_ b - return (MV_IPv4Range as bs) - {-# INLINE basicUnsafeRead #-} - basicUnsafeRead (MV_IPv4Range as bs) i_ - = do - a <- MGVector.basicUnsafeRead as i_ - b <- MGVector.basicUnsafeRead bs i_ - return (IPv4Range a b) - {-# INLINE basicUnsafeWrite #-} - basicUnsafeWrite (MV_IPv4Range as bs) i_ (IPv4Range a b) - = do - MGVector.basicUnsafeWrite as i_ a - MGVector.basicUnsafeWrite bs i_ b - {-# INLINE basicClear #-} - basicClear (MV_IPv4Range as bs) - = do - MGVector.basicClear as - MGVector.basicClear bs - {-# INLINE basicSet #-} - basicSet (MV_IPv4Range as bs) (IPv4Range a b) - = do - MGVector.basicSet as a - MGVector.basicSet bs b - {-# INLINE basicUnsafeCopy #-} - basicUnsafeCopy (MV_IPv4Range as1 bs1) (MV_IPv4Range as2 bs2) - = do - MGVector.basicUnsafeCopy as1 as2 - MGVector.basicUnsafeCopy bs1 bs2 - {-# INLINE basicUnsafeMove #-} - basicUnsafeMove (MV_IPv4Range as1 bs1) (MV_IPv4Range as2 bs2) - = do - MGVector.basicUnsafeMove as1 as2 - MGVector.basicUnsafeMove bs1 bs2 - {-# INLINE basicUnsafeGrow #-} - basicUnsafeGrow (MV_IPv4Range as bs) m_ - = do - as' <- MGVector.basicUnsafeGrow as m_ - bs' <- MGVector.basicUnsafeGrow bs m_ - return $ MV_IPv4Range as' bs' + {-# INLINE basicUnsafeSlice #-} + basicUnsafeSlice i_ m_ (MV_IPv4Range as bs) = + MV_IPv4Range + (MGVector.basicUnsafeSlice i_ m_ as) + (MGVector.basicUnsafeSlice i_ m_ bs) + {-# INLINE basicOverlaps #-} + basicOverlaps (MV_IPv4Range as1 bs1) (MV_IPv4Range as2 bs2) = + MGVector.basicOverlaps as1 as2 + || MGVector.basicOverlaps bs1 bs2 + {-# INLINE basicUnsafeNew #-} + basicUnsafeNew n_ = + do + as <- MGVector.basicUnsafeNew n_ + bs <- MGVector.basicUnsafeNew n_ + return $ MV_IPv4Range as bs + {-# INLINE basicInitialize #-} + basicInitialize (MV_IPv4Range as bs) = + do + MGVector.basicInitialize as + MGVector.basicInitialize bs + {-# INLINE basicUnsafeReplicate #-} + basicUnsafeReplicate n_ (IPv4Range a b) = + do + as <- MGVector.basicUnsafeReplicate n_ a + bs <- MGVector.basicUnsafeReplicate n_ b + return (MV_IPv4Range as bs) + {-# INLINE basicUnsafeRead #-} + basicUnsafeRead (MV_IPv4Range as bs) i_ = + do + a <- MGVector.basicUnsafeRead as i_ + b <- MGVector.basicUnsafeRead bs i_ + return (IPv4Range a b) + {-# INLINE basicUnsafeWrite #-} + basicUnsafeWrite (MV_IPv4Range as bs) i_ (IPv4Range a b) = + do + MGVector.basicUnsafeWrite as i_ a + MGVector.basicUnsafeWrite bs i_ b + {-# INLINE basicClear #-} + basicClear (MV_IPv4Range as bs) = + do + MGVector.basicClear as + MGVector.basicClear bs + {-# INLINE basicSet #-} + basicSet (MV_IPv4Range as bs) (IPv4Range a b) = + do + MGVector.basicSet as a + MGVector.basicSet bs b + {-# INLINE basicUnsafeCopy #-} + basicUnsafeCopy (MV_IPv4Range as1 bs1) (MV_IPv4Range as2 bs2) = + do + MGVector.basicUnsafeCopy as1 as2 + MGVector.basicUnsafeCopy bs1 bs2 + {-# INLINE basicUnsafeMove #-} + basicUnsafeMove (MV_IPv4Range as1 bs1) (MV_IPv4Range as2 bs2) = + do + MGVector.basicUnsafeMove as1 as2 + MGVector.basicUnsafeMove bs1 bs2 + {-# INLINE basicUnsafeGrow #-} + basicUnsafeGrow (MV_IPv4Range as bs) m_ = + do + as' <- MGVector.basicUnsafeGrow as m_ + bs' <- MGVector.basicUnsafeGrow bs m_ + return $ MV_IPv4Range as' bs' instance GVector.Vector UVector.Vector IPv4Range where - {-# INLINE basicUnsafeFreeze #-} - basicUnsafeFreeze (MV_IPv4Range as bs) - = do - as' <- GVector.basicUnsafeFreeze as - bs' <- GVector.basicUnsafeFreeze bs - return $ V_IPv4Range as' bs' - {-# INLINE basicUnsafeThaw #-} - basicUnsafeThaw (V_IPv4Range as bs) - = do - as' <- GVector.basicUnsafeThaw as - bs' <- GVector.basicUnsafeThaw bs - return $ MV_IPv4Range as' bs' - {-# INLINE basicLength #-} + {-# INLINE basicUnsafeFreeze #-} + basicUnsafeFreeze (MV_IPv4Range as bs) = + do + as' <- GVector.basicUnsafeFreeze as + bs' <- GVector.basicUnsafeFreeze bs + return $ V_IPv4Range as' bs' + {-# INLINE basicUnsafeThaw #-} + basicUnsafeThaw (V_IPv4Range as bs) = + do + as' <- GVector.basicUnsafeThaw as + bs' <- GVector.basicUnsafeThaw bs + return $ MV_IPv4Range as' bs' + {-# INLINE basicLength #-} basicLength (V_IPv4Range as _) = GVector.basicLength as - {-# INLINE basicUnsafeSlice #-} - basicUnsafeSlice i_ m_ (V_IPv4Range as bs) - = V_IPv4Range (GVector.basicUnsafeSlice i_ m_ as) - (GVector.basicUnsafeSlice i_ m_ bs) - {-# INLINE basicUnsafeIndexM #-} - basicUnsafeIndexM (V_IPv4Range as bs) i_ - = do - a <- GVector.basicUnsafeIndexM as i_ - b <- GVector.basicUnsafeIndexM bs i_ - return (IPv4Range a b) - {-# INLINE basicUnsafeCopy #-} - basicUnsafeCopy (MV_IPv4Range as1 bs1) (V_IPv4Range as2 bs2) - = do - GVector.basicUnsafeCopy as1 as2 - GVector.basicUnsafeCopy bs1 bs2 - {-# INLINE elemseq #-} - elemseq _ (IPv4Range a b) - = GVector.elemseq (undefined :: UVector.Vector a) a - . GVector.elemseq (undefined :: UVector.Vector b) b + {-# INLINE basicUnsafeSlice #-} + basicUnsafeSlice i_ m_ (V_IPv4Range as bs) = + V_IPv4Range + (GVector.basicUnsafeSlice i_ m_ as) + (GVector.basicUnsafeSlice i_ m_ bs) + {-# INLINE basicUnsafeIndexM #-} + basicUnsafeIndexM (V_IPv4Range as bs) i_ = + do + a <- GVector.basicUnsafeIndexM as i_ + b <- GVector.basicUnsafeIndexM bs i_ + return (IPv4Range a b) + {-# INLINE basicUnsafeCopy #-} + basicUnsafeCopy (MV_IPv4Range as1 bs1) (V_IPv4Range as2 bs2) = + do + GVector.basicUnsafeCopy as1 as2 + GVector.basicUnsafeCopy bs1 bs2 + {-# INLINE elemseq #-} + elemseq _ (IPv4Range a b) = + GVector.elemseq (undefined :: UVector.Vector a) a + . GVector.elemseq (undefined :: UVector.Vector b) b ----------------- -- Internal Stuff @@ -1362,7 +1444,6 @@ rangeToDotDecimalText = LText.toStrict . TBuilder.toLazyText . rangeToDotDecimal rangeToDotDecimalBuilder :: IPv4Range -> TBuilder.Builder rangeToDotDecimalBuilder (IPv4Range addr len) = - builder addr - <> TBuilder.singleton '/' - <> TBI.decimal len - + builder addr + <> TBuilder.singleton '/' + <> TBI.decimal len diff --git a/src/Net/IPv6.hs b/src/Net/IPv6.hs index dfa9baf..cf8d6ae 100644 --- a/src/Net/IPv6.hs +++ b/src/Net/IPv6.hs @@ -8,10 +8,9 @@ {-# LANGUAGE LambdaCase #-} {-# LANGUAGE MagicHash #-} {-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE TypeInType #-} {-# LANGUAGE UnboxedTuples #-} -{-| This module provides the IPv6 data type and functions for working +{- | This module provides the IPv6 data type and functions for working with it. -} module Net.IPv6 @@ -24,24 +23,31 @@ module Net.IPv6 , fromTupleWord32s , toWord16s , toWord32s + -- * Special IP Addresses , any , loopback , localhost + -- * Textual Conversion + -- ** Text , encode , encodeShort , decode , decodeShort , parser + -- * UTF-8 Bytes , parserUtf8Bytes , decodeUtf8Bytes , boundedBuilderUtf8 + -- ** Printing , print + -- * IPv6 Ranges + -- ** Range functions , range , fromBounds @@ -51,23 +57,27 @@ module Net.IPv6 , member , lowerInclusive , upperInclusive + -- ** Textual Conversion + -- *** Text , encodeRange , decodeRange , parserRange , printRange + -- ** UTF-8 Bytes , parserRangeUtf8Bytes , parserRangeUtf8BytesLenient + -- * Types - , IPv6(..) - , IPv6Range(..) + , IPv6 (..) + , IPv6Range (..) ) where import Prelude hiding (any, print) -import Net.IPv4 (IPv4(..)) +import Net.IPv4 (IPv4 (..)) import Control.Applicative import Control.DeepSeq (NFData) @@ -76,105 +86,108 @@ import Control.Monad.ST (ST) import Data.Bits import Data.Char (chr) import Data.Data (Data) +import Data.Hashable (Hashable, hashWithSalt) import Data.Ix (Ix) -import Data.Hashable (Hashable,hashWithSalt) -import Data.List (intercalate, group) +import Data.List (group, intercalate) import Data.Primitive (MutablePrimArray) import Data.Primitive.Types (Prim) import Data.Text (Text) import Data.Text.Short (ShortText) -import Data.WideWord.Word128 (Word128(..), zeroWord128) +import Data.WideWord.Word128 (Word128 (..), zeroWord128) import Data.Word import Foreign.Storable (Storable) -import GHC.Exts (Int#,Word#,Int(I#)) +import GHC.Exts (Int (I#), Int#, Word#) import GHC.Generics (Generic) import Numeric (showHex) -import Text.ParserCombinators.ReadPrec (prec,step) -import Text.Read (Read(..),Lexeme(Ident),lexP,parens) +import Text.ParserCombinators.ReadPrec (prec, step) +import Text.Read (Lexeme (Ident), Read (..), lexP, parens) import qualified Arithmetic.Lte as Lte import qualified Arithmetic.Nat as Nat import qualified Data.Aeson as Aeson import qualified Data.Attoparsec.Text as AT import qualified Data.Attoparsec.Text as Atto -import qualified Data.Bytes.Builder.Bounded as BB +import qualified Data.ByteString.Short.Internal as BSS import qualified Data.Bytes as Bytes +import qualified Data.Bytes.Builder.Bounded as BB import qualified Data.Bytes.Parser as Parser import qualified Data.Bytes.Parser.Latin as Latin -import qualified Data.ByteString.Short.Internal as BSS import qualified Data.Primitive as PM import qualified Data.Text as Text import qualified Data.Text.IO as TIO -import qualified Data.Text.Short.Unsafe as TS import qualified Data.Text.Short as TS +import qualified Data.Text.Short.Unsafe as TS import qualified GHC.Word.Compat as Compat import qualified Net.IPv4 as IPv4 --- $setup --- --- These are here to get doctest work. --- --- >>> import qualified Prelude as P --- >>> import qualified Data.Text.IO as T --- >>> import qualified Data.Text as Text --- >>> import qualified Data.Attoparsec.Text as Atto --- >>> import qualified Data.Bytes.Text.Ascii as Ascii --- >>> import Test.QuickCheck (Arbitrary(..)) --- >>> instance Arbitrary Word128 where { arbitrary = Word128 <$> arbitrary <*> arbitrary } --- >>> instance Arbitrary IPv6 where { arbitrary = IPv6 <$> arbitrary } --- >>> instance Arbitrary IPv6.IPv6Range where { arbitrary = IPv6.IPv6Range <$> arbitrary <*> arbitrary } --- +{- $setup + +These are here to get doctest work. + +>>> import qualified Prelude as P +>>> import qualified Data.Text.IO as T +>>> import qualified Data.Text as Text +>>> import qualified Data.Attoparsec.Text as Atto +>>> import qualified Data.Bytes.Text.Ascii as Ascii +>>> import Test.QuickCheck (Arbitrary(..)) +>>> instance Arbitrary Word128 where { arbitrary = Word128 <$> arbitrary <*> arbitrary } +>>> instance Arbitrary IPv6 where { arbitrary = IPv6 <$> arbitrary } +>>> instance Arbitrary IPv6.IPv6Range where { arbitrary = IPv6.IPv6Range <$> arbitrary <*> arbitrary } +-} -- | A 128-bit Internet Protocol version 6 address. -newtype IPv6 = IPv6 { getIPv6 :: Word128 } - deriving (Bounded,Enum,Eq,Ord,Storable,Bits,FiniteBits,NFData,Prim,Ix,Data,Generic) +newtype IPv6 = IPv6 {getIPv6 :: Word128} + deriving (Bounded, Enum, Eq, Ord, Storable, Bits, FiniteBits, NFData, Prim, Ix, Data, Generic) instance Hashable IPv6 where hashWithSalt s (IPv6 (Word128 a b)) = hashWithSalt (hashWithSalt s a) b instance Show IPv6 where - showsPrec p addr = showParen (p > 10) - $ showString "ipv6 " - . showHexWord16 a - . showChar ' ' - . showHexWord16 b - . showChar ' ' - . showHexWord16 c - . showChar ' ' - . showHexWord16 d - . showChar ' ' - . showHexWord16 e - . showChar ' ' - . showHexWord16 f - . showChar ' ' - . showHexWord16 g - . showChar ' ' - . showHexWord16 h - where - (a,b,c,d,e,f,g,h) = toWord16s addr + showsPrec p addr = + showParen (p > 10) $ + showString "ipv6 " + . showHexWord16 a + . showChar ' ' + . showHexWord16 b + . showChar ' ' + . showHexWord16 c + . showChar ' ' + . showHexWord16 d + . showChar ' ' + . showHexWord16 e + . showChar ' ' + . showHexWord16 f + . showChar ' ' + . showHexWord16 g + . showChar ' ' + . showHexWord16 h + where + (a, b, c, d, e, f, g, h) = toWord16s addr -- | Print an 'IPv6' using the textual encoding. print :: IPv6 -> IO () print = TIO.putStrLn . encode --- | Decode 'ShortText' as an 'IPv6' address. --- --- >>> decodeShort "ffff::2:b" --- Just (ipv6 0xffff 0x0000 0x0000 0x0000 0x0000 0x0000 0x0002 0x000b) +{- | Decode 'ShortText' as an 'IPv6' address. + + >>> decodeShort "ffff::2:b" + Just (ipv6 0xffff 0x0000 0x0000 0x0000 0x0000 0x0000 0x0002 0x000b) +-} decodeShort :: ShortText -> Maybe IPv6 decodeShort t = decodeUtf8Bytes (Bytes.fromByteArray b) - where b = shortByteStringToByteArray (TS.toShortByteString t) + where + b = shortByteStringToByteArray (TS.toShortByteString t) shortByteStringToByteArray :: BSS.ShortByteString -> PM.ByteArray shortByteStringToByteArray (BSS.SBS x) = PM.ByteArray x showHexWord16 :: Word16 -> ShowS showHexWord16 w = - showString "0x" - . showChar (nibbleToHex (unsafeShiftR (fromIntegral w) 12)) - . showChar (nibbleToHex ((unsafeShiftR (fromIntegral w) 8) .&. 0xF)) - . showChar (nibbleToHex ((unsafeShiftR (fromIntegral w) 4) .&. 0xF)) - . showChar (nibbleToHex ((fromIntegral w) .&. 0xF)) + showString "0x" + . showChar (nibbleToHex (unsafeShiftR (fromIntegral w) 12)) + . showChar (nibbleToHex ((unsafeShiftR (fromIntegral w) 8) .&. 0xF)) + . showChar (nibbleToHex ((unsafeShiftR (fromIntegral w) 4) .&. 0xF)) + . showChar (nibbleToHex ((fromIntegral w) .&. 0xF)) -- invariant: argument must be less than 16 nibbleToHex :: Word -> Char @@ -201,101 +214,167 @@ instance Aeson.ToJSON IPv6 where instance Aeson.FromJSON IPv6 where parseJSON = Aeson.withText "IPv6" $ \t -> case decode t of Nothing -> fail "invalid IPv6 address" - Just i -> return i + Just i -> return i rightToMaybe :: Either a b -> Maybe b rightToMaybe = either (const Nothing) Just --- | This could be useful for the rare occasion --- in which one could construct an 'IPv6' from --- octets. --- --- Note that while @Net.IPv4.'Net.IPv4.fromOctets' = Net.IPv4.'Net.IPv4.ipv4'@, --- @Net.IPv6.fromOctets /= Net.IPv6.ipv6@. While this should be obvious --- from their types, it is worth mentioning since the similarity in naming --- might be confusing. +{- | This could be useful for the rare occasion + in which one could construct an 'IPv6' from + octets. + + Note that while @Net.IPv4.'Net.IPv4.fromOctets' = Net.IPv4.'Net.IPv4.ipv4'@, + @Net.IPv6.fromOctets /= Net.IPv6.ipv6@. While this should be obvious + from their types, it is worth mentioning since the similarity in naming + might be confusing. +-} fromOctets :: - Word8 -> Word8 -> Word8 -> Word8 - -> Word8 -> Word8 -> Word8 -> Word8 - -> Word8 -> Word8 -> Word8 -> Word8 - -> Word8 -> Word8 -> Word8 -> Word8 - -> IPv6 + Word8 -> + Word8 -> + Word8 -> + Word8 -> + Word8 -> + Word8 -> + Word8 -> + Word8 -> + Word8 -> + Word8 -> + Word8 -> + Word8 -> + Word8 -> + Word8 -> + Word8 -> + Word8 -> + IPv6 fromOctets a b c d e f g h i j k l m n o p = - IPv6 $ fromOctetsWord128 - (fromIntegral a) (fromIntegral b) (fromIntegral c) (fromIntegral d) - (fromIntegral e) (fromIntegral f) (fromIntegral g) (fromIntegral h) - (fromIntegral i) (fromIntegral j) (fromIntegral k) (fromIntegral l) - (fromIntegral m) (fromIntegral n) (fromIntegral o) (fromIntegral p) + IPv6 $ + fromOctetsWord128 + (fromIntegral a) + (fromIntegral b) + (fromIntegral c) + (fromIntegral d) + (fromIntegral e) + (fromIntegral f) + (fromIntegral g) + (fromIntegral h) + (fromIntegral i) + (fromIntegral j) + (fromIntegral k) + (fromIntegral l) + (fromIntegral m) + (fromIntegral n) + (fromIntegral o) + (fromIntegral p) fromOctetsWord128 :: - Word128 -> Word128 -> Word128 -> Word128 - -> Word128 -> Word128 -> Word128 -> Word128 - -> Word128 -> Word128 -> Word128 -> Word128 - -> Word128 -> Word128 -> Word128 -> Word128 - -> Word128 -fromOctetsWord128 a b c d e f g h i j k l m n o p = fromIntegral + Word128 -> + Word128 -> + Word128 -> + Word128 -> + Word128 -> + Word128 -> + Word128 -> + Word128 -> + Word128 -> + Word128 -> + Word128 -> + Word128 -> + Word128 -> + Word128 -> + Word128 -> + Word128 -> + Word128 +fromOctetsWord128 a b c d e f g h i j k l m n o p = + fromIntegral ( shiftL a 120 - .|. shiftL b 112 - .|. shiftL c 104 - .|. shiftL d 96 - .|. shiftL e 88 - .|. shiftL f 80 - .|. shiftL g 72 - .|. shiftL h 64 - .|. shiftL i 56 - .|. shiftL j 48 - .|. shiftL k 40 - .|. shiftL l 32 - .|. shiftL m 24 - .|. shiftL n 16 - .|. shiftL o 8 - .|. p + .|. shiftL b 112 + .|. shiftL c 104 + .|. shiftL d 96 + .|. shiftL e 88 + .|. shiftL f 80 + .|. shiftL g 72 + .|. shiftL h 64 + .|. shiftL i 56 + .|. shiftL j 48 + .|. shiftL k 40 + .|. shiftL l 32 + .|. shiftL m 24 + .|. shiftL n 16 + .|. shiftL o 8 + .|. p ) --- | Create an 'IPv6' address from the eight 16-bit fragments that make --- it up. This closely resembles the standard IPv6 notation, so --- is used for the 'Show' instance. Note that this lacks the formatting --- feature for suppress zeroes in an 'IPv6' address, but it should be --- readable enough for hacking in GHCi. --- --- >>> let addr = ipv6 0x3124 0x0 0x0 0xDEAD 0xCAFE 0xFF 0xFE00 0x1 --- >>> addr --- ipv6 0x3124 0x0000 0x0000 0xdead 0xcafe 0x00ff 0xfe00 0x0001 --- >>> T.putStrLn (encode addr) --- 3124::dead:cafe:ff:fe00:1 +{- | Create an 'IPv6' address from the eight 16-bit fragments that make + it up. This closely resembles the standard IPv6 notation, so + is used for the 'Show' instance. Note that this lacks the formatting + feature for suppress zeroes in an 'IPv6' address, but it should be + readable enough for hacking in GHCi. + + >>> let addr = ipv6 0x3124 0x0 0x0 0xDEAD 0xCAFE 0xFF 0xFE00 0x1 + >>> addr + ipv6 0x3124 0x0000 0x0000 0xdead 0xcafe 0x00ff 0xfe00 0x0001 + >>> T.putStrLn (encode addr) + 3124::dead:cafe:ff:fe00:1 +-} ipv6 :: - Word16 -> Word16 -> Word16 -> Word16 - -> Word16 -> Word16 -> Word16 -> Word16 - -> IPv6 + Word16 -> + Word16 -> + Word16 -> + Word16 -> + Word16 -> + Word16 -> + Word16 -> + Word16 -> + IPv6 ipv6 = fromWord16s -- | An alias for the 'ipv6' smart constructor. fromWord16s :: - Word16 -> Word16 -> Word16 -> Word16 - -> Word16 -> Word16 -> Word16 -> Word16 - -> IPv6 + Word16 -> + Word16 -> + Word16 -> + Word16 -> + Word16 -> + Word16 -> + Word16 -> + Word16 -> + IPv6 fromWord16s a b c d e f g h = - IPv6 $ fromWord16sWord128 - (fromIntegral a) (fromIntegral b) (fromIntegral c) (fromIntegral d) - (fromIntegral e) (fromIntegral f) (fromIntegral g) (fromIntegral h) + IPv6 $ + fromWord16sWord128 + (fromIntegral a) + (fromIntegral b) + (fromIntegral c) + (fromIntegral d) + (fromIntegral e) + (fromIntegral f) + (fromIntegral g) + (fromIntegral h) fromWord16sWord128 :: - Word128 -> Word128 -> Word128 -> Word128 - -> Word128 -> Word128 -> Word128 -> Word128 - -> Word128 -fromWord16sWord128 a b c d e f g h = fromIntegral + Word128 -> + Word128 -> + Word128 -> + Word128 -> + Word128 -> + Word128 -> + Word128 -> + Word128 -> + Word128 +fromWord16sWord128 a b c d e f g h = + fromIntegral ( shiftL a 112 - .|. shiftL b 96 - .|. shiftL c 80 - .|. shiftL d 64 - .|. shiftL e 48 - .|. shiftL f 32 - .|. shiftL g 16 - .|. h + .|. shiftL b 96 + .|. shiftL c 80 + .|. shiftL d 64 + .|. shiftL e 48 + .|. shiftL f 32 + .|. shiftL g 16 + .|. h ) -- | Convert an 'IPv6' to eight 16-bit words. -toWord16s :: IPv6 -> (Word16,Word16,Word16,Word16,Word16,Word16,Word16,Word16) +toWord16s :: IPv6 -> (Word16, Word16, Word16, Word16, Word16, Word16, Word16, Word16) toWord16s (IPv6 (Word128 a b)) = -- Note: implementing this as 2 Word64 shifts with 'unsafeShiftR' -- is up to 40% faster than using 128-bit shifts on a Word128 value. @@ -310,32 +389,41 @@ toWord16s (IPv6 (Word128 a b)) = ) -- | Uncurried variant of 'fromWord16s'. -fromTupleWord16s :: (Word16,Word16,Word16,Word16,Word16,Word16,Word16,Word16) -> IPv6 -fromTupleWord16s (a,b,c,d,e,f,g,h) = fromWord16s a b c d e f g h +fromTupleWord16s :: (Word16, Word16, Word16, Word16, Word16, Word16, Word16, Word16) -> IPv6 +fromTupleWord16s (a, b, c, d, e, f, g, h) = fromWord16s a b c d e f g h --- | Build an 'IPv6' from four 32-bit words. The leftmost argument --- is the high word and the rightword is the low word. +{- | Build an 'IPv6' from four 32-bit words. The leftmost argument + is the high word and the rightword is the low word. +-} fromWord32s :: Word32 -> Word32 -> Word32 -> Word32 -> IPv6 fromWord32s a b c d = - IPv6 $ fromWord32sWord128 - (fromIntegral a) (fromIntegral b) (fromIntegral c) (fromIntegral d) + IPv6 $ + fromWord32sWord128 + (fromIntegral a) + (fromIntegral b) + (fromIntegral c) + (fromIntegral d) fromWord32sWord128 :: - Word128 -> Word128 -> Word128 -> Word128 - -> Word128 -fromWord32sWord128 a b c d = fromIntegral + Word128 -> + Word128 -> + Word128 -> + Word128 -> + Word128 +fromWord32sWord128 a b c d = + fromIntegral ( shiftL a 96 - .|. shiftL b 64 - .|. shiftL c 32 - .|. d + .|. shiftL b 64 + .|. shiftL c 32 + .|. d ) -- | Uncurried variant of 'fromWord32s'. -fromTupleWord32s :: (Word32,Word32,Word32,Word32) -> IPv6 -fromTupleWord32s (a,b,c,d) = fromWord32s a b c d +fromTupleWord32s :: (Word32, Word32, Word32, Word32) -> IPv6 +fromTupleWord32s (a, b, c, d) = fromWord32s a b c d -- | Convert an 'IPv6' to four 32-bit words. -toWord32s :: IPv6 -> (Word32,Word32,Word32,Word32) +toWord32s :: IPv6 -> (Word32, Word32, Word32, Word32) toWord32s (IPv6 (Word128 a b)) = -- Note: implementing this as 2 Word64 shifts with 'unsafeShiftR' -- is about 10% faster than using 128-bit shifts on a Word128 value. @@ -345,80 +433,86 @@ toWord32s (IPv6 (Word128 a b)) = , fromIntegral b ) --- | The local loopback IP address. --- --- >>> IPv6.loopback --- ipv6 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0001 +{- | The local loopback IP address. + + >>> IPv6.loopback + ipv6 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0001 +-} loopback :: IPv6 loopback = IPv6 (Word128 0 1) --- | A useful alias for 'loopback'. --- --- >>> IPv6.localhost --- ipv6 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0001 +{- | A useful alias for 'loopback'. + + >>> IPv6.localhost + ipv6 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0001 +-} localhost :: IPv6 localhost = loopback --- | The IP address representing any host. --- --- >>> IPv6.any --- ipv6 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 +{- | The IP address representing any host. + + >>> IPv6.any + ipv6 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 +-} any :: IPv6 any = IPv6 zeroWord128 --- | Encodes the 'IPv6' address using zero-compression on the leftmost longest --- string of zeroes in the address. --- Per , --- this uses mixed notation when encoding an IPv4-mapped IPv6 address: --- --- >>> T.putStrLn $ IPv6.encode $ IPv6.fromWord16s 0xDEAD 0xBEEF 0x0 0x0 0x0 0x0 0x0 0x1234 --- dead:beef::1234 --- >>> T.putStrLn $ IPv6.encode $ IPv6.fromWord16s 0x0 0x0 0x0 0x0 0x0 0xFFFF 0x6437 0xA5B4 --- ::ffff:100.55.165.180 --- >>> T.putStrLn $ IPv6.encode $ IPv6.fromWord16s 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 --- :: --- --- Per of the --- same RFC, this does not use @::@ to shorten a single 16-bit 0 field. Only --- runs of multiple 0 fields are considered. +{- | Encodes the 'IPv6' address using zero-compression on the leftmost longest +string of zeroes in the address. +Per , +this uses mixed notation when encoding an IPv4-mapped IPv6 address: + +>>> T.putStrLn $ IPv6.encode $ IPv6.fromWord16s 0xDEAD 0xBEEF 0x0 0x0 0x0 0x0 0x0 0x1234 +dead:beef::1234 +>>> T.putStrLn $ IPv6.encode $ IPv6.fromWord16s 0x0 0x0 0x0 0x0 0x0 0xFFFF 0x6437 0xA5B4 +::ffff:100.55.165.180 +>>> T.putStrLn $ IPv6.encode $ IPv6.fromWord16s 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 +:: + +Per of the +same RFC, this does not use @::@ to shorten a single 16-bit 0 field. Only +runs of multiple 0 fields are considered. +-} encode :: IPv6 -> Text encode !ip = -- TODO: This implementation, while correct, is not particularly efficient. -- It uses string all over the place. if isIPv4Mapped ip - -- This representation is RECOMMENDED by https://tools.ietf.org/html/rfc5952#section-5 - then + then -- This representation is RECOMMENDED by https://tools.ietf.org/html/rfc5952#section-5 + Text.pack "::ffff:" - `mappend` - IPv4.encode (IPv4.IPv4 (fromIntegral w7 `unsafeShiftL` 16 .|. fromIntegral w8)) + `mappend` IPv4.encode (IPv4.IPv4 (fromIntegral w7 `unsafeShiftL` 16 .|. fromIntegral w8)) else toText [w1, w2, w3, w4, w5, w6, w7, w8] - where + where (w1, w2, w3, w4, w5, w6, w7, w8) = toWord16s ip - toText ws = Text.pack $ intercalate ":" - $ expand 0 (if longestZ > 1 then longestZ else 0) grouped - where + toText ws = + Text.pack $ + intercalate ":" $ + expand 0 (if longestZ > 1 then longestZ else 0) grouped + where expand !_ 8 !_ = ["::"] expand !_ !_ [] = [] - expand !i !longest ((x, len):wsNext) - -- zero-compressed group: - | x == 0 && len == longest = - -- first and last need an extra colon since there's nothing - -- to concat against - (if i == 0 || (i+len) == 8 then ":" else "") - : expand (i+len) 0 wsNext - -- normal group: - | otherwise = replicate len (showHex x "") ++ expand (i+len) longest wsNext - longestZ = maximum . (0:) . map snd . filter ((==0) . fst) $ grouped + expand !i !longest ((x, len) : wsNext) + -- zero-compressed group: + | x == 0 && len == longest = + -- first and last need an extra colon since there's nothing + -- to concat against + (if i == 0 || (i + len) == 8 then ":" else "") + : expand (i + len) 0 wsNext + -- normal group: + | otherwise = replicate len (showHex x "") ++ expand (i + len) longest wsNext + longestZ = maximum . (0 :) . map snd . filter ((== 0) . fst) $ grouped grouped = map (\x -> (head x, length x)) (group ws) isIPv4Mapped :: IPv6 -> Bool isIPv4Mapped (IPv6 (Word128 w1 w2)) = w1 == 0 && (0xFFFFFFFF00000000 .&. w2 == 0x0000FFFF00000000) --- | Decode UTF-8-encoded 'Bytes' into an 'IPv6' address. --- --- >>> decodeUtf8Bytes (Ascii.fromString "::cab:1") --- Just (ipv6 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0cab 0x0001) +{- | Decode UTF-8-encoded 'Bytes' into an 'IPv6' address. + + >>> decodeUtf8Bytes (Ascii.fromString "::cab:1") + Just (ipv6 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0cab 0x0001) +-} decodeUtf8Bytes :: Bytes.Bytes -> Maybe IPv6 decodeUtf8Bytes !b = case Parser.parseBytes (parserUtf8Bytes ()) b of Parser.Success (Parser.Slice _ len addr) -> case len of @@ -426,52 +520,42 @@ decodeUtf8Bytes !b = case Parser.parseBytes (parserUtf8Bytes ()) b of _ -> Nothing Parser.Failure _ -> Nothing --- | Encodes the 'IPv6' address using zero-compression on the --- leftmost longest string of zeroes in the address. --- --- >>> BB.run Nat.constant $ IPv6.boundedBuilderUtf8 $ IPv6.fromWord16s 0xDEAD 0xBEEF 0x0 0x0 0x0 0x0 0x0 0x1234 --- [0x64, 0x65, 0x61, 0x64, 0x3a, 0x62, 0x65, 0x65, 0x66, 0x3a, 0x3a, 0x31, 0x32, 0x33, 0x34] +{- | Encodes the 'IPv6' address using zero-compression on the +leftmost longest string of zeroes in the address. + +>>> BB.run Nat.constant $ IPv6.boundedBuilderUtf8 $ IPv6.fromWord16s 0xDEAD 0xBEEF 0x0 0x0 0x0 0x0 0x0 0x1234 +[0x64, 0x65, 0x61, 0x64, 0x3a, 0x62, 0x65, 0x65, 0x66, 0x3a, 0x3a, 0x31, 0x32, 0x33, 0x34] +-} boundedBuilderUtf8 :: IPv6 -> BB.Builder 39 boundedBuilderUtf8 !ip@(IPv6 (Word128 hi lo)) - | hi == 0 && lo == 0 = BB.weaken Lte.constant - (BB.ascii ':' `BB.append` BB.ascii ':') - | isIPv4Mapped ip = BB.weaken Lte.constant $ - BB.ascii ':' - `BB.append` - BB.ascii ':' - `BB.append` - BB.ascii 'f' - `BB.append` - BB.ascii 'f' - `BB.append` - BB.ascii 'f' - `BB.append` - BB.ascii 'f' - `BB.append` - BB.ascii ':' - `BB.append` - IPv4.boundedBuilderUtf8 (IPv4.IPv4 (fromIntegral lo)) + | hi == 0 && lo == 0 = + BB.weaken + Lte.constant + (BB.ascii ':' `BB.append` BB.ascii ':') + | isIPv4Mapped ip = + BB.weaken Lte.constant $ + BB.ascii ':' + `BB.append` BB.ascii ':' + `BB.append` BB.ascii 'f' + `BB.append` BB.ascii 'f' + `BB.append` BB.ascii 'f' + `BB.append` BB.ascii 'f' + `BB.append` BB.ascii ':' + `BB.append` IPv4.boundedBuilderUtf8 (IPv4.IPv4 (fromIntegral lo)) | otherwise = - let (w0,w1,w2,w3,w4,w5,w6,w7) = toWord16s ip + let (w0, w1, w2, w3, w4, w5, w6, w7) = toWord16s ip IntTriple startLongest longest _ = longestRun w0 w1 w2 w3 w4 w5 w6 w7 start = startLongest end = start + longest - -- start is inclusive. end is exclusive - in firstPiece w0 start - `BB.append` - piece 1 w1 start end - `BB.append` - piece 2 w2 start end - `BB.append` - piece 3 w3 start end - `BB.append` - piece 4 w4 start end - `BB.append` - piece 5 w5 start end - `BB.append` - piece 6 w6 start end - `BB.append` - lastPiece w7 end + in -- start is inclusive. end is exclusive + firstPiece w0 start + `BB.append` piece 1 w1 start end + `BB.append` piece 2 w2 start end + `BB.append` piece 3 w3 start end + `BB.append` piece 4 w4 start end + `BB.append` piece 5 w5 start end + `BB.append` piece 6 w6 start end + `BB.append` lastPiece w7 end firstPiece :: Word16 -> Int -> BB.Builder 4 firstPiece !w !start = case start of @@ -487,19 +571,20 @@ firstPiece !w !start = case start of -- comes with this library, it can be observed that preventing -- this inlining improves performance of encodeShort by 50%. piece :: Int -> Word16 -> Int -> Int -> BB.Builder 5 -{-# inline piece #-} +{-# INLINE piece #-} piece (I# ix) (Compat.W16# w) (I# start) (I# end) = piece# ix w start end piece# :: Int# -> Word# -> Int# -> Int# -> BB.Builder 5 -{-# noinline piece# #-} +{-# NOINLINE piece# #-} piece# !ix# !w# !start# !end# = case compare ix start of LT -> BB.ascii ':' `BB.append` BB.word16LowerHex w EQ -> BB.weaken Lte.constant (BB.ascii ':') - GT -> if ix < end - then BB.weaken Lte.constant BB.empty - else BB.ascii ':' `BB.append` BB.word16LowerHex w - where + GT -> + if ix < end + then BB.weaken Lte.constant BB.empty + else BB.ascii ':' `BB.append` BB.word16LowerHex w + where ix = I# ix# start = I# start# end = I# end# @@ -516,70 +601,76 @@ data IntTriple = IntTriple !Int !Int !Int -- event of a tie. stepZeroRunLength :: Int -> Word16 -> IntTriple -> IntTriple stepZeroRunLength !ix !w (IntTriple startLongest longest current) = case w of - 0 -> let !x = current + 1 in - if x > longest - then IntTriple (ix - current) x x - else IntTriple startLongest longest x + 0 -> + let !x = current + 1 + in if x > longest + then IntTriple (ix - current) x x + else IntTriple startLongest longest x _ -> IntTriple startLongest longest 0 -- We start out by setting the longest run to size 1. This -- means that we will only detect runs of length two or greater. longestRun :: - Word16 - -> Word16 - -> Word16 - -> Word16 - -> Word16 - -> Word16 - -> Word16 - -> Word16 - -> IntTriple -longestRun !w0 !w1 !w2 !w3 !w4 !w5 !w6 !w7 = id - $ stepZeroRunLength 7 w7 - $ stepZeroRunLength 6 w6 - $ stepZeroRunLength 5 w5 - $ stepZeroRunLength 4 w4 - $ stepZeroRunLength 3 w3 - $ stepZeroRunLength 2 w2 - $ stepZeroRunLength 1 w1 - $ stepZeroRunLength 0 w0 - $ IntTriple (-1) 1 0 - --- | Encodes the 'IPv6' address as 'ShortText' using zero-compression on --- the leftmost longest string of zeroes in the address. --- Per , --- this uses mixed notation when encoding an IPv4-mapped IPv6 address. --- --- >>> IPv6.encodeShort $ IPv6.fromWord16s 0xDEAD 0xBEEF 0x0 0x0 0x0 0x0ABC 0x0 0x1234 --- "dead:beef::abc:0:1234" + Word16 -> + Word16 -> + Word16 -> + Word16 -> + Word16 -> + Word16 -> + Word16 -> + Word16 -> + IntTriple +longestRun !w0 !w1 !w2 !w3 !w4 !w5 !w6 !w7 = + id $ + stepZeroRunLength 7 w7 $ + stepZeroRunLength 6 w6 $ + stepZeroRunLength 5 w5 $ + stepZeroRunLength 4 w4 $ + stepZeroRunLength 3 w3 $ + stepZeroRunLength 2 w2 $ + stepZeroRunLength 1 w1 $ + stepZeroRunLength 0 w0 $ + IntTriple (-1) 1 0 + +{- | Encodes the 'IPv6' address as 'ShortText' using zero-compression on +the leftmost longest string of zeroes in the address. +Per , +this uses mixed notation when encoding an IPv4-mapped IPv6 address. + +>>> IPv6.encodeShort $ IPv6.fromWord16s 0xDEAD 0xBEEF 0x0 0x0 0x0 0x0ABC 0x0 0x1234 +"dead:beef::abc:0:1234" +-} encodeShort :: IPv6 -> ShortText -encodeShort w = id - $ TS.fromShortByteStringUnsafe - $ byteArrayToShortByteString - $ BB.run Nat.constant - $ boundedBuilderUtf8 - $ w +encodeShort w = + id $ + TS.fromShortByteStringUnsafe $ + byteArrayToShortByteString $ + BB.run Nat.constant $ + boundedBuilderUtf8 $ + w byteArrayToShortByteString :: PM.ByteArray -> BSS.ShortByteString byteArrayToShortByteString (PM.ByteArray x) = BSS.SBS x --- | Decode an 'IPv6' address. This accepts both standard IPv6 --- notation (with zero compression) and mixed notation for --- IPv4-mapped IPv6 addresses. For a decoding function that --- additionally accepts dot-decimal-encoded IPv4 addresses, --- see @Net.IP.decode@. +{- | Decode an 'IPv6' address. This accepts both standard IPv6 +notation (with zero compression) and mixed notation for +IPv4-mapped IPv6 addresses. For a decoding function that +additionally accepts dot-decimal-encoded IPv4 addresses, +see @Net.IP.decode@. +-} decode :: Text -> Maybe IPv6 decode t = rightToMaybe (AT.parseOnly (parser <* AT.endOfInput) t) --- | Parse UTF-8-encoded 'Bytes' as an 'IPv6' address. This accepts --- both uppercase and lowercase characters in the hexadecimal components. --- --- >>> let str = "dead:beef:3240:a426:ba68:1cd0:4263:109b -> alive" --- >>> Parser.parseBytes (parserUtf8Bytes ()) (Ascii.fromString str) --- Success (Slice {offset = 39, length = 9, value = ipv6 0xdead 0xbeef 0x3240 0xa426 0xba68 0x1cd0 0x4263 0x109b}) --- --- This does not currently support parsing embedded IPv4 address --- (e.g. @ff00:8000:abc::224.1.2.3@). +{- | Parse UTF-8-encoded 'Bytes' as an 'IPv6' address. This accepts +both uppercase and lowercase characters in the hexadecimal components. + +>>> let str = "dead:beef:3240:a426:ba68:1cd0:4263:109b -> alive" +>>> Parser.parseBytes (parserUtf8Bytes ()) (Ascii.fromString str) +Success (Slice {offset = 39, length = 9, value = ipv6 0xdead 0xbeef 0x3240 0xa426 0xba68 0x1cd0 0x4263 0x109b}) + +This does not currently support parsing embedded IPv4 address +(e.g. @ff00:8000:abc::224.1.2.3@). +-} parserUtf8Bytes :: e -> Parser.Parser e s IPv6 parserUtf8Bytes e = do marr <- Parser.effect (PM.newPrimArray 8) @@ -600,10 +691,10 @@ parserUtf8Bytes e = do -- compression. Or we may encounter another hex-encoded -- number. preZeroes :: - e - -> MutablePrimArray s Word16 -- length must be 8 - -> Int - -> Parser.Parser e s IPv6 + e -> + MutablePrimArray s Word16 -> -- length must be 8 + Int -> + Parser.Parser e s IPv6 preZeroes e !marr !ix = case ix of 8 -> Parser.effect (combinePieces marr) _ -> do @@ -622,31 +713,33 @@ preZeroes e !marr !ix = case ix of -- is only called by preZeroes, which ensures that -- this holds. postZeroesBegin :: - e - -> MutablePrimArray s Word16 -- length must be 8 - -> Int -- current index in array - -> Int -- index where compression happened - -> Parser.Parser e s IPv6 + e -> + MutablePrimArray s Word16 -> -- length must be 8 + Int -> -- current index in array + Int -> -- index where compression happened + Parser.Parser e s IPv6 postZeroesBegin e !marr !ix !compress = do optionalPieceParser e >>= \case - Nothing -> do -- the end has come + Nothing -> do + -- the end has come Parser.effect (conclude marr ix compress) Just w -> do Parser.effect (PM.writePrimArray marr ix w) postZeroes e marr (ix + 1) compress -- Should be run right before a colon. -postZeroes :: - e - -> MutablePrimArray s Word16 -- length must be 8 - -> Int -- current index in array - -> Int -- index where compression happened - -> Parser.Parser e s IPv6 +postZeroes :: + e -> + MutablePrimArray s Word16 -> -- length must be 8 + Int -> -- current index in array + Int -> -- index where compression happened + Parser.Parser e s IPv6 postZeroes e !marr !ix !compress = case ix of 8 -> Parser.fail e _ -> do Latin.trySatisfy (== ':') >>= \case - False -> -- The end has come + False -> + -- The end has come Parser.effect (conclude marr ix compress) True -> do w <- pieceParser e @@ -670,24 +763,26 @@ conclude !marr !ix !compress = do -- setPrimArray marr 3 4 (0 :: Word16) combinePieces :: - MutablePrimArray s Word16 - -> ST s IPv6 -combinePieces !marr = fromWord16s - <$> PM.readPrimArray marr 0 - <*> PM.readPrimArray marr 1 - <*> PM.readPrimArray marr 2 - <*> PM.readPrimArray marr 3 - <*> PM.readPrimArray marr 4 - <*> PM.readPrimArray marr 5 - <*> PM.readPrimArray marr 6 - <*> PM.readPrimArray marr 7 + MutablePrimArray s Word16 -> + ST s IPv6 +combinePieces !marr = + fromWord16s + <$> PM.readPrimArray marr 0 + <*> PM.readPrimArray marr 1 + <*> PM.readPrimArray marr 2 + <*> PM.readPrimArray marr 3 + <*> PM.readPrimArray marr 4 + <*> PM.readPrimArray marr 5 + <*> PM.readPrimArray marr 6 + <*> PM.readPrimArray marr 7 optionalPieceParser :: e -> Parser.Parser e s (Maybe Word16) -optionalPieceParser e = Latin.tryHexNibble >>= \case - Nothing -> pure Nothing - Just w0 -> do - r <- pieceParserStep e w0 - pure (Just r) +optionalPieceParser e = + Latin.tryHexNibble >>= \case + Nothing -> pure Nothing + Just w0 -> do + r <- pieceParserStep e w0 + pure (Just r) -- This should probably be moved into bytesmith and renamed. pieceParser :: e -> Parser.Parser e s Word16 @@ -700,25 +795,28 @@ pieceParser e = Latin.hexNibble e >>= pieceParserStep e -- if someone puts 00000 in a piece of an encoded IPv6 -- address, so I veer on the side of leniency. pieceParserStep :: - e - -> Word - -> Parser.Parser e s Word16 -pieceParserStep e !acc = if acc > 0xFFFF - then Parser.fail e - else Latin.tryHexNibble >>= \case - Nothing -> pure (fromIntegral acc) - Just w -> pieceParserStep e (16 * acc + w) - --- | Parse UTF-8-encoded 'Bytes' into an 'IPv4Range'. --- This requires the mask to be present. --- --- >>> maybe (putStrLn "nope") IPv6.printRange $ Parser.parseBytesMaybe (IPv6.parserRangeUtf8Bytes ()) (Ascii.fromString "1b02:f001:5:200b::/80") --- 1b02:f001:5:200b::/80 --- >>> maybe (putStrLn "nope") IPv6.printRange $ Parser.parseBytesMaybe (IPv6.parserRangeUtf8Bytes ()) (Ascii.fromString "abcd::") --- nope --- --- See 'parserRangeUtf8BytesLenient' for a variant that treats --- a missing mask as a @/32@ mask. + e -> + Word -> + Parser.Parser e s Word16 +pieceParserStep e !acc = + if acc > 0xFFFF + then Parser.fail e + else + Latin.tryHexNibble >>= \case + Nothing -> pure (fromIntegral acc) + Just w -> pieceParserStep e (16 * acc + w) + +{- | Parse UTF-8-encoded 'Bytes' into an 'IPv4Range'. +This requires the mask to be present. + +>>> maybe (putStrLn "nope") IPv6.printRange $ Parser.parseBytesMaybe (IPv6.parserRangeUtf8Bytes ()) (Ascii.fromString "1b02:f001:5:200b::/80") +1b02:f001:5:200b::/80 +>>> maybe (putStrLn "nope") IPv6.printRange $ Parser.parseBytesMaybe (IPv6.parserRangeUtf8Bytes ()) (Ascii.fromString "abcd::") +nope + +See 'parserRangeUtf8BytesLenient' for a variant that treats +a missing mask as a @/32@ mask. +-} parserRangeUtf8Bytes :: e -> Parser.Parser e s IPv6Range parserRangeUtf8Bytes e = do base <- parserUtf8Bytes e @@ -728,17 +826,18 @@ parserRangeUtf8Bytes e = do then Parser.fail e else pure $! normalize (IPv6Range base theMask) --- | Variant of 'parserRangeUtf8Bytes' that allows the mask --- to be omitted. An omitted mask is treated as a @/128@ mask. --- --- >>> maybe (putStrLn "nope") IPv6.printRange $ Parser.parseBytesMaybe (IPv6.parserRangeUtf8BytesLenient ()) (Ascii.fromString "1b02:f001:5:200b::/80") --- 1b02:f001:5:200b::/80 --- >>> maybe (putStrLn "nope") IPv6.printRange $ Parser.parseBytesMaybe (IPv6.parserRangeUtf8BytesLenient ()) (Ascii.fromString "abcd::") --- abcd::/128 +{- | Variant of 'parserRangeUtf8Bytes' that allows the mask +to be omitted. An omitted mask is treated as a @/128@ mask. + +>>> maybe (putStrLn "nope") IPv6.printRange $ Parser.parseBytesMaybe (IPv6.parserRangeUtf8BytesLenient ()) (Ascii.fromString "1b02:f001:5:200b::/80") +1b02:f001:5:200b::/80 +>>> maybe (putStrLn "nope") IPv6.printRange $ Parser.parseBytesMaybe (IPv6.parserRangeUtf8BytesLenient ()) (Ascii.fromString "abcd::") +abcd::/128 +-} parserRangeUtf8BytesLenient :: e -> Parser.Parser e s IPv6Range parserRangeUtf8BytesLenient e = do base <- parserUtf8Bytes e - Latin.trySatisfy (=='/') >>= \case + Latin.trySatisfy (== '/') >>= \case True -> do theMask <- Latin.decWord8 e if theMask > 128 @@ -746,13 +845,14 @@ parserRangeUtf8BytesLenient e = do else pure $! normalize (IPv6Range base theMask) False -> pure $! IPv6Range base 128 --- | Parse an 'IPv6' using 'Atto.Parser'. --- --- >>> Atto.parseOnly IPv6.parser (Text.pack "dead:beef:3240:a426:ba68:1cd0:4263:109b") --- Right (ipv6 0xdead 0xbeef 0x3240 0xa426 0xba68 0x1cd0 0x4263 0x109b) +{- | Parse an 'IPv6' using 'Atto.Parser'. + + >>> Atto.parseOnly IPv6.parser (Text.pack "dead:beef:3240:a426:ba68:1cd0:4263:109b") + Right (ipv6 0xdead 0xbeef 0x3240 0xa426 0xba68 0x1cd0 0x4263 0x109b) +-} parser :: Atto.Parser IPv6 parser = makeIP <$> ip - where + where makeIP [w1, w2, w3, w4, w5, w6, w7, w8] = fromWord16s w1 w2 w3 w4 w5 w6 w7 w8 makeIP _ = error "Net.IPv6.parser: Implementation error. Please open a bug report." @@ -766,16 +866,15 @@ parser = makeIP <$> ip -- after 6 parts it could end in IPv4 dotted notation 6 -> ipv4 <|> hexPart _ -> hexPart - where - hexPart = (:) - <$> Atto.hexadecimal - <*> (Atto.char ':' *> - ( - (Atto.char ':' *> doubleColon (n+1)) - <|> - part (n+1) + where + hexPart = + (:) + <$> Atto.hexadecimal + <*> ( Atto.char ':' + *> ( (Atto.char ':' *> doubleColon (n + 1)) + <|> part (n + 1) + ) ) - ) doubleColon :: Int -> Atto.Parser [Word16] doubleColon count = do @@ -788,8 +887,8 @@ parser = makeIP <$> ip -- after double colon, IPv4 dotted notation could appear anywhere afterDoubleColon :: Atto.Parser [Word16] afterDoubleColon = - ipv4 <|> - (:) <$> Atto.hexadecimal <*> ((Atto.char ':' *> afterDoubleColon) <|> pure []) + ipv4 + <|> (:) <$> Atto.hexadecimal <*> ((Atto.char ':' *> afterDoubleColon) <|> pure []) ipv4 :: Atto.Parser [Word16] ipv4 = ipv4ToWord16s <$> IPv4.parser @@ -797,12 +896,14 @@ parser = makeIP <$> ip ipv4ToWord16s :: IPv4 -> [Word16] ipv4ToWord16s (IPv4 word) = [fromIntegral (word `unsafeShiftR` 16), fromIntegral (word .&. 0xFFFF)] --- | An 'IPv6Range'. It is made up of the first 'IPv6' in the range --- and its length. +{- | An 'IPv6Range'. It is made up of the first 'IPv6' in the range + and its length. +-} data IPv6Range = IPv6Range - { ipv6RangeBase :: {-# UNPACK #-} !IPv6 + { ipv6RangeBase :: {-# UNPACK #-} !IPv6 , ipv6RangeLength :: {-# UNPACK #-} !Word8 - } deriving (Eq,Ord,Show,Read,Generic,Data) + } + deriving (Eq, Ord, Show, Read, Generic, Data) instance NFData IPv6Range @@ -821,44 +922,47 @@ mask128 = maxBound mask :: Word8 -> IPv6 mask = complement . shiftR mask128 . fromIntegral --- | Normalize an 'IPv6Range'. The first result of this is that the --- 'IPv6' inside the 'IPv6Range' is changed so that the insignificant --- bits are zeroed out. For example: --- --- >>> addr1 = IPv6.ipv6 0x0192 0x0168 0x0001 0x0019 0x0000 0x0000 0x0000 0x0000 --- >>> addr2 = IPv6.ipv6 0x0192 0x0168 0x0001 0x0163 0x0000 0x0000 0x0000 0x0000 --- >>> IPv6.printRange $ IPv6.normalize $ IPv6.IPv6Range addr1 24 --- 192:100::/24 --- >>> IPv6.printRange $ IPv6.normalize $ IPv6.IPv6Range addr2 28 --- 192:160::/28 --- --- The second effect of this is that the mask length is lowered to be 128 --- or smaller. Working with 'IPv6Range's that have not been normalized does --- not cause any issues for this library, although other applications may --- reject such ranges (especially those with a mask length above 128). --- --- Note that 'normalize is idempotent, that is: --- --- prop> IPv6.normalize r == (IPv6.normalize . IPv6.normalize) r +{- | Normalize an 'IPv6Range'. The first result of this is that the + 'IPv6' inside the 'IPv6Range' is changed so that the insignificant + bits are zeroed out. For example: + + >>> addr1 = IPv6.ipv6 0x0192 0x0168 0x0001 0x0019 0x0000 0x0000 0x0000 0x0000 + >>> addr2 = IPv6.ipv6 0x0192 0x0168 0x0001 0x0163 0x0000 0x0000 0x0000 0x0000 + >>> IPv6.printRange $ IPv6.normalize $ IPv6.IPv6Range addr1 24 + 192:100::/24 + >>> IPv6.printRange $ IPv6.normalize $ IPv6.IPv6Range addr2 28 + 192:160::/28 + + The second effect of this is that the mask length is lowered to be 128 + or smaller. Working with 'IPv6Range's that have not been normalized does + not cause any issues for this library, although other applications may + reject such ranges (especially those with a mask length above 128). + + Note that 'normalize is idempotent, that is: + + prop> IPv6.normalize r == (IPv6.normalize . IPv6.normalize) r +-} normalize :: IPv6Range -> IPv6Range normalize (IPv6Range ip len) = let len' = min len 128 ip' = ip .&. mask len' - in IPv6Range ip' len' + in IPv6Range ip' len' --- | Encode an 'IPv6Range' as 'Text'. --- --- >>> addr = IPv6.ipv6 0xDEAD 0xBEEF 0x3240 0xA426 0xBA68 0x1CD0 0x4263 0x109B --- >>> T.putStrLn $ IPv6.encodeRange $ IPv6.IPv6Range addr 28 --- dead:beef:3240:a426:ba68:1cd0:4263:109b/28 +{- | Encode an 'IPv6Range' as 'Text'. + + >>> addr = IPv6.ipv6 0xDEAD 0xBEEF 0x3240 0xA426 0xBA68 0x1CD0 0x4263 0x109B + >>> T.putStrLn $ IPv6.encodeRange $ IPv6.IPv6Range addr 28 + dead:beef:3240:a426:ba68:1cd0:4263:109b/28 +-} encodeRange :: IPv6Range -> Text encodeRange x = encode (ipv6RangeBase x) <> Text.pack "/" <> (Text.pack $ (show . fromEnum) $ ipv6RangeLength x) --- | Decode an 'IPv6Range' from 'Text'. --- --- >>> addr = IPv6.ipv6 0xDEAD 0xBEEF 0x3240 0xA426 0xBA68 0x1CD0 0x4263 0x109B --- >>> fmap IPv6.encodeRange $ IPv6.decodeRange (Text.pack "dead:beef:3240:a426:ba68:1cd0:4263:109b/28") --- Just "dead:bee0::/28" +{- | Decode an 'IPv6Range' from 'Text'. + + >>> addr = IPv6.ipv6 0xDEAD 0xBEEF 0x3240 0xA426 0xBA68 0x1CD0 0x4263 0x109B + >>> fmap IPv6.encodeRange $ IPv6.decodeRange (Text.pack "dead:beef:3240:a426:ba68:1cd0:4263:109b/28") + Just "dead:bee0::/28" +-} decodeRange :: Text -> Maybe IPv6Range decodeRange = rightToMaybe . AT.parseOnly (parserRange <* AT.endOfInput) @@ -869,33 +973,34 @@ parserRange = do _ <- AT.char '/' theMask <- AT.decimal >>= limitSize return (normalize (IPv6Range ip theMask)) - where + where limitSize i = if i > 128 then fail "An IP range length must be between 0 and 128" else return i --- | Checks to see if an 'IPv6' address belongs in the 'IPv6Range'. --- --- >>> let ip = IPv6.ipv6 0x2001 0x0db8 0x0db8 0x1094 0x2051 0x0000 0x0000 0x0001 --- >>> let iprange mask = IPv6.IPv6Range (IPv6.ipv6 0x2001 0x0db8 0x0000 0x0000 0x0000 0x0000 0x0000 0x0001) mask --- >>> IPv6.contains (iprange 8) ip --- True --- >>> IPv6.contains (iprange 48) ip --- False --- --- Typically, element-testing functions are written to take the element --- as the first argument and the set as the second argument. This is intentionally --- written the other way for better performance when iterating over a collection. --- For example, you might test elements in a list for membership like this: --- --- >>> let r = IPv6.IPv6Range (IPv6.ipv6 0x2001 0x0db8 0x0000 0x0000 0x0000 0x0000 0x0000 0x0001) 64 --- >>> fmap (IPv6.contains r) (take 5 $ iterate succ $ IPv6.ipv6 0x2001 0x0db8 0x0000 0x0000 0xffff 0xffff 0xffff 0xfffe) --- [True,True,False,False,False] --- --- The implementation of 'contains' ensures that (with GHC), the bitmask --- creation and range normalization only occur once in the above example. --- They are reused as the list is iterated. +{- | Checks to see if an 'IPv6' address belongs in the 'IPv6Range'. + +>>> let ip = IPv6.ipv6 0x2001 0x0db8 0x0db8 0x1094 0x2051 0x0000 0x0000 0x0001 +>>> let iprange mask = IPv6.IPv6Range (IPv6.ipv6 0x2001 0x0db8 0x0000 0x0000 0x0000 0x0000 0x0000 0x0001) mask +>>> IPv6.contains (iprange 8) ip +True +>>> IPv6.contains (iprange 48) ip +False + +Typically, element-testing functions are written to take the element +as the first argument and the set as the second argument. This is intentionally +written the other way for better performance when iterating over a collection. +For example, you might test elements in a list for membership like this: + +>>> let r = IPv6.IPv6Range (IPv6.ipv6 0x2001 0x0db8 0x0000 0x0000 0x0000 0x0000 0x0000 0x0001) 64 +>>> fmap (IPv6.contains r) (take 5 $ iterate succ $ IPv6.ipv6 0x2001 0x0db8 0x0000 0x0000 0xffff 0xffff 0xffff 0xfffe) +[True,True,False,False,False] + +The implementation of 'contains' ensures that (with GHC), the bitmask +creation and range normalization only occur once in the above example. +They are reused as the list is iterated. +-} contains :: IPv6Range -> IPv6 -> Bool contains (IPv6Range subnet len) = let theMask = mask len @@ -906,65 +1011,68 @@ contains (IPv6Range subnet len) = isSubsetOf :: IPv6Range -> IPv6Range -> Bool isSubsetOf a b = lowerInclusive a >= lowerInclusive b - && - upperInclusive a <= upperInclusive b + && upperInclusive a <= upperInclusive b + +{- | This is provided to mirror the interface provided by @Data.Set@. It +behaves just like 'contains' but with flipped arguments. --- | This is provided to mirror the interface provided by @Data.Set@. It --- behaves just like 'contains' but with flipped arguments. --- --- prop> IPv6.member ip r == IPv6.contains r ip +prop> IPv6.member ip r == IPv6.contains r ip +-} member :: IPv6 -> IPv6Range -> Bool member = flip contains --- | The inclusive lower bound of an 'IPv6Range'. This is conventionally --- understood to be the broadcast address of a subnet. For example: --- --- >>> T.putStrLn $ IPv6.encode $ IPv6.lowerInclusive $ IPv6.IPv6Range (IPv6.ipv6 0x2001 0x0db8 0x0000 0x0000 0x0000 0x0000 0x0000 0x0001) 25 --- 2001:d80:: --- --- Note that the lower bound of a normalized 'IPv6Range' is simply the --- ip address of the range: --- --- prop> IPv6.lowerInclusive r == IPv6.ipv6RangeBase (IPv6.normalize r) +{- | The inclusive lower bound of an 'IPv6Range'. This is conventionally + understood to be the broadcast address of a subnet. For example: + +>>> T.putStrLn $ IPv6.encode $ IPv6.lowerInclusive $ IPv6.IPv6Range (IPv6.ipv6 0x2001 0x0db8 0x0000 0x0000 0x0000 0x0000 0x0000 0x0001) 25 +2001:d80:: + +Note that the lower bound of a normalized 'IPv6Range' is simply the +ip address of the range: + +prop> IPv6.lowerInclusive r == IPv6.ipv6RangeBase (IPv6.normalize r) +-} lowerInclusive :: IPv6Range -> IPv6 lowerInclusive = ipv6RangeBase . normalize --- | The inclusive upper bound of an 'IPv6Range'. --- --- >>> let addr = IPv6.ipv6 0xDEAD 0xBEEF 0x3240 0xA426 0xBA68 0x1CD0 0x4263 0x109B --- >>> T.putStrLn $ IPv6.encode $ IPv6.upperInclusive $ IPv6.IPv6Range addr 25 --- dead:beff:ffff:ffff:ffff:ffff:ffff:ffff --- +{- | The inclusive upper bound of an 'IPv6Range'. + + >>> let addr = IPv6.ipv6 0xDEAD 0xBEEF 0x3240 0xA426 0xBA68 0x1CD0 0x4263 0x109B + >>> T.putStrLn $ IPv6.encode $ IPv6.upperInclusive $ IPv6.IPv6Range addr 25 + dead:beff:ffff:ffff:ffff:ffff:ffff:ffff +-} upperInclusive :: IPv6Range -> IPv6 upperInclusive (IPv6Range ip len) = let len' = min 128 len theInvertedMask :: IPv6 theInvertedMask = shiftR mask128 (fromIntegral len') - in ip .|. theInvertedMask + in ip .|. theInvertedMask -- | Print an 'IPv6Range' using the textual encoding. printRange :: IPv6Range -> IO () printRange = TIO.putStrLn . encodeRange --- | Smart constructor for 'IPv6Range'. Ensures the mask is appropriately --- sized and sets masked bits in the 'IPv6' to zero. --- --- >>> let addr = IPv6.ipv6 0xDEAD 0xBEEF 0x3240 0xA426 0xBA68 0x1CD0 0x4263 0x109B --- >>> IPv6.printRange $ IPv6.range addr 25 --- dead:be80::/25 +{- | Smart constructor for 'IPv6Range'. Ensures the mask is appropriately + sized and sets masked bits in the 'IPv6' to zero. + + >>> let addr = IPv6.ipv6 0xDEAD 0xBEEF 0x3240 0xA426 0xBA68 0x1CD0 0x4263 0x109B + >>> IPv6.printRange $ IPv6.range addr 25 + dead:be80::/25 +-} range :: IPv6 -> Word8 -> IPv6Range range addr len = normalize (IPv6Range addr len) --- | Given an inclusive lower and upper ip address, create the smallest 'IPv6Range' --- that contains the two. This is helpful in situations where input is given as a --- range, like @ @. --- --- This makes the range broader if it cannot be represented in notation. --- --- >>> addrLower = IPv6.ipv6 0xDEAD 0xBE80 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 --- >>> addrUpper = IPv6.ipv6 0xDEAD 0xBEFF 0xFFFF 0xFFFF 0xFFFF 0xFFFF 0xFFFF 0xFFFF --- >>> IPv6.printRange $ IPv6.fromBounds addrLower addrUpper --- dead:be80::/25 +{- | Given an inclusive lower and upper ip address, create the smallest 'IPv6Range' + that contains the two. This is helpful in situations where input is given as a + range, like @ @. + + This makes the range broader if it cannot be represented in notation. + + >>> addrLower = IPv6.ipv6 0xDEAD 0xBE80 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 + >>> addrUpper = IPv6.ipv6 0xDEAD 0xBEFF 0xFFFF 0xFFFF 0xFFFF 0xFFFF 0xFFFF 0xFFFF + >>> IPv6.printRange $ IPv6.fromBounds addrLower addrUpper + dead:be80::/25 +-} fromBounds :: IPv6 -> IPv6 -> IPv6Range fromBounds lo hi = normalize (IPv6Range lo (maskFromBounds lo hi)) diff --git a/src/Net/Mac.hs b/src/Net/Mac.hs index 400a54e..e4d6e5c 100644 --- a/src/Net/Mac.hs +++ b/src/Net/Mac.hs @@ -13,7 +13,7 @@ {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE UnboxedTuples #-} -{-| This module provides the Mac data type and functions for working +{- | This module provides the Mac data type and functions for working with it. -} module Net.Mac @@ -21,7 +21,9 @@ module Net.Mac mac , fromOctets , toOctets + -- * Textual Conversion + -- ** Text , encode , encodeWith @@ -30,8 +32,10 @@ module Net.Mac , builder , parser , parserWith + -- ** ShortText , encodeShort + -- ** UTF-8 ByteString , encodeUtf8 , encodeWithUtf8 @@ -40,38 +44,41 @@ module Net.Mac , builderUtf8 , parserUtf8 , parserWithUtf8 + -- ** ByteString , decodeBytes , decodeOctets + -- ** UTF-8 Bytes , boundedBuilderUtf8 , decodeUtf8Bytes , parserUtf8Bytes + -- ** Printing , print + -- * Default Codec , defCodec + -- * Types - , Mac(..) - , MacCodec(..) - , MacGrouping(..) + , Mac (..) + , MacCodec (..) + , MacGrouping (..) ) where import Prelude hiding (print) import Control.DeepSeq (NFData) -import Data.Aeson (FromJSON(..),ToJSON(..)) -import Data.Aeson (ToJSONKey(..),FromJSONKey(..)) -import Data.Aeson (ToJSONKeyFunction(..),FromJSONKeyFunction(..)) -import Data.Bits ((.|.),unsafeShiftL,unsafeShiftR,(.&.)) +import Data.Aeson (FromJSON (..), FromJSONKey (..), FromJSONKeyFunction (..), ToJSON (..), ToJSONKey (..), ToJSONKeyFunction (..)) +import Data.Bits (unsafeShiftL, unsafeShiftR, (.&.), (.|.)) import Data.ByteString (ByteString) -import Data.ByteString.Short.Internal (ShortByteString(SBS)) -import Data.Char (ord,chr) +import Data.ByteString.Short.Internal (ShortByteString (SBS)) +import Data.Char (chr, ord) import Data.Data (Data) import Data.Hashable (Hashable) import Data.Ix (Ix) -import Data.Primitive.ByteArray (ByteArray(ByteArray)) -import Data.Primitive.Types (Prim(..)) +import Data.Primitive.ByteArray (ByteArray (ByteArray)) +import Data.Primitive.Types (Prim (..)) import Data.Text (Text) import Data.Text.Short (ShortText) import Data.Word @@ -79,8 +86,8 @@ import Data.Word.Synthetic.Word12 (Word12) import GHC.Enum (predError, succError) import GHC.Exts import GHC.Generics (Generic) -import Text.ParserCombinators.ReadPrec (prec,step) -import Text.Read (Read(..),Lexeme(Ident),lexP,parens) +import Text.ParserCombinators.ReadPrec (prec, step) +import Text.Read (Lexeme (Ident), Read (..), lexP, parens) import qualified Arithmetic.Nat as Nat import qualified Data.Aeson as Aeson @@ -88,19 +95,19 @@ import qualified Data.Aeson.Types as Aeson import qualified Data.Attoparsec.ByteString as AB import qualified Data.Attoparsec.ByteString as ABW import qualified Data.Attoparsec.Text as AT -import qualified Data.Bytes.Builder.Bounded as BBB -import qualified Data.Bytes as Bytes -import qualified Data.Bytes.Parser as Parser -import qualified Data.Bytes.Parser.Latin as Latin import qualified Data.ByteString as B import qualified Data.ByteString.Builder as BB import qualified Data.ByteString.Builder.Fixed as BFB import qualified Data.ByteString.Unsafe as BU +import qualified Data.Bytes as Bytes +import qualified Data.Bytes.Builder.Bounded as BBB +import qualified Data.Bytes.Parser as Parser +import qualified Data.Bytes.Parser.Latin as Latin +import qualified Data.Text as Text () import qualified Data.Text.Builder.Fixed as TFB import qualified Data.Text.IO as TIO import qualified Data.Text.Lazy.Builder as TBuilder import qualified Data.Text.Short.Unsafe as TS -import qualified Data.Text as Text () import qualified GHC.Prim.Compat as Compat import qualified GHC.Word.Compat as Compat @@ -108,51 +115,60 @@ import qualified GHC.Word.Compat as Compat import qualified Data.Aeson.Key as AK #endif --- | A 48-bit MAC address. Do not use the data constructor for this --- type. It is not considered part of the stable API, and it --- allows you to construct invalid MAC addresses. +{- | A 48-bit MAC address. Do not use the data constructor for this + type. It is not considered part of the stable API, and it + allows you to construct invalid MAC addresses. +-} newtype Mac = Mac Word64 - deriving stock (Eq,Ord,Generic,Ix,Data) + deriving stock (Eq, Ord, Generic, Ix, Data) deriving newtype (Hashable) instance NFData Mac +{- $setup + +These are here to get doctest's property checking to work + +>>> :set -XOverloadedStrings +>>> import Test.QuickCheck (Arbitrary(..),CoArbitrary(..),coarbitraryEnum) +>>> import qualified Data.Text as Text (pack) +>>> import qualified Data.Text.IO as T +>>> import qualified Data.ByteString.Char8 as BC +>>> import qualified Data.ByteString as B +>>> import qualified Data.Bytes.Text.Ascii as Ascii +>>> import qualified Net.Mac as Mac +>>> import qualified Arithmetic.Nat as Nat +>>> import qualified Data.Attoparsec.Text as AT +>>> import qualified Data.Bytes.Builder.Bounded as BBB +>>> import Net.Mac (Mac(Mac)) +>>> instance Arbitrary Mac where { arbitrary = fmap (Mac . (0xFFFFFFFFFFFF .&.)) arbitrary } +>>> instance CoArbitrary Mac where { coarbitrary = coarbitraryEnum } +-} --- $setup --- --- These are here to get doctest's property checking to work --- --- >>> :set -XOverloadedStrings --- >>> import Test.QuickCheck (Arbitrary(..),CoArbitrary(..),coarbitraryEnum) --- >>> import qualified Data.Text as Text (pack) --- >>> import qualified Data.Text.IO as T --- >>> import qualified Data.ByteString.Char8 as BC --- >>> import qualified Data.ByteString as B --- >>> import qualified Data.Bytes.Text.Ascii as Ascii --- >>> import qualified Net.Mac as Mac --- >>> import qualified Arithmetic.Nat as Nat --- >>> import qualified Data.Attoparsec.Text as AT --- >>> import qualified Data.Bytes.Builder.Bounded as BBB --- >>> import Net.Mac (Mac(Mac)) --- >>> instance Arbitrary Mac where { arbitrary = fmap (Mac . (0xFFFFFFFFFFFF .&.)) arbitrary } --- >>> instance CoArbitrary Mac where { coarbitrary = coarbitraryEnum } - --- | Construct a 'Mac' address from a 'Word64'. Only the lower --- 48 bits are used. +{- | Construct a 'Mac' address from a 'Word64'. Only the lower + 48 bits are used. +-} mac :: Word64 -> Mac mac w = Mac (w .&. 0xFFFFFFFFFFFF) -- | Create a 'Mac' address from six octets. fromOctets :: Word8 -> Word8 -> Word8 -> Word8 -> Word8 -> Word8 -> Mac -fromOctets a b c d e f = Mac $ unsafeWord48FromOctets - (fromIntegral a) (fromIntegral b) (fromIntegral c) - (fromIntegral d) (fromIntegral e) (fromIntegral f) - --- | Convert a 'Mac' address to the six octets that make it up. --- This function and 'fromOctets' are inverses: --- --- prop> m == (let (a,b,c,d,e,f) = Mac.toOctets m in Mac.fromOctets a b c d e f) -toOctets :: Mac -> (Word8,Word8,Word8,Word8,Word8,Word8) +fromOctets a b c d e f = + Mac $ + unsafeWord48FromOctets + (fromIntegral a) + (fromIntegral b) + (fromIntegral c) + (fromIntegral d) + (fromIntegral e) + (fromIntegral f) + +{- | Convert a 'Mac' address to the six octets that make it up. + This function and 'fromOctets' are inverses: + + prop> m == (let (a,b,c,d,e,f) = Mac.toOctets m in Mac.fromOctets a b c d e f) +-} +toOctets :: Mac -> (Word8, Word8, Word8, Word8, Word8, Word8) toOctets (Mac w) = ( fromIntegral $ unsafeShiftR w 40 , fromIntegral $ unsafeShiftR w 32 @@ -162,35 +178,40 @@ toOctets (Mac w) = , fromIntegral w ) --- | This function is deprecated. It will be renamed in a future release --- since the name is misleading. +{- | This function is deprecated. It will be renamed in a future release + since the name is misleading. +-} decodeBytes :: ByteString -> Maybe Mac {-# DEPRECATED decodeBytes "Prefer decodeOctets" #-} decodeBytes = decodeOctets --- | Decode a 'Mac' address from a 'ByteString'. Each byte is interpreted --- as an octet of the 'Mac' address. Consequently, 'ByteString's --- of length 6 successfully decode, and all other 'ByteString's fail --- to decode. --- --- >>> Mac.decodeOctets (B.pack [0x6B,0x47,0x18,0x90,0x55,0xC3]) --- Just (mac 0x6b47189055c3) --- >>> Mac.decodeOctets (B.replicate 6 0x3A) --- Just (mac 0x3a3a3a3a3a3a) --- >>> Mac.decodeOctets (B.replicate 7 0x3A) --- Nothing --- --- Note that the octets are interpreted in a big-endian fashion. +{- | Decode a 'Mac' address from a 'ByteString'. Each byte is interpreted + as an octet of the 'Mac' address. Consequently, 'ByteString's + of length 6 successfully decode, and all other 'ByteString's fail + to decode. + + >>> Mac.decodeOctets (B.pack [0x6B,0x47,0x18,0x90,0x55,0xC3]) + Just (mac 0x6b47189055c3) + >>> Mac.decodeOctets (B.replicate 6 0x3A) + Just (mac 0x3a3a3a3a3a3a) + >>> Mac.decodeOctets (B.replicate 7 0x3A) + Nothing + + Note that the octets are interpreted in a big-endian fashion. +-} decodeOctets :: ByteString -> Maybe Mac -decodeOctets bs = if B.length bs == 6 - then Just $ fromOctets - (BU.unsafeIndex bs 0) - (BU.unsafeIndex bs 1) - (BU.unsafeIndex bs 2) - (BU.unsafeIndex bs 3) - (BU.unsafeIndex bs 4) - (BU.unsafeIndex bs 5) - else Nothing +decodeOctets bs = + if B.length bs == 6 + then + Just $ + fromOctets + (BU.unsafeIndex bs 0) + (BU.unsafeIndex bs 1) + (BU.unsafeIndex bs 2) + (BU.unsafeIndex bs 3) + (BU.unsafeIndex bs 4) + (BU.unsafeIndex bs 5) + else Nothing rightToMaybe :: Either a b -> Maybe b rightToMaybe = either (const Nothing) Just @@ -198,21 +219,23 @@ rightToMaybe = either (const Nothing) Just c2w :: Char -> Word8 c2w = fromIntegral . ord --- | Encode a 'Mac' address using the default 'MacCodec' 'defCodec'. --- --- >>> T.putStrLn (Mac.encode (Mac 0xA47F247AB423)) --- a4:7f:24:7a:b4:23 +{- | Encode a 'Mac' address using the default 'MacCodec' 'defCodec'. + + >>> T.putStrLn (Mac.encode (Mac 0xA47F247AB423)) + a4:7f:24:7a:b4:23 +-} encode :: Mac -> Text encode = encodeWith defCodec --- | Encode a 'Mac' address using the given 'MacCodec'. --- --- >>> m = Mac 0xA47F247AB423 --- >>> T.putStrLn $ Mac.encodeWith Mac.defCodec m --- a4:7f:24:7a:b4:23 --- --- >>> T.putStrLn $ Mac.encodeWith (Mac.MacCodec (Mac.MacGroupingTriples '-') True) m --- A47-F24-7AB-423 +{- | Encode a 'Mac' address using the given 'MacCodec'. + + >>> m = Mac 0xA47F247AB423 + >>> T.putStrLn $ Mac.encodeWith Mac.defCodec m + a4:7f:24:7a:b4:23 + + >>> T.putStrLn $ Mac.encodeWith (Mac.MacCodec (Mac.MacGroupingTriples '-') True) m + A47-F24-7AB-423 +-} encodeWith :: MacCodec -> Mac -> Text encodeWith (MacCodec g u) m = case g of MacGroupingNoSeparator -> case u of @@ -221,7 +244,7 @@ encodeWith (MacCodec g u) m = case g of MacGroupingPairs c -> case u of True -> TFB.run (fixedBuilderPairs TFB.word8HexFixedUpper) (Pair c m) False -> TFB.run (fixedBuilderPairs TFB.word8HexFixedLower) (Pair c m) - -- withCasedBuilder u $ \bw8 -> TFB.run (fixedBuilderPairs bw8) (Pair c m) + -- withCasedBuilder u $ \bw8 -> TFB.run (fixedBuilderPairs bw8) (Pair c m) MacGroupingTriples c -> case u of True -> TFB.run (fixedBuilderTriples TFB.word12HexFixedUpper) (Pair c m) False -> TFB.run (fixedBuilderTriples TFB.word12HexFixedLower) (Pair c m) @@ -229,23 +252,25 @@ encodeWith (MacCodec g u) m = case g of True -> TFB.run (fixedBuilderQuadruples TFB.word8HexFixedUpper) (Pair c m) False -> TFB.run (fixedBuilderQuadruples TFB.word8HexFixedLower) (Pair c m) --- | Decode a 'Mac' address using the default 'MacCodec' 'defCodec'. --- --- >>> Mac.decode (Text.pack "a4:7f:24:7a:b4:23") --- Just (mac 0xa47f247ab423) --- --- >>> Mac.decode (Text.pack "a47-f24-7ab-423") --- Nothing +{- | Decode a 'Mac' address using the default 'MacCodec' 'defCodec'. + + >>> Mac.decode (Text.pack "a4:7f:24:7a:b4:23") + Just (mac 0xa47f247ab423) + + >>> Mac.decode (Text.pack "a47-f24-7ab-423") + Nothing +-} decode :: Text -> Maybe Mac decode = decodeWith defCodec --- | Decode a 'Mac' address from 'Text' using the given 'MacCodec'. --- --- >>> Mac.decodeWith Mac.defCodec (Text.pack "a4:7f:24:7a:b4:23") --- Just (mac 0xa47f247ab423) --- --- >>> Mac.decodeWith (Mac.MacCodec Mac.MacGroupingNoSeparator False) (Text.pack "a47f247ab423") --- Just (mac 0xa47f247ab423) +{- | Decode a 'Mac' address from 'Text' using the given 'MacCodec'. + +>>> Mac.decodeWith Mac.defCodec (Text.pack "a4:7f:24:7a:b4:23") +Just (mac 0xa47f247ab423) + +>>> Mac.decodeWith (Mac.MacCodec Mac.MacGroupingNoSeparator False) (Text.pack "a47f247ab423") +Just (mac 0xa47f247ab423) +-} decodeWith :: MacCodec -> Text -> Maybe Mac decodeWith codec t = rightToMaybe (AT.parseOnly (parserWith codec <* AT.endOfInput) t) @@ -253,25 +278,27 @@ decodeWith codec t = rightToMaybe (AT.parseOnly (parserWith codec <* AT.endOfInp builder :: Mac -> TBuilder.Builder builder = TBuilder.fromText . encode --- | Parse a 'Mac' address using a 'AT.Parser'. --- --- >>> AT.parseOnly Mac.parser (Text.pack "a4:7f:24:7a:b4:23") --- Right (mac 0xa47f247ab423) --- --- >>> AT.parseOnly Mac.parser (Text.pack "a47-f24-7ab-423") --- Left "':': Failed reading: satisfy" +{- | Parse a 'Mac' address using a 'AT.Parser'. + + >>> AT.parseOnly Mac.parser (Text.pack "a4:7f:24:7a:b4:23") + Right (mac 0xa47f247ab423) + + >>> AT.parseOnly Mac.parser (Text.pack "a47-f24-7ab-423") + Left "':': Failed reading: satisfy" +-} parser :: AT.Parser Mac parser = parserWith defCodec --- | Parser a 'Mac' address using the given 'MacCodec'. --- --- >>> p1 = Mac.parserWith Mac.defCodec --- >>> AT.parseOnly p1 (Text.pack "a4:7f:24:7a:b4:23") --- Right (mac 0xa47f247ab423) --- --- >>> p2 = Mac.parserWith (Mac.MacCodec Mac.MacGroupingNoSeparator False) --- >>> AT.parseOnly p2 (Text.pack "a47f247ab423") --- Right (mac 0xa47f247ab423) +{- | Parser a 'Mac' address using the given 'MacCodec'. + + >>> p1 = Mac.parserWith Mac.defCodec + >>> AT.parseOnly p1 (Text.pack "a4:7f:24:7a:b4:23") + Right (mac 0xa47f247ab423) + + >>> p2 = Mac.parserWith (Mac.MacCodec Mac.MacGroupingNoSeparator False) + >>> AT.parseOnly p2 (Text.pack "a47f247ab423") + Right (mac 0xa47f247ab423) +-} parserWith :: MacCodec -> AT.Parser Mac parserWith (MacCodec g _) = case g of MacGroupingQuadruples c -> parserQuadruples c @@ -279,27 +306,40 @@ parserWith (MacCodec g _) = case g of MacGroupingPairs c -> parserPairs c MacGroupingNoSeparator -> parserNoSeparator --- | The default 'MacCodec': all characters are lowercase hex, separated by colons into pairs. --- --- >>> T.putStrLn $ Mac.encodeWith Mac.defCodec (Mac 0xa47f247ab423) --- a4:7f:24:7a:b4:23 +{- | The default 'MacCodec': all characters are lowercase hex, separated by colons into pairs. + + >>> T.putStrLn $ Mac.encodeWith Mac.defCodec (Mac 0xa47f247ab423) + a4:7f:24:7a:b4:23 +-} defCodec :: MacCodec defCodec = MacCodec (MacGroupingPairs ':') False parserQuadruples :: Char -> AT.Parser Mac -parserQuadruples s = fromOctets - <$> parseTwoHex <*> parseTwoHex <* AT.char s - <*> parseTwoHex <*> parseTwoHex <* AT.char s - <*> parseTwoHex <*> parseTwoHex +parserQuadruples s = + fromOctets + <$> parseTwoHex + <*> parseTwoHex + <* AT.char s + <*> parseTwoHex + <*> parseTwoHex + <* AT.char s + <*> parseTwoHex + <*> parseTwoHex parserPairs :: Char -> AT.Parser Mac -parserPairs s = fromOctets - <$> parseTwoHex <* AT.char s - <*> parseTwoHex <* AT.char s - <*> parseTwoHex <* AT.char s - <*> parseTwoHex <* AT.char s - <*> parseTwoHex <* AT.char s - <*> parseTwoHex +parserPairs s = + fromOctets + <$> parseTwoHex + <* AT.char s + <*> parseTwoHex + <* AT.char s + <*> parseTwoHex + <* AT.char s + <*> parseTwoHex + <* AT.char s + <*> parseTwoHex + <* AT.char s + <*> parseTwoHex parserTriples :: Char -> AT.Parser Mac parserTriples s = do @@ -318,22 +358,24 @@ parserTriples s = do a10 <- parseOneHex a11 <- parseOneHex a12 <- parseOneHex - return $ fromOctets - (unsafeShiftL a1 4 + a2) - (unsafeShiftL a3 4 + a4) - (unsafeShiftL a5 4 + a6) - (unsafeShiftL a7 4 + a8) - (unsafeShiftL a9 4 + a10) - (unsafeShiftL a11 4 + a12) + return $ + fromOctets + (unsafeShiftL a1 4 + a2) + (unsafeShiftL a3 4 + a4) + (unsafeShiftL a5 4 + a6) + (unsafeShiftL a7 4 + a8) + (unsafeShiftL a9 4 + a10) + (unsafeShiftL a11 4 + a12) parserNoSeparator :: AT.Parser Mac -parserNoSeparator = fromOctets - <$> parseTwoHex - <*> parseTwoHex - <*> parseTwoHex - <*> parseTwoHex - <*> parseTwoHex - <*> parseTwoHex +parserNoSeparator = + fromOctets + <$> parseTwoHex + <*> parseTwoHex + <*> parseTwoHex + <*> parseTwoHex + <*> parseTwoHex + <*> parseTwoHex parseTwoHex :: AT.Parser Word8 parseTwoHex = do @@ -347,7 +389,8 @@ tryParseCharHex a c | w >= 65 && w <= 70 = return (w - 55) | w >= 97 && w <= 102 = return (w - 87) | otherwise = a - where w = c2w c + where + w = c2w c parseOneHex :: AT.Parser Word8 parseOneHex = AT.anyChar >>= parseCharHex @@ -362,50 +405,50 @@ data Pair = Pair fixedBuilderTriples :: TFB.Builder Word12 -> TFB.Builder Pair fixedBuilderTriples tripBuilder = - TFB.contramapBuilder (word12At 36 . pairMac) tripBuilder - <> TFB.contramapBuilder pairSep TFB.charBmp - <> TFB.contramapBuilder (word12At 24 . pairMac) tripBuilder - <> TFB.contramapBuilder pairSep TFB.charBmp - <> TFB.contramapBuilder (word12At 12 . pairMac) tripBuilder - <> TFB.contramapBuilder pairSep TFB.charBmp - <> TFB.contramapBuilder (word12At 0 . pairMac) tripBuilder + TFB.contramapBuilder (word12At 36 . pairMac) tripBuilder + <> TFB.contramapBuilder pairSep TFB.charBmp + <> TFB.contramapBuilder (word12At 24 . pairMac) tripBuilder + <> TFB.contramapBuilder pairSep TFB.charBmp + <> TFB.contramapBuilder (word12At 12 . pairMac) tripBuilder + <> TFB.contramapBuilder pairSep TFB.charBmp + <> TFB.contramapBuilder (word12At 0 . pairMac) tripBuilder {-# INLINE fixedBuilderTriples #-} fixedBuilderNoSeparator :: TFB.Builder Word8 -> TFB.Builder Mac fixedBuilderNoSeparator hexBuilder = - TFB.contramapBuilder (word8At 40) hexBuilder - <> TFB.contramapBuilder (word8At 32) hexBuilder - <> TFB.contramapBuilder (word8At 24) hexBuilder - <> TFB.contramapBuilder (word8At 16) hexBuilder - <> TFB.contramapBuilder (word8At 8) hexBuilder - <> TFB.contramapBuilder (word8At 0) hexBuilder + TFB.contramapBuilder (word8At 40) hexBuilder + <> TFB.contramapBuilder (word8At 32) hexBuilder + <> TFB.contramapBuilder (word8At 24) hexBuilder + <> TFB.contramapBuilder (word8At 16) hexBuilder + <> TFB.contramapBuilder (word8At 8) hexBuilder + <> TFB.contramapBuilder (word8At 0) hexBuilder {-# INLINE fixedBuilderNoSeparator #-} fixedBuilderQuadruples :: TFB.Builder Word8 -> TFB.Builder Pair fixedBuilderQuadruples pairBuilder = - TFB.contramapBuilder (word8At 40 . pairMac) pairBuilder - <> TFB.contramapBuilder (word8At 32 . pairMac) pairBuilder - <> TFB.contramapBuilder pairSep TFB.charBmp - <> TFB.contramapBuilder (word8At 24 . pairMac) pairBuilder - <> TFB.contramapBuilder (word8At 16 . pairMac) pairBuilder - <> TFB.contramapBuilder pairSep TFB.charBmp - <> TFB.contramapBuilder (word8At 8 . pairMac) pairBuilder - <> TFB.contramapBuilder (word8At 0 . pairMac) pairBuilder + TFB.contramapBuilder (word8At 40 . pairMac) pairBuilder + <> TFB.contramapBuilder (word8At 32 . pairMac) pairBuilder + <> TFB.contramapBuilder pairSep TFB.charBmp + <> TFB.contramapBuilder (word8At 24 . pairMac) pairBuilder + <> TFB.contramapBuilder (word8At 16 . pairMac) pairBuilder + <> TFB.contramapBuilder pairSep TFB.charBmp + <> TFB.contramapBuilder (word8At 8 . pairMac) pairBuilder + <> TFB.contramapBuilder (word8At 0 . pairMac) pairBuilder {-# INLINE fixedBuilderQuadruples #-} fixedBuilderPairs :: TFB.Builder Word8 -> TFB.Builder Pair fixedBuilderPairs pairBuilder = - TFB.contramapBuilder (word8At 40 . pairMac) pairBuilder - <> TFB.contramapBuilder pairSep TFB.charBmp - <> TFB.contramapBuilder (word8At 32 . pairMac) pairBuilder - <> TFB.contramapBuilder pairSep TFB.charBmp - <> TFB.contramapBuilder (word8At 24 . pairMac) pairBuilder - <> TFB.contramapBuilder pairSep TFB.charBmp - <> TFB.contramapBuilder (word8At 16 . pairMac) pairBuilder - <> TFB.contramapBuilder pairSep TFB.charBmp - <> TFB.contramapBuilder (word8At 8 . pairMac) pairBuilder - <> TFB.contramapBuilder pairSep TFB.charBmp - <> TFB.contramapBuilder (word8At 0 . pairMac) pairBuilder + TFB.contramapBuilder (word8At 40 . pairMac) pairBuilder + <> TFB.contramapBuilder pairSep TFB.charBmp + <> TFB.contramapBuilder (word8At 32 . pairMac) pairBuilder + <> TFB.contramapBuilder pairSep TFB.charBmp + <> TFB.contramapBuilder (word8At 24 . pairMac) pairBuilder + <> TFB.contramapBuilder pairSep TFB.charBmp + <> TFB.contramapBuilder (word8At 16 . pairMac) pairBuilder + <> TFB.contramapBuilder pairSep TFB.charBmp + <> TFB.contramapBuilder (word8At 8 . pairMac) pairBuilder + <> TFB.contramapBuilder pairSep TFB.charBmp + <> TFB.contramapBuilder (word8At 0 . pairMac) pairBuilder {-# INLINE fixedBuilderPairs #-} word8At :: Int -> Mac -> Word8 @@ -416,92 +459,89 @@ word12At :: Int -> Mac -> Word12 word12At i (Mac w) = fromIntegral (unsafeShiftR w i) {-# INLINE word12At #-} --- | Encode a 'Mac' address using the default 'MacCodec' 'defCodec'. --- --- >>> BC.putStrLn (Mac.encodeUtf8 (Mac.mac 0x64255A0F2C47)) --- 64:25:5a:0f:2c:47 +{- | Encode a 'Mac' address using the default 'MacCodec' 'defCodec'. + + >>> BC.putStrLn (Mac.encodeUtf8 (Mac.mac 0x64255A0F2C47)) + 64:25:5a:0f:2c:47 +-} encodeUtf8 :: Mac -> ByteString encodeUtf8 = encodeWithUtf8 defCodec --- | Lenient decoding of MAC address that accepts lowercase, uppercase, --- and any kind of separator. --- --- >>> Mac.decodeUtf8 "A2:DE:AD:BE:EF:67" --- Just (mac 0xa2deadbeef67) --- >>> Mac.decodeUtf8 "13-a2-fe-a4-17-96" --- Just (mac 0x13a2fea41796) --- >>> Mac.decodeUtf8 "0A42.47BA.67C2" --- Just (mac 0x0a4247ba67c2) +{- | Lenient decoding of MAC address that accepts lowercase, uppercase, + and any kind of separator. + + >>> Mac.decodeUtf8 "A2:DE:AD:BE:EF:67" + Just (mac 0xa2deadbeef67) + >>> Mac.decodeUtf8 "13-a2-fe-a4-17-96" + Just (mac 0x13a2fea41796) + >>> Mac.decodeUtf8 "0A42.47BA.67C2" + Just (mac 0x0a4247ba67c2) +-} decodeUtf8 :: ByteString -> Maybe Mac decodeUtf8 = decodeLenientUtf8 --- | Decode a 'ByteString' as a 'Mac' address using the given 'MacCodec'. --- --- >>> Mac.decodeWithUtf8 Mac.defCodec (BC.pack "64:25:5a:0f:2c:47") --- Just (mac 0x64255a0f2c47) --- --- >>> Mac.decodeWithUtf8 (Mac.MacCodec Mac.MacGroupingNoSeparator False) (BC.pack "64255a0f2c47") --- Just (mac 0x64255a0f2c47) +{- | Decode a 'ByteString' as a 'Mac' address using the given 'MacCodec'. + + >>> Mac.decodeWithUtf8 Mac.defCodec (BC.pack "64:25:5a:0f:2c:47") + Just (mac 0x64255a0f2c47) + + >>> Mac.decodeWithUtf8 (Mac.MacCodec Mac.MacGroupingNoSeparator False) (BC.pack "64255a0f2c47") + Just (mac 0x64255a0f2c47) +-} decodeWithUtf8 :: MacCodec -> ByteString -> Maybe Mac decodeWithUtf8 codec bs = rightToMaybe (AB.parseOnly (parserWithUtf8 codec <* AB.endOfInput) bs) decodeLenientUtf8 :: ByteString -> Maybe Mac decodeLenientUtf8 bs = rightToMaybe (AB.parseOnly (parserLenientUtf8 <* AB.endOfInput) bs) --- | Encode a 'Mac' address as colon-separated hexadecimal octets, --- preferring lowercase for alphabetical characters. +{- | Encode a 'Mac' address as colon-separated hexadecimal octets, + preferring lowercase for alphabetical characters. +-} encodeShort :: Mac -> ShortText encodeShort !m = case BBB.run Nat.constant (boundedBuilderUtf8 m) of ByteArray x -> TS.fromShortByteStringUnsafe (SBS x) --- | Encode a 'Mac' address as colon-separated hexadecimal octets, --- preferring lowercase for alphabetical characters. --- --- >>> BBB.run Nat.constant $ Mac.boundedBuilderUtf8 $ Mac.mac 0xDEADBEEF1609 --- [0x64, 0x65, 0x3a, 0x61, 0x64, 0x3a, 0x62, 0x65, 0x3a, 0x65, 0x66, 0x3a, 0x31, 0x36, 0x3a, 0x30, 0x39] +{- | Encode a 'Mac' address as colon-separated hexadecimal octets, + preferring lowercase for alphabetical characters. + + >>> BBB.run Nat.constant $ Mac.boundedBuilderUtf8 $ Mac.mac 0xDEADBEEF1609 + [0x64, 0x65, 0x3a, 0x61, 0x64, 0x3a, 0x62, 0x65, 0x3a, 0x65, 0x66, 0x3a, 0x31, 0x36, 0x3a, 0x30, 0x39] +-} boundedBuilderUtf8 :: Mac -> BBB.Builder 17 boundedBuilderUtf8 !w = BBB.word8PaddedLowerHex w0 - `BBB.append` - BBB.ascii ':' - `BBB.append` - BBB.word8PaddedLowerHex w1 - `BBB.append` - BBB.ascii ':' - `BBB.append` - BBB.word8PaddedLowerHex w2 - `BBB.append` - BBB.ascii ':' - `BBB.append` - BBB.word8PaddedLowerHex w3 - `BBB.append` - BBB.ascii ':' - `BBB.append` - BBB.word8PaddedLowerHex w4 - `BBB.append` - BBB.ascii ':' - `BBB.append` - BBB.word8PaddedLowerHex w5 - where - (w0,w1,w2,w3,w4,w5) = toOctets w - --- | Lenient decoding of MAC address. This --- is case insensitive and allows either @:@ or @-@ as the separator. --- It also allows leading zeroes to be missing. --- --- >>> Mac.decodeUtf8Bytes (Ascii.fromString "A2:DE:AD:BE:EF:67") --- Just (mac 0xa2deadbeef67) --- >>> Mac.decodeUtf8Bytes (Ascii.fromString "13-a2-FE-A4-17-96") --- Just (mac 0x13a2fea41796) + `BBB.append` BBB.ascii ':' + `BBB.append` BBB.word8PaddedLowerHex w1 + `BBB.append` BBB.ascii ':' + `BBB.append` BBB.word8PaddedLowerHex w2 + `BBB.append` BBB.ascii ':' + `BBB.append` BBB.word8PaddedLowerHex w3 + `BBB.append` BBB.ascii ':' + `BBB.append` BBB.word8PaddedLowerHex w4 + `BBB.append` BBB.ascii ':' + `BBB.append` BBB.word8PaddedLowerHex w5 + where + (w0, w1, w2, w3, w4, w5) = toOctets w + +{- | Lenient decoding of MAC address. This + is case insensitive and allows either @:@ or @-@ as the separator. + It also allows leading zeroes to be missing. + + >>> Mac.decodeUtf8Bytes (Ascii.fromString "A2:DE:AD:BE:EF:67") + Just (mac 0xa2deadbeef67) + >>> Mac.decodeUtf8Bytes (Ascii.fromString "13-a2-FE-A4-17-96") + Just (mac 0x13a2fea41796) +-} decodeUtf8Bytes :: Bytes.Bytes -> Maybe Mac decodeUtf8Bytes = Parser.parseBytesMaybe (parserUtf8Bytes ()) --- | Leniently parse UTF-8-encoded 'Bytes' as a 'Mac' address. This --- is case insensitive and allows either @:@ or @-@ as the separator. --- It also allows leading zeroes to be missing. --- --- >>> Parser.parseBytes (Mac.parserUtf8Bytes ()) (Ascii.fromString "de:ad:BE:EF:1:23") --- Success (Slice {offset = 16, length = 0, value = mac 0xdeadbeef0123}) +{- | Leniently parse UTF-8-encoded 'Bytes' as a 'Mac' address. This + is case insensitive and allows either @:@ or @-@ as the separator. + It also allows leading zeroes to be missing. + + >>> Parser.parseBytes (Mac.parserUtf8Bytes ()) (Ascii.fromString "de:ad:BE:EF:1:23") + Success (Slice {offset = 16, length = 0, value = mac 0xdeadbeef0123}) +-} parserUtf8Bytes :: e -> Parser.Parser e s Mac parserUtf8Bytes e = do w1 <- Latin.hexWord8 e @@ -522,14 +562,16 @@ parserUtf8Bytes e = do pure (fromOctets w1 w2 w3 w4 w5 w6) _ -> Parser.fail e --- | Make a bytestring builder from a 'Mac' address --- using a colon as the separator. +{- | Make a bytestring builder from a 'Mac' address + using a colon as the separator. +-} builderUtf8 :: Mac -> BB.Builder builderUtf8 = BB.byteString . encodeUtf8 --- | Lenient parser for a 'Mac' address using any character --- as the separator and accepting any digit grouping --- (i.e. @FA:43:B2:C0:0F:99@ or @A065.647B.87FA@). +{- | Lenient parser for a 'Mac' address using any character + as the separator and accepting any digit grouping + (i.e. @FA:43:B2:C0:0F:99@ or @A065.647B.87FA@). +-} parserUtf8 :: AB.Parser Mac parserUtf8 = parserLenientUtf8 @@ -555,32 +597,39 @@ parserLenientUtf8 = do a10 <- parseOneHexLenientUtf8 a11 <- parseOneHexLenientUtf8 a12 <- parseOneHexLenientUtf8 - return $ fromOctets - (unsafeShiftL a1 4 + a2) - (unsafeShiftL a3 4 + a4) - (unsafeShiftL a5 4 + a6) - (unsafeShiftL a7 4 + a8) - (unsafeShiftL a9 4 + a10) - (unsafeShiftL a11 4 + a12) - + return $ + fromOctets + (unsafeShiftL a1 4 + a2) + (unsafeShiftL a3 4 + a4) + (unsafeShiftL a5 4 + a6) + (unsafeShiftL a7 4 + a8) + (unsafeShiftL a9 4 + a10) + (unsafeShiftL a11 4 + a12) parserNoSeparatorUtf8 :: AB.Parser Mac -parserNoSeparatorUtf8 = fromOctets - <$> parseTwoHexUtf8 - <*> parseTwoHexUtf8 - <*> parseTwoHexUtf8 - <*> parseTwoHexUtf8 - <*> parseTwoHexUtf8 - <*> parseTwoHexUtf8 +parserNoSeparatorUtf8 = + fromOctets + <$> parseTwoHexUtf8 + <*> parseTwoHexUtf8 + <*> parseTwoHexUtf8 + <*> parseTwoHexUtf8 + <*> parseTwoHexUtf8 + <*> parseTwoHexUtf8 parserPairsUtf8 :: Word8 -> AB.Parser Mac -parserPairsUtf8 s = fromOctets - <$> parseTwoHexUtf8 <* ABW.word8 s - <*> parseTwoHexUtf8 <* ABW.word8 s - <*> parseTwoHexUtf8 <* ABW.word8 s - <*> parseTwoHexUtf8 <* ABW.word8 s - <*> parseTwoHexUtf8 <* ABW.word8 s - <*> parseTwoHexUtf8 +parserPairsUtf8 s = + fromOctets + <$> parseTwoHexUtf8 + <* ABW.word8 s + <*> parseTwoHexUtf8 + <* ABW.word8 s + <*> parseTwoHexUtf8 + <* ABW.word8 s + <*> parseTwoHexUtf8 + <* ABW.word8 s + <*> parseTwoHexUtf8 + <* ABW.word8 s + <*> parseTwoHexUtf8 parserTriplesUtf8 :: Word8 -> AB.Parser Mac parserTriplesUtf8 s = do @@ -599,25 +648,33 @@ parserTriplesUtf8 s = do a10 <- parseOneHexUtf8 a11 <- parseOneHexUtf8 a12 <- parseOneHexUtf8 - return $ fromOctets - (unsafeShiftL a1 4 + a2) - (unsafeShiftL a3 4 + a4) - (unsafeShiftL a5 4 + a6) - (unsafeShiftL a7 4 + a8) - (unsafeShiftL a9 4 + a10) - (unsafeShiftL a11 4 + a12) + return $ + fromOctets + (unsafeShiftL a1 4 + a2) + (unsafeShiftL a3 4 + a4) + (unsafeShiftL a5 4 + a6) + (unsafeShiftL a7 4 + a8) + (unsafeShiftL a9 4 + a10) + (unsafeShiftL a11 4 + a12) parserQuadruplesUtf8 :: Word8 -> AB.Parser Mac -parserQuadruplesUtf8 s = fromOctets - <$> parseTwoHexUtf8 <*> parseTwoHexUtf8 <* ABW.word8 s - <*> parseTwoHexUtf8 <*> parseTwoHexUtf8 <* ABW.word8 s - <*> parseTwoHexUtf8 <*> parseTwoHexUtf8 +parserQuadruplesUtf8 s = + fromOctets + <$> parseTwoHexUtf8 + <*> parseTwoHexUtf8 + <* ABW.word8 s + <*> parseTwoHexUtf8 + <*> parseTwoHexUtf8 + <* ABW.word8 s + <*> parseTwoHexUtf8 + <*> parseTwoHexUtf8 parseOneHexUtf8 :: AB.Parser Word8 parseOneHexUtf8 = ABW.anyWord8 >>= parseWord8Hex --- | Parse a single hexidecimal character. This will skip --- at most one character to do this. +{- | Parse a single hexidecimal character. This will skip + at most one character to do this. +-} parseOneHexLenientUtf8 :: AB.Parser Word8 parseOneHexLenientUtf8 = do a <- ABW.anyWord8 @@ -631,9 +688,10 @@ parseTwoHexUtf8 = do b <- ABW.anyWord8 >>= parseWord8Hex return (unsafeShiftL a 4 + b) --- | Kind of a confusing type signature. The Word8 that stands --- alone represented an ascii-encoded value. The others actually --- describes the numbers that would be decoded from this value. +{- | Kind of a confusing type signature. The Word8 that stands + alone represented an ascii-encoded value. The others actually + describes the numbers that would be decoded from this value. +-} tryParseWord8Hex :: AB.Parser Word8 -> Word8 -> AB.Parser Word8 tryParseWord8Hex a w | w >= 48 && w <= 57 = return (w - 48) @@ -644,14 +702,15 @@ tryParseWord8Hex a w parseWord8Hex :: Word8 -> AB.Parser Word8 parseWord8Hex = tryParseWord8Hex (fail "invalid hexadecimal character") --- | Encode a 'Mac' address as a 'ByteString' using the given 'MacCodec'. --- --- >>> m = Mac 0xA47F247AB423 --- >>> BC.putStrLn $ Mac.encodeWithUtf8 Mac.defCodec m --- a4:7f:24:7a:b4:23 --- --- >>> BC.putStrLn $ Mac.encodeWithUtf8 (Mac.MacCodec (Mac.MacGroupingTriples '-') True) m --- A47-F24-7AB-423 +{- | Encode a 'Mac' address as a 'ByteString' using the given 'MacCodec'. + + >>> m = Mac 0xA47F247AB423 + >>> BC.putStrLn $ Mac.encodeWithUtf8 Mac.defCodec m + a4:7f:24:7a:b4:23 + + >>> BC.putStrLn $ Mac.encodeWithUtf8 (Mac.MacCodec (Mac.MacGroupingTriples '-') True) m + A47-F24-7AB-423 +-} encodeWithUtf8 :: MacCodec -> Mac -> ByteString encodeWithUtf8 (MacCodec g u) m = case g of MacGroupingNoSeparator -> case u of @@ -660,7 +719,7 @@ encodeWithUtf8 (MacCodec g u) m = case g of MacGroupingPairs c -> case u of True -> BFB.run (fixedBuilderPairsUtf8 BFB.word8HexFixedUpper) (PairUtf8 (c2w c) m) False -> BFB.run (fixedBuilderPairsUtf8 BFB.word8HexFixedLower) (PairUtf8 (c2w c) m) - -- withCasedBuilder u $ \bw8 -> BFB.run (fixedBuilderPairs bw8) (Pair c m) + -- withCasedBuilder u $ \bw8 -> BFB.run (fixedBuilderPairs bw8) (Pair c m) MacGroupingTriples c -> case u of True -> BFB.run (fixedBuilderTriplesUtf8 BFB.word12HexFixedUpper) (PairUtf8 (c2w c) m) False -> BFB.run (fixedBuilderTriplesUtf8 BFB.word12HexFixedLower) (PairUtf8 (c2w c) m) @@ -675,50 +734,50 @@ data PairUtf8 = PairUtf8 fixedBuilderTriplesUtf8 :: BFB.Builder Word12 -> BFB.Builder PairUtf8 fixedBuilderTriplesUtf8 tripBuilder = - BFB.contramapBuilder (word12AtUtf8 36 . pairMacUtf8) tripBuilder - <> BFB.contramapBuilder pairSepUtf8 BFB.word8 - <> BFB.contramapBuilder (word12AtUtf8 24 . pairMacUtf8) tripBuilder - <> BFB.contramapBuilder pairSepUtf8 BFB.word8 - <> BFB.contramapBuilder (word12AtUtf8 12 . pairMacUtf8) tripBuilder - <> BFB.contramapBuilder pairSepUtf8 BFB.word8 - <> BFB.contramapBuilder (word12AtUtf8 0 . pairMacUtf8) tripBuilder + BFB.contramapBuilder (word12AtUtf8 36 . pairMacUtf8) tripBuilder + <> BFB.contramapBuilder pairSepUtf8 BFB.word8 + <> BFB.contramapBuilder (word12AtUtf8 24 . pairMacUtf8) tripBuilder + <> BFB.contramapBuilder pairSepUtf8 BFB.word8 + <> BFB.contramapBuilder (word12AtUtf8 12 . pairMacUtf8) tripBuilder + <> BFB.contramapBuilder pairSepUtf8 BFB.word8 + <> BFB.contramapBuilder (word12AtUtf8 0 . pairMacUtf8) tripBuilder {-# INLINE fixedBuilderTriplesUtf8 #-} fixedBuilderQuadruplesUtf8 :: BFB.Builder Word8 -> BFB.Builder PairUtf8 fixedBuilderQuadruplesUtf8 pairBuilder = - BFB.contramapBuilder (word8AtUtf8 40 . pairMacUtf8) pairBuilder - <> BFB.contramapBuilder (word8AtUtf8 32 . pairMacUtf8) pairBuilder - <> BFB.contramapBuilder pairSepUtf8 BFB.word8 - <> BFB.contramapBuilder (word8AtUtf8 24 . pairMacUtf8) pairBuilder - <> BFB.contramapBuilder (word8AtUtf8 16 . pairMacUtf8) pairBuilder - <> BFB.contramapBuilder pairSepUtf8 BFB.word8 - <> BFB.contramapBuilder (word8AtUtf8 8 . pairMacUtf8) pairBuilder - <> BFB.contramapBuilder (word8AtUtf8 0 . pairMacUtf8) pairBuilder + BFB.contramapBuilder (word8AtUtf8 40 . pairMacUtf8) pairBuilder + <> BFB.contramapBuilder (word8AtUtf8 32 . pairMacUtf8) pairBuilder + <> BFB.contramapBuilder pairSepUtf8 BFB.word8 + <> BFB.contramapBuilder (word8AtUtf8 24 . pairMacUtf8) pairBuilder + <> BFB.contramapBuilder (word8AtUtf8 16 . pairMacUtf8) pairBuilder + <> BFB.contramapBuilder pairSepUtf8 BFB.word8 + <> BFB.contramapBuilder (word8AtUtf8 8 . pairMacUtf8) pairBuilder + <> BFB.contramapBuilder (word8AtUtf8 0 . pairMacUtf8) pairBuilder {-# INLINE fixedBuilderQuadruplesUtf8 #-} fixedBuilderPairsUtf8 :: BFB.Builder Word8 -> BFB.Builder PairUtf8 fixedBuilderPairsUtf8 pairBuilder = - BFB.contramapBuilder (word8AtUtf8 40 . pairMacUtf8) pairBuilder - <> BFB.contramapBuilder pairSepUtf8 BFB.word8 - <> BFB.contramapBuilder (word8AtUtf8 32 . pairMacUtf8) pairBuilder - <> BFB.contramapBuilder pairSepUtf8 BFB.word8 - <> BFB.contramapBuilder (word8AtUtf8 24 . pairMacUtf8) pairBuilder - <> BFB.contramapBuilder pairSepUtf8 BFB.word8 - <> BFB.contramapBuilder (word8AtUtf8 16 . pairMacUtf8) pairBuilder - <> BFB.contramapBuilder pairSepUtf8 BFB.word8 - <> BFB.contramapBuilder (word8AtUtf8 8 . pairMacUtf8) pairBuilder - <> BFB.contramapBuilder pairSepUtf8 BFB.word8 - <> BFB.contramapBuilder (word8AtUtf8 0 . pairMacUtf8) pairBuilder + BFB.contramapBuilder (word8AtUtf8 40 . pairMacUtf8) pairBuilder + <> BFB.contramapBuilder pairSepUtf8 BFB.word8 + <> BFB.contramapBuilder (word8AtUtf8 32 . pairMacUtf8) pairBuilder + <> BFB.contramapBuilder pairSepUtf8 BFB.word8 + <> BFB.contramapBuilder (word8AtUtf8 24 . pairMacUtf8) pairBuilder + <> BFB.contramapBuilder pairSepUtf8 BFB.word8 + <> BFB.contramapBuilder (word8AtUtf8 16 . pairMacUtf8) pairBuilder + <> BFB.contramapBuilder pairSepUtf8 BFB.word8 + <> BFB.contramapBuilder (word8AtUtf8 8 . pairMacUtf8) pairBuilder + <> BFB.contramapBuilder pairSepUtf8 BFB.word8 + <> BFB.contramapBuilder (word8AtUtf8 0 . pairMacUtf8) pairBuilder {-# INLINE fixedBuilderPairsUtf8 #-} fixedBuilderNoSeparatorUtf8 :: BFB.Builder Word8 -> BFB.Builder Mac fixedBuilderNoSeparatorUtf8 hexBuilder = - BFB.contramapBuilder (word8AtUtf8 40) hexBuilder - <> BFB.contramapBuilder (word8AtUtf8 32) hexBuilder - <> BFB.contramapBuilder (word8AtUtf8 24) hexBuilder - <> BFB.contramapBuilder (word8AtUtf8 16) hexBuilder - <> BFB.contramapBuilder (word8AtUtf8 8) hexBuilder - <> BFB.contramapBuilder (word8AtUtf8 0) hexBuilder + BFB.contramapBuilder (word8AtUtf8 40) hexBuilder + <> BFB.contramapBuilder (word8AtUtf8 32) hexBuilder + <> BFB.contramapBuilder (word8AtUtf8 24) hexBuilder + <> BFB.contramapBuilder (word8AtUtf8 16) hexBuilder + <> BFB.contramapBuilder (word8AtUtf8 8) hexBuilder + <> BFB.contramapBuilder (word8AtUtf8 0) hexBuilder {-# INLINE fixedBuilderNoSeparatorUtf8 #-} word8AtUtf8 :: Int -> Mac -> Word8 @@ -729,76 +788,88 @@ word12AtUtf8 :: Int -> Mac -> Word12 word12AtUtf8 i (Mac w) = fromIntegral (unsafeShiftR w i) {-# INLINE word12AtUtf8 #-} --- | This only preserves the lower 6 bytes of the 8-byte word that backs a mac address. --- It runs slower than it would if it used a full 8-byte word, but it consumes less --- space. When storing millions of mac addresses, this is a good trade to make. When --- storing a small number of mac address, it might be preferable to make a primitive --- array of 'Word64' instead and use the mac address data constructor to coerce between --- the two. +{- | This only preserves the lower 6 bytes of the 8-byte word that backs a mac address. +It runs slower than it would if it used a full 8-byte word, but it consumes less +space. When storing millions of mac addresses, this is a good trade to make. When +storing a small number of mac address, it might be preferable to make a primitive +array of 'Word64' instead and use the mac address data constructor to coerce between +the two. +-} instance Prim Mac where sizeOf# _ = 6# alignment# _ = 2# - indexByteArray# arr i0 = macFromWord16# - (Compat.indexWord16Array# arr i) - (Compat.indexWord16Array# arr (i +# 1#)) - (Compat.indexWord16Array# arr (i +# 2#)) - where !i = 3# *# i0 + indexByteArray# arr i0 = + macFromWord16# + (Compat.indexWord16Array# arr i) + (Compat.indexWord16Array# arr (i +# 1#)) + (Compat.indexWord16Array# arr (i +# 2#)) + where + !i = 3# *# i0 readByteArray# arr i0 s0 = case Compat.readWord16Array# arr i s0 of (# s1, a #) -> case Compat.readWord16Array# arr (i +# 1#) s1 of (# s2, b #) -> case Compat.readWord16Array# arr (i +# 2#) s2 of (# s3, c #) -> (# s3, macFromWord16# a b c #) - where !i = 3# *# i0 + where + !i = 3# *# i0 writeByteArray# arr i0 m s0 = case Compat.writeWord16Array# arr i (macToWord16A# m) s0 of s1 -> case Compat.writeWord16Array# arr (i +# 1#) (macToWord16B# m) s1 of s2 -> Compat.writeWord16Array# arr (i +# 2#) (macToWord16C# m) s2 - where !i = 3# *# i0 - indexOffAddr# arr i0 = macFromWord16# - (Compat.indexWord16OffAddr# arr i) - (Compat.indexWord16OffAddr# arr (i +# 1#)) - (Compat.indexWord16OffAddr# arr (i +# 2#)) - where !i = 3# *# i0 + where + !i = 3# *# i0 + indexOffAddr# arr i0 = + macFromWord16# + (Compat.indexWord16OffAddr# arr i) + (Compat.indexWord16OffAddr# arr (i +# 1#)) + (Compat.indexWord16OffAddr# arr (i +# 2#)) + where + !i = 3# *# i0 readOffAddr# arr i0 s0 = case Compat.readWord16OffAddr# arr i s0 of (# s1, a #) -> case Compat.readWord16OffAddr# arr (i +# 1#) s1 of (# s2, b #) -> case Compat.readWord16OffAddr# arr (i +# 2#) s2 of (# s3, c #) -> (# s3, macFromWord16# a b c #) - where !i = 3# *# i0 + where + !i = 3# *# i0 writeOffAddr# arr i0 m s0 = case Compat.writeWord16OffAddr# arr i (macToWord16A# m) s0 of s1 -> case Compat.writeWord16OffAddr# arr (i +# 1#) (macToWord16B# m) s1 of s2 -> Compat.writeWord16OffAddr# arr (i +# 2#) (macToWord16C# m) s2 - where !i = 3# *# i0 + where + !i = 3# *# i0 setByteArray# arr# i# len# ident = go 0# - where - go ix# s0 = if isTrue# (ix# <# len#) + where + go ix# s0 = + if isTrue# (ix# <# len#) then case writeByteArray# arr# (i# +# ix#) ident s0 of s1 -> go (ix# +# 1#) s1 else s0 setOffAddr# addr# i# len# ident = go 0# - where - go ix# s0 = if isTrue# (ix# <# len#) + where + go ix# s0 = + if isTrue# (ix# <# len#) then case writeOffAddr# addr# (i# +# ix#) ident s0 of s1 -> go (ix# +# 1#) s1 else s0 macToWord16A# :: Mac -> Word# -{-# inline macToWord16A# #-} +{-# INLINE macToWord16A# #-} macToWord16A# (Mac w) = case word64ToWord16 (unsafeShiftR w 32) of Compat.W16# x -> x macToWord16B# :: Mac -> Word# -{-# inline macToWord16B# #-} +{-# INLINE macToWord16B# #-} macToWord16B# (Mac w) = case word64ToWord16 (unsafeShiftR w 16) of Compat.W16# x -> x macToWord16C# :: Mac -> Word# -{-# inline macToWord16C# #-} +{-# INLINE macToWord16C# #-} macToWord16C# (Mac w) = case word64ToWord16 w of Compat.W16# x -> x macFromWord16# :: Word# -> Word# -> Word# -> Mac -macFromWord16# a b c = Mac - $ (unsafeShiftL (word16ToWord64 (Compat.W16# a)) 32) - .|. (unsafeShiftL (word16ToWord64 (Compat.W16# b)) 16) - .|. (word16ToWord64 (Compat.W16# c)) +macFromWord16# a b c = + Mac $ + (unsafeShiftL (word16ToWord64 (Compat.W16# a)) 32) + .|. (unsafeShiftL (word16ToWord64 (Compat.W16# b)) 16) + .|. (word16ToWord64 (Compat.W16# c)) word16ToWord64 :: Word16 -> Word64 word16ToWord64 = fromIntegral @@ -812,9 +883,10 @@ word64ToWord16 = fromIntegral -- It also uses the smart constructor instead -- of the actual constructor instance Show Mac where - showsPrec p (Mac addr) = showParen (p > 10) - $ showString "mac " - . showHexWord48 addr + showsPrec p (Mac addr) = + showParen (p > 10) $ + showString "mac " + . showHexWord48 addr instance Read Mac where readPrec = parens $ prec 10 $ do @@ -842,43 +914,52 @@ print = TIO.putStrLn . encode showHexWord48 :: Word64 -> ShowS showHexWord48 w = showString "0x" . go 11 - where + where go :: Int -> ShowS - go !ix = if ix >= 0 - then showChar (nibbleToHex ((unsafeShiftR (fromIntegral w) (ix * 4)) .&. 0xF)) . go (ix - 1) - else id + go !ix = + if ix >= 0 + then showChar (nibbleToHex ((unsafeShiftR (fromIntegral w) (ix * 4)) .&. 0xF)) . go (ix - 1) + else id nibbleToHex :: Word -> Char nibbleToHex w | w < 10 = chr (fromIntegral (w + 48)) | otherwise = chr (fromIntegral (w + 87)) --- | A 'MacCodec' allows users to control the encoding/decoding --- of their 'Mac' addresses. +{- | A 'MacCodec' allows users to control the encoding/decoding + of their 'Mac' addresses. +-} data MacCodec = MacCodec { macCodecGrouping :: !MacGrouping , macCodecUpperCase :: !Bool - } deriving (Eq,Ord,Show,Read,Generic,Data) + } + deriving (Eq, Ord, Show, Read, Generic, Data) --- | The format expected by the mac address parser. The 'Word8' taken --- by some of these constructors is the ascii value of the character --- to be used as the separator. This is typically a colon, a hyphen, or --- a space character. All decoding functions are case insensitive. +{- | The format expected by the mac address parser. The 'Word8' taken + by some of these constructors is the ascii value of the character + to be used as the separator. This is typically a colon, a hyphen, or + a space character. All decoding functions are case insensitive. +-} data MacGrouping - = MacGroupingPairs !Char -- ^ Two-character groups, @FA:2B:40:09:8C:11@ - | MacGroupingTriples !Char -- ^ Three-character groups, @24B-F0A-025-829@ - | MacGroupingQuadruples !Char -- ^ Four-character groups, @A220.0745.CAC7@ - | MacGroupingNoSeparator -- ^ No separator, @24AF4B5B0780@ - deriving (Eq,Ord,Show,Read,Generic,Data) + = -- | Two-character groups, @FA:2B:40:09:8C:11@ + MacGroupingPairs !Char + | -- | Three-character groups, @24B-F0A-025-829@ + MacGroupingTriples !Char + | -- | Four-character groups, @A220.0745.CAC7@ + MacGroupingQuadruples !Char + | -- | No separator, @24AF4B5B0780@ + MacGroupingNoSeparator + deriving (Eq, Ord, Show, Read, Generic, Data) instance ToJSON Mac where toJSON = Aeson.String . encode instance ToJSONKey Mac where - toJSONKey = ToJSONKeyText - (keyFromText . encode) - (\m -> Aeson.unsafeToEncoding $ BB.char7 '"' <> builderUtf8 m <> BB.char7 '"') - where + toJSONKey = + ToJSONKeyText + (keyFromText . encode) + (\m -> Aeson.unsafeToEncoding $ BB.char7 '"' <> builderUtf8 m <> BB.char7 '"') + where #if MIN_VERSION_aeson(2,0,0) keyFromText = AK.fromText #else @@ -898,7 +979,7 @@ attoparsecParseJSON p v = case v of Aeson.String t -> case AT.parseOnly p t of - Left err -> fail err + Left err -> fail err Right res -> return res _ -> fail "expected a String" @@ -906,11 +987,10 @@ attoparsecParseJSON p v = -- than 256. unsafeWord48FromOctets :: Word64 -> Word64 -> Word64 -> Word64 -> Word64 -> Word64 -> Word64 unsafeWord48FromOctets a b c d e f = - fromIntegral - $ unsafeShiftL a 40 - .|. unsafeShiftL b 32 - .|. unsafeShiftL c 24 - .|. unsafeShiftL d 16 - .|. unsafeShiftL e 8 - .|. f - + fromIntegral $ + unsafeShiftL a 40 + .|. unsafeShiftL b 32 + .|. unsafeShiftL c 24 + .|. unsafeShiftL d 16 + .|. unsafeShiftL e 8 + .|. f diff --git a/src/Net/Types.hs b/src/Net/Types.hs index cbeefa1..d97677f 100644 --- a/src/Net/Types.hs +++ b/src/Net/Types.hs @@ -1,18 +1,16 @@ -{-| This module re-exports all of the thematic types that this library defines. --} +-- | This module re-exports all of the thematic types that this library defines. module Net.Types - ( IPv4(..) - , IPv6(..) - , IP(..) - , IPv4Range(..) - , IPv6Range(..) - , Mac(..) - , MacCodec(..) - , MacGrouping(..) + ( IPv4 (..) + , IPv6 (..) + , IP (..) + , IPv4Range (..) + , IPv6Range (..) + , Mac (..) + , MacCodec (..) + , MacGrouping (..) ) where -import Net.IPv6 (IPv6(..),IPv6Range(..)) -import Net.IPv4 (IPv4(..), IPv4Range(..)) -import Net.IP (IP(..)) -import Net.Mac (Mac(..),MacCodec(..),MacGrouping(..)) - +import Net.IP (IP (..)) +import Net.IPv4 (IPv4 (..), IPv4Range (..)) +import Net.IPv6 (IPv6 (..), IPv6Range (..)) +import Net.Mac (Mac (..), MacCodec (..), MacGrouping (..)) diff --git a/test/Bench.hs b/test/Bench.hs index 683cf83..1da1f3c 100644 --- a/test/Bench.hs +++ b/test/Bench.hs @@ -1,40 +1,39 @@ module Main (main) where import Criterion.Main -import Net.Types (IPv4(..),MacGrouping(..),MacCodec(..)) -import Data.Maybe (fromJust) -import Data.Primitive (PrimArray,foldlPrimArray') import Data.Bool (bool) -import System.Random (mkStdGen,randoms) -import qualified Data.Bytes as Bytes +import Data.Maybe (fromJust) +import Data.Primitive (PrimArray, foldlPrimArray') import qualified Data.Text as Text -import qualified Net.Mac as Mac import qualified Net.IPv4 as IPv4 import qualified Net.IPv6 as IPv6 +import qualified Net.Mac as Mac +import Net.Types (IPv4 (..), MacCodec (..), MacGrouping (..)) +import System.Random (mkStdGen, randoms) -import qualified Naive -import qualified IPv4Text1 -import qualified IPv4Text2 +import qualified Data.Bytes.Text.Ascii as Ascii +import qualified GHC.Exts as Exts import qualified IPv4ByteString1 import qualified IPv4DecodeText1 import qualified IPv4DecodeText2 -import qualified GHC.Exts as Exts --- import qualified IPv4TextVariableBuilder +import qualified IPv4Text1 +import qualified IPv4Text2 +import qualified Naive main :: IO () main = do let ipAddr = IPv4 1000000009 ipText = Text.pack "192.168.5.99" - ipBytes = Bytes.fromAsciiString "192.168.5.99" + ipBytes = Ascii.fromString "192.168.5.99" mac = Mac.fromOctets 0xFA 0xBB 0x43 0xA1 0x22 0x09 ip6Text = Text.pack "::" ip6 = fromJust $ IPv6.decode ip6Text ip6StrBigger = "1:2:3:4:5:6:7:8" ip6TextBigger = Text.pack ip6StrBigger - ip6BytesBigger = Bytes.fromAsciiString "1:2:3:4:5:6:7:8" + ip6BytesBigger = Ascii.fromString "1:2:3:4:5:6:7:8" ip6Bigger = fromJust $ IPv6.decode ip6TextBigger ip6ComplicatedStr = "2001:db8:ba1:0:aaaa:542c:bb:cc00" - ip6ComplicatedBytes = Bytes.fromAsciiString ip6ComplicatedStr + ip6ComplicatedBytes = Ascii.fromString ip6ComplicatedStr ip6Complicated = fromJust $ IPv6.decode (Text.pack ip6ComplicatedStr) ip6TextSkip = Text.pack "1:2::7:8" ip6Skip = fromJust $ IPv6.decode ip6TextSkip @@ -42,74 +41,86 @@ main = do ip6Hex = fromJust $ IPv6.decode ip6TextHex hundredAddrs = Exts.fromList (map IPv4 (take 100 (randoms (mkStdGen 42)))) :: PrimArray IPv4 defaultMain - [ bgroup "Mac to Text" - [ bench "Current Implementation, pairs" $ whnf Mac.encode mac - , bench "Current Implementation, no separator" - $ whnf (Mac.encodeWith (MacCodec MacGroupingNoSeparator True)) mac - , bench "Current Implementation, quads" - $ whnf (Mac.encodeWith (MacCodec (MacGroupingQuadruples '-') True)) mac - , bench "Current Implementation, triples" - $ whnf (Mac.encodeWith (MacCodec (MacGroupingQuadruples '.') False)) mac - ] - , bgroup "Mac to ByteString" - [ bench "Current Implementation, pairs" $ whnf Mac.encodeUtf8 mac - , bench "Current Implementation, no separator" - $ whnf (Mac.encodeWithUtf8 (MacCodec MacGroupingNoSeparator True)) mac - ] - , bgroup "IPv4 to ShortText" - [ bench "Implementation" $ whnf IPv4.encodeShort ipAddr - ] - , bgroup "IPv4 to Text" - [ bench "Naive" $ whnf Naive.encodeText ipAddr - , bench "Text Builder" $ whnf IPv4Text2.encode ipAddr - , bench "Preallocated" $ whnf IPv4Text1.encode ipAddr - -- , bench "Variable Builder" $ whnf IPv4TextVariableBuilder.encode ipAddr - ] - , bgroup "IPv4 from Text" - [ bench "Naive" $ whnf Naive.decodeText ipText - , bench "Attoparsec" $ whnf IPv4DecodeText2.decodeText ipText - , bench "Text Reader" $ whnf IPv4DecodeText1.decodeText ipText - ] - , bgroup "IPv4 from Bytes" - [ bench "Current" $ whnf IPv4.decodeUtf8Bytes ipBytes - ] - , bgroup "IPv4 to ByteString" - [ bench "Naive" $ whnf Naive.encodeByteString ipAddr - , bench "Preallocated: No Lookup Tables" $ whnf IPv4ByteString1.encode ipAddr - , bench "Preallocated" $ whnf IPv4.encodeUtf8 ipAddr - ] - , bgroup "IPv6 from Text" - [ bench "::" $ whnf IPv6.decode ip6Text - , bench "1:2:3:4:5:6:7:8" $ whnf IPv6.decode ip6TextBigger - , bench "1:2::7:8" $ whnf IPv6.decode ip6TextSkip - , bench "a:b::c:d" $ whnf IPv6.decode ip6TextHex - ] - , bgroup "IPv6 bytesmith" - [ bench "1:2:3:4:5:6:7:8" $ whnf IPv6.decodeUtf8Bytes ip6BytesBigger - , bench "2001:db8:ba1:0:aaaa:542c:bb:cc00" $ whnf IPv6.decodeUtf8Bytes ip6ComplicatedBytes - ] - , bgroup "IPv6 to Text" - [ bench "::" $ whnf IPv6.encode ip6 - , bench "1:2:3:4:5:6:7:8" $ whnf IPv6.encode ip6Bigger - , bench "1:2::7:8" $ whnf IPv6.encode ip6Skip - , bench "a:b::c:d" $ whnf IPv6.encode ip6Hex - ] - , bgroup "IPv6 to ShortText" - [ bench "1:2:3:4:5:6:7:8" $ whnf IPv6.encodeShort ip6Bigger - , bench "1:2::7:8" $ whnf IPv6.encodeShort ip6Skip - , bench "a:b::c:d" $ whnf IPv6.encodeShort ip6Hex - , bench "2001:db8:ba1:0:aaaa:542c:bb:cc00" $ whnf IPv6.encodeShort ip6Complicated - ] - , bgroup "CIDR Inclusion" - [ bench "reserved" $ whnf manyReserved hundredAddrs - , bench "private" $ whnf manyPrivate hundredAddrs - ] + [ bgroup + "Mac to Text" + [ bench "Current Implementation, pairs" $ whnf Mac.encode mac + , bench "Current Implementation, no separator" $ + whnf (Mac.encodeWith (MacCodec MacGroupingNoSeparator True)) mac + , bench "Current Implementation, quads" $ + whnf (Mac.encodeWith (MacCodec (MacGroupingQuadruples '-') True)) mac + , bench "Current Implementation, triples" $ + whnf (Mac.encodeWith (MacCodec (MacGroupingQuadruples '.') False)) mac + ] + , bgroup + "Mac to ByteString" + [ bench "Current Implementation, pairs" $ whnf Mac.encodeUtf8 mac + , bench "Current Implementation, no separator" $ + whnf (Mac.encodeWithUtf8 (MacCodec MacGroupingNoSeparator True)) mac + ] + , bgroup + "IPv4 to ShortText" + [ bench "Implementation" $ whnf IPv4.encodeShort ipAddr + ] + , bgroup + "IPv4 to Text" + [ bench "Naive" $ whnf Naive.encodeText ipAddr + , bench "Text Builder" $ whnf IPv4Text2.encode ipAddr + , bench "Preallocated" $ whnf IPv4Text1.encode ipAddr + -- , bench "Variable Builder" $ whnf IPv4TextVariableBuilder.encode ipAddr + ] + , bgroup + "IPv4 from Text" + [ bench "Naive" $ whnf Naive.decodeText ipText + , bench "Attoparsec" $ whnf IPv4DecodeText2.decodeText ipText + , bench "Text Reader" $ whnf IPv4DecodeText1.decodeText ipText + ] + , bgroup + "IPv4 from Bytes" + [ bench "Current" $ whnf IPv4.decodeUtf8Bytes ipBytes + ] + , bgroup + "IPv4 to ByteString" + [ bench "Naive" $ whnf Naive.encodeByteString ipAddr + , bench "Preallocated: No Lookup Tables" $ whnf IPv4ByteString1.encode ipAddr + , bench "Preallocated" $ whnf IPv4.encodeUtf8 ipAddr + ] + , bgroup + "IPv6 from Text" + [ bench "::" $ whnf IPv6.decode ip6Text + , bench "1:2:3:4:5:6:7:8" $ whnf IPv6.decode ip6TextBigger + , bench "1:2::7:8" $ whnf IPv6.decode ip6TextSkip + , bench "a:b::c:d" $ whnf IPv6.decode ip6TextHex + ] + , bgroup + "IPv6 bytesmith" + [ bench "1:2:3:4:5:6:7:8" $ whnf IPv6.decodeUtf8Bytes ip6BytesBigger + , bench "2001:db8:ba1:0:aaaa:542c:bb:cc00" $ whnf IPv6.decodeUtf8Bytes ip6ComplicatedBytes + ] + , bgroup + "IPv6 to Text" + [ bench "::" $ whnf IPv6.encode ip6 + , bench "1:2:3:4:5:6:7:8" $ whnf IPv6.encode ip6Bigger + , bench "1:2::7:8" $ whnf IPv6.encode ip6Skip + , bench "a:b::c:d" $ whnf IPv6.encode ip6Hex + ] + , bgroup + "IPv6 to ShortText" + [ bench "1:2:3:4:5:6:7:8" $ whnf IPv6.encodeShort ip6Bigger + , bench "1:2::7:8" $ whnf IPv6.encodeShort ip6Skip + , bench "a:b::c:d" $ whnf IPv6.encodeShort ip6Hex + , bench "2001:db8:ba1:0:aaaa:542c:bb:cc00" $ whnf IPv6.encodeShort ip6Complicated + ] + , bgroup + "CIDR Inclusion" + [ bench "reserved" $ whnf manyReserved hundredAddrs + , bench "private" $ whnf manyPrivate hundredAddrs + ] ] manyReserved :: PrimArray IPv4 -> Int -{-# noinline manyReserved #-} +{-# NOINLINE manyReserved #-} manyReserved x = foldlPrimArray' (\acc addr -> bool 0 1 (IPv4.reserved addr) + acc) 0 x manyPrivate :: PrimArray IPv4 -> Int -{-# noinline manyPrivate #-} +{-# NOINLINE manyPrivate #-} manyPrivate x = foldlPrimArray' (\acc addr -> bool 0 1 (IPv4.private addr) + acc) 0 x diff --git a/test/Doctests.hs b/test/Doctests.hs index 313254e..481b1a8 100644 --- a/test/Doctests.hs +++ b/test/Doctests.hs @@ -1,14 +1,15 @@ import Test.DocTest main :: IO () -main = doctest - [ "src/Net/IPv4.hs" - , "src/Net/IPv6.hs" - , "src/Net/IP.hs" - , "src/Data/Word/Synthetic/Word12.hs" - , "src/Data/Text/Builder/Common/Internal.hs" - , "src/Data/Text/Builder/Fixed.hs" - , "src/Data/ByteString/Builder/Fixed.hs" - , "src/Net/Mac.hs" - , "-XCPP" - ] +main = + doctest + [ "src/Net/IPv4.hs" + , "src/Net/IPv6.hs" + , "src/Net/IP.hs" + , "src/Data/Word/Synthetic/Word12.hs" + , "src/Data/Text/Builder/Common/Internal.hs" + , "src/Data/Text/Builder/Fixed.hs" + , "src/Data/ByteString/Builder/Fixed.hs" + , "src/Net/Mac.hs" + , "-XCPP" + ] diff --git a/test/IPv4ByteString1.hs b/test/IPv4ByteString1.hs index 2f6b96d..af0d59b 100644 --- a/test/IPv4ByteString1.hs +++ b/test/IPv4ByteString1.hs @@ -1,43 +1,48 @@ module IPv4ByteString1 where -import Net.Types (IPv4(..)) +import Net.Types (IPv4 (..)) -import Data.ByteString.Internal as I import Data.Bits +import Data.ByteString.Internal as I +import Data.Word import Foreign.Ptr import Foreign.Storable -import Data.Word encode :: IPv4 -> ByteString -encode (IPv4 w) = I.unsafeCreateUptoN 15 (\ptr1 -> - do (len1,ptr2) <- writeWord ptr1 w1 - poke ptr2 dot - (len2,ptr3) <- writeWord (ptr2 `plusPtr` 1) w2 - poke ptr3 dot - (len3,ptr4) <- writeWord (ptr3 `plusPtr` 1) w3 - poke ptr4 dot - (len4,_) <- writeWord (ptr4 `plusPtr` 1) w4 - return (3 + len1 + len2 + len3 + len4)) - where w1 = fromIntegral $ shiftR w 24 - w2 = fromIntegral $ shiftR w 16 - w3 = fromIntegral $ shiftR w 8 - w4 = fromIntegral w - dot = 46 - writeWord :: Ptr Word8 -> Word8 -> IO (Int,Ptr Word8) - writeWord ptr word - | word >= 100 = do - let (word1,char3) = word `quotRem` 10 - (char1,char2) = word1 `quotRem` 10 - poke ptr (char1 + 48) - poke (ptr `plusPtr` 1) (char2 + 48) - poke (ptr `plusPtr` 2) (char3 + 48) - return (3,ptr `plusPtr` 3) - | word >= 10 = do - let (char1,char2) = word `quotRem` 10 - poke ptr (char1 + 48) - poke (ptr `plusPtr` 1) (char2 + 48) - return (2,ptr `plusPtr` 2) - | otherwise = do - poke ptr (word + 48) - return (1,ptr `plusPtr` 1) - +encode (IPv4 w) = + I.unsafeCreateUptoN + 15 + ( \ptr1 -> + do + (len1, ptr2) <- writeWord ptr1 w1 + poke ptr2 dot + (len2, ptr3) <- writeWord (ptr2 `plusPtr` 1) w2 + poke ptr3 dot + (len3, ptr4) <- writeWord (ptr3 `plusPtr` 1) w3 + poke ptr4 dot + (len4, _) <- writeWord (ptr4 `plusPtr` 1) w4 + return (3 + len1 + len2 + len3 + len4) + ) + where + w1 = fromIntegral $ shiftR w 24 + w2 = fromIntegral $ shiftR w 16 + w3 = fromIntegral $ shiftR w 8 + w4 = fromIntegral w + dot = 46 + writeWord :: Ptr Word8 -> Word8 -> IO (Int, Ptr Word8) + writeWord ptr word + | word >= 100 = do + let (word1, char3) = word `quotRem` 10 + (char1, char2) = word1 `quotRem` 10 + poke ptr (char1 + 48) + poke (ptr `plusPtr` 1) (char2 + 48) + poke (ptr `plusPtr` 2) (char3 + 48) + return (3, ptr `plusPtr` 3) + | word >= 10 = do + let (char1, char2) = word `quotRem` 10 + poke ptr (char1 + 48) + poke (ptr `plusPtr` 1) (char2 + 48) + return (2, ptr `plusPtr` 2) + | otherwise = do + poke ptr (word + 48) + return (1, ptr `plusPtr` 1) diff --git a/test/IPv4DecodeText1.hs b/test/IPv4DecodeText1.hs index 26fdc3c..228b7fb 100644 --- a/test/IPv4DecodeText1.hs +++ b/test/IPv4DecodeText1.hs @@ -1,30 +1,31 @@ module IPv4DecodeText1 where -import Net.Types -import Data.Word -import Data.Text.Internal (Text(..)) import Control.Monad -import Data.Bits ((.|.),shiftL) -import qualified Data.Text as Text -import qualified Data.Text.Read as TextRead +import Data.Bits (shiftL, (.|.)) +import qualified Data.Text as Text +import Data.Text.Internal (Text (..)) +import qualified Data.Text.Read as TextRead +import Data.Word +import Net.Types stripDecimal :: Text -> Either String Text stripDecimal t = case Text.uncons t of Nothing -> Left "expected a dot but input ended instead" - Just (c,tnext) -> if c == '.' - then Right tnext - else Left "expected a dot but found a different character" + Just (c, tnext) -> + if c == '.' + then Right tnext + else Left "expected a dot but found a different character" {-# INLINE stripDecimal #-} decodeIPv4TextEither :: Text -> Either String Word32 decodeIPv4TextEither t1' = do - (a,t2) <- TextRead.decimal t1' + (a, t2) <- TextRead.decimal t1' t2' <- stripDecimal t2 - (b,t3) <- TextRead.decimal t2' + (b, t3) <- TextRead.decimal t2' t3' <- stripDecimal t3 - (c,t4) <- TextRead.decimal t3' + (c, t4) <- TextRead.decimal t3' t4' <- stripDecimal t4 - (d,t5) <- TextRead.decimal t4' + (d, t5) <- TextRead.decimal t4' when (not (Text.null t5)) (Left "expected end of text but it continued instead") if a > 255 || b > 255 || c > 255 || d > 255 then Left ipOctetSizeErrorMsg @@ -40,8 +41,8 @@ ipOctetSizeErrorMsg = "All octets in an IPv4 address must be between 0 and 255" fromOctets' :: Word32 -> Word32 -> Word32 -> Word32 -> Word32 fromOctets' a b c d = - ( shiftL a 24 - .|. shiftL b 16 - .|. shiftL c 8 - .|. d - ) + ( shiftL a 24 + .|. shiftL b 16 + .|. shiftL c 8 + .|. d + ) diff --git a/test/IPv4DecodeText2.hs b/test/IPv4DecodeText2.hs index 901d899..09cc07a 100644 --- a/test/IPv4DecodeText2.hs +++ b/test/IPv4DecodeText2.hs @@ -1,21 +1,22 @@ module IPv4DecodeText2 where -import Net.Types +import qualified Data.Attoparsec.Text as AT +import Data.Bits (shiftL, (.|.)) +import Data.Text.Internal (Text (..)) import Data.Word -import Data.Bits (shiftL,(.|.)) -import Data.Text.Internal (Text(..)) -import qualified Data.Attoparsec.Text as AT +import Net.Types dotDecimalParser :: AT.Parser Word32 -dotDecimalParser = fromOctets' - <$> (AT.decimal >>= limitSize) - <* AT.char '.' - <*> (AT.decimal >>= limitSize) - <* AT.char '.' - <*> (AT.decimal >>= limitSize) - <* AT.char '.' - <*> (AT.decimal >>= limitSize) - where +dotDecimalParser = + fromOctets' + <$> (AT.decimal >>= limitSize) + <* AT.char '.' + <*> (AT.decimal >>= limitSize) + <* AT.char '.' + <*> (AT.decimal >>= limitSize) + <* AT.char '.' + <*> (AT.decimal >>= limitSize) + where limitSize i = if i > 255 then fail ipOctetSizeErrorMsg @@ -31,8 +32,8 @@ ipOctetSizeErrorMsg = "All octets in an IPv4 address must be between 0 and 255" fromOctets' :: Word32 -> Word32 -> Word32 -> Word32 -> Word32 fromOctets' a b c d = - ( shiftL a 24 - .|. shiftL b 16 - .|. shiftL c 8 - .|. d - ) + ( shiftL a 24 + .|. shiftL b 16 + .|. shiftL c 8 + .|. d + ) diff --git a/test/IPv4Text1.hs b/test/IPv4Text1.hs index dcca32c..73d5687 100644 --- a/test/IPv4Text1.hs +++ b/test/IPv4Text1.hs @@ -1,17 +1,18 @@ {-# LANGUAGE CPP #-} + module IPv4Text1 where -import Net.Types (IPv4(..)) -import Data.Text (Text) -import Data.Text.Internal (Text(..)) -import Data.Word -import Data.ByteString (ByteString) import Control.Monad.ST -import Data.Bits (shiftR,(.&.)) -import qualified Data.ByteString.Char8 as BC8 -import qualified Data.ByteString as ByteString +import Data.Bits (shiftR, (.&.)) +import Data.ByteString (ByteString) +import qualified Data.ByteString as ByteString +import qualified Data.ByteString.Char8 as BC8 import qualified Data.ByteString.Unsafe as ByteString -import qualified Data.Text.Array as TArray +import Data.Text (Text) +import qualified Data.Text.Array as TArray +import Data.Text.Internal (Text (..)) +import Data.Word +import Net.Types (IPv4 (..)) ------------------------ -- This implementation operates directly on @@ -27,7 +28,7 @@ encode (IPv4 w) = w2 = fromIntegral $ 255 .&. shiftR w 16 w3 = fromIntegral $ 255 .&. shiftR w 8 w4 = fromIntegral $ 255 .&. w - (arr,len) = runST $ do + (arr, len) = runST $ do marr <- TArray.new 15 i1 <- putAndCount 0 w1 marr let n1 = i1 @@ -43,15 +44,15 @@ encode (IPv4 w) = TArray.unsafeWrite marr n3 dot i4 <- putAndCount n3' w4 marr theArr <- TArray.unsafeFreeze marr - return (theArr,i4 + n3') - in Text arr 0 len + return (theArr, i4 + n3') + in Text arr 0 len putAndCount :: Int -> Word8 -> TArray.MArray s -> ST s Int putAndCount pos w marr | w < 10 = TArray.unsafeWrite marr pos (i2w w) >> return 1 | w < 100 = write2 pos w >> return 2 | otherwise = write3 pos w >> return 3 - where + where write2 off i0 = do let i = fromIntegral i0; j = i + i TArray.unsafeWrite marr off $ get2 j @@ -72,7 +73,7 @@ type Codepoint = Word8 type Codepoint = Word16 #endif -zero,dot :: Codepoint +zero, dot :: Codepoint zero = 48 {-# INLINE zero #-} dot = 46 @@ -83,6 +84,7 @@ i2w v = zero + fromIntegral v {-# INLINE i2w #-} -- Note: these double backslashes are need here because CPP is enabled. +{- FOURMOLU_DISABLE -} twoDigits :: ByteString twoDigits = BC8.pack "0001020304050607080910111213141516171819\\ @@ -106,4 +108,4 @@ threeDigits = \217218219220221222223224225226227228229\\ \230231232233234235236237238239240241242\\ \243244245246247248249250251252253254255" - +{- FOURMOLU_ENABLE -} diff --git a/test/IPv4Text2.hs b/test/IPv4Text2.hs index aeed326..c8ae514 100644 --- a/test/IPv4Text2.hs +++ b/test/IPv4Text2.hs @@ -1,11 +1,11 @@ module IPv4Text2 where -import Net.Types (IPv4(..)) +import Data.Bits (shiftR, (.&.)) import Data.Text (Text) -import Data.Bits ((.&.),shiftR) -import Data.Text.Lazy.Builder.Int (decimal) -import qualified Data.Text.Lazy as LText +import qualified Data.Text.Lazy as LText import qualified Data.Text.Lazy.Builder as TBuilder +import Data.Text.Lazy.Builder.Int (decimal) +import Net.Types (IPv4 (..)) ----------------------------------------- -- Text Builder implementation. This ends @@ -17,13 +17,13 @@ encode = LText.toStrict . TBuilder.toLazyText . toDotDecimalBuilder toDotDecimalBuilder :: IPv4 -> TBuilder.Builder toDotDecimalBuilder (IPv4 w) = - decimal (255 .&. shiftR w 24 ) - <> dot - <> decimal (255 .&. shiftR w 16 ) - <> dot - <> decimal (255 .&. shiftR w 8 ) - <> dot - <> decimal (255 .&. w) - where dot = TBuilder.singleton '.' + decimal (255 .&. shiftR w 24) + <> dot + <> decimal (255 .&. shiftR w 16) + <> dot + <> decimal (255 .&. shiftR w 8) + <> dot + <> decimal (255 .&. w) + where + dot = TBuilder.singleton '.' {-# INLINE toDotDecimalBuilder #-} - diff --git a/test/IPv4TextVariableBuilder.hs b/test/IPv4TextVariableBuilder.hs index 8a580ed..3bd5408 100644 --- a/test/IPv4TextVariableBuilder.hs +++ b/test/IPv4TextVariableBuilder.hs @@ -1,13 +1,13 @@ module IPv4TextVariableBuilder where -import Net.Types (IPv4(..)) -import Data.Text (Text) -import Data.Monoid -import Data.Word import Data.Bits -import qualified Net.IPv4 as IPv4 +import Data.Monoid +import Data.Text (Text) import qualified Data.Text as Text import qualified Data.Text.Builder.Variable as VB +import Data.Word +import qualified Net.IPv4 as IPv4 +import Net.Types (IPv4 (..)) encode :: IPv4 -> Text encode = VB.run variableBuilder @@ -15,16 +15,13 @@ encode = VB.run variableBuilder variableBuilder :: VB.Builder IPv4 variableBuilder = VB.contramap (word8At 24) VB.word8 - <> VB.staticCharBmp '.' - <> VB.contramap (word8At 16) VB.word8 - <> VB.staticCharBmp '.' - <> VB.contramap (word8At 8) VB.word8 - <> VB.staticCharBmp '.' - <> VB.contramap (word8At 0) VB.word8 + <> VB.staticCharBmp '.' + <> VB.contramap (word8At 16) VB.word8 + <> VB.staticCharBmp '.' + <> VB.contramap (word8At 8) VB.word8 + <> VB.staticCharBmp '.' + <> VB.contramap (word8At 0) VB.word8 word8At :: Int -> IPv4 -> Word8 word8At i (IPv4 w) = fromIntegral (unsafeShiftR w i) {-# INLINE word8At #-} - - - diff --git a/test/Naive.hs b/test/Naive.hs index 0d93223..dc71ad5 100644 --- a/test/Naive.hs +++ b/test/Naive.hs @@ -1,31 +1,33 @@ module Naive where -import Net.Types (IPv4(..)) +import Data.ByteString (ByteString) import Data.Text (Text) -import qualified Net.IPv4 as IPv4 import qualified Data.Text as Text -import Text.Read (readMaybe) -import Data.ByteString (ByteString) import Data.Text.Encoding (encodeUtf8) +import qualified Net.IPv4 as IPv4 +import Net.Types (IPv4 (..)) +import Text.Read (readMaybe) encodeByteString :: IPv4 -> ByteString encodeByteString = encodeUtf8 . encodeText encodeText :: IPv4 -> Text -encodeText i = Text.pack $ concat - [ show a - , "." - , show b - , "." - , show c - , "." - , show d - ] - where (a,b,c,d) = IPv4.toOctets i +encodeText i = + Text.pack $ + concat + [ show a + , "." + , show b + , "." + , show c + , "." + , show d + ] + where + (a, b, c, d) = IPv4.toOctets i decodeText :: Text -> Maybe IPv4 decodeText t = case mapM (readMaybe . Text.unpack) (Text.splitOn (Text.pack ".") t) of - Just [a,b,c,d] -> Just (IPv4.fromOctets a b c d) + Just [a, b, c, d] -> Just (IPv4.fromOctets a b c d) _ -> Nothing - diff --git a/test/Net/IPv4Spec.hs b/test/Net/IPv4Spec.hs index 7e041c1..daea8b0 100644 --- a/test/Net/IPv4Spec.hs +++ b/test/Net/IPv4Spec.hs @@ -1,68 +1,99 @@ {-# OPTIONS_GHC -Wno-deprecations #-} + module Net.IPv4Spec (spec) where -import Prelude hiding (any) + import Data.Bits import Net.IPv4 import Test.Hspec +import Prelude hiding (any) spec :: Spec spec = do - describe "Bits" $ do - it ".&." $ do - any .&. any `shouldBe` any - any .&. loopback `shouldBe` any - loopback .&. broadcast `shouldBe` loopback - broadcast .&. broadcast `shouldBe` broadcast - it ".|." $ do - any .|. any `shouldBe` any - any .|. loopback `shouldBe` loopback - loopback .|. broadcast `shouldBe` broadcast - broadcast .|. broadcast `shouldBe` broadcast - it "xor" $ do - any `xor` any `shouldBe` any - any `xor` loopback `shouldBe` loopback - loopback `xor` broadcast `shouldBe` complement loopback - broadcast `xor` broadcast `shouldBe` any - it "complement" $ do - complement any `shouldBe` broadcast - complement loopback `shouldBe` ipv4 128 255 255 254 - complement broadcast `shouldBe` any - it "shift" $ do - shift any 0 `shouldBe` any - shift broadcast 0 `shouldBe` broadcast - shift broadcast 8 `shouldBe` ipv4 255 255 255 0 - shift broadcast (-8) `shouldBe` ipv4 0 255 255 255 - shift broadcast 32 `shouldBe` any - shift broadcast 40 `shouldBe` any - it "rotate" $ do - rotate loopback 0 `shouldBe` loopback - rotate loopback 0 `shouldBe` loopback - rotate loopback 8 `shouldBe` ipv4 0 0 1 127 - rotate loopback (-8) `shouldBe` ipv4 1 127 0 0 - rotate loopback 32 `shouldBe` loopback - it "bitSize" $ do - bitSize any `shouldBe` 32 - it "bitSizeMaybe" $ do - bitSizeMaybe any `shouldBe` Just 32 - it "isSigned" $ do - isSigned any `shouldBe` False - isSigned broadcast `shouldBe` False - it "testBit" $ do - testBit loopback <$> [0..31] `shouldBe` - [ True, False, False, False, False, False, False, False - , False, False, False, False, False, False, False, False - , False, False, False, False, False, False, False, False - , True, True, True, True, True, True, True, False ] - it "bit" $ do - bit 0 `shouldBe` ipv4 0 0 0 1 - bit 1 `shouldBe` ipv4 0 0 0 2 - bit 31 `shouldBe` ipv4 128 0 0 0 - it "popCount" $ do - popCount any `shouldBe` 0 - popCount loopback `shouldBe` 8 - popCount broadcast `shouldBe` 32 - describe "FiniteBits" $ do - it "finiteBitSize" $ do - finiteBitSize any `shouldBe` 32 - finiteBitSize loopback `shouldBe` 32 - finiteBitSize broadcast `shouldBe` 32 + describe "Bits" $ do + it ".&." $ do + any .&. any `shouldBe` any + any .&. loopback `shouldBe` any + loopback .&. broadcast `shouldBe` loopback + broadcast .&. broadcast `shouldBe` broadcast + it ".|." $ do + any .|. any `shouldBe` any + any .|. loopback `shouldBe` loopback + loopback .|. broadcast `shouldBe` broadcast + broadcast .|. broadcast `shouldBe` broadcast + it "xor" $ do + any `xor` any `shouldBe` any + any `xor` loopback `shouldBe` loopback + loopback `xor` broadcast `shouldBe` complement loopback + broadcast `xor` broadcast `shouldBe` any + it "complement" $ do + complement any `shouldBe` broadcast + complement loopback `shouldBe` ipv4 128 255 255 254 + complement broadcast `shouldBe` any + it "shift" $ do + shift any 0 `shouldBe` any + shift broadcast 0 `shouldBe` broadcast + shift broadcast 8 `shouldBe` ipv4 255 255 255 0 + shift broadcast (-8) `shouldBe` ipv4 0 255 255 255 + shift broadcast 32 `shouldBe` any + shift broadcast 40 `shouldBe` any + it "rotate" $ do + rotate loopback 0 `shouldBe` loopback + rotate loopback 0 `shouldBe` loopback + rotate loopback 8 `shouldBe` ipv4 0 0 1 127 + rotate loopback (-8) `shouldBe` ipv4 1 127 0 0 + rotate loopback 32 `shouldBe` loopback + it "bitSize" $ do + bitSize any `shouldBe` 32 + it "bitSizeMaybe" $ do + bitSizeMaybe any `shouldBe` Just 32 + it "isSigned" $ do + isSigned any `shouldBe` False + isSigned broadcast `shouldBe` False + it "testBit" $ do + testBit loopback <$> [0 .. 31] + `shouldBe` [ True + , False + , False + , False + , False + , False + , False + , False + , False + , False + , False + , False + , False + , False + , False + , False + , False + , False + , False + , False + , False + , False + , False + , False + , True + , True + , True + , True + , True + , True + , True + , False + ] + it "bit" $ do + bit 0 `shouldBe` ipv4 0 0 0 1 + bit 1 `shouldBe` ipv4 0 0 0 2 + bit 31 `shouldBe` ipv4 128 0 0 0 + it "popCount" $ do + popCount any `shouldBe` 0 + popCount loopback `shouldBe` 8 + popCount broadcast `shouldBe` 32 + describe "FiniteBits" $ do + it "finiteBitSize" $ do + finiteBitSize any `shouldBe` 32 + finiteBitSize loopback `shouldBe` 32 + finiteBitSize broadcast `shouldBe` 32 diff --git a/test/Test.hs b/test/Test.hs index 05cb5bf..004d82b 100644 --- a/test/Test.hs +++ b/test/Test.hs @@ -1,242 +1,311 @@ -{-# LANGUAGE StandaloneDeriving #-} +{-# LANGUAGE CPP #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} - +{-# LANGUAGE StandaloneDeriving #-} {-# OPTIONS_GHC -Wall -fno-warn-orphans #-} module Main (main) where -import Naive +#if MIN_VERSION_base(4,18,0) +#else import Control.Applicative (liftA2) +#endif + +import Data.Bifunctor import Data.Bytes (Bytes) -import Data.Proxy (Proxy(..)) -import Test.Tasty (defaultMain, testGroup, TestTree) -import Test.Tasty.QuickCheck (testProperty) -import Test.QuickCheck (Arbitrary(..),oneof,Gen,elements,choose,(===)) -import Test.HUnit (Assertion,(@?=),(@=?)) +import Data.Proxy (Proxy (..)) +import Naive import Numeric (showHex) -import Test.QuickCheck.Property (failed,succeeded,Result(..)) -import Data.Bifunctor -import Test.QuickCheck.Classes (Laws(..),jsonLaws,showReadLaws,primLaws,boundedEnumLaws,bitsLaws) +import Test.HUnit (Assertion, (@=?), (@?=)) +import Test.QuickCheck (Arbitrary (..), Gen, choose, elements, oneof, (===)) +import Test.QuickCheck.Classes (Laws (..), bitsLaws, boundedEnumLaws, jsonLaws, primLaws, showReadLaws) +import Test.QuickCheck.Property (Result (..), failed, succeeded) +import Test.Tasty (TestTree, defaultMain, testGroup) import qualified Test.Tasty.HUnit as PH +import Test.Tasty.QuickCheck (testProperty) -import Net.Types (IP,IPv4(..),IPv4Range(..),Mac(..),IPv6(..),MacGrouping(..),MacCodec(..),IPv6Range(..)) -import Data.WideWord (Word128(..)) +import qualified Data.ByteString.Char8 as BC8 import qualified Data.Bytes.Text.Ascii as Ascii import qualified Data.Text as Text import qualified Data.Text.Short as TS -import qualified Data.ByteString.Char8 as BC8 +import Data.WideWord (Word128 (..)) +import qualified Net.IP as IP import qualified Net.IPv4 as IPv4 import qualified Net.IPv6 as IPv6 import qualified Net.Mac as Mac -import qualified Net.IP as IP +import Net.Types (IP, IPv4 (..), IPv4Range (..), IPv6 (..), IPv6Range (..), Mac (..), MacCodec (..), MacGrouping (..)) -import qualified Data.Attoparsec.Text as AT import qualified Data.Attoparsec.ByteString as AB +import qualified Data.Attoparsec.Text as AT +import qualified IPv4ByteString1 import qualified IPv4Text1 import qualified IPv4Text2 -import qualified IPv4ByteString1 + -- import qualified IPv4TextVariableBuilder main :: IO () main = defaultMain tests tests :: TestTree -tests = testGroup "tests" - [ testGroup "Encoding and Decoding" - [ testGroup "Currently used IPv4 encode/decode" $ - [ testProperty "Isomorphism" - $ propEncodeDecodeIso IPv4.encode IPv4.decode - , PH.testCase "Decode an IP" testIPv4Decode - ] ++ testDecodeFailures - , testGroup "Currently used IPv4 encodeShort/decodeShort" $ - [ testProperty "Isomorphism" - $ propEncodeDecodeIso IPv4.encodeShort IPv4.decodeShort - ] ++ testDecodeFailures - , testGroup "Currently used IPv4 UTF-8 Bytes decode" - [ testProperty "Isomorphism" - $ propEncodeDecodeIso (byteStringToBytes . IPv4.encodeUtf8) IPv4.decodeUtf8Bytes - , PH.testCase "Encode a MAC Address" testMacEncode - ] - , testGroup "Currently used MAC Text encode/decode" - [ testProperty "Isomorphism" - $ propEncodeDecodeIsoSettings Mac.encodeWith Mac.decodeWith - , PH.testCase "Encode a MAC Address" testMacEncode - ] - , testGroup "Currently used MAC ByteString encode/decode" - [ testProperty "Isomorphism" - $ propEncodeDecodeIsoSettings Mac.encodeWithUtf8 Mac.decodeWithUtf8 - , PH.testCase "Lenient Decoding" testLenientMacByteStringParser - ] - , testGroup "Naive IPv4 encode/decode" - [ testProperty "Isomorphism" - $ propEncodeDecodeIso Naive.encodeText Naive.decodeText - ] - , testGroup "Text Builder IPv4 Text encode/decode" - [ testProperty "Identical to Naive" - $ propMatching IPv4Text2.encode Naive.encodeText - ] - -- , testGroup "Variable Text Builder IPv4 Text encode/decode" - -- [ testProperty "Identical to Naive" - -- $ propMatching IPv4TextVariableBuilder.encode Naive.encodeText - -- ] - , testGroup "Raw byte array IPv4 Text encode/decode" - [ testProperty "Identical to Naive" - $ propMatching IPv4Text1.encode Naive.encodeText - ] - , testGroup "Raw byte array (without lookup table) IPv4 ByteString encode/decode" - [ testProperty "Identical to Naive" - $ propMatching IPv4ByteString1.encode Naive.encodeByteString - ] - , testGroup "Raw byte array (with lookup table) IPv4 ByteString encode/decode" - [ testProperty "Identical to Naive" - $ propMatching IPv4.encodeUtf8 Naive.encodeByteString - ] - , testGroup "IPv4 encode/decode" - [ PH.testCase "Parser Test Cases" testIPv4Parser - ] - , testGroup "IPv6 encode/decode" - [ PH.testCase "Parser Test Cases" $ testIPv6Parser $ \str -> - either (\_ -> Nothing) (Just . HexIPv6) - (AT.parseOnly - (IPv6.parser <* AT.endOfInput) - (Text.pack str) - ) - , PH.testCase "Bytes Parser Test Cases" $ testIPv6Parser $ \str -> - fmap HexIPv6 (IPv6.decodeUtf8Bytes (Ascii.fromString str)) - , PH.testCase "Encode test cases" (testIPv6Encode IPv6.encode) - , PH.testCase "Encode ShortText" (testIPv6Encode (TS.toText . IPv6.encodeShort)) - , PH.testCase "Parser Failure Test Cases" - (testIPv6ParserFailure expectIPv6ParserFailure) - , PH.testCase "Bytes Parser Failure Test Cases" - (testIPv6ParserFailure expectIPv6BytesParserFailure) - ] +tests = + testGroup + "tests" + [ testGroup + "Encoding and Decoding" + [ testGroup "Currently used IPv4 encode/decode" $ + [ testProperty "Isomorphism" $ + propEncodeDecodeIso IPv4.encode IPv4.decode + , PH.testCase "Decode an IP" testIPv4Decode + ] + ++ testDecodeFailures + , testGroup "Currently used IPv4 encodeShort/decodeShort" $ + [ testProperty "Isomorphism" $ + propEncodeDecodeIso IPv4.encodeShort IPv4.decodeShort + ] + ++ testDecodeFailures + , testGroup + "Currently used IPv4 UTF-8 Bytes decode" + [ testProperty "Isomorphism" $ + propEncodeDecodeIso (byteStringToBytes . IPv4.encodeUtf8) IPv4.decodeUtf8Bytes + , PH.testCase "Encode a MAC Address" testMacEncode + ] + , testGroup + "Currently used MAC Text encode/decode" + [ testProperty "Isomorphism" $ + propEncodeDecodeIsoSettings Mac.encodeWith Mac.decodeWith + , PH.testCase "Encode a MAC Address" testMacEncode + ] + , testGroup + "Currently used MAC ByteString encode/decode" + [ testProperty "Isomorphism" $ + propEncodeDecodeIsoSettings Mac.encodeWithUtf8 Mac.decodeWithUtf8 + , PH.testCase "Lenient Decoding" testLenientMacByteStringParser + ] + , testGroup + "Naive IPv4 encode/decode" + [ testProperty "Isomorphism" $ + propEncodeDecodeIso Naive.encodeText Naive.decodeText + ] + , testGroup + "Text Builder IPv4 Text encode/decode" + [ testProperty "Identical to Naive" $ + propMatching IPv4Text2.encode Naive.encodeText + ] + , -- , testGroup "Variable Text Builder IPv4 Text encode/decode" + -- [ testProperty "Identical to Naive" + -- $ propMatching IPv4TextVariableBuilder.encode Naive.encodeText + -- ] + testGroup + "Raw byte array IPv4 Text encode/decode" + [ testProperty "Identical to Naive" $ + propMatching IPv4Text1.encode Naive.encodeText + ] + , testGroup + "Raw byte array (without lookup table) IPv4 ByteString encode/decode" + [ testProperty "Identical to Naive" $ + propMatching IPv4ByteString1.encode Naive.encodeByteString + ] + , testGroup + "Raw byte array (with lookup table) IPv4 ByteString encode/decode" + [ testProperty "Identical to Naive" $ + propMatching IPv4.encodeUtf8 Naive.encodeByteString + ] + , testGroup + "IPv4 encode/decode" + [ PH.testCase "Parser Test Cases" testIPv4Parser + ] + , testGroup + "IPv6 encode/decode" + [ PH.testCase "Parser Test Cases" $ testIPv6Parser $ \str -> + either + (\_ -> Nothing) + (Just . HexIPv6) + ( AT.parseOnly + (IPv6.parser <* AT.endOfInput) + (Text.pack str) + ) + , PH.testCase "Bytes Parser Test Cases" $ testIPv6Parser $ \str -> + fmap HexIPv6 (IPv6.decodeUtf8Bytes (Ascii.fromString str)) + , PH.testCase "Encode test cases" (testIPv6Encode IPv6.encode) + , PH.testCase "Encode ShortText" (testIPv6Encode (TS.toText . IPv6.encodeShort)) + , PH.testCase + "Parser Failure Test Cases" + (testIPv6ParserFailure expectIPv6ParserFailure) + , PH.testCase + "Bytes Parser Failure Test Cases" + (testIPv6ParserFailure expectIPv6BytesParserFailure) + ] + ] + , testGroup + "IPv4 Range Operations" + [ testProperty "Idempotence of normalizing IPv4 range" $ + propIdempotence IPv4.normalize + , testProperty "Normalize does not affect membership" propNormalizeMember + , testProperty "Membership agrees with bounds" propMemberUpperLower + , testProperty "Range contains self" propRangeSelf + , testGroup + "reserved" + [ PH.testCase "A" $ IPv4.reserved (IPv4.ipv4 0 1 2 3) @=? True + , PH.testCase "B" $ IPv4.reserved (IPv4.ipv4 1 0 0 0) @=? False + , PH.testCase "C" $ IPv4.reserved (IPv4.ipv4 100 64 0 3) @=? True + , PH.testCase "D" $ IPv4.reserved (IPv4.ipv4 127 255 255 255) @=? True + , PH.testCase "E" $ IPv4.reserved (IPv4.ipv4 110 0 0 255) @=? False + , PH.testCase "F" $ IPv4.reserved (IPv4.ipv4 192 0 2 255) @=? True + , PH.testCase "G" $ IPv4.reserved (IPv4.ipv4 203 0 113 0) @=? True + , PH.testCase "H" $ IPv4.reserved (IPv4.ipv4 225 0 0 0) @=? True + , PH.testCase "I" $ IPv4.reserved (IPv4.ipv4 226 0 0 0) @=? True + , PH.testCase "J" $ IPv4.reserved (IPv4.ipv4 255 255 255 254) @=? True + , PH.testCase "K" $ IPv4.reserved (IPv4.ipv4 255 255 255 255) @=? True + , PH.testCase "L" $ IPv4.reserved (IPv4.ipv4 224 0 0 0) @=? True + , PH.testCase "M" $ IPv4.reserved (IPv4.ipv4 239 255 255 255) @=? True + , PH.testCase "N" $ IPv4.reserved (IPv4.ipv4 223 255 255 255) @=? False + , PH.testCase "O" $ IPv4.reserved (IPv4.ipv4 203 0 114 0) @=? False + , PH.testCase "P" $ IPv4.reserved (IPv4.ipv4 203 0 112 255) @=? False + , PH.testCase "Q" $ IPv4.reserved (IPv4.ipv4 203 0 113 255) @=? True + , PH.testCase "R" $ IPv4.reserved (IPv4.ipv4 192 88 100 0) @=? False + , PH.testCase "S" $ IPv4.reserved (IPv4.ipv4 192 88 99 0) @=? True + , PH.testCase "T" $ IPv4.reserved (IPv4.ipv4 192 0 1 0) @=? False + ] + , testGroup + "private" + [ PH.testCase "A" $ IPv4.private (IPv4.ipv4 198 73 8 38) @=? False + , PH.testCase "B" $ IPv4.private (IPv4.ipv4 192 168 100 5) @=? True + , PH.testCase "C" $ IPv4.private (IPv4.ipv4 10 0 0 0) @=? True + , PH.testCase "D" $ IPv4.private (IPv4.ipv4 10 255 255 255) @=? True + ] + ] + , testGroup + "IPv6 Range Operations" + [ testProperty "Idempotence of normalizing IPv6 range" $ + propIdempotence IPv6.normalize + , testProperty "Normalize does not affect membership" $ \i r -> + IPv6.member i r == IPv6.member i (IPv6.normalize r) + , testProperty "Membership agrees with bounds" $ \i r -> + (i >= IPv6.lowerInclusive r && i <= IPv6.upperInclusive r) == IPv6.member i r + , testProperty "Range contains self" $ \r -> + IPv6.member (ipv6RangeBase r) r == True + , testProperty "Idempotence of upperInclusive-lowerInclusive and fromBounds" $ \r -> + IPv6.fromBounds (IPv6.lowerInclusive r) (IPv6.upperInclusive r) === r + , testGroup + "Cases" + [ PH.testCase "A" $ + False + @=? IPv6.contains + (IPv6.range (IPv6.ipv6 0 0 0 1 0 0 0 0) 64) + (IPv6.ipv6 0 0 0 0 0 0 0 0) + , PH.testCase "B" $ + True + @=? IPv6.contains + (IPv6.range (IPv6.ipv6 0 0 0 0 0 0 0 0) 126) + (IPv6.ipv6 0 0 0 0 0 0 0 1) + , PH.testCase "C" $ + False + @=? IPv6.contains + (IPv6.range (IPv6.ipv6 0 0 0 0 0 0 0 0) 125) + (IPv6.ipv6 0 0 0 0 0 0 0 0xFFFF) + ] + ] + , testGroup + "Instances" + [ testGroup + "IPv4" + [ lawsToTest (jsonLaws (Proxy :: Proxy IPv4)) + , lawsToTest (showReadLaws (Proxy :: Proxy IPv4)) + , lawsToTest (bitsLaws (Proxy :: Proxy IPv4)) + ] + , testGroup + "IPv4Range" + [ lawsToTest (jsonLaws (Proxy :: Proxy IPv4Range)) + , lawsToTest (showReadLaws (Proxy :: Proxy IPv4Range)) + ] + , testGroup + "IPv6" + [ lawsToTest (jsonLaws (Proxy :: Proxy IPv6)) + , lawsToTest (showReadLaws (Proxy :: Proxy IPv6)) + , lawsToTest (primLaws (Proxy :: Proxy IPv6)) + , lawsToTest (boundedEnumLaws (Proxy :: Proxy IPv6)) + , lawsToTest (bitsLaws (Proxy :: Proxy IPv6)) + ] + , testGroup + "IPv6Range" + [ lawsToTest (jsonLaws (Proxy :: Proxy IPv6Range)) + , lawsToTest (showReadLaws (Proxy :: Proxy IPv6Range)) + ] + , testGroup + "IP" + [ lawsToTest (jsonLaws (Proxy :: Proxy IP)) + , lawsToTest (showReadLaws (Proxy :: Proxy IP)) + ] + , testGroup + "Mac" + [ lawsToTest (jsonLaws (Proxy :: Proxy Mac)) + , lawsToTest (showReadLaws (Proxy :: Proxy Mac)) + , lawsToTest (primLaws (Proxy :: Proxy Mac)) + ] + ] ] - , testGroup "IPv4 Range Operations" - [ testProperty "Idempotence of normalizing IPv4 range" - $ propIdempotence IPv4.normalize - , testProperty "Normalize does not affect membership" propNormalizeMember - , testProperty "Membership agrees with bounds" propMemberUpperLower - , testProperty "Range contains self" propRangeSelf - , testGroup "reserved" - [ PH.testCase "A" $ IPv4.reserved (IPv4.ipv4 0 1 2 3) @=? True - , PH.testCase "B" $ IPv4.reserved (IPv4.ipv4 1 0 0 0) @=? False - , PH.testCase "C" $ IPv4.reserved (IPv4.ipv4 100 64 0 3) @=? True - , PH.testCase "D" $ IPv4.reserved (IPv4.ipv4 127 255 255 255) @=? True - , PH.testCase "E" $ IPv4.reserved (IPv4.ipv4 110 0 0 255) @=? False - , PH.testCase "F" $ IPv4.reserved (IPv4.ipv4 192 0 2 255) @=? True - , PH.testCase "G" $ IPv4.reserved (IPv4.ipv4 203 0 113 0) @=? True - , PH.testCase "H" $ IPv4.reserved (IPv4.ipv4 225 0 0 0) @=? True - , PH.testCase "I" $ IPv4.reserved (IPv4.ipv4 226 0 0 0) @=? True - , PH.testCase "J" $ IPv4.reserved (IPv4.ipv4 255 255 255 254) @=? True - , PH.testCase "K" $ IPv4.reserved (IPv4.ipv4 255 255 255 255) @=? True - , PH.testCase "L" $ IPv4.reserved (IPv4.ipv4 224 0 0 0) @=? True - , PH.testCase "M" $ IPv4.reserved (IPv4.ipv4 239 255 255 255) @=? True - , PH.testCase "N" $ IPv4.reserved (IPv4.ipv4 223 255 255 255) @=? False - , PH.testCase "O" $ IPv4.reserved (IPv4.ipv4 203 0 114 0) @=? False - , PH.testCase "P" $ IPv4.reserved (IPv4.ipv4 203 0 112 255) @=? False - , PH.testCase "Q" $ IPv4.reserved (IPv4.ipv4 203 0 113 255) @=? True - , PH.testCase "R" $ IPv4.reserved (IPv4.ipv4 192 88 100 0) @=? False - , PH.testCase "S" $ IPv4.reserved (IPv4.ipv4 192 88 99 0) @=? True - , PH.testCase "T" $ IPv4.reserved (IPv4.ipv4 192 0 1 0) @=? False - ] - , testGroup "private" - [ PH.testCase "A" $ IPv4.private (IPv4.ipv4 198 73 8 38) @=? False - , PH.testCase "B" $ IPv4.private (IPv4.ipv4 192 168 100 5) @=? True - , PH.testCase "C" $ IPv4.private (IPv4.ipv4 10 0 0 0) @=? True - , PH.testCase "D" $ IPv4.private (IPv4.ipv4 10 255 255 255) @=? True - ] - ] - , testGroup "IPv6 Range Operations" - [ testProperty "Idempotence of normalizing IPv6 range" - $ propIdempotence IPv6.normalize - , testProperty "Normalize does not affect membership" $ \i r -> - IPv6.member i r == IPv6.member i (IPv6.normalize r) - , testProperty "Membership agrees with bounds" $ \i r -> - (i >= IPv6.lowerInclusive r && i <= IPv6.upperInclusive r) == IPv6.member i r - , testProperty "Range contains self" $ \r -> - IPv6.member (ipv6RangeBase r) r == True - , testProperty "Idempotence of upperInclusive-lowerInclusive and fromBounds" $ \r -> - IPv6.fromBounds (IPv6.lowerInclusive r) (IPv6.upperInclusive r) === r - , testGroup "Cases" - [ PH.testCase "A" $ False @=? IPv6.contains - (IPv6.range (IPv6.ipv6 0 0 0 1 0 0 0 0) 64) - (IPv6.ipv6 0 0 0 0 0 0 0 0) - , PH.testCase "B" $ True @=? IPv6.contains - (IPv6.range (IPv6.ipv6 0 0 0 0 0 0 0 0) 126) - (IPv6.ipv6 0 0 0 0 0 0 0 1) - , PH.testCase "C" $ False @=? IPv6.contains - (IPv6.range (IPv6.ipv6 0 0 0 0 0 0 0 0) 125) - (IPv6.ipv6 0 0 0 0 0 0 0 0xFFFF) - ] - ] - , testGroup "Instances" - [ testGroup "IPv4" - [ lawsToTest (jsonLaws (Proxy :: Proxy IPv4)) - , lawsToTest (showReadLaws (Proxy :: Proxy IPv4)) - , lawsToTest (bitsLaws (Proxy :: Proxy IPv4)) - ] - , testGroup "IPv4Range" - [ lawsToTest (jsonLaws (Proxy :: Proxy IPv4Range)) - , lawsToTest (showReadLaws (Proxy :: Proxy IPv4Range)) - ] - , testGroup "IPv6" - [ lawsToTest (jsonLaws (Proxy :: Proxy IPv6)) - , lawsToTest (showReadLaws (Proxy :: Proxy IPv6)) - , lawsToTest (primLaws (Proxy :: Proxy IPv6)) - , lawsToTest (boundedEnumLaws (Proxy :: Proxy IPv6)) - , lawsToTest (bitsLaws (Proxy :: Proxy IPv6)) - ] - , testGroup "IPv6Range" - [ lawsToTest (jsonLaws (Proxy :: Proxy IPv6Range)) - , lawsToTest (showReadLaws (Proxy :: Proxy IPv6Range)) - ] - , testGroup "IP" - [ lawsToTest (jsonLaws (Proxy :: Proxy IP)) - , lawsToTest (showReadLaws (Proxy :: Proxy IP)) - ] - , testGroup "Mac" - [ lawsToTest (jsonLaws (Proxy :: Proxy Mac)) - , lawsToTest (showReadLaws (Proxy :: Proxy Mac)) - , lawsToTest (primLaws (Proxy :: Proxy Mac)) - ] - ] - ] lawsToTest :: Laws -> TestTree lawsToTest (Laws name pairs) = testGroup name (map (uncurry testProperty) pairs) -propEncodeDecodeIso :: (Eq a, Show a, Show b) - => (a -> b) -> (b -> Maybe a) -> a -> Result +propEncodeDecodeIso :: + (Eq a, Show a, Show b) => + (a -> b) -> + (b -> Maybe a) -> + a -> + Result propEncodeDecodeIso f g a = let fa = f a gfa = g fa in if gfa == Just a then succeeded - else failure $ concat - [ "x: ", show a, "\n" - , "f(x): ", show fa, "\n" - , "g(f(x)): ", show gfa, "\n" - ] - -propEncodeDecodeIsoSettings :: (Eq a,Show a,Show b,Show e) - => (e -> a -> b) -> (e -> b -> Maybe a) -> e -> a -> Result + else + failure $ + concat + [ "x: " + , show a + , "\n" + , "f(x): " + , show fa + , "\n" + , "g(f(x)): " + , show gfa + , "\n" + ] + +propEncodeDecodeIsoSettings :: + (Eq a, Show a, Show b, Show e) => + (e -> a -> b) -> + (e -> b -> Maybe a) -> + e -> + a -> + Result propEncodeDecodeIsoSettings f g e a = let fa = f e a gfa = g e fa in if gfa == Just a then succeeded - else failure $ concat - [ "env: ", show e, "\n" - , "x: ", show a, "\n" - , "f(x): ", show fa, "\n" - , "g(f(x)): ", show gfa, "\n" - ] - -propMatching :: Eq b => (a -> b) -> (a -> b) -> a -> Bool + else + failure $ + concat + [ "env: " + , show e + , "\n" + , "x: " + , show a + , "\n" + , "f(x): " + , show fa + , "\n" + , "g(f(x)): " + , show gfa + , "\n" + ] + +propMatching :: (Eq b) => (a -> b) -> (a -> b) -> a -> Bool propMatching f g a = f a == g a -propIdempotence :: Eq a => (a -> a) -> a -> Bool +propIdempotence :: (Eq a) => (a -> a) -> a -> Bool propIdempotence f a = f a == f (f a) propNormalizeMember :: IPv4 -> IPv4Range -> Bool @@ -250,58 +319,134 @@ propRangeSelf :: IPv4Range -> Bool propRangeSelf r = IPv4.member (ipv4RangeBase r) r == True testIPv4Decode :: Assertion -testIPv4Decode = IPv4.decode (Text.pack "124.222.255.0") - @?= Just (IPv4.fromOctets 124 222 255 0) +testIPv4Decode = + IPv4.decode (Text.pack "124.222.255.0") + @?= Just (IPv4.fromOctets 124 222 255 0) testLenientMacByteStringParser :: Assertion testLenientMacByteStringParser = do - go 0xAB 0x12 0x0F 0x1C 0x88 0x79 - "AB:12:0F:1C:88:79" - go 0xAB 0x12 0x0F 0x0C 0xAA 0x76 - "AB1-20F-0CA-A76" - where + go + 0xAB + 0x12 + 0x0F + 0x1C + 0x88 + 0x79 + "AB:12:0F:1C:88:79" + go + 0xAB + 0x12 + 0x0F + 0x0C + 0xAA + 0x76 + "AB1-20F-0CA-A76" + where go a b c d e f str = Just (HexMac (Mac.fromOctets a b c d e f)) - @=? fmap HexMac (Mac.decodeUtf8 (BC8.pack str)) + @=? fmap HexMac (Mac.decodeUtf8 (BC8.pack str)) testIPv4Parser :: Assertion testIPv4Parser = do go 202 10 19 54 "202.10.19.54" go 10 202 96 25 "10.202.96.25" - where + where go a b c d str = Right (IPv4.fromOctets a b c d) - @=? (AB.parseOnly - (IPv4.parserUtf8 <* AT.endOfInput) - (BC8.pack str) - ) + @=? ( AB.parseOnly + (IPv4.parserUtf8 <* AT.endOfInput) + (BC8.pack str) + ) testIPv6Parser :: (String -> Maybe HexIPv6) -> Assertion testIPv6Parser decode = do -- Basic test - go 0xABCD 0x1234 0xABCD 0x1234 0xDCBA 0x4321 0xFFFF 0xE0E0 - "ABCD:1234:ABCD:1234:DCBA:4321:FFFF:E0E0" + go + 0xABCD + 0x1234 + 0xABCD + 0x1234 + 0xDCBA + 0x4321 + 0xFFFF + 0xE0E0 + "ABCD:1234:ABCD:1234:DCBA:4321:FFFF:E0E0" -- Tests that leading zeros can be omitted - go 0x1234 0x5678 0x9ABC 0xDEF0 0x0123 0x4567 0x89AB 0xCDEF - "1234:5678:9ABC:DEF0:123:4567:89AB:CDEF" + go + 0x1234 + 0x5678 + 0x9ABC + 0xDEF0 + 0x0123 + 0x4567 + 0x89AB + 0xCDEF + "1234:5678:9ABC:DEF0:123:4567:89AB:CDEF" -- Test that the IPv6 "any" abbreviation works - go 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 - "::" - go 0x1623 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 - "1623::" - go 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0xABCD 0x1234 - "::ABCD:1234" - go 0xAAAA 0x0000 0x0000 0x0000 0x0000 0x0000 0xABCD 0x1234 - "AAAA::ABCD:1234" - go 0xAAAA 0x0000 0x0000 0x0000 0xBBBB 0x0000 0xABCD 0x1234 - "AAAA::BBBB:0000:ABCD:1234" - go 0xAAAA 0x0000 0x0000 0x0000 0xBBBB 0x0000 0xABCD 0x1234 - "AAAA:0000:0000:0000:BBBB::ABCD:1234" - where + go + 0x0000 + 0x0000 + 0x0000 + 0x0000 + 0x0000 + 0x0000 + 0x0000 + 0x0000 + "::" + go + 0x1623 + 0x0000 + 0x0000 + 0x0000 + 0x0000 + 0x0000 + 0x0000 + 0x0000 + "1623::" + go + 0x0000 + 0x0000 + 0x0000 + 0x0000 + 0x0000 + 0x0000 + 0xABCD + 0x1234 + "::ABCD:1234" + go + 0xAAAA + 0x0000 + 0x0000 + 0x0000 + 0x0000 + 0x0000 + 0xABCD + 0x1234 + "AAAA::ABCD:1234" + go + 0xAAAA + 0x0000 + 0x0000 + 0x0000 + 0xBBBB + 0x0000 + 0xABCD + 0x1234 + "AAAA::BBBB:0000:ABCD:1234" + go + 0xAAAA + 0x0000 + 0x0000 + 0x0000 + 0xBBBB + 0x0000 + 0xABCD + 0x1234 + "AAAA:0000:0000:0000:BBBB::ABCD:1234" + where go a b c d e f g h str = Just (HexIPv6 (IPv6.fromWord16s a b c d e f g h)) - @=? - decode str + @=? decode str testIPv6ParserFailure :: (String -> Assertion) -> Assertion testIPv6ParserFailure go = do @@ -350,22 +495,21 @@ testIPv6ParserFailure go = do expectIPv6ParserFailure :: String -> Assertion expectIPv6ParserFailure str = Left () - @=? - bimap (\_ -> ()) HexIPv6 - (AT.parseOnly - (IPv6.parser <* AT.endOfInput) - (Text.pack str) - ) + @=? bimap + (\_ -> ()) + HexIPv6 + ( AT.parseOnly + (IPv6.parser <* AT.endOfInput) + (Text.pack str) + ) expectIPv6BytesParserFailure :: String -> Assertion expectIPv6BytesParserFailure s = Nothing - @=? - IPv6.decodeUtf8Bytes (Ascii.fromString s) + @=? IPv6.decodeUtf8Bytes (Ascii.fromString s) testIPv6Encode :: (IPv6 -> Text.Text) -> Assertion testIPv6Encode enc = do - -- degenerate cases: "::" `roundTripsTo` "::" "1234::" `roundTripsTo` "1234::" @@ -402,10 +546,9 @@ testIPv6Encode enc = do "::ffff:00ff:ff00" `roundTripsTo` "::ffff:0.255.255.0" "::ffff:203.0.113.17" `roundTripsTo` "::ffff:203.0.113.17" "1234:5678::10.0.1.2" `roundTripsTo` "1234:5678::a00:102" - where - roundTripsTo s sExpected = - case AT.parseOnly (IPv6.parser <* AT.endOfInput) (Text.pack s) of + roundTripsTo s sExpected = + case AT.parseOnly (IPv6.parser <* AT.endOfInput) (Text.pack s) of Right result -> enc result @?= Text.pack sExpected Left failMsg -> fail ("failed to parse '" ++ s ++ "': " ++ failMsg) @@ -427,98 +570,117 @@ testDecodeFailures = flip map textBadIPv4 $ \str -> PH.testCase ("Should fail to decode [" ++ str ++ "]") $ IPv4.decode (Text.pack str) @?= Nothing testMacEncode :: Assertion -testMacEncode = Mac.encode (Mac.fromOctets 0xFF 0x00 0xAB 0x12 0x99 0x0F) - @?= Text.pack "ff:00:ab:12:99:0f" +testMacEncode = + Mac.encode (Mac.fromOctets 0xFF 0x00 0xAB 0x12 0x99 0x0F) + @?= Text.pack "ff:00:ab:12:99:0f" failure :: String -> Result -failure msg = failed - { reason = msg - , theException = Nothing - } +failure msg = + failed + { reason = msg + , theException = Nothing + } newtype HexMac = HexMac Mac deriving (Eq) instance Show HexMac where showsPrec _ (HexMac v) = - let (a,b,c,d,e,f) = Mac.toOctets v - in showHex a . showChar ':' - . showHex b . showChar ':' - . showHex c . showChar ':' - . showHex d . showChar ':' - . showHex e . showChar ':' - . showHex f - + let (a, b, c, d, e, f) = Mac.toOctets v + in showHex a + . showChar ':' + . showHex b + . showChar ':' + . showHex c + . showChar ':' + . showHex d + . showChar ':' + . showHex e + . showChar ':' + . showHex f newtype HexIPv6 = HexIPv6 IPv6 deriving (Eq) instance Show HexIPv6 where showsPrec _ (HexIPv6 v) = - let (a,b,c,d,e,f,g,h) = IPv6.toWord16s v - in showHex a . showChar ':' - . showHex b . showChar ':' - . showHex c . showChar ':' - . showHex d . showChar ':' - . showHex e . showChar ':' - . showHex f . showChar ':' - . showHex g . showChar ':' - . showHex h - + let (a, b, c, d, e, f, g, h) = IPv6.toWord16s v + in showHex a + . showChar ':' + . showHex b + . showChar ':' + . showHex c + . showChar ':' + . showHex d + . showChar ':' + . showHex e + . showChar ':' + . showHex f + . showChar ':' + . showHex g + . showChar ':' + . showHex h deriving instance Arbitrary IPv4 instance Arbitrary Word128 where arbitrary = Word128 <$> arbitrary <*> arbitrary - shrink (Word128 a b) = filter (/= Word128 a b) - [ Word128 0 0 - , Word128 (div a 2) b - , Word128 a (div b 2) - ] + shrink (Word128 a b) = + filter + (/= Word128 a b) + [ Word128 0 0 + , Word128 (div a 2) b + , Word128 a (div b 2) + ] deriving instance Arbitrary IPv6 -- Half of the test cases generated are IPv6 mapped -- IPv4 addresses. instance Arbitrary IP where - arbitrary = oneof - [ IP.fromIPv4 <$> arbitrary - , IP.fromIPv6 <$> arbitrary - ] + arbitrary = + oneof + [ IP.fromIPv4 <$> arbitrary + , IP.fromIPv6 <$> arbitrary + ] instance Arbitrary Mac where - arbitrary = Mac.fromOctets - <$> arbitrary - <*> arbitrary - <*> arbitrary - <*> arbitrary - <*> arbitrary - <*> arbitrary + arbitrary = + Mac.fromOctets + <$> arbitrary + <*> arbitrary + <*> arbitrary + <*> arbitrary + <*> arbitrary + <*> arbitrary -- This instance can generate masks that exceed the recommended -- length of 32. instance Arbitrary IPv4Range where - arbitrary = IPv4.range <$> arbitrary <*> choose (0,32) + arbitrary = IPv4.range <$> arbitrary <*> choose (0, 32) instance Arbitrary IPv6Range where - arbitrary = IPv6.range <$> arbitrary <*> choose (0,128) - shrink (IPv6Range addr mask) = liftA2 IPv6.range - (shrink addr) - (filter (/= mask) [0,div mask 2,if mask > 0 then mask - 1 else 0]) + arbitrary = IPv6.range <$> arbitrary <*> choose (0, 128) + shrink (IPv6Range addr mask) = + liftA2 + IPv6.range + (shrink addr) + (filter (/= mask) [0, div mask 2, if mask > 0 then mask - 1 else 0]) instance Arbitrary MacCodec where arbitrary = MacCodec <$> arbitrary <*> arbitrary instance Arbitrary MacGrouping where - arbitrary = oneof - [ MacGroupingPairs <$> arbitraryMacSeparator - , MacGroupingTriples <$> arbitraryMacSeparator - , MacGroupingQuadruples <$> arbitraryMacSeparator - , pure MacGroupingNoSeparator - ] + arbitrary = + oneof + [ MacGroupingPairs <$> arbitraryMacSeparator + , MacGroupingTriples <$> arbitraryMacSeparator + , MacGroupingQuadruples <$> arbitraryMacSeparator + , pure MacGroupingNoSeparator + ] arbitraryMacSeparator :: Gen Char -arbitraryMacSeparator = elements [':','-','.','_'] +arbitraryMacSeparator = elements [':', '-', '.', '_'] byteStringToBytes :: BC8.ByteString -> Bytes byteStringToBytes = Ascii.fromString . BC8.unpack