#![no_std] //! Fixed-capacity shell word and quote parsing. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Error { UnterminatedQuote, TrailingEscape, TooManyWords, TooManyBytes, Nul } #[derive(Clone, Copy, Debug, PartialEq, Eq)] struct Span { start: usize, end: usize } pub struct Words { bytes: [u8; BYTES], spans: [Option; COUNT], used: usize, count: usize, } impl Words { pub fn parse(line: &str) -> Result { let mut words = Self {bytes: [0; BYTES], spans: [None; COUNT], used: 0, count: 0}; let input = line.as_bytes(); let mut index = 0; let mut state = State::Plain; let mut start = None; while index < input.len() { let byte = input[index]; if byte == 0 { return Err(Error::Nul); } match state { State::Plain => match byte { b' ' | b'\t' => words.finish(&mut start)?, b'\'' => { words.begin(&mut start); state = State::Single; } b'"' => { words.begin(&mut start); state = State::Double; } b'\\' => { index += 1; if index == input.len() { return Err(Error::TrailingEscape); } words.begin(&mut start); words.push(input[index])?; } _ => { words.begin(&mut start); words.push(byte)?; } }, State::Single => { if byte == b'\'' { state = State::Plain; } else { words.push(byte)?; } } State::Double => match byte { b'"' => state = State::Plain, b'\\' => { index += 1; if index == input.len() { return Err(Error::TrailingEscape); } words.push(input[index])?; } _ => words.push(byte)?, }, } index += 1; } if state != State::Plain { return Err(Error::UnterminatedQuote); } words.finish(&mut start)?; Ok(words) } fn begin(&self, start: &mut Option) { if start.is_none() { *start = Some(self.used); } } fn push(&mut self, byte: u8) -> Result<(), Error> { if byte == 0 { return Err(Error::Nul); } if self.used == BYTES { return Err(Error::TooManyBytes); } self.bytes[self.used] = byte; self.used += 1; Ok(()) } fn finish(&mut self, start: &mut Option) -> Result<(), Error> { if let Some(first) = start.take() { if self.count == COUNT { return Err(Error::TooManyWords); } self.spans[self.count] = Some(Span {start: first, end: self.used}); self.count += 1; } Ok(()) } pub const fn len(&self) -> usize { self.count } pub const fn is_empty(&self) -> bool { self.count == 0 } pub fn get(&self, index: usize) -> Option<&str> { let span = self.spans.get(index).copied().flatten()?; Some(core::str::from_utf8(&self.bytes[span.start..span.end]) .expect("source UTF-8 preserved by ASCII syntax removal")) } } #[derive(Clone, Copy, PartialEq, Eq)] enum State { Plain, Single, Double }