Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ if(BUILD_TOOLS)
add_executable(
ifc
src/tools/ifc.cxx
src/tools/ifc-archive.cxx
src/tools/tool-support.cxx
)
add_executable(Microsoft.IFC::Tool ALIAS ifc)
set_property(TARGET ifc PROPERTY EXPORT_NAME Tool)
Expand Down
20 changes: 17 additions & 3 deletions ifc-driver.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,27 @@ The IFC SDK contains a command-line executable named `ifc`. It is a simple driv


where:
- _cmd_ is either a built-in sucommand, or an external extension executable
- _cmd_ is either a built-in subcommand, or an external extension executable
- _file1_, _file2_, _..._ are the IFC files to be acted on

At the moment, the tool supports only one built-in command: `version`. That is, the invocation
At the moment, the tool supports three built-in commands: `version`, `archive`, and `extract`.

The invocation

> `ifc` `version` _file1_ _file2_ _..._

will display each _file_ along with the version of the of IFC Specification it was generated for.
will display each _file_ along with the version of the IFC Specification it was generated for.

The invocation

> `ifc` `archive` [`--name` _name_] `-o` _archive_ _file1_ _file2_ _..._

packages the IFC files _file1_, _file2_, _..._ into a single archive IFC file named _archive_ (an IFC file whose translation unit sort is `Archive`). Each input IFC file is embedded verbatim, retaining its own complete file structure. The archive's table of contents records, for each member, its **sort** and **canonical name** — the module name for a named module or the header-unit name for a header unit, read from that member's own unit descriptor — together with a normalized relative **filepath**, and its offset and size within the archive. A member is identified by its sort and canonical name together, so a named module and a header unit may share a name; no two members may share both. Members are stored by canonical name with sort breaking ties. Input paths containing parent traversal, or paths that normalize to the same extraction destination, are rejected. The optional `--name` gives the archive itself a canonical name. If `-o` is omitted, the first file argument names the archive to create and the rest are the IFC files to package. The inputs must be non-archive IFC files; archives cannot be nested.

The invocation

> `ifc` `extract` [`--force`] [`-o` _dir_] _archive_ (`--all` | _selector1_ _selector2_ _..._)

recovers IFC files from an _archive_ created by the `archive` command. With `--all`, every contained member is extracted. Otherwise, a positional _selector_ is the canonical name of a named module, while header units are selected with the repeatable options `--quote-header` _name_ and `--angle-header` _name_. Their arguments are bare header-name payloads: `--quote-header detail/config.h` selects the member named `"detail/config.h"`, and `--angle-header vector` selects the member named `<vector>`. Keeping the delimiter form in the option avoids shell removal of quotes and interpretation of angle brackets as redirection. Each member is written to its recorded filepath beneath _dir_ (the current directory by default), recreating intermediate directories without following archive-controlled symbolic links or reparse points. Existing files are left unchanged unless `--force` is supplied, in which case each completed temporary file atomically replaces its destination. This is the inverse of `archive`, so `extract` followed by a byte comparison is a convenient way to test archive round-tripping.

