Schreibe einen Streaming-Adapter, der eine Referenz auf einen Schlüssel enthält und diesen bitweise mit beliebigen Daten XOR-verknüpft.
XOR ist eine grundlegende binäre Operation: Für jedes Bit der Eingaben setzt du das entsprechende Bit der Ausgabe auf 1, wenn die Eingabebits unterschiedlich sind. Sind beide Eingaben 1 oder beide 0, dann ist das entsprechende Ausgabebit 0.
Wenn du ein Dokument mit einem Schlüssel XOR-verknüpfst, wird der Schlüssel so oft wie nötig wiederholt, sodass ein Ausgabedokument entsteht, das genauso lang ist wie das Eingabedokument.
Die XOR-Verknüpfung eines Dokuments mit einem Schlüssel wurde noch Anfang des 20. Jahrhunderts für die Kryptografie verwendet. Als Methode zum Verbergen von Daten ist das zwar längst überholt, aber überraschend nützlich, um verrauschte, scheinbar zufällige Daten zu erzeugen, ohne sich mit echtem Zufall herumschlagen zu müssen. In der modernen Kryptografie wird es für bestimmte Chiffren noch gelegentlich eingesetzt: Die Chiffre selbst ist nur ein Mechanismus, um einen sehr zufälligen, unendlich langen Schlüssel zu erzeugen, der mit dem Dokument XOR-verknüpft wird.
Eine interessante Eigenschaft der XOR-Verschlüsselung ist ihre Symmetrie: Wird eine beliebige Zahl mit sich selbst XOR-verknüpft, ergibt das 0, und wird eine beliebige Zahl mit 0 XOR-verknüpft, bleibt die eingegebene Zahl unverändert. Um ein XOR-verschlüsseltes Dokument zu entschlüsseln, verschlüsselst du es also einfach noch einmal mit demselben Schlüssel.
Es ist nicht praktikabel, einen Test zu schreiben, der sicherstellt, dass dein Struct eine Referenz auf den Schlüssel hält, statt ihn zu kopieren. Ebenso wenig ist es praktikabel, mit einem Test zu beweisen, dass weder munge noch munge_in_place noch ihre Hilfsfunktionen auf dem Heap alloziieren. Trotzdem solltest du versuchen, deine Lösung so zu schreiben.
Du musst ein struct Xorcism schreiben, das eine Referenz auf einen Schlüssel hält. Dieses Struct muss zwei Methoden bereitstellen: munge_in_place und munge. Erstere passt einen Byte-Puffer an Ort und Stelle an. Letztere ist ein Iterator-Adapter: Sie nimmt einen beliebigen Iterator mit Daten entgegen und gibt einen neuen Iterator mit Daten zurück.
Die Stub-Signaturen dieser Übung sind syntaktisch größtenteils korrekt, aber sie kompilieren nicht: Ein großer Teil des Sinns dieser Übung besteht darin, dass du dich mit der Verwendung von Lifetimes und Generics vertraut machst, also musst du sie selbst ausfüllen. Ein weiteres Ziel dieser Übung ist, dass du eine passende Aufteilung findest, mit der du beide Methoden mit möglichst wenig doppelter Arbeit implementieren kannst. Scheue dich nicht, zusätzliche Hilfsfunktionen einzuführen!
Diese Traits sind nützlich:
Diese Übung enthält Bonus-Tests, die hinter dem Feature-Flag io liegen. Um sie zu aktivieren, führe Folgendes aus
cargo test --features io
Für diese Tests musst du eine Methode reader mit der Signatur
fn reader(self, impl Read) -> impl Read
und eine Methode writer mit der Signatur
fn writer(self, impl Write) -> impl Write
implementieren. Diese Funktionen wandeln das Struct Xorcism jeweils in einen Stream-Adapter in die passende Richtung um. Sie verwenden diese Traits:
munge
Da du das Feature impl Trait verwendest, kann die Verwaltung von Lebensdauern beim Implementieren der Methode munge etwas knifflig werden. Vielleicht ist es für dich einfacher, eine eigene struct mit einer Iterator-Implementierung zu schreiben und diesen konkreten Typ zurückzugeben, zumindest für den Anfang. Letztendlich ist es eine gute Idee, die Lösung direkt mit Iterator-Kombinatoren umzusetzen.
Melde dich bei Exercism an, um Rust mit 99 Übungen und echtem menschlichen Mentoring zu lernen und zu meistern, alles kostenlos.