Programmieren in Rust

Schnittstellen zum Betriebssystem

Inhaltsverzeichnis

  1. Kommandos ausführen
  2. Pfade

Kommandos ausführen

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.

Pfade

Konkatenation

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());

Normalisierung

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:

Ü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);
}