use std::collections::{BTreeMap, HashMap};
use std::path::Path;
use crate::crypto::{self, AuthKey};
use crate::qt::{QReader, QWriter};
use crate::tdata::{APP_VERSION, FillRandom, encrypt, write_tdf};
use crate::{Error, MapReadFailure, Result, tdf};
const MAP_STREAM_FAILED: &str = "Could not stream data from mapData";
pub const MAP_FILE: &str = "map";
pub const LSK_DRAFT: u32 = 0x01;
pub const LSK_DRAFT_POSITION: u32 = 0x02;
pub const LSK_LEGACY_IMAGES: u32 = 0x03;
pub const LSK_LEGACY_STICKER_IMAGES: u32 = 0x05;
pub const LSK_LEGACY_AUDIOS: u32 = 0x06;
pub const LSK_USER_SETTINGS: u32 = 0x09;
pub const LSK_SELF_SERIALIZED: u32 = 0x15;
pub const LSK_WEBVIEW_TOKENS: u32 = 0x19;
pub const LSK_BOT_STORAGES: u32 = 0x1D;
pub const LSK_WALLET_ENGINE_STORAGES: u32 = 0x21;
pub const NEW_ACCOUNT_SETTINGS_KEY: u64 = 1_851_671_142_505_648_812;
#[must_use]
pub fn file_key_count(kind: u32) -> Option<usize> {
match kind {
0x04 | 0x07..=0x0F | 0x11..=0x13 | 0x18 | 0x1A..=0x1C | 0x1E..=0x20 => Some(1),
0x14 => Some(2),
0x10 => Some(4),
0x16 | 0x17 => Some(3),
_ => None,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MapEntry {
FileKeys {
kind: u32,
keys: Vec<u64>,
},
PeerKeys {
kind: u32,
pairs: Vec<(u64, u64)>,
},
LegacyLocations {
kind: u32,
items: Vec<(u64, u64, u64, i32)>,
},
SelfSerialized(Option<Vec<u8>>),
WebviewTokens {
bots: Option<Vec<u8>>,
other: Option<Vec<u8>>,
},
WalletEngineStorages(Vec<(u64, Option<String>)>),
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct MapData {
pub version: u32,
pub legacy_salt: Option<Vec<u8>>,
pub legacy_key_encrypted: Option<Vec<u8>>,
pub entries: Vec<MapEntry>,
pub stopped_at: Option<u32>,
}
impl MapData {
pub fn parse_entries(data: &[u8]) -> Result<(Vec<MapEntry>, Option<u32>)> {
let mut reader = QReader::new(data);
let mut entries = Vec::new();
while !reader.is_empty() {
let Ok(kind) = reader.read_u32() else {
return Ok((entries, Some(0)));
};
match read_entry(&mut reader, kind)? {
Some(entry) => entries.push(entry),
None => return Ok((entries, Some(kind))),
}
}
Ok((entries, None))
}
pub fn read(dir: impl AsRef<Path>, local_key: &AuthKey) -> Result<Self> {
let file = tdf::read(dir, MAP_FILE).map_err(|_| Error::ReadMapDataFailed {
context: MapReadFailure::Open,
})?;
let mut reader = QReader::new(&file.payload);
let mut array = || {
reader
.read_bytearray()
.map_err(|_| Error::QDataStreamFailed {
context: Some(MAP_STREAM_FAILED),
})
};
let legacy_salt = array()?.map(<[u8]>::to_vec);
let legacy_key_encrypted = array()?.map(<[u8]>::to_vec);
let encrypted = array()?.unwrap_or_default();
let map =
crypto::decrypt_local(encrypted, local_key).map_err(|_| Error::ReadMapDataFailed {
context: MapReadFailure::Decrypt,
})?;
let (entries, stopped_at) = Self::parse_entries(&map)?;
Ok(Self {
version: file.version,
legacy_salt,
legacy_key_encrypted,
entries,
stopped_at,
})
}
#[must_use]
pub fn new_account() -> Self {
Self {
version: APP_VERSION,
legacy_salt: None,
legacy_key_encrypted: None,
entries: vec![MapEntry::FileKeys {
kind: LSK_USER_SETTINGS,
keys: vec![NEW_ACCOUNT_SETTINGS_KEY],
}],
stopped_at: None,
}
}
pub fn encode_entries(&self) -> Result<Vec<u8>> {
MapState::fold(&self.entries).encode()
}
pub(crate) fn write(
&self,
dir: &Path,
local_key: &AuthKey,
rng: &mut FillRandom<'_>,
) -> Result<()> {
let entries = self.plaintext()?;
let mut payload = QWriter::new();
payload.write_bytearray(None)?;
payload.write_bytearray(None)?;
payload.write_bytearray(Some(&encrypt(&entries, local_key, rng)?))?;
write_tdf(dir, MAP_FILE, payload.into_bytes())
}
pub(crate) fn plaintext(&self) -> Result<Vec<u8>> {
let entries = self.encode_entries()?;
if entries.is_empty() {
return Err(Error::SaveFailed);
}
Ok(entries)
}
}
mod slot {
pub const PREFS: usize = 0;
pub const LOCATIONS: usize = 1;
pub const TRUSTED_BOTS: usize = 2;
pub const RECENT_STICKERS_OLD: usize = 3;
pub const INSTALLED_STICKERS: usize = 4;
pub const FEATURED_STICKERS: usize = 5;
pub const RECENT_STICKERS: usize = 6;
pub const FAVED_STICKERS: usize = 7;
pub const ARCHIVED_STICKERS: usize = 8;
pub const SAVED_GIFS: usize = 9;
pub const INSTALLED_MASKS: usize = 10;
pub const RECENT_MASKS: usize = 11;
pub const ARCHIVED_MASKS: usize = 12;
pub const INSTALLED_CUSTOM_EMOJI: usize = 13;
pub const FEATURED_CUSTOM_EMOJI: usize = 14;
pub const ARCHIVED_CUSTOM_EMOJI: usize = 15;
pub const SEARCH_SUGGESTIONS: usize = 16;
pub const BACKGROUND_DAY: usize = 17;
pub const BACKGROUND_NIGHT: usize = 18;
pub const USER_SETTINGS: usize = 19;
pub const RECENT_HASHTAGS: usize = 20;
pub const EXPORT_SETTINGS: usize = 21;
pub const ROUND_PLACEHOLDER: usize = 22;
pub const INLINE_BOTS_DOWNLOADS: usize = 23;
pub const MEDIA_PLAYBACK_POSITIONS: usize = 24;
pub const COUNT: usize = 25;
}
fn key_slots(kind: u32) -> &'static [usize] {
match kind {
0x1E => &[slot::PREFS],
0x04 => &[slot::LOCATIONS],
0x11 => &[slot::TRUSTED_BOTS],
0x07 => &[slot::RECENT_STICKERS_OLD],
0x08 => &[slot::BACKGROUND_DAY],
LSK_USER_SETTINGS => &[slot::USER_SETTINGS],
0x0A => &[slot::RECENT_HASHTAGS],
0x0B => &[slot::INSTALLED_STICKERS],
0x12 => &[slot::FAVED_STICKERS],
0x0F => &[slot::SAVED_GIFS],
0x13 => &[slot::EXPORT_SETTINGS],
0x18 => &[slot::SEARCH_SUGGESTIONS],
0x14 => &[slot::BACKGROUND_DAY, slot::BACKGROUND_NIGHT],
0x10 => &[
slot::INSTALLED_STICKERS,
slot::FEATURED_STICKERS,
slot::RECENT_STICKERS,
slot::ARCHIVED_STICKERS,
],
0x16 => &[
slot::INSTALLED_MASKS,
slot::RECENT_MASKS,
slot::ARCHIVED_MASKS,
],
0x17 => &[
slot::INSTALLED_CUSTOM_EMOJI,
slot::FEATURED_CUSTOM_EMOJI,
slot::ARCHIVED_CUSTOM_EMOJI,
],
0x1A => &[slot::ROUND_PLACEHOLDER],
0x1B => &[slot::INLINE_BOTS_DOWNLOADS],
0x1C => &[slot::MEDIA_PLAYBACK_POSITIONS],
_ => &[],
}
}
const PEER_SERIALIZED_FLAG: u64 = 0x80 << 48;
fn peer_from_serialized(serialized: u64) -> u64 {
if serialized & PEER_SERIALIZED_FLAG != 0 {
return serialized & !PEER_SERIALIZED_FLAG;
}
let bare = serialized & 0xFFFF_FFFF;
match serialized & 0xF_0000_0000 {
0 => bare,
0x1_0000_0000 => bare | (1 << 48),
0x2_0000_0000 => bare | (2 << 48),
0xF_0000_0000 => bare | (0x7F << 48),
_ => 0,
}
}
#[derive(Default)]
struct PeerMap {
pairs: Vec<(u64, u64)>,
index: HashMap<u64, usize>,
}
impl PeerMap {
fn insert(&mut self, peer: u64, file_key: u64) {
if let Some(&at) = self.index.get(&peer) {
self.pairs[at].1 = file_key;
} else {
self.index.insert(peer, self.pairs.len());
self.pairs.push((peer, file_key));
}
}
fn encode(&self, writer: &mut QWriter, kind: u32) -> Result<()> {
if self.pairs.is_empty() {
return Ok(());
}
let count = u32::try_from(self.pairs.len())
.map_err(|_| Error::QDataStreamFailed { context: None })?;
writer.write_u32(kind);
writer.write_u32(count);
for &(peer, file_key) in &self.pairs {
writer.write_u64(file_key);
writer.write_u64(peer | PEER_SERIALIZED_FLAG);
}
Ok(())
}
}
struct MapState<'a> {
keys: [u64; slot::COUNT],
self_serialized: Option<&'a [u8]>,
drafts: PeerMap,
draft_positions: PeerMap,
bot_storages: PeerMap,
webview_bots: Option<&'a [u8]>,
webview_other: Option<&'a [u8]>,
wallet: BTreeMap<&'a str, u64>,
}
impl<'a> MapState<'a> {
fn fold(entries: &'a [MapEntry]) -> Self {
let mut state = Self {
keys: [0; slot::COUNT],
self_serialized: None,
drafts: PeerMap::default(),
draft_positions: PeerMap::default(),
bot_storages: PeerMap::default(),
webview_bots: None,
webview_other: None,
wallet: BTreeMap::new(),
};
for entry in entries {
match entry {
MapEntry::FileKeys { kind, keys } => {
for (&at, &key) in key_slots(*kind).iter().zip(keys) {
state.keys[at] = key;
}
}
MapEntry::PeerKeys { kind, pairs } => {
let target = match *kind {
LSK_DRAFT => &mut state.drafts,
LSK_DRAFT_POSITION => &mut state.draft_positions,
LSK_BOT_STORAGES => &mut state.bot_storages,
_ => continue,
};
for &(file_key, peer) in pairs {
target.insert(peer_from_serialized(peer), file_key);
}
}
MapEntry::LegacyLocations { .. } => {}
MapEntry::SelfSerialized(blob) => state.self_serialized = blob.as_deref(),
MapEntry::WebviewTokens { bots, other } => {
state.webview_bots = bots.as_deref();
state.webview_other = other.as_deref();
}
MapEntry::WalletEngineStorages(items) => {
state.wallet = items
.iter()
.map(|(file_key, name)| (name.as_deref().unwrap_or_default(), *file_key))
.collect();
}
}
}
state
}
fn encode_keys(&self, writer: &mut QWriter, kind: u32) {
let slots = key_slots(kind);
if slots.iter().any(|&at| self.keys[at] != 0) {
writer.write_u32(kind);
for &at in slots {
writer.write_u64(self.keys[at]);
}
}
}
fn encode(&self) -> Result<Vec<u8>> {
let mut writer = QWriter::new();
if let Some(blob) = self.self_serialized.filter(|blob| !blob.is_empty()) {
writer.write_u32(LSK_SELF_SERIALIZED);
writer.write_bytearray(Some(blob))?;
}
self.encode_keys(&mut writer, 0x1E);
self.drafts.encode(&mut writer, LSK_DRAFT)?;
self.draft_positions
.encode(&mut writer, LSK_DRAFT_POSITION)?;
self.bot_storages.encode(&mut writer, LSK_BOT_STORAGES)?;
for kind in [
0x04, 0x11, 0x07, 0x10, 0x12, 0x0F, 0x09, 0x0A, 0x13, 0x16, 0x17, 0x18,
] {
self.encode_keys(&mut writer, kind);
}
let non_empty = |token: Option<&[u8]>| token.is_some_and(|t| !t.is_empty());
if non_empty(self.webview_bots) || non_empty(self.webview_other) {
writer.write_u32(LSK_WEBVIEW_TOKENS);
writer.write_bytearray(self.webview_bots)?;
writer.write_bytearray(self.webview_other)?;
}
for kind in [0x1A, 0x1B, 0x1C] {
self.encode_keys(&mut writer, kind);
}
if !self.wallet.is_empty() {
let count = u32::try_from(self.wallet.len())
.map_err(|_| Error::QDataStreamFailed { context: None })?;
writer.write_u32(LSK_WALLET_ENGINE_STORAGES);
writer.write_u32(count);
for (&name, &file_key) in &self.wallet {
writer.write_u64(file_key);
writer.write_qstring(Some(name))?;
}
}
self.encode_keys(&mut writer, 0x14);
Ok(writer.into_bytes())
}
}
fn read_counted<'a, T>(
reader: &mut QReader<'a>,
mut item: impl FnMut(&mut QReader<'a>) -> Result<T>,
) -> Result<Vec<T>> {
let count = reader.read_u32()?;
let mut items = Vec::new();
for _ in 0..count {
items.push(item(reader)?);
}
Ok(items)
}
fn owned(blob: Option<&[u8]>) -> Option<Vec<u8>> {
blob.map(<[u8]>::to_vec)
}
fn read_entry(reader: &mut QReader<'_>, kind: u32) -> Result<Option<MapEntry>> {
let entry = match kind {
LSK_DRAFT | LSK_DRAFT_POSITION | LSK_BOT_STORAGES => MapEntry::PeerKeys {
kind,
pairs: read_counted(reader, |r| Ok((r.read_u64()?, r.read_u64()?)))?,
},
LSK_LEGACY_IMAGES | LSK_LEGACY_STICKER_IMAGES | LSK_LEGACY_AUDIOS => {
MapEntry::LegacyLocations {
kind,
items: read_counted(reader, |r| {
Ok((r.read_u64()?, r.read_u64()?, r.read_u64()?, r.read_i32()?))
})?,
}
}
LSK_SELF_SERIALIZED => MapEntry::SelfSerialized(owned(reader.read_bytearray()?)),
LSK_WEBVIEW_TOKENS => MapEntry::WebviewTokens {
bots: owned(reader.read_bytearray()?),
other: owned(reader.read_bytearray()?),
},
LSK_WALLET_ENGINE_STORAGES => MapEntry::WalletEngineStorages(read_counted(reader, |r| {
Ok((r.read_u64()?, r.read_qstring()?))
})?),
_ => {
let Some(count) = file_key_count(kind) else {
return Ok(None);
};
let mut keys = Vec::with_capacity(count);
for _ in 0..count {
keys.push(reader.read_u64()?);
}
MapEntry::FileKeys { kind, keys }
}
};
Ok(Some(entry))
}