diff --git a/src/Internal/Grammar/ServerName.elm b/src/Internal/Grammar/ServerName.elm index b61e913..5146bcc 100644 --- a/src/Internal/Grammar/ServerName.elm +++ b/src/Internal/Grammar/ServerName.elm @@ -1,6 +1,6 @@ module Internal.Grammar.ServerName exposing ( ServerName, toString, fromString - , servernameParser + , serverNameParser ) {-| @@ -83,9 +83,11 @@ dnsNameParser = |> P.getChompedString +{-| Convert a string to a server name. +-} fromString : String -> Maybe ServerName fromString s = - P.run (servernameParser |. P.end) s + P.run (serverNameParser |. P.end) s |> (\out -> case out of Ok _ -> @@ -212,8 +214,11 @@ portParser = ) -servernameParser : Parser ServerName -servernameParser = +{-| Parse a server name. Generally used by other identifiers that have a server +name as one of its parts. +-} +serverNameParser : Parser ServerName +serverNameParser = P.succeed ServerName |= hostnameParser |= P.oneOf @@ -224,6 +229,8 @@ servernameParser = ] +{-| Convert a parsed server name back to a string. +-} toString : ServerName -> String toString { host, port_ } = let diff --git a/src/Internal/Grammar/UserId.elm b/src/Internal/Grammar/UserId.elm index 2a2327f..24baf06 100644 --- a/src/Internal/Grammar/UserId.elm +++ b/src/Internal/Grammar/UserId.elm @@ -1,4 +1,7 @@ -module Internal.Grammar.UserId exposing (..) +module Internal.Grammar.UserId exposing + ( UserID, toString, fromString + , userIdParser, isHistorical + ) {-| @@ -36,6 +39,16 @@ localparts from the expanded character set: extended_user_id_char = %x21-39 / %x3B-7E ; all ASCII printing chars except : + +## User ID + +@docs UserID, toString, fromString + + +## Extra + +@docs userIdParser, isHistorical + -} import Internal.Grammar.ServerName as ServerName exposing (ServerName) @@ -43,13 +56,39 @@ import Internal.Tools.ParserExtra as PE import Parser as P exposing ((|.), (|=), Parser) +{-| The User ID type defining a user. +-} type alias UserID = { localpart : String, domain : ServerName } +{-| Convert a Matrix User ID back into its uniquely identifying string. +-} fromString : String -> Maybe UserID fromString = - P.run userIdParser >> Result.toMaybe + P.run (userIdParser |. P.end) >> Result.toMaybe + + +{-| Return a boolean on whether a Matrix user has a historical user ID. +Since this user ID is not SUPPOSED to be legal but clients are nevertheless +forced to support them due to backwards compatibility, clients may occasionally +attempt to break the rules in an attempt to find undefined behaviour. + +As a result, an explicit method to spot historical users is added to the SDK. + +-} +isHistorical : UserID -> Bool +isHistorical { localpart } = + String.any + (\c -> + let + i : Int + i = + Char.toCode c + in + not ((0x61 <= i && i <= 0x7A) || Char.isAlpha c) + ) + localpart localpartParser : Parser String @@ -60,18 +99,23 @@ localpartParser = |> P.map String.concat +{-| Convert a parsed User ID to a string. +-} toString : UserID -> String toString { localpart, domain } = String.concat [ "@", localpart, ":", ServerName.toString domain ] +{-| Parse a UserID from a string. +-} userIdParser : Parser UserID userIdParser = P.succeed UserID |. P.symbol "@" |= localpartParser |. P.symbol ":" - |= ServerName.servernameParser + |= ServerName.serverNameParser + |> PE.maxLength 255 validHistoricalUsernameChar : Char -> Bool diff --git a/tests/Test/Grammar/ServerName.elm b/tests/Test/Grammar/ServerName.elm index 02ac97a..e3e1a38 100644 --- a/tests/Test/Grammar/ServerName.elm +++ b/tests/Test/Grammar/ServerName.elm @@ -101,8 +101,7 @@ suite = (\server -> SN.fromString server |> Maybe.map SN.toString - |> Maybe.map (String.replace "::" ":") - |> Expect.equal (Just <| String.replace "::" ":" server) + |> Expect.equal (Just server) ) , test "Checking spec examples" (\() -> diff --git a/tests/Test/Grammar/UserId.elm b/tests/Test/Grammar/UserId.elm new file mode 100644 index 0000000..8e1100a --- /dev/null +++ b/tests/Test/Grammar/UserId.elm @@ -0,0 +1,145 @@ +module Test.Grammar.UserId exposing (..) + +import Expect +import Fuzz exposing (Fuzzer) +import Internal.Grammar.UserId as U +import Test exposing (..) +import Test.Grammar.ServerName as ServerName + + +modernUserCharFuzzer : Fuzzer Char +modernUserCharFuzzer = + Fuzz.oneOf + [ Fuzz.intRange 0x61 0x7A + |> Fuzz.map Char.fromCode + , "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + |> String.toList + |> Fuzz.oneOfValues + ] + + +historicalUserCharFuzzer : Fuzzer Char +historicalUserCharFuzzer = + [ ( 0x21, 0x39 ), ( 0x3B, 0x7E ) ] + |> List.map (\( low, high ) -> Fuzz.intRange low high) + |> Fuzz.oneOf + |> Fuzz.map Char.fromCode + + +modernUserFuzzer : Fuzzer String +modernUserFuzzer = + Fuzz.map2 + (\localpart domain -> + let + maxLocalSize : Int + maxLocalSize = + 255 - String.length domain - 2 + in + localpart + |> List.take maxLocalSize + |> String.fromList + |> (\l -> "@" ++ l ++ ":" ++ domain) + ) + (Fuzz.listOfLengthBetween 1 255 modernUserCharFuzzer) + (ServerName.serverNameFuzzer + |> Fuzz.filter + (\name -> + String.length name < 255 - 2 + ) + ) + + +historicalUserFuzzer : Fuzzer String +historicalUserFuzzer = + Fuzz.map2 + (\localpart domain -> + let + maxLocalSize : Int + maxLocalSize = + 255 - String.length domain - 2 + in + localpart + |> List.take maxLocalSize + |> String.fromList + |> (\l -> "@" ++ l ++ ":" ++ domain) + ) + (Fuzz.listOfLengthBetween 1 255 historicalUserCharFuzzer) + (ServerName.serverNameFuzzer + |> Fuzz.filter + (\name -> + String.length name < 255 - 2 + ) + ) + + +userFuzzer : Fuzzer String +userFuzzer = + Fuzz.oneOf [ modernUserFuzzer, historicalUserFuzzer ] + + +suite : Test +suite = + describe "UserId" + [ describe "Size" + [ fuzz ServerName.serverNameFuzzer + "Username cannot be length 0" + (\domain -> + "@" + ++ ":" + ++ domain + |> U.fromString + |> Expect.equal Nothing + ) + , fuzz2 (Fuzz.listOfLengthBetween 1 255 historicalUserCharFuzzer) + ServerName.serverNameFuzzer + "Username length cannot exceed 255" + (\localpart domain -> + let + username : String + username = + "@" + ++ String.fromList localpart + ++ ":" + ++ domain + in + Expect.equal + (U.fromString username == Nothing) + (String.length username > 255) + ) + , fuzz modernUserFuzzer + "Modern fuzzer has appropriate size" + (String.length >> Expect.lessThan 256) + , fuzz historicalUserFuzzer + "Historical fuzzer has appropriate size" + (String.length >> Expect.lessThan 256) + ] + , describe "From string evaluation" + [ fuzz userFuzzer + "fromString always returns a value on fuzzer" + (U.fromString >> Expect.notEqual Nothing) + , fuzz userFuzzer + "fromString -> toString returns the same value" + (\username -> + username + |> U.fromString + |> Maybe.map U.toString + |> Expect.equal (Just username) + ) + , fuzz historicalUserFuzzer + "Historical users are historical" + (\username -> + username + |> U.fromString + |> Maybe.map U.isHistorical + |> Expect.equal (Just True) + ) + , fuzz modernUserFuzzer + "Modern users are not historical" + (\username -> + username + |> U.fromString + |> Maybe.map U.isHistorical + |> Expect.equal (Just False) + ) + ] + ]