use std::fmt;
use crate::types::{DbFieldType, EpicsValue, PvString};
const DBR_STRING_CAPACITY: usize = 39;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConvertJsonError {
pub refusal: Option<String>,
pub diagnostic: String,
}
impl fmt::Display for ConvertJsonError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(refusal) = &self.refusal {
f.write_str(refusal)?;
}
f.write_str(&self.diagnostic)
}
}
impl std::error::Error for ConvertJsonError {}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Tok {
Eof,
Error,
LeftBrace,
RightBrace,
LeftBracket,
RightBracket,
Comma,
Colon,
Bool,
Null,
Integer,
Double,
Str,
StrWithEscapes,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum LexError {
None,
StringInvalidUtf8,
StringInvalidEscapedChar,
StringInvalidJsonChar,
StringInvalidHexUChar,
StringInvalidHexXChar,
InvalidChar,
InvalidString,
MissingIntegerAfterExponent,
MissingIntegerAfterDecimal,
MissingIntegerAfterMinus,
MissingHexDigitAfter0x,
}
impl LexError {
fn text(self) -> &'static str {
match self {
Self::None => "ok, no error",
Self::StringInvalidUtf8 => "invalid bytes in UTF8 string.",
Self::StringInvalidEscapedChar => {
"inside a string, '\\' occurs before a character which it may not."
}
Self::StringInvalidJsonChar => "invalid character inside string.",
Self::StringInvalidHexUChar => {
"invalid (non-hex) character occurs after '\\u' inside string."
}
Self::StringInvalidHexXChar => {
"invalid (non-hex) character occurs after '\\x' inside string."
}
Self::InvalidChar => "invalid char in json text.",
Self::InvalidString => "invalid string in json text.",
Self::MissingIntegerAfterExponent => {
"malformed number, a digit is required after the exponent."
}
Self::MissingIntegerAfterDecimal => {
"malformed number, a digit is required after the decimal point."
}
Self::MissingIntegerAfterMinus => {
"malformed number, a digit is required after the plus/minus sign."
}
Self::MissingHexDigitAfter0x => {
"malformed number, a hex digit is required after the 0x/0X."
}
}
}
}
enum Utf8 {
Ok,
Eof,
Error,
}
enum CommentEnd {
Done,
Eof,
Error,
}
#[derive(Default)]
struct Lexer {
buf: Vec<u8>,
buf_off: usize,
buf_in_use: bool,
error: LexError,
}
impl Default for LexError {
fn default() -> Self {
Self::None
}
}
impl Lexer {
fn read_char(&mut self, txt: &[u8], off: &mut usize) -> u8 {
if self.buf_in_use && self.buf_off < self.buf.len() {
let c = self.buf[self.buf_off];
self.buf_off += 1;
return c;
}
let c = txt.get(*off).copied().unwrap_or(0);
*off += 1;
c
}
fn unread_char(&mut self, off: &mut usize) {
if *off > 0 {
*off -= 1;
} else {
self.buf_off = self.buf_off.saturating_sub(1);
}
}
fn lex(&mut self, txt: &[u8], off: &mut usize) -> (Tok, Vec<u8>) {
let mut start_offset = *off;
let tok = loop {
if *off >= txt.len() {
break Tok::Eof;
}
let c = self.read_char(txt, off);
match c {
b'{' => break Tok::LeftBrace,
b'}' => break Tok::RightBrace,
b'[' => break Tok::LeftBracket,
b']' => break Tok::RightBracket,
b',' => break Tok::Comma,
b':' => break Tok::Colon,
b'\t' | b'\n' | 0x0b | 0x0c | b'\r' | b' ' => start_offset += 1,
b't' => break self.lex_want(txt, off, b"rue", Tok::Bool),
b'f' => break self.lex_want(txt, off, b"alse", Tok::Bool),
b'n' => break self.lex_want(txt, off, b"ull", Tok::Null),
b'I' => break self.lex_want(txt, off, b"nfinity", Tok::Double),
b'N' => break self.lex_want(txt, off, b"aN", Tok::Double),
b'\'' | b'"' => break self.lex_string(txt, off, c),
b'+' | b'.' | b'-' | b'0'..=b'9' => {
self.unread_char(off);
break self.lex_number(txt, off);
}
b'/' => match self.lex_comment(txt, off) {
CommentEnd::Done => {
self.buf.clear();
self.buf_in_use = false;
start_offset = *off;
}
CommentEnd::Eof => break Tok::Eof,
CommentEnd::Error => break Tok::Error,
},
_ => {
self.error = LexError::InvalidChar;
break Tok::Error;
}
}
};
let mut out = Vec::new();
if tok == Tok::Eof || self.buf_in_use {
if !self.buf_in_use {
self.buf.clear();
}
self.buf_in_use = true;
let span = txt.get(start_offset..*off).unwrap_or(&[]);
self.buf.extend_from_slice(span);
self.buf_off = 0;
if tok != Tok::Eof {
out = self.buf.clone();
self.buf_in_use = false;
}
} else if tok != Tok::Error {
out = txt.get(start_offset..*off).unwrap_or(&[]).to_vec();
}
if matches!(tok, Tok::Str | Tok::StrWithEscapes) && out.len() >= 2 {
out = out[1..out.len() - 1].to_vec();
}
(tok, out)
}
fn lex_want(&mut self, txt: &[u8], off: &mut usize, want: &[u8], on_match: Tok) -> Tok {
for &w in want {
if *off >= txt.len() {
return Tok::Eof;
}
if self.read_char(txt, off) != w {
self.unread_char(off);
self.error = LexError::InvalidString;
return Tok::Error;
}
}
on_match
}
fn lex_string(&mut self, txt: &[u8], off: &mut usize, quote: u8) -> Tok {
let mut tok = Tok::Error;
let mut has_escapes = false;
'lex: loop {
if *off >= txt.len() {
tok = Tok::Eof;
break 'lex;
}
let c = self.read_char(txt, off);
if c == quote {
tok = Tok::Str;
break 'lex;
}
if c == b'\\' {
has_escapes = true;
if *off >= txt.len() {
tok = Tok::Eof;
break 'lex;
}
let esc = self.read_char(txt, off);
if esc == b'u' || esc == b'x' {
let (n, err) = if esc == b'u' {
(4, LexError::StringInvalidHexUChar)
} else {
(2, LexError::StringInvalidHexXChar)
};
for _ in 0..n {
if *off >= txt.len() {
tok = Tok::Eof;
break 'lex;
}
if !self.read_char(txt, off).is_ascii_hexdigit() {
self.unread_char(off);
self.error = err;
break 'lex;
}
}
} else if (b'1'..=b'9').contains(&esc) {
self.unread_char(off);
self.error = LexError::StringInvalidEscapedChar;
break 'lex;
} else if esc == b'\r' {
if *off >= txt.len() {
tok = Tok::Eof;
break 'lex;
}
if self.read_char(txt, off) != b'\n' {
self.unread_char(off);
}
}
} else if c < 0x20 {
self.unread_char(off);
self.error = LexError::StringInvalidJsonChar;
break 'lex;
} else {
match self.lex_utf8_char(txt, off, c) {
Utf8::Ok => {}
Utf8::Eof => {
tok = Tok::Eof;
break 'lex;
}
Utf8::Error => {
self.error = LexError::StringInvalidUtf8;
break 'lex;
}
}
}
}
if has_escapes && tok == Tok::Str {
Tok::StrWithEscapes
} else {
tok
}
}
fn lex_utf8_char(&mut self, txt: &[u8], off: &mut usize, cur: u8) -> Utf8 {
let need = if cur <= 0x7f {
return Utf8::Ok;
} else if cur >> 5 == 0x6 {
1
} else if cur >> 4 == 0x0e {
2
} else if cur >> 3 == 0x1e {
3
} else {
return Utf8::Error;
};
for _ in 0..need {
if *off >= txt.len() {
return Utf8::Eof;
}
if self.read_char(txt, off) >> 6 != 0x2 {
return Utf8::Error;
}
}
Utf8::Ok
}
fn lex_number(&mut self, txt: &[u8], off: &mut usize) -> Tok {
macro_rules! eof {
() => {
if *off >= txt.len() {
return Tok::Eof;
}
};
}
let mut tok = Tok::Integer;
let mut num_rd = 0u32;
eof!();
let mut c = self.read_char(txt, off);
if c == b'-' || c == b'+' {
eof!();
c = self.read_char(txt, off);
}
if c == b'I' {
for &w in b"nfinity" {
eof!();
if self.read_char(txt, off) != w {
self.unread_char(off);
self.error = LexError::InvalidString;
return Tok::Error;
}
}
return Tok::Double;
}
let mut fraction = false;
if c == b'0' {
num_rd += 1;
eof!();
c = self.read_char(txt, off);
if c == b'x' || c == b'X' {
return self.lex_hex(txt, off);
}
} else if (b'1'..=b'9').contains(&c) {
loop {
num_rd += 1;
eof!();
c = self.read_char(txt, off);
if !c.is_ascii_digit() {
break;
}
}
} else if c == b'.' {
fraction = true;
} else {
self.unread_char(off);
self.error = LexError::MissingIntegerAfterMinus;
return Tok::Error;
}
if fraction || c == b'.' {
eof!();
c = self.read_char(txt, off);
while c.is_ascii_digit() {
num_rd += 1;
eof!();
c = self.read_char(txt, off);
}
if num_rd == 0 {
self.unread_char(off);
self.error = LexError::MissingIntegerAfterDecimal;
return Tok::Error;
}
tok = Tok::Double;
}
if c == b'e' || c == b'E' {
eof!();
c = self.read_char(txt, off);
if c == b'+' || c == b'-' {
eof!();
c = self.read_char(txt, off);
}
if !c.is_ascii_digit() {
self.unread_char(off);
self.error = LexError::MissingIntegerAfterExponent;
return Tok::Error;
}
loop {
eof!();
c = self.read_char(txt, off);
if !c.is_ascii_digit() {
break;
}
}
tok = Tok::Double;
}
self.unread_char(off);
tok
}
fn lex_hex(&mut self, txt: &[u8], off: &mut usize) -> Tok {
if *off >= txt.len() {
return Tok::Eof;
}
if !self.read_char(txt, off).is_ascii_hexdigit() {
self.unread_char(off);
self.error = LexError::MissingHexDigitAfter0x;
return Tok::Error;
}
loop {
if *off >= txt.len() {
return Tok::Eof;
}
if !self.read_char(txt, off).is_ascii_hexdigit() {
break;
}
}
self.unread_char(off);
Tok::Integer
}
fn lex_comment(&mut self, txt: &[u8], off: &mut usize) -> CommentEnd {
macro_rules! eof {
() => {
if *off >= txt.len() {
return CommentEnd::Eof;
}
};
}
eof!();
match self.read_char(txt, off) {
b'/' => loop {
eof!();
if self.read_char(txt, off) == b'\n' {
return CommentEnd::Done;
}
},
b'*' => loop {
eof!();
if self.read_char(txt, off) == b'*' {
eof!();
if self.read_char(txt, off) == b'/' {
return CommentEnd::Done;
}
self.unread_char(off);
}
},
_ => {
self.error = LexError::InvalidChar;
CommentEnd::Error
}
}
}
}
fn parse_integer(number: &[u8]) -> Result<i64, ()> {
let mut ret: i64 = 0;
let mut sign: i64 = 1;
let mut base: i64 = 10;
let mut pos = 0;
match number.first() {
Some(b'-') => {
pos = 1;
sign = -1;
}
Some(b'+') => pos = 1,
_ => {}
}
if number.get(pos) == Some(&b'0') && matches!(number.get(pos + 1), Some(b'x' | b'X')) {
base = 16;
pos += 2;
}
let max = i64::MAX / base;
while pos < number.len() {
if ret > max {
return Err(());
}
ret *= base;
let mut digit = i64::from(number[pos]) - i64::from(b'0');
pos += 1;
if digit > 9 {
digit = (digit - i64::from(b'A' - b'0') + 10) & 0xf;
}
if i64::MAX - ret < digit {
return Err(());
}
ret += digit;
}
Ok(sign * ret)
}
fn parse_double(buf: &[u8]) -> Result<f64, ()> {
let text = String::from_utf8_lossy(buf);
let value: f64 = text.parse().unwrap_or(f64::NAN);
if value.is_infinite() && !text.trim_start_matches(['+', '-']).starts_with('I') {
return Err(());
}
Ok(value)
}
fn string_decode(src: &[u8]) -> Vec<u8> {
fn hex_to_digit(src: &[u8], at: usize, len: usize) -> u32 {
let mut val = 0u32;
for i in 0..len {
let mut c = src.get(at + i).copied().unwrap_or(b'0');
if c >= b'A' {
c = (c & !0x20) - 7;
}
val = (val << 4) | u32::from(c - b'0');
}
val
}
fn utf32_to_utf8(cp: u32, out: &mut Vec<u8>) {
match cp {
0..=0x7f => out.push(cp as u8),
0x80..=0x7ff => {
out.push((cp >> 6) as u8 | 0xc0);
out.push((cp & 0x3f) as u8 | 0x80);
}
0x800..=0xffff => {
out.push((cp >> 12) as u8 | 0xe0);
out.push(((cp >> 6) & 0x3f) as u8 | 0x80);
out.push((cp & 0x3f) as u8 | 0x80);
}
0x10000..=0x1f_ffff => {
out.push((cp >> 18) as u8 | 0xf0);
out.push(((cp >> 12) & 0x3f) as u8 | 0x80);
out.push(((cp >> 6) & 0x3f) as u8 | 0x80);
out.push((cp & 0x3f) as u8 | 0x80);
}
_ => out.push(b'?'),
}
}
let mut out = Vec::with_capacity(src.len());
let mut beg = 0usize;
let mut end = 0usize;
while end < src.len() {
if src[end] != b'\\' {
end += 1;
continue;
}
out.extend_from_slice(&src[beg..end]);
end += 1;
match src.get(end).copied().unwrap_or(0) {
b'r' => out.push(b'\r'),
b'n' => out.push(b'\n'),
b'\\' => out.push(b'\\'),
b'f' => out.push(0x0c),
b'b' => out.push(0x08),
b't' => out.push(b'\t'),
b'v' => out.push(0x0b),
b'u' => {
end += 1;
let mut cp = hex_to_digit(src, end, 4);
end += 3;
if cp & 0xfc00 == 0xd800 {
if src.get(end + 1) == Some(&b'\\') && src.get(end + 2) == Some(&b'u') {
end += 1;
let surrogate = hex_to_digit(src, end + 2, 4);
cp = ((cp & 0x3f) << 10)
| (((((cp >> 6) & 0xf) + 1) << 16) | (surrogate & 0x3ff));
end += 5;
} else {
out.push(b'?');
end += 1;
beg = end;
continue;
}
}
utf32_to_utf8(cp, &mut out);
}
b'\n' => {
end += 1;
beg = end;
continue;
}
b'\r' => {
end += 1;
if src.get(end) == Some(&b'\n') {
end += 1;
}
beg = end;
continue;
}
b'0' => out.push(0),
b'x' => {
end += 1;
out.push(hex_to_digit(src, end, 2) as u8);
end += 1;
}
other => out.push(other),
}
end += 1;
beg = end;
}
out.extend_from_slice(&src[beg.min(src.len())..]);
out
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum State {
Start,
ParseComplete,
ArrayStart,
ArrayNeedVal,
ArrayGotVal,
ParseError,
LexicalError,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Status {
Ok,
Error,
ClientCanceled,
}
struct Context {
depth: i32,
target: DbFieldType,
elems: usize,
tokens: Vec<Token>,
refusal: Option<String>,
}
impl Context {
fn refuse(&mut self, message: String) -> bool {
self.refusal = Some(message);
false
}
fn store(&mut self, token: Token) -> bool {
if self.elems > 0 {
self.tokens.push(token);
self.elems -= 1;
}
true
}
fn null(&mut self) -> bool {
self.refuse("dbConvertJSON: Null objects not supported\n".into())
}
fn boolean(&mut self) -> bool {
self.refuse("dbConvertJSON: Boolean not supported\n".into())
}
fn start_map(&mut self) -> bool {
self.refuse("dbConvertJSON: Map type not supported\n".into())
}
fn start_array(&mut self) -> bool {
self.depth += 1;
if self.depth > 1 {
self.refusal = Some("dbConvertJSON: Embedded arrays not supported\n".into());
}
self.depth == 1
}
fn integer(&mut self, value: i64) -> bool {
self.store(Token::Int(value))
}
fn double(&mut self, value: f64) -> bool {
self.store(Token::Double(value))
}
fn string(&mut self, bytes: &[u8]) -> bool {
if self.target != DbFieldType::String {
let shown = String::from_utf8_lossy(bytes);
return self.refuse(format!(
"dbConvertJSON: String \"{shown}\" provided, numeric value expected\n"
));
}
let len = bytes.len().min(DBR_STRING_CAPACITY);
self.store(Token::Text(PvString::from_bytes(&bytes[..len])))
}
}
struct Parser {
lexer: Lexer,
stack: Vec<State>,
bytes_consumed: usize,
parse_error: Option<&'static str>,
ctx: Context,
}
impl Parser {
fn new(ctx: Context) -> Self {
Self {
lexer: Lexer::default(),
stack: vec![State::Start],
bytes_consumed: 0,
parse_error: None,
ctx,
}
}
fn state(&self) -> State {
*self.stack.last().expect("the base state is never popped")
}
fn set(&mut self, state: State) {
*self
.stack
.last_mut()
.expect("the base state is never popped") = state;
}
fn cancel(&mut self) -> Status {
self.set(State::ParseError);
self.parse_error = Some("client cancelled parse via callback return value");
Status::ClientCanceled
}
fn do_parse(&mut self, txt: &[u8]) -> Status {
self.bytes_consumed = 0;
loop {
match self.state() {
State::ParseComplete => {
if self.bytes_consumed == txt.len() {
return Status::Ok;
}
let mut off = self.bytes_consumed;
let (tok, _) = self.lexer.lex(txt, &mut off);
self.bytes_consumed = off;
if tok != Tok::Eof {
self.set(State::ParseError);
self.parse_error = Some("trailing garbage");
}
}
State::LexicalError | State::ParseError => return Status::Error,
State::Start | State::ArrayStart | State::ArrayNeedVal => {
let mut off = self.bytes_consumed;
let (tok, buf) = self.lexer.lex(txt, &mut off);
self.bytes_consumed = off;
let mut push_array = false;
let accepted = match tok {
Tok::Eof => return Status::Ok,
Tok::Error => {
self.set(State::LexicalError);
continue;
}
Tok::Str => self.ctx.string(&buf),
Tok::StrWithEscapes => {
let decoded = string_decode(&buf);
self.ctx.string(&decoded)
}
Tok::Bool => self.ctx.boolean(),
Tok::Null => self.ctx.null(),
Tok::LeftBrace => self.ctx.start_map(),
Tok::LeftBracket => {
push_array = true;
self.ctx.start_array()
}
Tok::Integer => match parse_integer(&buf) {
Ok(value) => self.ctx.integer(value),
Err(()) => {
self.set(State::ParseError);
self.parse_error = Some("integer overflow");
self.restore_error_offset(buf.len());
continue;
}
},
Tok::Double => match parse_double(&buf) {
Ok(value) => self.ctx.double(value),
Err(()) => {
self.set(State::ParseError);
self.parse_error = Some("numeric (floating point) overflow");
self.restore_error_offset(buf.len());
continue;
}
},
Tok::RightBracket
if matches!(self.state(), State::ArrayStart | State::ArrayNeedVal) =>
{
self.stack.pop();
continue;
}
Tok::RightBracket | Tok::Colon | Tok::Comma | Tok::RightBrace => {
self.set(State::ParseError);
self.parse_error = Some("unallowed token at this point in JSON text");
continue;
}
};
if !accepted {
return self.cancel();
}
if self.state() == State::Start {
self.set(State::ParseComplete);
} else {
self.set(State::ArrayGotVal);
}
if push_array {
self.stack.push(State::ArrayStart);
}
}
State::ArrayGotVal => {
let mut off = self.bytes_consumed;
let (tok, _) = self.lexer.lex(txt, &mut off);
self.bytes_consumed = off;
match tok {
Tok::RightBracket => {
self.stack.pop();
}
Tok::Comma => self.set(State::ArrayNeedVal),
Tok::Eof => return Status::Ok,
Tok::Error => self.set(State::LexicalError),
_ => {
self.set(State::ParseError);
self.parse_error = Some("after array element, I expect ',' or ']'");
}
}
}
}
}
}
fn restore_error_offset(&mut self, token_len: usize) {
self.bytes_consumed = self.bytes_consumed.saturating_sub(token_len);
}
fn do_finish(&mut self) -> Status {
let status = self.do_parse(b" ");
if status != Status::Ok {
return status;
}
match self.state() {
State::ParseError | State::LexicalError => Status::Error,
State::ParseComplete => Status::Ok,
_ => {
self.set(State::ParseError);
self.parse_error = Some("premature EOF");
Status::Error
}
}
}
fn render_error(&self, txt: &[u8]) -> String {
let (kind, detail) = match self.state() {
State::ParseError => ("parse", self.parse_error),
State::LexicalError => ("lexical", Some(self.lexer.error.text())),
_ => ("unknown", None),
};
let mut out = format!("{kind} error");
if let Some(detail) = detail {
out.push_str(": ");
out.push_str(detail);
}
out.push('\n');
let offset = self.bytes_consumed;
let spaces = if offset < 30 { 40 - offset } else { 10 };
let start = offset.saturating_sub(30);
let end = (offset + 30).min(txt.len()).max(start);
out.push_str(&" ".repeat(spaces));
let window: Vec<u8> = txt[start..end]
.iter()
.map(|&b| if b == b'\n' || b == b'\r' { b' ' } else { b })
.collect();
out.push_str(&String::from_utf8_lossy(&window));
out.push('\n');
out.push_str(" (right here) ------^\n");
out
}
}
enum Token {
Int(i64),
Double(f64),
Text(PvString),
}
impl Token {
fn as_dbr_string(&self) -> PvString {
match self {
Token::Text(s) => s.clone(),
Token::Int(i) => truncate(&EpicsValue::Int64(*i).convert_to(DbFieldType::String)),
Token::Double(d) => truncate(&EpicsValue::String(
crate::types::codec::cvt_double_to_string(*d, 6).into(),
)),
}
}
fn int(&self) -> Option<i64> {
match self {
Token::Int(i) => Some(*i),
_ => None,
}
}
fn real(&self) -> f64 {
match self {
Token::Int(i) => *i as f64,
Token::Double(d) => *d,
Token::Text(s) => match EpicsValue::String(s.clone()).convert_to(DbFieldType::Double) {
EpicsValue::Double(d) => d,
_ => f64::NAN,
},
}
}
}
fn truncate(v: &EpicsValue) -> PvString {
let text = v.to_string();
let bytes = text.as_bytes();
PvString::from_bytes(&bytes[..bytes.len().min(DBR_STRING_CAPACITY)])
}
pub fn db_put_convert_json(
json: &str,
target: DbFieldType,
capacity: usize,
) -> Result<EpicsValue, ConvertJsonError> {
if json.is_empty() {
return Ok(EpicsValue::DoubleArray(Vec::new()).convert_to(target));
}
let txt = json.as_bytes();
let mut parser = Parser::new(Context {
depth: 0,
target,
elems: capacity,
tokens: Vec::new(),
refusal: None,
});
let mut status = parser.do_parse(txt);
if status == Status::Ok {
status = parser.do_finish();
}
if status != Status::Ok {
return Err(ConvertJsonError {
refusal: parser.ctx.refusal.take(),
diagnostic: format!("dbConvertJSON: {}", parser.render_error(txt)),
});
}
let tokens = parser.ctx.tokens;
if target == DbFieldType::String {
return Ok(EpicsValue::StringArray(
tokens.iter().map(Token::as_dbr_string).collect(),
));
}
Ok(
match tokens.iter().map(Token::int).collect::<Option<Vec<_>>>() {
Some(ints) => EpicsValue::Int64Array(ints),
None => EpicsValue::DoubleArray(tokens.iter().map(Token::real).collect()),
}
.convert_to(target),
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn long_array_conversions_match_softioc() {
let conv = |json| db_put_convert_json(json, DbFieldType::Long, 4);
assert_eq!(conv("7").unwrap(), EpicsValue::LongArray(vec![7]));
assert_eq!(
conv("[1,2,3]").unwrap(),
EpicsValue::LongArray(vec![1, 2, 3])
);
assert_eq!(
conv("[1,2,3,4,5]").unwrap(),
EpicsValue::LongArray(vec![1, 2, 3, 4])
);
assert_eq!(conv("").unwrap(), EpicsValue::LongArray(vec![]));
assert_eq!(
conv("[1.9,2.1]").unwrap(),
EpicsValue::LongArray(vec![1, 2])
);
}
#[test]
fn string_target_takes_text_and_truncates_at_39() {
assert_eq!(
db_put_convert_json("[\"a\",\"bb\"]", DbFieldType::String, 3).unwrap(),
EpicsValue::StringArray(vec!["a".into(), "bb".into()])
);
let long = "x".repeat(50);
let EpicsValue::StringArray(v) =
db_put_convert_json(&format!("[\"{long}\"]"), DbFieldType::String, 1).unwrap()
else {
panic!("a DBF_STRING target yields a StringArray");
};
assert_eq!(v[0].as_str_lossy().len(), DBR_STRING_CAPACITY);
assert_eq!(
db_put_convert_json("[1,\"b\"]", DbFieldType::String, 2).unwrap(),
EpicsValue::StringArray(vec!["1".into(), "b".into()])
);
}
#[test]
fn a_real_token_into_a_string_target_renders_at_precision_six() {
assert_eq!(
db_put_convert_json("[1.0, 2.5, 1.23456789]", DbFieldType::String, 4).unwrap(),
EpicsValue::StringArray(vec![
"1.000000".into(),
"2.500000".into(),
"1.234568".into()
])
);
}
#[test]
fn integers_past_two_to_the_53_stay_exact() {
assert_eq!(
db_put_convert_json("[9007199254740993]", DbFieldType::Int64, 1).unwrap(),
EpicsValue::Int64Array(vec![9007199254740993])
);
}
#[test]
fn the_json5_dialect_every_epics_handle_carries() {
let long = |j| db_put_convert_json(j, DbFieldType::Long, 4).unwrap();
let real = |j| db_put_convert_json(j, DbFieldType::Double, 4).unwrap();
let text = |j| db_put_convert_json(j, DbFieldType::String, 4).unwrap();
assert_eq!(long("[1,2,]"), EpicsValue::LongArray(vec![1, 2]));
assert_eq!(real("[1,]"), EpicsValue::DoubleArray(vec![1.0]));
assert_eq!(long("[0x10]"), EpicsValue::LongArray(vec![16]));
assert_eq!(long("[0X1f]"), EpicsValue::LongArray(vec![31]));
assert_eq!(long("[+5]"), EpicsValue::LongArray(vec![5]));
assert_eq!(real("[.5]"), EpicsValue::DoubleArray(vec![0.5]));
assert_eq!(real("[5.]"), EpicsValue::DoubleArray(vec![5.0]));
assert_eq!(long("[/*c*/1]"), EpicsValue::LongArray(vec![1]));
assert_eq!(long("[1 // tail\n]"), EpicsValue::LongArray(vec![1]));
assert_eq!(
text("['a','b']"),
EpicsValue::StringArray(vec!["a".into(), "b".into()])
);
assert_eq!(text("'abc'"), EpicsValue::StringArray(vec!["abc".into()]));
assert_eq!(
real("[Infinity]"),
EpicsValue::DoubleArray(vec![f64::INFINITY])
);
assert_eq!(
real("[-Infinity]"),
EpicsValue::DoubleArray(vec![f64::NEG_INFINITY])
);
let EpicsValue::DoubleArray(nan) = real("[NaN]") else {
panic!("a DBF_DOUBLE target yields a DoubleArray");
};
assert!(nan[0].is_nan());
}
#[test]
fn a_leading_zero_ends_the_number_token() {
assert_eq!(
db_put_convert_json("[0]", DbFieldType::Long, 4).unwrap(),
EpicsValue::LongArray(vec![0])
);
let e = db_put_convert_json("[01]", DbFieldType::Long, 4).unwrap_err();
assert!(
e.diagnostic
.contains("after array element, I expect ',' or ']'")
);
}
const C_REFUSALS: &[(&str, DbFieldType, Option<&str>, &str)] = &[
(
"[1,2,zz]",
DbFieldType::Long,
None,
"dbConvertJSON: lexical error: invalid char in json text.\n [1,2,zz]\n (right here) ------^\n",
),
(
"[1 2]",
DbFieldType::Long,
None,
"dbConvertJSON: parse error: after array element, I expect ',' or ']'\n [1 2]\n (right here) ------^\n",
),
(
"[1",
DbFieldType::Long,
None,
"dbConvertJSON: parse error: premature EOF\n [1\n (right here) ------^\n",
),
(
"[1] x",
DbFieldType::Long,
None,
"dbConvertJSON: parse error: trailing garbage\n [1] x\n (right here) ------^\n",
),
(
"{}",
DbFieldType::Long,
Some("dbConvertJSON: Map type not supported\n"),
"dbConvertJSON: parse error: client cancelled parse via callback return value\n {}\n (right here) ------^\n",
),
(
"[[1]]",
DbFieldType::Long,
Some("dbConvertJSON: Embedded arrays not supported\n"),
"dbConvertJSON: parse error: client cancelled parse via callback return value\n [[1]]\n (right here) ------^\n",
),
(
"[null]",
DbFieldType::Long,
Some("dbConvertJSON: Null objects not supported\n"),
"dbConvertJSON: parse error: client cancelled parse via callback return value\n [null]\n (right here) ------^\n",
),
(
"[true]",
DbFieldType::Long,
Some("dbConvertJSON: Boolean not supported\n"),
"dbConvertJSON: parse error: client cancelled parse via callback return value\n [true]\n (right here) ------^\n",
),
(
"['a']",
DbFieldType::Long,
Some("dbConvertJSON: String \"a\" provided, numeric value expected\n"),
"dbConvertJSON: parse error: client cancelled parse via callback return value\n ['a']\n (right here) ------^\n",
),
(
"[1e]",
DbFieldType::Long,
None,
"dbConvertJSON: lexical error: malformed number, a digit is required after the exponent.\n [1e]\n (right here) ------^\n",
),
(
"[-]",
DbFieldType::Long,
None,
"dbConvertJSON: lexical error: malformed number, a digit is required after the plus/minus sign.\n [-]\n (right here) ------^\n",
),
(
"[01]",
DbFieldType::Long,
None,
"dbConvertJSON: parse error: after array element, I expect ',' or ']'\n [01]\n (right here) ------^\n",
),
(
"[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,zz]",
DbFieldType::Long,
None,
"dbConvertJSON: lexical error: invalid char in json text.\n 1,12,13,14,15,16,17,18,19,20,zz]\n (right here) ------^\n",
),
(
"['a\\1b']",
DbFieldType::String,
None,
"dbConvertJSON: lexical error: inside a string, '\\' occurs before a character which it may not.\n ['a\\1b']\n (right here) ------^\n",
),
(
"['a\\uZZZZ']",
DbFieldType::String,
None,
"dbConvertJSON: lexical error: invalid (non-hex) character occurs after '\\u' inside string.\n ['a\\uZZZZ']\n (right here) ------^\n",
),
(
"['a\\xZZ']",
DbFieldType::String,
None,
"dbConvertJSON: lexical error: invalid (non-hex) character occurs after '\\x' inside string.\n ['a\\xZZ']\n (right here) ------^\n",
),
(
"['ab",
DbFieldType::String,
None,
"dbConvertJSON: parse error: premature EOF\n ['ab\n (right here) ------^\n",
),
(
"[1e",
DbFieldType::Long,
None,
"dbConvertJSON: lexical error: malformed number, a digit is required after the exponent.\n [1e\n (right here) ------^\n",
),
(
"}",
DbFieldType::Long,
None,
"dbConvertJSON: parse error: unallowed token at this point in JSON text\n }\n (right here) ------^\n",
),
(
"[,1]",
DbFieldType::Long,
None,
"dbConvertJSON: parse error: unallowed token at this point in JSON text\n [,1]\n (right here) ------^\n",
),
(
"[1,,2]",
DbFieldType::Long,
None,
"dbConvertJSON: parse error: unallowed token at this point in JSON text\n [1,,2]\n (right here) ------^\n",
),
(
"[99999999999999999999]",
DbFieldType::Long,
None,
"dbConvertJSON: parse error: integer overflow\n [99999999999999999999]\n (right here) ------^\n",
),
(
"[1e999]",
DbFieldType::Double,
None,
"dbConvertJSON: parse error: numeric (floating point) overflow\n [1e999]\n (right here) ------^\n",
),
(
"[0x]",
DbFieldType::Long,
None,
"dbConvertJSON: lexical error: malformed number, a hex digit is required after the 0x/0X.\n [0x]\n (right here) ------^\n",
),
(
"[/*c",
DbFieldType::Long,
None,
"dbConvertJSON: parse error: premature EOF\n [/*c\n (right here) ------^\n",
),
(
"[//c",
DbFieldType::Long,
None,
"dbConvertJSON: parse error: premature EOF\n [//c\n (right here) ------^\n",
),
(
"{a:1}",
DbFieldType::Long,
Some("dbConvertJSON: Map type not supported\n"),
"dbConvertJSON: parse error: client cancelled parse via callback return value\n {a:1}\n (right here) ------^\n",
),
(
"[tru]",
DbFieldType::Long,
None,
"dbConvertJSON: lexical error: invalid string in json text.\n [tru]\n (right here) ------^\n",
),
(
"[nul]",
DbFieldType::Long,
None,
"dbConvertJSON: lexical error: invalid string in json text.\n [nul]\n (right here) ------^\n",
),
(
"[Inf]",
DbFieldType::Long,
None,
"dbConvertJSON: lexical error: invalid string in json text.\n [Inf]\n (right here) ------^\n",
),
(
"[NaM]",
DbFieldType::Double,
None,
"dbConvertJSON: lexical error: invalid string in json text.\n [NaM]\n (right here) ------^\n",
),
(
"[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,{}]",
DbFieldType::Long,
Some("dbConvertJSON: Map type not supported\n"),
"dbConvertJSON: parse error: client cancelled parse via callback return value\n 1,12,13,14,15,16,17,18,19,20,{}]\n (right here) ------^\n",
),
(
"[",
DbFieldType::Long,
None,
"dbConvertJSON: parse error: premature EOF\n [\n (right here) ------^\n",
),
];
#[test]
fn the_error_block_is_the_one_softioc_prints() {
for (json, target, refusal, diagnostic) in C_REFUSALS {
let e =
db_put_convert_json(json, *target, 4).expect_err(&format!("C refuses {json:?}"));
assert_eq!(
(e.refusal.as_deref(), e.diagnostic.as_str()),
(*refusal, *diagnostic),
"{json:?}"
);
}
}
#[test]
fn a_callback_refusal_carries_both_records() {
let e = db_put_convert_json("[null]", DbFieldType::Long, 4).unwrap_err();
assert_eq!(
e.refusal.as_deref(),
Some("dbConvertJSON: Null objects not supported\n")
);
assert_eq!(
e.to_string(),
format!("{}{}", e.refusal.unwrap(), e.diagnostic)
);
let e = db_put_convert_json("[zz]", DbFieldType::Long, 4).unwrap_err();
assert_eq!(e.refusal, None);
assert_eq!(e.to_string(), e.diagnostic);
}
#[test]
fn overflow_is_decided_by_the_literal_not_by_history() {
let real = |j| db_put_convert_json(j, DbFieldType::Double, 4);
assert!(real("[1e999]").is_err());
assert_eq!(
real("[Infinity]").unwrap(),
EpicsValue::DoubleArray(vec![f64::INFINITY])
);
assert_eq!(
real("[1e-999]").unwrap(),
EpicsValue::DoubleArray(vec![0.0])
);
assert!(db_put_convert_json("[9223372036854775808]", DbFieldType::Int64, 1).is_err());
assert_eq!(
db_put_convert_json("[9223372036854775807]", DbFieldType::Int64, 1).unwrap(),
EpicsValue::Int64Array(vec![i64::MAX])
);
}
#[test]
fn escapes_resolve_the_way_yajl_decodes_them() {
let text = |j: &str| {
let EpicsValue::StringArray(v) =
db_put_convert_json(j, DbFieldType::String, 4).unwrap()
else {
panic!("a DBF_STRING target yields a StringArray");
};
v[0].as_str_lossy().into_owned()
};
assert_eq!(text(r"['a\x41b']"), "aAb");
assert_eq!(text(r"['aBb']"), "aBb");
assert_eq!(text(r"['a\nb']"), "a\nb");
assert_eq!(text(r"['a\tb']"), "a\tb");
assert_eq!(text(r"['a\'b']"), "a'b");
assert_eq!(text(r"['a\/b']"), "a/b");
assert_eq!(text("['a\\\nb']"), "ab");
assert_eq!(text(r"['a\0b']").as_bytes(), b"a\0b");
}
}