use super::Server;
use crate::formats::{
FormatCodecError, FormatCodecErrorKind, FormatCodecPhase, TmuxText, TransportDialect,
};
use crate::version::since::LIST_KEYS_FORMAT;
use crate::{Command, Error, ListingDecodeError};
const FIELDS: [&str; 5] = [
"key_table",
"key_string",
"key_repeat",
"key_note",
"key_command",
];
const TEMPLATE: &str =
"#{q:key_table}=#{q:key_string}=#{key_repeat}=#{q:key_note}=#{q:key_command}=";
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct KeyBinding {
table: TmuxText,
key: TmuxText,
command: TmuxText,
note: Option<TmuxText>,
repeats: bool,
}
impl KeyBinding {
#[must_use]
pub const fn table(&self) -> &TmuxText {
&self.table
}
#[must_use]
pub const fn key(&self) -> &TmuxText {
&self.key
}
#[must_use]
pub const fn command(&self) -> &TmuxText {
&self.command
}
#[must_use]
pub const fn note(&self) -> Option<&TmuxText> {
self.note.as_ref()
}
#[must_use]
pub const fn repeats(&self) -> bool {
self.repeats
}
}
fn parse(stdout: &[u8]) -> Result<Vec<KeyBinding>, FormatCodecError> {
crate::formats::split_quoted_rows(stdout, FIELDS, TransportDialect::RawQ)?
.into_iter()
.enumerate()
.map(|(row, [table, key, repeat, note, command])| {
let repeats = match repeat.as_slice() {
b"0" => false,
b"1" => true,
_ => {
return Err(FormatCodecError::uncatalogued(
FormatCodecErrorKind::InvalidValue,
FormatCodecPhase::Decode,
row,
2,
FIELDS[2],
None,
));
}
};
Ok(KeyBinding {
table: TmuxText::from(table),
key: TmuxText::from(key),
command: TmuxText::from(command),
note: (!note.is_empty()).then(|| TmuxText::from(note)),
repeats,
})
})
.collect()
}
impl Server {
pub async fn typed_key_bindings(&self, table: Option<&str>) -> Result<Vec<KeyBinding>, Error> {
self.require("list-keys formats", LIST_KEYS_FORMAT).await?;
let result = self
.cmd(Command::new("list-keys").arg("-F").arg(TEMPLATE))
.await?;
if !result.success() {
return Err(Error::from_refused_result("list-keys", &result, None));
}
let bindings = parse(result.stdout()).map_err(|detail| Error::DecodeListing {
list_command: "list-keys",
detail: ListingDecodeError::new(detail),
})?;
Ok(match table {
Some(table) => bindings
.into_iter()
.filter(|binding| binding.table == table)
.collect(),
None => bindings,
})
}
}
#[cfg(feature = "unstable-fuzzing")]
#[doc(hidden)]
pub fn __fuzz_parse_key_bindings(stdout: &[u8]) {
let _ = parse(stdout);
}
#[cfg(test)]
mod tests {
use super::{KeyBinding, parse};
use crate::TmuxText;
use crate::formats::FormatCodecErrorKind;
fn binding(table: &str, key: &str, command: &str) -> KeyBinding {
KeyBinding {
table: TmuxText::from(table),
key: TmuxText::from(key),
command: TmuxText::from(command),
note: None,
repeats: false,
}
}
#[test]
fn rows_decode_escapes_and_keep_bare_newlines() {
let stdout = br#"solo=M-\'=0==display-message\ a\=b\ \\\;\ display-message\ c=
sp\ ace=\"=1=a
note=display-message\ \"two\ words\"=
"#;
let solo = binding("solo", "M-'", r"display-message a=b \; display-message c");
let mut spaced = binding("sp ace", "\"", "display-message \"two words\"");
spaced.note = Some(TmuxText::from("a\nnote"));
spaced.repeats = true;
assert_eq!(parse(stdout), Ok(vec![solo, spaced]));
assert_eq!(parse(b""), Ok(Vec::new()));
}
#[test]
fn a_row_that_does_not_frame_fails_the_listing() {
let good = b"root=x=0==send-keys=\n".as_slice();
for (stdout, kind) in [
(
b"root=x=0==send-keys".as_slice(),
FormatCodecErrorKind::MissingFieldTerminator,
),
(b"root=x=0==send-keys=", FormatCodecErrorKind::MissingRowLf),
(
b"root=x=0==send-keys=x\n",
FormatCodecErrorKind::UnexpectedRowTerminator,
),
(
b"root=\\x=0==send-keys=\n",
FormatCodecErrorKind::InvalidEscape,
),
(b"root=x\\", FormatCodecErrorKind::DanglingEscape),
(
b"root=x\0=0==send-keys=\n",
FormatCodecErrorKind::EmbeddedNul,
),
(
b"root=x=2==send-keys=\n",
FormatCodecErrorKind::InvalidValue,
),
] {
let listing = [good, stdout].concat();
let error = parse(&listing).expect_err("a malformed row fails");
assert_eq!(error.kind(), kind, "{:?}", String::from_utf8_lossy(stdout));
assert_eq!(error.row(), Some(1));
}
}
}