↑Programmieren in Rust
Manchmal benötigt ein Programm zur Erledigung einer Aufgabe die Hilfe eines anderen Programms. In der modernen Programmierung sollte dieses andere Programm als Bibliothek vorliegen und eine streng typisierte öffentliche Schnittstelle anbieten, – das ist am effizientesten und sichersten. Bisweilen kommt aber auch der Weg zum Einsatz, das andere Programm als Kindprozess über das Betriebssystem aufrufen zu lassen. Ein großer Vorteil liegt hierbei darin, dass das andere Programm in einer beliebigen anderen Programmiersprache formuliert sein darf. Zudem ist das andere Programm abgekoppelt, womit man quasi dynamisches Laden bekommt. Nachteile sind ggf. weniger formal festgelegte Schnittstellen bis hin zu Benutzerschnittstellen die eigentlich nicht für die Verarbeitung durch Maschinen gedacht sind.
Die Standardbibliothek stellt im Modul std::process
einen Mechanismus zum Aufruf anderer Programme zur Verfügung.
Zur Erläuterung ein Beispiel.
Unter Linux gibt es so ein Programm date
.
Führt man dieses im Terminal aus, schreibt es das aktuelle
Datum und die aktuelle Uhrzeit in die Ausgabe.
Ein Aufruf als Kindprozess gestaltet sich folgendermaßen:
use std::process::Command; fn main() -> std::io::Result<()> { let mut child = Command::new("date").spawn()?; let _status = child.wait()?; Ok(()) }
Die Methode status
fasst spawn
und wait
zusammen:
let _status = Command::new("date").status()?;
Die Methode output
fängt stdout
und
stderr
auf und macht diese als Rückgabewert verfügbar.
let output = Command::new("date").output()?; let date = std::str::from_utf8(&output.stdout).unwrap(); println!("[{}]", date.trim());
Die Methode arg
legt ein Kommandozeilenargument
fest. Gibt es mehrere Argumente, ist ein Aufruf von arg
je Argument notwendig. Bspw. entspricht das Kommando
Werkzeug -x -y Datei.txt
dieser Verkettung:
Command::new("Werkzeug").arg("-x").arg("-y").arg("Datei.txt")
Alternativ steht dafür die Methode args
zur
Verfügung:
Command::new("Werkzeug").args(&["-x", "-y", "Datei.txt"])
Das Datum im Format Jahr-Monat-Tag
bekommt
man unter Linux bspw. so:
let output = Command::new("date").arg("+%Y-%m-%d").output()?; let s = std::str::from_utf8(&output.stdout).unwrap(); let date: Vec<&str> = s.trim().split("-").collect(); println!("{:?}", date);
Basteleien wie die folgende sind höchst fragwürdig:
use std::process::Command; use std::io::Write; fn main() -> std::io::Result<()> { loop { let mut s = String::new(); std::io::stdin().read_line(&mut s)?; let mut iter = s.trim().split_ascii_whitespace(); if let Some(cmd) = iter.next() { let output = Command::new(cmd).args(iter).output()?; std::io::stdout().write(&output.stdout)?; } } }
Der Nutzer erhält hiermit Zugriff auf alle möglichen Programme, womit das Benutzerkonto letztendlich kompromittierbar ist. Ein Angreifer könnte beispielsweise unbemerkt mit GET von irgendwoher ein Skript downloaden, welches bei jedem Login automatisch gestartet wird und für den Angreifer eine SSH-Verbindung aufbaut, womit dieser oder dessen Skripte in aller Ruhe unbemerkte Manipulationen vornehmen können. Eine solcher Angriff wird Command injection genannt und fällt unter den Obergriff Code injection.
Das Fazit lautet, dass Command
so restriktiv wie
möglich genutzt werden sollte und niemals unvalidierte
Zeichenketten bekommen darf.
Zur Verbindung von Pfadteilen zieht man PathBuf
heran. Das geht so:
use std::path::PathBuf; fn main() { let path = PathBuf::from_iter(["Pfad", "Texte", "Datei.txt"]); assert_eq!("Pfad/Texte/Datei.txt", path.to_str().unwrap()); }
Das Vorhandensein angefügter Schrägstriche macht dabei keinen Unterschied:
let path = PathBuf::from_iter(["Pfad/", "Texte/", "Datei.txt"]); assert_eq!("Pfad/Texte/Datei.txt", path.to_str().unwrap());
Ein angefügter Schrägstrich (tailing slash) macht bei Pfaden keinen semantischen Unterschied, wohl aber bei ihrer inneren Darstellung. So gilt folgendes:
use std::path::Path; fn main() { let path1 = Path::new("Pfad"); let path2 = Path::new("Pfad/"); assert_eq!(path1, path2); assert_ne!(path1.as_os_str(), path2.as_os_str()); }
Die Gleichheit von Pfaden ist insofern eine neue Äquivalenzrelation, die sich von der Gleichheit ihrer inneren Darstellung unterscheidet. Man kann das je nach Sichtweise als kontraintuitiv empfinden oder nicht.
Wie geht diese Gleichheit vonstatten? Das geschieht durch die
Funktion components
, die einen Pfad in seine
Bestandteile zerlegt und dabei eine Normalisierung vornimmt.
Die Normalisierung tut folgendes:
a//b
wie a/b
in die Bestandteile
a
und b
zerlegt wird.
.
werden ignoriert, sofern
sie nicht am Anfang des Pfades stehen. So bekommen auch
a/./b
und a/b/.
jeweils die Komponenten a
und b
zugeordnet.
a
und a/
äquivalent sind.
Übrigens greift neben der Implementierung für PartielEq
auch die für Hash
auf components
zurück.
So gilt folgendes:
use std::{collections::HashSet, path::Path}; fn main() { let mut s = HashSet::new(); s.insert(Path::new("Pfad")); s.insert(Path::new("Pfad/")); assert_eq!(s.len(), 1); }