use crate::rpc::functions::monitor::SemihostingEvent;
use probe_rs::{
Core,
semihosting::{
CloseRequest, FileLengthRequest, OpenRequest, ReadRequest, RemoveRequest, RenameRequest,
SeekRequest, SemihostingCommand, WriteRequest,
},
};
#[cfg(target_family = "unix")]
use std::os::unix::net::UnixStream;
use std::{
fs::File,
io::{Read, Seek, SeekFrom, Write},
net::TcpStream,
num::NonZeroU32,
};
enum FileHandle {
Stdout,
Stderr,
File(File),
TcpStream(TcpStream),
#[cfg(target_family = "unix")]
UnixStream(UnixStream),
}
use std::convert::Infallible;
use postcard_schema::Schema;
use regex::Regex;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Schema, Clone)]
enum Mapping {
Exact(String, String),
Prefix(String, String),
Regex(String, String),
}
enum FileVariant {
File(String),
TcpStream(String),
UnixStream(String),
}
#[derive(Serialize, Deserialize, Schema, Clone)]
pub struct SemihostingOptions {
mappings: Vec<Mapping>,
}
impl SemihostingOptions {
pub fn new() -> Self {
Self { mappings: vec![] }
}
pub fn add_file(&mut self, from: String, to: String) -> Result<(), Infallible> {
self.mappings.push(Mapping::Exact(from, to));
Ok(())
}
pub fn add_file_prefix(&mut self, from: String, to: String) -> Result<(), Infallible> {
self.mappings.push(Mapping::Prefix(from, to));
Ok(())
}
pub fn add_file_regex(&mut self, re: String, to: String) -> Result<(), regex::Error> {
let _ = Regex::new(&re)?; self.mappings.push(Mapping::Regex(re, to));
Ok(())
}
fn map_file(&self, value: &str) -> Option<String> {
for item in &self.mappings {
match item {
Mapping::Exact(from, to) => {
if value == from {
return Some(to.clone());
}
}
Mapping::Prefix(prefix, to) => {
if let Some(rest) = value.strip_prefix(prefix) {
return Some(to.clone() + rest);
}
}
Mapping::Regex(re, to) => {
let re = Regex::new(re).expect("valid Regex");
if let Some(captures) = re.captures(value) {
let mut ret = String::new();
captures.expand(to, &mut ret);
return Some(ret);
}
}
}
}
None
}
fn find_file(&self, value: &str) -> Option<FileVariant> {
let v = self.map_file(value)?;
if let Some(r) = v.strip_prefix("file:") {
return Some(FileVariant::File(r.into()));
}
if let Some(r) = v.strip_prefix("tcp:") {
return Some(FileVariant::TcpStream(r.into()));
}
if let Some(r) = v.strip_prefix("unix:") {
return Some(FileVariant::UnixStream(r.into()));
}
Some(FileVariant::File(v))
}
}
pub struct SemihostingFileManager {
file_handles: Vec<Option<FileHandle>>,
semihosting_options: SemihostingOptions,
}
impl SemihostingFileManager {
pub fn new(semihosting_options: SemihostingOptions) -> Self {
Self {
file_handles: vec![],
semihosting_options,
}
}
pub fn can_handle(other: SemihostingCommand) -> bool {
matches!(
other,
SemihostingCommand::Open(_)
| SemihostingCommand::Close(_)
| SemihostingCommand::WriteConsole(_)
| SemihostingCommand::Write(_)
| SemihostingCommand::Read(_)
| SemihostingCommand::Seek(_)
| SemihostingCommand::FileLength(_)
| SemihostingCommand::Remove(_)
| SemihostingCommand::Rename(_)
)
}
pub fn handle(
&mut self,
command: SemihostingCommand,
core: &mut Core<'_>,
send: &mut impl FnMut(SemihostingEvent),
) -> anyhow::Result<()> {
let mut send_output = |stream: &str, data: String| {
send(SemihostingEvent::Output {
stream: stream.into(),
data,
});
};
match command {
SemihostingCommand::Open(request) => self.handle_open(core, request),
SemihostingCommand::Close(request) => self.handle_close(core, request),
SemihostingCommand::Write(request) => self.handle_write(core, request, send_output),
SemihostingCommand::WriteConsole(request) => {
send_output("stdout", request.read(core)?);
Ok(())
}
SemihostingCommand::Read(request) => self.handle_read(core, request),
SemihostingCommand::Seek(request) => self.handle_seek(core, request),
SemihostingCommand::FileLength(request) => self.handle_file_length(core, request),
SemihostingCommand::Remove(request) => self.handle_remove(core, request),
SemihostingCommand::Rename(request) => self.handle_rename(core, request),
_ => Ok(()),
}
}
fn open_tt(&self, mode: &str) -> Option<FileHandle> {
match mode.as_bytes()[0] {
b'w' => Some(FileHandle::Stdout),
b'a' => Some(FileHandle::Stderr),
mode => {
tracing::warn!(
"Target wanted to open file :tt with mode {mode}, \
but probe-rs does not support this operation yet. Continuing..."
);
None
}
}
}
fn open_tcp_stream(&self, addr: &str) -> Option<FileHandle> {
TcpStream::connect(addr)
.inspect_err(|err| {
tracing::warn!(
"Target wanted to open a TCP stream to {addr}, \
but it failed with {err:?}. Continuing..."
)
})
.map(FileHandle::TcpStream)
.ok()
}
#[cfg(target_family = "unix")]
fn open_unix_stream(&self, path: &str) -> Option<FileHandle> {
UnixStream::connect(path)
.inspect_err(|err| {
tracing::warn!(
"Target wanted to open an UNIX stream to {path}, \
but it failed with {err:?}. Continuing..."
)
})
.map(FileHandle::UnixStream)
.ok()
}
#[cfg(not(target_family = "unix"))]
fn open_unix_stream(&self, path: &str) -> Option<FileHandle> {
tracing::error!(
"Target wanted to open an UNIX stream to {path}, \
but probe-rs does not support this on the current platform. Continuing..."
);
None
}
fn open_file(&self, path: &str, mode: &str) -> Option<FileHandle> {
let mut options = File::options();
match mode {
"r" | "rb" => options.read(true).write(false).truncate(true).create(false),
"r+" | "r+b" => options.read(true).write(true).truncate(true).create(false),
"w" | "wb" => options.read(false).write(true).truncate(true).create(true),
"w+" | "w+b" => options.read(true).write(true).truncate(true).create(true),
"a" | "ab" => options.read(false).write(true).truncate(false).create(true),
"a+" | "a+b" => options.read(true).write(false).truncate(false).create(true),
mode => {
tracing::error!(
"Target wanted to open file {path} with invalid mode {mode}. Continuing..."
);
return None;
}
};
options
.open(path)
.inspect_err(|err| {
tracing::warn!(
"Target wanted to open file {path} with mode {mode}, \
but it failed with {err:?}. Continuing..."
)
})
.map(FileHandle::File)
.ok()
}
fn handle_open(&mut self, core: &mut Core<'_>, request: OpenRequest) -> anyhow::Result<()> {
let path = request.path(core)?;
let f = if path == ":tt" {
self.open_tt(request.mode())
} else if let Some(path) = self.semihosting_options.find_file(&path) {
match path {
FileVariant::File(path) => self.open_file(&path, request.mode()),
FileVariant::TcpStream(addr) => self.open_tcp_stream(&addr),
FileVariant::UnixStream(path) => self.open_unix_stream(&path),
}
} else {
tracing::warn!(
"Target wanted to open file {path} which is not configured \
for access via semihosting. Continuing..."
);
None
};
if let Some(f) = f {
self.file_handles.push(Some(f));
request.respond_with_handle(
core,
NonZeroU32::new(self.file_handles.len() as u32).unwrap(),
)?;
}
Ok(())
}
fn handle_close(&mut self, core: &mut Core<'_>, request: CloseRequest) -> anyhow::Result<()> {
let handle = request.file_handle();
if self.take_file_handle(handle).is_some() {
self.trim_file_handles();
request.success(core)?;
} else {
tracing::warn!("Target wanted to close invalid file handle {handle}. Continuing...");
}
Ok(())
}
fn handle_write(
&mut self,
core: &mut Core<'_>,
request: WriteRequest,
mut send_output: impl FnMut(&str, String),
) -> anyhow::Result<()> {
let Some((f, log)) = self.get_file_handle("write to", request.file_handle()) else {
return Ok(());
};
let buf = request.read(core)?;
let len = match f {
FileHandle::Stdout => {
send_output("stdout", String::from_utf8_lossy(&buf).into());
Some(buf.len())
}
FileHandle::Stderr => {
send_output("stderr", String::from_utf8_lossy(&buf).into());
Some(buf.len())
}
FileHandle::File(file) => log.inspect(file.write(&buf)),
FileHandle::TcpStream(stream) => log.inspect(stream.write(&buf)),
#[cfg(target_family = "unix")]
FileHandle::UnixStream(stream) => log.inspect(stream.write(&buf)),
};
if let Some(len) = len {
request.write_status(core, (buf.len() - len) as i32)?;
}
Ok(())
}
fn handle_read(&mut self, core: &mut Core<'_>, request: ReadRequest) -> anyhow::Result<()> {
let Some((f, log)) = self.get_file_handle("read from", request.file_handle()) else {
return Ok(());
};
let mut buf = vec![0; request.bytes_to_read() as usize];
let len = match f {
FileHandle::File(file) => log.inspect(file.read(&mut buf)),
FileHandle::TcpStream(stream) => log.inspect(stream.read(&mut buf)),
#[cfg(target_family = "unix")]
FileHandle::UnixStream(stream) => log.inspect(stream.read(&mut buf)),
_ => {
log.not_supported();
None
}
};
if let Some(len) = len {
request.write_buffer_to_target(core, &buf[..len])?;
}
Ok(())
}
fn handle_seek(&mut self, core: &mut Core<'_>, request: SeekRequest) -> anyhow::Result<()> {
let Some((f, log)) = self.get_file_handle("seek in", request.file_handle()) else {
return Ok(());
};
if let FileHandle::File(file) = f {
let pos = SeekFrom::Start(request.position() as u64);
if log.inspect(file.seek(pos)).is_some() {
request.success(core)?;
}
} else {
log.not_supported();
}
Ok(())
}
fn handle_file_length(
&mut self,
core: &mut Core<'_>,
request: FileLengthRequest,
) -> anyhow::Result<()> {
let action = "read the file length of";
let Some((f, log)) = self.get_file_handle(action, request.file_handle()) else {
return Ok(());
};
if let FileHandle::File(file) = f {
if let Some(meta) = log.inspect(file.metadata()) {
request.write_length(core, meta.len() as i32)?;
}
} else {
log.not_supported();
}
Ok(())
}
fn handle_remove(&mut self, core: &mut Core<'_>, request: RemoveRequest) -> anyhow::Result<()> {
let path = request.path(core)?;
tracing::warn!(
"Target wanted to remove file {path}, \
but probe-rs does not support this operation yet. Continuing..."
);
Ok(())
}
fn handle_rename(&mut self, core: &mut Core<'_>, request: RenameRequest) -> anyhow::Result<()> {
let from_path = request.from_path(core)?;
let to_path = request.to_path(core)?;
tracing::warn!(
"Target wanted to rename file {from_path} to {to_path}, \
but probe-rs does not support this operation yet. Continuing..."
);
Ok(())
}
fn get_file_handle_entry(&mut self, handle: u32) -> Option<&mut Option<FileHandle>> {
self.file_handles.get_mut(handle as usize - 1)
}
fn take_file_handle(&mut self, handle: u32) -> Option<FileHandle> {
self.get_file_handle_entry(handle)
.and_then(|inner| inner.take())
}
fn get_file_handle(
&mut self,
action: &'static str,
handle: u32,
) -> Option<(&mut FileHandle, FileHandleLog)> {
let Some(Some(file_handle)) = self.get_file_handle_entry(handle) else {
tracing::warn!("Target wanted to {action} invalid file handle {handle}. Continuing...");
return None;
};
let variant = match file_handle {
FileHandle::Stdout => "stdout",
FileHandle::Stderr => "stderr",
FileHandle::File(_) => "file",
FileHandle::TcpStream(_) => "TCP stream",
#[cfg(target_family = "unix")]
FileHandle::UnixStream(_) => "UNIX stream",
};
Some((
file_handle,
FileHandleLog {
action,
handle,
variant,
},
))
}
fn trim_file_handles(&mut self) {
while let Some(None) = self.file_handles.last() {
self.file_handles.pop();
}
}
}
struct FileHandleLog {
action: &'static str,
handle: u32,
variant: &'static str,
}
impl FileHandleLog {
fn not_supported(&self) {
self.warn("probe-rs does not support this operation")
}
fn inspect<T>(&self, result: std::io::Result<T>) -> Option<T> {
result
.inspect_err(|err| {
self.warn(&format!("it failed with {err:?}"));
})
.ok()
}
fn warn(&self, reason: &str) {
tracing::warn!(
"Target wanted to {action} file handle {handle} ({variant}), \
but {reason}. Continuing...",
action = self.action,
handle = self.handle,
variant = self.variant,
);
}
}