toxcore-0.2.11: src/Network/Tox/SaveData.lhs
\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}