use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value, json};
use uuid::Uuid;
use crate::common::{Block, EditOp, ImageSource, Message, Meta, Role, Tool, ToolOutput};
use crate::error::{Error, Result};
use crate::transcript::{Codec, Common, Discovered, Harness, Saved, Store, TextCodec, Transcript};
#[cfg(feature = "opencode")]
use rusqlite::{Connection, OpenFlags, params};
#[cfg(feature = "opencode")]
use std::fs;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Cursor;
impl Harness for Cursor {
const NAME: &'static str = "cursor";
type Body = CursorDb;
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CursorDb {
pub blobs: Vec<CursorBlob>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub meta: Vec<CursorMetaEntry>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub session_meta: Option<Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CursorBlob {
pub id: String,
#[serde(with = "serde_hex")]
pub data: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CursorMetaEntry {
pub key: String,
pub value: String,
}
impl Codec for Cursor {
fn to_common(transcript: &Transcript<Self>) -> Result<Transcript<Common>> {
Ok(Transcript::new(
transcript.meta.clone(),
db_to_messages(&transcript.body, &transcript.meta),
))
}
fn from_common(transcript: &Transcript<Common>) -> Result<Transcript<Self>> {
Ok(Transcript::new(
transcript.meta.clone(),
db_from_messages(&transcript.meta, &transcript.body)?,
))
}
}
impl TextCodec for Cursor {
fn from_text(text: &str) -> Result<Transcript<Self>> {
let body: CursorDb = serde_json::from_str(text)?;
let meta = meta_from_db(&body, None);
Ok(Transcript::new(meta, body))
}
fn to_text(transcript: &Transcript<Self>) -> Result<String> {
Ok(serde_json::to_string_pretty(&transcript.body)?)
}
}
#[derive(Debug, Clone)]
pub struct CursorStore {
pub chats_dir: PathBuf,
}
impl CursorStore {
pub fn new(chats_dir: impl Into<PathBuf>) -> Self {
Self {
chats_dir: chats_dir.into(),
}
}
pub fn default_root() -> Option<Self> {
dirs_home().map(|h| Self::new(h.join(".cursor").join("chats")))
}
#[cfg(feature = "opencode")]
fn collect_sessions(&self) -> Vec<PathBuf> {
let Ok(workspaces) = fs::read_dir(&self.chats_dir) else {
return Vec::new();
};
let mut out = Vec::new();
for workspace in workspaces.flatten() {
let workspace_dir = workspace.path();
if !workspace_dir.is_dir() {
continue;
}
let Ok(sessions) = fs::read_dir(workspace_dir) else {
continue;
};
for session in sessions.flatten() {
let db = session.path().join("store.db");
if db.is_file() {
out.push(db);
}
}
}
out
}
#[cfg(feature = "opencode")]
fn file_timestamp(path: &Path) -> DateTime<Utc> {
fs::metadata(path)
.ok()
.and_then(|m| m.modified().ok())
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.and_then(|d| DateTime::from_timestamp(d.as_secs() as i64, d.subsec_nanos()))
.unwrap_or_else(Utc::now)
}
}
#[cfg(feature = "opencode")]
impl Store for CursorStore {
type H = Cursor;
type Ref = PathBuf;
fn discover(&self) -> Result<Vec<Discovered<PathBuf>>> {
let mut out = Vec::new();
for db_path in self.collect_sessions() {
let Ok(body) = read_db(&db_path) else {
continue;
};
let mut meta = meta_from_db(&body, Some(&db_path));
if meta.timestamp.timestamp_millis() == 0 {
meta.timestamp = Self::file_timestamp(&db_path);
}
out.push(Discovered {
meta,
reference: db_path,
});
}
Ok(out)
}
fn load(&self, reference: &PathBuf) -> Result<Transcript<Cursor>> {
let body = read_db(reference)?;
let mut meta = meta_from_db(&body, Some(reference));
if meta.timestamp.timestamp_millis() == 0 {
meta.timestamp = Self::file_timestamp(reference);
}
Ok(Transcript::new(meta, body))
}
fn save(&self, transcript: &Transcript<Cursor>) -> Result<Saved<PathBuf>> {
let id = if transcript.meta.id.is_empty() {
Uuid::new_v4().to_string()
} else {
transcript.meta.id.clone()
};
let cwd = transcript
.meta
.cwd
.clone()
.or_else(|| {
std::env::current_dir()
.ok()
.map(|p| p.to_string_lossy().into_owned())
})
.unwrap_or_default();
let workspace = md5_hex(cwd.as_bytes());
let session_dir = self.chats_dir.join(workspace).join(&id);
fs::create_dir_all(&session_dir)?;
let mut body = transcript.body.clone();
ensure_cursor_meta(&mut body, &transcript.meta, &id);
let db_path = session_dir.join("store.db");
write_db(&db_path, &body)?;
write_session_files(&session_dir, &body, &transcript.meta, &id)?;
Ok(Saved {
id,
reference: db_path,
})
}
fn fingerprints(&self, refs: &[PathBuf]) -> Result<HashMap<String, String>> {
let mut out = HashMap::with_capacity(refs.len());
for path in refs {
out.insert(path.to_string_lossy().into_owned(), file_fingerprint(path));
}
Ok(out)
}
}
#[cfg(not(feature = "opencode"))]
impl Store for CursorStore {
type H = Cursor;
type Ref = PathBuf;
fn discover(&self) -> Result<Vec<Discovered<PathBuf>>> {
Ok(Vec::new())
}
fn load(&self, _reference: &PathBuf) -> Result<Transcript<Cursor>> {
Err(sqlite_unavailable())
}
fn save(&self, _transcript: &Transcript<Cursor>) -> Result<Saved<PathBuf>> {
Err(sqlite_unavailable())
}
}
#[cfg(feature = "opencode")]
fn read_db(db_path: &Path) -> Result<CursorDb> {
let conn = Connection::open_with_flags(
db_path,
OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX,
)
.map_err(sqlite_err)?;
let mut blobs_stmt = conn
.prepare("SELECT id, data FROM blobs ORDER BY rowid")
.map_err(sqlite_err)?;
let blobs = blobs_stmt
.query_map([], |row| {
Ok(CursorBlob {
id: row.get(0)?,
data: row.get(1)?,
})
})
.map_err(sqlite_err)?
.collect::<std::result::Result<Vec<_>, _>>()
.map_err(sqlite_err)?;
let mut meta = Vec::new();
if let Ok(mut stmt) = conn.prepare("SELECT key, value FROM meta ORDER BY key") {
let rows = stmt
.query_map([], |row| {
Ok(CursorMetaEntry {
key: row.get(0)?,
value: row.get(1)?,
})
})
.map_err(sqlite_err)?;
meta = rows
.collect::<std::result::Result<Vec<_>, _>>()
.map_err(sqlite_err)?;
}
let session_meta = db_path
.parent()
.and_then(|p| fs::read_to_string(p.join("meta.json")).ok())
.and_then(|text| serde_json::from_str::<Value>(&text).ok());
Ok(CursorDb {
blobs,
meta,
session_meta,
})
}
#[cfg(feature = "opencode")]
fn write_db(db_path: &Path, body: &CursorDb) -> Result<()> {
let conn = Connection::open(db_path).map_err(sqlite_err)?;
conn.execute_batch(
"CREATE TABLE IF NOT EXISTS blobs (id TEXT PRIMARY KEY, data BLOB);
CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT);
DELETE FROM blobs;
DELETE FROM meta;",
)
.map_err(sqlite_err)?;
for blob in &body.blobs {
conn.execute(
"INSERT OR REPLACE INTO blobs (id, data) VALUES (?1, ?2)",
params![blob.id, blob.data],
)
.map_err(sqlite_err)?;
}
for entry in &body.meta {
conn.execute(
"INSERT OR REPLACE INTO meta (key, value) VALUES (?1, ?2)",
params![entry.key, entry.value],
)
.map_err(sqlite_err)?;
}
Ok(())
}
#[cfg(feature = "opencode")]
fn sqlite_err(e: rusqlite::Error) -> Error {
Error::Malformed {
harness: Cursor::NAME,
detail: e.to_string(),
}
}
#[cfg(not(feature = "opencode"))]
fn sqlite_unavailable() -> Error {
Error::Unconvertible {
harness: Cursor::NAME,
detail: "Cursor store support requires the `opencode` feature for SQLite".to_string(),
}
}
fn db_to_messages(db: &CursorDb, meta: &Meta) -> Vec<Message> {
let fallback_ts = meta.timestamp;
let mut messages = Vec::new();
let mut message_idx = 0usize;
for blob in &db.blobs {
let Ok(obj) = serde_json::from_slice::<Value>(&blob.data) else {
continue;
};
let Some(role) = obj.get("role").and_then(Value::as_str) else {
continue;
};
match role {
"system" => {}
"user" => {
if is_context_injection(&obj) {
continue;
}
let content = parse_user_content(&obj);
if content.is_empty() {
continue;
}
messages.push(Message {
role: Role::User,
content,
timestamp: timestamp_from_obj(&obj).unwrap_or(fallback_ts),
model: None,
stop_reason: None,
usage: None,
});
message_idx += 1;
}
"assistant" => {
let content = parse_assistant_content(&obj, &meta.id, message_idx);
if content.is_empty() {
continue;
}
messages.push(Message {
role: Role::Assistant,
content,
timestamp: timestamp_from_obj(&obj).unwrap_or(fallback_ts),
model: cursor_model(&obj).or_else(|| meta.model.clone()),
stop_reason: None,
usage: None,
});
message_idx += 1;
}
"tool" => {
let content = parse_tool_content(&obj);
if content.is_empty() {
continue;
}
messages.push(Message {
role: Role::User,
content,
timestamp: timestamp_from_obj(&obj).unwrap_or(fallback_ts),
model: None,
stop_reason: None,
usage: None,
});
message_idx += 1;
}
_ => {}
}
}
messages
}
fn db_from_messages(meta: &Meta, messages: &[Message]) -> Result<CursorDb> {
let mut values = Vec::new();
let mut tool_names = HashMap::new();
for message in messages {
match message.role {
Role::User => {
let mut user_blocks = Vec::new();
for block in &message.content {
match block {
Block::ToolResult {
tool_use_id,
content,
is_error,
} => {
let tool_name = tool_names
.get(tool_use_id)
.cloned()
.unwrap_or_else(|| "tool".to_string());
values.push(json!({
"role": "tool",
"content": [{
"type": "tool-result",
"toolCallId": tool_use_id,
"toolName": tool_name,
"result": tool_output_text(content),
}],
"createdAt": message.timestamp.timestamp_millis(),
"isError": is_error,
}));
}
other => {
user_blocks.push(serialize_user_block(other));
}
}
}
if !user_blocks.is_empty() {
values.push(json!({
"role": "user",
"content": user_blocks,
"createdAt": message.timestamp.timestamp_millis(),
}));
}
}
Role::Assistant => {
let mut content = Vec::new();
for block in &message.content {
if let Some(v) = serialize_assistant_block(block, &mut tool_names) {
content.push(v);
}
}
if !content.is_empty() {
let mut obj = json!({
"role": "assistant",
"content": content,
"createdAt": message.timestamp.timestamp_millis(),
});
if let Some(model) = message.model.as_ref().or(meta.model.as_ref()) {
obj["providerOptions"] = json!({"cursor": {"modelName": model}});
}
values.push(obj);
}
}
}
}
let mut blobs = Vec::new();
let mut message_ids = Vec::new();
for value in values {
let data = serde_json::to_vec(&value)?;
let id = sha256_hex(&data);
message_ids.push(id.clone());
blobs.push(CursorBlob { id, data });
}
blobs.extend(cursor_state_blobs(meta, messages, &message_ids)?);
let mut body = CursorDb {
blobs,
meta: Vec::new(),
session_meta: None,
};
ensure_cursor_meta(&mut body, meta, &meta.id);
Ok(body)
}
#[derive(Debug, Default)]
struct CursorStateTurn {
user_text: String,
steps: Vec<CursorStateStep>,
}
#[derive(Debug)]
enum CursorStateStep {
Assistant(String),
Thinking(String),
Tool(CursorStateToolCall),
}
#[derive(Debug)]
struct CursorStateToolCall {
id: String,
tool: Tool,
result: Option<CursorStateToolResult>,
}
#[derive(Debug, Clone)]
struct CursorStateToolResult {
content: ToolOutput,
is_error: bool,
}
fn cursor_state_blobs(
meta: &Meta,
messages: &[Message],
_message_ids: &[String],
) -> Result<Vec<CursorBlob>> {
let turns = cursor_state_turns(meta, messages);
let mut blobs = Vec::new();
let mut turn_ids = Vec::new();
for (turn_idx, turn) in turns.iter().enumerate() {
let user_blob = cursor_blob(cursor_user_message_proto(meta, turn_idx, &turn.user_text));
let user_id = sha256(&user_blob.data);
blobs.push(user_blob);
let mut step_ids = Vec::new();
for step in &turn.steps {
let data = match step {
CursorStateStep::Assistant(text) => cursor_assistant_step_proto(text),
CursorStateStep::Thinking(text) => cursor_thinking_step_proto(text),
CursorStateStep::Tool(call) => cursor_tool_step_proto(meta, call),
};
if data.is_empty() {
continue;
}
let step_blob = cursor_blob(data);
step_ids.push(sha256(&step_blob.data));
blobs.push(step_blob);
}
let turn_blob = cursor_blob(cursor_turn_structure_proto(&user_id, &step_ids));
turn_ids.push(sha256(&turn_blob.data));
blobs.push(turn_blob);
}
blobs.push(cursor_blob(cursor_root_state_proto(meta, &turn_ids)));
Ok(blobs)
}
fn cursor_state_turns(meta: &Meta, messages: &[Message]) -> Vec<CursorStateTurn> {
let mut turns = Vec::new();
let mut current: Option<CursorStateTurn> = None;
let tool_results = cursor_tool_results(messages);
let known_tool_uses = cursor_tool_use_ids(messages);
for message in messages {
match message.role {
Role::User => {
let user_text = user_state_text(&message.content);
if !user_text.is_empty() {
if let Some(turn) = current.take() {
turns.push(turn);
}
current = Some(CursorStateTurn {
user_text,
steps: Vec::new(),
});
}
let tool_result_text =
orphan_tool_result_state_text(&message.content, &known_tool_uses);
if !tool_result_text.is_empty() {
let turn = current.get_or_insert_with(|| CursorStateTurn {
user_text: "Tool result".to_string(),
steps: Vec::new(),
});
turn.steps
.push(CursorStateStep::Assistant(tool_result_text));
}
}
Role::Assistant => {
let turn = current.get_or_insert_with(|| CursorStateTurn {
user_text: meta
.title
.clone()
.unwrap_or_else(|| "Continue.".to_string()),
steps: Vec::new(),
});
turn.steps
.extend(assistant_state_steps(&message.content, &tool_results));
}
}
}
if let Some(turn) = current {
turns.push(turn);
}
turns
.into_iter()
.filter(|turn| !turn.user_text.trim().is_empty() || !turn.steps.is_empty())
.collect()
}
fn cursor_tool_results(messages: &[Message]) -> HashMap<String, CursorStateToolResult> {
let mut out = HashMap::new();
for message in messages {
for block in &message.content {
let Block::ToolResult {
tool_use_id,
content,
is_error,
} = block
else {
continue;
};
out.insert(
tool_use_id.clone(),
CursorStateToolResult {
content: content.clone(),
is_error: *is_error,
},
);
}
}
out
}
fn cursor_tool_use_ids(messages: &[Message]) -> HashSet<String> {
messages
.iter()
.flat_map(|message| message.content.iter())
.filter_map(|block| match block {
Block::ToolUse { id, .. } => Some(id.clone()),
_ => None,
})
.collect()
}
fn user_state_text(blocks: &[Block]) -> String {
blocks
.iter()
.filter_map(|block| match block {
Block::Text { text } => Some(text.trim().to_string()),
Block::Image { source } => Some(format!("[image: {}]", source.media_type)),
Block::Thinking { text, .. } if !text.trim().is_empty() => {
Some(text.trim().to_string())
}
Block::Thinking { .. } | Block::ToolUse { .. } | Block::ToolResult { .. } => None,
})
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join("\n\n")
}
fn assistant_state_steps(
blocks: &[Block],
tool_results: &HashMap<String, CursorStateToolResult>,
) -> Vec<CursorStateStep> {
blocks
.iter()
.filter_map(|block| match block {
Block::Text { text } if !text.trim().is_empty() => {
Some(CursorStateStep::Assistant(text.trim().to_string()))
}
Block::Thinking { text, .. } if !text.trim().is_empty() => {
Some(CursorStateStep::Thinking(text.trim().to_string()))
}
Block::ToolUse { id, tool } => Some(CursorStateStep::Tool(CursorStateToolCall {
id: id.clone(),
tool: tool.clone(),
result: tool_results.get(id).cloned(),
})),
Block::Image { source } => Some(CursorStateStep::Assistant(format!(
"[image: {}]",
source.media_type
))),
Block::Text { .. } | Block::Thinking { .. } | Block::ToolResult { .. } => None,
})
.collect()
}
fn tool_use_state_text(id: &str, tool: &Tool) -> String {
let (name, input) = denormalize_tool(tool);
let args = serde_json::to_string_pretty(&input).unwrap_or_else(|_| input.to_string());
format!("Tool call {name} ({id}):\n{args}")
}
fn orphan_tool_result_state_text(blocks: &[Block], known_tool_uses: &HashSet<String>) -> String {
blocks
.iter()
.filter_map(|block| {
let Block::ToolResult {
tool_use_id,
content,
is_error,
} = block
else {
return None;
};
if known_tool_uses.contains(tool_use_id) {
return None;
}
let status = if *is_error { "error" } else { "result" };
Some(format!(
"Tool {status} for {tool_use_id}:\n{}",
tool_output_text(content)
))
})
.collect::<Vec<_>>()
.join("\n\n")
}
fn cursor_blob(data: Vec<u8>) -> CursorBlob {
let id = sha256_hex(&data);
CursorBlob { id, data }
}
fn cursor_user_message_proto(meta: &Meta, turn_idx: usize, text: &str) -> Vec<u8> {
let mut out = Vec::new();
pb_string(&mut out, 1, text);
pb_string(&mut out, 2, &cursor_message_id(&meta.id, turn_idx, text));
pb_varint_field(&mut out, 4, 1);
out
}
fn cursor_assistant_step_proto(text: &str) -> Vec<u8> {
let mut assistant = Vec::new();
pb_string(&mut assistant, 1, text);
let mut step = Vec::new();
pb_len(&mut step, 1, &assistant);
step
}
fn cursor_thinking_step_proto(text: &str) -> Vec<u8> {
let mut thinking = Vec::new();
pb_string(&mut thinking, 1, text);
let mut step = Vec::new();
pb_len(&mut step, 3, &thinking);
step
}
fn cursor_tool_step_proto(meta: &Meta, call: &CursorStateToolCall) -> Vec<u8> {
let Some(tool_call) = cursor_tool_call_proto(meta, call) else {
return cursor_assistant_step_proto(&tool_use_state_text(&call.id, &call.tool));
};
let mut step = Vec::new();
pb_len(&mut step, 2, &tool_call);
step
}
fn cursor_tool_call_proto(meta: &Meta, call: &CursorStateToolCall) -> Option<Vec<u8>> {
let (field, tool_data) = match &call.tool {
Tool::Bash {
command,
workdir,
timeout_ms,
description,
run_in_background,
} => (
1,
cursor_shell_tool_call_proto(
command,
workdir
.as_deref()
.or(meta.cwd.as_deref())
.unwrap_or_default(),
*timeout_ms,
description.as_deref(),
*run_in_background,
&call.id,
call.result.as_ref(),
),
),
Tool::Read {
file_path,
offset,
limit,
} => (
8,
cursor_read_tool_call_proto(file_path, *offset, *limit, call.result.as_ref()),
),
Tool::Edit {
file_path,
old_string,
new_string,
..
} => {
let payload =
CursorEditPayload::replace(file_path, old_string, new_string, call.result.as_ref());
(
12,
cursor_edit_tool_call_proto(file_path, &payload, call.result.as_ref()),
)
}
Tool::Write { file_path, content } => {
let payload = CursorEditPayload::write(file_path, content, call.result.as_ref());
(
12,
cursor_edit_tool_call_proto(file_path, &payload, call.result.as_ref()),
)
}
Tool::MultiEdit { file_path, edits } => {
let payload = CursorEditPayload::multi_edit(file_path, edits, call.result.as_ref());
(
12,
cursor_edit_tool_call_proto(file_path, &payload, call.result.as_ref()),
)
}
Tool::Raw { .. } => return None,
};
let mut out = Vec::new();
pb_len(&mut out, field, &tool_data);
pb_string(&mut out, 57, &call.id);
Some(out)
}
fn cursor_shell_tool_call_proto(
command: &str,
workdir: &str,
timeout_ms: Option<u64>,
description: Option<&str>,
run_in_background: bool,
tool_call_id: &str,
result: Option<&CursorStateToolResult>,
) -> Vec<u8> {
let mut args = Vec::new();
pb_string(&mut args, 1, command);
pb_string(&mut args, 2, workdir);
if let Some(timeout_ms) = timeout_ms.and_then(|v| u32::try_from(v).ok()) {
pb_varint_field(&mut args, 3, timeout_ms as u64);
}
pb_string(&mut args, 4, tool_call_id);
if run_in_background {
pb_bool_field(&mut args, 11, true);
pb_varint_field(&mut args, 13, 2);
}
if let Some(description) = description {
pb_string(&mut args, 15, description);
}
let mut shell = Vec::new();
pb_len(&mut shell, 1, &args);
if let Some(result) = result {
pb_len(
&mut shell,
2,
&cursor_shell_result_proto(command, workdir, result),
);
}
if let Some(description) = description {
pb_string(&mut shell, 3, description);
}
shell
}
fn cursor_shell_result_proto(
command: &str,
workdir: &str,
result: &CursorStateToolResult,
) -> Vec<u8> {
let output = tool_output_text(&result.content);
let mut shell_result = Vec::new();
if result.is_error {
let mut failure = Vec::new();
pb_string(&mut failure, 1, command);
pb_string(&mut failure, 2, workdir);
pb_varint_field(&mut failure, 3, 1);
pb_string(&mut failure, 6, &output);
pb_len(&mut shell_result, 2, &failure);
} else {
let mut success = Vec::new();
pb_string(&mut success, 1, command);
pb_string(&mut success, 2, workdir);
pb_varint_field(&mut success, 3, 0);
pb_string(&mut success, 5, &output);
pb_len(&mut shell_result, 1, &success);
}
shell_result
}
fn cursor_read_tool_call_proto(
path: &str,
offset: Option<u64>,
limit: Option<u64>,
result: Option<&CursorStateToolResult>,
) -> Vec<u8> {
let offset_u32 = offset.and_then(|v| u32::try_from(v).ok());
let limit_u32 = limit.and_then(|v| u32::try_from(v).ok());
let mut args = Vec::new();
pb_string(&mut args, 1, path);
if let Some(offset) = offset_u32 {
pb_varint_field(&mut args, 2, offset as u64);
}
if let Some(limit) = limit_u32 {
pb_varint_field(&mut args, 3, limit as u64);
}
let mut read = Vec::new();
pb_len(&mut read, 1, &args);
if let Some(result) = result {
pb_len(
&mut read,
2,
&cursor_read_result_proto(path, offset_u32, result),
);
}
read
}
fn cursor_read_result_proto(
path: &str,
offset: Option<u32>,
result: &CursorStateToolResult,
) -> Vec<u8> {
let mut read_result = Vec::new();
let text = tool_output_text(&result.content);
if result.is_error {
let mut error = Vec::new();
pb_string(&mut error, 1, &text);
pb_len(&mut read_result, 2, &error);
} else {
let mut success = Vec::new();
pb_string(&mut success, 1, &text);
if text.is_empty() {
pb_bool_field(&mut success, 2, true);
}
pb_varint_field(&mut success, 4, line_count(&text));
pb_varint_field(&mut success, 5, text.len() as u64);
pb_string(&mut success, 7, path);
if let Some(offset) = offset {
let mut range = Vec::new();
let start = offset.saturating_add(1);
let end = start.saturating_add(line_count(&text).saturating_sub(1) as u32);
pb_varint_field(&mut range, 1, start as u64);
pb_varint_field(&mut range, 2, end as u64);
pb_len(&mut success, 8, &range);
}
pb_len(&mut read_result, 1, &success);
}
read_result
}
fn cursor_edit_tool_call_proto(
path: &str,
payload: &CursorEditPayload,
result: Option<&CursorStateToolResult>,
) -> Vec<u8> {
let mut args = Vec::new();
pb_string(&mut args, 1, path);
pb_string(&mut args, 6, &payload.stream_content);
let mut edit = Vec::new();
pb_len(&mut edit, 1, &args);
if let Some(result) = result {
pb_len(
&mut edit,
2,
&cursor_edit_result_proto(path, payload, result),
);
}
edit
}
fn cursor_edit_result_proto(
path: &str,
payload: &CursorEditPayload,
result: &CursorStateToolResult,
) -> Vec<u8> {
let output = tool_output_text(&result.content);
let mut edit_result = Vec::new();
if result.is_error {
let mut error = Vec::new();
pb_string(&mut error, 1, path);
pb_string(&mut error, 2, &output);
pb_string(&mut error, 5, &output);
pb_len(&mut edit_result, 7, &error);
} else {
let mut success = Vec::new();
pb_string(&mut success, 1, path);
if let Some(lines_added) = payload.lines_added {
pb_varint_field(&mut success, 3, lines_added);
}
if let Some(lines_removed) = payload.lines_removed {
pb_varint_field(&mut success, 4, lines_removed);
}
if let Some(diff_string) = &payload.diff_string {
pb_string(&mut success, 5, diff_string);
}
if let Some(before_full_file_content) = &payload.before_full_file_content {
pb_string(&mut success, 6, before_full_file_content);
}
pb_string(&mut success, 7, &payload.after_full_file_content);
pb_string(&mut success, 8, &payload.message);
pb_len(&mut edit_result, 1, &success);
}
edit_result
}
#[derive(Debug)]
struct CursorEditPayload {
stream_content: String,
diff_string: Option<String>,
before_full_file_content: Option<String>,
after_full_file_content: String,
lines_added: Option<u64>,
lines_removed: Option<u64>,
message: String,
}
impl CursorEditPayload {
fn replace(
path: &str,
old_string: &str,
new_string: &str,
result: Option<&CursorStateToolResult>,
) -> Self {
Self {
stream_content: new_string.to_string(),
diff_string: Some(unified_replace_diff(path, old_string, new_string)),
before_full_file_content: Some(old_string.to_string()),
after_full_file_content: new_string.to_string(),
lines_added: Some(line_count(new_string)),
lines_removed: Some(line_count(old_string)),
message: edit_success_message(path, result, "updated"),
}
}
fn write(path: &str, content: &str, result: Option<&CursorStateToolResult>) -> Self {
Self {
stream_content: content.to_string(),
diff_string: Some(unified_write_diff(path, content)),
before_full_file_content: None,
after_full_file_content: content.to_string(),
lines_added: Some(line_count(content)),
lines_removed: Some(0),
message: edit_success_message(path, result, "written"),
}
}
fn multi_edit(path: &str, edits: &[EditOp], result: Option<&CursorStateToolResult>) -> Self {
let stream_content = edits
.iter()
.map(|edit| edit.new_string.as_str())
.collect::<Vec<_>>()
.join("\n");
let diff_string = edits
.iter()
.map(|edit| unified_replace_diff(path, &edit.old_string, &edit.new_string))
.collect::<Vec<_>>()
.join("\n");
let before_full_file_content = edits
.iter()
.map(|edit| edit.old_string.as_str())
.collect::<Vec<_>>()
.join("\n");
let after_full_file_content = if stream_content.is_empty() {
serde_json::to_string_pretty(edits).unwrap_or_else(|_| "[]".to_string())
} else {
stream_content.clone()
};
Self {
stream_content,
diff_string: (!diff_string.is_empty()).then_some(diff_string),
before_full_file_content: (!before_full_file_content.is_empty())
.then_some(before_full_file_content),
after_full_file_content,
lines_added: Some(
edits
.iter()
.map(|edit| line_count(&edit.new_string))
.sum::<u64>(),
),
lines_removed: Some(
edits
.iter()
.map(|edit| line_count(&edit.old_string))
.sum::<u64>(),
),
message: edit_success_message(path, result, "updated"),
}
}
}
fn edit_success_message(
path: &str,
result: Option<&CursorStateToolResult>,
fallback_action: &str,
) -> String {
result
.filter(|result| !result.is_error)
.map(|result| tool_output_text(&result.content))
.filter(|text| !text.trim().is_empty())
.unwrap_or_else(|| match fallback_action {
"written" => format!("Wrote contents to {path}"),
_ => format!("The file {path} has been updated."),
})
}
fn unified_replace_diff(path: &str, old_string: &str, new_string: &str) -> String {
let old_len = line_count(old_string);
let new_len = line_count(new_string);
let old_range = diff_range(old_len);
let new_range = diff_range(new_len);
let mut out = format!("--- a/{path}\n+++ b/{path}\n@@ -{old_range} +{new_range} @@\n");
append_diff_lines(&mut out, '-', old_string);
append_diff_lines(&mut out, '+', new_string);
out
}
fn unified_write_diff(path: &str, content: &str) -> String {
let new_len = line_count(content);
let new_range = diff_range(new_len);
let mut out = format!("--- /dev/null\n+++ b/{path}\n@@ -0,0 +{new_range} @@\n");
append_diff_lines(&mut out, '+', content);
out
}
fn diff_range(lines: u64) -> String {
match lines {
0 => "0,0".to_string(),
1 => "1".to_string(),
_ => format!("1,{lines}"),
}
}
fn append_diff_lines(out: &mut String, prefix: char, text: &str) {
if text.is_empty() {
return;
}
for line in text.lines() {
out.push(prefix);
out.push_str(line);
out.push('\n');
}
}
fn line_count(text: &str) -> u64 {
if text.is_empty() {
0
} else {
text.lines().count() as u64
}
}
fn cursor_turn_structure_proto(user_id: &[u8; 32], step_ids: &[[u8; 32]]) -> Vec<u8> {
let mut agent_turn = Vec::new();
pb_len(&mut agent_turn, 1, user_id);
for step_id in step_ids {
pb_len(&mut agent_turn, 2, step_id);
}
let mut turn = Vec::new();
pb_len(&mut turn, 1, &agent_turn);
turn
}
fn cursor_root_state_proto(meta: &Meta, turn_ids: &[[u8; 32]]) -> Vec<u8> {
let mut root = Vec::new();
for turn_id in turn_ids {
pb_len(&mut root, 8, turn_id);
}
pb_varint_field(&mut root, 10, 1);
let started_ms = meta.timestamp.timestamp_millis();
if started_ms > 0 {
pb_varint_field(&mut root, 26, started_ms as u64);
}
root
}
fn cursor_message_id(session_id: &str, turn_idx: usize, text: &str) -> String {
const NS: Uuid = Uuid::from_bytes([
0x7a, 0x63, 0x24, 0x1e, 0x88, 0x0d, 0x4d, 0xc9, 0xa7, 0x99, 0x3b, 0x25, 0x9f, 0x64, 0x12,
0x02,
]);
Uuid::new_v5(&NS, format!("{session_id}:{turn_idx}:{text}").as_bytes()).to_string()
}
fn pb_string(out: &mut Vec<u8>, field: u32, value: &str) {
if !value.is_empty() {
pb_len(out, field, value.as_bytes());
}
}
fn pb_len(out: &mut Vec<u8>, field: u32, bytes: &[u8]) {
pb_key(out, field, 2);
pb_varint(out, bytes.len() as u64);
out.extend(bytes);
}
fn pb_varint_field(out: &mut Vec<u8>, field: u32, value: u64) {
pb_key(out, field, 0);
pb_varint(out, value);
}
fn pb_bool_field(out: &mut Vec<u8>, field: u32, value: bool) {
pb_varint_field(out, field, u64::from(value));
}
fn pb_key(out: &mut Vec<u8>, field: u32, wire_type: u8) {
pb_varint(out, ((field as u64) << 3) | wire_type as u64);
}
fn pb_varint(out: &mut Vec<u8>, mut value: u64) {
while value >= 0x80 {
out.push((value as u8) | 0x80);
value >>= 7;
}
out.push(value as u8);
}
fn parse_user_content(obj: &Value) -> Vec<Block> {
match obj.get("content") {
Some(Value::String(s)) => text_blocks_from_str(s, true),
Some(Value::Array(arr)) => arr.iter().filter_map(parse_user_block).collect(),
_ => Vec::new(),
}
}
fn parse_user_block(v: &Value) -> Option<Block> {
match v.get("type").and_then(Value::as_str)? {
"text" => {
let text = strip_user_query(v.get("text")?.as_str()?);
let text = clean_redacted_text(&text);
(!text.is_empty()).then_some(Block::Text { text })
}
"image" => parse_image(v),
_ => None,
}
}
fn parse_assistant_content(obj: &Value, session_id: &str, message_idx: usize) -> Vec<Block> {
let Some(Value::Array(arr)) = obj.get("content") else {
return Vec::new();
};
arr.iter()
.enumerate()
.filter_map(|(i, v)| parse_assistant_block(v, session_id, message_idx, i))
.collect()
}
fn parse_assistant_block(
v: &Value,
session_id: &str,
message_idx: usize,
block_idx: usize,
) -> Option<Block> {
match v.get("type").and_then(Value::as_str)? {
"text" => {
let text = clean_redacted_text(v.get("text")?.as_str()?);
(!text.is_empty()).then_some(Block::Text { text })
}
"reasoning" | "thinking" => Some(Block::Thinking {
text: v
.get("text")
.or_else(|| v.get("thinking"))
.and_then(Value::as_str)
.unwrap_or_default()
.to_string(),
signature: v.get("signature").and_then(Value::as_str).map(String::from),
encrypted: v.get("data").and_then(Value::as_str).map(String::from),
}),
"redacted-reasoning" => Some(Block::Thinking {
text: String::new(),
signature: None,
encrypted: v.get("data").and_then(Value::as_str).map(String::from),
}),
"tool-call" => {
let name = v.get("toolName")?.as_str()?;
let input = v.get("args").cloned().unwrap_or(Value::Object(Map::new()));
let id = v
.get("toolCallId")
.and_then(Value::as_str)
.map(String::from)
.unwrap_or_else(|| tool_use_id(session_id, message_idx, block_idx, name));
Some(Block::ToolUse {
id,
tool: cursor_tool(name, input),
})
}
"image" => parse_image(v),
_ => None,
}
}
fn parse_tool_content(obj: &Value) -> Vec<Block> {
let Some(Value::Array(arr)) = obj.get("content") else {
return Vec::new();
};
arr.iter()
.filter_map(|v| {
if v.get("type").and_then(Value::as_str)? != "tool-result" {
return None;
}
let tool_use_id = v.get("toolCallId")?.as_str()?.to_string();
let content = match v.get("result") {
Some(Value::String(s)) => ToolOutput::Text(s.clone()),
Some(other) => ToolOutput::Json(other.clone()),
None => ToolOutput::Text(String::new()),
};
let is_error = v
.get("isError")
.or_else(|| obj.get("isError"))
.and_then(Value::as_bool)
.unwrap_or_else(|| matches!(&content, ToolOutput::Text(s) if looks_error(s)));
Some(Block::ToolResult {
tool_use_id,
content,
is_error,
})
})
.collect()
}
fn serialize_user_block(block: &Block) -> Value {
match block {
Block::Text { text } => json!({"type": "text", "text": wrap_user_query(text)}),
Block::Image { source } => serialize_image(source),
other => {
let text = match other {
Block::Thinking { text, .. } => text.clone(),
Block::ToolUse { tool, .. } => format!("{tool:?}"),
Block::ToolResult { content, .. } => tool_output_text(content),
Block::Text { .. } | Block::Image { .. } => String::new(),
};
json!({"type": "text", "text": text})
}
}
}
fn serialize_assistant_block(
block: &Block,
tool_names: &mut HashMap<String, String>,
) -> Option<Value> {
match block {
Block::Text { text } => Some(json!({"type": "text", "text": text})),
Block::Thinking {
text,
signature,
encrypted,
} => {
if let Some(data) = encrypted {
Some(json!({"type": "redacted-reasoning", "data": data}))
} else {
let mut obj = json!({"type": "reasoning", "text": text});
if let Some(sig) = signature {
obj["signature"] = Value::String(sig.clone());
}
Some(obj)
}
}
Block::ToolUse { id, tool } => {
let (name, args) = denormalize_tool(tool);
tool_names.insert(id.clone(), name.clone());
Some(json!({
"type": "tool-call",
"toolCallId": id,
"toolName": name,
"args": args,
}))
}
Block::Image { source } => Some(serialize_image(source)),
Block::ToolResult { .. } => None,
}
}
fn parse_image(v: &Value) -> Option<Block> {
let source = v.get("source")?;
Some(Block::Image {
source: ImageSource {
source_type: source
.get("type")
.and_then(Value::as_str)
.unwrap_or("base64")
.to_string(),
media_type: source.get("media_type")?.as_str()?.to_string(),
data: source.get("data")?.as_str()?.to_string(),
},
})
}
fn serialize_image(source: &ImageSource) -> Value {
json!({
"type": "image",
"source": {
"type": source.source_type,
"media_type": source.media_type,
"data": source.data,
},
})
}
fn meta_from_db(db: &CursorDb, db_path: Option<&Path>) -> Meta {
let db_meta = cursor_meta_value(db);
let session_meta = db.session_meta.as_ref();
let created_ms = session_meta
.and_then(|v| v.get("createdAtMs"))
.and_then(Value::as_i64)
.or_else(|| {
db_meta
.as_ref()
.and_then(|v| v.get("createdAt"))
.and_then(Value::as_i64)
})
.unwrap_or(0);
let timestamp = DateTime::from_timestamp_millis(created_ms)
.unwrap_or_else(|| DateTime::from_timestamp_millis(0).unwrap_or_else(Utc::now));
let mut title = session_meta
.and_then(|v| v.get("title"))
.and_then(Value::as_str)
.map(String::from)
.or_else(|| {
db_meta
.as_ref()
.and_then(|v| v.get("name"))
.and_then(Value::as_str)
.map(String::from)
});
let mut cwd = None;
let mut model = db_meta
.as_ref()
.and_then(|v| v.get("lastUsedModel"))
.and_then(Value::as_str)
.map(String::from);
let mut first_user_text = None;
if cwd.is_none() {
cwd = db_meta
.as_ref()
.and_then(|v| v.get("workspacePath"))
.and_then(Value::as_str)
.map(String::from);
}
for blob in &db.blobs {
let Ok(obj) = serde_json::from_slice::<Value>(&blob.data) else {
continue;
};
if cwd.is_none() {
cwd = workspace_from_message(&obj);
}
if model.is_none() {
model = cursor_model(&obj);
}
if first_user_text.is_none()
&& obj.get("role").and_then(Value::as_str) == Some("user")
&& !is_context_injection(&obj)
{
first_user_text = parse_user_content(&obj)
.into_iter()
.find_map(|block| match block {
Block::Text { text } if !text.is_empty() => Some(text),
_ => None,
});
}
}
if title.as_deref().is_none_or(str::is_empty) {
title = first_user_text.map(|s| truncate_title(&s));
}
Meta {
id: db_path
.and_then(|p| p.parent())
.and_then(|p| p.file_name())
.map(|s| s.to_string_lossy().into_owned())
.or_else(|| {
db_meta
.as_ref()
.and_then(|v| v.get("agentId"))
.and_then(Value::as_str)
.map(String::from)
})
.unwrap_or_default(),
timestamp,
cwd,
git_branch: None,
title,
cli_version: None,
model,
}
}
fn ensure_cursor_meta(body: &mut CursorDb, meta: &Meta, id: &str) {
let now_ms = Utc::now().timestamp_millis();
let created_at = body
.session_meta
.as_ref()
.and_then(|v| v.get("createdAtMs"))
.and_then(Value::as_i64)
.unwrap_or_else(|| {
if meta.timestamp.timestamp_millis() > 0 {
meta.timestamp.timestamp_millis()
} else {
now_ms
}
});
let updated_at = body
.session_meta
.as_ref()
.and_then(|v| v.get("updatedAtMs"))
.and_then(Value::as_i64)
.unwrap_or(now_ms);
let title = meta
.title
.clone()
.unwrap_or_else(|| "Imported Session".to_string());
let latest = body.blobs.last().map(|b| b.id.clone()).unwrap_or_default();
let model = meta
.model
.clone()
.unwrap_or_else(|| "composer-2.5".to_string());
let mut value = json!({
"agentId": id,
"latestRootBlobId": latest,
"name": title,
"createdAt": created_at,
"mode": "default",
"isRunEverything": true,
"approvalMode": "unrestricted",
"lastUsedModel": model,
});
if let Some(cwd) = meta.cwd.as_ref() {
value["workspacePath"] = Value::String(cwd.clone());
}
let encoded = hex_encode(value.to_string().as_bytes());
if let Some(entry) = body.meta.iter_mut().find(|entry| entry.key == "0") {
entry.value = encoded;
} else {
body.meta.push(CursorMetaEntry {
key: "0".to_string(),
value: encoded,
});
}
body.session_meta = Some(json!({
"schemaVersion": 1,
"createdAtMs": created_at,
"hasConversation": !body.blobs.is_empty(),
"title": title,
"updatedAtMs": updated_at,
}));
}
#[cfg(feature = "opencode")]
fn write_session_files(session_dir: &Path, body: &CursorDb, meta: &Meta, _id: &str) -> Result<()> {
let session_meta = body.session_meta.clone().unwrap_or_else(|| {
json!({
"schemaVersion": 1,
"createdAtMs": meta.timestamp.timestamp_millis(),
"hasConversation": !body.blobs.is_empty(),
"title": meta.title.clone().unwrap_or_default(),
"updatedAtMs": Utc::now().timestamp_millis(),
})
});
fs::write(
session_dir.join("meta.json"),
serde_json::to_string(&session_meta)?,
)?;
let prompts = db_to_messages(body, meta)
.into_iter()
.filter(|message| message.role == Role::User)
.flat_map(|message| message.content)
.filter_map(|block| match block {
Block::Text { text } if !text.is_empty() => Some(text),
_ => None,
})
.collect::<Vec<_>>();
fs::write(
session_dir.join("prompt_history.json"),
serde_json::to_string_pretty(&prompts)?,
)?;
Ok(())
}
fn cursor_meta_value(db: &CursorDb) -> Option<Value> {
let raw = &db.meta.iter().find(|entry| entry.key == "0")?.value;
if let Ok(v) = serde_json::from_str::<Value>(raw) {
return Some(v);
}
let decoded = hex_decode(raw).ok()?;
serde_json::from_slice(&decoded).ok()
}
fn timestamp_from_obj(obj: &Value) -> Option<DateTime<Utc>> {
obj.get("createdAt")
.or_else(|| obj.get("timestamp"))
.and_then(Value::as_i64)
.and_then(DateTime::from_timestamp_millis)
.or_else(|| {
obj.get("timestamp")
.and_then(Value::as_str)
.and_then(|s| s.parse::<DateTime<Utc>>().ok())
})
}
fn cursor_model(obj: &Value) -> Option<String> {
obj.get("providerOptions")
.and_then(|p| p.pointer("/cursor/modelName"))
.and_then(Value::as_str)
.map(String::from)
.or_else(|| {
obj.get("content")
.and_then(Value::as_array)
.and_then(|arr| arr.iter().find_map(cursor_model))
})
}
fn workspace_from_message(obj: &Value) -> Option<String> {
content_strings(obj.get("content")).find_map(|s| {
s.lines()
.find_map(|line| line.strip_prefix("Workspace Path: ").map(String::from))
})
}
fn is_context_injection(obj: &Value) -> bool {
content_strings(obj.get("content")).any(|s| s.contains("<user_info>"))
}
fn content_strings(content: Option<&Value>) -> impl Iterator<Item = &str> {
let mut strings = Vec::new();
match content {
Some(Value::String(s)) => strings.push(s.as_str()),
Some(Value::Array(arr)) => {
for block in arr {
if let Some(text) = block.get("text").and_then(Value::as_str) {
strings.push(text);
} else if let Some(text) = block.get("result").and_then(Value::as_str) {
strings.push(text);
}
}
}
_ => {}
}
strings.into_iter()
}
fn cursor_tool(name: &str, args: Value) -> Tool {
let canonical = match name {
"Shell" => "Bash",
"StrReplace" => "Edit",
other => other,
};
Tool::from_canonical(canonical, normalize_cursor_args(canonical, args))
}
fn normalize_cursor_args(tool: &str, args: Value) -> Value {
let Some(obj) = args.as_object() else {
return args;
};
let mut out = Map::new();
for (k, v) in obj {
let key = match (tool, k.as_str()) {
(_, "path" | "filePath") => "file_path",
("Bash", "cwd") => "workdir",
("Edit", "oldString") => "old_string",
("Edit", "newString") => "new_string",
_ => k.as_str(),
};
out.insert(key.to_string(), v.clone());
}
Value::Object(out)
}
fn denormalize_tool(tool: &Tool) -> (String, Value) {
let (name, input) = tool.to_canonical();
let cursor_name = match name.as_str() {
"Bash" => "Shell",
"Edit" => "StrReplace",
other => other,
}
.to_string();
(cursor_name, denormalize_cursor_args(&name, input))
}
fn denormalize_cursor_args(tool: &str, input: Value) -> Value {
let Some(obj) = input.as_object() else {
return input;
};
let mut out = Map::new();
for (k, v) in obj {
let key = match (tool, k.as_str()) {
(_, "file_path") => "path",
("Bash", "workdir") => "cwd",
_ => k.as_str(),
};
out.insert(key.to_string(), v.clone());
}
Value::Object(out)
}
fn text_blocks_from_str(s: &str, strip_query: bool) -> Vec<Block> {
let text = if strip_query {
strip_user_query(s)
} else {
s.to_string()
};
let text = clean_redacted_text(&text);
if text.is_empty() {
Vec::new()
} else {
vec![Block::Text { text }]
}
}
fn strip_user_query(text: &str) -> String {
let trimmed = text.trim();
if let Some(inner) = trimmed
.strip_prefix("<user_query>")
.and_then(|s| s.strip_suffix("</user_query>"))
{
inner.trim().to_string()
} else {
text.to_string()
}
}
fn wrap_user_query(text: &str) -> String {
if text.trim_start().starts_with("<user_query>") {
text.to_string()
} else {
format!("<user_query>\n{text}\n</user_query>")
}
}
fn clean_redacted_text(text: &str) -> String {
text.replace("\n\n[REDACTED]", "")
.replace("[REDACTED]\n\n", "")
.replace("[REDACTED]", "")
.trim()
.to_string()
}
fn looks_error(text: &str) -> bool {
text.starts_with("Error:") || text.contains(" exited with code ")
}
fn tool_output_text(out: &ToolOutput) -> String {
match out {
ToolOutput::Text(s) => s.clone(),
ToolOutput::Json(v) => v.to_string(),
}
}
fn truncate_title(text: &str) -> String {
const MAX: usize = 80;
let one_line = text.split_whitespace().collect::<Vec<_>>().join(" ");
if one_line.chars().count() <= MAX {
one_line
} else {
let mut t: String = one_line.chars().take(MAX.saturating_sub(1)).collect();
t.push('…');
t
}
}
fn tool_use_id(session_id: &str, message_idx: usize, block_idx: usize, name: &str) -> String {
const NS: Uuid = Uuid::from_bytes([
0x3c, 0x7a, 0x1b, 0x44, 0x9e, 0x2f, 0x4d, 0x68, 0xa1, 0x0c, 0x55, 0x6e, 0x7f, 0x88, 0x99,
0xaa,
]);
Uuid::new_v5(
&NS,
format!("{session_id}:{message_idx}:{block_idx}:{name}").as_bytes(),
)
.to_string()
}
#[cfg(feature = "opencode")]
fn file_fingerprint(path: &Path) -> String {
let Ok(meta) = fs::metadata(path) else {
return String::new();
};
let mtime = meta
.modified()
.ok()
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_nanos())
.unwrap_or(0);
format!("{mtime}:{}", meta.len())
}
fn dirs_home() -> Option<PathBuf> {
std::env::var_os("HOME").map(PathBuf::from)
}
#[cfg(any(feature = "opencode", test))]
fn md5_hex(data: &[u8]) -> String {
let mut msg = data.to_vec();
let bit_len = (msg.len() as u64).wrapping_mul(8);
msg.push(0x80);
while msg.len() % 64 != 56 {
msg.push(0);
}
msg.extend(bit_len.to_le_bytes());
let mut a0 = 0x6745_2301u32;
let mut b0 = 0xefcd_ab89u32;
let mut c0 = 0x98ba_dcfeu32;
let mut d0 = 0x1032_5476u32;
let shifts: [u32; 64] = [
7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 5, 9, 14, 20, 5, 9, 14, 20, 5,
9, 14, 20, 5, 9, 14, 20, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 6, 10,
15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21,
];
let mut k = [0u32; 64];
for (i, slot) in k.iter_mut().enumerate() {
*slot = ((f64::sin((i + 1) as f64).abs() * 4_294_967_296.0).floor()) as u32;
}
for chunk in msg.chunks_exact(64) {
let mut m = [0u32; 16];
for (i, word) in m.iter_mut().enumerate() {
let start = i * 4;
*word = u32::from_le_bytes([
chunk[start],
chunk[start + 1],
chunk[start + 2],
chunk[start + 3],
]);
}
let (mut a, mut b, mut c, mut d) = (a0, b0, c0, d0);
for i in 0..64 {
let (f, g) = match i {
0..=15 => ((b & c) | ((!b) & d), i),
16..=31 => ((d & b) | ((!d) & c), (5 * i + 1) % 16),
32..=47 => (b ^ c ^ d, (3 * i + 5) % 16),
_ => (c ^ (b | !d), (7 * i) % 16),
};
let tmp = d;
d = c;
c = b;
b = b.wrapping_add(
a.wrapping_add(f)
.wrapping_add(k[i])
.wrapping_add(m[g])
.rotate_left(shifts[i]),
);
a = tmp;
}
a0 = a0.wrapping_add(a);
b0 = b0.wrapping_add(b);
c0 = c0.wrapping_add(c);
d0 = d0.wrapping_add(d);
}
let mut out = Vec::with_capacity(16);
out.extend(a0.to_le_bytes());
out.extend(b0.to_le_bytes());
out.extend(c0.to_le_bytes());
out.extend(d0.to_le_bytes());
hex_encode(&out)
}
fn sha256_hex(data: &[u8]) -> String {
hex_encode(&sha256(data))
}
fn sha256(data: &[u8]) -> [u8; 32] {
const K: [u32; 64] = [
0x428a_2f98,
0x7137_4491,
0xb5c0_fbcf,
0xe9b5_dba5,
0x3956_c25b,
0x59f1_11f1,
0x923f_82a4,
0xab1c_5ed5,
0xd807_aa98,
0x1283_5b01,
0x2431_85be,
0x550c_7dc3,
0x72be_5d74,
0x80de_b1fe,
0x9bdc_06a7,
0xc19b_f174,
0xe49b_69c1,
0xefbe_4786,
0x0fc1_9dc6,
0x240c_a1cc,
0x2de9_2c6f,
0x4a74_84aa,
0x5cb0_a9dc,
0x76f9_88da,
0x983e_5152,
0xa831_c66d,
0xb003_27c8,
0xbf59_7fc7,
0xc6e0_0bf3,
0xd5a7_9147,
0x06ca_6351,
0x1429_2967,
0x27b7_0a85,
0x2e1b_2138,
0x4d2c_6dfc,
0x5338_0d13,
0x650a_7354,
0x766a_0abb,
0x81c2_c92e,
0x9272_2c85,
0xa2bf_e8a1,
0xa81a_664b,
0xc24b_8b70,
0xc76c_51a3,
0xd192_e819,
0xd699_0624,
0xf40e_3585,
0x106a_a070,
0x19a4_c116,
0x1e37_6c08,
0x2748_774c,
0x34b0_bcb5,
0x391c_0cb3,
0x4ed8_aa4a,
0x5b9c_ca4f,
0x682e_6ff3,
0x748f_82ee,
0x78a5_636f,
0x84c8_7814,
0x8cc7_0208,
0x90be_fffa,
0xa450_6ceb,
0xbef9_a3f7,
0xc671_78f2,
];
let mut h = [
0x6a09_e667u32,
0xbb67_ae85,
0x3c6e_f372,
0xa54f_f53a,
0x510e_527f,
0x9b05_688c,
0x1f83_d9ab,
0x5be0_cd19,
];
let mut msg = data.to_vec();
let bit_len = (msg.len() as u64).wrapping_mul(8);
msg.push(0x80);
while msg.len() % 64 != 56 {
msg.push(0);
}
msg.extend(bit_len.to_be_bytes());
for chunk in msg.chunks_exact(64) {
let mut w = [0u32; 64];
for (i, word) in w.iter_mut().take(16).enumerate() {
let start = i * 4;
*word = u32::from_be_bytes([
chunk[start],
chunk[start + 1],
chunk[start + 2],
chunk[start + 3],
]);
}
for i in 16..64 {
let s0 = w[i - 15].rotate_right(7) ^ w[i - 15].rotate_right(18) ^ (w[i - 15] >> 3);
let s1 = w[i - 2].rotate_right(17) ^ w[i - 2].rotate_right(19) ^ (w[i - 2] >> 10);
w[i] = w[i - 16]
.wrapping_add(s0)
.wrapping_add(w[i - 7])
.wrapping_add(s1);
}
let (mut a, mut b, mut c, mut d, mut e, mut f, mut g, mut hh) =
(h[0], h[1], h[2], h[3], h[4], h[5], h[6], h[7]);
for i in 0..64 {
let s1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25);
let ch = (e & f) ^ ((!e) & g);
let temp1 = hh
.wrapping_add(s1)
.wrapping_add(ch)
.wrapping_add(K[i])
.wrapping_add(w[i]);
let s0 = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22);
let maj = (a & b) ^ (a & c) ^ (b & c);
let temp2 = s0.wrapping_add(maj);
hh = g;
g = f;
f = e;
e = d.wrapping_add(temp1);
d = c;
c = b;
b = a;
a = temp1.wrapping_add(temp2);
}
h[0] = h[0].wrapping_add(a);
h[1] = h[1].wrapping_add(b);
h[2] = h[2].wrapping_add(c);
h[3] = h[3].wrapping_add(d);
h[4] = h[4].wrapping_add(e);
h[5] = h[5].wrapping_add(f);
h[6] = h[6].wrapping_add(g);
h[7] = h[7].wrapping_add(hh);
}
let mut out = [0u8; 32];
for (i, word) in h.iter().enumerate() {
out[i * 4..i * 4 + 4].copy_from_slice(&word.to_be_bytes());
}
out
}
fn hex_encode(bytes: &[u8]) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut out = String::with_capacity(bytes.len() * 2);
for &byte in bytes {
out.push(char::from(HEX[(byte >> 4) as usize]));
out.push(char::from(HEX[(byte & 0x0f) as usize]));
}
out
}
fn hex_decode(s: &str) -> std::result::Result<Vec<u8>, String> {
let bytes = s.as_bytes();
if bytes.len() % 2 != 0 {
return Err("hex string has odd length".to_string());
}
let mut out = Vec::with_capacity(bytes.len() / 2);
for pair in bytes.chunks_exact(2) {
let hi = hex_value(pair[0])?;
let lo = hex_value(pair[1])?;
out.push((hi << 4) | lo);
}
Ok(out)
}
fn hex_value(b: u8) -> std::result::Result<u8, String> {
match b {
b'0'..=b'9' => Ok(b - b'0'),
b'a'..=b'f' => Ok(b - b'a' + 10),
b'A'..=b'F' => Ok(b - b'A' + 10),
_ => Err(format!("invalid hex byte `{}`", char::from(b))),
}
}
mod serde_hex {
use serde::{Deserialize, Deserializer, Serializer, de};
pub fn serialize<S>(bytes: &[u8], serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&super::hex_encode(bytes))
}
pub fn deserialize<'de, D>(deserializer: D) -> std::result::Result<Vec<u8>, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
super::hex_decode(&s).map_err(de::Error::custom)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hashes_match_cursor_paths_and_blob_ids() {
assert_eq!(
md5_hex(b"/Users/nishantjoshi/Workspace/10-Work-Projects/10.03-Replay/transcript"),
"bee2c84d4cc659714c766b9d0e4af911"
);
assert_eq!(
sha256_hex(br#"{"role":"user","content":"hello"}"#),
"b5344ebd07750817371cf4670e556b15a4babb975bde955ff4ce24bca09731ef"
);
}
}