Skip to content

feat: convert public SFrame API to use Result<T>#83

Closed
k-wasniowski wants to merge 1 commit intocisco:mainfrom
k-wasniowski:sframe-api-refactored
Closed

feat: convert public SFrame API to use Result<T>#83
k-wasniowski wants to merge 1 commit intocisco:mainfrom
k-wasniowski:sframe-api-refactored

Conversation

@k-wasniowski
Copy link
Copy Markdown
Contributor

@k-wasniowski k-wasniowski commented Mar 12, 2026

Refactor: Convert Public SFrame API to Return Result<T>

Summary

This PR completes the migration of the SFrame public API away from exception-based error handling to the Result<T> return type. All public methods on Context and MLSContext now return Result<T> instead of throwing exceptions, giving callers explicit, composable control over error propagation.

This builds on the two preceding PRs that introduced Result<T> to the crypto layer (#81) and the SFrame internals (#82).

What Changed

Public API (include/sframe/sframe.h)

Every public method that previously threw exceptions now returns a Result<T>:

Method Before After
Context::add_key void (throws) Result<void>
Context::protect output_bytes (throws) Result<output_bytes>
Context::unprotect output_bytes (throws) Result<output_bytes>
MLSContext::add_epoch (both overloads) void (throws) Result<void>
MLSContext::protect (both overloads) output_bytes (throws) Result<output_bytes>
MLSContext::unprotect output_bytes (throws) Result<output_bytes>

Private helpers were also updated:

Method Before After
MLSContext::form_key_id KeyID (throws) Result<KeyID>
MLSContext::ensure_key void (throws) Result<void>
MLSContext::EpochKeys constructor Throwing constructor static Result<EpochKeys> create() factory

Implementation (src/sframe.cpp)

  • All throw statements in the public/private API surface replaced with return SFrameError(...).
  • Internal error propagation updated to use early-return pattern instead of exceptions.
  • EpochKeys converted from a throwing constructor to a static create() factory that returns Result<EpochKeys>.
  • EpochKeys scalar fields now have in-class default initializers (= 0) to eliminate clang-analyzer warnings about uninitialized members in implicit constructors.

How to Use the New API

Basic Context usage

#include <sframe/sframe.h>
#include <sframe/result.h>

using namespace sframe;

// Check the result manually:
auto result = ctx.protect(kid, ct, pt, {});
if (result.is_err()) {
  auto err = result.error();
  // err.type()    -> SFrameErrorType enum
  // err.message() -> const char* (may be nullptr)
} else {
  auto encrypted = result.value();
}

MLSContext usage

auto add_result = mls.add_epoch(epoch, epoch_secret);
if (add_result.is_err()) {
  // handle error...
}

auto protect_result = mls.protect(epoch, sender, ct, pt, metadata);
if (protect_result.is_err()) {
  // handle error...
}
auto encrypted = protect_result.value();

Error Types (SFrameErrorType)

Value Meaning
internal_error Unexpected internal failure
invalid_parameter_error Bad input (unknown epoch, sender overflow, etc.)
buffer_too_small_error Output buffer too small for the operation
crypto_error Cryptographic operation failed
unsupported_ciphersuite_error Requested cipher suite not available
authentication_error Decryption/authentication check failed
invalid_key_usage_error Key used for wrong direction (protect vs. unprotect)

Migration Guide

If you are currently calling the SFrame API and catching exceptions:

// BEFORE (exception-based)
try {
  ctx.add_key(kid, KeyUsage::protect, key);
  auto ct = ctx.protect(kid, ct_buf, pt, metadata);
} catch (const invalid_parameter_error& e) { ... }
  catch (const buffer_too_small_error& e) { ... }

// AFTER (Result-based)
auto add_result = ctx.add_key(kid, KeyUsage::protect, key);
if (add_result.is_err()) {
  // handle add_result.error()
}

auto protect_result = ctx.protect(kid, ct_buf, pt, metadata);
if (protect_result.is_err()) {
  auto err = protect_result.error();
  switch (err.type()) {
    case SFrameErrorType::invalid_parameter_error: /* ... */ break;
    case SFrameErrorType::buffer_too_small_error:  /* ... */ break;
    default: break;
  }
}
auto ct = protect_result.value();

Note: In NO_ALLOC mode, the fixed-size internal vector and map containers may still throw std::out_of_range if their compile-time capacity is exceeded (e.g., too many keys or epochs). These exceptions originate from the container layer, not from the SFrame API itself.

@k-wasniowski k-wasniowski force-pushed the sframe-api-refactored branch 5 times, most recently from 23949f6 to 36d284f Compare March 12, 2026 15:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant