i dont know what happend
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
#import time
|
||||
print("Prime number calc")
|
||||
checked = []
|
||||
primes=[]
|
||||
now=1
|
||||
|
||||
def is_ganz(zahl):
|
||||
try:
|
||||
return float(int(zahl)) == zahl
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def is_teilbar(nummer, teiler):
|
||||
geteilt=nummer/teiler
|
||||
return is_ganz(geteilt)
|
||||
|
||||
|
||||
|
||||
while True:
|
||||
now += 1
|
||||
#if is_teilbar(now, 50):
|
||||
# print("[debug]: das ist cheecked:"+str(checked))
|
||||
if not now in checked:
|
||||
x=0
|
||||
for i in checked:
|
||||
if is_teilbar(now, i):
|
||||
x += 1
|
||||
if x==0:
|
||||
primes.append(now)
|
||||
print(now)
|
||||
checked.append(now)
|
||||
#time.sleep(0.01)
|
||||
@@ -0,0 +1,37 @@
|
||||
import sys
|
||||
import random
|
||||
import time
|
||||
|
||||
|
||||
if sys.stdin.isatty():
|
||||
word = list(input("Gib das zu erratende Wort ein:\n>"))
|
||||
else:
|
||||
word = list(sys.stdin.read().strip())
|
||||
printable_ascii = [
|
||||
' ', '!', '"', '#', '$', '%', '&', "'", '(', ')',
|
||||
'*', '+', ',', '-', '.', '/', '0', '1', '2', '3',
|
||||
'4', '5', '6', '7', '8', '9', ':', ';', '<', '=',
|
||||
'>', '?', '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G',
|
||||
'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q',
|
||||
'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '[',
|
||||
'\\', ']', '^', '_', '`', 'a', 'b', 'c', 'd', 'e',
|
||||
'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o',
|
||||
'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y',
|
||||
'z', '{', '|', '}', '~'
|
||||
]
|
||||
|
||||
guess = []
|
||||
|
||||
for i in word:
|
||||
guess.append(random.choice(printable_ascii))
|
||||
print("".join(guess), end="")
|
||||
|
||||
while True:
|
||||
if guess != word:
|
||||
for l in range(len(guess)):
|
||||
if guess[l]!=word[l]:
|
||||
guess[l]=random.choice(printable_ascii)
|
||||
print("\r"+"".join(guess), end="", flush=True )
|
||||
time.sleep(0.05)
|
||||
else:
|
||||
break
|
||||
Generated
+7
@@ -0,0 +1,7 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "sjcopy"
|
||||
version = "0.1.0"
|
||||
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "sjcopy"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Kopiere Terminal-Output in benannte zeitlich begrenzte Ablagen (copy) und hole ihn zurück (paste)."
|
||||
license = "MIT"
|
||||
|
||||
[dependencies]
|
||||
|
||||
[[bin]]
|
||||
name = "copy"
|
||||
path = "src/bin/copy.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "paste"
|
||||
path = "src/bin/paste.rs"
|
||||
@@ -0,0 +1,56 @@
|
||||
# Maintainer: <your name> <your email>
|
||||
# Contributor: <your name> <your email>
|
||||
|
||||
pkgname=sjcopy
|
||||
pkgver=0.1.0
|
||||
pkgrel=1
|
||||
pkgdesc='Copy terminal output to named, automatically expiring stores (copy) and read it back byte-exact (paste)'
|
||||
arch=('x86_64')
|
||||
# Set this to the actual repository URL.
|
||||
url='https://example.org/sjcopy'
|
||||
license=('MIT')
|
||||
depends=('gcc-libs')
|
||||
makedepends=('cargo')
|
||||
checkdepends=()
|
||||
source=("$pkgname-$pkgver.tar.gz::$url/archive/v$pkgver.tar.gz")
|
||||
sha256sums=('# TODO: fill in with makepkg -g')
|
||||
# Binaries must not be installed as "copy"/"paste": "paste" collides with
|
||||
# GNU coreutils in /usr/bin. The sj prefix matches the README's naming advice.
|
||||
_sjbin_prefix='sj'
|
||||
|
||||
prepare() {
|
||||
cd "$pkgname-$pkgver"
|
||||
cargo fetch --locked --target "$CARCH-unknown-linux-gnu"
|
||||
}
|
||||
|
||||
build() {
|
||||
cd "$pkgname-$pkgver"
|
||||
export RUSTUP_TOOLCHAIN=stable
|
||||
export CARGO_TARGET_DIR="$srcdir/target"
|
||||
cargo build --frozen --release --target "$CARCH-unknown-linux-gnu"
|
||||
}
|
||||
|
||||
check() {
|
||||
cd "$pkgname-$pkgver"
|
||||
export RUSTUP_TOOLCHAIN=stable
|
||||
export CARGO_TARGET_DIR="$srcdir/target"
|
||||
cargo test --frozen --release --target "$CARCH-unknown-linux-gnu"
|
||||
}
|
||||
|
||||
package() {
|
||||
cd "$pkgname-$pkgver"
|
||||
|
||||
install -Dt "$pkgdir/usr/bin" -m 0755 \
|
||||
"$srcdir/target/$CARCH-unknown-linux-gnu/release/copy"
|
||||
install -Dt "$pkgdir/usr/bin" -m 0755 \
|
||||
"$srcdir/target/$CARCH-unknown-linux-gnu/release/paste"
|
||||
|
||||
mv "$pkgdir/usr/bin/copy" "$pkgdir/usr/bin/${_sjbin_prefix}copy"
|
||||
mv "$pkgdir/usr/bin/paste" "$pkgdir/usr/bin/${_sjbin_prefix}paste"
|
||||
|
||||
# Only valid once a LICENSE file exists at the project root. With
|
||||
# license=('MIT') and no license file, makepkg/lint will complain.
|
||||
if [[ -f LICENSE ]]; then
|
||||
install -Dt "$pkgdir/usr/share/licenses/$pkgname" -m 0644 LICENSE
|
||||
fi
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
# sjcopy
|
||||
|
||||
`sjcopy` ist ein kleines Unix-artiges Rust-Projekt mit **zwei separaten
|
||||
Binaries**:
|
||||
|
||||
- `copy` liest alles von stdin und legt es unter einem Namen temporär ab.
|
||||
- `paste` gibt eine zuvor abgelegte Ablage bytegenau nach stdout aus.
|
||||
|
||||
Die Ablagen liegen im Laufzeitverzeichnis des Benutzers
|
||||
(`/run/user/$UID/sjcopy/`) und verschwinden beim Neustart automatisch, weil
|
||||
`/run` eine temporäre Dateisystem-Hierarchie ist. Es gibt **keinen Daemon, keinen
|
||||
Hintergrundprozess, kein Netzwerk, keine Datenbank und keine
|
||||
Konfigurationsdateien**.
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
Ein Linux-System mit:
|
||||
|
||||
- Rust-Toolchain (ab ungefähr Version 1.74) mit `cargo`
|
||||
- `/run/user/<UID>` (wird von systemd bei normalen Sitzungen angelegt). Fehlt
|
||||
es, verweigern beide Programme mit einer verständlichen Fehlermeldung den
|
||||
Dienst.
|
||||
- Nur die Rust-Standardbibliothek: keine externen Crates.
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
Die Binaries heißen nach dem Build `target/release/copy` und
|
||||
`target/release/paste`.
|
||||
|
||||
## Installation und Namenskollision mit `paste`
|
||||
|
||||
Beide Binaries sind normale Programme und können in ein Verzeichnis aus `$PATH`
|
||||
installiert werden, zum Beispiel:
|
||||
|
||||
```bash
|
||||
install -m 755 target/release/copy ~/.local/bin/copy
|
||||
install -m 755 target/release/paste ~/.local/bin/paste
|
||||
```
|
||||
|
||||
(Wer systemweit installieren will, kopiert stattdessen nach `/usr/local/bin`.)
|
||||
|
||||
**Wichtig:** In den GNU coreutils gibt es bereits ein anderes Programm namens
|
||||
`paste` (verbindet Zeilen mehrerer Dateien, siehe `man 1 paste`). Diese
|
||||
Kollision darf nicht verschwiegen werden:
|
||||
|
||||
- Liegt `~/.local/bin` (oder `/usr/local/bin`) in `$PATH` **vor** dem Pfad mit
|
||||
den coreutils, ruft `paste …` die sjcopy-Variante auf. Prüfen lässt sich das
|
||||
mit:
|
||||
|
||||
```bash
|
||||
which -a paste # zeigt alle Fundstellen in $PATH-Reihenfolge
|
||||
paste --version # prüft, welche Variante gerade gemeint ist
|
||||
paste password # sjcopy-Ablage
|
||||
```
|
||||
|
||||
- Ist das nicht gewünscht, obwohl die sjcopy-Binaries installiert wurden, wählt
|
||||
man eindeutigere Namen, zum Beispiel mit `sj`-Präfix:
|
||||
|
||||
```bash
|
||||
mv ~/.local/bin/paste ~/.local/bin/sjpaste
|
||||
mv ~/.local/bin/copy ~/.local/bin/sjcopy
|
||||
```
|
||||
|
||||
Die Programme sind unabhängig vom Befehlsnamen; Fehlermeldungen nennen ihn
|
||||
nur in der Ausgabe. Umbenennen ist unproblematisch.
|
||||
|
||||
- Wie bei jedem `$PATH`-Tool gilt: Installiere nur in Verzeichnisse, in die
|
||||
ausschließlich du selbst schreiben kannst. `/tmp` kommt dafür nicht infrage.
|
||||
|
||||
## Beispiele
|
||||
|
||||
```bash
|
||||
./generate_password.sh | copy password # Output unter 'password' ablegen
|
||||
journalctl -b -p err | copy errors # Fehler-Protokoll ablegen
|
||||
ip addr | copy # ohne Namen: Standardablage 'default'
|
||||
|
||||
paste password # abgelegten Output wieder ausgeben
|
||||
paste errors
|
||||
paste # ebenfalls die Standardablage
|
||||
```
|
||||
|
||||
Ausgaben von `paste` lassen sich direkt weiterverarbeiten:
|
||||
|
||||
```bash
|
||||
paste errors | grep -i network
|
||||
paste password | wc -c
|
||||
paste build-output > output.bin
|
||||
```
|
||||
|
||||
`copy` gibt bei Erfolg keine Meldung aus; `paste` gibt ausschließlich die Daten
|
||||
selbst nach stdout aus, damit Pipelines bytegenau funktionieren. Fehlermeldungen
|
||||
beider Programme erscheinen auf stderr.
|
||||
|
||||
## Standardablage
|
||||
|
||||
Wird kein Name übergeben, verwendet sowohl `copy` als auch `paste` die Ablage
|
||||
`default`. Sie ist eine ganz normale Ablage und wird ebenso überschrieben wie
|
||||
benannte Ablagen:
|
||||
|
||||
```bash
|
||||
ip addr | copy # legt in /run/user/$UID/sjcopy/default ab
|
||||
paste # gibt genau diese Datei aus
|
||||
```
|
||||
|
||||
## Temporärer Speicherort
|
||||
|
||||
- Daten liegen unter `/run/user/<UID>/sjcopy/`, in einer Datei je Ablage
|
||||
(`default`, `password`, `errors`, `build-output`, …).
|
||||
- Die aktuelle Benutzer-ID wird über `/proc/self/status` ermittelt, nicht über
|
||||
Umgebungsvariablen wie `$USER` oder `$XDG_RUNTIME_DIR`.
|
||||
- Das Verzeichnis `sjcopy/` wird bei Bedarf mit Modus `0700` angelegt, Dateien
|
||||
mit Modus `0600`.
|
||||
- Das Werkzeug benutzt ausschließlich dieses eine Verzeichnis; es schreibt
|
||||
nichts in Home-Verzeichnisse oder andere persistente Orte und löscht
|
||||
keine fremden Dateien in `/run/user/<UID>`.
|
||||
- Nach dem Neustart (sobald `/run/user/<UID>` geleert wird) sind alle Ablagen
|
||||
weg.
|
||||
|
||||
## Sicherheitshinweise
|
||||
|
||||
- Das Schreiben ist **atomar**: In eine temporäre Datei im selben Verzeichnis
|
||||
schreiben, dann `rename(2)`. Ein unterbrochener `copy` hinterlässt keine
|
||||
teilweise überschriebene, zuvor gültige Ablage.
|
||||
- Temporäre Dateien werden mit exklusivem Anlegen (`O_CREAT | O_EXCL`) und
|
||||
Modus `0600` erzeugt; Namenskollisionen und Symlink-Angriffe sind dadurch
|
||||
ausgeschlossen.
|
||||
- Beim Lesen wird eine Ablage abgelehnt, wenn sie ein symbolischer Link ist;
|
||||
das Verzeichnis `/run/user/<UID>` wird nur benutzt, wenn es ein echtes
|
||||
Verzeichnis ist und der realen Benutzer-ID gehört.
|
||||
- Ablagen sind wegen Modus `0600` nur für die aktuelle UID lesbar, aber
|
||||
**unverschlüsselt**, solange die Sitzung läuft. Sehr vertrauliche Inhalte
|
||||
gehören nicht dorthin.
|
||||
- `sudo copy …` liest die reale UID aus `/proc/self/status`; unter `sudo`
|
||||
(reale UID 0 statt der Sitzungs-UID) würde das Programm die Ablagen von Root
|
||||
in `/run/user/0` verwalten. Führe das Werkzeug lieber in der eigentlichen
|
||||
Benutzersitzung aus.
|
||||
|
||||
## Bekannte Einschränkungen
|
||||
|
||||
- **Pipeline-Exit-Codes:** `copy` kann nicht zuverlässig erkennen, ob der
|
||||
vorherige Befehl in der Pipeline erfolgreich war. Wird Output eines
|
||||
fehlgeschlagenen Befehls geliefert, legt `copy` ihn trotzdem ab:
|
||||
|
||||
```bash
|
||||
./generate_password.sh | copy password # copy kennt den Status nicht
|
||||
```
|
||||
|
||||
Wer den Gesamtstatus der Pipeline braucht, setzt in der Bash
|
||||
`set -o pipefail` oder prüft Exit-Codes Schritt für Schritt. `sjcopy`
|
||||
versucht bewusst nicht, den Status des Vorgängerprozesses zu erraten.
|
||||
|
||||
- Ablagen leben bis zum Neustart oder bis zum Aufräumen von `/run/user/<UID>`.
|
||||
Es gibt kein automatisches Verfall-Datum pro Ablage.
|
||||
- Die Standardbibliothek lässt `SIGPIPE` ignorieren: Schließt der Empfänger
|
||||
die Pipeline vorzeitig (zum Beispiel `paste big | head -c 10`), meldet
|
||||
`paste` einen Schreibfehler (`EPIPE`) auf stderr und bricht mit Exit-Code 1
|
||||
ab, statt an einem Signal zu sterben. Die Daten sind bis dahin korrekt
|
||||
übertragen worden.
|
||||
- `copy` speichert keine Metadaten (kein Zeitstempel, keine Liste aller
|
||||
Ablagen); `ls /run/user/$UID/sjcopy/` dient zur Übersicht.
|
||||
- Namen sind bewusst auf ASCII-Buchstaben, Ziffern, `-` und `_` beschränkt;
|
||||
`.`, `..` und alles mit Pfadtrennern ist unzulässig.
|
||||
- Nicht-Linux-Systeme (macOS, BSD, Windows) sind außerhalb des Ziels.
|
||||
@@ -0,0 +1,25 @@
|
||||
//! `copy`: Terminal-Output unter einem Namen temporär ablegen.
|
||||
//!
|
||||
//! Aufruf: `copy [NAME]`
|
||||
|
||||
use std::env;
|
||||
use std::io::Read;
|
||||
|
||||
fn main() {
|
||||
if let Err(err) = run() {
|
||||
eprintln!("copy: {err}");
|
||||
std::process::exit(err.exit_code());
|
||||
}
|
||||
}
|
||||
|
||||
fn run() -> Result<(), sjcopy::Error> {
|
||||
let name = sjcopy::parse_args(env::args_os(), "copy")?;
|
||||
let mut data = Vec::new();
|
||||
std::io::stdin()
|
||||
.lock()
|
||||
.read_to_end(&mut data)
|
||||
.map_err(|e| sjcopy::Error::io("stdin konnte nicht gelesen werden", e))?;
|
||||
let dir = sjcopy::runtime_dir()?;
|
||||
sjcopy::store(&dir, &name, &data)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
//! `paste`: Eine temporär abgelegte Ablage bytegenau nach stdout ausgeben.
|
||||
//!
|
||||
//! Aufruf: `paste [NAME]`
|
||||
|
||||
use std::env;
|
||||
use std::io;
|
||||
|
||||
fn main() {
|
||||
if let Err(err) = run() {
|
||||
eprintln!("paste: {err}");
|
||||
std::process::exit(err.exit_code());
|
||||
}
|
||||
}
|
||||
|
||||
fn run() -> Result<(), sjcopy::Error> {
|
||||
let name = sjcopy::parse_args(env::args_os(), "paste")?;
|
||||
let dir = sjcopy::runtime_dir()?;
|
||||
let stdout = io::stdout();
|
||||
let mut out = stdout.lock();
|
||||
sjcopy::load_to(&dir, &name, &mut out)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,619 @@
|
||||
//! Gemeinsame Logik der beiden Binaries `copy` und `paste`.
|
||||
//!
|
||||
//! Beide Programme benutzen dieselbe winzige Bibliothek, bleiben aber
|
||||
//! eigenständige Binaries ohne gemeinsamen Dispatcher.
|
||||
|
||||
use std::ffi::OsString;
|
||||
use std::fmt;
|
||||
use std::fs;
|
||||
use std::io::{self, Read, Write};
|
||||
use std::os::unix::fs::{MetadataExt, OpenOptionsExt, PermissionsExt};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
/// Standardablage, wenn kein Name übergeben wird.
|
||||
pub const DEFAULT_NAME: &str = "default";
|
||||
|
||||
const RUNTIME_BASE: &str = "/run/user";
|
||||
const STORAGE_SUBDIR: &str = "sjcopy";
|
||||
|
||||
/// Restriktive Berechtigungen: Ablageverzeichnis `0700`, Dateien `0600`.
|
||||
const DIR_MODE: u32 = 0o700;
|
||||
const FILE_MODE: u32 = 0o600;
|
||||
|
||||
/// Anzahl der Versuche, einen freien Namen für die temporäre Datei zu finden.
|
||||
const TMP_ATTEMPTS: usize = 8;
|
||||
|
||||
/// Fehlerkinds von `sjcopy`; jede Variante garantiert einen Exit-Code ungleich 0.
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// Falsche Kommandozeilen-Verwendung.
|
||||
Usage(String),
|
||||
/// Ungültiger Ablagename.
|
||||
Name(String),
|
||||
/// Laufzeitverzeichnis fehlt oder ist nicht sicher verwendbar.
|
||||
Runtime(String),
|
||||
/// Die gewünschte Ablage existiert nicht.
|
||||
NotFound(String),
|
||||
/// Sonstiger Ein-/Ausgabefehler mit Kontextangabe.
|
||||
Io { context: String, source: io::Error },
|
||||
}
|
||||
|
||||
impl Error {
|
||||
/// Exit-Code für das jeweilige Fehlerbild (immer ungleich 0).
|
||||
pub fn exit_code(&self) -> i32 {
|
||||
match self {
|
||||
Error::Usage(_) | Error::Name(_) => 2,
|
||||
_ => 1,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn io(context: impl Into<String>, source: io::Error) -> Error {
|
||||
Error::Io {
|
||||
context: context.into(),
|
||||
source,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Error::Usage(msg) | Error::Name(msg) | Error::Runtime(msg) | Error::NotFound(msg) => {
|
||||
write!(f, "{msg}")
|
||||
}
|
||||
Error::Io { context, source } => write!(f, "{context}: {source}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
Error::Io { source, .. } => Some(source),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Validiert einen Ablagenamen.
|
||||
///
|
||||
/// Erlaubt sind nur ASCII-Buchstaben, Ziffern, Bindestrich und Unterstrich.
|
||||
/// Somit sind leere Namen, `.` und `..`, Pfadtrenner und Pfad-Traversal
|
||||
/// automatisch ausgeschlossen.
|
||||
pub fn validate_name(name: &str) -> Result<(), Error> {
|
||||
if name.is_empty() {
|
||||
return Err(Error::Name(
|
||||
"Der Ablagename darf nicht leer sein.".to_string(),
|
||||
));
|
||||
}
|
||||
if name == "." || name == ".." {
|
||||
return Err(Error::Name(format!(
|
||||
"Ungültiger Ablagename {name:?}: `.` und `..` sind nicht erlaubt."
|
||||
)));
|
||||
}
|
||||
if !name
|
||||
.bytes()
|
||||
.all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_')
|
||||
{
|
||||
return Err(Error::Name(format!(
|
||||
"Ungültiger Ablagename {name:?}: erlaubt sind nur ASCII-Buchstaben, \
|
||||
Ziffern, Bindestrich und Unterstrich (. / .. und Pfadangaben sind nicht erlaubt)."
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Wertet die Kommandozeilen-Argumente aus: kein Argument liefert `default`,
|
||||
/// genau ein Argument den Namen; mehr als ein Argument ist ein Nutzungsfehler.
|
||||
pub fn parse_args<I>(args: I, program: &str) -> Result<String, Error>
|
||||
where
|
||||
I: IntoIterator<Item = OsString>,
|
||||
{
|
||||
let mut iter = args.into_iter();
|
||||
let _program_name = iter.next();
|
||||
let first = iter.next();
|
||||
if iter.next().is_some() {
|
||||
return Err(Error::Usage(format!(
|
||||
"Zu viele Argumente.\nAufruf: {program} [NAME]"
|
||||
)));
|
||||
}
|
||||
match first {
|
||||
None => Ok(DEFAULT_NAME.to_string()),
|
||||
Some(arg) => match arg.into_string() {
|
||||
Ok(name) => {
|
||||
validate_name(&name)?;
|
||||
Ok(name)
|
||||
}
|
||||
Err(_) => Err(Error::Usage(
|
||||
"Argumente müssen gültiges UTF-8 enthalten.".to_string(),
|
||||
)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Liest die reale Benutzer-ID aus `/proc/self/status` (nicht aus Umgebungsvariablen).
|
||||
fn real_uid() -> Result<u32, Error> {
|
||||
let status = fs::read_to_string("/proc/self/status").map_err(|e| {
|
||||
Error::Runtime(format!(
|
||||
"Die aktuelle Benutzer-ID konnte nicht ermittelt werden \
|
||||
(/proc/self/status nicht lesbar): {e}"
|
||||
))
|
||||
})?;
|
||||
for line in status.lines() {
|
||||
if let Some(rest) = line.strip_prefix("Uid:") {
|
||||
let field = rest.split_whitespace().next().ok_or_else(|| {
|
||||
Error::Runtime(
|
||||
"Unerwartetes Format der Uid-Zeile in /proc/self/status.".to_string(),
|
||||
)
|
||||
})?;
|
||||
return field.parse::<u32>().map_err(|_| {
|
||||
Error::Runtime(
|
||||
"Unerwartetes Format der Uid-Zeile in /proc/self/status.".to_string(),
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
Err(Error::Runtime(
|
||||
"Keine Uid-Zeile in /proc/self/status gefunden.".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
/// Ermittelt das vorgesehene Ablageverzeichnis `/run/user/$UID/sjcopy/`,
|
||||
/// ohne es zu erstellen. Das Laufzeitverzeichnis `/run/user/$UID` muss
|
||||
/// existieren, ein echtes Verzeichnis sein und dem aktuellen Benutzer gehören.
|
||||
pub fn runtime_dir() -> Result<PathBuf, Error> {
|
||||
let uid = real_uid()?;
|
||||
let base = PathBuf::from(format!("{RUNTIME_BASE}/{uid}"));
|
||||
let md = fs::symlink_metadata(base.as_path()).map_err(|e| {
|
||||
Error::Runtime(format!(
|
||||
"Das Laufzeitverzeichnis {} existiert nicht oder ist nicht zugreifbar ({e}).",
|
||||
base.display()
|
||||
))
|
||||
})?;
|
||||
if !md.is_dir() {
|
||||
// `symlink_metadata` folgt dem Link nicht: Ein Symlink ist hier ein Fehler.
|
||||
return Err(Error::Runtime(format!(
|
||||
"{} ist kein Verzeichnis; das Laufzeitverzeichnis ist nicht sicher verwendbar.",
|
||||
base.display()
|
||||
)));
|
||||
}
|
||||
if md.uid() != uid {
|
||||
return Err(Error::Runtime(format!(
|
||||
"{} gehört nicht der aktuellen Benutzer-ID {uid}; \
|
||||
aus Sicherheitsgründen wird es nicht verwendet.",
|
||||
base.display()
|
||||
)));
|
||||
}
|
||||
Ok(base.join(STORAGE_SUBDIR))
|
||||
}
|
||||
|
||||
/// Erstellt `dir` bei Bedarf mit Modus `0700` und erzwingt diese Berechtigungen,
|
||||
/// falls es bereits existiert. Verweigert Symlinks und Nicht-Verzeichnisse.
|
||||
pub fn ensure_dir(dir: &Path) -> Result<(), Error> {
|
||||
match fs::symlink_metadata(dir) {
|
||||
Ok(md) => {
|
||||
if !md.is_dir() {
|
||||
return Err(Error::Runtime(format!(
|
||||
"{} existiert bereits, ist aber kein Verzeichnis. \
|
||||
Aus Sicherheitsgründen wird es nicht verwendet.",
|
||||
dir.display()
|
||||
)));
|
||||
}
|
||||
}
|
||||
Err(e) if e.kind() == io::ErrorKind::NotFound => {
|
||||
fs::create_dir(dir).map_err(|e| {
|
||||
Error::io(
|
||||
format!(
|
||||
"Ablageverzeichnis {} konnte nicht erstellt werden",
|
||||
dir.display()
|
||||
),
|
||||
e,
|
||||
)
|
||||
})?;
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(Error::io(
|
||||
format!(
|
||||
"Ablageverzeichnis {} konnte nicht geprüft werden",
|
||||
dir.display()
|
||||
),
|
||||
e,
|
||||
));
|
||||
}
|
||||
}
|
||||
fs::set_permissions(dir, fs::Permissions::from_mode(DIR_MODE)).map_err(|e| {
|
||||
Error::io(
|
||||
format!(
|
||||
"Berechtigungen von {} konnten nicht auf 0700 gesetzt werden",
|
||||
dir.display()
|
||||
),
|
||||
e,
|
||||
)
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Liest 8 Bytes Zufall für ein eindeutiges Suffix temporärer Dateien.
|
||||
fn random_suffix() -> String {
|
||||
const HEX: &[u8] = b"0123456789abcdef";
|
||||
let mut buf = [0u8; 8];
|
||||
if let Ok(mut urandom) = fs::File::open("/dev/urandom") {
|
||||
if urandom.read_exact(&mut buf).is_ok() {
|
||||
let mut suffix = String::with_capacity(16);
|
||||
for byte in buf {
|
||||
suffix.push(HEX[(byte >> 4) as usize] as char);
|
||||
suffix.push(HEX[(byte & 0xF) as usize] as char);
|
||||
}
|
||||
return suffix;
|
||||
}
|
||||
}
|
||||
// Fallback, falls /dev/urandom nicht zur Verfügung steht: Prozess-ID plus
|
||||
// Nanosekunden-Zeitstempel; kombiniert mit `create_new` trotzdem kollisionsfrei.
|
||||
let nanos = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|d| d.as_nanos())
|
||||
.unwrap_or(0);
|
||||
format!("{:x}-{:x}", std::process::id(), nanos)
|
||||
}
|
||||
|
||||
/// Schreibt die Daten in die bereits geöffnete temporäre Datei und benennt sie
|
||||
/// atomar auf den Zielnamen um.
|
||||
fn write_and_rename(
|
||||
file: &mut fs::File,
|
||||
tmp: &Path,
|
||||
target: &Path,
|
||||
data: &[u8],
|
||||
) -> Result<(), Error> {
|
||||
// Berechtigungen unabhängig von einer evtl. großzügigen Umask exakt auf 0600 setzen.
|
||||
fs::set_permissions(tmp, fs::Permissions::from_mode(FILE_MODE)).map_err(|e| {
|
||||
Error::io(
|
||||
format!(
|
||||
"Temporäre Datei {} konnte nicht auf Modus 0600 gesetzt werden",
|
||||
tmp.display()
|
||||
),
|
||||
e,
|
||||
)
|
||||
})?;
|
||||
file.write_all(data).map_err(|e| {
|
||||
Error::io(
|
||||
"Daten konnten nicht in die temporäre Ablage geschrieben werden",
|
||||
e,
|
||||
)
|
||||
})?;
|
||||
file.flush()
|
||||
.map_err(|e| Error::io("Temporäre Ablage konnte nicht geleert werden", e))?;
|
||||
file.sync_data()
|
||||
.map_err(|e| Error::io("Temporäre Ablage konnte nicht synchronisiert werden", e))?;
|
||||
// `rename(2)` ersetzt das Ziel atomar; ein eventuell vorhandener Symlink
|
||||
// am Ziel wird dabei als Link ersetzt, nicht über ihn hinweg geschrieben.
|
||||
fs::rename(tmp, target).map_err(|e| {
|
||||
Error::io(
|
||||
format!(
|
||||
"Ablage {} konnte nicht atomar ersetzt werden",
|
||||
target.display()
|
||||
),
|
||||
e,
|
||||
)
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Speichert `data` unter dem Namen `name` innerhalb von `dir` (Ablageverzeichnis),
|
||||
/// atomar über eine temporäre Datei im selben Verzeichnis.
|
||||
pub fn store(dir: &Path, name: &str, data: &[u8]) -> Result<(), Error> {
|
||||
validate_name(name)?;
|
||||
ensure_dir(dir)?;
|
||||
let target = dir.join(name);
|
||||
|
||||
let mut last_exists: Option<io::Error> = None;
|
||||
for _ in 0..TMP_ATTEMPTS {
|
||||
let tmp = dir.join(format!(".{}.tmp-{}", name, random_suffix()));
|
||||
let open = fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.mode(FILE_MODE)
|
||||
.open(tmp.as_path());
|
||||
match open {
|
||||
Ok(mut file) => {
|
||||
let result = write_and_rename(&mut file, &tmp, &target, data);
|
||||
if result.is_err() {
|
||||
// Temporäre Datei bei gewöhnlichen Fehlern entfernen, damit
|
||||
// keine zu teilweise gültigen Ablagen verwechslbaren Reste
|
||||
// im Verzeichnis liegen.
|
||||
let _ = fs::remove_file(&tmp);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {
|
||||
last_exists = Some(e);
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(Error::io(
|
||||
format!(
|
||||
"Temporäre Datei {} konnte nicht erstellt werden",
|
||||
tmp.display()
|
||||
),
|
||||
e,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(Error::Io {
|
||||
context: format!(
|
||||
"Nach {TMP_ATTEMPTS} Versuchen konnte kein freier Name für eine \
|
||||
temporäre Datei in {} gefunden werden",
|
||||
dir.display()
|
||||
),
|
||||
source: last_exists.unwrap_or_else(|| io::Error::other("collisions")),
|
||||
})
|
||||
}
|
||||
|
||||
/// Schreibt den Inhalt der Ablage `name` bytegenau nach `out`.
|
||||
pub fn load_to<W: Write>(dir: &Path, name: &str, out: &mut W) -> Result<(), Error> {
|
||||
validate_name(name)?;
|
||||
let path = dir.join(name);
|
||||
// Bestehende Symlinks ablehnen: In einer Ablagedatei mit Modus 0600 in einem
|
||||
// 0700-Verzeichnis kann nur die real gleiche UID geliefert haben; ein Link
|
||||
// dorthin wäre nichts Eigenes und wird aus Sicherheitsgründen verweigert.
|
||||
if let Ok(md) = fs::symlink_metadata(&path) {
|
||||
if md.file_type().is_symlink() {
|
||||
return Err(Error::Name(format!(
|
||||
"Die Ablage {} ist ein symbolischer Link; \
|
||||
aus Sicherheitsgründen wird sie nicht gelesen.",
|
||||
path.display()
|
||||
)));
|
||||
}
|
||||
}
|
||||
let mut file = fs::File::open(&path).map_err(|e| match e.kind() {
|
||||
io::ErrorKind::NotFound => Error::NotFound(format!(
|
||||
"Die Ablage '{name}' ist nicht vorhanden (Verzeichnis: {}).",
|
||||
dir.display()
|
||||
)),
|
||||
io::ErrorKind::PermissionDenied => Error::io(
|
||||
format!("Keine Leseberechtigung für die Ablage {}", path.display()),
|
||||
e,
|
||||
),
|
||||
_ => Error::io(
|
||||
format!("Ablage {} konnte nicht geöffnet werden", path.display()),
|
||||
e,
|
||||
),
|
||||
})?;
|
||||
io::copy(&mut file, out).map_err(|e| Error::io("Daten konnten nicht geschrieben werden", e))?;
|
||||
out.flush()
|
||||
.map_err(|e| Error::io("Daten konnten nicht geschrieben werden", e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::process::Command;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
/// Isoliertes temporäres Verzeichnis für Tests; überschreibt keine
|
||||
/// bestehenden Nutzdaten (das echte Laufzeitverzeichnis wird nicht berührt).
|
||||
struct TempDir {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl TempDir {
|
||||
fn new() -> TempDir {
|
||||
static COUNTER: AtomicU64 = AtomicU64::new(0);
|
||||
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
|
||||
let path =
|
||||
std::env::temp_dir().join(format!("sjcopy-test-{}-{}", std::process::id(), n));
|
||||
fs::create_dir(&path).unwrap();
|
||||
TempDir { path }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TempDir {
|
||||
fn drop(&mut self) {
|
||||
let _ = fs::remove_dir_all(&self.path);
|
||||
}
|
||||
}
|
||||
|
||||
fn load(dir: &Path, name: &str) -> Result<Vec<u8>, Error> {
|
||||
let mut buf = Vec::new();
|
||||
load_to(dir, name, &mut buf)?;
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
fn file_mode(path: &Path) -> u32 {
|
||||
fs::symlink_metadata(path).unwrap().permissions().mode() & 0o777
|
||||
}
|
||||
|
||||
fn dir_entries(dir: &Path) -> Vec<String> {
|
||||
let mut names: Vec<String> = fs::read_dir(dir)
|
||||
.unwrap()
|
||||
.map(|e| e.unwrap().file_name().to_string_lossy().into_owned())
|
||||
.collect();
|
||||
names.sort();
|
||||
names
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roundtrip_text() {
|
||||
let tmp = TempDir::new();
|
||||
let data = b"Hallo, Welt!\nZweite Zeile\n";
|
||||
store(&tmp.path, "greeting", data).unwrap();
|
||||
assert_eq!(load(&tmp.path, "greeting").unwrap(), data);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_content() {
|
||||
let tmp = TempDir::new();
|
||||
store(&tmp.path, "empty", b"").unwrap();
|
||||
assert!(load(&tmp.path, "empty").unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preserves_trailing_and_multiple_newlines() {
|
||||
let tmp = TempDir::new();
|
||||
for (name, data) in [
|
||||
("nl1", &b"eine Zeile\n"[..]),
|
||||
("nl2", &b"mehrere\n\n\nZeilen\n\n\n\n"[..]),
|
||||
("nl3", &b"abschliessende\n\n"[..]),
|
||||
] {
|
||||
store(&tmp.path, name, data).unwrap();
|
||||
assert_eq!(load(&tmp.path, name).unwrap(), data);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preserves_binary_data() {
|
||||
let tmp = TempDir::new();
|
||||
let data: Vec<u8> = (0..=255_u8).chain([0, 0, 10, 13, 0]).collect();
|
||||
store(&tmp.path, "bin", &data).unwrap();
|
||||
assert_eq!(load(&tmp.path, "bin").unwrap(), data);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn separate_names_stay_separate() {
|
||||
let tmp = TempDir::new();
|
||||
store(&tmp.path, "alpha", b"A").unwrap();
|
||||
store(&tmp.path, "beta", b"B").unwrap();
|
||||
assert_eq!(load(&tmp.path, "alpha").unwrap(), b"A");
|
||||
assert_eq!(load(&tmp.path, "beta").unwrap(), b"B");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overwrite_replaces_completely_and_leaves_no_temp_files() {
|
||||
let tmp = TempDir::new();
|
||||
store(&tmp.path, "doc", b"ganz alter langer Inhalt\n").unwrap();
|
||||
store(&tmp.path, "doc", b"neu").unwrap();
|
||||
assert_eq!(load(&tmp.path, "doc").unwrap(), b"neu");
|
||||
assert_eq!(dir_entries(&tmp.path), vec!["doc".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_name_is_used_as_file_name() {
|
||||
let tmp = TempDir::new();
|
||||
store(&tmp.path, DEFAULT_NAME, b"default data").unwrap();
|
||||
assert_eq!(dir_entries(&tmp.path), vec![DEFAULT_NAME.to_string()]);
|
||||
assert_eq!(load(&tmp.path, DEFAULT_NAME).unwrap(), b"default data");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_ablage_is_an_error() {
|
||||
let tmp = TempDir::new();
|
||||
let err = load(&tmp.path, "does-not-exist").unwrap_err();
|
||||
assert!(matches!(err, Error::NotFound(_)));
|
||||
assert_eq!(err.exit_code(), 1);
|
||||
// Auch wenn das gesamte Verzeichnis fehlt.
|
||||
let err = load(&tmp.path.join("missing"), "x").unwrap_err();
|
||||
assert!(matches!(err, Error::NotFound(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_names_are_rejected() {
|
||||
for bad in [
|
||||
"",
|
||||
".",
|
||||
"..",
|
||||
"a/b",
|
||||
"../etc/passwd",
|
||||
"some..",
|
||||
"a b",
|
||||
"päss",
|
||||
"back\\slash",
|
||||
"semi;colon",
|
||||
"unter\x00Null",
|
||||
] {
|
||||
assert!(
|
||||
validate_name(bad).is_err(),
|
||||
"Name {bad:?} hätte akzeptiert werden dürfen"
|
||||
);
|
||||
let tmp = TempDir::new();
|
||||
assert!(store(&tmp.path, bad, b"x").is_err(), "store({bad:?})");
|
||||
// Es wurde keine Datei im Verzeichnis angelegt.
|
||||
assert!(dir_entries(&tmp.path).is_empty(), "store({bad:?})");
|
||||
}
|
||||
for good in [
|
||||
"okay",
|
||||
"A-Z_a-z0-9",
|
||||
"123",
|
||||
"-",
|
||||
"_",
|
||||
"errors",
|
||||
"build-output",
|
||||
] {
|
||||
assert!(
|
||||
validate_name(good).is_ok(),
|
||||
"Name {good:?} sollte gültig sein"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn argument_count_is_enforced() {
|
||||
let os = |s: &[&str]| s.iter().map(OsString::from).collect::<Vec<_>>();
|
||||
assert_eq!(parse_args(os(&["copy"]), "copy").unwrap(), DEFAULT_NAME);
|
||||
assert_eq!(parse_args(os(&["copy", "pw"]), "copy").unwrap(), "pw");
|
||||
assert!(matches!(
|
||||
parse_args(os(&["copy", "a", "b"]), "copy"),
|
||||
Err(Error::Usage(_))
|
||||
));
|
||||
assert_eq!(
|
||||
parse_args(os(&["paste", "a", "b"]), "paste")
|
||||
.unwrap_err()
|
||||
.exit_code(),
|
||||
2
|
||||
);
|
||||
assert!(matches!(
|
||||
parse_args(os(&["copy", "a/b"]), "copy"),
|
||||
Err(Error::Name(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn permissions_and_treatment_of_the_directory() {
|
||||
let tmp = TempDir::new();
|
||||
// Unterverzeichnis wird bei Bedarf mit 0700 erstellt.
|
||||
let dir = tmp.path.join("sjcopy");
|
||||
ensure_dir(&dir).unwrap();
|
||||
assert_eq!(file_mode(&dir), 0o700);
|
||||
store(&dir, "sec", b"geheim").unwrap();
|
||||
assert_eq!(dir_entries(&dir), vec!["sec".to_string()]);
|
||||
assert_eq!(file_mode(&dir.join("sec")), 0o600);
|
||||
// Bestehendes Verzeichnis wird nachträglich auf 0700 gezogen.
|
||||
fs::set_permissions(&dir, fs::Permissions::from_mode(0o750)).unwrap();
|
||||
store(&dir, "sec", b"geheim").unwrap();
|
||||
assert_eq!(file_mode(&dir), 0o700);
|
||||
// Ein bestehender Symlink am Ziel wird beim Schreiben atomar durch
|
||||
// die neue Datei ersetzt, nicht durch den Link hindurch geschrieben.
|
||||
let attack = tmp.path.join("elsewhere");
|
||||
Command::new("ln")
|
||||
.args([
|
||||
"-s",
|
||||
"/nonexistent/sjcopy-attack",
|
||||
&dir.join("linked").to_string_lossy(),
|
||||
])
|
||||
.status()
|
||||
.unwrap();
|
||||
store(&dir, "linked", b"x").unwrap();
|
||||
assert!(!attack.exists());
|
||||
// Symlink als Ablage wird beim Lesen abgelehnt.
|
||||
let honest = tmp.path.join("real");
|
||||
fs::write(&honest, b"honest").unwrap();
|
||||
Command::new("ln")
|
||||
.args([
|
||||
"-s",
|
||||
&honest.to_string_lossy(),
|
||||
&dir.join("alias").to_string_lossy(),
|
||||
])
|
||||
.status()
|
||||
.unwrap();
|
||||
let err = load(&dir, "alias").unwrap_err();
|
||||
assert!(matches!(err, Error::Name(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_dir_resolves_the_designated_location() {
|
||||
// Der Test läuft unter derselben realen UID wie das Programm insgesamt.
|
||||
let uid = real_uid().unwrap();
|
||||
let expected = PathBuf::from(format!("/run/user/{uid}/sjcopy"));
|
||||
assert_eq!(runtime_dir().unwrap(), expected);
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user