\chapter{State Format} \begin{code} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE RecordWildCards #-} module Network.Tox.SaveData ( SaveData (..) , Section (..) , NospamKeys (..) , Friends (..) , Bytes (..) ) where \end{code} The reference Tox implementation uses a custom binary format to save the state of a Tox client between restarts. This format is far from perfect and will be replaced eventually. For the sake of maintaining compatibility down the road, it is documented here. The binary encoding of all integer types in the state format is a fixed-width byte sequence with the integer encoded in Little Endian unless stated otherwise. \begin{code} import Control.Arrow (second) import Control.Monad (when) import Data.Binary (Binary (..)) import Data.Binary.Get (Get) import qualified Data.Binary.Get as Get import Data.Binary.Put (Put) import qualified Data.Binary.Put as Put import qualified Data.ByteString as BS import qualified Data.ByteString.Lazy as LBS import Data.Word (Word16, Word32, Word8) import GHC.Generics (Generic) import Network.Tox.Crypto.Key (PublicKey, SecretKey) import Network.Tox.SaveData.Conferences (Conferences) import Network.Tox.SaveData.DHT (DHT) import Network.Tox.SaveData.Friend (Friend) import Network.Tox.SaveData.Nodes (Nodes) import qualified Network.Tox.SaveData.Util as Util import Test.QuickCheck.Arbitrary (Arbitrary (..), genericShrink) import qualified Test.QuickCheck.Gen as Gen \end{code} \begin{tabular}{l|l} Length & Contents \\ \hline \texttt{4} & Zeroes \\ \texttt{4} & \texttt{uint32\_t} (0x15ED1B1F) \\ \texttt{?} & List of sections \\ \end{tabular} \begin{code} saveDataMagic :: Word32 saveDataMagic = 0x15ED1B1F newtype SaveData = SaveData [Section] deriving (Eq, Show, Read, Generic) instance Binary SaveData where get = do zeroes <- Get.getWord32le when (zeroes /= 0) $ fail $ "savedata should start with 32 zero-bits, but got " ++ show zeroes magic <- Get.getWord32le when (magic /= saveDataMagic) $ fail $ "wrong magic number for savedata: " ++ show magic ++ " != " ++ show saveDataMagic SaveData <$> getSections put (SaveData sections) = do Put.putWord32le 0 Put.putWord32le saveDataMagic putSections sections instance Arbitrary SaveData where arbitrary = SaveData . (++ [SectionEOF]) <$> arbitrary shrink = filter (\(SaveData ss) -> SectionEOF `elem` ss) . genericShrink \end{code} \section{Sections} The core of the state format consists of a list of sections. Every section has its type and length specified at the beginning. In some cases, a section only contains one item and thus takes up the entire length of the section. This is denoted with '?'. \begin{tabular}{l|l} Length & Contents \\ \hline \texttt{4} & \texttt{uint32\_t} Length of this section \\ \texttt{2} & \texttt{uint16\_t} Section type \\ \texttt{2} & \texttt{uint16\_t} (0x01CE) \\ \texttt{?} & Section \\ \end{tabular} \begin{code} sectionMagic :: Word16 sectionMagic = 0x01CE \end{code} Section types: \begin{tabular}{l|l} Name & Value \\ \hline NospamKeys & 0x01 \\ DHT & 0x02 \\ Friends & 0x03 \\ Name & 0x04 \\ StatusMessage & 0x05 \\ Status & 0x06 \\ TcpRelays & 0x0A \\ PathNodes & 0x0B \\ Conferences & 0x14 \\ EOF & 0xFF \\ \end{tabular} \begin{code} getSections :: Get [Section] getSections = go where go = do (len, ty) <- Util.getSectionHeader sectionMagic let load f = (:) <$> (f <$> Get.isolate (fromIntegral len) get) <*> go case ty of 0x01 -> load SectionNospamKeys 0x02 -> load SectionDHT 0x03 -> load SectionFriends 0x04 -> load SectionName 0x05 -> load SectionStatusMessage 0x06 -> load SectionStatus 0x0A -> load SectionTcpRelays 0x0B -> load SectionPathNodes 0x14 -> load SectionConferences 0xFF -> return [SectionEOF] _ -> fail $ show ty putSections :: [Section] -> Put putSections = mapM_ go where go section = do let (ty, bytes) = second Put.runPut $ putSection section Util.putSectionHeader sectionMagic (fromIntegral $ LBS.length bytes) ty Put.putLazyByteString bytes putSection = \case SectionNospamKeys x -> (0x01, put x) SectionDHT x -> (0x02, put x) SectionFriends x -> (0x03, put x) SectionName x -> (0x04, put x) SectionStatusMessage x -> (0x05, put x) SectionStatus x -> (0x06, put x) SectionTcpRelays x -> (0x0A, put x) SectionPathNodes x -> (0x0B, put x) SectionConferences x -> (0x14, put x) SectionEOF -> (0xFF, return ()) \end{code} Not every section listed above is required to be present in order to restore from a state file. Only NospamKeys is required. \subsection{Nospam and Keys (0x01)} \begin{tabular}{l|l} Length & Contents \\ \hline \texttt{4} & \texttt{uint32\_t} Nospam \\ \texttt{32} & Long term public key \\ \texttt{32} & Long term secret key \\ \end{tabular} \input{src/Network/Tox/SaveData/DHT.lhs} \subsection{Friends (0x03)} This section contains a list of friends. A friend can either be a peer we've sent a friend request to or a peer we've accepted a friend request from. \begin{tabular}{l|l} Length & Contents \\ \hline \texttt{?} & List of friends \\ \end{tabular} \input{src/Network/Tox/SaveData/Friend.lhs} \subsection{Name (0x04)} \begin{tabular}{l|l} Length & Contents \\ \hline \texttt{?} & Name as a UTF-8 encoded string \\ \end{tabular} \subsection{Status Message (0x05)} \begin{tabular}{l|l} Length & Contents \\ \hline \texttt{?} & Status message as a UTF-8 encoded string \\ \end{tabular} \subsection{Status (0x06)} \begin{tabular}{l|l} Length & Contents \\ \hline \texttt{1} & \texttt{uint8\_t} User status (see also: \texttt{USERSTATUS}) \\ \end{tabular} \subsection{Tcp Relays (0x0A)} This section contains a list of TCP relays. \begin{tabular}{l|l} Length & Contents \\ \hline \texttt{?} & List of TCP relays \\ \end{tabular} The structure of a TCP relay is the same as \texttt{Node Info}. Note: this means that the integers stored in these nodes are stored in Big Endian as well. \subsection{Path Nodes (0x0B)} This section contains a list of path nodes used for onion routing. \begin{tabular}{l|l} Length & Contents \\ \hline \texttt{?} & List of path nodes \\ \end{tabular} The structure of a path node is the same as \texttt{Node Info}. Note: this means that the integers stored in these nodes are stored in Big Endian as well. \input{src/Network/Tox/SaveData/Conferences.lhs} \subsection{EOF (0xFF)} This section indicates the end of the state file. This section doesn't have any content and thus its length is 0. \begin{code} data Section = SectionNospamKeys NospamKeys | SectionDHT DHT | SectionFriends Friends | SectionName Bytes | SectionStatusMessage Bytes | SectionStatus Word8 | SectionTcpRelays Nodes | SectionPathNodes Nodes | SectionConferences Conferences | SectionEOF deriving (Eq, Show, Read, Generic) instance Arbitrary Section where arbitrary = Gen.oneof [ SectionNospamKeys <$> arbitrary , SectionDHT <$> arbitrary , SectionFriends <$> arbitrary , SectionName <$> arbitrary , SectionStatusMessage <$> arbitrary , SectionStatus <$> arbitrary , SectionTcpRelays <$> arbitrary , SectionPathNodes <$> arbitrary , SectionConferences <$> arbitrary ] shrink = genericShrink data NospamKeys = NospamKeys { nospam :: Word32 , publicKey :: PublicKey , secretKey :: SecretKey } deriving (Eq, Show, Read, Generic) instance Binary NospamKeys where get = NospamKeys <$> Get.getWord32le <*> get <*> get put NospamKeys{..} = do Put.putWord32le nospam put publicKey put secretKey instance Arbitrary NospamKeys where arbitrary = NospamKeys <$> arbitrary <*> arbitrary <*> arbitrary shrink = genericShrink newtype Friends = Friends [Friend] deriving (Eq, Show, Read, Generic) instance Binary Friends where get = Friends <$> Util.getList put (Friends xs) = mapM_ put xs instance Arbitrary Friends where arbitrary = Friends <$> arbitrary shrink = genericShrink newtype Bytes = Bytes LBS.ByteString deriving (Eq, Show, Read, Generic) instance Binary Bytes where get = Bytes <$> Get.getRemainingLazyByteString put (Bytes bs) = Put.putLazyByteString bs instance Arbitrary Bytes where arbitrary = Bytes . LBS.pack <$> arbitrary \end{code}