Курс ЛР4 · Місія 4
Сюжет С.І.Д.↔ АкадемічнаСРС до ЛР⌨️ команди 7

Місія 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 → оцінка «3»)

Зроби буквально те, що написано нижче — цього достатньо для проходження автотестів рівня 1.

  1. Виконай у терміналі Codespaces:
    cargo new lockpick --bin
    cd lockpick
    
  2. У 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)
    }
    
  3. Додай main, що викликає generate_key зі своїм сідом за таблицею варіантів і друкує результат:
    fn main() {
        let key = generate_key("techno-b7-alpha"); // підстав свій варіант
        println!("Ключ відмички: {key}");
    }
    
  4. Допиши тест, який реально перевіряє поведінку функції (детермінованість — однаковий сід дає однаковий ключ):
    #[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);
        }
    }
    
  5. Запусти по черзі й переконайся, що все зелене:
    cargo build
    cargo run
    cargo test
    

Це вся вимога рівня 1. Ніякої творчості — точне відтворення дає "3".

РІВЕНЬ 2 · «4» · МАГІСТР

⚙️ Магістр (рівень 2 → оцінка «4», захист)

Підключи сторонній крейт для криптографічного хешування й реально скористайся ним усередині generate_key — не просто додай рядок у Cargo.toml "про всяк випадок".

  1. Додай залежність:
    [dependencies]
    sha2 = "0.10"
    
  2. Перепиши 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()
    }
    
  3. Онови тест так, щоб він і далі перевіряв детермінованість (той самий сід → той самий 32-байтовий хеш у hex-вигляді, тепер уже 64 символи замість 8).
  4. Захист: будь готовий пояснити усно — чому SHA-256 детермінований, чому він односторонній (не можна відновити сід із хешу) і чим це відрізняється від твоєї власної "рукописної" функції з рівня 1.
РІВЕНЬ 3 · «5» · ЛЕГЕНДА

🏆 Легенда Коледжу (рівень 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
❓ КОНТРОЛЬНІ ПИТАННЯ

❓ Контрольні питання

  1. Яка різниця між cargo build і cargo run? Що додатково робить cargo build --release?
  2. Що робить атрибут #[cfg(test)] і чому тестовий код не потрапляє у фінальний бінарник?
  3. Що таке семантичне версіонування (major.minor.patch) і навіщо потрібен Cargo.lock, якщо версія залежності вже вказана в Cargo.toml?
  4. Чим тест, що повертає Result<(), E>, відрізняється від тесту, що використовує assert_eq! й нічого не повертає?
  5. Навіщо потрібен мок, якщо теоретично можна написати тест, що звертається напряму до реального сервера?
  6. Який макрос вибрати, щоб тест провалився з детальним повідомленням, якщо два значення не рівні — і чим він кращий за голий assert!(a == b)?
  7. Де фізично зберігаються завантажені крейти на диску після cargo build і що станеться, якщо видалити цей кеш?
  8. Чим 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 не пробачає недбалості з пам'яттю — і саме тому С.І.Д. обрав його для інструментів, від яких залежатиме, чи відчиняться двері на мінус третьому поверсі.

Але один викуваний ключ — це ще не зломщик. Десь попереду чекають замки складніші за просту функцію, а сама мова, якою ви куєте, має власного вартового — суворого, але напрочуд справедливого, — того, хто перевіряє кожну позичену й повернену річ і не пропускає нікого, хто хоч раз збрехав про те, кому належить пам'ять.

⏭️ ДАЛІ БУДЕ

Аватар на екрані переводить очі-капсули на сусіднє вікно, де мерехтить перший здобутий лог «ТехНови», — і зачудовано завмирає.

— Ключ у нас уже є, — тепло каже С.І.Д. — А от прочитати те, що він відмикає, буде цікавіше: за наступними дверима вартує Прикордонник, який перевіряє кожну позичену адресу пам'яті й не пропускає жодного посилання, що пережило свого власника. Славний хлопака, до речі, — суворий, але чесний, я його встиг оцінити. Один мертвий лог, ранере. Одне посилання на те, чого вже немає. Хочеш його прочитати — спершу порозумійся з тим, хто нічого не пробачає. І повір, воно того варте.

Далі буде…

⌨️ КОМАНДИ НА ЦІЙ СТОРІНЦІ 7

Кожна команда терміналу з цієї сторінки — одним реченням. Позначка нове — команда зустрічається в курсі вперше; далі вважаємо її знайомою.

cargo new
Створює новий проєкт із Cargo.toml і src/main.rs (cargo new lockpick; --lib — бібліотека).
cargo
Система збірки й менеджер пакетів Rust: створює проєкт, тягне залежності (крейти), збирає, тестує, запускає.
cd
Переходить в інший каталог (change directory); cd .. — на рівень вище.
cargo build
Компілює проєкт у target/debug/; --release — з оптимізаціями у target/release/.
cargo run
Збирає й одразу запускає програму; аргументи після -- йдуть програмі (cargo run -- staff.log).
cargo test
Збирає й виконує тести #[test]; можна вказати ім'я тесту (cargo test назва_тесту) або -- --show-output.
rustc
Компілятор Rust; напряму викликається рідко (rustc --version, rustc --explain E0382 — розшифрувати помилку), зазвичай його запускає Cargo.

💡 Прочитати README прямо в терміналі: cat README.md (виведе весь файл) або less README.md (посторінково; вихід — клавіша q).
Довідка з будь-якої команди: man curl (повний посібник, вихід — q) або коротко curl --help; для підкоманд — docker run --help, cargo build --help.