An IFC external extension `cmd` is any executable named `ifc-`_cmd_ that can be found via the `PATH` environment variable. Such an executable will be invoked by the `ifc` driver along with the rest of the command-line arguments it was originally invoked with. It is a convenient way to structure tooling around IFC files, making your own extension appear as if it was a built-in facility.
43 changes: 42 additions & 1 deletion include/ifc/file.hxx
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,18 @@ namespace ifc {
// Index into the scope table.
enum ScopeIndex : std::uint32_t {};

// -- SHA-256 digest whose object bytes match the 32-byte on-disk digest representation.
struct SHA256Hash {
std::array<std::uint32_t, 8> value;
std::array<std::uint32_t, 8> value; // Word storage preserves the SDK's existing comparison API.
};

// -- The IFC signature precedes the stored content digest in every file image.
inline constexpr std::size_t content_hash_offset = sizeof InterfaceSignature;

// -- IFC integrity covers every byte after the stored digest, including the rest of the header.
inline constexpr std::size_t hashed_contents_offset = content_hash_offset + sizeof(SHA256Hash);
static_assert(hashed_contents_offset == 36);

// The various sort of translation units that can be represented in an IFC file.
enum class UnitSort : std::uint8_t {
Source, // General source translation unit.
Expand Down Expand Up @@ -246,8 +254,41 @@ namespace ifc {
// against some external source.
};

// -- Incremental SHA-256. Feed the message through update() in any number of pieces, then call
// -- finish() exactly once. hash_bytes() below is the one-shot form of the same computation.
struct Sha256 {
// -- Stateful hashing permits bounded-memory processing of archives and other streams.
Sha256();
// -- The Windows backend owns a bcrypt hash handle; portable state needs no special cleanup.
~Sha256();
// -- A hash computation has one evolving state and therefore cannot be duplicated safely.
Sha256(const Sha256&) = delete;
Sha256& operator=(const Sha256&) = delete;

// -- Chunk boundaries carry no message semantics, allowing callers to choose bounded buffers.
void update(gsl::span<const std::byte> bytes);
// -- Finalization consumes the pending state and is valid exactly once per computation.
SHA256Hash finish();

private:
#ifdef _WIN32
void* hash_handle = nullptr; // Bcrypt owns algorithm state behind this native handle.
#else
// -- Portable compression state starts at the SHA-256 initialization vector.
std::array<std::uint32_t, 8> state {
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19 };
std::array<std::byte, 64> block {}; // Retains the partial SHA-256 block across update calls.
std::size_t block_length = 0; // Number of meaningful bytes currently held in block.
std::uint64_t message_length = 0; // Total bytes before padding, needed by SHA-256 finalization.
#endif
};

// -- One-shot compatibility entry point implemented through the same incremental backend.
SHA256Hash hash_bytes(const std::byte* first, const std::byte* last);

// -- Applies the IFC integrity boundary consistently to a complete mapped file image.
SHA256Hash hash_ifc_contents(gsl::span<const std::byte> image);

inline SHA256Hash bytes_to_hash(const std::uint8_t* first, const std::uint8_t* last)
{
auto byte_count = std::distance(first, last);
Expand Down
4 changes: 3 additions & 1 deletion include/ifc/tooling.hxx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ namespace ifc::tool {

// -- Base class for an ifc subcommand extension.
struct Extension {
virtual Name name() const = 0;
// -- Constexpr names let builtin registries prove lookup ordering at compile time.
virtual constexpr Name name() const = 0;
// -- Each extension owns its argument policy while the driver owns dispatch and exception containment.
virtual int run_with(const Arguments&) const = 0;
};

Expand Down
22 changes: 15 additions & 7 deletions src/file.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,26 @@
#include <ifc/file.hxx>

namespace ifc {
SHA256Hash hash_bytes(const std::byte* first, const std::byte* last)
{
Sha256 hasher;
hasher.update({ first, static_cast<std::size_t>(last - first) });
return hasher.finish();
}

SHA256Hash hash_ifc_contents(gsl::span<const std::byte> image)
{
IFCASSERT(image.size() >= hashed_contents_offset);
return hash_bytes(image.data() + hashed_contents_offset, image.data() + image.size());
}

void InputIfc::validate_content_integrity(const InputIfc& file)
{
// Verify integrity of ifc. To do this we know that the header content after the hash
// starts after the interface signature and the first 256 bits.
constexpr std::size_t hash_start = sizeof(InterfaceSignature);
constexpr std::size_t contents_start = hash_start + sizeof(SHA256Hash);
static_assert(contents_start == 36); // 4 bytes for Signature + 8*4 bytes for SHA2
const auto& contents = file.contents();
auto result = hash_bytes(contents.data() + contents_start, contents.data() + contents.size());
auto result = hash_ifc_contents(contents);
auto actual_first = reinterpret_cast<const std::uint8_t*>(result.value.data());
auto actual_last = actual_first + std::size(result.value) * 4;
auto expected_first = reinterpret_cast<const std::uint8_t*>(&contents[hash_start]);
auto expected_first = reinterpret_cast<const std::uint8_t*>(&contents[content_hash_offset]);
auto expected_last = expected_first + sizeof(SHA256Hash);
if (not std::equal(actual_first, actual_last, expected_first, expected_last))
{
Expand Down
136 changes: 42 additions & 94 deletions src/hash_win.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -5,113 +5,61 @@

#include <windows.h>
#include <bcrypt.h>
#include <gsl/util>

#include <ifc/file.hxx>
#include <vector>
#include <ifc/assertions.hxx>

namespace {
// Tags for catch handler.
struct OpenAlgorithmError {
NTSTATUS result;
};
struct HashLengthPropertyError {
NTSTATUS result;
};
struct ObjectLengthPropertyError {
NTSTATUS result;
};
// -- Preserves a BCryptCreateHash failure without imposing a standard exception hierarchy.
struct CreateHashError {
NTSTATUS result;
NTSTATUS result; // Original bcrypt status retained for a future diagnostic boundary.
};

// -- Distinguishes an input-feeding failure from construction and finalization failures.
struct HashDataError {
NTSTATUS result;
NTSTATUS result; // Original bcrypt status retained for a future diagnostic boundary.
};

// -- Identifies failure to materialize the digest after all message bytes were accepted.
struct FinishHashError {
NTSTATUS result;
NTSTATUS result; // Original bcrypt status retained for a future diagnostic boundary.
};
// A simple helper class designed to be an RAII container for the Windows crypto machinery.
class SHA256Helper {
public:
SHA256Helper()
{
digest_ntstatus<OpenAlgorithmError>(BCryptOpenAlgorithmProvider(&alg_handle_, BCRYPT_SHA256_ALGORITHM,
/*pszImplementation = */ nullptr,
/*dwFlags = */ 0));

DWORD cb_result = 0;
digest_ntstatus<HashLengthPropertyError>(BCryptGetProperty(alg_handle_, BCRYPT_HASH_LENGTH,
reinterpret_cast<PUCHAR>(&hash_byte_length_),
sizeof hash_byte_length_, &cb_result,
/*dwFlags = */ 0));

digest_ntstatus<ObjectLengthPropertyError>(BCryptGetProperty(alg_handle_, BCRYPT_OBJECT_LENGTH,
reinterpret_cast<PUCHAR>(&object_byte_length_),
sizeof object_byte_length_, &cb_result,
/*dwFlags = */ 0));
}

~SHA256Helper()
{
if (alg_handle_ != nullptr)
{
BCryptCloseAlgorithmProvider(alg_handle_, /*dwFlags = */ 0);
}
}

ifc::SHA256Hash hash(const std::byte* first, const std::byte* last)
{
BCRYPT_HASH_HANDLE hash_handle = nullptr;

using ByteVector = std::vector<uint8_t>;
ByteVector object_buf(object_byte_length_);
}

digest_ntstatus<CreateHashError>(BCryptCreateHash(alg_handle_, &hash_handle, object_buf.data(),
object_byte_length_,
/*pbSecret = */ nullptr,
/*cbSecret = */ 0,
/*dwFlags = */ 0));
auto final_act = gsl::finally([&] {
if (hash_handle != nullptr)
{
BCryptDestroyHash(hash_handle);
}
});
digest_ntstatus<HashDataError>(BCryptHashData(hash_handle,
reinterpret_cast<PUCHAR>(const_cast<std::byte*>(first)),
static_cast<ULONG>(std::distance(first, last)),
/*dwFlags = */ 0));
ifc::SHA256Hash hash = {};
// uint32_t array should map to a uint8_t[32] array
IFCASSERT(hash_byte_length_ == std::size(hash.value) * 4);
digest_ntstatus<FinishHashError>(
BCryptFinishHash(hash_handle, reinterpret_cast<uint8_t*>(hash.value.data()), hash_byte_length_,
/*dwFlags = */ 0));
return hash;
}

private:
template<typename T>
void digest_ntstatus(NTSTATUS result)
{
// Success for an NTSTATUS is >= 0.
if (result < 0)
{
throw T{result};
}
}
namespace ifc {
// Uses the SHA-256 pseudo-algorithm handle, so there is no provider to open or close, and lets
// bcrypt manage the hash-object memory (pbHashObject == nullptr).
Sha256::Sha256()
{
const NTSTATUS status = BCryptCreateHash(BCRYPT_SHA256_ALG_HANDLE, &hash_handle,
/*pbHashObject = */ nullptr, /*cbHashObject = */ 0,
/*pbSecret = */ nullptr, /*cbSecret = */ 0, /*dwFlags = */ 0);
if (status < 0)
throw CreateHashError{status};
}

BCRYPT_ALG_HANDLE alg_handle_ = nullptr;
DWORD hash_byte_length_ = 0;
DWORD object_byte_length_ = 0;
};
Sha256::~Sha256()
{
if (hash_handle != nullptr)
BCryptDestroyHash(hash_handle);
}

} // namespace
void Sha256::update(gsl::span<const std::byte> bytes)
{
const NTSTATUS status = BCryptHashData(hash_handle,
reinterpret_cast<PUCHAR>(const_cast<std::byte*>(bytes.data())),
static_cast<ULONG>(bytes.size()), /*dwFlags = */ 0);
if (status < 0)
throw HashDataError{status};
}

namespace ifc {
SHA256Hash hash_bytes(const std::byte* first, const std::byte* last)
SHA256Hash Sha256::finish()
{
SHA256Helper helper;
return helper.hash(first, last);
SHA256Hash hash = {};
const NTSTATUS status = BCryptFinishHash(hash_handle, reinterpret_cast<PUCHAR>(hash.value.data()),
static_cast<ULONG>(sizeof hash.value), /*dwFlags = */ 0);
if (status < 0)
throw FinishHashError{status};
return hash;
}

} // namespace ifc
Loading
Loading