From d0d4ccbd55c3ecbe34ac32a39957194bd882ce4b Mon Sep 17 00:00:00 2001 From: Ethan Simmons Date: Wed, 17 Apr 2024 02:01:14 -0500 Subject: [PATCH] Initial Commit Will write more useful commit messages in the, the start of this project was extremely rushed. --- .gitignore | 1 + Cargo.lock | 39 ++++++++ Cargo.toml | 10 ++ layout.txt | 202 ++++++++++++++++++++++++++++++++++++++ src/main.rs | 28 ++++++ src/parse.rs | 42 ++++++++ src/parse/entry.rs | 97 ++++++++++++++++++ src/parse/header.rs | 175 +++++++++++++++++++++++++++++++++ src/parse/start_header.rs | 154 +++++++++++++++++++++++++++++ src/undo.rs | 50 ++++++++++ undo_file | Bin 0 -> 72572 bytes 11 files changed, 798 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 layout.txt create mode 100644 src/main.rs create mode 100644 src/parse.rs create mode 100644 src/parse/entry.rs create mode 100644 src/parse/header.rs create mode 100644 src/parse/start_header.rs create mode 100644 src/undo.rs create mode 100644 undo_file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..047c59c --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,39 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "anyhow" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" + +[[package]] +name = "memchr" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "vim_undo_extractor" +version = "0.1.0" +dependencies = [ + "anyhow", + "nom", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..45feca0 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "vim_undo_extractor" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1.0.82" +nom = "7.1.3" diff --git a/layout.txt b/layout.txt new file mode 100644 index 0000000..eb1eb21 --- /dev/null +++ b/layout.txt @@ -0,0 +1,202 @@ +START_MAGIC: 56 69 6D 9F 55 6E 44 6F E5 "Vim.UnDoF" + +Version: 00 03 + +SHA-256: A5 B4 88 3B 3A AB FB 50 30 CF 8E 57 58 30 AE 44 38 25 A8 A3 DF B2 BD DF D6 FB E6 F2 EB E8 14 45 + +Line Count: 00 00 00 7C + +Line Length : 00 00 00 16 +Line: 20 20 20 20 73 74 64 3A 3A 73 74 72 69 6E 67 20 69 6E 70 75 74 3B " std::string input;" +Line Number: 00 00 00 2B +Column Number: 00 00 00 15 + +Old Head Sequence or 0 if null: 00 00 00 01 +New Head Sequence or 0 if null: 00 00 00 83 +Current Head Sequence or 0 if null: 00 00 00 83 + +Numhead: 00 00 00 83 +Sequence Last: 00 00 00 83 +Sequence Current: 00 00 00 82 + + +Time: 00 00 00 00 66 10 CE 39 + +Optional fields + 04 + 01 + 00 00 00 40 + +End Marker: 00 + +Header Magic: 5F D0 + +Next Pointer: 00 00 00 00 +Previous Pointer: 00 00 00 02 +Alt Next Pointer: 00 00 00 00 +Alt Previous Pointer: 00 00 00 00 + +Sequence: 00 00 00 01 + +Position +Line Number: 00 00 00 54 +Column: 00 00 00 00 +Coladd: 00 00 00 00 + +Cursor VCol: FF FF FF FF +Flags: 00 01 +00 00 + +Marks +00 00 00 00 +00 00 00 00 +00 00 00 00 + +00 00 00 00 +00 00 00 00 +00 00 00 00 + +00 00 00 00 +00 00 00 00 +00 00 00 00 + +00 00 00 00 +00 00 00 00 +00 00 00 00 + +00 00 00 00 +00 00 00 00 +00 00 00 00 + +00 00 00 00 +00 00 00 00 +00 00 00 00 + +00 00 00 00 +00 00 00 00 +00 00 00 00 + +00 00 00 00 +00 00 00 00 +00 00 00 00 + +00 00 00 00 +00 00 00 00 +00 00 00 00 + +00 00 00 00 +00 00 00 00 +00 00 00 00 + +00 00 00 00 +00 00 00 00 +00 00 00 00 + +00 00 00 00 +00 00 00 00 +00 00 00 00 + +00 00 00 00 +00 00 00 00 +00 00 00 00 + +00 00 00 00 +00 00 00 00 +00 00 00 00 + +00 00 00 00 +00 00 00 00 +00 00 00 00 + +00 00 00 00 +00 00 00 00 +00 00 00 00 + +00 00 00 00 +00 00 00 00 +00 00 00 00 + +00 00 00 00 +00 00 00 00 +00 00 00 00 + +00 00 00 00 +00 00 00 00 +00 00 00 00 + +00 00 00 00 +00 00 00 00 +00 00 00 00 + +00 00 00 00 +00 00 00 00 +00 00 00 00 + +00 00 00 00 +00 00 00 00 +00 00 00 00 + +00 00 00 00 +00 00 00 00 +00 00 00 00 + +00 00 00 00 +00 00 00 00 +00 00 00 00 + +00 00 00 00 +00 00 00 00 +00 00 00 00 + +00 00 00 00 +00 00 00 00 +00 00 00 00 + + +Visual Info: + Start Position: + 00 00 00 00 + 00 00 00 00 + 00 00 00 00 + + End Position: + 00 00 00 00 + 00 00 00 00 + 00 00 00 00 + + Vi Mode: 00 00 00 00 + Vi_curswant: 00 00 00 00 + +Time: 00 00 00 00 66 10 BC DC + +Optional Fields: + 04 + 01 + 00 00 00 00 + +End Marker: 00 + +Entry Magic: F5 18 + +Entry Type: 00 00 00 00 +Entry Data: 54 00 00 00 00 00 00 00 00 05 00 00 00 00 00 00 00 + +Entry Magic: F5 18 + +Entry Type: 00 00 00 00 +Entry Data: 55 00 00 00 04 00 00 00 00 00 00 00 05 +00 00 00 00 00 00 00 05 00 00 00 71 06 00 00 00 +00 00 00 05 00 00 00 00 00 00 00 05 00 00 00 00 +00 00 00 + +Entry End Magic: 35 81 + +Header Magic: 5F D0 +00 00 00 01 00 00 00 03 00 + + + + + + + diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..0a3fa64 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,28 @@ +use anyhow::Result; + + +mod undo; +mod parse; + +use undo::UndoFile; + + +fn main() -> Result<()> { + + let undo_file = UndoFile::from_path("./undo_file")?; + + for header in undo_file.headers { + for entry in header.entries { + let section: String = entry.section + .iter() + .map(|b| char::from_u32(*b as u32).unwrap_or(' ')) + .collect(); + + println!("{}", section); + + } + } + + Ok(()) +} + diff --git a/src/parse.rs b/src/parse.rs new file mode 100644 index 0000000..e6be60e --- /dev/null +++ b/src/parse.rs @@ -0,0 +1,42 @@ +use nom::{ + bytes::complete::take, + combinator::map_res, + IResult, + error::Error, +}; + +pub(crate) mod start_header; +pub(crate) mod header; +pub(crate) mod entry; + +pub fn bytes_u32(input: &[u8]) -> IResult<&[u8], u32> { + map_res( + take(4usize), + |b: &[u8]| Ok::>(u32::from_be_bytes(b.try_into().unwrap())) + )(input) +} + +fn time(input: &[u8]) -> IResult<&[u8], u64> { + map_res( + take(8usize), + |b: &[u8]| Ok::>(u64::from_be_bytes(b.try_into().unwrap())) + )(input) +} + +#[derive(Debug)] +struct OptionalFields((u8, u8, u32)); + +fn optional_fields(input: &[u8]) -> IResult<&[u8], OptionalFields> { + map_res( + take(6usize), + |b: &[u8]| { + Ok::>( + OptionalFields (( + b[0], + b[1], + u32::from_be_bytes(b[2..6].try_into().unwrap()) + )) + ) + } + )(input) +} diff --git a/src/parse/entry.rs b/src/parse/entry.rs new file mode 100644 index 0000000..145d19e --- /dev/null +++ b/src/parse/entry.rs @@ -0,0 +1,97 @@ +use nom::{ + IResult, + Parser, + Or, + error::Error, + combinator::map_res, + sequence::{tuple, pair}, + multi::many0, + bytes::complete::{tag, take, take_until}, +}; + +use super::bytes_u32; + +#[derive(Debug, PartialEq, Eq)] +pub(crate) struct Entry { + pub(crate) entry_type: EntryType, + pub(crate) section: Vec, +} + +#[derive(Debug, PartialEq, Eq)] +enum EntryType { + Unknown(u32), +} + +fn magic(input: &[u8]) -> IResult<&[u8], &[u8]> { + tag(b"\xf5\x18")(input) +} + +fn entry(input: &[u8]) -> IResult<&[u8], Entry> { + let (input, section_type) = take(4usize)(input)?; + let (input, section) = take_until(b"\xf5\x18".as_ref())(input)?; + let (input, _) = tag(b"\xf5\x18")(input)?; + + let entry_type = match section_type { + num => EntryType::Unknown(u32::from_be_bytes(num.try_into().unwrap())), + }; + + println!("{:#?}", section); + + Ok(( + input, + Entry { + entry_type, + section: section.to_vec(), + } + )) + +} + +fn sections(input: &[u8]) -> IResult<&[u8], Vec> { + let (input, sections) = take_until(b"\x35\x81".as_ref())(input)?; + let (last_entry, mut entries) = many0(entry)(sections)?; + entries.push( + Entry { + entry_type: match last_entry[0..4] { + _ => EntryType::Unknown(u32::from_be_bytes(last_entry[0..4].try_into().unwrap())), + }, + section: last_entry[4..].to_vec(), + }); + Ok((input, entries)) +} + +pub(crate) fn parse(input: &[u8]) -> IResult<&[u8], Vec> { + let (input, ( + _, + entries, + _, + )) = tuple(( + magic, + sections, + tag(b"\x35\x81".as_ref()) + ))(input).unwrap(); + + Ok(( + input, + entries, + )) + +} + +#[cfg(test)] +mod tests { + + use super::{sections, Entry, EntryType}; + + #[test] + fn test_sections() { + let test_str = b"\x00\x00\x00\x00\xaa\xaa\xf5\x18\x00\x00\x00\x00\xaa\xaa\xf5\x18\x00\x00\x00\x00\xaa\xaa\x35\x81"; + + assert_eq!(sections(test_str).unwrap().1, vec![ + Entry { + entry_type: EntryType::Unknown, + section: vec![b'\xaa', b'\xaa'], + }, + ]) + } +} diff --git a/src/parse/header.rs b/src/parse/header.rs new file mode 100644 index 0000000..bf591b2 --- /dev/null +++ b/src/parse/header.rs @@ -0,0 +1,175 @@ +use nom::{ + IResult, + error::Error, + combinator::map_res, + sequence::tuple, + bytes::complete::{tag, take}, +}; + +use super::{bytes_u32, time, optional_fields, OptionalFields}; + +use crate::parse::entry::Entry; + +#[derive(Debug)] +pub(crate) struct Header { + pub(crate) next: u32, + pub(crate) previous: u32, + pub(crate) alt_next: u32, + pub(crate) alt_previous: u32, + pub(crate) sequence: u32, + pub(crate) position: Position, + pub(crate) cursor_vcol: u32, + pub(crate) flags: Vec, + pub(crate) marks: Vec, + pub(crate) visual_info: VisualInfo, + pub(crate) time: u64, + pub(crate) optional_fields: OptionalFields, + pub(crate) entries: Vec +} + +#[derive(Debug)] +struct Position { + line_number: u32, + column_number: u32, + coladd: u32, +} + +#[derive(Debug)] +enum Flag { + Unknown, +} + +#[derive(Debug)] +struct VisualInfo { + start: Position, + end: Position, + mode: u32, + curswant: u32, +} + +fn magic(input: &[u8]) -> IResult<&[u8], &[u8]> { + tag(b"\x5f\xd0".as_ref())(input) +} + +fn position(input: &[u8]) -> IResult<&[u8], Position> { + map_res( + tuple(( + bytes_u32, + bytes_u32, + bytes_u32, + )), + |(line_number, column_number, coladd)| { + Ok::>(Position { + line_number, + column_number, + coladd, + }) + } + )(input) +} + +fn visual_info(input: &[u8]) -> IResult<&[u8], VisualInfo> { + map_res( + tuple(( + position, + position, + bytes_u32, + bytes_u32, + )), + |(start, end, mode, curswant)| { + Ok::>( + VisualInfo { + start, + end, + mode, + curswant, + } + ) + } + )(input) +} + +fn flags(input: &[u8]) -> IResult<&[u8], Vec> { + map_res( + take(4usize), + |flags: &[u8]| { + Ok::, Error<&[u8]>>( + flags.into_iter().map(|b| { + match b { + _ => Flag::Unknown + } + }).collect() + ) + } + )(input) +} + +fn marks(input: &[u8]) ->IResult<&[u8], &[u8]> { + take(310usize)(input) +} + +fn end_marker(input: &[u8]) -> IResult<&[u8], &[u8]> { + tag(b"\x00")(input) +} + +pub(crate) fn parse(input: &[u8]) -> IResult<&[u8], Header> { + let next = bytes_u32; + let previous = bytes_u32; + let alt_next = bytes_u32; + let alt_previous = bytes_u32; + let sequence = bytes_u32; + let cursor_vcol = bytes_u32; + let time = time; + + let (input, ( + _, + next, + previous, + alt_next, + alt_previous, + sequence, + position, + cursor_vcol, + flags, + marks, + visual_info, + time, + optional_fields, + _, + )) = tuple(( + magic, + next, + previous, + alt_next, + alt_previous, + sequence, + position, + cursor_vcol, + flags, + marks, + visual_info, + time, + optional_fields, + end_marker, + ))(input).unwrap(); + + Ok(( + + input, + Header { + next, + previous, + alt_next, + alt_previous, + sequence, + position, + visual_info, + cursor_vcol, + flags, + marks: marks.to_vec(), + time, + optional_fields, + entries: Vec::new(), + } + )) +} diff --git a/src/parse/start_header.rs b/src/parse/start_header.rs new file mode 100644 index 0000000..63782c5 --- /dev/null +++ b/src/parse/start_header.rs @@ -0,0 +1,154 @@ +use nom::{ + IResult, + error::Error, + combinator::map_res, + sequence::tuple, + bytes::complete::{tag, take}, +}; + +use super::{bytes_u32, time, optional_fields, OptionalFields}; + + +fn magic(input: &[u8]) -> IResult<&[u8], &[u8]> { + tag(b"\x56\x69\x6D\x9F\x55\x6E\x44\x6F\xE5")(input) +} + +fn version(input: &[u8]) -> IResult<&[u8], u16> { + map_res( + take(2usize), + |b: &[u8]| Ok::>(u16::from_be_bytes(b.try_into().unwrap())) + )(input) +} + +fn hash(input: &[u8]) -> IResult<&[u8], &[u8; 32]> { + map_res( + take(32usize), + |b: &[u8]| Ok::<&[u8; 32], Error<&[u8]>>(b.try_into().unwrap()) + )(input) +} + +fn line_with_length(input: &[u8]) -> IResult<&[u8], String> { + let (input, line_length) = bytes_u32(input)?; + map_res( + take(line_length), + |b: &[u8]| Ok::>(String::from_utf8(b.to_vec()).expect("Invalid UTF-8")) + )(input) +} + + + +fn end_marker(input: &[u8]) -> IResult<&[u8], &[u8]> { + tag(b"\x00".as_ref())(input) +} + +#[derive(Debug)] +pub(crate) struct StartHeader { + version: u16, + hash: Vec, + line_count: u32, + line: String, + line_number: u32, + column_number: u32, + old_head_sequence: u32, + new_head_sequence: u32, + current_head_sequence: u32, + numhead: u32, + last_sequence: u32, + current_sequence: u32, + time: u64, + optional_fields: OptionalFields, +} + +pub(crate) fn parse(input: &[u8]) -> IResult<&[u8], StartHeader> { + let line_count = bytes_u32; + let line_number = bytes_u32; + let column_number = bytes_u32; + let old_head_sequence = bytes_u32; + let new_head_sequence = bytes_u32; + let current_head_sequence = bytes_u32; + let numhead = bytes_u32; + let last_sequence = bytes_u32; + let current_sequence = bytes_u32; + + + let (input, ( + _, + version, + hash, + line_count, + line, + line_number, + column_number, + old_head_sequence, + new_head_sequence, + current_head_sequence, + numhead, + last_sequence, + current_sequence, + time, + optional_fields, + _, + )) = tuple (( + magic, + version, + hash, + line_count, + line_with_length, + line_number, + column_number, + old_head_sequence, + new_head_sequence, + current_head_sequence, + numhead, + last_sequence, + current_sequence, + time, + optional_fields, + end_marker, + ))(&input).unwrap(); + + Ok( + ( + input, + StartHeader { + version, + hash: hash.to_vec(), + line_count, + line, + line_number, + column_number, + old_head_sequence, + new_head_sequence, + current_head_sequence, + numhead, + last_sequence, + current_sequence, + time, + optional_fields, + } + ) + ) +} + +#[cfg(test)] +mod tests { + use super::{magic, version}; + + #[test] + fn test_magic() { + let test_str = b"\x56\x69\x6d\x9f\x55\x6e\x44\x6f\xe5\x00\x03"; + + let (_, magic) = magic(test_str).unwrap(); + + assert_eq!(magic, b"\x56\x69\x6d\x9f\x55\x6e\x44\x6f\xe5"); + } + + #[test] + fn test_version() { + let test_str = b"\x00\x03\xa5\xb4"; + + let (_, version) = version(test_str).unwrap(); + + assert_eq!(version, 3); + } +} diff --git a/src/undo.rs b/src/undo.rs new file mode 100644 index 0000000..5e466e4 --- /dev/null +++ b/src/undo.rs @@ -0,0 +1,50 @@ +use std::fs::File; +use std::io::Read; + +use anyhow::{Result, anyhow}; + +use crate::parse::start_header::{self, StartHeader}; +use crate::parse::header::{self, Header}; +use crate::parse::entry::{self, Entry}; + +use nom::{ + sequence::tuple, + multi::many0, + bytes::complete::{take_until, tag}, +}; + +#[derive(Debug)] +pub(crate) struct UndoFile { + pub(crate) start_header: StartHeader, + pub(crate) headers: Vec
, +} + +impl UndoFile { + pub(crate) fn from_path(path: &str) -> Result { + let mut buffer: Vec = Vec::new(); + let mut file = File::open(path)?; + + let _ = match file.read_to_end(&mut buffer) { + Ok(_) => Ok(()), + Err(e) => Err(anyhow!(e)), + }; + + let (input, start_header) = start_header::parse(&buffer).unwrap(); + let (_, out) = many0(tuple((header::parse, entry::parse, take_until(b"\x5f\xd0".as_ref()))))(input).unwrap(); + + let headers = out + .into_iter() + .map(|(mut header, entries, _)| { + entries.into_iter().for_each(|entry| header.entries.push(entry)); + header + }).collect(); + + Ok( + Self { + start_header, + headers, + } + ) + } + +} diff --git a/undo_file b/undo_file new file mode 100644 index 0000000000000000000000000000000000000000..73370a5a74d2785a1f8e3163632650647f7ab11e GIT binary patch literal 72572 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 YvFctkF-KM+4kcxkby#gV{P-jP2dKqgt^fc4 literal 0 HcmV?d00001