1use crate::error::LlmError;
15use serde::{Deserialize, Serialize};
16use serde_json::Value;
17use std::collections::HashMap;
18use std::path::Path;
19use std::process::Command;
20use rusqlite::Connection;
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct SymbolSet {
52 pub symbol_ids: Vec<String>,
54}
55
56impl SymbolSet {
57 pub fn from_file(path: &Path) -> Result<Self, LlmError> {
82 let content = std::fs::read_to_string(path).map_err(LlmError::IoError)?;
83 serde_json::from_str(&content).map_err(LlmError::JsonError)
84 }
85
86 pub fn validate(&self) -> Result<(), LlmError> {
112 for symbol_id in &self.symbol_ids {
113 if symbol_id.len() != 32 {
114 return Err(LlmError::InvalidQuery {
115 query: format!(
116 "Invalid SymbolId format: '{}'. Expected 32 hex characters.",
117 symbol_id
118 ),
119 });
120 }
121 if !symbol_id.chars().all(|c| c.is_ascii_hexdigit()) {
122 return Err(LlmError::InvalidQuery {
123 query: format!(
124 "Invalid SymbolId format: '{}'. Expected only hexadecimal characters.",
125 symbol_id
126 ),
127 });
128 }
129 }
130 Ok(())
131 }
132
133 pub fn is_empty(&self) -> bool {
135 self.symbol_ids.is_empty()
136 }
137
138 pub fn len(&self) -> usize {
140 self.symbol_ids.len()
141 }
142}
143
144pub fn check_magellan_available() -> Result<(), LlmError> {
165 check_magellan_version()
166}
167
168fn check_magellan_version() -> Result<(), LlmError> {
173 use std::thread;
174 use std::time::Duration;
175
176 let child = Command::new("magellan")
177 .arg("--version")
178 .spawn();
179
180 let output = match child {
181 Ok(mut child) => {
182 let timeout = Duration::from_secs(5);
183 let start = std::time::Instant::now();
184
185 loop {
186 if let Ok(status) = child.try_wait() {
187 match status {
188 Some(_status) => {
189 break child.wait_with_output();
191 }
192 None => {
193 if start.elapsed() >= timeout {
195 child.kill().ok();
196 return Err(LlmError::MagellanExecutionFailed {
197 algorithm: "version check".to_string(),
198 stderr: "Version check timed out after 5 seconds".to_string(),
199 });
200 }
201 thread::sleep(Duration::from_millis(100));
202 }
203 }
204 }
205 }
206 }
207 Err(e) => return Err(match e.kind() {
208 std::io::ErrorKind::NotFound => LlmError::MagellanNotFound,
209 _ => LlmError::MagellanExecutionFailed {
210 algorithm: "version check".to_string(),
211 stderr: e.to_string(),
212 },
213 }),
214 };
215
216 let output = output.map_err(|e| match e.kind() {
217 std::io::ErrorKind::NotFound => LlmError::MagellanNotFound,
218 _ => LlmError::MagellanExecutionFailed {
219 algorithm: "version check".to_string(),
220 stderr: e.to_string(),
221 },
222 })?;
223
224 if !output.status.success() {
225 return Err(LlmError::MagellanExecutionFailed {
226 algorithm: "version check".to_string(),
227 stderr: String::from_utf8_lossy(&output.stderr).to_string(),
228 });
229 }
230
231 let version_str = String::from_utf8_lossy(&output.stdout);
233 let version = parse_magellan_version(&version_str)?;
234
235 if version < (2, 1, 0) {
237 return Err(LlmError::MagellanVersionMismatch {
238 current: format!("{}.{}.{}", version.0, version.1, version.2),
239 required: "2.1.0".to_string(),
240 });
241 }
242
243 Ok(())
244}
245
246fn parse_magellan_version(output: &str) -> Result<(u32, u32, u32), LlmError> {
250 let first_line = output.lines().next().unwrap_or("");
252 let version_part = first_line
253 .strip_prefix("magellan")
254 .unwrap_or("")
255 .split_whitespace()
256 .next()
257 .unwrap_or("");
258
259 let parts: Vec<&str> = version_part.split('.').collect();
261 if parts.len() != 3 {
262 return Err(LlmError::MagellanExecutionFailed {
263 algorithm: "version parsing".to_string(),
264 stderr: format!("Unable to parse version from: {}", first_line),
265 });
266 }
267
268 let major: u32 = parts[0].parse().map_err(|_| LlmError::MagellanExecutionFailed {
269 algorithm: "version parsing".to_string(),
270 stderr: format!("Invalid major version: {}", parts[0]),
271 })?;
272 let minor: u32 = parts[1].parse().map_err(|_| LlmError::MagellanExecutionFailed {
273 algorithm: "version parsing".to_string(),
274 stderr: format!("Invalid minor version: {}", parts[1]),
275 })?;
276 let patch: u32 = parts[2].parse().map_err(|_| LlmError::MagellanExecutionFailed {
277 algorithm: "version parsing".to_string(),
278 stderr: format!("Invalid patch version: {}", parts[2]),
279 })?;
280
281 Ok((major, minor, patch))
282}
283
284pub fn run_magellan_algorithm(
329 db_path: &Path,
330 algorithm: &str,
331 args: &[&str],
332) -> Result<SymbolSet, LlmError> {
333 check_magellan_available()?;
335
336 let mut cmd = Command::new("magellan");
338 cmd.arg(algorithm)
339 .arg("--db")
340 .arg(db_path)
341 .arg("--output")
342 .arg("json")
343 .args(args);
344
345 let output = cmd.output().map_err(|e| match e.kind() {
347 std::io::ErrorKind::NotFound => LlmError::MagellanNotFound,
348 _ => LlmError::MagellanExecutionFailed {
349 algorithm: algorithm.to_string(),
350 stderr: e.to_string(),
351 },
352 })?;
353
354 if !output.status.success() {
355 let stderr = String::from_utf8_lossy(&output.stderr);
356 return Err(LlmError::MagellanExecutionFailed {
357 algorithm: algorithm.to_string(),
358 stderr: stderr.to_string(),
359 });
360 }
361
362 let json_str = String::from_utf8_lossy(&output.stdout);
364 extract_symbol_ids_from_magellan_json(&json_str, algorithm)
365}
366
367fn extract_symbol_ids_from_magellan_json(
389 json: &str,
390 algorithm: &str,
391) -> Result<SymbolSet, LlmError> {
392 let parsed: Value = serde_json::from_str(json).map_err(LlmError::JsonError)?;
393
394 let symbol_ids = match algorithm {
396 "reachable" => {
397 let symbols = parsed["result"]["symbols"]
398 .as_array()
399 .ok_or_else(|| LlmError::InvalidQuery {
400 query: "Missing 'symbols' array in reachable output".to_string(),
401 })?;
402 symbols
403 .iter()
404 .filter_map(|s| s["symbol_id"].as_str().map(|id| id.to_string()))
405 .collect()
406 }
407 "dead-code" => {
408 let dead_symbols = parsed["result"]["dead_symbols"]
409 .as_array()
410 .ok_or_else(|| LlmError::InvalidQuery {
411 query: "Missing 'dead_symbols' array in dead-code output".to_string(),
412 })?;
413 dead_symbols
414 .iter()
415 .filter_map(|s| s["symbol"]["symbol_id"].as_str().map(|id| id.to_string()))
416 .collect()
417 }
418 "slice" => {
419 let included_symbols = parsed["result"]["included_symbols"]
420 .as_array()
421 .ok_or_else(|| LlmError::InvalidQuery {
422 query: "Missing 'included_symbols' array in slice output".to_string(),
423 })?;
424 included_symbols
425 .iter()
426 .filter_map(|s| s["symbol_id"].as_str().map(|id| id.to_string()))
427 .collect()
428 }
429 "cycles" => {
430 let cycles = parsed["result"]["cycles"]
431 .as_array()
432 .ok_or_else(|| LlmError::InvalidQuery {
433 query: "Missing 'cycles' array in cycles output".to_string(),
434 })?;
435 let mut ids = Vec::new();
436 for cycle in cycles {
437 if let Some(members) = cycle["members"].as_array() {
438 for member in members {
439 if let Some(id) = member["symbol_id"].as_str() {
440 ids.push(id.to_string());
441 }
442 }
443 }
444 }
445 ids
446 }
447 _ => {
448 return Err(LlmError::InvalidQuery {
449 query: format!("Unknown algorithm: {}", algorithm),
450 });
451 }
452 };
453
454 Ok(SymbolSet { symbol_ids })
455}
456
457pub fn parse_condense_output(json: &str) -> Result<(Vec<String>, std::collections::HashMap<String, String>), LlmError> {
505 use std::collections::HashMap;
506
507 let parsed: Value = serde_json::from_str(json).map_err(LlmError::JsonError)?;
508
509 let supernodes = if parsed["data"]["supernodes"].is_array() {
512 parsed["data"]["supernodes"].as_array()
513 } else {
514 parsed["supernodes"].as_array()
515 }
516 .ok_or_else(|| LlmError::MagellanExecutionFailed {
517 algorithm: "condense".to_string(),
518 stderr: "Missing 'supernodes' array in condense output".to_string(),
519 })?;
520
521 let mut all_symbol_ids = Vec::new();
522 let mut supernode_map = HashMap::new();
523
524 for supernode in supernodes {
526 let supernode_id = if let Some(id_str) = supernode["id"].as_str() {
528 id_str.to_string()
529 } else if let Some(id_num) = supernode["id"].as_u64() {
530 format!("supernode_{}", id_num)
531 } else if let Some(id_i64) = supernode["id"].as_i64() {
532 format!("supernode_{}", id_i64)
533 } else {
534 return Err(LlmError::MagellanExecutionFailed {
535 algorithm: "condense".to_string(),
536 stderr: "Supernode missing 'id' field".to_string(),
537 });
538 };
539
540 let members = supernode["members"]
541 .as_array()
542 .ok_or_else(|| LlmError::MagellanExecutionFailed {
543 algorithm: "condense".to_string(),
544 stderr: "Supernode missing 'members' array".to_string(),
545 })?;
546
547 for member in members {
548 let symbol_id = member["symbol_id"]
549 .as_str()
550 .ok_or_else(|| LlmError::MagellanExecutionFailed {
551 algorithm: "condense".to_string(),
552 stderr: "Member missing 'symbol_id' field".to_string(),
553 })?;
554
555 all_symbol_ids.push(symbol_id.to_string());
556 supernode_map.insert(symbol_id.to_string(), supernode_id.to_string());
557 }
558 }
559
560 Ok((all_symbol_ids, supernode_map))
561}
562
563pub fn parse_paths_output(json: &str) -> Result<(Vec<String>, bool), LlmError> {
595 use std::collections::HashSet;
596
597 let parsed: Value = serde_json::from_str(json).map_err(LlmError::JsonError)?;
598
599 let paths = if parsed["data"]["paths"].is_array() {
602 parsed["data"]["paths"].as_array()
603 } else {
604 parsed["paths"].as_array()
605 }
606 .ok_or_else(|| LlmError::MagellanExecutionFailed {
607 algorithm: "paths".to_string(),
608 stderr: "Missing 'paths' array in output".to_string(),
609 })?;
610
611 let mut all_symbol_ids = HashSet::new();
612
613 for path in paths {
615 let symbols = path["symbols"]
616 .as_array()
617 .ok_or_else(|| LlmError::MagellanExecutionFailed {
618 algorithm: "paths".to_string(),
619 stderr: "Path missing 'symbols' array".to_string(),
620 })?;
621
622 for symbol in symbols {
623 let symbol_id = symbol["symbol_id"]
624 .as_str()
625 .ok_or_else(|| LlmError::MagellanExecutionFailed {
626 algorithm: "paths".to_string(),
627 stderr: "Symbol missing 'symbol_id' field".to_string(),
628 })?;
629
630 all_symbol_ids.insert(symbol_id.to_string());
631 }
632 }
633
634 let bounded_hit = parsed["bounded_hit"]
636 .as_bool()
637 .or_else(|| parsed["data"]["bounded_hit"].as_bool())
638 .unwrap_or(false);
639
640 Ok((all_symbol_ids.into_iter().collect(), bounded_hit))
642}
643
644pub fn parse_symbol_set_file(path: &Path) -> Result<SymbolSet, LlmError> {
673 let symbol_set = SymbolSet::from_file(path)?;
674 symbol_set.validate()?;
675 Ok(symbol_set)
676}
677
678#[derive(Debug, Clone, Default)]
680pub struct AlgorithmOptions<'a> {
681 pub from_symbol_set: Option<&'a str>,
683 pub reachable_from: Option<&'a str>,
685 pub dead_code_in: Option<&'a str>,
687 pub in_cycle: Option<&'a str>,
689 pub slice_backward_from: Option<&'a str>,
691 pub slice_forward_from: Option<&'a str>,
693 pub condense: bool,
695 pub paths_from: Option<&'a str>,
697 pub paths_to: Option<&'a str>,
699}
700
701impl<'a> AlgorithmOptions<'a> {
702 pub fn is_active(&self) -> bool {
704 self.from_symbol_set.is_some()
705 || self.reachable_from.is_some()
706 || self.dead_code_in.is_some()
707 || self.in_cycle.is_some()
708 || self.slice_backward_from.is_some()
709 || self.slice_forward_from.is_some()
710 || self.condense
711 || self.paths_from.is_some()
712 }
713
714 pub fn new() -> Self {
716 Self::default()
717 }
718}
719
720pub type AlgorithmFilterResult = Result<(Vec<String>, HashMap<String, String>, bool), LlmError>;
727
728pub fn apply_algorithm_filters(
738 db_path: &Path,
739 options: &AlgorithmOptions<'_>,
740) -> AlgorithmFilterResult {
741 if let Some(file_path) = options.from_symbol_set {
743 let symbol_set = parse_symbol_set_file(Path::new(file_path))?;
744 symbol_set.validate()?;
745 return Ok((symbol_set.symbol_ids, HashMap::new(), false));
746 }
747
748 let active_count = [
751 options.reachable_from.is_some(),
752 options.dead_code_in.is_some(),
753 options.in_cycle.is_some(),
754 options.slice_backward_from.is_some(),
755 options.slice_forward_from.is_some(),
756 options.condense,
757 options.paths_from.is_some(),
758 ]
759 .iter()
760 .filter(|&&x| x)
761 .count();
762
763 if active_count > 1 {
764 return Err(LlmError::InvalidQuery {
765 query: "Only one one-shot algorithm filter may be specified. Use --from-symbol-set for composed filters.".to_string(),
766 });
767 }
768
769 if let Some(symbol) = options.reachable_from {
771 let symbol_id = resolve_fqn_to_symbol_id(db_path, symbol)?;
772 let args = ["--from", &symbol_id];
773 return Ok((run_magellan_algorithm(db_path, "reachable", &args)?.symbol_ids, HashMap::new(), false));
774 }
775
776 if let Some(symbol) = options.dead_code_in {
777 let symbol_id = resolve_fqn_to_symbol_id(db_path, symbol)?;
778 let args = ["--entry", &symbol_id];
779 return Ok((run_magellan_algorithm(db_path, "dead-code", &args)?.symbol_ids, HashMap::new(), false));
780 }
781
782 if let Some(symbol) = options.in_cycle {
783 let symbol_id = resolve_fqn_to_symbol_id(db_path, symbol)?;
784 let args = ["--symbol", &symbol_id];
785 return Ok((run_magellan_algorithm(db_path, "cycles", &args)?.symbol_ids, HashMap::new(), false));
786 }
787
788 if let Some(symbol) = options.slice_backward_from {
789 let symbol_id = resolve_fqn_to_symbol_id(db_path, symbol)?;
790 let args = ["--target", &symbol_id, "--direction", "backward"];
791 return Ok((run_magellan_algorithm(db_path, "slice", &args)?.symbol_ids, HashMap::new(), false));
792 }
793
794 if let Some(symbol) = options.slice_forward_from {
795 let symbol_id = resolve_fqn_to_symbol_id(db_path, symbol)?;
796 let args = ["--target", &symbol_id, "--direction", "forward"];
797 return Ok((run_magellan_algorithm(db_path, "slice", &args)?.symbol_ids, HashMap::new(), false));
798 }
799
800 if options.condense {
802 let output = Command::new("magellan")
803 .args(["condense", "--db", &db_path.to_string_lossy(), "--output", "json"])
804 .output()
805 .map_err(|e| match e.kind() {
806 std::io::ErrorKind::NotFound => LlmError::MagellanNotFound,
807 _ => LlmError::MagellanExecutionFailed {
808 algorithm: "condense".to_string(),
809 stderr: format!("{}\n\nTry running: magellan condense --db {} for more details",
810 e, db_path.to_string_lossy()),
811 },
812 })?;
813
814 if !output.status.success() {
815 let stderr = String::from_utf8_lossy(&output.stderr);
816 return Err(LlmError::MagellanExecutionFailed {
817 algorithm: "condense".to_string(),
818 stderr: format!("{}\n\nTry running: magellan condense --db {} for more details",
819 stderr, db_path.to_string_lossy()),
820 });
821 }
822
823 let json = String::from_utf8_lossy(&output.stdout);
824 let (symbol_ids, supernode_map) = parse_condense_output(&json)?;
825
826 return Ok((symbol_ids, supernode_map, false));
829 }
830
831 if let Some(start_symbol) = options.paths_from {
833 let start_id = resolve_fqn_to_symbol_id(db_path, start_symbol)?;
834 let db_path_str = db_path.to_string_lossy().to_string();
835
836 let mut args = vec![
838 "paths".to_string(),
839 "--db".to_string(),
840 db_path_str.clone(),
841 "--start".to_string(),
842 start_id.clone(),
843 "--max-depth".to_string(),
844 "100".to_string(),
845 "--max-paths".to_string(),
846 "1000".to_string(),
847 "--output".to_string(),
848 "json".to_string(),
849 ];
850
851 if let Some(end_symbol) = options.paths_to {
852 let end_id = resolve_fqn_to_symbol_id(db_path, end_symbol)?;
853 args.push("--end".to_string());
854 args.push(end_id);
855 }
856
857 let args_refs: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
858
859 let output = Command::new("magellan")
860 .args(&args_refs)
861 .output()
862 .map_err(|e| match e.kind() {
863 std::io::ErrorKind::NotFound => LlmError::MagellanNotFound,
864 _ => LlmError::MagellanExecutionFailed {
865 algorithm: "paths".to_string(),
866 stderr: format!("{}\n\nTry running: magellan paths --db {} --start {} for more details",
867 e, db_path_str, start_id),
868 },
869 })?;
870
871 if !output.status.success() {
872 let stderr = String::from_utf8_lossy(&output.stderr);
873 return Err(LlmError::MagellanExecutionFailed {
874 algorithm: "paths".to_string(),
875 stderr: format!("{}\n\nTry running: magellan paths --db {} --start {} for more details",
876 stderr, db_path_str, start_id),
877 });
878 }
879
880 let json = String::from_utf8_lossy(&output.stdout);
881 let (symbol_ids, bounded_hit) = parse_paths_output(&json)?;
882
883 return Ok((symbol_ids, HashMap::new(), bounded_hit));
886 }
887
888 Ok((Vec::new(), HashMap::new(), false))
890}
891
892const SYMBOL_SET_TEMP_TABLE_THRESHOLD: usize = 1000;
894
895pub fn create_symbol_set_temp_table(
900 conn: &Connection,
901 symbol_ids: &[String],
902) -> Result<String, LlmError> {
903 let table_name = format!("symbol_set_filter_{}", std::process::id());
904
905 conn.execute(
907 &format!("CREATE TEMP TABLE {} (symbol_id TEXT PRIMARY KEY)", table_name),
908 [],
909 )
910 .map_err(LlmError::SqliteError)?;
911
912 let mut stmt = conn
914 .prepare(&format!("INSERT INTO {} (symbol_id) VALUES (?)", table_name))
915 .map_err(LlmError::SqliteError)?;
916
917 for symbol_id in symbol_ids {
918 stmt.execute([symbol_id]).map_err(LlmError::SqliteError)?;
919 }
920
921 Ok(table_name)
922}
923
924pub fn symbol_set_filter_strategy(symbol_ids: &[String]) -> SymbolSetStrategy {
926 if symbol_ids.is_empty() {
927 SymbolSetStrategy::None
928 } else if symbol_ids.len() > SYMBOL_SET_TEMP_TABLE_THRESHOLD {
929 SymbolSetStrategy::TempTable
930 } else {
931 SymbolSetStrategy::InClause
932 }
933}
934
935#[derive(Debug, Clone, PartialEq)]
937pub enum SymbolSetStrategy {
938 None,
940 InClause,
942 TempTable,
944}
945
946pub fn resolve_fqn_to_symbol_id(db_path: &Path, name: &str) -> Result<String, LlmError> {
985 check_magellan_available()?;
987
988 let output = Command::new("magellan")
990 .args(["find", "--db", &db_path.to_string_lossy(), "--name", name, "--output", "json"])
991 .output()
992 .map_err(|e| match e.kind() {
993 std::io::ErrorKind::NotFound => LlmError::MagellanNotFound,
994 _ => LlmError::SearchFailed {
995 reason: format!("Failed to execute magellan find: {}", e),
996 },
997 })?;
998
999 if !output.status.success() {
1000 let stderr = String::from_utf8_lossy(&output.stderr);
1001 return Err(LlmError::SearchFailed {
1002 reason: format!("magellan find failed: {}", stderr),
1003 });
1004 }
1005
1006 let json: Value = serde_json::from_slice(&output.stdout).map_err(LlmError::JsonError)?;
1008
1009 let matches = json["matches"]
1011 .as_array()
1012 .ok_or_else(|| LlmError::InvalidQuery {
1013 query: "Invalid magellan find output: missing 'matches' array".to_string(),
1014 })?;
1015
1016 if matches.len() > 1 {
1018 return Err(LlmError::AmbiguousSymbolName {
1019 name: name.to_string(),
1020 count: matches.len(),
1021 });
1022 }
1023
1024 if matches.is_empty() {
1026 return Err(LlmError::InvalidQuery {
1027 query: format!("Symbol '{}' not found in database", name),
1028 });
1029 }
1030
1031 matches[0]["symbol_id"]
1033 .as_str()
1034 .map(|id| id.to_string())
1035 .ok_or_else(|| LlmError::InvalidQuery {
1036 query: format!(
1037 "Symbol '{}' found but missing symbol_id in magellan output",
1038 name
1039 ),
1040 })
1041}
1042
1043#[cfg(test)]
1044mod tests {
1045 use super::*;
1046
1047 #[test]
1048 fn test_symbol_set_validation_valid() {
1049 let symbol_set = SymbolSet {
1050 symbol_ids: vec![
1051 "abc123def456789012345678901234ab".to_string(),
1052 "0123456789abcdef0123456789abcdef".to_string(),
1053 "ffffffffffffffffffffffffffffffff".to_string(),
1054 ],
1055 };
1056 assert!(symbol_set.validate().is_ok());
1057 assert_eq!(symbol_set.len(), 3);
1058 assert!(!symbol_set.is_empty());
1059 }
1060
1061 #[test]
1062 fn test_symbol_set_validation_invalid_length() {
1063 let symbol_set = SymbolSet {
1064 symbol_ids: vec!["abc123".to_string()],
1065 };
1066 assert!(symbol_set.validate().is_err());
1067 }
1068
1069 #[test]
1070 fn test_symbol_set_validation_invalid_chars() {
1071 let symbol_set = SymbolSet {
1072 symbol_ids: vec!["abc123def456789012345678901234g!".to_string()],
1073 };
1074 assert!(symbol_set.validate().is_err());
1075 }
1076
1077 #[test]
1078 fn test_symbol_set_empty() {
1079 let symbol_set = SymbolSet {
1080 symbol_ids: vec![],
1081 };
1082 assert!(symbol_set.validate().is_ok());
1083 assert_eq!(symbol_set.len(), 0);
1084 assert!(symbol_set.is_empty());
1085 }
1086
1087 #[test]
1088 fn test_symbol_set_json_deserialize() {
1089 let json = r#"{"symbol_ids": ["abc123def456789012345678901234ab"]}"#;
1090 let symbol_set: SymbolSet = serde_json::from_str(json).unwrap();
1091 assert_eq!(symbol_set.symbol_ids.len(), 1);
1092 assert_eq!(
1093 symbol_set.symbol_ids[0],
1094 "abc123def456789012345678901234ab"
1095 );
1096 }
1097
1098 #[test]
1099 fn test_symbol_set_json_serialize() {
1100 let symbol_set = SymbolSet {
1101 symbol_ids: vec!["abc123def456789012345678901234ab".to_string()],
1102 };
1103 let json = serde_json::to_string(&symbol_set).unwrap();
1104 assert!(json.contains("symbol_ids"));
1105 assert!(json.contains("abc123def456789012345678901234ab"));
1106 }
1107}