syncable_cli/analyzer/
tool_installer.rs

1use crate::analyzer::dependency_parser::Language;
2use crate::error::{AnalysisError, IaCGeneratorError, Result};
3use log::{info, warn, debug};
4use std::process::Command;
5use std::collections::HashMap;
6use std::path::PathBuf;
7
8/// Tool installer for vulnerability scanning dependencies
9pub struct ToolInstaller {
10    installed_tools: HashMap<String, bool>,
11}
12
13impl ToolInstaller {
14    pub fn new() -> Self {
15        Self {
16            installed_tools: HashMap::new(),
17        }
18    }
19    
20    /// Ensure all required tools for vulnerability scanning are available
21    pub fn ensure_tools_for_languages(&mut self, languages: &[Language]) -> Result<()> {
22        for language in languages {
23            match language {
24                Language::Rust => self.ensure_cargo_audit()?,
25                Language::JavaScript | Language::TypeScript => self.ensure_npm()?,
26                Language::Python => self.ensure_pip_audit()?,
27                Language::Go => self.ensure_govulncheck()?,
28                Language::Java | Language::Kotlin => self.ensure_grype()?,
29                _ => {} // Unknown languages don't need tools
30            }
31        }
32        Ok(())
33    }
34    
35    /// Check if cargo-audit is installed, install if needed
36    fn ensure_cargo_audit(&mut self) -> Result<()> {
37        if self.is_tool_installed("cargo-audit") {
38            return Ok(());
39        }
40        
41        info!("šŸ”§ Installing cargo-audit for Rust vulnerability scanning...");
42        
43        let output = Command::new("cargo")
44            .args(&["install", "cargo-audit"])
45            .output()
46            .map_err(|e| IaCGeneratorError::Analysis(AnalysisError::DependencyParsing {
47                file: "cargo-audit installation".to_string(),
48                reason: format!("Failed to install cargo-audit: {}", e),
49            }))?;
50        
51        if output.status.success() {
52            info!("āœ… cargo-audit installed successfully");
53            self.installed_tools.insert("cargo-audit".to_string(), true);
54        } else {
55            let stderr = String::from_utf8_lossy(&output.stderr);
56            warn!("āŒ Failed to install cargo-audit: {}", stderr);
57            return Err(IaCGeneratorError::Analysis(AnalysisError::DependencyParsing {
58                file: "cargo-audit installation".to_string(),
59                reason: format!("Installation failed: {}", stderr),
60            }));
61        }
62        
63        Ok(())
64    }
65    
66    /// Check if npm is available (comes with Node.js)
67    fn ensure_npm(&mut self) -> Result<()> {
68        if self.is_tool_installed("npm") {
69            return Ok(());
70        }
71        
72        warn!("šŸ“¦ npm not found. Please install Node.js from https://nodejs.org/");
73        warn!("   npm audit is required for JavaScript/TypeScript vulnerability scanning");
74        
75        Ok(()) // Don't fail, just warn
76    }
77    
78    /// Check if pip-audit is installed, install if needed
79    fn ensure_pip_audit(&mut self) -> Result<()> {
80        if self.is_tool_installed("pip-audit") {
81            return Ok(());
82        }
83        
84        info!("šŸ”§ Installing pip-audit for Python vulnerability scanning...");
85        
86        // Try different installation methods
87        let install_commands = vec![
88            vec!["pipx", "install", "pip-audit"],
89            vec!["pip3", "install", "--user", "pip-audit"],
90            vec!["pip", "install", "--user", "pip-audit"],
91        ];
92        
93        for cmd in install_commands {
94            debug!("Trying installation command: {:?}", cmd);
95            
96            let output = Command::new(&cmd[0])
97                .args(&cmd[1..])
98                .output();
99                
100            if let Ok(result) = output {
101                if result.status.success() {
102                    info!("āœ… pip-audit installed successfully using {}", cmd[0]);
103                    self.installed_tools.insert("pip-audit".to_string(), true);
104                    return Ok(());
105                }
106            }
107        }
108        
109        warn!("šŸ“¦ Failed to auto-install pip-audit. Please install manually:");
110        warn!("   Option 1: pipx install pip-audit");
111        warn!("   Option 2: pip3 install --user pip-audit");
112        
113        Ok(()) // Don't fail, just warn
114    }
115    
116    /// Check if govulncheck is installed, install if needed
117    fn ensure_govulncheck(&mut self) -> Result<()> {
118        if self.is_tool_installed("govulncheck") {
119            return Ok(());
120        }
121        
122        info!("šŸ”§ Installing govulncheck for Go vulnerability scanning...");
123        
124        let output = Command::new("go")
125            .args(&["install", "golang.org/x/vuln/cmd/govulncheck@latest"])
126            .output()
127            .map_err(|e| IaCGeneratorError::Analysis(AnalysisError::DependencyParsing {
128                file: "govulncheck installation".to_string(),
129                reason: format!("Failed to install govulncheck (is Go installed?): {}", e),
130            }))?;
131        
132        if output.status.success() {
133            info!("āœ… govulncheck installed successfully");
134            self.installed_tools.insert("govulncheck".to_string(), true);
135            
136            // Also add Go bin directory to PATH hint
137            info!("šŸ’” Note: Make sure ~/go/bin is in your PATH to use govulncheck");
138        } else {
139            let stderr = String::from_utf8_lossy(&output.stderr);
140            warn!("āŒ Failed to install govulncheck: {}", stderr);
141            warn!("šŸ“¦ Please install Go from https://golang.org/ first");
142        }
143        
144        Ok(())
145    }
146    
147    /// Check if Grype is available, install if possible
148    fn ensure_grype(&mut self) -> Result<()> {
149        if self.is_tool_installed("grype") {
150            return Ok(());
151        }
152        
153        info!("šŸ”§ Installing grype for vulnerability scanning...");
154        
155        // Detect platform and architecture
156        let os = std::env::consts::OS;
157        let arch = std::env::consts::ARCH;
158        
159        // Try platform-specific installation methods
160        match os {
161            "macos" => {
162                // Try to install with Homebrew
163                let output = Command::new("brew")
164                    .args(&["install", "grype"])
165                    .output();
166                    
167                match output {
168                    Ok(result) if result.status.success() => {
169                        info!("āœ… grype installed successfully via Homebrew");
170                        self.installed_tools.insert("grype".to_string(), true);
171                        return Ok(());
172                    }
173                    _ => {
174                        warn!("āŒ Failed to install via Homebrew. Trying manual installation...");
175                    }
176                }
177            }
178            _ => {}
179        }
180        
181        // Try manual installation via curl
182        self.install_grype_manually(os, arch)
183    }
184    
185    /// Install grype manually by downloading from GitHub releases
186    fn install_grype_manually(&mut self, os: &str, arch: &str) -> Result<()> {
187        use std::fs;
188        use std::path::PathBuf;
189        
190        info!("šŸ“„ Downloading grype from GitHub releases...");
191        
192        let version = "v0.92.2"; // Latest stable version
193        
194        // Use platform-appropriate directories
195        let bin_dir = if cfg!(windows) {
196            // On Windows, use %USERPROFILE%\.local\bin or %APPDATA%\syncable-cli\bin
197            let home_dir = std::env::var("USERPROFILE")
198                .or_else(|_| std::env::var("APPDATA"))
199                .unwrap_or_else(|_| ".".to_string());
200            PathBuf::from(&home_dir).join(".local").join("bin")
201        } else {
202            // On Unix systems, use $HOME/.local/bin
203            let home_dir = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
204            PathBuf::from(&home_dir).join(".local").join("bin")
205        };
206        
207        // Create bin directory
208        fs::create_dir_all(&bin_dir).map_err(|e| {
209            IaCGeneratorError::Analysis(AnalysisError::DependencyParsing {
210                file: "grype installation".to_string(),
211                reason: format!("Failed to create directory: {}", e),
212            })
213        })?;
214        
215        // Determine the correct binary name based on OS and architecture
216        let (os_name, arch_name, file_extension) = match (os, arch) {
217            ("macos", "x86_64") => ("darwin", "amd64", ""),
218            ("macos", "aarch64") => ("darwin", "arm64", ""),
219            ("linux", "x86_64") => ("linux", "amd64", ""),
220            ("linux", "aarch64") => ("linux", "arm64", ""),
221            ("windows", "x86_64") => ("windows", "amd64", ".exe"),
222            ("windows", "aarch64") => ("windows", "arm64", ".exe"),
223            _ => {
224                warn!("āŒ Unsupported platform: {} {}", os, arch);
225                return Ok(());
226            }
227        };
228        
229        // Windows uses zip files, Unix uses tar.gz
230        let (archive_name, download_url) = if cfg!(windows) {
231            let archive_name = format!("grype_{}_windows_{}.zip", version.trim_start_matches('v'), arch_name);
232            let download_url = format!(
233                "https://github.com/anchore/grype/releases/download/{}/{}",
234                version, archive_name
235            );
236            (archive_name, download_url)
237        } else {
238            let archive_name = format!("grype_{}_{}.tar.gz", os_name, arch_name);
239            let download_url = format!(
240                "https://github.com/anchore/grype/releases/download/{}/grype_{}_{}_{}.tar.gz",
241                version, version.trim_start_matches('v'), os_name, arch_name
242            );
243            (archive_name, download_url)
244        };
245        
246        let archive_path = bin_dir.join(&archive_name);
247        let grype_binary = bin_dir.join(format!("grype{}", file_extension));
248        
249        info!("šŸ“¦ Downloading from: {}", download_url);
250        
251        // Use platform-appropriate download method
252        let download_success = if cfg!(windows) {
253            // On Windows, try PowerShell first, then curl if available
254            self.download_file_windows(&download_url, &archive_path)
255        } else {
256            // On Unix, use curl
257            self.download_file_unix(&download_url, &archive_path)
258        };
259        
260        if download_success {
261            info!("āœ… Download complete. Extracting...");
262            
263            let extract_success = if cfg!(windows) {
264                self.extract_zip_windows(&archive_path, &bin_dir)
265            } else {
266                self.extract_tar_unix(&archive_path, &bin_dir)
267            };
268            
269            if extract_success {
270                info!("āœ… grype installed successfully to {}", bin_dir.display());
271                if cfg!(windows) {
272                    info!("šŸ’” Make sure {} is in your PATH", bin_dir.display());
273                } else {
274                    info!("šŸ’” Make sure ~/.local/bin is in your PATH");
275                }
276                self.installed_tools.insert("grype".to_string(), true);
277                
278                // Clean up archive
279                fs::remove_file(&archive_path).ok();
280                
281                return Ok(());
282            }
283        }
284        
285        warn!("āŒ Automatic installation failed. Please install manually:");
286        if cfg!(windows) {
287            warn!("   • Download from: https://github.com/anchore/grype/releases");
288            warn!("   • Or use: scoop install grype (if you have Scoop)");
289        } else {
290            warn!("   • macOS: brew install grype");
291            warn!("   • Download: https://github.com/anchore/grype/releases");
292        }
293        
294        Ok(())
295    }
296    
297    /// Download file on Windows using PowerShell or curl
298    fn download_file_windows(&self, url: &str, output_path: &PathBuf) -> bool {
299        use std::process::Command;
300        
301        // Try PowerShell first (available on all modern Windows)
302        let powershell_result = Command::new("powershell")
303            .args(&[
304                "-Command",
305                &format!(
306                    "Invoke-WebRequest -Uri '{}' -OutFile '{}' -UseBasicParsing",
307                    url,
308                    output_path.to_string_lossy()
309                )
310            ])
311            .output();
312            
313        if let Ok(result) = powershell_result {
314            if result.status.success() {
315                return true;
316            }
317        }
318        
319        // Fallback to curl if available
320        let curl_result = Command::new("curl")
321            .args(&["-L", "-o", &output_path.to_string_lossy(), url])
322            .output();
323            
324        curl_result.map(|o| o.status.success()).unwrap_or(false)
325    }
326    
327    /// Download file on Unix using curl
328    fn download_file_unix(&self, url: &str, output_path: &PathBuf) -> bool {
329        use std::process::Command;
330        
331        let output = Command::new("curl")
332            .args(&["-L", "-o", &output_path.to_string_lossy(), url])
333            .output();
334            
335        output.map(|o| o.status.success()).unwrap_or(false)
336    }
337    
338    /// Extract ZIP file on Windows
339    fn extract_zip_windows(&self, archive_path: &PathBuf, extract_dir: &PathBuf) -> bool {
340        use std::process::Command;
341        
342        // Try PowerShell Expand-Archive first
343        let powershell_result = Command::new("powershell")
344            .args(&[
345                "-Command",
346                &format!(
347                    "Expand-Archive -Path '{}' -DestinationPath '{}' -Force",
348                    archive_path.to_string_lossy(),
349                    extract_dir.to_string_lossy()
350                )
351            ])
352            .output();
353            
354        if let Ok(result) = powershell_result {
355            if result.status.success() {
356                return true;
357            }
358        }
359        
360        // Fallback: try tar (available in newer Windows versions)
361        let tar_result = Command::new("tar")
362            .args(&["-xf", &archive_path.to_string_lossy(), "-C", &extract_dir.to_string_lossy()])
363            .output();
364            
365        tar_result.map(|o| o.status.success()).unwrap_or(false)
366    }
367    
368    /// Extract TAR file on Unix
369    fn extract_tar_unix(&self, archive_path: &PathBuf, extract_dir: &PathBuf) -> bool {
370        use std::process::Command;
371        
372        let extract_output = Command::new("tar")
373            .args(&["-xzf", &archive_path.to_string_lossy(), "-C", &extract_dir.to_string_lossy()])
374            .output();
375            
376        if let Ok(result) = extract_output {
377            if result.status.success() {
378                // Make it executable on Unix
379                #[cfg(unix)]
380                {
381                    let grype_path = extract_dir.join("grype");
382                    Command::new("chmod")
383                        .args(&["+x", &grype_path.to_string_lossy()])
384                        .output()
385                        .ok();
386                }
387                return true;
388            }
389        }
390        
391        false
392    }
393    
394    /// Check if OWASP dependency-check is available, install if possible
395    fn ensure_dependency_check(&mut self) -> Result<()> {
396        if self.is_tool_installed("dependency-check") {
397            return Ok(());
398        }
399        
400        info!("šŸ”§ Installing dependency-check for Java/Kotlin vulnerability scanning...");
401        
402        // Detect platform and try to install
403        let os = std::env::consts::OS;
404        
405        match os {
406            "macos" => {
407                // Try to install with Homebrew
408                let output = Command::new("brew")
409                    .args(&["install", "dependency-check"])
410                    .output();
411                    
412                match output {
413                    Ok(result) if result.status.success() => {
414                        info!("āœ… dependency-check installed successfully via Homebrew");
415                        self.installed_tools.insert("dependency-check".to_string(), true);
416                        return Ok(());
417                    }
418                    _ => {
419                        warn!("āŒ Failed to install via Homebrew. Trying manual installation...");
420                    }
421                }
422            }
423            "linux" => {
424                // Try to install via snap
425                let output = Command::new("snap")
426                    .args(&["install", "dependency-check"])
427                    .output();
428                    
429                if output.map(|o| o.status.success()).unwrap_or(false) {
430                    info!("āœ… dependency-check installed successfully via snap");
431                    self.installed_tools.insert("dependency-check".to_string(), true);
432                    return Ok(());
433                }
434            }
435            _ => {}
436        }
437        
438        // Try manual installation
439        self.install_dependency_check_manually()
440    }
441    
442    /// Install dependency-check manually by downloading from GitHub
443    fn install_dependency_check_manually(&mut self) -> Result<()> {
444        use std::fs;
445        use std::path::PathBuf;
446        
447        info!("šŸ“„ Downloading dependency-check from GitHub releases...");
448        
449        let version = "11.1.0"; // Latest stable version
450        
451        // Use platform-appropriate directories
452        let (home_dir, install_dir) = if cfg!(windows) {
453            let home = std::env::var("USERPROFILE")
454                .or_else(|_| std::env::var("APPDATA"))
455                .unwrap_or_else(|_| ".".to_string());
456            let install = PathBuf::from(&home).join("dependency-check");
457            (home, install)
458        } else {
459            let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
460            let install = PathBuf::from(&home).join(".local").join("share").join("dependency-check");
461            (home, install)
462        };
463        
464        // Create installation directory
465        fs::create_dir_all(&install_dir).map_err(|e| {
466            IaCGeneratorError::Analysis(AnalysisError::DependencyParsing {
467                file: "dependency-check installation".to_string(),
468                reason: format!("Failed to create directory: {}", e),
469            })
470        })?;
471        
472        let archive_name = "dependency-check-11.1.0-release.zip";
473        let download_url = format!(
474            "https://github.com/jeremylong/DependencyCheck/releases/download/v{}/{}",
475            version, archive_name
476        );
477        
478        let archive_path = install_dir.join(archive_name);
479        
480        info!("šŸ“¦ Downloading from: {}", download_url);
481        
482        // Use platform-appropriate download method
483        let download_success = if cfg!(windows) {
484            self.download_file_windows(&download_url, &archive_path)
485        } else {
486            self.download_file_unix(&download_url, &archive_path)
487        };
488        
489        if download_success {
490            info!("āœ… Download complete. Extracting...");
491            
492            let extract_success = if cfg!(windows) {
493                self.extract_zip_windows(&archive_path, &install_dir)
494            } else {
495                // Use unzip on Unix for .zip files
496                let output = std::process::Command::new("unzip")
497                    .args(&["-o", &archive_path.to_string_lossy(), "-d", &install_dir.to_string_lossy()])
498                    .output();
499                output.map(|o| o.status.success()).unwrap_or(false)
500            };
501                
502            if extract_success {
503                // Create appropriate launcher
504                if cfg!(windows) {
505                    self.create_windows_launcher(&install_dir, &home_dir)?;
506                } else {
507                    self.create_unix_launcher(&install_dir, &home_dir)?;
508                }
509                
510                info!("āœ… dependency-check installed successfully to {}", install_dir.display());
511                self.installed_tools.insert("dependency-check".to_string(), true);
512                
513                // Clean up archive
514                fs::remove_file(&archive_path).ok();
515                return Ok(());
516            }
517        }
518        
519        warn!("āŒ Automatic installation failed. Please install manually:");
520        if cfg!(windows) {
521            warn!("   • Download: https://github.com/jeremylong/DependencyCheck/releases");
522            warn!("   • Or use: scoop install dependency-check (if you have Scoop)");
523        } else {
524            warn!("   • macOS: brew install dependency-check");
525            warn!("   • Download: https://github.com/jeremylong/DependencyCheck/releases");
526        }
527        
528        Ok(())
529    }
530    
531    /// Create Windows launcher for dependency-check
532    fn create_windows_launcher(&self, install_dir: &PathBuf, home_dir: &str) -> Result<()> {
533        use std::fs;
534        
535        let bin_dir = PathBuf::from(home_dir).join(".local").join("bin");
536        fs::create_dir_all(&bin_dir).ok();
537        
538        let dc_script = install_dir.join("dependency-check").join("bin").join("dependency-check.bat");
539        let launcher_path = bin_dir.join("dependency-check.bat");
540        
541        // Create a batch file launcher
542        let launcher_content = format!(
543            "@echo off\n\"{}\" %*\n",
544            dc_script.to_string_lossy()
545        );
546        
547        fs::write(&launcher_path, launcher_content).map_err(|e| {
548            IaCGeneratorError::Analysis(AnalysisError::DependencyParsing {
549                file: "dependency-check launcher".to_string(),
550                reason: format!("Failed to create launcher: {}", e),
551            })
552        })?;
553        
554        info!("šŸ’” Added to {}", launcher_path.display());
555        info!("šŸ’” Make sure {} is in your PATH", bin_dir.display());
556        
557        Ok(())
558    }
559    
560    /// Create Unix launcher for dependency-check
561    fn create_unix_launcher(&self, install_dir: &PathBuf, home_dir: &str) -> Result<()> {
562        use std::fs;
563        
564        let bin_dir = PathBuf::from(home_dir).join(".local").join("bin");
565        fs::create_dir_all(&bin_dir).ok();
566        
567        let dc_script = install_dir.join("dependency-check").join("bin").join("dependency-check.sh");
568        let symlink = bin_dir.join("dependency-check");
569        
570        // Remove old symlink if exists
571        fs::remove_file(&symlink).ok();
572        
573        // Create new symlink (Unix only)
574        #[cfg(unix)]
575        {
576            if std::os::unix::fs::symlink(&dc_script, &symlink).is_ok() {
577                info!("šŸ’” Added to ~/.local/bin/dependency-check");
578                info!("šŸ’” Make sure ~/.local/bin is in your PATH");
579                return Ok(());
580            }
581        }
582        
583        // Fallback: create a shell script wrapper
584        let wrapper_content = format!(
585            "#!/bin/bash\nexec \"{}\" \"$@\"\n",
586            dc_script.to_string_lossy()
587        );
588        
589        fs::write(&symlink, wrapper_content).map_err(|e| {
590            IaCGeneratorError::Analysis(AnalysisError::DependencyParsing {
591                file: "dependency-check wrapper".to_string(),
592                reason: format!("Failed to create wrapper: {}", e),
593            })
594        })?;
595        
596        // Make executable
597        #[cfg(unix)]
598        {
599            use std::process::Command;
600            Command::new("chmod")
601                .args(&["+x", &symlink.to_string_lossy()])
602                .output()
603                .ok();
604        }
605        
606        Ok(())
607    }
608    
609    /// Check if a tool is installed and available
610    fn is_tool_installed(&self, tool: &str) -> bool {
611        use std::process::Command;
612        
613        // Check cache first
614        if let Some(&cached) = self.installed_tools.get(tool) {
615            return cached;
616        }
617        
618        // Different version check commands for different tools
619        let version_arg = match tool {
620            "grype" => "version",
621            "cargo-audit" => "--version",
622            "pip-audit" => "--version", 
623            "govulncheck" => "-version",
624            "dependency-check" => "--version",
625            _ => "--version",
626        };
627        
628        let result = Command::new(tool)
629            .arg(version_arg)
630            .output();
631            
632        match result {
633            Ok(output) => output.status.success(),
634            Err(_) => {
635                // Try platform-specific paths
636                self.try_alternative_paths(tool, version_arg)
637            }
638        }
639    }
640    
641    /// Try alternative paths for tools
642    fn try_alternative_paths(&self, tool: &str, version_arg: &str) -> bool {
643        use std::process::Command;
644        
645        let alternative_paths = if cfg!(windows) {
646            // Windows-specific paths
647            let userprofile = std::env::var("USERPROFILE").unwrap_or_default();
648            let appdata = std::env::var("APPDATA").unwrap_or_default();
649            vec![
650                format!("{}/.local/bin/{}.exe", userprofile, tool),
651                format!("{}/syncable-cli/bin/{}.exe", appdata, tool),
652                format!("C:/Program Files/{}/{}.exe", tool, tool),
653            ]
654        } else {
655            // Unix-specific paths
656            let home = std::env::var("HOME").unwrap_or_default();
657            vec![
658                format!("{}/go/bin/{}", home, tool),
659                format!("{}/.local/bin/{}", home, tool),
660                format!("{}/.cargo/bin/{}", home, tool),
661            ]
662        };
663        
664        for path in alternative_paths {
665            if let Ok(output) = Command::new(&path).arg(version_arg).output() {
666                if output.status.success() {
667                    return true;
668                }
669            }
670        }
671        
672        false
673    }
674    
675    /// Test if a tool is available by running version command (public method for external use)
676    pub fn test_tool_availability(&self, tool: &str) -> bool {
677        self.is_tool_installed(tool)
678    }
679    
680    /// Get installation status summary
681    pub fn get_tool_status(&self) -> HashMap<String, bool> {
682        self.installed_tools.clone()
683    }
684    
685    /// Print tool installation status
686    pub fn print_tool_status(&self, languages: &[Language]) {
687        println!("\nšŸ”§ Vulnerability Scanning Tools Status:");
688        println!("{}", "=".repeat(50));
689        
690        for language in languages {
691            let (tool, status) = match language {
692                Language::Rust => ("cargo-audit", self.installed_tools.get("cargo-audit").unwrap_or(&false)),
693                Language::JavaScript | Language::TypeScript => ("npm", self.installed_tools.get("npm").unwrap_or(&false)),
694                Language::Python => ("pip-audit", self.installed_tools.get("pip-audit").unwrap_or(&false)),
695                Language::Go => ("govulncheck", self.installed_tools.get("govulncheck").unwrap_or(&false)),
696                Language::Java | Language::Kotlin => ("grype", self.installed_tools.get("grype").unwrap_or(&false)),
697                _ => continue,
698            };
699            
700            let status_icon = if *status { "āœ…" } else { "āŒ" };
701            println!("  {} {:?}: {} {}", status_icon, language, tool, if *status { "installed" } else { "missing" });
702        }
703        println!();
704    }
705}