| 1 | //! Parameterized string expansion |
| 2 | |
| 3 | use std::iter::repeat; |
| 4 | |
| 5 | use self::Param::*; |
| 6 | use self::States::*; |
| 7 | |
| 8 | #[cfg (test)] |
| 9 | mod tests; |
| 10 | |
| 11 | #[derive (Clone, Copy, PartialEq)] |
| 12 | enum States { |
| 13 | Nothing, |
| 14 | Percent, |
| 15 | SetVar, |
| 16 | GetVar, |
| 17 | PushParam, |
| 18 | CharConstant, |
| 19 | CharClose, |
| 20 | IntConstant(i32), |
| 21 | FormatPattern(Flags, FormatState), |
| 22 | SeekIfElse(usize), |
| 23 | SeekIfElsePercent(usize), |
| 24 | SeekIfEnd(usize), |
| 25 | SeekIfEndPercent(usize), |
| 26 | } |
| 27 | |
| 28 | #[derive (Copy, PartialEq, Clone)] |
| 29 | enum FormatState { |
| 30 | Flags, |
| 31 | Width, |
| 32 | Precision, |
| 33 | } |
| 34 | |
| 35 | /// Types of parameters a capability can use |
| 36 | #[allow (missing_docs)] |
| 37 | #[derive (Clone)] |
| 38 | pub(crate) enum Param { |
| 39 | Number(i32), |
| 40 | } |
| 41 | |
| 42 | /// Container for static and dynamic variable arrays |
| 43 | pub(crate) struct Variables { |
| 44 | /// Static variables A-Z |
| 45 | sta_va: [Param; 26], |
| 46 | /// Dynamic variables a-z |
| 47 | dyn_va: [Param; 26], |
| 48 | } |
| 49 | |
| 50 | impl Variables { |
| 51 | /// Returns a new zero-initialized Variables |
| 52 | pub(crate) fn new() -> Variables { |
| 53 | Variables { |
| 54 | sta_va: [ |
| 55 | Number(0), |
| 56 | Number(0), |
| 57 | Number(0), |
| 58 | Number(0), |
| 59 | Number(0), |
| 60 | Number(0), |
| 61 | Number(0), |
| 62 | Number(0), |
| 63 | Number(0), |
| 64 | Number(0), |
| 65 | Number(0), |
| 66 | Number(0), |
| 67 | Number(0), |
| 68 | Number(0), |
| 69 | Number(0), |
| 70 | Number(0), |
| 71 | Number(0), |
| 72 | Number(0), |
| 73 | Number(0), |
| 74 | Number(0), |
| 75 | Number(0), |
| 76 | Number(0), |
| 77 | Number(0), |
| 78 | Number(0), |
| 79 | Number(0), |
| 80 | Number(0), |
| 81 | ], |
| 82 | dyn_va: [ |
| 83 | Number(0), |
| 84 | Number(0), |
| 85 | Number(0), |
| 86 | Number(0), |
| 87 | Number(0), |
| 88 | Number(0), |
| 89 | Number(0), |
| 90 | Number(0), |
| 91 | Number(0), |
| 92 | Number(0), |
| 93 | Number(0), |
| 94 | Number(0), |
| 95 | Number(0), |
| 96 | Number(0), |
| 97 | Number(0), |
| 98 | Number(0), |
| 99 | Number(0), |
| 100 | Number(0), |
| 101 | Number(0), |
| 102 | Number(0), |
| 103 | Number(0), |
| 104 | Number(0), |
| 105 | Number(0), |
| 106 | Number(0), |
| 107 | Number(0), |
| 108 | Number(0), |
| 109 | ], |
| 110 | } |
| 111 | } |
| 112 | } |
| 113 | |
| 114 | /// Expand a parameterized capability |
| 115 | /// |
| 116 | /// # Arguments |
| 117 | /// * `cap` - string to expand |
| 118 | /// * `params` - vector of params for %p1 etc |
| 119 | /// * `vars` - Variables struct for %Pa etc |
| 120 | /// |
| 121 | /// To be compatible with ncurses, `vars` should be the same between calls to `expand` for |
| 122 | /// multiple capabilities for the same terminal. |
| 123 | pub(crate) fn expand( |
| 124 | cap: &[u8], |
| 125 | params: &[Param], |
| 126 | vars: &mut Variables, |
| 127 | ) -> Result<Vec<u8>, String> { |
| 128 | let mut state = Nothing; |
| 129 | |
| 130 | // expanded cap will only rarely be larger than the cap itself |
| 131 | let mut output = Vec::with_capacity(cap.len()); |
| 132 | |
| 133 | let mut stack: Vec<Param> = Vec::new(); |
| 134 | |
| 135 | // Copy parameters into a local vector for mutability |
| 136 | let mut mparams = [ |
| 137 | Number(0), |
| 138 | Number(0), |
| 139 | Number(0), |
| 140 | Number(0), |
| 141 | Number(0), |
| 142 | Number(0), |
| 143 | Number(0), |
| 144 | Number(0), |
| 145 | Number(0), |
| 146 | ]; |
| 147 | for (dst, src) in mparams.iter_mut().zip(params.iter()) { |
| 148 | *dst = (*src).clone(); |
| 149 | } |
| 150 | |
| 151 | for &c in cap.iter() { |
| 152 | let cur = c as char; |
| 153 | let mut old_state = state; |
| 154 | match state { |
| 155 | Nothing => { |
| 156 | if cur == '%' { |
| 157 | state = Percent; |
| 158 | } else { |
| 159 | output.push(c); |
| 160 | } |
| 161 | } |
| 162 | Percent => { |
| 163 | match cur { |
| 164 | '%' => { |
| 165 | output.push(c); |
| 166 | state = Nothing |
| 167 | } |
| 168 | 'c' => { |
| 169 | match stack.pop() { |
| 170 | // if c is 0, use 0200 (128) for ncurses compatibility |
| 171 | Some(Number(0)) => output.push(128u8), |
| 172 | // Don't check bounds. ncurses just casts and truncates. |
| 173 | Some(Number(c)) => output.push(c as u8), |
| 174 | None => return Err("stack is empty" .to_string()), |
| 175 | } |
| 176 | } |
| 177 | 'p' => state = PushParam, |
| 178 | 'P' => state = SetVar, |
| 179 | 'g' => state = GetVar, |
| 180 | ' \'' => state = CharConstant, |
| 181 | '{' => state = IntConstant(0), |
| 182 | 'l' => match stack.pop() { |
| 183 | Some(_) => return Err("a non-str was used with %l" .to_string()), |
| 184 | None => return Err("stack is empty" .to_string()), |
| 185 | }, |
| 186 | '+' | '-' | '/' | '*' | '^' | '&' | '|' | 'm' => { |
| 187 | match (stack.pop(), stack.pop()) { |
| 188 | (Some(Number(y)), Some(Number(x))) => stack.push(Number(match cur { |
| 189 | '+' => x + y, |
| 190 | '-' => x - y, |
| 191 | '*' => x * y, |
| 192 | '/' => x / y, |
| 193 | '|' => x | y, |
| 194 | '&' => x & y, |
| 195 | '^' => x ^ y, |
| 196 | 'm' => x % y, |
| 197 | _ => unreachable!("All cases handled" ), |
| 198 | })), |
| 199 | _ => return Err("stack is empty" .to_string()), |
| 200 | } |
| 201 | } |
| 202 | '=' | '>' | '<' | 'A' | 'O' => match (stack.pop(), stack.pop()) { |
| 203 | (Some(Number(y)), Some(Number(x))) => stack.push(Number( |
| 204 | if match cur { |
| 205 | '=' => x == y, |
| 206 | '<' => x < y, |
| 207 | '>' => x > y, |
| 208 | 'A' => x > 0 && y > 0, |
| 209 | 'O' => x > 0 || y > 0, |
| 210 | _ => unreachable!(), |
| 211 | } { |
| 212 | 1 |
| 213 | } else { |
| 214 | 0 |
| 215 | }, |
| 216 | )), |
| 217 | _ => return Err("stack is empty" .to_string()), |
| 218 | }, |
| 219 | '!' | '~' => match stack.pop() { |
| 220 | Some(Number(x)) => stack.push(Number(match cur { |
| 221 | '!' if x > 0 => 0, |
| 222 | '!' => 1, |
| 223 | '~' => !x, |
| 224 | _ => unreachable!(), |
| 225 | })), |
| 226 | None => return Err("stack is empty" .to_string()), |
| 227 | }, |
| 228 | 'i' => match (&mparams[0], &mparams[1]) { |
| 229 | (&Number(x), &Number(y)) => { |
| 230 | mparams[0] = Number(x + 1); |
| 231 | mparams[1] = Number(y + 1); |
| 232 | } |
| 233 | }, |
| 234 | |
| 235 | // printf-style support for %doxXs |
| 236 | 'd' | 'o' | 'x' | 'X' | 's' => { |
| 237 | if let Some(arg) = stack.pop() { |
| 238 | let flags = Flags::new(); |
| 239 | let res = format(arg, FormatOp::from_char(cur), flags)?; |
| 240 | output.extend(res.iter().cloned()); |
| 241 | } else { |
| 242 | return Err("stack is empty" .to_string()); |
| 243 | } |
| 244 | } |
| 245 | ':' | '#' | ' ' | '.' | '0' ..='9' => { |
| 246 | let mut flags = Flags::new(); |
| 247 | let mut fstate = FormatState::Flags; |
| 248 | match cur { |
| 249 | ':' => (), |
| 250 | '#' => flags.alternate = true, |
| 251 | ' ' => flags.space = true, |
| 252 | '.' => fstate = FormatState::Precision, |
| 253 | '0' ..='9' => { |
| 254 | flags.width = cur as usize - '0' as usize; |
| 255 | fstate = FormatState::Width; |
| 256 | } |
| 257 | _ => unreachable!(), |
| 258 | } |
| 259 | state = FormatPattern(flags, fstate); |
| 260 | } |
| 261 | |
| 262 | // conditionals |
| 263 | '?' => (), |
| 264 | 't' => match stack.pop() { |
| 265 | Some(Number(0)) => state = SeekIfElse(0), |
| 266 | Some(Number(_)) => (), |
| 267 | None => return Err("stack is empty" .to_string()), |
| 268 | }, |
| 269 | 'e' => state = SeekIfEnd(0), |
| 270 | ';' => (), |
| 271 | _ => return Err(format!("unrecognized format option {cur}" )), |
| 272 | } |
| 273 | } |
| 274 | PushParam => { |
| 275 | // params are 1-indexed |
| 276 | stack.push( |
| 277 | mparams[match cur.to_digit(10) { |
| 278 | Some(d) => d as usize - 1, |
| 279 | None => return Err("bad param number" .to_string()), |
| 280 | }] |
| 281 | .clone(), |
| 282 | ); |
| 283 | } |
| 284 | SetVar => { |
| 285 | if cur.is_ascii_uppercase() { |
| 286 | if let Some(arg) = stack.pop() { |
| 287 | let idx = (cur as u8) - b'A' ; |
| 288 | vars.sta_va[idx as usize] = arg; |
| 289 | } else { |
| 290 | return Err("stack is empty" .to_string()); |
| 291 | } |
| 292 | } else if cur.is_ascii_lowercase() { |
| 293 | if let Some(arg) = stack.pop() { |
| 294 | let idx = (cur as u8) - b'a' ; |
| 295 | vars.dyn_va[idx as usize] = arg; |
| 296 | } else { |
| 297 | return Err("stack is empty" .to_string()); |
| 298 | } |
| 299 | } else { |
| 300 | return Err("bad variable name in %P" .to_string()); |
| 301 | } |
| 302 | } |
| 303 | GetVar => { |
| 304 | if cur.is_ascii_uppercase() { |
| 305 | let idx = (cur as u8) - b'A' ; |
| 306 | stack.push(vars.sta_va[idx as usize].clone()); |
| 307 | } else if cur.is_ascii_lowercase() { |
| 308 | let idx = (cur as u8) - b'a' ; |
| 309 | stack.push(vars.dyn_va[idx as usize].clone()); |
| 310 | } else { |
| 311 | return Err("bad variable name in %g" .to_string()); |
| 312 | } |
| 313 | } |
| 314 | CharConstant => { |
| 315 | stack.push(Number(c as i32)); |
| 316 | state = CharClose; |
| 317 | } |
| 318 | CharClose => { |
| 319 | if cur != ' \'' { |
| 320 | return Err("malformed character constant" .to_string()); |
| 321 | } |
| 322 | } |
| 323 | IntConstant(i) => { |
| 324 | if cur == '}' { |
| 325 | stack.push(Number(i)); |
| 326 | state = Nothing; |
| 327 | } else if let Some(digit) = cur.to_digit(10) { |
| 328 | match i.checked_mul(10).and_then(|i_ten| i_ten.checked_add(digit as i32)) { |
| 329 | Some(i) => { |
| 330 | state = IntConstant(i); |
| 331 | old_state = Nothing; |
| 332 | } |
| 333 | None => return Err("int constant too large" .to_string()), |
| 334 | } |
| 335 | } else { |
| 336 | return Err("bad int constant" .to_string()); |
| 337 | } |
| 338 | } |
| 339 | FormatPattern(ref mut flags, ref mut fstate) => { |
| 340 | old_state = Nothing; |
| 341 | match (*fstate, cur) { |
| 342 | (_, 'd' ) | (_, 'o' ) | (_, 'x' ) | (_, 'X' ) | (_, 's' ) => { |
| 343 | if let Some(arg) = stack.pop() { |
| 344 | let res = format(arg, FormatOp::from_char(cur), *flags)?; |
| 345 | output.extend(res.iter().cloned()); |
| 346 | // will cause state to go to Nothing |
| 347 | old_state = FormatPattern(*flags, *fstate); |
| 348 | } else { |
| 349 | return Err("stack is empty" .to_string()); |
| 350 | } |
| 351 | } |
| 352 | (FormatState::Flags, '#' ) => { |
| 353 | flags.alternate = true; |
| 354 | } |
| 355 | (FormatState::Flags, '-' ) => { |
| 356 | flags.left = true; |
| 357 | } |
| 358 | (FormatState::Flags, '+' ) => { |
| 359 | flags.sign = true; |
| 360 | } |
| 361 | (FormatState::Flags, ' ' ) => { |
| 362 | flags.space = true; |
| 363 | } |
| 364 | (FormatState::Flags, '0' ..='9' ) => { |
| 365 | flags.width = cur as usize - '0' as usize; |
| 366 | *fstate = FormatState::Width; |
| 367 | } |
| 368 | (FormatState::Flags, '.' ) => { |
| 369 | *fstate = FormatState::Precision; |
| 370 | } |
| 371 | (FormatState::Width, '0' ..='9' ) => { |
| 372 | let old = flags.width; |
| 373 | flags.width = flags.width * 10 + (cur as usize - '0' as usize); |
| 374 | if flags.width < old { |
| 375 | return Err("format width overflow" .to_string()); |
| 376 | } |
| 377 | } |
| 378 | (FormatState::Width, '.' ) => { |
| 379 | *fstate = FormatState::Precision; |
| 380 | } |
| 381 | (FormatState::Precision, '0' ..='9' ) => { |
| 382 | let old = flags.precision; |
| 383 | flags.precision = flags.precision * 10 + (cur as usize - '0' as usize); |
| 384 | if flags.precision < old { |
| 385 | return Err("format precision overflow" .to_string()); |
| 386 | } |
| 387 | } |
| 388 | _ => return Err("invalid format specifier" .to_string()), |
| 389 | } |
| 390 | } |
| 391 | SeekIfElse(level) => { |
| 392 | if cur == '%' { |
| 393 | state = SeekIfElsePercent(level); |
| 394 | } |
| 395 | old_state = Nothing; |
| 396 | } |
| 397 | SeekIfElsePercent(level) => { |
| 398 | if cur == ';' { |
| 399 | if level == 0 { |
| 400 | state = Nothing; |
| 401 | } else { |
| 402 | state = SeekIfElse(level - 1); |
| 403 | } |
| 404 | } else if cur == 'e' && level == 0 { |
| 405 | state = Nothing; |
| 406 | } else if cur == '?' { |
| 407 | state = SeekIfElse(level + 1); |
| 408 | } else { |
| 409 | state = SeekIfElse(level); |
| 410 | } |
| 411 | } |
| 412 | SeekIfEnd(level) => { |
| 413 | if cur == '%' { |
| 414 | state = SeekIfEndPercent(level); |
| 415 | } |
| 416 | old_state = Nothing; |
| 417 | } |
| 418 | SeekIfEndPercent(level) => { |
| 419 | if cur == ';' { |
| 420 | if level == 0 { |
| 421 | state = Nothing; |
| 422 | } else { |
| 423 | state = SeekIfEnd(level - 1); |
| 424 | } |
| 425 | } else if cur == '?' { |
| 426 | state = SeekIfEnd(level + 1); |
| 427 | } else { |
| 428 | state = SeekIfEnd(level); |
| 429 | } |
| 430 | } |
| 431 | } |
| 432 | if state == old_state { |
| 433 | state = Nothing; |
| 434 | } |
| 435 | } |
| 436 | Ok(output) |
| 437 | } |
| 438 | |
| 439 | #[derive (Copy, PartialEq, Clone)] |
| 440 | struct Flags { |
| 441 | width: usize, |
| 442 | precision: usize, |
| 443 | alternate: bool, |
| 444 | left: bool, |
| 445 | sign: bool, |
| 446 | space: bool, |
| 447 | } |
| 448 | |
| 449 | impl Flags { |
| 450 | fn new() -> Flags { |
| 451 | Flags { width: 0, precision: 0, alternate: false, left: false, sign: false, space: false } |
| 452 | } |
| 453 | } |
| 454 | |
| 455 | #[derive (Copy, Clone)] |
| 456 | enum FormatOp { |
| 457 | Digit, |
| 458 | Octal, |
| 459 | LowerHex, |
| 460 | UpperHex, |
| 461 | String, |
| 462 | } |
| 463 | |
| 464 | impl FormatOp { |
| 465 | fn from_char(c: char) -> FormatOp { |
| 466 | match c { |
| 467 | 'd' => FormatOp::Digit, |
| 468 | 'o' => FormatOp::Octal, |
| 469 | 'x' => FormatOp::LowerHex, |
| 470 | 'X' => FormatOp::UpperHex, |
| 471 | 's' => FormatOp::String, |
| 472 | _ => panic!("bad FormatOp char" ), |
| 473 | } |
| 474 | } |
| 475 | } |
| 476 | |
| 477 | fn format(val: Param, op: FormatOp, flags: Flags) -> Result<Vec<u8>, String> { |
| 478 | let mut s = match val { |
| 479 | Number(d) => { |
| 480 | match op { |
| 481 | FormatOp::Digit => { |
| 482 | if flags.sign { |
| 483 | format!(" {:+01$}" , d, flags.precision) |
| 484 | } else if d < 0 { |
| 485 | // C doesn't take sign into account in precision calculation. |
| 486 | format!(" {:01$}" , d, flags.precision + 1) |
| 487 | } else if flags.space { |
| 488 | format!(" {:01$}" , d, flags.precision) |
| 489 | } else { |
| 490 | format!(" {:01$}" , d, flags.precision) |
| 491 | } |
| 492 | } |
| 493 | FormatOp::Octal => { |
| 494 | if flags.alternate { |
| 495 | // Leading octal zero counts against precision. |
| 496 | format!("0 {:01$o}" , d, flags.precision.saturating_sub(1)) |
| 497 | } else { |
| 498 | format!(" {:01$o}" , d, flags.precision) |
| 499 | } |
| 500 | } |
| 501 | FormatOp::LowerHex => { |
| 502 | if flags.alternate && d != 0 { |
| 503 | format!("0x {:01$x}" , d, flags.precision) |
| 504 | } else { |
| 505 | format!(" {:01$x}" , d, flags.precision) |
| 506 | } |
| 507 | } |
| 508 | FormatOp::UpperHex => { |
| 509 | if flags.alternate && d != 0 { |
| 510 | format!("0X {:01$X}" , d, flags.precision) |
| 511 | } else { |
| 512 | format!(" {:01$X}" , d, flags.precision) |
| 513 | } |
| 514 | } |
| 515 | FormatOp::String => return Err("non-number on stack with %s" .to_string()), |
| 516 | } |
| 517 | .into_bytes() |
| 518 | } |
| 519 | }; |
| 520 | if flags.width > s.len() { |
| 521 | let n = flags.width - s.len(); |
| 522 | if flags.left { |
| 523 | s.extend(repeat(b' ' ).take(n)); |
| 524 | } else { |
| 525 | let mut s_ = Vec::with_capacity(flags.width); |
| 526 | s_.extend(repeat(b' ' ).take(n)); |
| 527 | s_.extend(s); |
| 528 | s = s_; |
| 529 | } |
| 530 | } |
| 531 | Ok(s) |
| 532 | } |
| 533 | |