use std::ffi::OsStr;
use crate::{Command, Error, Layout, Server, TmuxVersion};
const NAMES: [Layout; 7] = [
Layout::EvenHorizontal,
Layout::EvenVertical,
Layout::MainHorizontal,
Layout::MainVertical,
Layout::Tiled,
Layout::MainHorizontalMirrored,
Layout::MainVerticalMirrored,
];
fn named(input: &[u8], mirrored: bool) -> Option<Layout> {
let names = &NAMES[..if mirrored { 7 } else { 5 }];
if let Some(layout) = names
.iter()
.find(|layout| layout.as_str().as_bytes() == input)
{
return Some(*layout);
}
let mut matches = names
.iter()
.filter(|layout| layout.as_str().as_bytes().starts_with(input));
let first = matches.next().copied()?;
matches.next().is_none().then_some(first)
}
pub(crate) fn accepts(input: &[u8], panes: usize, mirrored: bool) -> bool {
if input.is_empty() || panes == 0 {
return false;
}
if named(input, mirrored).is_some() {
return true;
}
if input.len() <= 5 || input[4] != b',' || !input[..4].iter().all(u8::is_ascii_hexdigit) {
return false;
}
let expected = std::str::from_utf8(&input[..4])
.ok()
.and_then(|header| u16::from_str_radix(header, 16).ok());
let body = &input[5..];
let checksum = body.iter().fold(0_u16, |sum, byte| {
sum.rotate_right(1).wrapping_add(u16::from(*byte))
});
if expected != Some(checksum) {
return false;
}
let mut parser = Parser {
input: body,
offset: 0,
leaves: 0,
};
parser.cell(0) && parser.offset == body.len() && parser.leaves >= panes
}
pub(crate) fn prepare<'a>(
layouts: impl IntoIterator<Item = (&'a OsStr, usize)>,
) -> Result<Vec<(&'a OsStr, usize)>, Error> {
let mut pending = Vec::new();
for (layout, panes) in layouts {
let bytes = layout.as_encoded_bytes();
if bytes.starts_with(b"{") {
pending.push((layout, panes));
continue;
}
let before = accepts(bytes, panes, false);
let after = accepts(bytes, panes, true);
if !before && !after {
return Err(Error::InvalidLayout {
reason: "use a unique layout name or a checksummed nonempty tree with enough pane cells",
});
}
if before != after {
pending.push((layout, panes));
}
}
Ok(pending)
}
pub(crate) fn resolve(pending: &[(&OsStr, usize)], version: &TmuxVersion) -> Result<(), Error> {
let mirrored = version
.behavior_release()
.is_none_or(|release| release >= crate::since::MIRRORED_LAYOUTS);
for (layout, panes) in pending {
let bytes = layout.as_encoded_bytes();
if bytes.starts_with(b"{") {
version.require("a JSON layout string", crate::since::JSON_LAYOUTS)?;
continue;
}
if !accepts(bytes, *panes, mirrored) {
if let Some(layout) = named(bytes, true) {
version.require(layout.as_str(), layout.minimum_release())?;
}
return Err(Error::InvalidLayout {
reason: "use a layout name that is unique on the running tmux version",
});
}
}
Ok(())
}
pub(crate) async fn version(server: &Server) -> Result<TmuxVersion, Error> {
server.endpoint_version().await
}
pub(crate) struct VersionProbe {
marker: String,
}
impl VersionProbe {
pub(crate) fn new() -> Self {
use std::hash::{BuildHasher as _, Hasher as _};
let mut hasher = std::collections::hash_map::RandomState::new().build_hasher();
hasher.write_u32(std::process::id());
Self {
marker: format!("libtmux-version-{:016x}-", hasher.finish()),
}
}
pub(crate) fn command(&self) -> Command {
Command::new("run-shell")
.arg("-C")
.arg(format!("{}#{{version}}", self.marker))
}
pub(crate) fn parse(&self, stderr: &[u8]) -> Result<Option<TmuxVersion>, Error> {
let Some(version) = stderr
.trim_ascii()
.strip_prefix(b"unknown command: ")
.and_then(|rest| rest.strip_prefix(self.marker.as_bytes()))
else {
return Ok(None);
};
let mut framed = b"tmux ".to_vec();
framed.extend_from_slice(version);
framed.push(b'\n');
TmuxVersion::parse_report(&framed, stderr.len()).map(Some)
}
}
struct Parser<'a> {
input: &'a [u8],
offset: usize,
leaves: usize,
}
impl Parser<'_> {
fn take(&mut self, byte: u8) -> bool {
if self.input.get(self.offset) != Some(&byte) {
return false;
}
self.offset += 1;
true
}
fn number(&mut self) -> bool {
let start = self.offset;
let mut value = 0_u32;
while let Some(digit) = self.input.get(self.offset) {
if !digit.is_ascii_digit() {
break;
}
let Some(next) = value
.checked_mul(10)
.and_then(|value| value.checked_add(u32::from(digit - b'0')))
else {
return false;
};
value = next;
self.offset += 1;
}
self.offset > start
}
fn cell(&mut self, depth: usize) -> bool {
if depth > 256
|| !self.number()
|| !self.take(b'x')
|| !self.number()
|| !self.take(b',')
|| !self.number()
|| !self.take(b',')
|| !self.number()
{
return false;
}
let saved = self.offset;
if self.take(b',') && (!self.number() || self.input.get(self.offset) == Some(&b'x')) {
self.offset = saved;
}
let close = if self.take(b'{') {
b'}'
} else if self.take(b'[') {
b']'
} else {
self.leaves += 1;
return true;
};
if !self.cell(depth + 1) {
return false;
}
while self.take(b',') {
if !self.cell(depth + 1) {
return false;
}
}
self.take(close)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn serialized(body: &[u8]) -> Vec<u8> {
let checksum = body.iter().fold(0_u16, |sum, byte| {
sum.rotate_right(1).wrapping_add(u16::from(*byte))
});
[format!("{checksum:04x},").as_bytes(), body].concat()
}
#[test]
fn layout_preflight_shared_corpus_keeps_geometry_with_tmux() {
let corpus: serde_json::Value =
serde_json::from_str(include_str!("../tests/fixtures/layout-preflight.json")).unwrap();
for case in corpus.as_array().unwrap() {
let id = case["id"].as_str().unwrap();
let layout = case["layout"].as_str().unwrap();
let panes = usize::try_from(case["pane_count"].as_u64().unwrap()).unwrap();
for (version, mirrored) in [("3.2a", false), ("3.3a", false), ("3.7c", true)] {
let geometry = matches!(
id,
"bad-inner-size" | "nested-invalid-width" | "nested-short-parent"
);
let expected = !layout.is_empty()
&& (geometry || case["expected_valid"][version].as_bool().unwrap());
assert_eq!(
accepts(layout.as_bytes(), panes, mirrored),
expected,
"{version} {id}"
);
}
}
}
#[test]
fn layout_preflight_unsigned_fields_depth_and_bytes() {
assert!(accepts(
&serialized(b"4294967295x0,0,0,4294967295"),
1,
false
));
for body in [
b"4294967296x1,0,0".as_slice(),
b"1x1,0,4294967296",
b"1x1,0,0,4294967296",
b"1x1,0,0{}",
b"1x1,0,0[]",
b"\xffx1,0,0",
] {
assert!(!accepts(&serialized(body), 1, true));
}
let nested = format!("{}1x1,0,0{}", "1x1,0,0{".repeat(256), "}".repeat(256));
assert!(accepts(&serialized(nested.as_bytes()), 1, true));
assert!(!accepts(
&serialized(format!("1x1,0,0{{{nested}}}").as_bytes()),
1,
true
));
let long = format!("{}1x1,0,0", "0".repeat(9000));
assert!(accepts(&serialized(long.as_bytes()), 1, true));
assert!(!accepts(b"tiled", 0, true));
}
#[test]
fn layout_preflight_retains_native_version_and_error_semantics() {
let old = TmuxVersion::parse_output(b"tmux 3.2a\n").unwrap();
let new = TmuxVersion::parse_output(b"tmux 3.7c\n").unwrap();
let json = [(OsStr::new(r#"{"V":2,"L":[]}"#), 1)];
let pending = prepare(json).unwrap();
for output in ["tmux 3.7c\n", "tmux 3.8-rc\n"] {
let json_incapable = TmuxVersion::parse_output(output.as_bytes()).unwrap();
assert_eq!(
resolve(&pending, &json_incapable).unwrap_err().kind(),
crate::ErrorKind::UnsupportedVersion,
"{output}"
);
}
assert_eq!(
resolve(&pending, &new).unwrap_err().kind(),
crate::ErrorKind::UnsupportedVersion
);
for output in [
"tmux 3.8-rc2\n",
"tmux 3.8-rc3\n",
"tmux 3.8\n",
"tmux next-3.9\n",
] {
let json_capable = TmuxVersion::parse_output(output.as_bytes()).unwrap();
resolve(&pending, &json_capable).unwrap();
}
let mirrored = [(OsStr::new("main-horizontal-mirrored"), 1)];
assert_eq!(
resolve(&prepare(mirrored).unwrap(), &old)
.unwrap_err()
.kind(),
crate::ErrorKind::UnsupportedVersion
);
resolve(&prepare(mirrored).unwrap(), &new).unwrap();
let abbreviation = [(OsStr::new("main-h"), 1)];
resolve(&prepare(abbreviation).unwrap(), &old).unwrap();
let error = resolve(&prepare(abbreviation).unwrap(), &new).unwrap_err();
assert_eq!(error.kind(), crate::ErrorKind::InvalidInput);
assert!(!error.is_transient());
}
}