packages feed

dbus-core-0.8.3: src/addresses.anansi

:# Copyright (C) 2009-2010 John Millikin <jmillikin@gmail.com>
:# 
:# This program is free software: you can redistribute it and/or modify
:# it under the terms of the GNU General Public License as published by
:# the Free Software Foundation, either version 3 of the License, or
:# any later version.
:# 
:# This program is distributed in the hope that it will be useful,
:# but WITHOUT ANY WARRANTY; without even the implied warranty of
:# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
:# GNU General Public License for more details.
:# 
:# You should have received a copy of the GNU General Public License
:# along with this program.  If not, see <http://www.gnu.org/licenses/>.

\section{Addresses}

:f DBus/Address.hs
|copyright|
|text extensions|
module DBus.Address
	( Address
	, addressMethod
	, addressParameters
	, mkAddresses
	, strAddress
	) where
|text imports|

import Data.Char (ord, chr)
import qualified Data.Map as M
import Text.Printf (printf)
import qualified Text.Parsec as P
import Text.Parsec ((<|>))
import DBus.Util (hexToInt, eitherToMaybe)
:

\subsection{Address syntax}

A bus address is in the format {\tt $method$:$key$=$value$,$key$=$value$...}
where the method may be empty and parameters are optional. An address's
parameter list, if present, may end with a comma. Addresses in environment
variables are separated by semicolons, and the full address list may end
in a semicolon. Multiple parameters may have the same key; in this case,
only the first parameter for each key will be stored.

The bytes allowed in each component of the address are given by the following
chart, where each character is understood to be its ASCII value:

\begin{table}[h]
\begin{center}
\begin{tabular}{ll}
\toprule
Component   & Allowed Characters \\
\midrule
Method      & Any except {\tt `;'} and {\tt `:'} \\
Param key   & Any except {\tt `;'}, {\tt `,'}, and {\tt `='} \\
Param value & {\tt `0'} to {\tt `9'} \\
            & {\tt `a'} to {\tt `z'} \\
            & {\tt `A'} to {\tt `Z'} \\
            & Any of: {\tt - \textunderscore{} / \textbackslash{} * . \%} \\
\bottomrule
\end{tabular}
\end{center}
\end{table}

In parameter values, any byte may be encoded by prepending the \% character
to its value in hexadecimal. \% is not allowed to appear unless it is
followed by two hexadecimal digits. Every other allowed byte is termed
an ``optionally encoded'' byte, and may appear unescaped in parameter
values.

:f DBus/Address.hs
optionallyEncoded :: [Char]
optionallyEncoded = ['0'..'9'] ++ ['a'..'z'] ++ ['A'..'Z'] ++ "-_/\\*."
:

The address simply stores its method and parameter map, with a custom
{\tt Show} instance to provide easier debugging.

:f DBus/Address.hs
data Address = Address
	{ addressMethod     :: Text
	, addressParameters :: M.Map Text Text
	} deriving (Eq)

instance Show Address where
	showsPrec d x = showParen (d> 10) $
		showString "Address " . shows (strAddress x)
:

Parsing is straightforward; the input string is divided into addresses by
semicolons, then further by colons and commas. Parsing will fail if any
of the addresses in the input failed to parse.

:f DBus/Address.hs
mkAddresses :: Text -> Maybe [Address]
mkAddresses s = eitherToMaybe . P.parse parser "" . TL.unpack $ s where
	address = do
		method <- P.many (P.noneOf ":;")
		P.char ':'
		params <- P.sepEndBy param (P.char ',')
		return $ Address (TL.pack method) (M.fromList params)
	
	param = do
		key <- P.many1 (P.noneOf "=;,")
		P.char '='
		value <- P.many1 (encodedValue <|> unencodedValue)
		return (TL.pack key, TL.pack value)
	
	parser = do
		as <- P.sepEndBy1 address (P.char ';')
		P.eof
		return as
	
	unencodedValue = P.oneOf optionallyEncoded
	encodedValue = do
		P.char '%'
		hex <- P.count 2 P.hexDigit
		return . chr . hexToInt $ hex
:

Converting an {\tt Address} back to a {\tt String} is just the reverse
operation. Note that because the original parameter order is not preserved,
the string produced might differ from the original input.

:f DBus/Address.hs
strAddress :: Address -> Text
strAddress (Address t ps) = TL.concat [t, ":", ps'] where
	ps' = TL.intercalate "," $ do
		(k, v) <- M.toList ps
		return $ TL.concat [k, "=", TL.concatMap encode v]
	encode c | elem c optionallyEncoded = TL.singleton c
	         | otherwise       = TL.pack $ printf "%%%02X" (ord c)
:

:f Tests.hs
instance Arbitrary Address where
	arbitrary = genAddress where
		optional = ['0'..'9'] ++ ['a'..'z'] ++ ['A'..'Z'] ++ "-_/\\*."
		methodChars = filter (flip notElem ":;") ['!'..'~']
		keyChars = filter (flip notElem "=;,") ['!'..'~']
		
		genMethod = atLeast 0 $ elements methodChars
		genParam = do
			key <- genKey
			value <- genValue
			return . concat $ [key, "=", value]
		
		genKey = atLeast 1 $ elements keyChars
		genValue = oneof [encodedValue, plainValue]
		genHex = elements $ ['0'..'9'] ++ ['a'..'f'] ++ ['A'..'F']
		encodedValue = do
			x1 <- genHex
			x2 <- genHex
			return ['%', x1, x2]
		plainValue = atLeast 1 $ elements optional
		
		genParams = do
			params <- atLeast 0 genParam
			let params' = intercalate "," params
			extraComma <- if null params
				then return ""
				else elements ["", ","]
			return $ concat [params', extraComma]
		
		genAddress = do
			m <- genMethod
			params <- genParams
			extraSemicolon <- elements ["", ";"]
			let addrStr = concat [m, ":", params, extraSemicolon]
			let Just [addr] = mkAddresses $ TL.pack addrStr
			return addr
:

:d test cases
, F.testGroup "Addresses"
	[ testProperty "Address identity"
		$ \x -> mkAddresses (strAddress x) == Just [x]
	, testProperty "Multiple addresses"
		$ \x y -> let
		joined = TL.concat [strAddress x, ";", strAddress y]
		in mkAddresses joined == Just [x, y]
	, testProperty "Ignore trailing semicolon"
		$ \x -> mkAddresses (TL.append (strAddress x) ";") == Just [x]
	, testProperty "Ignore trailing comma"
		$ \x -> let
		hasParams = not . Map.null . addressParameters $ x
		parsed = mkAddresses (TL.append (strAddress x) ",")
		in hasParams ==> parsed == Just [x]
	, F.testGroup "Valid addresses"
		[ test "colon" $ isJust . mkAddresses $ ":"
		, test "just scheme" $ isJust . mkAddresses $ "a:"
		, test "param" $ isJust . mkAddresses $ "a:b=c"
		, test "trailing semicolon" $ isJust . mkAddresses $ "a:;"
		, test "two schemes" $ isJust . mkAddresses $ "a:;b:"
		, test "trailing comma" $ isJust . mkAddresses $ "a:b=c,"
		]
	, F.testGroup "Invalid addresses"
		[ test "empty" $ isNothing . mkAddresses $ ""
		, test "no colon" $ isNothing . mkAddresses $ "a"
		, test "no equals" $ isNothing . mkAddresses $ "a:b"
		, test "no param" $ isNothing . mkAddresses $ "a:b="
		, test "no param" $ isNothing . mkAddresses $ "a:,"
		]
	]
: