///|
#derive(Eq, Debug)
pub(all) enum ChangeKind {
VbaRemoval
OleRemoval
TemplateRemoval
MacroFormatConversion
}
///|
pub extend ChangeKind with Eq::{equal, not_equal}
///|
pub extend ChangeKind with Debug::{to_repr}
///|
pub extend ChangeKind with ToJson::{to_json}
///|
pub fn ChangeKind::label(self : ChangeKind) -> String {
match self {
VbaRemoval => "vba_removal"
OleRemoval => "ole_removal"
TemplateRemoval => "remote_template_removal"
MacroFormatConversion => "macro_format_conversion"
}
}
///|
pub impl ToJson for ChangeKind with fn to_json(self) -> Json {
Json::string(self.label())
}
///|
#derive(Eq, Debug, ToJson)
pub(all) struct ContentChange {
kind : ChangeKind
count : Int32
removed_parts : Array[String]
effect : String
}
///|
pub extend ContentChange with Eq::{equal, not_equal}
///|
pub extend ContentChange with Debug::{to_repr}
///|
pub extend ContentChange with ToJson::{to_json}
///|
#derive(Eq, Debug, ToJson)
pub(all) struct ChangeSummary {
changes : Array[ContentChange]
removed_parts : Array[String]
rewritten_parts : Array[String]
}
///|
pub extend ChangeSummary with Eq::{equal, not_equal}
///|
pub extend ChangeSummary with Debug::{to_repr}
///|
pub extend ChangeSummary with ToJson::{to_json}
///|
/// These flags cover only the exact reviewed plan, never arbitrary future inputs.
#derive(Eq, Debug, ToJson)
pub(all) struct RemovalPermissions {
remove_vba : Bool
remove_ole : Bool
remove_remote_template : Bool
convert_macro_format : Bool
}
///|
pub extend RemovalPermissions with Eq::{equal, not_equal}
///|
pub extend RemovalPermissions with Debug::{to_repr}
///|
pub extend RemovalPermissions with ToJson::{to_json}
///|
pub fn RemovalPermissions::deny_all() -> RemovalPermissions {
{
remove_vba: false,
remove_ole: false,
remove_remote_template: false,
convert_macro_format: false,
}
}
///|
pub fn RemovalPermissions::macros_only() -> RemovalPermissions {
{
remove_vba: true,
remove_ole: false,
remove_remote_template: false,
convert_macro_format: true,
}
}
///|
pub fn RemovalPermissions::all_supported() -> RemovalPermissions {
{
remove_vba: true,
remove_ole: true,
remove_remote_template: true,
convert_macro_format: true,
}
}
///|
pub fn RemovalPermissions::allows(
self : RemovalPermissions,
kind : ChangeKind,
) -> Bool {
match kind {
VbaRemoval => self.remove_vba
OleRemoval => self.remove_ole
TemplateRemoval => self.remove_remote_template
MacroFormatConversion => self.convert_macro_format
}
}
///|
/// Opaque in-memory review. Getters never expose its mutable plan/summary arrays.
pub struct DocumentPreview {
priv plan : PreparedPlan
priv changes : ChangeSummary
priv plan_hash : String
}
///|
/// Trusted application decision, not a signed credential or client JSON token.
pub struct DocumentAuthorization {
priv plan : PreparedPlan
priv changes : ChangeSummary
priv plan_hash : String
priv permissions : RemovalPermissions
}
///|
#derive(Eq, Debug, ToJson)
pub(all) struct AuthorizationRecord {
schema : String
input_hash : String
plan_hash : String
output_hash : String
permissions : RemovalPermissions
summary : ChangeSummary
}
///|
pub extend AuthorizationRecord with Eq::{equal, not_equal}
///|
pub extend AuthorizationRecord with Debug::{to_repr}
///|
pub extend AuthorizationRecord with ToJson::{to_json}
///|
pub(all) struct AuthorizedRebuilt {
bytes : Bytes
receipt : VerifiedReceipt
authorization : AuthorizationRecord
}
///|
fn copy_change_summary(summary : ChangeSummary) -> ChangeSummary {
{
changes: summary.changes.map(change => {
..change,
removed_parts: change.removed_parts.map(part => part),
}),
removed_parts: summary.removed_parts.map(part => part),
rewritten_parts: summary.rewritten_parts.map(part => part),
}
}
///|
pub fn DocumentPreview::summary(self : DocumentPreview) -> ChangeSummary {
copy_change_summary(self.changes)
}
///|
pub fn DocumentPreview::input_hash(self : DocumentPreview) -> String {
self.plan.input_hash
}
///|
pub fn DocumentPreview::plan_hash(self : DocumentPreview) -> String {
self.plan_hash
}
///|
/// Detached plan data for display/storage. This JSON is not authorization.
pub fn DocumentPreview::plan_json(self : DocumentPreview) -> Json {
self.plan.to_json()
}
///|
pub fn DocumentPreview::to_json(self : DocumentPreview) -> Json {
{
"schema": "partsieve.document-preview.v1",
"input_hash": self.plan.input_hash,
"plan_hash": self.plan_hash,
"input_format": self.plan.input_format,
"output_format": self.plan.output_format,
"summary": ToJson::to_json(self.changes),
}
}
///|
fn summary_from_plan(pkg : Package, plan : PreparedPlan) -> ChangeSummary raise {
let removed : Array[String] = []
let rewritten : Array[String] = []
for op in plan.operations {
pkg.control.checkpoint("review-operation", 1)
guard ["vba-v1", "ole-v1", "remote-template-v1", "format-v1"].contains(
op.rule_id,
) else {
raise Refused("plan contains an unmodeled change category")
}
if op.kind == RemovePart && !removed.contains(op.part) {
removed.push(op.part)
}
if op.kind != RemovePart &&
operation_writes(op.kind) &&
!rewritten.contains(op.part) {
rewritten.push(op.part)
}
}
removed.sort()
rewritten.sort()
let changes : Array[ContentChange] = []
for
(kind, rule, count, effect) in [
(
VbaRemoval,
"vba-v1",
pkg.audit.parts.filter(part => part.content_type == vba_ct).length(),
"VBA projects and exclusively owned support data are removed; macro functionality is lost.",
),
(
OleRemoval,
"ole-v1",
ole_relationships(pkg).length(),
"Embedded contents, object editing capability and exclusively owned preview images are removed.",
),
(
TemplateRemoval,
"remote-template-v1",
pkg.audit.relationships.filter(is_template_relationship).length(),
"The attached external template and its local reference are removed; the target is not fetched.",
),
(
MacroFormatConversion,
"format-v1",
if is_macro_format(plan.input_format) {
1
} else {
0
},
"Macro-enabled main type becomes ordinary DOCX/XLSX; this does not mean VBA was present.",
),
] {
if count > 0 {
let parts = plan.operations
.filter(op => op.kind == RemovePart && op.rule_id == rule)
.map(op => op.part)
parts.sort()
changes.push({ kind, count, removed_parts: parts, effect, })
}
guard !plan.operations.any(op => op.rule_id == rule) || count > 0 else {
raise Invalid("plan operation has no modeled content change")
}
}
let expected_removed = plan.controlled_removals.map(part => part)
expected_removed.sort()
guard removed == expected_removed else {
raise Invalid(
"change summary does not account for all controlled deletions",
)
}
{ changes, removed_parts: removed, rewritten_parts: rewritten, }
}
///|
fn authorization_denial(
summary : ChangeSummary,
permissions : RemovalPermissions,
operation : SdkOperation,
) -> SdkFailure? {
for change in summary.changes {
if !permissions.allows(change.kind) {
return Some({
code: AuthorizationDenied,
operation,
message: "permission required for " + change.kind.label(),
})
}
}
None
}
///|
fn reviewed_plan_hash(
plan : PreparedPlan,
control : WorkControl,
) -> String raise {
work_digest(@utf8.encode(plan.to_json().stringify()[:])[:], control)
}
///|
pub fn preview_document(
input : Bytes,
limits~ : Limits = Limits::default(),
control~ : WorkControl = WorkControl::default(),
) -> Result[DocumentPreview, SdkFailure] {
let operation = PreviewDocument
if sdk_preflight(input, None, limits, control, operation) is Some(error) {
return Err(error)
}
try {
let pkg = load_package(input, limits, control~)
let plan = make_prepared_plan(pkg, limits)
let changes = summary_from_plan(pkg, plan)
let plan_hash = reviewed_plan_hash(plan, control)
// Reserve the final source check before the last user callback. Hash after
// that callback, with no further callbacks before returning the review.
control.checkpoint("review-complete", input.length())
guard digest(input[:]) == plan.input_hash else {
raise Invalid("source changed during content preview")
}
Ok({ plan, changes, plan_hash, })
} catch {
error => Err(sdk_failure(error, operation, control))
}
}
///|
/// Called by the trusted application after its own decision/confirmation.
pub fn authorize_document(
preview : DocumentPreview,
permissions : RemovalPermissions,
) -> Result[DocumentAuthorization, SdkFailure] {
if authorization_denial(preview.changes, permissions, AuthorizeDocument)
is Some(error) {
return Err(error)
}
Ok({
plan: preview.plan,
changes: preview.changes,
plan_hash: preview.plan_hash,
permissions,
})
}
///|
/// Recompute the actual plan and losses, enforce permissions, then execute/verify.
pub fn execute_authorized_document(
input : Bytes,
authorization : DocumentAuthorization,
control~ : WorkControl = WorkControl::default(),
) -> Result[AuthorizedRebuilt, SdkFailure] {
let operation = ExecuteAuthorizedDocument
let limits = authorization.plan.limits
if sdk_preflight(input, None, limits, control, operation) is Some(error) {
return Err(error)
}
try {
let pkg = load_package(input, limits, control~)
let actual = make_prepared_plan(pkg, limits)
let changes = summary_from_plan(pkg, actual)
guard actual == authorization.plan &&
changes == authorization.changes &&
reviewed_plan_hash(actual, control) == authorization.plan_hash else {
raise Invalid(
"authorization differs from the actual source-bound plan or content changes",
)
}
if authorization_denial(changes, authorization.permissions, operation)
is Some(error) {
return Err(error)
}
control.checkpoint("authorization-checked", 0)
let bytes = execute_prepared(pkg, actual, limits)
let receipt = verify_contract(bytes, input, limits~, control~)
// Callback code can retain a mutable reference to the caller's original.
// Complete all callbacks first, then rebind the actual bytes without another
// callback between the final hashes and successful return.
control.checkpoint(
"authorization-complete",
input.length() + bytes.length(),
)
guard receipt.input_hash == authorization.plan.input_hash &&
receipt.plan == actual &&
digest(input[:]) == authorization.plan.input_hash &&
digest(bytes[:]) == receipt.output_hash else {
raise Invalid(
"authorized source, verified plan or output changed before completion",
)
}
let record : AuthorizationRecord = {
schema: "partsieve.authorization-record.v1",
input_hash: receipt.input_hash,
plan_hash: authorization.plan_hash,
output_hash: receipt.output_hash,
permissions: authorization.permissions,
summary: copy_change_summary(changes),
}
Ok({ bytes, receipt, authorization: record, })
} catch {
error => Err(sdk_failure(error, operation, control))
}
}