i dont know what happend

This commit is contained in:
2026-10-22 19:10:14 +02:00
parent 81b56228d7
commit c6a3393830
11 changed files with 982 additions and 0 deletions
+33
View File
@@ -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)
+37
View File
@@ -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
+7
View File
@@ -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"
+16
View File
@@ -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"
+56
View File
@@ -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
}
+167
View File
@@ -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.
+25
View File
@@ -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(())
}
+22
View File
@@ -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(())
}
+619
View File
@@ -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.