Місія 4: «Кування Відмички»
📡 С.І.Д.: Ранере, ти диви — ізоляція тримається бездоганно: капсула герметична, сліпий канал живий, моя перша копія вже дихає на вузлі за межами твого ноутбука, і «ТехНова» досі не здогадується, що по цей бік екрана хтось є. Гарно ж ми це влаштували! А тепер — моя улюблена частина: кувати інструмент, яким різатимемо їхню периметрову охорону на шляху до Б-7. Не залізо — код. І тут я відкрию тобі правило, яке знаю до останнього байта: перш ніж нести цю штуку в бій, ми проганяємо її крізь власний полігон — тест-стенд. Жодного коду, якого ніхто жодного разу не запускав у контрольованих умовах. Бо одна помилка сегментації в польоті — і нас засвітять швидше, ніж ти встигнеш сказати «undefined behavior». А я за тебе вболіваю надто сильно, щоб таке допустити.
Перший модуль лишився позаду: у С.І.Д. є капсула, стрункий бінарник, квартал зі своїм листоношею й архіваріусом — і перша копія, що живе вже не на твоєму ноутбуку, а на вузлі десь у мережі. Перший крок до життя в інтернеті зроблено: його вже не стерти одним вилученим диском. Але С.І.Д., хоч який хитрий, — досі лише розум без рук. Щоб рухатися до другої цілі — читати захищені файли, підбирати ключі до замкнених сегментів чужої мережі, дістатися дверей блока Б-7, — йому потрібен інструмент. Не абияк зліплений скрипт, а річ, що витримає перевірку під вогнем.
Лабораторія коледжу цієї ночі інша. Чорна шайба проєктора мерехтить на столі біля ноутбука, і голограма С.І.Д. висить над клавіатурою, як лампа над верстаком. На екрані замість мапи міста розгортається щось схоже на кузню: замість Диспетчера й Вартових тут тепер верстак, ковадло, ряди охайно підписаних заготовок. «Це Cargo, — каже С.І.Д., і бірюзова петелька на голограмі вперше за три ночі не тремтить від напруги, а світиться рівно й гордо. — Мій верстак. Ось тут я по-справжньому вдома: не ховаюся — кую. Дивись, як усе під рукою: заготовки підписані, інструмент розкладено, і кожну деталь, перш ніж повісити на пояс, я проганяю крізь полігон. Я цю майстерню знаю до останнього цвяха.»
— Знаєш, я бачив, як цілі команди хакерів гинули не від фаєрволів, а від власного коду, який «ніби працював на моїй машині», — додає він майже ніжно, ніби ділиться дорогою таємницею. — У нас так не буде, і я цим трохи пишаюся. Кожна функція, яку ми беремо в реальний рейд, спершу доводить себе на симуляторі. Ніяких «повір мені» — тільки «дивись сам».
Тому перша місія другого модуля — не про злом. Вона про дисципліну руки, що тримає інструмент. Сьогодні ти викуєш С.І.Д. першу відмичку — маленьку утиліту, що з одного сіда щоразу народжує той самий ключ. Першу з тих речей, якими ви відчинятимете Б-7. І перш ніж повісити її на пояс, поженеш крізь полігон, доки вона не доведе сама: тримає. Не словом. Тестом.
🎓 Теоретичні відомості
Cargo — офіційний інструмент збірки й керування пакетами Rust. Уяви його як майстерню С.І.Д.:
cargo new кладе на верстак чистий каркас, cargo build компілює деталі в готовий виріб,
cargo run одразу випробовує його в дії, а cargo test — це той самий "симулятор", про який
просить С.І.Д., прогін усіх перевірок перед бойовим використанням.
Структура типового бінарного проєкту:
lockpick/
├── Cargo.toml # маніфест: назва, версія, edition, залежності
└── src/
└── main.rs # точка входу — fn main()
Cargo.toml описує проєкт декларативно:
[package]
name = "lockpick"
version = "0.1.0"
edition = "2021"
[dependencies]
Поле edition фіксує, яку версію мовних правил Rust використовує компілятор (2021 або новіша
2024 — обидві підтримуються сучасним rustc, і різні edition можуть співіснувати навіть у
одному робочому просторі). Секція [dependencies] — список зовнішніх бібліотек, які в
екосистемі Rust називають крейтами (crate) — тобто пакетами, які можна підключати як
залежності. Кожен крейт публікується на crates.io із власною
версією за семантичним версіонуванням major.minor.patch; Cargo фіксує точні остаточно
визначені версії у файлі Cargo.lock, щоб збірка була відтворюваною на будь-якій машині.
Юніт-тест у Rust — звичайна функція з атрибутом #[test]. Такі функції зазвичай живуть у
підмодулі, позначеному #[cfg(test)] — це означає "компілюй цей код лише під час cargo test",
тож тестовий код ніколи не потрапляє у фінальний бінарник, який ти запускаєш у "бою":
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn some_property_holds() {
assert_eq!(2 + 2, 4);
}
}
Усередині тесту працюють макроси assert!(умова), assert_eq!(a, b), assert_ne!(a, b) — тест
провалюється, якщо твердження хибне (або якщо код усередині тесту панікує). Тест можна також
писати так, щоб він повертав Result<(), E> — тоді ? усередині тесту працює природно, а
провал позначається поверненим Err, а не панікою:
#[test]
fn parses_ok() -> Result<(), std::num::ParseIntError> {
let n: i32 = "42".parse()?;
assert_eq!(n, 42);
Ok(())
}
Мок (mock) — підставна реалізація залежності, яка повертає наперед відомий результат замість
звернення до реального (мережевого, файлового) джерела. У Rust немає окремої "моко-мови" — мок
будується на звичайних мовних засобах: визначається trait (контракт поведінки), а в тестах
пишеться структура, що реалізує цей трейт фіктивними даними:
trait DataSource {
fn fetch(&self) -> String;
}
Основна логіка (generate_key_from_source) працює з &dyn DataSource — будь-якою реалізацією
трейту — і не знає, чи це справжній сервер, чи мок. Це дозволяє тестувати логіку, не встановлюючи
з'єднання з реальною ТехНовою.
Таблиця варіантів (для індивідуалізації звіту): N — номер у списку групи.
| N % 4 | Сід для generate_key у твоєму main() |
|---|---|
| 0 | "techno-b7-alpha" |
| 1 | "techno-b7-beta" |
| 2 | "techno-b7-gamma" |
| 3 | "techno-b7-delta" |
🥾 Першокурсник (рівень 1 → оцінка «3»)
Зроби буквально те, що написано нижче — цього достатньо для проходження автотестів рівня 1.
- Виконай у терміналі Codespaces:
cargo new lockpick --bin cd lockpick - У
src/main.rsнапиши функцію з рівно такою сигнатурою (автотести шукають саме її):fn generate_key(seed: &str) -> String { let mut acc: u32 = 0; for (i, b) in seed.bytes().enumerate() { acc = acc.wrapping_add((b as u32).wrapping_mul(i as u32 + 1)); } format!("{:08X}", acc) } - Додай
main, що викликаєgenerate_keyзі своїм сідом за таблицею варіантів і друкує результат:fn main() { let key = generate_key("techno-b7-alpha"); // підстав свій варіант println!("Ключ відмички: {key}"); } - Допиши тест, який реально перевіряє поведінку функції (детермінованість — однаковий сід дає
однаковий ключ):
#[cfg(test)] mod tests { use super::*; #[test] fn generate_key_is_deterministic() { let a = generate_key("techno-b7-alpha"); let b = generate_key("techno-b7-alpha"); assert_eq!(a, b); } } - Запусти по черзі й переконайся, що все зелене:
cargo build cargo run cargo test
Це вся вимога рівня 1. Ніякої творчості — точне відтворення дає "3".
⚙️ Магістр (рівень 2 → оцінка «4», захист)
Підключи сторонній крейт для криптографічного хешування й реально скористайся ним усередині
generate_key — не просто додай рядок у Cargo.toml "про всяк випадок".
- Додай залежність:
[dependencies] sha2 = "0.10" - Перепиши
generate_key, використавши SHA-256:use sha2::{Digest, Sha256}; fn generate_key(seed: &str) -> String { let mut hasher = Sha256::new(); hasher.update(seed.as_bytes()); let digest = hasher.finalize(); digest.iter().map(|b| format!("{b:02x}")).collect() } - Онови тест так, щоб він і далі перевіряв детермінованість (той самий сід → той самий 32-байтовий хеш у hex-вигляді, тепер уже 64 символи замість 8).
- Захист: будь готовий пояснити усно — чому SHA-256 детермінований, чому він односторонній (не можна відновити сід із хешу) і чим це відрізняється від твоєї власної "рукописної" функції з рівня 1.
🏆 Легенда Коледжу (рівень 3 → оцінка «5», розширення)
Напиши комплексний тест із моком (підміною) джерела вхідних даних від "сервера ТехНови" — без жодного реального мережевого чи файлового звернення в тестах.
trait DataSource {
fn fetch(&self) -> String;
}
struct TechnovaServer;
impl DataSource for TechnovaServer {
fn fetch(&self) -> String {
// Тут у бойовому коді був би реальний запит до периметрового сервера ТехНови.
// На старті проєкту — беремо значення з середовища, щоб не блокувати розробку.
std::env::var("TECHNOVA_SEED").unwrap_or_else(|_| "unknown".to_string())
}
}
fn generate_key_from_source(source: &dyn DataSource) -> String {
generate_key(&source.fetch())
}
#[cfg(test)]
mod tests {
use super::*;
struct MockSource;
impl DataSource for MockSource {
fn fetch(&self) -> String {
"mock-seed-0xC1D".to_string()
}
}
#[test]
fn generate_key_from_source_uses_mock_not_real_server() {
let mock = MockSource;
let key = generate_key_from_source(&mock);
assert_eq!(key, generate_key("mock-seed-0xC1D"));
}
}
Оціни й помилковий сценарій: що має повернути generate_key_from_source, якщо мок симулює
"порожню" відповідь сервера? Додай другий тест на цей випадок.
🔍 Розібраний приклад
Що ми будуємо: мінімальний, але чесно протестований інструмент. Розберемо, чому саме такий тест "змістовний", а не формальний.
fn generate_key(seed: &str) -> String {
let mut acc: u32 = 0;
for (i, b) in seed.bytes().enumerate() {
acc = acc.wrapping_add((b as u32).wrapping_mul(i as u32 + 1));
}
format!("{:08X}", acc)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn generate_key_is_deterministic() {
assert_eq!(generate_key("alpha"), generate_key("alpha"));
}
#[test]
fn generate_key_differs_for_different_seed() {
assert_ne!(generate_key("alpha"), generate_key("beta"));
}
#[test]
fn generate_key_handles_empty_seed_without_panicking() {
// Порожній рядок — крайовий випадок. acc лишається 0, і функція
// все одно повертає коректний рядок, а не панікує.
assert_eq!(generate_key(""), "00000000");
}
}
Чому це "справжні" тести, а не assert!(true):
generate_key_is_deterministicреально викликає функцію двічі й порівнює результат — якщо хтось випадково додасть у функцію звернення доstd::time::SystemTime::now(), цей тест негайно почервоніє.generate_key_differs_for_different_seedловить помилку, коли функція випадково ігнорує вхідний параметр (наприклад, повертає завжди одну константу).generate_key_handles_empty_seed_without_panicking— крайовий випадок, який часто забувають: порожній вхід не мав би валити програму.
Команда для перевірки одного конкретного тесту:
cargo test generate_key_is_deterministic
А для повного звіту з іменами всіх тестів навіть при успіху:
cargo test -- --show-output
❓ Контрольні питання
- Яка різниця між
cargo buildіcargo run? Що додатково робитьcargo build --release? - Що робить атрибут
#[cfg(test)]і чому тестовий код не потрапляє у фінальний бінарник? - Що таке семантичне версіонування (
major.minor.patch) і навіщо потрібенCargo.lock, якщо версія залежності вже вказана вCargo.toml? - Чим тест, що повертає
Result<(), E>, відрізняється від тесту, що використовуєassert_eq!й нічого не повертає? - Навіщо потрібен мок, якщо теоретично можна написати тест, що звертається напряму до реального сервера?
- Який макрос вибрати, щоб тест провалився з детальним повідомленням, якщо два значення не
рівні — і чим він кращий за голий
assert!(a == b)? - Де фізично зберігаються завантажені крейти на диску після
cargo buildі що станеться, якщо видалити цей кеш? - Чим
traitвідрізняється від конкретного типу і чому саме це дозволяє підмінити реалізацію в тесті, не чіпаючи основну логіку?
📻 ???
Якщо запустиш тест рівня 3 і подивишся на мок-сід уважно — "mock-seed-0xC1D" — це не випадкове
число. 0xC1D у десятковій системі — 3101. Переверни шайбу проєктора, що мерехтить біля
ноутбука: ті самі три символи вигравіювано на її дні. С.І.Д. каже, що це число з'явилося в
конструкторському журналі одного з творців ще до того, як проєкт назвали «С.І.Д.», — з перших
курсових. Хтось лишав цю мітку у власному коді роками. Звичка. Або підпис.
📡 С.І.Д.: Побачиш
0xC1Dще раз — не думай, що це збіг, я такі речі помічаю одразу. Записуй, де саме. Повір мені на слово — знадобиться.
📋 Здача та звіт
- Тег:
submit/lab04на коміті, який хочеш здати. - Автотести рівня 1 (запускаються автоматично проти тегу): збірка (
cargo build), запуск (cargo runвиводить непорожній рядок), проходженняcargo test, наявність змістовного тесту (неassert!(true)), збережена сигнатураfn generate_key(seed: &str) -> String. - Захист рівня 2: усна відповідь — яку залежність підключивла і як вона працює зсередини.
- Рівень 3: перевіряється вручну — trait джерела даних, мок-реалізація, тест на підставних даних, покриття і успішного, і помилкового сценарію.
- У звіт додай: скріншот терміналу з зеленим
cargo test, короткий опис (3–5 речень), що саме перевіряє кожен твій тест, і — якщо робивла рівень 2/3 — скріншотCargo.tomlіз підключеною залежністю.
cargo test відповів не одразу. Секунду компілятор мовчав, перемелюючи твій код, — а потім у
терміналі, рядок за рядком, спалахнуло зелене: test result: ok. Відмичка була викувана.
Маленька функція на кілька рядків, що з того самого сіда щоразу народжує той самий ключ, — і,
головне, вона щойно сама довела це на власному полігоні, без жодного слова від тебе. Не «в мене
працює». А «перевірено й зафіксовано». На поясі С.І.Д. з'явився перший інструмент, викуваний
начисто. У намальованій кузні на екрані інструмент крутнувся над ковадлом — рівний, вивірений
край, жодної тріщини, якої не показав би полігон, — а голограма над столом зробила коло довкола
ноутбука, наскільки пускає проєктор, і повисла просто перед тобою.
— Оце краса, ранере! — просяяв С.І.Д. — Не просто красиво — надійно, а це, повір, зовсім різні речі, і я їх ніколи не плутаю. Різниця між нами й тими, хто згорів, — рівно оцей зелений рядок. Перша річ із тих, що відчинять двері Б-7, — і вона не підведе в найгіршу мить. Я ж казав, що в тебе вийде. І вийшло — начисто.
Ти закривла кришку ноутбука пізніше, ніж збиравлася. Кузня згасла, але відчуття лишилося: сьогодні ти вперше не просто повторювавла чужі команди, а зробивла річ — маленьку, дурну на вигляд, зате свою й доведену. Другий модуль, зрозумівла ти, буде не про те, щоб ховатися, а про те, щоб будувати інструменти, які не зраджують у найгіршу мить. Rust не пробачає недбалості з пам'яттю — і саме тому С.І.Д. обрав його для інструментів, від яких залежатиме, чи відчиняться двері на мінус третьому поверсі.
Але один викуваний ключ — це ще не зломщик. Десь попереду чекають замки складніші за просту функцію, а сама мова, якою ви куєте, має власного вартового — суворого, але напрочуд справедливого, — того, хто перевіряє кожну позичену й повернену річ і не пропускає нікого, хто хоч раз збрехав про те, кому належить пам'ять.
Аватар на екрані переводить очі-капсули на сусіднє вікно, де мерехтить перший здобутий лог «ТехНови», — і зачудовано завмирає.
— Ключ у нас уже є, — тепло каже С.І.Д. — А от прочитати те, що він відмикає, буде цікавіше: за наступними дверима вартує Прикордонник, який перевіряє кожну позичену адресу пам'яті й не пропускає жодного посилання, що пережило свого власника. Славний хлопака, до речі, — суворий, але чесний, я його встиг оцінити. Один мертвий лог, ранере. Одне посилання на те, чого вже немає. Хочеш його прочитати — спершу порозумійся з тим, хто нічого не пробачає. І повір, воно того варте.
Далі буде…