diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-06-29 01:30:42 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-06-29 01:31:37 +0900 |
| commit | 1036b7e33a4360df8b99f81ef03492fee8328bd0 (patch) | |
| tree | d963d6f83674884dabbd20fd60c5c8f75c661347 /crates/shirabe-external-packages/src/seld/json_lint/lexer.rs | |
| parent | 9be0f98f71fe8071ab839ac1036b4064ac3172b4 (diff) | |
| download | php-shirabe-1036b7e33a4360df8b99f81ef03492fee8328bd0.tar.gz php-shirabe-1036b7e33a4360df8b99f81ef03492fee8328bd0.tar.zst php-shirabe-1036b7e33a4360df8b99f81ef03492fee8328bd0.zip | |
refactor(json): replace seld/jsonlint with serde_json
Validate JSON syntax with serde_json's parse errors in JsonFile, and detect
duplicate keys in ConfigValidator with a hand-written serde visitor, dropping
the now-unused JsonParser/Lexer/DuplicateKeyException ports. ParsingException
is kept as the thrown error type and downcast signal.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe-external-packages/src/seld/json_lint/lexer.rs')
| -rw-r--r-- | crates/shirabe-external-packages/src/seld/json_lint/lexer.rs | 545 |
1 files changed, 0 insertions, 545 deletions
diff --git a/crates/shirabe-external-packages/src/seld/json_lint/lexer.rs b/crates/shirabe-external-packages/src/seld/json_lint/lexer.rs deleted file mode 100644 index b959486..0000000 --- a/crates/shirabe-external-packages/src/seld/json_lint/lexer.rs +++ /dev/null @@ -1,545 +0,0 @@ -//! ref: composer/vendor/seld/jsonlint/src/Seld/JsonLint/Lexer.php - -use super::JsonParser; -use super::ParsingException; - -/// ref: array{first_line, first_column, last_line, last_column} ($yylloc) -#[derive(Debug, Clone, Default)] -pub struct YylLoc { - pub first_line: i64, - pub first_column: i64, - pub last_line: i64, - pub last_column: i64, -} - -#[derive(Debug)] -pub struct Lexer { - rules: Vec<Rule>, - input: Vec<u8>, - more: bool, - done: bool, - offset: usize, - flags: u32, - - pub match_: Vec<u8>, - pub yylineno: i64, - pub yyleng: i64, - pub yytext: Vec<u8>, - pub yylloc: YylLoc, -} - -/// Each variant replicates the corresponding `\G`-anchored PHP rule. The PCRE rules are not portable -/// to the `regex` crate (possessive quantifiers, `\G`, `\b`), so they are matched by hand. -#[derive(Debug, Clone, Copy)] -enum Rule { - BreakLine, // 0: /\G\s*\n\r?/ - Whitespace, // 1: /\G\s+/ - Number, // 2 - Str, // 3 - BraceOpen, // 4: { - BraceClose, // 5: } - BracketOpen, // 6: [ - BracketClose, // 7: ] - Comma, // 8: , - Colon, // 9: : - True, // 10 - False, // 11 - Null, // 12 - End, // 13: /\G$/ - LineComment, // 14: // - OpenComment, // 15: /* - CloseComment, // 16: */ - AnyChar, // 17: /\G./ -} - -impl Lexer { - pub const EOF: i64 = 1; - pub const T_INVALID: i64 = -1; - pub const T_SKIP_WHITESPACE: i64 = 0; - pub const T_ERROR: i64 = 2; - pub const T_BREAK_LINE: i64 = 3; - pub const T_COMMENT: i64 = 30; - pub const T_OPEN_COMMENT: i64 = 31; - pub const T_CLOSE_COMMENT: i64 = 32; - - pub fn new(flags: u32) -> Self { - let rules = vec![ - Rule::BreakLine, - Rule::Whitespace, - Rule::Number, - Rule::Str, - Rule::BraceOpen, - Rule::BraceClose, - Rule::BracketOpen, - Rule::BracketClose, - Rule::Comma, - Rule::Colon, - Rule::True, - Rule::False, - Rule::Null, - Rule::End, - Rule::LineComment, - Rule::OpenComment, - Rule::CloseComment, - Rule::AnyChar, - ]; - Self { - rules, - input: Vec::new(), - more: false, - done: false, - offset: 0, - flags, - match_: Vec::new(), - yylineno: 0, - yyleng: 0, - yytext: Vec::new(), - yylloc: YylLoc::default(), - } - } - - pub fn lex(&mut self) -> Result<i64, ParsingException> { - loop { - let symbol = self.next()?; - match symbol { - Self::T_SKIP_WHITESPACE | Self::T_BREAK_LINE => {} - Self::T_COMMENT | Self::T_OPEN_COMMENT => { - if self.flags & JsonParser::ALLOW_COMMENTS == 0 { - return Err(self.parse_error(format!( - "Lexical error on line {}. Comments are not allowed.\n{}", - self.yylineno + 1, - self.show_position() - ))); - } - let until = if symbol == Self::T_COMMENT { - Self::T_BREAK_LINE - } else { - Self::T_CLOSE_COMMENT - }; - self.skip_until(until)?; - if self.done { - // last symbol '/\G$/' before EOF - return Ok(14); - } - } - Self::T_CLOSE_COMMENT => { - return Err(self.parse_error(format!( - "Lexical error on line {}. Unexpected token.\n{}", - self.yylineno + 1, - self.show_position() - ))); - } - _ => return Ok(symbol), - } - } - } - - pub fn set_input(&mut self, input: &str) { - self.input = input.as_bytes().to_vec(); - self.more = false; - self.done = false; - self.offset = 0; - self.yylineno = 0; - self.yyleng = 0; - self.yytext = Vec::new(); - self.match_ = Vec::new(); - self.yylloc = YylLoc { - first_line: 1, - first_column: 0, - last_line: 1, - last_column: 0, - }; - } - - pub fn show_position(&self) -> String { - if self.yylineno == 0 && self.offset == 1 && self.match_ != b"{" { - let mut s = String::from_utf8_lossy(&self.match_).into_owned(); - s.push_str("...\n^"); - return s; - } - - let pre = str_replace_nl(&self.get_past_input()); - let dash_count = (pre.len() as i64 - 1).max(0) as usize; - let c = "-".repeat(dash_count); - - let upcoming = str_replace_nl(&self.get_upcoming_input()); - format!("{pre}{upcoming}\n{c}^") - } - - pub fn get_past_input(&self) -> Vec<u8> { - let past_length = self.offset as i64 - self.match_.len() as i64; - let prefix = if past_length > 20 { "..." } else { "" }; - let start = (past_length - 20).max(0) as usize; - let len = past_length.clamp(0, 20) as usize; - let slice = substr_bytes(&self.input, start, len); - let mut out = prefix.as_bytes().to_vec(); - out.extend_from_slice(&slice); - out - } - - pub fn get_upcoming_input(&self) -> Vec<u8> { - let mut next = self.match_.clone(); - if next.len() < 20 { - let want = 20 - next.len(); - next.extend_from_slice(&substr_bytes(&self.input, self.offset, want)); - } - let too_long = next.len() > 20; - let mut out = substr_bytes(&next, 0, 20); - if too_long { - out.extend_from_slice(b"..."); - } - out - } - - pub fn get_full_upcoming_input(&self) -> Vec<u8> { - let mut next = self.match_.clone(); - if next.first() == Some(&b'"') && byte_count(&next, b'"') == 1 { - let len = self.input.len(); - let str_end = if len == self.offset { - len - } else { - let q = find_byte_from(&self.input, b'"', self.offset + 1).unwrap_or(len); - let q = if q == 0 { len } else { q }; - let n = find_byte_from(&self.input, b'\n', self.offset + 1).unwrap_or(len); - let n = if n == 0 { len } else { n }; - q.min(n) - }; - next.extend_from_slice(&self.input[self.offset..str_end]); - } else if next.len() < 20 { - let want = 20 - next.len(); - next.extend_from_slice(&substr_bytes(&self.input, self.offset, want)); - } - next - } - - fn parse_error(&self, str: String) -> ParsingException { - ParsingException::new(str, Default::default()) - } - - fn skip_until(&mut self, token: i64) -> Result<(), ParsingException> { - let mut symbol = self.next()?; - while symbol != token && !self.done { - symbol = self.next()?; - } - Ok(()) - } - - fn next(&mut self) -> Result<i64, ParsingException> { - if self.done { - return Ok(Self::EOF); - } - if self.offset == self.input.len() { - self.done = true; - } - - if !self.more { - self.yytext = Vec::new(); - self.match_ = Vec::new(); - } - - for i in 0..self.rules.len() { - if let Some(matched) = self.match_rule(self.rules[i]) { - let lines: Vec<&[u8]> = split_bytes(&matched, b'\n'); - // array_shift: drop first element, count remaining - let line_count = lines.len().saturating_sub(1); - self.yylineno += line_count as i64; - self.yylloc = YylLoc { - first_line: self.yylloc.last_line, - last_line: self.yylineno + 1, - first_column: self.yylloc.last_column, - last_column: if line_count > 0 { - lines[lines.len() - 1].len() as i64 - } else { - self.yylloc.last_column + matched.len() as i64 - }, - }; - self.yytext.extend_from_slice(&matched); - self.match_.extend_from_slice(&matched); - self.yyleng = self.yytext.len() as i64; - self.more = false; - self.offset += matched.len(); - return Ok(self.perform_action(i, &matched)); - } - } - - if self.offset == self.input.len() { - return Ok(Self::EOF); - } - - Err(self.parse_error(format!( - "Lexical error on line {}. Unrecognized text.\n{}", - self.yylineno + 1, - self.show_position() - ))) - } - - /// Returns the matched bytes (anchored at `self.offset`) if the rule applies, mirroring - /// `preg_match($rule, $input, $m, 0, $offset)` for the corresponding `\G`-anchored PHP pattern. - fn match_rule(&self, rule: Rule) -> Option<Vec<u8>> { - let input = &self.input; - let off = self.offset; - match rule { - Rule::BreakLine => match_break_line(input, off), - Rule::Whitespace => match_whitespace(input, off), - Rule::Number => match_number(input, off), - Rule::Str => match_string(input, off), - Rule::BraceOpen => match_char(input, off, b'{'), - Rule::BraceClose => match_char(input, off, b'}'), - Rule::BracketOpen => match_char(input, off, b'['), - Rule::BracketClose => match_char(input, off, b']'), - Rule::Comma => match_char(input, off, b','), - Rule::Colon => match_char(input, off, b':'), - Rule::True => match_keyword(input, off, b"true"), - Rule::False => match_keyword(input, off, b"false"), - Rule::Null => match_keyword(input, off, b"null"), - Rule::End => match_end(input, off), - Rule::LineComment => match_literal(input, off, b"//"), - Rule::OpenComment => match_literal(input, off, b"/*"), - Rule::CloseComment => match_literal(input, off, b"*/"), - Rule::AnyChar => match_any_char(input, off), - } - } - - fn perform_action(&mut self, rule: usize, _matched: &[u8]) -> i64 { - match rule { - 0 => Self::T_BREAK_LINE, - 1 => Self::T_SKIP_WHITESPACE, - 2 => 6, - 3 => { - // strip surrounding quotes: substr($yytext, 1, $yyleng-2) - let len = self.yyleng; - self.yytext = substr_bytes(&self.yytext, 1, (len - 2).max(0) as usize); - 4 - } - 4 => 17, - 5 => 18, - 6 => 23, - 7 => 24, - 8 => 22, - 9 => 21, - 10 => 10, - 11 => 11, - 12 => 8, - 13 => 14, - 14 => Self::T_COMMENT, - 15 => Self::T_OPEN_COMMENT, - 16 => Self::T_CLOSE_COMMENT, - 17 => Self::T_INVALID, - _ => panic!("Unsupported rule {rule}"), - } - } -} - -/// PHP `\s` (matches space, \t, \n, \r, \f, \v). -fn is_php_space(b: u8) -> bool { - matches!(b, b' ' | b'\t' | b'\n' | b'\r' | 0x0c | 0x0b) -} - -/// Rule 0: /\G\s*\n\r?/ — greedy whitespace ending at a newline, optionally followed by `\r`. -fn match_break_line(input: &[u8], off: usize) -> Option<Vec<u8>> { - // Find the run of whitespace that ends with `\n` (followed by optional `\r`). PCRE backtracks - // so the `\s*` consumes up to and including the last reachable `\n` within the leading run. - let mut i = off; - let mut last_nl: Option<usize> = None; - while i < input.len() && is_php_space(input[i]) { - if input[i] == b'\n' { - last_nl = Some(i); - } - i += 1; - } - let nl = last_nl?; - let mut end = nl + 1; - if end < input.len() && input[end] == b'\r' { - end += 1; - } - Some(input[off..end].to_vec()) -} - -/// Rule 1: /\G\s+/ -fn match_whitespace(input: &[u8], off: usize) -> Option<Vec<u8>> { - let mut i = off; - while i < input.len() && is_php_space(input[i]) { - i += 1; - } - if i == off { - None - } else { - Some(input[off..i].to_vec()) - } -} - -/// Rule 2: /\G-?([0-9]|[1-9][0-9]+)(\.[0-9]+)?([eE][+-]?[0-9]+)?\b/ -fn match_number(input: &[u8], off: usize) -> Option<Vec<u8>> { - let mut i = off; - if i < input.len() && input[i] == b'-' { - i += 1; - } - // integer part: a single digit OR [1-9][0-9]+ - let int_start = i; - if i >= input.len() || !input[i].is_ascii_digit() { - return None; - } - if input[i] == b'0' { - // single 0 only (the [1-9][0-9]+ alternative cannot start with 0) - i += 1; - } else { - // [1-9] then [0-9]* ; but grammar is `[0-9]` (single) | `[1-9][0-9]+` (2+ digits). - // Both collapse to: one or more digits starting with 1-9. - i += 1; - while i < input.len() && input[i].is_ascii_digit() { - i += 1; - } - } - let _ = int_start; - // fraction - if i < input.len() && input[i] == b'.' { - let mut j = i + 1; - let frac_start = j; - while j < input.len() && input[j].is_ascii_digit() { - j += 1; - } - if j > frac_start { - i = j; - } - } - // exponent - if i < input.len() && (input[i] == b'e' || input[i] == b'E') { - let mut j = i + 1; - if j < input.len() && (input[j] == b'+' || input[j] == b'-') { - j += 1; - } - let exp_start = j; - while j < input.len() && input[j].is_ascii_digit() { - j += 1; - } - if j > exp_start { - i = j; - } - } - // \b word boundary: previous char (input[i-1]) is a word char (digit), so the next char must be - // a non-word char (or end). - if i < input.len() && is_word_byte(input[i]) { - return None; - } - Some(input[off..i].to_vec()) -} - -fn is_word_byte(b: u8) -> bool { - b.is_ascii_alphanumeric() || b == b'_' -} - -/// Rule 3: {\G"(?>\\["bfnrt/\\]|\\u[a-fA-F0-9]{4}|[^\0-\x1f\\"]++)*+"} -fn match_string(input: &[u8], off: usize) -> Option<Vec<u8>> { - if input.get(off) != Some(&b'"') { - return None; - } - let mut i = off + 1; - loop { - let c = *input.get(i)?; - if c == b'"' { - return Some(input[off..=i].to_vec()); - } - if c == b'\\' { - let n = *input.get(i + 1)?; - match n { - b'"' | b'b' | b'f' | b'n' | b'r' | b't' | b'/' | b'\\' => { - i += 2; - } - b'u' => { - // require 4 hex digits - for k in 0..4 { - let h = *input.get(i + 2 + k)?; - if !h.is_ascii_hexdigit() { - return None; - } - } - i += 6; - } - _ => return None, - } - } else if c <= 0x1f { - // [^\0-\x1f\\"] excludes control chars - return None; - } else { - i += 1; - } - } -} - -/// Rules 4-9: single literal char. -fn match_char(input: &[u8], off: usize, ch: u8) -> Option<Vec<u8>> { - if input.get(off) == Some(&ch) { - Some(vec![ch]) - } else { - None - } -} - -/// Rules 10-12: keyword followed by a `\b` boundary. -fn match_keyword(input: &[u8], off: usize, kw: &[u8]) -> Option<Vec<u8>> { - if input.len() < off + kw.len() || &input[off..off + kw.len()] != kw { - return None; - } - let after = off + kw.len(); - if after < input.len() && is_word_byte(input[after]) { - return None; - } - Some(kw.to_vec()) -} - -/// Rule 13: /\G$/ — matches the empty string at end of input (or before a trailing final newline, -/// per PCRE `$` without the `m` modifier). -fn match_end(input: &[u8], off: usize) -> Option<Vec<u8>> { - if off == input.len() { - return Some(Vec::new()); - } - if off == input.len() - 1 && input[off] == b'\n' { - return Some(Vec::new()); - } - None -} - -/// Rules 14-16: multi-char literal. -fn match_literal(input: &[u8], off: usize, lit: &[u8]) -> Option<Vec<u8>> { - if input.len() >= off + lit.len() && &input[off..off + lit.len()] == lit { - Some(lit.to_vec()) - } else { - None - } -} - -/// Rule 17: /\G./ — any single character except newline. -fn match_any_char(input: &[u8], off: usize) -> Option<Vec<u8>> { - match input.get(off) { - Some(&b'\n') | None => None, - Some(&c) => Some(vec![c]), - } -} - -fn substr_bytes(input: &[u8], start: usize, len: usize) -> Vec<u8> { - if start >= input.len() { - return Vec::new(); - } - let end = (start + len).min(input.len()); - input[start..end].to_vec() -} - -fn str_replace_nl(input: &[u8]) -> String { - let filtered: Vec<u8> = input.iter().copied().filter(|&b| b != b'\n').collect(); - String::from_utf8_lossy(&filtered).into_owned() -} - -fn split_bytes(input: &[u8], sep: u8) -> Vec<&[u8]> { - input.split(|&b| b == sep).collect() -} - -fn byte_count(input: &[u8], b: u8) -> usize { - input.iter().filter(|&&x| x == b).count() -} - -fn find_byte_from(input: &[u8], b: u8, from: usize) -> Option<usize> { - if from > input.len() { - return None; - } - input[from..].iter().position(|&x| x == b).map(|p| p + from) -} |
