Skip to main content

ocular_protocol/
mongodb.rs

1//! MongoDB wire protocol parser (OP_MSG only, modern MongoDB 3.6+)
2//! All integers are little-endian.
3
4const OP_MSG: i32 = 2013;
5const OP_COMPRESSED: i32 = 2012;
6
7/// Get the total message length from a MongoDB wire protocol header.
8/// Returns None if buffer is too small or length is invalid.
9pub fn mongo_msg_len(buf: &[u8]) -> Option<usize> {
10    if buf.len() < 4 { return None; }
11    let len = i32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]) as usize;
12    if !(16..=48 * 1024 * 1024).contains(&len) { return None; }
13    Some(len)
14}
15
16/// Parse a MongoDB request (client→server), returning a command summary.
17pub fn parse_mongo_request(buf: &[u8]) -> Option<String> {
18    let doc = extract_body_doc(buf)?;
19    let cmd = first_key(&doc)?;
20    let db = get_string_field(&doc, "$db").unwrap_or_default();
21    let detail = match cmd.as_str() {
22        "find" => {
23            let coll = get_string_field(&doc, "find").unwrap_or_default();
24            let filter = get_doc_field_summary(&doc, "filter");
25            format!("find {}.{} {}", db, coll, filter)
26        }
27        "insert" => {
28            let coll = get_string_field(&doc, "insert").unwrap_or_default();
29            let n = get_array_len(&doc, "documents");
30            format!("insert {}.{} ({} docs)", db, coll, n)
31        }
32        "update" => {
33            let coll = get_string_field(&doc, "update").unwrap_or_default();
34            let n = get_array_len(&doc, "updates");
35            format!("update {}.{} ({} ops)", db, coll, n)
36        }
37        "delete" => {
38            let coll = get_string_field(&doc, "delete").unwrap_or_default();
39            let n = get_array_len(&doc, "deletes");
40            format!("delete {}.{} ({} ops)", db, coll, n)
41        }
42        "aggregate" => {
43            let coll = get_string_field(&doc, "aggregate").unwrap_or_default();
44            format!("aggregate {}.{}", db, coll)
45        }
46        "getMore" => {
47            let coll = get_string_field(&doc, "collection").unwrap_or_default();
48            format!("getMore {}.{}", db, coll)
49        }
50        _ => {
51            if db.is_empty() { cmd.clone() } else { format!("{} {}", cmd, db) }
52        }
53    };
54    Some(detail)
55}
56
57/// Extract full command detail (for Detail panel) — mongosh-style replayable statements.
58pub fn extract_mongo_full_command(buf: &[u8]) -> Option<String> {
59    let doc = extract_body_doc(buf)?;
60    let cmd = first_key(&doc)?;
61    let db = get_string_field(&doc, "$db").unwrap_or_default();
62    match cmd.as_str() {
63        "find" => {
64            let coll = get_string_field(&doc, "find").unwrap_or_default();
65            let filter = get_doc_field_summary(&doc, "filter");
66            let limit = get_i32_field(&doc, "limit");
67            let sort = get_raw_doc_field(&doc, "sort").map(|d| bson_doc_to_json_like(&d));
68            let mut s = format!("db.{}.find({})", coll, filter);
69            if let Some(sort_str) = sort { s.push_str(&format!(".sort({})", sort_str)); }
70            if let Some(l) = limit { s.push_str(&format!(".limit({})", l)); }
71            Some(s)
72        }
73        "insert" => {
74            let coll = get_string_field(&doc, "insert").unwrap_or_default();
75            let docs = get_array_docs(&doc, "documents");
76            if docs.len() == 1 {
77                Some(format!("db.{}.insertOne({})", coll, bson_doc_to_json_like(&docs[0])))
78            } else {
79                let items: Vec<String> = docs.iter().take(10).map(|d| bson_doc_to_json_like(d)).collect();
80                let mut s = format!("db.{}.insertMany([{}])", coll, items.join(", "));
81                if docs.len() > 10 { s.push_str(&format!(" // +{} more", docs.len() - 10)); }
82                Some(s)
83            }
84        }
85        "update" => {
86            let coll = get_string_field(&doc, "update").unwrap_or_default();
87            let updates = get_array_docs(&doc, "updates");
88            if updates.len() == 1 {
89                let q = get_doc_field_summary(&updates[0], "q");
90                let u = get_doc_field_summary(&updates[0], "u");
91                let multi = get_i32_field(&updates[0], "multi").unwrap_or(0) != 0
92                    || has_field(&updates[0], "multi") && get_f64_field(&updates[0], "multi") == Some(1.0);
93                let method = if multi { "updateMany" } else { "updateOne" };
94                Some(format!("db.{}.{}({}, {})", coll, method, q, u))
95            } else {
96                Some(format!("db.{}.bulkWrite([...{} ops])", coll, updates.len()))
97            }
98        }
99        "delete" => {
100            let coll = get_string_field(&doc, "delete").unwrap_or_default();
101            let deletes = get_array_docs(&doc, "deletes");
102            if deletes.len() == 1 {
103                let q = get_doc_field_summary(&deletes[0], "q");
104                let limit = get_i32_field(&deletes[0], "limit").unwrap_or(0);
105                let method = if limit == 1 { "deleteOne" } else { "deleteMany" };
106                Some(format!("db.{}.{}({})", coll, method, q))
107            } else {
108                Some(format!("db.{}.bulkWrite([...{} ops])", coll, deletes.len()))
109            }
110        }
111        "aggregate" => {
112            let coll = get_string_field(&doc, "aggregate").unwrap_or_default();
113            Some(format!("db.{}.aggregate([...])", coll))
114        }
115        "findAndModify" => {
116            let coll = get_string_field(&doc, "findAndModify").unwrap_or_default();
117            let query = get_doc_field_summary(&doc, "query");
118            let update = get_doc_field_summary(&doc, "update");
119            Some(format!("db.{}.findOneAndUpdate({}, {})", coll, query, update))
120        }
121        "count" | "countDocuments" => {
122            let coll = get_string_field(&doc, &cmd).unwrap_or_default();
123            let query = get_doc_field_summary(&doc, "query");
124            Some(format!("db.{}.countDocuments({})", coll, query))
125        }
126        _ => {
127            if db.is_empty() { Some(cmd) } else { Some(format!("{} {}", cmd, db)) }
128        }
129    }
130}
131
132/// Parse a MongoDB response (server→client), returning a summary.
133pub fn parse_mongo_response(buf: &[u8]) -> Option<String> {
134    let doc = extract_body_doc(buf)?;
135    let ok = get_f64_field(&doc, "ok");
136    if ok == Some(0.0) {
137        let errmsg = get_string_field(&doc, "errmsg").unwrap_or("error".into());
138        let code = get_i32_field(&doc, "code").map(|c| format!(" ({})", c)).unwrap_or_default();
139        return Some(format!("ERR{} {}", code, errmsg));
140    }
141    // Check for cursor result
142    if let Some(cursor_doc) = get_raw_doc_field(&doc, "cursor") {
143        let batch_key = if has_field(&cursor_doc, "firstBatch") { "firstBatch" } else { "nextBatch" };
144        let n = get_array_len(&cursor_doc, batch_key);
145        return Some(format!("OK ({} docs)", n));
146    }
147    // Check for n (insert/update/delete result)
148    if let Some(n) = get_i32_field(&doc, "n") {
149        let modified = get_i32_field(&doc, "nModified");
150        if let Some(m) = modified {
151            return Some(format!("OK (n={}, modified={})", n, m));
152        }
153        return Some(format!("OK (n={})", n));
154    }
155    Some("OK".into())
156}
157
158/// Format detailed response for the detail panel.
159pub fn format_mongo_response_detail(buf: &[u8]) -> Option<String> {
160    let doc = extract_body_doc(buf)?;
161    let ok = get_f64_field(&doc, "ok");
162    if ok == Some(0.0) {
163        let errmsg = get_string_field(&doc, "errmsg").unwrap_or("error".into());
164        let code = get_i32_field(&doc, "code").unwrap_or(0);
165        let codename = get_string_field(&doc, "codeName").unwrap_or_default();
166        return Some(format!("ERROR {} ({}): {}", code, codename, errmsg));
167    }
168    if let Some(cursor_doc) = get_raw_doc_field(&doc, "cursor") {
169        let batch_key = if has_field(&cursor_doc, "firstBatch") { "firstBatch" } else { "nextBatch" };
170        let docs = get_array_docs(&cursor_doc, batch_key);
171        let mut lines = Vec::new();
172        lines.push(format!("{} documents:", docs.len()));
173        for (i, d) in docs.iter().enumerate().take(20) {
174            lines.push(format!("  [{}] {}", i, bson_doc_to_json_like(d)));
175        }
176        if docs.len() > 20 {
177            lines.push(format!("  ... ({} more)", docs.len() - 20));
178        }
179        return Some(lines.join("\n"));
180    }
181    parse_mongo_response(buf)
182}
183
184// --- Internal helpers ---
185
186/// Extract the Kind 0 body BSON document from an OP_MSG.
187fn extract_body_doc(buf: &[u8]) -> Option<Vec<u8>> {
188    if buf.len() < 21 { return None; } // header(16) + flags(4) + kind(1)
189    let opcode = i32::from_le_bytes([buf[12], buf[13], buf[14], buf[15]]);
190    if opcode != OP_MSG && opcode != OP_COMPRESSED { return None; }
191    if opcode == OP_COMPRESSED { return None; } // skip compressed for now
192    // flags at offset 16, sections start at offset 20
193    let mut pos = 20;
194    while pos < buf.len() {
195        let kind = buf[pos];
196        pos += 1;
197        if kind == 0 {
198            // Kind 0: single BSON document
199            if pos + 4 > buf.len() { return None; }
200            let doc_len = i32::from_le_bytes([buf[pos], buf[pos+1], buf[pos+2], buf[pos+3]]) as usize;
201            if pos + doc_len > buf.len() { return None; }
202            return Some(buf[pos..pos+doc_len].to_vec());
203        } else if kind == 1 {
204            // Kind 1: document sequence, skip
205            if pos + 4 > buf.len() { return None; }
206            let sec_len = i32::from_le_bytes([buf[pos], buf[pos+1], buf[pos+2], buf[pos+3]]) as usize;
207            pos += sec_len;
208        } else {
209            break;
210        }
211    }
212    None
213}
214
215/// Get the first key name from a BSON document (the command name).
216fn first_key(doc: &[u8]) -> Option<String> {
217    if doc.len() < 6 { return None; }
218    // doc[0..4] = size, doc[4] = element type, doc[5..] = cstring key
219    let key = read_cstr(&doc[5..])?;
220    Some(key)
221}
222
223/// Read a null-terminated C string.
224fn read_cstr(buf: &[u8]) -> Option<String> {
225    let end = buf.iter().position(|&b| b == 0)?;
226    Some(String::from_utf8_lossy(&buf[..end]).to_string())
227}
228
229/// Get a string field value from a BSON document.
230fn get_string_field(doc: &[u8], name: &str) -> Option<String> {
231    let mut pos = 4; // skip doc size
232    while pos < doc.len() - 1 {
233        let etype = doc[pos];
234        if etype == 0 { break; } // end of doc
235        pos += 1;
236        let key = read_cstr(&doc[pos..])?;
237        pos += key.len() + 1;
238        match etype {
239            0x02 => { // string
240                if pos + 4 > doc.len() { return None; }
241                let slen = i32::from_le_bytes([doc[pos], doc[pos+1], doc[pos+2], doc[pos+3]]) as usize;
242                pos += 4;
243                if key == name {
244                    let s = String::from_utf8_lossy(&doc[pos..pos+slen.saturating_sub(1)]).to_string();
245                    return Some(s);
246                }
247                pos += slen;
248            }
249            0x01 => { pos += 8; } // double
250            0x03 | 0x04 => { // document or array
251                if pos + 4 > doc.len() { return None; }
252                let dlen = i32::from_le_bytes([doc[pos], doc[pos+1], doc[pos+2], doc[pos+3]]) as usize;
253                pos += dlen;
254            }
255            0x05 => { // binary
256                if pos + 4 > doc.len() { return None; }
257                let blen = i32::from_le_bytes([doc[pos], doc[pos+1], doc[pos+2], doc[pos+3]]) as usize;
258                pos += 5 + blen;
259            }
260            0x07 => { pos += 12; } // ObjectId
261            0x08 => { pos += 1; } // boolean
262            0x09 | 0x11 | 0x12 => { pos += 8; } // datetime, timestamp, int64
263            0x0A => {} // null
264            0x10 => { pos += 4; } // int32
265            0x13 => { pos += 16; } // decimal128
266            _ => { return None; } // unknown type, bail
267        }
268    }
269    None
270}
271
272fn get_f64_field(doc: &[u8], name: &str) -> Option<f64> {
273    let mut pos = 4;
274    while pos < doc.len() - 1 {
275        let etype = doc[pos];
276        if etype == 0 { break; }
277        pos += 1;
278        let key = read_cstr(&doc[pos..])?;
279        pos += key.len() + 1;
280        match etype {
281            0x01 => {
282                if key == name && pos + 8 <= doc.len() {
283                    return Some(f64::from_le_bytes([doc[pos], doc[pos+1], doc[pos+2], doc[pos+3], doc[pos+4], doc[pos+5], doc[pos+6], doc[pos+7]]));
284                }
285                pos += 8;
286            }
287            0x10 => {
288                if key == name && pos + 4 <= doc.len() {
289                    let v = i32::from_le_bytes([doc[pos], doc[pos+1], doc[pos+2], doc[pos+3]]);
290                    return Some(v as f64);
291                }
292                pos += 4;
293            }
294            0x02 => { if pos + 4 > doc.len() { return None; } let slen = i32::from_le_bytes([doc[pos], doc[pos+1], doc[pos+2], doc[pos+3]]) as usize; pos += 4 + slen; }
295            0x03 | 0x04 => { if pos + 4 > doc.len() { return None; } let dlen = i32::from_le_bytes([doc[pos], doc[pos+1], doc[pos+2], doc[pos+3]]) as usize; pos += dlen; }
296            0x05 => { if pos + 4 > doc.len() { return None; } let blen = i32::from_le_bytes([doc[pos], doc[pos+1], doc[pos+2], doc[pos+3]]) as usize; pos += 5 + blen; }
297            0x07 => { pos += 12; }
298            0x08 => { pos += 1; }
299            0x09 | 0x11 | 0x12 => { pos += 8; }
300            0x0A => {}
301            0x13 => { pos += 16; }
302            _ => { return None; }
303        }
304    }
305    None
306}
307
308fn get_i32_field(doc: &[u8], name: &str) -> Option<i32> {
309    let mut pos = 4;
310    while pos < doc.len() - 1 {
311        let etype = doc[pos];
312        if etype == 0 { break; }
313        pos += 1;
314        let key = read_cstr(&doc[pos..])?;
315        pos += key.len() + 1;
316        match etype {
317            0x10 => {
318                if key == name && pos + 4 <= doc.len() {
319                    return Some(i32::from_le_bytes([doc[pos], doc[pos+1], doc[pos+2], doc[pos+3]]));
320                }
321                pos += 4;
322            }
323            0x01 => { pos += 8; }
324            0x02 => { if pos + 4 > doc.len() { return None; } let slen = i32::from_le_bytes([doc[pos], doc[pos+1], doc[pos+2], doc[pos+3]]) as usize; pos += 4 + slen; }
325            0x03 | 0x04 => { if pos + 4 > doc.len() { return None; } let dlen = i32::from_le_bytes([doc[pos], doc[pos+1], doc[pos+2], doc[pos+3]]) as usize; pos += dlen; }
326            0x05 => { if pos + 4 > doc.len() { return None; } let blen = i32::from_le_bytes([doc[pos], doc[pos+1], doc[pos+2], doc[pos+3]]) as usize; pos += 5 + blen; }
327            0x07 => { pos += 12; }
328            0x08 => { pos += 1; }
329            0x09 | 0x11 | 0x12 => { pos += 8; }
330            0x0A => {}
331            0x13 => { pos += 16; }
332            _ => { return None; }
333        }
334    }
335    None
336}
337
338fn get_raw_doc_field(doc: &[u8], name: &str) -> Option<Vec<u8>> {
339    let mut pos = 4;
340    while pos < doc.len() - 1 {
341        let etype = doc[pos];
342        if etype == 0 { break; }
343        pos += 1;
344        let key = read_cstr(&doc[pos..])?;
345        pos += key.len() + 1;
346        match etype {
347            0x03 | 0x04 => {
348                if pos + 4 > doc.len() { return None; }
349                let dlen = i32::from_le_bytes([doc[pos], doc[pos+1], doc[pos+2], doc[pos+3]]) as usize;
350                if key == name {
351                    return Some(doc[pos..pos+dlen].to_vec());
352                }
353                pos += dlen;
354            }
355            0x01 => { pos += 8; }
356            0x02 => { if pos + 4 > doc.len() { return None; } let slen = i32::from_le_bytes([doc[pos], doc[pos+1], doc[pos+2], doc[pos+3]]) as usize; pos += 4 + slen; }
357            0x05 => { if pos + 4 > doc.len() { return None; } let blen = i32::from_le_bytes([doc[pos], doc[pos+1], doc[pos+2], doc[pos+3]]) as usize; pos += 5 + blen; }
358            0x07 => { pos += 12; }
359            0x08 => { pos += 1; }
360            0x09 | 0x10 | 0x11 | 0x12 => { pos += if etype == 0x10 { 4 } else { 8 }; }
361            0x0A => {}
362            0x13 => { pos += 16; }
363            _ => { return None; }
364        }
365    }
366    None
367}
368
369fn has_field(doc: &[u8], name: &str) -> bool {
370    let mut pos = 4;
371    while pos < doc.len().saturating_sub(1) {
372        let etype = doc[pos];
373        if etype == 0 { break; }
374        pos += 1;
375        let Some(key) = read_cstr(&doc[pos..]) else { break };
376        if key == name { return true; }
377        pos += key.len() + 1;
378        match etype {
379            0x01 => { pos += 8; }
380            0x02 => { if pos + 4 > doc.len() { break; } let slen = i32::from_le_bytes([doc[pos], doc[pos+1], doc[pos+2], doc[pos+3]]) as usize; pos += 4 + slen; }
381            0x03 | 0x04 => { if pos + 4 > doc.len() { break; } let dlen = i32::from_le_bytes([doc[pos], doc[pos+1], doc[pos+2], doc[pos+3]]) as usize; pos += dlen; }
382            0x05 => { if pos + 4 > doc.len() { break; } let blen = i32::from_le_bytes([doc[pos], doc[pos+1], doc[pos+2], doc[pos+3]]) as usize; pos += 5 + blen; }
383            0x07 => { pos += 12; }
384            0x08 => { pos += 1; }
385            0x09 | 0x11 | 0x12 => { pos += 8; }
386            0x0A => {}
387            0x10 => { pos += 4; }
388            0x13 => { pos += 16; }
389            _ => { break; }
390        }
391    }
392    false
393}
394
395fn get_array_len(doc: &[u8], name: &str) -> usize {
396    let Some(arr) = get_raw_doc_field(doc, name) else { return 0 };
397    // BSON array is a document with "0", "1", ... keys
398    let mut count = 0;
399    let mut pos = 4;
400    while pos < arr.len().saturating_sub(1) {
401        if arr[pos] == 0 { break; }
402        count += 1;
403        pos += 1;
404        let Some(key) = read_cstr(&arr[pos..]) else { break };
405        pos += key.len() + 1;
406        // skip value based on type
407        let etype = arr[pos - key.len() - 2];
408        match etype {
409            0x01 => { pos += 8; }
410            0x02 => { if pos + 4 > arr.len() { break; } let slen = i32::from_le_bytes([arr[pos], arr[pos+1], arr[pos+2], arr[pos+3]]) as usize; pos += 4 + slen; }
411            0x03 | 0x04 => { if pos + 4 > arr.len() { break; } let dlen = i32::from_le_bytes([arr[pos], arr[pos+1], arr[pos+2], arr[pos+3]]) as usize; pos += dlen; }
412            0x05 => { if pos + 4 > arr.len() { break; } let blen = i32::from_le_bytes([arr[pos], arr[pos+1], arr[pos+2], arr[pos+3]]) as usize; pos += 5 + blen; }
413            0x07 => { pos += 12; }
414            0x08 => { pos += 1; }
415            0x09 | 0x11 | 0x12 => { pos += 8; }
416            0x0A => {}
417            0x10 => { pos += 4; }
418            0x13 => { pos += 16; }
419            _ => { break; }
420        }
421    }
422    count
423}
424
425fn get_array_docs(doc: &[u8], name: &str) -> Vec<Vec<u8>> {
426    let Some(arr) = get_raw_doc_field(doc, name) else { return vec![] };
427    let mut docs = Vec::new();
428    let mut pos = 4;
429    while pos < arr.len().saturating_sub(1) {
430        let etype = arr[pos];
431        if etype == 0 { break; }
432        pos += 1;
433        let Some(key) = read_cstr(&arr[pos..]) else { break };
434        pos += key.len() + 1;
435        if etype == 0x03 {
436            if pos + 4 > arr.len() { break; }
437            let dlen = i32::from_le_bytes([arr[pos], arr[pos+1], arr[pos+2], arr[pos+3]]) as usize;
438            if pos + dlen <= arr.len() {
439                docs.push(arr[pos..pos+dlen].to_vec());
440            }
441            pos += dlen;
442        } else {
443            break; // unexpected type in result array
444        }
445    }
446    docs
447}
448
449fn get_doc_field_summary(doc: &[u8], name: &str) -> String {
450    let Some(subdoc) = get_raw_doc_field(doc, name) else { return "{}".into() };
451    bson_doc_to_json_like(&subdoc)
452}
453
454/// Simple BSON doc to JSON-like string (for display, not full fidelity).
455fn bson_doc_to_json_like(doc: &[u8]) -> String {
456    let mut parts = Vec::new();
457    let mut pos = 4;
458    while pos < doc.len().saturating_sub(1) {
459        let etype = doc[pos];
460        if etype == 0 { break; }
461        pos += 1;
462        let Some(key) = read_cstr(&doc[pos..]) else { break };
463        pos += key.len() + 1;
464        let val = match etype {
465            0x01 => { let v = if pos + 8 <= doc.len() { f64::from_le_bytes([doc[pos], doc[pos+1], doc[pos+2], doc[pos+3], doc[pos+4], doc[pos+5], doc[pos+6], doc[pos+7]]) } else { 0.0 }; pos += 8; format!("{}", v) }
466            0x02 => { if pos + 4 > doc.len() { break; } let slen = i32::from_le_bytes([doc[pos], doc[pos+1], doc[pos+2], doc[pos+3]]) as usize; pos += 4; let s = String::from_utf8_lossy(&doc[pos..pos+slen.saturating_sub(1)]).to_string(); pos += slen; format!("\"{}\"", s) }
467            0x03 => { if pos + 4 > doc.len() { break; } let dlen = i32::from_le_bytes([doc[pos], doc[pos+1], doc[pos+2], doc[pos+3]]) as usize; let s = bson_doc_to_json_like(&doc[pos..pos+dlen]); pos += dlen; s }
468            0x04 => { if pos + 4 > doc.len() { break; } let dlen = i32::from_le_bytes([doc[pos], doc[pos+1], doc[pos+2], doc[pos+3]]) as usize; pos += dlen; "[...]".into() }
469            0x07 => { pos += 12; "ObjectId(...)".into() }
470            0x08 => { let v = doc[pos] != 0; pos += 1; format!("{}", v) }
471            0x09 => { pos += 8; "Date(...)".into() }
472            0x0A => { "null".into() }
473            0x10 => { let v = if pos + 4 <= doc.len() { i32::from_le_bytes([doc[pos], doc[pos+1], doc[pos+2], doc[pos+3]]) } else { 0 }; pos += 4; format!("{}", v) }
474            0x12 => { let v = if pos + 8 <= doc.len() { i64::from_le_bytes([doc[pos], doc[pos+1], doc[pos+2], doc[pos+3], doc[pos+4], doc[pos+5], doc[pos+6], doc[pos+7]]) } else { 0 }; pos += 8; format!("{}", v) }
475            _ => { break; }
476        };
477        if key == "_id" || key == "lsid" { continue; }
478        parts.push(format!("{}: {}", key, val));
479        if parts.len() >= 8 { parts.push("...".into()); break; }
480    }
481    format!("{{{}}}", parts.join(", "))
482}
483
484#[cfg(test)]
485mod tests {
486    use super::*;
487
488    /// Build a minimal OP_MSG with a Kind 0 BSON body document.
489    fn build_op_msg(doc: &[u8]) -> Vec<u8> {
490        let msg_len = 16 + 4 + 1 + doc.len(); // header + flags + kind + doc
491        let mut buf = Vec::new();
492        buf.extend_from_slice(&(msg_len as i32).to_le_bytes()); // messageLength
493        buf.extend_from_slice(&1i32.to_le_bytes()); // requestID
494        buf.extend_from_slice(&0i32.to_le_bytes()); // responseTo
495        buf.extend_from_slice(&OP_MSG.to_le_bytes()); // opCode
496        buf.extend_from_slice(&0u32.to_le_bytes()); // flagBits
497        buf.push(0); // kind 0
498        buf.extend_from_slice(doc);
499        buf
500    }
501
502    /// Build a simple BSON document: {"cmd": "coll", "$db": "testdb"}
503    fn build_simple_cmd(cmd: &str, coll: &str) -> Vec<u8> {
504        let mut doc = Vec::new();
505        doc.extend_from_slice(&[0; 4]); // placeholder for size
506        // cmd: coll (string)
507        doc.push(0x02); // string type
508        doc.extend_from_slice(cmd.as_bytes());
509        doc.push(0);
510        let val = format!("{}\0", coll);
511        doc.extend_from_slice(&(val.len() as i32).to_le_bytes());
512        doc.extend_from_slice(val.as_bytes());
513        // $db: "testdb" (string)
514        doc.push(0x02);
515        doc.extend_from_slice(b"$db\0");
516        let db = "testdb\0";
517        doc.extend_from_slice(&(db.len() as i32).to_le_bytes());
518        doc.extend_from_slice(db.as_bytes());
519        // end
520        doc.push(0);
521        let len = doc.len() as i32;
522        doc[0..4].copy_from_slice(&len.to_le_bytes());
523        doc
524    }
525
526    #[test]
527    fn test_parse_find_request() {
528        let doc = build_simple_cmd("find", "users");
529        let buf = build_op_msg(&doc);
530        let result = parse_mongo_request(&buf).unwrap();
531        assert!(result.contains("find"));
532        assert!(result.contains("testdb"));
533        assert!(result.contains("users"));
534    }
535
536    #[test]
537    fn test_parse_insert_request() {
538        let doc = build_simple_cmd("insert", "users");
539        let buf = build_op_msg(&doc);
540        let result = parse_mongo_request(&buf).unwrap();
541        assert!(result.contains("insert"));
542        assert!(result.contains("testdb.users"));
543    }
544
545    #[test]
546    fn test_parse_response_ok() {
547        // {"ok": 1.0}
548        let mut doc = Vec::new();
549        doc.extend_from_slice(&[0; 4]);
550        doc.push(0x01); // double
551        doc.extend_from_slice(b"ok\0");
552        doc.extend_from_slice(&1.0f64.to_le_bytes());
553        doc.push(0);
554        let len = doc.len() as i32;
555        doc[0..4].copy_from_slice(&len.to_le_bytes());
556
557        let buf = build_op_msg(&doc);
558        let result = parse_mongo_response(&buf).unwrap();
559        assert_eq!(result, "OK");
560    }
561
562    #[test]
563    fn test_parse_response_error() {
564        // {"ok": 0.0, "errmsg": "not found", "code": 26}
565        let mut doc = Vec::new();
566        doc.extend_from_slice(&[0; 4]);
567        // ok: 0.0
568        doc.push(0x01);
569        doc.extend_from_slice(b"ok\0");
570        doc.extend_from_slice(&0.0f64.to_le_bytes());
571        // errmsg: "not found"
572        doc.push(0x02);
573        doc.extend_from_slice(b"errmsg\0");
574        let msg = "not found\0";
575        doc.extend_from_slice(&(msg.len() as i32).to_le_bytes());
576        doc.extend_from_slice(msg.as_bytes());
577        // code: 26
578        doc.push(0x10);
579        doc.extend_from_slice(b"code\0");
580        doc.extend_from_slice(&26i32.to_le_bytes());
581        doc.push(0);
582        let len = doc.len() as i32;
583        doc[0..4].copy_from_slice(&len.to_le_bytes());
584
585        let buf = build_op_msg(&doc);
586        let result = parse_mongo_response(&buf).unwrap();
587        assert!(result.contains("ERR"));
588        assert!(result.contains("26"));
589        assert!(result.contains("not found"));
590    }
591
592    #[test]
593    fn test_mongo_msg_len() {
594        let buf = build_op_msg(&build_simple_cmd("ping", "admin"));
595        assert_eq!(mongo_msg_len(&buf), Some(buf.len()));
596    }
597
598    #[test]
599    fn test_mongo_msg_len_too_short() {
600        assert_eq!(mongo_msg_len(&[1, 2, 3]), None);
601    }
602
603    #[test]
604    fn test_extract_full_command_find() {
605        let doc = build_simple_cmd("find", "users");
606        let buf = build_op_msg(&doc);
607        let result = extract_mongo_full_command(&buf).unwrap();
608        assert!(result.contains("db.users.find"));
609    }
610}