use byteorder::{LittleEndian, ReadBytesExt};
use lnk_parser::LNKParser;
use serde::{Serialize, Serializer};
use winparsingtools::structs::Guid;
use std::collections::HashMap;
use std::io::{Read, Seek, SeekFrom};
use crate::Flaten;
use crate::errors::JumplistParserError;
use winparsingtools::{traits::Normalize, utils::read_utf16_string};
#[derive(Debug, Serialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum CatagoryType {
Custom = 0x00,
Known = 0x01,
Task = 0x02,
}
#[derive(Debug, Serialize)]
pub struct CustomDestinationsHeader {
pub version: u32,
pub num_of_cat: u32,
#[serde(skip_serializing)]
pub unkonwn: u32,
}
impl CustomDestinationsHeader {
pub fn from_path(path: &str) -> Result<Self, JumplistParserError> {
let mut file = std::fs::File::open(path).map_err(|e| {
JumplistParserError::General(e.to_string(), line!(), file!().to_string())
})?;
Self::from_reader(&mut file)
}
pub fn from_reader<R: Read + Seek>(reader: &mut R) -> Result<Self, JumplistParserError> {
let version = reader.read_u32::<LittleEndian>().map_err(|e| {
JumplistParserError::FileStructure(e.to_string(), line!(), file!().to_string())
})?;
let num_of_cat = reader.read_u32::<LittleEndian>().map_err(|e| {
JumplistParserError::FileStructure(e.to_string(), line!(), file!().to_string())
})?;
let unkonwn = reader.read_u32::<LittleEndian>().map_err(|e| {
JumplistParserError::FileStructure(e.to_string(), line!(), file!().to_string())
})?;
Ok(CustomDestinationsHeader {
version,
num_of_cat,
unkonwn,
})
}
}
#[derive(Debug)]
#[repr(i32)]
pub enum CategoryID {
Frequent = 0x01,
Recent = 0x02,
None = -1,
Unknown(i32),
}
impl Serialize for CategoryID {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
CategoryID::Frequent => serializer.serialize_str("frequent"),
CategoryID::Recent => serializer.serialize_str("recent"),
CategoryID::None => serializer.serialize_str("none"),
CategoryID::Unknown(val) => serializer.serialize_str(&format!("{:04X}", val)),
}
}
}
#[derive(Debug, Serialize)]
pub struct Catagory {
pub r#type: CatagoryType,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub num_of_entries: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<CategoryID>,
#[serde(skip_serializing_if = "Option::is_none")]
pub entries: Option<Vec<LNKParser>>,
}
#[derive(Debug, Serialize)]
pub struct CustomDestinations {
pub header: CustomDestinationsHeader,
pub entries: Vec<Catagory>,
}
impl CustomDestinations {
pub fn from_path(path: &str) -> Result<Self, JumplistParserError> {
let mut file = std::fs::File::open(path).map_err(|e| {
JumplistParserError::FileStructure(e.to_string(), line!(), file!().to_string())
})?;
Self::from_reader(&mut file)
}
pub fn from_reader<R: Read + Seek>(reader: &mut R) -> Result<Self, JumplistParserError> {
let header = CustomDestinationsHeader::from_reader(reader)?;
let mut categories = Vec::new();
fn read_guid_and_validate<R: Read + Seek>(
reader: &mut R,
category: &str,
) -> Result<LNKParser, JumplistParserError> {
let mut guid_data = [0; 16];
reader.read_exact(&mut guid_data).map_err(|e| {
JumplistParserError::FileStructure(e.to_string(), line!(), file!().to_string())
})?;
let guid = Guid::from_buffer(&guid_data).map_err(|e| {
JumplistParserError::FileStructure(e.to_string(), line!(), file!().to_string())
})?;
if guid.to_string() != "00021401-0000-0000-C000-000000000046" {
return Err(JumplistParserError::FileStructure(
format!("{category} Category with unknown entry GUID '{}'", guid),
line!(),
file!().to_string(),
));
}
LNKParser::from_reader(reader).map_err(|e| {
JumplistParserError::LnkEntry(e.to_string(), line!(), file!().to_string())
})
}
fn parse_lnk_entries<R: Read + Seek>(
reader: &mut R,
count: u32,
category: &str,
) -> Result<Vec<LNKParser>, JumplistParserError> {
let mut entries = Vec::with_capacity(count as usize);
for _ in 0..count {
entries.push(read_guid_and_validate(reader, category)?);
}
Ok(entries)
}
for _ in 0..header.num_of_cat {
let r#type = match reader.read_u32::<LittleEndian>() {
Ok(0x00) => CatagoryType::Custom,
Ok(0x01) => CatagoryType::Known,
Ok(0x02) => CatagoryType::Task,
Ok(x) => {
return Err(JumplistParserError::FileStructure(
format!("CatagoryType unknown '{}'", x),
line!(),
file!().to_string(),
))
}
Err(e) => {
return Err(JumplistParserError::FileStructure(
e.to_string(),
line!(),
file!().to_string(),
))
}
};
match r#type {
CatagoryType::Custom => {
let name_len = reader.read_u16::<LittleEndian>().ok().unwrap();
let name = read_utf16_string(reader, Some(name_len as usize)).ok();
let num_of_entries = reader.read_u32::<LittleEndian>().ok();
let entries = parse_lnk_entries(reader, num_of_entries.unwrap(), "Custom")?;
categories.push(Catagory {
r#type,
name,
num_of_entries,
entries: Some(entries),
id: None,
});
}
CatagoryType::Known => {
let id = match reader.read_i32::<LittleEndian>().map_err(|e| {
JumplistParserError::FileStructure(e.to_string(), line!(), file!().to_string())
})? {
1 => CategoryID::Frequent,
2 => CategoryID::Recent,
-1 => CategoryID::None,
x => CategoryID::Unknown(x),
};
categories.push(Catagory {
r#type,
name: None,
num_of_entries: None,
id: Some(id),
entries: None,
});
}
CatagoryType::Task => {
let num_of_entries = reader.read_u32::<LittleEndian>().ok();
let entries = parse_lnk_entries(reader, num_of_entries.unwrap(), "Task")?;
categories.push(Catagory {
r#type,
name: None,
num_of_entries,
entries: Some(entries),
id: None,
});
}
}
reader.seek(SeekFrom::Current(4)).map_err(|e| {
JumplistParserError::FileStructure(e.to_string(), line!(), file!().to_string())
})?;
}
Ok(Self {
entries: categories,
header,
})
}
}
impl Flaten for CustomDestinations {
fn flaten(&self) -> Vec<HashMap<String, String>> {
let mut results: Vec<HashMap<String, String>> = Vec::new();
for entry in &self.entries {
if let Some(lnks) = &entry.entries {
for lnk in lnks {
let mut lnk_normalized = lnk.normalize();
let name_string = match lnk.get_name_string() {
Some(s) => s.to_string(),
None => String::from(""),
};
let command_line_arguments = match lnk.get_command_line_arguments() {
Some(s) => s.to_string(),
None => String::from(""),
};
lnk_normalized.insert("name_string".to_string(), name_string);
lnk_normalized
.insert("command_line_arguments".to_string(), command_line_arguments);
results.push(lnk_normalized);
}
}
}
results
}
}