4. Projeto: qualifique o Workspace Guard
O Workspace Guard cria snapshots de um diretório, verifica-os, compara manifestos e pode executar um processo filho somente após a verificação. O código separa comandos, manifesto canônico, modelo e varredura recursiva.
Qualificar importa mais que apenas compilar. Verifique e teste o projeto, crie duas vezes o snapshot de uma árvore fixa e compare os bytes. Altere um arquivo e confirme falha com o status documentado. Execute a partir de um path com espaço. Por fim, examine a limpeza em retornos antecipados e confirme a ordenação da iteração antes da serialização.
O projeto combina strings e coleções próprias, views emprestadas, erros de alocação, imports, filesystem, SHA-256, ambiente e processos sem concentrar todas as responsabilidades em um módulo.
Exemplo testado
Qualifique Workspace Guard como pacote: uso explícito, seleção da raiz, códigos de saída e ajuda determinística.
Trecho em foco
curriculum/main-evidence/projects/workspace_guard/src/main.fl:73–116
error snapshot_or_verify([]const string values, bool createSnapshot) { string root = none; stringv manifestPath = ""; if values.len() == 4 { error copyError; root, copyError = model::copy_text(values[2].view()); if copyError != none { return copyError; } manifestPath = values[3].view(); } else if values.len() == 3 { bool present; error environmentError; root, present, environmentError = environment::get(ROOT_ENVIRONMENT); if environmentError != none { return environmentError; } if !present or root.len() == 0 { fmt::eprint("workspace_guard: set "); fmt::eprintln(ROOT_ENVIRONMENT); return model::GuardError.Usage; } manifestPath = values[2].view(); } else { return model::GuardError.Usage; }
if createSnapshot { return commands::snapshot(root.view(), manifestPath); } return commands::verify(root.view(), manifestPath);}
void print_usage() { fmt::println("workspace_guard - deterministic workspace snapshots"); fmt::println(""); fmt::println("usage:"); fmt::println(" workspace_guard snapshot <root> <manifest>"); fmt::println(" workspace_guard snapshot <manifest> # root from FLECHA_GUARD_ROOT"); fmt::println(" workspace_guard verify <root> <manifest>"); fmt::println(" workspace_guard verify <manifest> # root from FLECHA_GUARD_ROOT"); fmt::println(" workspace_guard diff <old-manifest> <new-manifest>"); fmt::println(" workspace_guard run <root> <manifest> -- <program> [arguments...]");}Este trecho depende dos outros arquivos do pacote. O código completo compilado está disponível abaixo.
Código completo do pacote (7 arquivos)
longbow.toml
[package]name = "workspace_guard"version = "0.1.0"kind = "program"
[dependencies]longbow.lock
{ "schema": 1, "root": "root", "packages": [ { "id": "root", "identity": "local/workspace_guard", "source": { "kind": "workspace", "path": "." }, "version": "0.1.0", "dependencies": { "std": "toolchain-std" } }, { "id": "toolchain-std", "identity": "flecha/std", "source": { "kind": "toolchain" }, "dependencies": {} } ]}src/commands.fl
module commands;
@imports :: root::manifest :: root::model :: root::scanner :: lib::std::containers::collections :: lib::std::fmt :: lib::std::process::command;
export error snapshot(stringv rootPath, stringv manifestPath) { entries, scanError := scanner::scan(rootPath); if scanError != none { return scanError; } writeError := manifest::write(manifestPath, &entries); if writeError != none { return writeError; } fmt::print("snapshot: "); fmt::print(entries.len()); fmt::println(" files"); return none;}
export error verify(stringv rootPath, stringv manifestPath, bool quiet = false) { expected, manifestError := manifest::read(manifestPath); if manifestError != none { return manifestError; } actual, scanError := scanner::scan(rootPath); if scanError != none { return scanError; }
usize differences = compare(&expected, &actual, false); if differences != 0 { fmt::eprint("workspace_guard: "); fmt::eprint(differences); fmt::eprintln(" workspace differences"); return model::GuardError.Mismatch; } if !quiet { fmt::print("verified: "); fmt::print(actual.len()); fmt::println(" files"); } return none;}
export error diff(stringv oldManifest, stringv newManifest) { oldEntries, oldError := manifest::read(oldManifest); if oldError != none { return oldError; } newEntries, newError := manifest::read(newManifest); if newError != none { return newError; }
usize differences = compare(&oldEntries, &newEntries, true); fmt::print("diff: "); fmt::print(differences); fmt::println(" differences"); return none;}
usize compare( &const collections::Array<model::Entry> expected, &const collections::Array<model::Entry> actual, bool standardOutput) { expectedValues := expected->view(); actualValues := actual->view(); usize expectedIndex = 0; usize actualIndex = 0; usize differences = 0;
while expectedIndex < expectedValues.len() or actualIndex < actualValues.len() { if expectedIndex == expectedValues.len() { report("added", actualValues[actualIndex].path(), standardOutput); actualIndex++; differences++; proceed; } if actualIndex == actualValues.len() { report("removed", expectedValues[expectedIndex].path(), standardOutput); expectedIndex++; differences++; proceed; }
expectedPath := expectedValues[expectedIndex].path(); actualPath := actualValues[actualIndex].path(); if model::text_less(expectedPath, actualPath) { report("removed", expectedPath, standardOutput); expectedIndex++; differences++; proceed; } if model::text_less(actualPath, expectedPath) { report("added", actualPath, standardOutput); actualIndex++; differences++; proceed; }
if !model::same_content( &expectedValues[expectedIndex], &actualValues[actualIndex] ) { report("modified", expectedPath, standardOutput); differences++; } expectedIndex++; actualIndex++; }
return differences;}
void report(stringv kind, stringv path, bool standardOutput) { if standardOutput { fmt::print(kind); fmt::print(" "); fmt::println(path); } else { fmt::eprint(kind); fmt::eprint(" "); fmt::eprintln(path); }}
export (u32, error) run( stringv rootPath, stringv manifestPath, []const string arguments) { verifyError := verify(rootPath, manifestPath, true); if verifyError != none { return (none, verifyError); } if arguments.len() == 0 { return (none, model::GuardError.Usage); }
executable, copyError := model::copy_text(arguments[0].view()); if copyError != none { return (none, copyError); } childCommand := command::Command::init(executable); loop usize index = 1; index < arguments.len(); index++ { argument, argumentError := model::copy_text(arguments[index].view()); if argumentError != none { return (none, argumentError); } if childCommand.arg(argument) != none { return (none, AllocationError.OutOfMemory); } }
child, spawnError := childCommand.spawn(); if spawnError != none { return (none, spawnError); } status, waitError := child.wait(); if waitError != none { return (none, waitError); } if !status.exited() { return (none, model::GuardError.ProcessFailure); } return (status.detail(), none);}src/main.fl
module main;
@imports :: root::commands :: root::model :: lib::std::fmt :: lib::std::os::arguments :: lib::std::os::environment;
const stringv ROOT_ENVIRONMENT = "FLECHA_GUARD_ROOT";
i32 main() { values, captureError := arguments::capture(); if captureError != none { fmt::eprintln("workspace_guard: cannot capture command-line arguments"); return 3; } if values.len() < 2 { print_usage(); return 2; }
command := values[1].view(); error result = none; u32 childStatus = 0;
if command == "snapshot" or command == "verify" { result = snapshot_or_verify(values[:], command == "snapshot"); } else if command == "diff" and values.len() == 4 { result = commands::diff(values[2].view(), values[3].view()); } else if command == "run" and values.len() >= 6 and values[4] == "--" { childStatus, result = commands::run( values[2].view(), values[3].view(), values[5:] ); if result == none { return childStatus.cast<i32>; } } else if command == "help" or command == "--help" or command == "-h" { print_usage(); return 0; } else { result = model::GuardError.Usage; }
if result == none { return 0; } if result == model::GuardError.Usage { print_usage(); return 2; } if result == model::GuardError.InvalidManifest { fmt::eprintln("workspace_guard: invalid manifest"); return 4; } if result == model::GuardError.Mismatch { return 5; } if result == model::GuardError.UnsupportedEntry { return 6; } if result == model::GuardError.ProcessFailure { fmt::eprintln("workspace_guard: child process terminated abnormally"); return 7; }
fmt::eprintln("workspace_guard: operation failed"); return 3;}
error snapshot_or_verify([]const string values, bool createSnapshot) { string root = none; stringv manifestPath = ""; if values.len() == 4 { error copyError; root, copyError = model::copy_text(values[2].view()); if copyError != none { return copyError; } manifestPath = values[3].view(); } else if values.len() == 3 { bool present; error environmentError; root, present, environmentError = environment::get(ROOT_ENVIRONMENT); if environmentError != none { return environmentError; } if !present or root.len() == 0 { fmt::eprint("workspace_guard: set "); fmt::eprintln(ROOT_ENVIRONMENT); return model::GuardError.Usage; } manifestPath = values[2].view(); } else { return model::GuardError.Usage; }
if createSnapshot { return commands::snapshot(root.view(), manifestPath); } return commands::verify(root.view(), manifestPath);}
void print_usage() { fmt::println("workspace_guard - deterministic workspace snapshots"); fmt::println(""); fmt::println("usage:"); fmt::println(" workspace_guard snapshot <root> <manifest>"); fmt::println(" workspace_guard snapshot <manifest> # root from FLECHA_GUARD_ROOT"); fmt::println(" workspace_guard verify <root> <manifest>"); fmt::println(" workspace_guard verify <manifest> # root from FLECHA_GUARD_ROOT"); fmt::println(" workspace_guard diff <old-manifest> <new-manifest>"); fmt::println(" workspace_guard run <root> <manifest> -- <program> [arguments...]");}src/manifest.fl
module manifest;
@imports :: root::model :: lib::std::containers::collections :: lib::std::encoding::hex :: lib::std::io::buffered :: lib::std::io::errors as io_errors :: lib::std::io::file :: lib::std::io::filesystem :: lib::std::text::strings :: lib::std::text::utf8;
const stringv HEADER = "FLECHA-WORKSPACE-SNAPSHOT\t1\n";const usize WRITE_CAPACITY = 8192;
export error write( stringv destination, &const collections::Array<model::Entry> entries) { present, existsError := filesystem::exists(destination); if existsError != none { return existsError; } if present { return io_errors::IoError.AlreadyExists; }
options := build file::OpenOptions { self.write = true; self.createNew = true; }; output, openError := file::File::open(destination, options); if openError != none { return openError; }
error result = none; { adapter := output.writer(); writer, writerError := buffered::BufferedWriter::withCapacity(adapter, WRITE_CAPACITY); if writerError != none { result = writerError; } else { result = write_contents(&writer, entries); } }
if result == none { result = output.flush(); } closeError := output.close(); if result == none { result = closeError; }
if result != none { error _ = filesystem::remove_file(destination); } return result;}
error write_contents( &buffered::BufferedWriter writer, &const collections::Array<model::Entry> entries) { error writeError = writer->writeAll(HEADER.cast<[]const u8>); if writeError != none { return writeError; }
values := entries->view(); loop usize index = 0; index < values.len(); index++ { pathHex, pathError := hex::encode(values[index].path()); if pathError != none { return pathError; } sizeText, sizeError := strings::int_to_string<u64>(values[index].byteSize()); if sizeError != none { return sizeError; }
writeError = writer->writeAll( values[index].hash().cast<[]const u8> ); if writeError == none { writeError = writer->writeAll("\t".cast<[]const u8>); } if writeError == none { writeError = writer->writeAll(sizeText.view().cast<[]const u8>); } if writeError == none { writeError = writer->writeAll("\t".cast<[]const u8>); } if writeError == none { writeError = writer->writeAll(pathHex.view().cast<[]const u8>); } if writeError == none { writeError = writer->writeAll("\n".cast<[]const u8>); } if writeError != none { return writeError; } }
return writer->flush();}
export (collections::Array<model::Entry>, error) read(stringv sourcePath) { contents, readError := filesystem::read_file(sourcePath); if readError != none { return (collections::Array<model::Entry>::init(), readError); } return parse(contents.view());}
(collections::Array<model::Entry>, error) parse(stringv contents) { entries := collections::Array<model::Entry>::init(); if contents.len() < HEADER.len() or contents.substr(0, HEADER.len()) != HEADER { return (entries, model::GuardError.InvalidManifest); }
u32 offset = HEADER.len(); while offset < contents.len() { u32 end = offset; while end < contents.len() and contents[end] != '\n' { end++; } if end == contents.len() or end == offset { return (entries, model::GuardError.InvalidManifest); }
entry, entryError := parse_line(contents.substr(offset, end - offset)); if entryError != none { return (entries, entryError); } if !entries.empty() { { values := entries.view(); if !model::text_less( values[values.len() - 1].path(), entry.path() ) { return (entries, model::GuardError.InvalidManifest); } } } pushError := entries.push(entry); if pushError != none { return (entries, pushError); } offset = end + 1; }
return (entries, none);}
(model::Entry, error) parse_line(stringv line) { u32 first = line.len(); u32 second = line.len(); loop u32 index = 0; index < line.len(); index++ { if line[index] != '\t' { proceed; } if first == line.len() { first = index; } else if second == line.len() { second = index; } else { return (none, model::GuardError.InvalidManifest); } } if first == 0 or second <= first + 1 or second + 1 >= line.len() { return (none, model::GuardError.InvalidManifest); }
digestText := line.substr(0, first); sizeText := line.substr(first + 1, second - first - 1); pathText := line.substr(second + 1, line.len() - second - 1);
digestBytes, digestDecodeError := hex::decode_to_bytes(digestText); if digestDecodeError != none or digestBytes.len() != 32 { return (none, model::GuardError.InvalidManifest); } canonicalDigest, digestEncodeError := hex::encode_bytes(digestBytes[:]); if digestEncodeError != none { return (none, digestEncodeError); } if canonicalDigest.view() != digestText { return (none, model::GuardError.InvalidManifest); }
size, sizeParseError := strings::stringv_to_int<u64>(sizeText); if sizeParseError != none { return (none, model::GuardError.InvalidManifest); } canonicalSize, sizeFormatError := strings::int_to_string<u64>(size); if sizeFormatError != none { return (none, sizeFormatError); } if canonicalSize.view() != sizeText { return (none, model::GuardError.InvalidManifest); }
pathValue, pathDecodeError := hex::decode(pathText); if pathDecodeError != none or utf8::validate(pathValue.view()) != none or !model::valid_relative_path(pathValue.view()) { return (none, model::GuardError.InvalidManifest); } canonicalPath, pathEncodeError := hex::encode(pathValue.view()); if pathEncodeError != none { return (none, pathEncodeError); } if canonicalPath.view() != pathText { return (none, model::GuardError.InvalidManifest); }
return (model::Entry::init(pathValue, canonicalDigest, size), none);}src/model.fl
module model;
@imports :: lib::std::text::strings;
export error GuardError { Usage, InvalidManifest, UnsupportedEntry, Mismatch, ProcessFailure}
export structure Entry { string relativePath; string digest; u64 size;};
Entry Entry::init(string relativePath, string digest, u64 size) override { return build Entry { self.relativePath = relativePath; self.digest = digest; self.size = size; };}
stringv Entry::path(&const self) <-self { return self->relativePath.view();}
stringv Entry::hash(&const self) <-self { return self->digest.view();}
u64 Entry::byteSize(&const self) { return self->size;}
export bool text_less(stringv left, stringv right) { u32 common = left.len(); if right.len() < common { common = right.len(); }
loop u32 index = 0; index < common; index++ { if left[index] < right[index] { return true; } if left[index] > right[index] { return false; } }
return left.len() < right.len();}
export bool entry_less(&const Entry left, &const Entry right) { return text_less(left->path(), right->path());}
export bool same_content(&const Entry left, &const Entry right) { return left->byteSize() == right->byteSize() and left->hash() == right->hash();}
export (string, error) copy_text(stringv value) { return strings::string_from_bytes(value.cast<[]const u8>);}
export (string, error) child_path(stringv parent, stringv name) { if parent.len() == 0 { return copy_text(name); }
usize capacity = parent.len().cast<usize> + name.len().cast<usize> + 1; if capacity <= parent.len().cast<usize> { return (none, AllocationError.OutOfMemory); }
builder, allocationError := strings::StringBuilder::withCapacity(capacity); if allocationError != none { return (none, allocationError); } if builder.append(parent) != none or builder.appendChar('/') != none or builder.append(name) != none { return (none, AllocationError.OutOfMemory); }
return builder.toString();}
export bool valid_relative_path(stringv value) { if value.len() == 0 or value[0] == '/' or value[value.len() - 1] == '/' { return false; }
u32 segmentStart = 0; loop u32 index = 0; index <= value.len(); index++ { if index < value.len() { const char BYTE = value[index]; const u8 BYTE_VALUE = BYTE.cast<u8>; if BYTE_VALUE < 32 or BYTE_VALUE == 127 or BYTE == '\\' or BYTE == ':' or BYTE == '*' or BYTE == '?' or BYTE == '"' or BYTE == '<' or BYTE == '>' or BYTE == '|' { return false; } if BYTE != '/' { proceed; } }
const u32 LENGTH = index - segmentStart; if LENGTH == 0 { return false; } segment := value.substr(segmentStart, LENGTH); if segment == "." or segment == ".." { return false; } segmentStart = index + 1; }
return true;}src/scanner.fl
module scanner;
@imports :: root::model :: lib::std::containers::collections :: lib::std::cryptography::sha2 :: lib::std::encoding::hex :: lib::std::fmt :: lib::std::io::buffered :: lib::std::io::file :: lib::std::io::path :: lib::std::text::utf8;
const usize READ_CAPACITY = 32768;
export (collections::Array<model::Entry>, error) scan(stringv rootPath) { entries := collections::Array<model::Entry>::init(); scanError := collect(rootPath, "", &entries); if scanError != none { return (entries, scanError); }
entries.sortBy(model::entry_less); return (entries, none);}
error collect( stringv rootPath, stringv relativePath, &collections::Array<model::Entry> entries) { string directoryPath = none; if relativePath.len() == 0 { error copyError; directoryPath, copyError = model::copy_text(rootPath); if copyError != none { return copyError; } } else { error joinError; directoryPath, joinError = path::join(rootPath, relativePath); if joinError != none { return joinError; } }
directory, openError := file::Directory::open(directoryPath.view()); if openError != none { fmt::eprint("workspace_guard: cannot open directory: "); fmt::eprintln(directoryPath.view()); return openError; }
while true { entry, present, readError := directory.next(); if readError != none { return readError; } if !present { break; } if utf8::validate(entry.name()) != none { fmt::eprintln("workspace_guard: non-UTF-8 path is unsupported"); return model::GuardError.UnsupportedEntry; }
childRelative, relativeError := model::child_path(relativePath, entry.name()); if relativeError != none { return relativeError; } if !model::valid_relative_path(childRelative.view()) { fmt::eprint("workspace_guard: non-portable path: "); fmt::eprintln(childRelative.view()); return model::GuardError.UnsupportedEntry; }
if entry.kind() == file::EntryType.Directory { recurseError := collect(rootPath, childRelative.view(), entries); if recurseError != none { return recurseError; } proceed; } if entry.kind() != file::EntryType.File { fmt::eprint("workspace_guard: unsupported entry: "); fmt::eprintln(childRelative.view()); return model::GuardError.UnsupportedEntry; }
actualPath, joinError := path::join(rootPath, childRelative.view()); if joinError != none { return joinError; } digest, size, hashError := hash_file(actualPath.view()); if hashError != none { fmt::eprint("workspace_guard: cannot hash file: "); fmt::eprintln(childRelative.view()); return hashError; }
pushError := entries->push( model::Entry::init(childRelative, digest, size) ); if pushError != none { return pushError; } }
return directory.close();}
(string, u64, error) hash_file(stringv filePath) { input, openError := file::File::open(filePath, file::OpenOptions::readOnly()); if openError != none { return (none, none, openError); }
hasher := sha2::Sha256::init(); u64 total = 0; error result = none; { adapter := input.reader(); reader, readerError := buffered::BufferedReader::withCapacity(adapter, READ_CAPACITY); if readerError != none { return (none, none, readerError); }
[32768]u8 chunk = none; while true { count, readError := reader.read(chunk[:]); if count > 0 { const u64 NEXT = total + count.cast<u64>; if NEXT < total { return (none, none, AllocationError.OutOfMemory); } total = NEXT; result = hasher.update(chunk[:count]); if result != none { return (none, none, result); } } if readError != none { return (none, none, readError); } if count == 0 { break; } } }
closeError := input.close(); if closeError != none { return (none, none, closeError); } digest, digestError := hasher.finalize(); if digestError != none { return (none, none, digestError); } encoded, encodeError := hex::encode_bytes(digest.bytes()); return (encoded, total, encodeError);}O ramo selecionado coordena os módulos já separados de scanner, manifesto e processo. O pacote completo é necessário porque o comportamento resulta da interação entre esses módulos, não do trecho isolado.
Comandos
longbow check curriculum/main-evidence/projects/workspace_guardlongbow test curriculum/main-evidence/projects/workspace_guardlongbow build curriculum/main-evidence/projects/workspace_guardlongbow run curriculum/main-evidence/projects/workspace_guard -- helpSaída esperada
workspace_guard - deterministic workspace snapshots
usage: workspace_guard snapshot <root> <manifest> workspace_guard snapshot <manifest> # root from FLECHA_GUARD_ROOT workspace_guard verify <root> <manifest> workspace_guard verify <manifest> # root from FLECHA_GUARD_ROOT workspace_guard diff <old-manifest> <new-manifest> workspace_guard run <root> <manifest> -- <program> [arguments...]status de saída: 0Referência relacionada: /examples/workspace_guard/src/main/.
Trilha concluída
Seção intitulada “Trilha concluída”Continue em Avançado: contratos de propriedade e builds determinísticos.