krabka_trace_context/
sqlcommenter.rs1use crate::{
12 carrier::parse_traceparent,
13 propagation::{TRACEPARENT, TRACESTATE},
14};
15
16#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct SqlCommenterTrace<'a> {
25 pub traceparent: &'a str,
27 pub tracestate: Option<&'a str>,
29}
30
31#[must_use]
41pub fn extract_sqlcommenter(sql: &str) -> Option<SqlCommenterTrace<'_>> {
42 sql.find(TRACEPARENT)?;
43 scan_comments(sql)
44}
45
46fn scan_comments(sql: &str) -> Option<SqlCommenterTrace<'_>> {
53 let bytes = sql.as_bytes();
54 let mut index = 0;
55
56 while let Some(&byte) = bytes.get(index) {
57 match byte {
58 b'\'' => index = skip_quoted(bytes, index, b'\'')?,
59 b'"' => index = skip_quoted(bytes, index, b'"')?,
60 b'$' => match dollar_quote_delimiter(sql, index) {
61 None => index += 1,
63 Some(delimiter) => {
64 let body = index + delimiter.len();
65 index = body + sql.get(body..)?.find(delimiter)? + delimiter.len();
66 }
67 },
68 b'-' if bytes.get(index + 1) == Some(&b'-') => {
69 let start = index + 2;
70 let end = line_comment_end(bytes, start);
71 if let Some(found) = read_comment(sql.get(start..end)?) {
72 return Some(found);
73 }
74 index = end;
75 }
76 b'/' if bytes.get(index + 1) == Some(&b'*') => {
77 let (end, closed) = block_comment_end(bytes, index);
78 if !closed {
79 return None;
80 }
81 if let Some(found) = read_comment(sql.get(index + 2..end - 2)?) {
82 return Some(found);
83 }
84 index = end;
85 }
86 _ => index += 1,
87 }
88 }
89
90 None
91}
92
93fn read_comment(content: &str) -> Option<SqlCommenterTrace<'_>> {
99 let traceparent = find_field(content, TRACEPARENT)?;
100 parse_traceparent(traceparent).ok()?;
101 Some(SqlCommenterTrace {
102 traceparent,
103 tracestate: find_field(content, TRACESTATE),
104 })
105}
106
107fn find_field<'a>(content: &'a str, key: &str) -> Option<&'a str> {
115 let bytes = content.as_bytes();
116 let mut from = 0;
117
118 while let Some(offset) = content.get(from..).and_then(|rest| rest.find(key)) {
119 let start = from + offset;
120 let end = start + key.len();
121 let preceded_by_identifier = start
122 .checked_sub(1)
123 .and_then(|before| bytes.get(before))
124 .is_some_and(|&byte| is_identifier_byte(byte));
125 if !preceded_by_identifier && let Some(value) = read_value(content, end) {
126 return Some(value);
127 }
128 from = end;
129 }
130
131 None
132}
133
134fn read_value(content: &str, index: usize) -> Option<&str> {
139 let bytes = content.as_bytes();
140 let mut cursor = skip_spaces(bytes, index);
141 if bytes.get(cursor) != Some(&b'=') {
142 return None;
143 }
144 cursor = skip_spaces(bytes, cursor + 1);
145
146 if bytes.get(cursor) == Some(&b'\'') {
147 let start = cursor + 1;
148 let end = start + content.get(start..)?.find('\'')?;
149 return content.get(start..end);
150 }
151
152 let start = cursor;
153 let mut end = cursor;
154 while bytes
155 .get(end)
156 .is_some_and(|&byte| !is_value_terminator(byte))
157 {
158 end += 1;
159 }
160 content.get(start..end).filter(|value| !value.is_empty())
161}
162
163fn skip_spaces(bytes: &[u8], mut index: usize) -> usize {
164 while bytes.get(index).is_some_and(u8::is_ascii_whitespace) {
165 index += 1;
166 }
167 index
168}
169
170fn is_identifier_byte(byte: u8) -> bool {
171 byte.is_ascii_alphanumeric() || byte == b'_'
172}
173
174fn is_value_terminator(byte: u8) -> bool {
175 byte.is_ascii_whitespace() || matches!(byte, b',' | b'*' | b'/' | b'\'' | b'"')
176}
177
178fn skip_quoted(bytes: &[u8], mut index: usize, quote: u8) -> Option<usize> {
184 index += 1;
185 while let Some(&byte) = bytes.get(index) {
186 if byte != quote {
187 index += 1;
188 } else if bytes.get(index + 1) == Some("e) {
189 index += 2;
190 } else {
191 return Some(index + 1);
192 }
193 }
194 None
195}
196
197fn dollar_quote_delimiter(sql: &str, index: usize) -> Option<&str> {
203 let bytes = sql.as_bytes();
204 let mut end = index + 1;
205 if bytes.get(end).is_some_and(u8::is_ascii_digit) {
206 return None;
207 }
208 while bytes.get(end).is_some_and(|&byte| is_identifier_byte(byte)) {
209 end += 1;
210 }
211 if bytes.get(end) == Some(&b'$') {
212 sql.get(index..=end)
213 } else {
214 None
215 }
216}
217
218fn line_comment_end(bytes: &[u8], mut index: usize) -> usize {
219 while bytes.get(index).is_some_and(|&byte| byte != b'\n') {
220 index += 1;
221 }
222 index
223}
224
225fn block_comment_end(bytes: &[u8], index: usize) -> (usize, bool) {
231 let mut depth = 1usize;
232 let mut cursor = index + 2;
233
234 while let Some(&byte) = bytes.get(cursor) {
235 if byte == b'/' && bytes.get(cursor + 1) == Some(&b'*') {
236 depth += 1;
237 cursor += 2;
238 } else if byte == b'*' && bytes.get(cursor + 1) == Some(&b'/') {
239 depth -= 1;
240 cursor += 2;
241 if depth == 0 {
242 return (cursor, true);
243 }
244 } else {
245 cursor += 1;
246 }
247 }
248
249 (cursor, false)
250}