chore: initial commit

This commit is contained in:
Anna 2021-10-03 15:52:21 -04:00
commit 8cb38875af
Signed by: anna
GPG Key ID: 0B391D8F06FCD9E0
37 changed files with 1287 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
Cargo.lock

19
Cargo.toml Normal file
View File

@ -0,0 +1,19 @@
[package]
name = "sestring"
version = "0.1.0"
edition = "2018"
authors = ["Anna Clemens <sestring-crate@annaclemens.io>"]
description = "SeString parser/encoder for FFXIV-related purposes."
license = "EUPL-1.2"
categories = ["ffxiv"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
byteorder = "1"
thiserror = "1"
serde = { version = "1", optional = true, features = ["derive"] }
[dev-dependencies]
lazy_static = "1"
paste = "1"

18
src/error.rs Normal file
View File

@ -0,0 +1,18 @@
use thiserror::Error;
use std::backtrace::Backtrace;
#[derive(Debug, Error)]
pub enum Error {
#[error("io error")]
Io {
#[from]
error: std::io::Error,
backtrace: Backtrace,
},
#[error("string was not valid utf-8")]
Utf8 {
#[from]
error: std::string::FromUtf8Error,
backtrace: Backtrace,
},
}

43
src/lib.rs Normal file
View File

@ -0,0 +1,43 @@
#![feature(backtrace)]
pub mod error;
pub mod payload;
#[cfg(feature = "serde")]
pub mod serde;
#[cfg(test)]
mod test;
pub use self::{
error::Error,
payload::Payload,
};
#[derive(Debug, PartialEq)]
pub struct SeString(Vec<Payload>);
impl SeString {
pub fn parse<B: AsRef<[u8]>>(bytes: B) -> Result<Self, Error> {
let payloads = Payload::parse(bytes)?;
Ok(Self(payloads))
}
pub fn encode(&self) -> Vec<u8> {
self.0
.iter()
.flat_map(|p| p.encode())
.collect()
}
pub fn text(&self) -> String {
self.0.iter()
.flat_map(|payload| {
match payload {
Payload::Text(t) => Some(&*t.0),
_ => None,
}
})
.collect()
}
}

300
src/payload.rs Normal file
View File

@ -0,0 +1,300 @@
pub mod auto_translate;
pub mod dalamud_link;
pub mod emphasis;
pub mod icon;
pub mod item;
pub mod map_link;
pub mod new_line;
pub mod player;
pub mod quest;
pub mod raw;
pub mod se_hyphen;
pub mod status;
pub mod text;
pub mod ui_foreground;
pub mod ui_glow;
pub use self::{
auto_translate::AutoTranslatePayload,
dalamud_link::DalamudLinkPayload,
emphasis::EmphasisPayload,
icon::IconPayload,
item::ItemPayload,
map_link::MapLinkPayload,
new_line::NewLinePayload,
player::PlayerPayload,
quest::QuestPayload,
raw::RawPayload,
se_hyphen::SeHyphenPayload,
status::StatusPayload,
text::TextPayload,
ui_foreground::UiForegroundPayload,
ui_glow::UiGlowPayload,
};
use crate::Error;
use std::io::{Read, Cursor, Seek, SeekFrom};
#[derive(Debug, PartialEq)]
pub enum Payload {
AutoTranslate(AutoTranslatePayload),
DalamudLink(DalamudLinkPayload),
Emphasis(EmphasisPayload),
Icon(IconPayload),
Item(ItemPayload),
MapLink(MapLinkPayload),
NewLine(NewLinePayload),
Player(PlayerPayload),
Quest(QuestPayload),
Raw(RawPayload),
SeHyphen(SeHyphenPayload),
Status(StatusPayload),
Text(TextPayload),
UiForeground(UiForegroundPayload),
UiGlow(UiGlowPayload),
}
impl Payload {
const START_BYTE: u8 = 0x02;
const END_BYTE: u8 = 0x03;
pub fn parse<B: AsRef<[u8]>>(bytes: B) -> Result<Vec<Self>, Error> {
let bytes = bytes.as_ref();
let num_bytes = bytes.len();
let mut cursor = Cursor::new(bytes);
let mut payloads = Vec::with_capacity(1);
while (cursor.position() as usize) < num_bytes {
payloads.push(Self::read_one(&mut cursor)?);
}
Ok(payloads)
}
pub fn read_one<R: Read + Seek>(mut reader: R) -> Result<Self, Error> {
use byteorder::ReadBytesExt;
let initial = reader.read_u8().map_err(Error::from)?;
reader.seek(SeekFrom::Current(-1)).map_err(Error::from)?;
if initial != Self::START_BYTE {
// chunk len doesn't matter here
TextPayload::decode(&mut reader, 0).map(Self::Text)
} else {
// seek back forward
reader.seek(SeekFrom::Current(1)).map_err(Error::from)?;
let post_start = reader.stream_position().map_err(Error::from)?;
let chunk_type = reader.read_u8().map_err(Error::from)?;
let chunk_len = TextPayload::read_integer(&mut reader)? as usize;
let start = reader.stream_position().map_err(Error::from)?;
let chunk = match SeStringChunkKind::from_u8(chunk_type) {
SeStringChunkKind::Emphasis => EmphasisPayload::decode(&mut reader, chunk_len).map(Self::Emphasis)?,
SeStringChunkKind::NewLine => Self::NewLine(NewLinePayload),
SeStringChunkKind::SeHyphen => Self::SeHyphen(SeHyphenPayload),
SeStringChunkKind::Interactable => {
let sub_type = reader.read_u8().map_err(Error::from)?;
match SeInteractableKind::from_u8(sub_type) {
SeInteractableKind::PlayerName => PlayerPayload::decode(&mut reader, chunk_len).map(Self::Player)?,
SeInteractableKind::ItemLink => ItemPayload::decode(&mut reader, chunk_len).map(Self::Item)?,
SeInteractableKind::MapPositionLink => MapLinkPayload::decode(&mut reader, chunk_len).map(Self::MapLink)?,
SeInteractableKind::QuestLink => QuestPayload::decode(&mut reader, chunk_len).map(Self::Quest)?,
SeInteractableKind::Status => StatusPayload::decode(&mut reader, chunk_len).map(Self::Status)?,
SeInteractableKind::DalamudLink => DalamudLinkPayload::decode(&mut reader, chunk_len).map(Self::DalamudLink)?,
SeInteractableKind::LinkTerminator | SeInteractableKind::Unknown(_) => {
reader.seek(SeekFrom::Start(post_start)).map_err(Error::from)?;
let additional = start - post_start;
RawPayload::decode(&mut reader, chunk_len + additional as usize).map(Self::Raw)?
}
}
}
SeStringChunkKind::AutoTranslate => AutoTranslatePayload::decode(&mut reader, chunk_len).map(Self::AutoTranslate)?,
SeStringChunkKind::UiForeground => UiForegroundPayload::decode(&mut reader, chunk_len).map(Self::UiForeground)?,
SeStringChunkKind::UiGlow => UiGlowPayload::decode(&mut reader, chunk_len).map(Self::UiGlow)?,
SeStringChunkKind::Icon => IconPayload::decode(&mut reader, chunk_len).map(Self::Icon)?,
SeStringChunkKind::Unknown(_) => {
reader.seek(SeekFrom::Start(post_start)).map_err(Error::from)?;
let additional = start - post_start;
RawPayload::decode(&mut reader, chunk_len + additional as usize).map(Self::Raw)?
},
};
// skip whatever's left
let total_read = reader.stream_position().map_err(Error::from)? - start;
// +1 for END_BYTE
reader.seek(SeekFrom::Current((chunk_len as u64 - total_read + 1) as i64)).map_err(Error::from)?;
Ok(chunk)
}
}
pub fn encode(&self) -> Vec<u8> {
match self {
Payload::AutoTranslate(p) => p.encode(),
Payload::DalamudLink(p) => p.encode(),
Payload::Emphasis(p) => p.encode(),
Payload::Icon(p) => p.encode(),
Payload::Item(p) => p.encode(),
Payload::MapLink(p) => p.encode(),
Payload::NewLine(p) => p.encode(),
Payload::Player(p) => p.encode(),
Payload::Quest(p) => p.encode(),
Payload::Raw(p) => p.encode(),
Payload::SeHyphen(p) => p.encode(),
Payload::Status(p) => p.encode(),
Payload::Text(p) => p.encode(),
Payload::UiForeground(p) => p.encode(),
Payload::UiGlow(p) => p.encode(),
}
}
}
trait Encode {
fn encode(&self) -> Vec<u8>;
fn make_integer(int: u32) -> Vec<u8> {
use byteorder::{ByteOrder, LittleEndian};
let mut vec = Vec::with_capacity(4);
if int < 0xCF {
vec.push(int as u8 + 1);
return vec;
}
let mut bytes = [0; 4];
LittleEndian::write_u32(&mut bytes, int);
vec.push(0xF0);
for i in (0..4).rev() {
if bytes[i] != 0 {
vec.push(bytes[i]);
vec[0] |= 1 << i;
}
}
vec[0] -= 1;
vec
}
fn make_packed_integers(high: u32, low: u32) -> Vec<u8> {
let value = (high << 16) | (low & 0xFF);
Self::make_integer(value)
}
}
trait Decode {
fn decode<R: Read + Seek>(reader: R, chunk_len: usize) -> Result<Self, crate::Error>
where Self: Sized;
fn read_integer<R: Read>(mut reader: R) -> Result<u32, crate::Error> {
use byteorder::{ByteOrder, LittleEndian, ReadBytesExt};
let mut marker = reader.read_u8().map_err(Error::from)?;
if marker < 0xD0 {
return Ok((marker - 1) as u32);
}
marker = (marker + 1) & 0b1111;
let mut ret = [0; 4];
for i in (0..4).rev() {
ret[i] = if (marker & (1 << i)) == 0 {
0
} else {
reader.read_u8().map_err(Error::from)?
};
}
Ok(LittleEndian::read_u32(&ret))
}
fn read_packed_integers<R: Read>(reader: R) -> Result<(u32, u32), crate::Error> {
let value = Self::read_integer(reader)?;
Ok((value >> 16, value & 0xFFFF))
}
}
pub(crate) enum SeStringChunkKind {
Icon,
Emphasis,
NewLine,
SeHyphen,
Interactable,
AutoTranslate,
UiForeground,
UiGlow,
Unknown(u8),
}
impl SeStringChunkKind {
fn from_u8(byte: u8) -> Self {
match byte {
0x12 => Self::Icon,
0x1A => Self::Emphasis,
0x10 => Self::NewLine,
0x1F => Self::SeHyphen,
0x27 => Self::Interactable,
0x2E => Self::AutoTranslate,
0x48 => Self::UiForeground,
0x49 => Self::UiGlow,
x => Self::Unknown(x),
}
}
pub(crate) fn as_u8(&self) -> u8 {
match self {
Self::Icon => 0x12,
Self::Emphasis => 0x1A,
Self::NewLine => 0x10,
Self::SeHyphen => 0x1F,
Self::Interactable => 0x27,
Self::AutoTranslate => 0x2E,
Self::UiForeground => 0x48,
Self::UiGlow => 0x49,
Self::Unknown(x) => *x,
}
}
}
enum SeInteractableKind {
PlayerName,
ItemLink,
MapPositionLink,
QuestLink,
Status,
DalamudLink,
LinkTerminator,
Unknown(u8),
}
impl SeInteractableKind {
fn from_u8(byte: u8) -> Self {
match byte {
0x01 => Self::PlayerName,
0x03 => Self::ItemLink,
0x04 => Self::MapPositionLink,
0x05 => Self::QuestLink,
0x09 => Self::Status,
0x0F => Self::DalamudLink,
0xCF => Self::LinkTerminator,
x => Self::Unknown(x),
}
}
fn as_u8(&self) -> u8 {
match self {
Self::PlayerName => 0x01,
Self::ItemLink => 0x03,
Self::MapPositionLink => 0x04,
Self::QuestLink => 0x05,
Self::Status => 0x09,
Self::DalamudLink => 0x0F,
Self::LinkTerminator => 0xCF,
Self::Unknown(x) => *x,
}
}
}

View File

@ -0,0 +1,39 @@
use crate::{Error, payload::{Decode, Encode}, Payload};
use std::{
io::{Read, Seek},
iter::once,
};
use byteorder::ReadBytesExt;
use crate::payload::SeStringChunkKind;
#[derive(Debug, PartialEq)]
pub struct AutoTranslatePayload {
pub group: u8,
pub key: u32,
}
impl Decode for AutoTranslatePayload {
fn decode<R: Read + Seek>(mut reader: R, _chunk_len: usize) -> Result<Self, Error> {
let group = reader.read_u8().map_err(Error::from)?;
let key = Self::read_integer(reader)?;
Ok(Self {
group,
key,
})
}
}
impl Encode for AutoTranslatePayload {
fn encode(&self) -> Vec<u8> {
let key = Self::make_integer(self.key);
let chunk_len = key.len() + 2;
once(Payload::START_BYTE)
.chain(once(SeStringChunkKind::AutoTranslate.as_u8()))
.chain(once(chunk_len as u8))
.chain(once(self.group))
.chain(key.into_iter())
.chain(once(Payload::END_BYTE))
.collect()
}
}

View File

@ -0,0 +1,48 @@
use crate::{Error, payload::{Decode, Encode}, Payload};
use std::io::{Read, Seek};
use byteorder::ReadBytesExt;
use crate::payload::{SeStringChunkKind, SeInteractableKind};
#[derive(Debug, PartialEq)]
pub struct DalamudLinkPayload {
pub plugin: String,
pub command: u32,
}
impl Decode for DalamudLinkPayload {
fn decode<R: Read + Seek>(mut reader: R, _chunk_len: usize) -> Result<Self, Error> {
let plugin_len = reader.read_u8().map_err(Error::from)?;
let mut plugin_bytes = vec![0; plugin_len as usize];
reader.read_exact(&mut plugin_bytes).map_err(Error::from)?;
let plugin = String::from_utf8(plugin_bytes).map_err(Error::from)?;
let command = Self::read_integer(reader)?;
Ok(Self {
plugin,
command,
})
}
}
impl Encode for DalamudLinkPayload {
fn encode(&self) -> Vec<u8> {
use std::iter::once;
let plugin = self.plugin.as_bytes();
let command = Self::make_integer(self.command);
let chunk_len = 3 + plugin.len() + command.len();
// FIXME: check if chunk_len > 255
once(Payload::START_BYTE)
.chain(once(SeStringChunkKind::Interactable.as_u8()))
.chain(once(chunk_len as u8))
.chain(once(SeInteractableKind::DalamudLink.as_u8()))
.chain(once(plugin.len() as u8))
.chain(plugin.iter().copied())
.chain(command.into_iter())
.chain(once(Payload::END_BYTE))
.collect()
}
}

49
src/payload/emphasis.rs Normal file
View File

@ -0,0 +1,49 @@
use crate::payload::{Decode, Encode, SeStringChunkKind};
use std::io::{Read, Seek};
use crate::Payload;
#[derive(Debug, PartialEq)]
pub struct EmphasisPayload(pub bool);
impl EmphasisPayload {
pub fn enable() -> Self {
Self(true)
}
pub fn disable() -> Self {
Self(false)
}
pub fn enabled(&self) -> bool {
self.0
}
}
impl From<bool> for EmphasisPayload {
fn from(enabled: bool) -> Self {
Self(enabled)
}
}
impl Decode for EmphasisPayload {
fn decode<R: Read + Seek>(reader: R, _chunk_len: usize) -> Result<Self, crate::Error> {
let enabled = Self::read_integer(reader)?;
Ok(Self(enabled == 1))
}
}
impl Encode for EmphasisPayload {
fn encode(&self) -> Vec<u8> {
use std::iter::once;
let enabled = Self::make_integer(if self.0 { 1 } else { 0 });
let chunk_len = enabled.len() + 1;
once(Payload::START_BYTE)
.chain(once(SeStringChunkKind::Emphasis.as_u8()))
.chain(once(chunk_len as u8))
.chain(enabled.into_iter())
.chain(once(Payload::END_BYTE))
.collect()
}
}

35
src/payload/icon.rs Normal file
View File

@ -0,0 +1,35 @@
use crate::payload::{Decode, Encode, SeStringChunkKind};
use std::io::{Read, Seek};
use crate::Payload;
#[derive(Debug, PartialEq)]
pub struct IconPayload(pub u32);
impl From<u32> for IconPayload {
fn from(icon: u32) -> Self {
Self(icon)
}
}
impl Decode for IconPayload {
fn decode<R: Read + Seek>(reader: R, _chunk_len: usize) -> Result<Self, crate::Error> {
let icon = Self::read_integer(reader)?;
Ok(Self(icon))
}
}
impl Encode for IconPayload {
fn encode(&self) -> Vec<u8> {
use std::iter::once;
let index = Self::make_integer(self.0);
let chunk_len = index.len() + 1;
once(Payload::START_BYTE)
.chain(once(SeStringChunkKind::Icon.as_u8()))
.chain(once(chunk_len as u8))
.chain(index.into_iter())
.chain(once(Payload::END_BYTE))
.collect()
}
}

76
src/payload/item.rs Normal file
View File

@ -0,0 +1,76 @@
use crate::{Error, payload::{Decode, Encode}, Payload};
use std::io::{Read, Seek, SeekFrom};
use crate::payload::{SeStringChunkKind, SeInteractableKind};
#[derive(Debug, PartialEq)]
pub struct ItemPayload {
pub id: u32,
pub hq: bool,
pub name: Option<String>,
}
impl ItemPayload {
const HQ_THRESHOLD: u32 = 1_000_000;
}
impl Decode for ItemPayload {
fn decode<R: Read + Seek>(mut reader: R, chunk_len: usize) -> Result<Self, Error> {
let mut id = Self::read_integer(&mut reader)?;
let hq = id > Self::HQ_THRESHOLD;
if hq {
id -= Self::HQ_THRESHOLD;
}
let name = if reader.stream_position().map_err(Error::from)? + 3 < chunk_len as u64 {
reader.seek(SeekFrom::Current(3)).map_err(Error::from)?; // unk
let name_len = Self::read_integer(&mut reader)?;
let mut name_bytes = vec![0; name_len as usize];
reader.read_exact(&mut name_bytes).map_err(Error::from)?;
Some(String::from_utf8(name_bytes).map_err(Error::from)?)
} else {
None
};
Ok(Self {
id,
hq,
name,
})
}
}
impl Encode for ItemPayload {
fn encode(&self) -> Vec<u8> {
use std::iter::once;
let actual_id = if self.hq { self.id + Self::HQ_THRESHOLD } else { self.id };
let id = Self::make_integer(actual_id);
let name_bytes = self.name.as_ref().map(|x| x.as_bytes());
let mut chunk_len = 4 + id.len();
if let Some(name_bytes) = &name_bytes {
chunk_len += 1 + 1 + name_bytes.len();
}
let name_chain: Vec<u8> = match name_bytes {
Some(bs) => once(0xFF)
.chain(once((bs.len() + 1) as u8))
.chain(bs.iter().copied())
.collect(),
None => Vec::new(),
};
once(Payload::START_BYTE)
.chain(once(SeStringChunkKind::Interactable.as_u8()))
.chain(once(chunk_len as u8))
.chain(once(SeInteractableKind::ItemLink.as_u8()))
.chain(id.into_iter())
.chain(once(0x02))
.chain(once(0x01))
.chain(name_chain.into_iter())
.chain(once(Payload::END_BYTE))
.collect()
}
}

51
src/payload/map_link.rs Normal file
View File

@ -0,0 +1,51 @@
use crate::{Error, payload::{Decode, Encode}, Payload};
use std::io::{Read, Seek, SeekFrom};
use crate::payload::{SeStringChunkKind, SeInteractableKind};
#[derive(Debug, PartialEq)]
pub struct MapLinkPayload {
pub territory_type: u32,
pub map: u32,
pub raw_x: i32,
pub raw_y: i32,
}
impl Decode for MapLinkPayload {
fn decode<R: Read + Seek>(mut reader: R, _chunk_len: usize) -> Result<Self, Error> {
let (territory_type, map) = Self::read_packed_integers(&mut reader)?;
let raw_x = Self::read_integer(&mut reader)? as i32;
let raw_y = Self::read_integer(&mut reader)? as i32;
reader.seek(SeekFrom::Current(2)).map_err(Error::from)?;
Ok(Self {
territory_type,
map,
raw_x,
raw_y,
})
}
}
impl Encode for MapLinkPayload {
fn encode(&self) -> Vec<u8> {
use std::iter::once;
let territory_map_packed = Self::make_packed_integers(self.territory_type, self.map);
let x = Self::make_integer(self.raw_x as u32);
let y = Self::make_integer(self.raw_y as u32);
let chunk_len = 4 + territory_map_packed.len() + x.len() + y.len();
once(Payload::START_BYTE)
.chain(once(SeStringChunkKind::Interactable.as_u8()))
.chain(once(chunk_len as u8))
.chain(once(SeInteractableKind::MapPositionLink.as_u8()))
.chain(territory_map_packed.into_iter())
.chain(x.into_iter())
.chain(y.into_iter())
.chain(once(0xFF))
.chain(once(0x01))
.chain(once(Payload::END_BYTE))
.collect()
}
}

16
src/payload/new_line.rs Normal file
View File

@ -0,0 +1,16 @@
use crate::payload::{Encode, SeStringChunkKind};
use crate::Payload;
#[derive(Debug, PartialEq)]
pub struct NewLinePayload;
impl Encode for NewLinePayload {
fn encode(&self) -> Vec<u8> {
vec![
Payload::START_BYTE,
SeStringChunkKind::NewLine.as_u8(),
0x01,
Payload::END_BYTE,
]
}
}

53
src/payload/player.rs Normal file
View File

@ -0,0 +1,53 @@
use crate::{Error, payload::{Decode, Encode}, Payload};
use std::io::{Read, Seek, SeekFrom};
use crate::payload::{SeStringChunkKind, SeInteractableKind};
#[derive(Debug, PartialEq)]
pub struct PlayerPayload {
pub server_id: u32,
pub name: String,
}
impl Decode for PlayerPayload {
fn decode<R: Read + Seek>(mut reader: R, _chunk_len: usize) -> Result<Self, Error> {
reader.seek(SeekFrom::Current(1)).map_err(Error::from)?; // unk
let server_id = Self::read_integer(&mut reader)?;
reader.seek(SeekFrom::Current(2)).map_err(Error::from)?; // unk
let name_len = Self::read_integer(&mut reader)?;
let mut name_bytes = vec![0; name_len as usize];
reader.read_exact(&mut name_bytes).map_err(Error::from)?;
let name = String::from_utf8(name_bytes).map_err(Error::from)?;
Ok(Self {
server_id,
name,
})
}
}
impl Encode for PlayerPayload {
fn encode(&self) -> Vec<u8> {
use std::iter::once;
let name = self.name.as_bytes();
let name_len = Self::make_integer(name.len() as u32);
let server = Self::make_integer(self.server_id);
let chunk_len = name.len() + server.len() + name_len.len() + 5;
once(Payload::START_BYTE)
.chain(once(SeStringChunkKind::Interactable.as_u8()))
.chain(once(chunk_len as u8))
.chain(once(SeInteractableKind::PlayerName.as_u8()))
.chain(once(0x01))
.chain(server.into_iter())
.chain(once(0x01))
.chain(once(0xFF))
.chain(name_len.into_iter())
.chain(name.iter().copied())
.chain(once(Payload::END_BYTE))
.collect()
}
}

42
src/payload/quest.rs Normal file
View File

@ -0,0 +1,42 @@
use crate::payload::{Decode, Encode, SeStringChunkKind, SeInteractableKind};
use std::io::{Read, Seek};
use crate::Payload;
#[derive(Debug, PartialEq)]
pub struct QuestPayload(pub u32);
impl QuestPayload {
const OFFSET: u32 = 65536;
}
impl From<u32> for QuestPayload {
fn from(id: u32) -> Self {
Self(id)
}
}
impl Decode for QuestPayload {
fn decode<R: Read + Seek>(reader: R, _chunk_len: usize) -> Result<Self, crate::Error> {
let id = Self::read_integer(reader)? + Self::OFFSET;
Ok(Self(id))
}
}
impl Encode for QuestPayload {
fn encode(&self) -> Vec<u8> {
use std::iter::once;
let id = Self::make_integer(self.0 - Self::OFFSET);
let chunk_len = id.len() + 4;
once(Payload::START_BYTE)
.chain(once(SeStringChunkKind::Interactable.as_u8()))
.chain(once(chunk_len as u8))
.chain(once(SeInteractableKind::QuestLink.as_u8()))
.chain(id.into_iter())
.chain(once(0x01))
.chain(once(0x01))
.chain(once(Payload::END_BYTE))
.collect()
}
}

31
src/payload/raw.rs Normal file
View File

@ -0,0 +1,31 @@
use crate::{Error, payload::{Decode, Encode}, Payload};
use std::io::{Read, Seek};
#[derive(Debug, PartialEq)]
pub struct RawPayload(pub Vec<u8>);
impl AsRef<[u8]> for RawPayload {
fn as_ref(&self) -> &[u8] {
&self.0
}
}
impl Decode for RawPayload {
fn decode<R: Read + Seek>(mut reader: R, chunk_len: usize) -> Result<Self, Error> {
let mut data = vec![0; chunk_len];
reader.read_exact(&mut data).map_err(Error::from)?;
Ok(Self(data))
}
}
impl Encode for RawPayload {
fn encode(&self) -> Vec<u8> {
use std::iter::once;
once(Payload::START_BYTE)
.chain(self.0.iter().copied())
.chain(once(Payload::END_BYTE))
.collect()
}
}

16
src/payload/se_hyphen.rs Normal file
View File

@ -0,0 +1,16 @@
use crate::payload::{Encode, SeStringChunkKind};
use crate::Payload;
#[derive(Debug, PartialEq)]
pub struct SeHyphenPayload;
impl Encode for SeHyphenPayload {
fn encode(&self) -> Vec<u8> {
vec![
Payload::START_BYTE,
SeStringChunkKind::SeHyphen.as_u8(),
0x01,
Payload::END_BYTE,
]
}
}

41
src/payload/status.rs Normal file
View File

@ -0,0 +1,41 @@
use crate::payload::{Decode, Encode, SeStringChunkKind, SeInteractableKind};
use std::io::{Read, Seek};
use crate::Payload;
#[derive(Debug, PartialEq)]
pub struct StatusPayload(pub u32);
impl From<u32> for StatusPayload {
fn from(id: u32) -> Self {
Self(id)
}
}
impl Decode for StatusPayload {
fn decode<R: Read + Seek>(reader: R, _chunk_len: usize) -> Result<Self, crate::Error> {
let id = Self::read_integer(reader)?;
Ok(Self(id))
}
}
impl Encode for StatusPayload {
fn encode(&self) -> Vec<u8> {
use std::iter::once;
let id = Self::make_integer(self.0);
let chunk_len = id.len() + 7;
once(Payload::START_BYTE)
.chain(once(SeStringChunkKind::Interactable.as_u8()))
.chain(once(chunk_len as u8))
.chain(once(SeInteractableKind::Status.as_u8()))
.chain(id.into_iter())
.chain(once(0x01))
.chain(once(0x01))
.chain(once(0xFF))
.chain(once(0x02))
.chain(once(0x20))
.chain(once(Payload::END_BYTE))
.collect()
}
}

72
src/payload/text.rs Normal file
View File

@ -0,0 +1,72 @@
use crate::{
Error,
payload::{Decode, Encode},
};
use std::io::{Read, Seek, SeekFrom};
use crate::Payload;
#[derive(Debug, PartialEq)]
pub struct TextPayload(pub String);
impl<T: Into<String>> From<T> for TextPayload {
fn from(text: T) -> Self {
Self(text.into())
}
}
impl AsRef<str> for TextPayload {
fn as_ref(&self) -> &str {
&self.0
}
}
impl Decode for TextPayload {
fn decode<R: Read + Seek>(mut reader: R, _chunk_len: usize) -> Result<Self, crate::Error> {
let mut text = Vec::with_capacity(32);
let mut buf = [0; 32];
let mut read: usize;
loop {
read = reader.read(&mut buf).map_err(Error::from)?;
// end of stream
if read == 0 {
break;
}
let start = buf[..read].iter().position(|&b| b == Payload::START_BYTE);
if let Some(start) = start {
// 5 4 3 5 4 5 = 6 bytes read
// ^ - start byte at idx 2
// seek backwards until start byte is next to read: read - idx = 4 bytes
let offset = (read - start) as i64;
reader.seek(SeekFrom::Current(-offset)).map_err(Error::from)?;
// tell the rest of the loop that we only read up to the start byte
read = start as usize;
}
for &byte in &buf[..read] {
text.push(byte);
}
// we encountered a real payload, so break now
if start.is_some() {
break;
}
}
let text = if text.len() > 0 {
String::from_utf8(text).map_err(Error::from)?
} else {
String::new()
};
Ok(Self(text))
}
}
impl Encode for TextPayload {
fn encode(&self) -> Vec<u8> {
self.0.as_bytes().to_vec()
}
}

View File

@ -0,0 +1,35 @@
use crate::payload::{Decode, Encode, SeStringChunkKind};
use std::io::{Read, Seek};
use crate::Payload;
#[derive(Debug, PartialEq)]
pub struct UiForegroundPayload(pub u32);
impl From<u32> for UiForegroundPayload {
fn from(colour: u32) -> Self {
Self(colour)
}
}
impl Decode for UiForegroundPayload {
fn decode<R: Read + Seek>(reader: R, _chunk_len: usize) -> Result<Self, crate::Error> {
let colour = Self::read_integer(reader)?;
Ok(Self(colour))
}
}
impl Encode for UiForegroundPayload {
fn encode(&self) -> Vec<u8> {
use std::iter::once;
let colour = Self::make_integer(self.0);
let chunk_len = colour.len() + 1;
once(Payload::START_BYTE)
.chain(once(SeStringChunkKind::UiForeground.as_u8()))
.chain(once(chunk_len as u8))
.chain(colour.into_iter())
.chain(once(Payload::END_BYTE))
.collect()
}
}

35
src/payload/ui_glow.rs Normal file
View File

@ -0,0 +1,35 @@
use crate::payload::{Decode, Encode, SeStringChunkKind};
use std::io::{Read, Seek};
use crate::Payload;
#[derive(Debug, PartialEq)]
pub struct UiGlowPayload(pub u32);
impl From<u32> for UiGlowPayload {
fn from(colour: u32) -> Self {
Self(colour)
}
}
impl Decode for UiGlowPayload {
fn decode<R: Read + Seek>(reader: R, _chunk_len: usize) -> Result<Self, crate::Error> {
let colour = Self::read_integer(reader)?;
Ok(Self(colour))
}
}
impl Encode for UiGlowPayload {
fn encode(&self) -> Vec<u8> {
use std::iter::once;
let colour = Self::make_integer(self.0);
let chunk_len = colour.len() + 1;
once(Payload::START_BYTE)
.chain(once(SeStringChunkKind::UiGlow.as_u8()))
.chain(once(chunk_len as u8))
.chain(colour.into_iter())
.chain(once(Payload::END_BYTE))
.collect()
}
}

19
src/serde.rs Normal file
View File

@ -0,0 +1,19 @@
use serde::{Serialize, Serializer, Deserialize, Deserializer};
use crate::SeString;
impl Serialize for SeString {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where S: Serializer
{
self.encode().serialize(serializer)
}
}
impl<'de> Deserialize<'de> for SeString {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where D: Deserializer<'de>
{
let bytes = Vec::<u8>::deserialize(deserializer)?;
SeString::parse(&bytes).map_err(|e| serde::de::Error::custom(format!("invalid sestring: {:?}", e)))
}
}

View File

@ -0,0 +1,14 @@
use crate::{Payload, SeString};
use crate::payload::AutoTranslatePayload;
super::basic_tests! {
auto_translate {
payload = &[2, 46, 5, 5, 242, 2, 12, 3];
expected = SeString(vec![
Payload::AutoTranslate(AutoTranslatePayload {
group: 5,
key: 524,
}),
]);
}
}

16
src/test/dalamud_link.rs Normal file
View File

@ -0,0 +1,16 @@
use crate::{Payload, SeString};
use crate::payload::{DalamudLinkPayload, RawPayload, TextPayload};
super::basic_tests! {
dalamud_link {
payload = &[2, 39, 22, 15, 18, 83, 105, 109, 112, 108, 101, 84, 119, 101, 97, 107, 115, 80, 108, 117, 103, 105, 110, 2, 3, 104, 116, 116, 112, 115, 58, 47, 47, 101, 120, 97, 109, 112, 108, 101, 46, 99, 111, 109, 47, 2, 39, 7, 207, 1, 1, 1, 255, 1, 3];
expected = SeString(vec![
Payload::DalamudLink(DalamudLinkPayload {
plugin: "SimpleTweaksPlugin".to_string(),
command: 1,
}),
Payload::Text(TextPayload::from("https://example.com/")),
Payload::Raw(RawPayload(vec![39, 7, 207, 1, 1, 1, 255, 1])),
]);
}
}

15
src/test/emphasis.rs Normal file
View File

@ -0,0 +1,15 @@
use crate::{Payload, SeString};
use crate::payload::{EmphasisPayload, TextPayload};
super::basic_tests! {
emphasis {
payload = &[78, 111, 119, 32, 112, 108, 97, 121, 105, 110, 103, 32, 2, 26, 2, 2, 3, 87, 97, 105, 108, 101, 114, 115, 32, 97, 110, 100, 32, 87, 97, 116, 101, 114, 119, 104, 101, 101, 108, 115, 2, 26, 2, 1, 3, 46];
expected = SeString(vec![
Payload::Text(TextPayload::from("Now playing ")),
Payload::Emphasis(EmphasisPayload::enable()),
Payload::Text(TextPayload::from("Wailers and Waterwheels")),
Payload::Emphasis(EmphasisPayload::disable()),
Payload::Text(TextPayload::from(".")),
]);
}
}

11
src/test/icon.rs Normal file
View File

@ -0,0 +1,11 @@
use crate::{Payload, SeString};
use crate::payload::IconPayload;
super::basic_tests! {
icon {
payload = &[2, 18, 2, 67, 3];
expected = SeString(vec![
Payload::Icon(IconPayload::from(66)),
]);
}
}

15
src/test/item.rs Normal file
View File

@ -0,0 +1,15 @@
use crate::{Payload, SeString};
use crate::payload::ItemPayload;
super::basic_tests! {
item {
payload = &[2, 39, 7, 3, 242, 19, 9, 2, 1, 3];
expected = SeString(vec![
Payload::Item(ItemPayload {
id: 4873,
hq: false,
name: None,
}),
]);
}
}

18
src/test/map_link.rs Normal file
View File

@ -0,0 +1,18 @@
use crate::{Payload, SeString};
use crate::payload::MapLinkPayload;
super::basic_tests! {
map {
payload = &[2, 39, 15, 4, 244, 132, 2, 246, 1, 16, 42, 246, 1, 130, 161, 255, 1, 3];
expected = SeString(vec!{
Payload::MapLink(MapLinkPayload {
territory_type: 132,
map: 2,
raw_x: 69674,
// x: 12.644841
raw_y: 98977,
// y: 13.231473
}),
});
}
}

52
src/test/mod.rs Normal file
View File

@ -0,0 +1,52 @@
mod auto_translate;
mod dalamud_link;
mod emphasis;
mod icon;
mod item;
mod map_link;
mod new_line;
mod player;
mod quest;
mod raw;
mod se_hyphen;
mod status;
mod text;
mod ui_foreground;
mod ui_glow;
macro_rules! basic_tests {
($prefix:ident { payload = $payload:expr; expected = $expected:expr; }) => {
const PAYLOAD: &[u8] = $payload;
lazy_static::lazy_static! {
static ref EXPECTED: crate::SeString = $expected;
}
paste::paste! {
#[test]
fn [<$prefix _parse>]() {
assert_eq!(
crate::SeString::parse(PAYLOAD).unwrap(),
*EXPECTED,
);
}
#[test]
fn [<$prefix _encode>]() {
assert_eq!(
EXPECTED.encode(),
PAYLOAD,
);
}
#[test]
fn [<$prefix _round_trip>]() {
assert_eq!(
crate::SeString::parse(PAYLOAD).unwrap().encode(),
PAYLOAD,
);
}
}
};
}
pub(crate) use basic_tests;

13
src/test/new_line.rs Normal file
View File

@ -0,0 +1,13 @@
use crate::{SeString, Payload};
use crate::payload::{TextPayload, NewLinePayload};
super::basic_tests! {
new_line {
payload = &[66, 101, 102, 111, 114, 101, 2, 16, 1, 3, 65, 102, 116, 101, 114];
expected = SeString(vec![
Payload::Text(TextPayload::from("Before")),
Payload::NewLine(NewLinePayload),
Payload::Text(TextPayload::from("After")),
]);
}
}

14
src/test/player.rs Normal file
View File

@ -0,0 +1,14 @@
use crate::{Payload, SeString};
use crate::payload::PlayerPayload;
super::basic_tests! {
player {
payload = &[2, 39, 23, 1, 1, 74, 1, 255, 17, 69, 108, 111, 114, 97, 32, 65, 98, 121, 115, 115, 105, 110, 105, 97, 110, 3];
expected = SeString(vec![
Payload::Player(PlayerPayload {
server_id: 73,
name: "Elora Abyssinian".to_string(),
}),
]);
}
}

11
src/test/quest.rs Normal file
View File

@ -0,0 +1,11 @@
use crate::{Payload, SeString};
use crate::payload::QuestPayload;
super::basic_tests! {
quest {
payload = &[2, 39, 7, 5, 242, 2, 12, 1, 1, 3];
expected = SeString(vec![
Payload::Quest(QuestPayload::from(66060)),
]);
}
}

11
src/test/raw.rs Normal file
View File

@ -0,0 +1,11 @@
use crate::{SeString, Payload};
use crate::payload::RawPayload;
super::basic_tests! {
raw {
payload = &[2, 39, 7, 207, 1, 1, 1, 255, 1, 3];
expected = SeString(vec![
Payload::Raw(RawPayload(vec![39, 7, 207, 1, 1, 1, 255, 1])),
]);
}
}

13
src/test/se_hyphen.rs Normal file
View File

@ -0,0 +1,13 @@
use crate::{SeString, Payload};
use crate::payload::{TextPayload, SeHyphenPayload};
super::basic_tests! {
se_hyphen {
payload = &[66, 101, 102, 111, 114, 101, 2, 31, 1, 3, 65, 102, 116, 101, 114];
expected = SeString(vec![
Payload::Text(TextPayload::from("Before")),
Payload::SeHyphen(SeHyphenPayload),
Payload::Text(TextPayload::from("After")),
]);
}
}

11
src/test/status.rs Normal file
View File

@ -0,0 +1,11 @@
use crate::{Payload, SeString};
use crate::payload::StatusPayload;
super::basic_tests! {
status {
payload = &[2, 39, 10, 9, 242, 1, 41, 1, 1, 255, 2, 32, 3];
expected = SeString(vec!{
Payload::Status(StatusPayload::from(297)),
});
}
}

11
src/test/text.rs Normal file
View File

@ -0,0 +1,11 @@
use crate::{Payload, SeString};
use crate::payload::TextPayload;
super::basic_tests! {
text {
payload = b"Hello, world!";
expected = SeString(vec!{
Payload::Text(TextPayload::from("Hello, world!")),
});
}
}

11
src/test/ui_foreground.rs Normal file
View File

@ -0,0 +1,11 @@
use crate::{Payload, SeString};
use crate::payload::UiForegroundPayload;
super::basic_tests! {
ui_foreground {
payload = &[2, 72, 4, 242, 1, 244, 3];
expected = SeString(vec!{
Payload::UiForeground(UiForegroundPayload::from(500)),
});
}
}

11
src/test/ui_glow.rs Normal file
View File

@ -0,0 +1,11 @@
use crate::{Payload, SeString};
use crate::payload::UiGlowPayload;
super::basic_tests! {
ui_glow {
payload = &[2, 73, 4, 242, 1, 245, 3];
expected = SeString(vec!{
Payload::UiGlow(UiGlowPayload::from(501)),
});
}
}