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
23 changes: 10 additions & 13 deletions .github/workflows/actions.lock
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ workflows:
'.github/workflows/fmt.yml':
- 'actions/checkout@v7.0.1'
- 'dtolnay/rust-toolchain@v1'
- 'Swatinem/rust-cache@v2.9.2'
- 'swatinem/rust-cache@v2.9.2'
'.github/workflows/boj-build.yml':
- 'actions/checkout@v7.0.1'
'.github/workflows/casket-pages.yml':
Expand All @@ -19,14 +19,14 @@ workflows:
- 'actions/configure-pages@v6.0.0'
- 'actions/deploy-pages@v5.0.1'
- 'actions/upload-pages-artifact@v5.0.0'
- 'haskell-actions/setup@v2.12.0'
- 'haskell-actions/setup@v2.12.1'
'.github/workflows/ci.yml':
- 'actions/checkout@v7.0.1'
- 'dtolnay/rust-toolchain@v1'
- 'swatinem/rust-cache@v2.9.2'
'.github/workflows/codeql.yml':
- 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
- 'github/codeql-action@v4.38.0'
- 'github/codeql-action@v4.38.2'
'.github/workflows/dependabot-automerge.yml':
- 'dependabot/fetch-metadata@v3.1.0'
'.github/workflows/dogfood-gate.yml':
Expand Down Expand Up @@ -54,10 +54,7 @@ workflows:
- 'dtolnay/rust-toolchain@v1'
- 'softprops/action-gh-release@v3.0.3'
- 'swatinem/rust-cache@v2.9.2'
'.github/workflows/rust-ci.yml':
- 'actions/checkout@v7.0.1'
- 'dtolnay/rust-toolchain@v1'
- 'Swatinem/rust-cache@v2.9.2'
'.github/workflows/rust-ci.yml': []
'.github/workflows/scorecard.yml': []
'.github/workflows/secret-scanner.yml': []
'.github/workflows/security.yml':
Expand Down Expand Up @@ -134,14 +131,14 @@ dependencies:
commit: 'sha1-02cb101ec7c40f2c49e1d9714d64511d8e1b74de'
owner_id: 1940490
repo_id: 260749683
'github/codeql-action@v4.38.0':
ref: 'v4.38.0'
commit: 'sha1-b96794f015dfd88f77b49b1c93e0fa7110f94c63'
'github/codeql-action@v4.38.2':
ref: 'v4.38.2'
commit: 'sha1-2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2'
owner_id: 9919
repo_id: 259445878
'haskell-actions/setup@v2.12.0':
ref: 'v2.12.0'
commit: 'sha1-6037f33647c3f17758a2356c80fc4a53d7e0685d'
'haskell-actions/setup@v2.12.1':
ref: 'v2.12.1'
commit: 'sha1-0f8e8c99d88aeb3fbfd523f1ef2c6f762d10d64d'
owner_id: 75048950
repo_id: 623796603
'hyperpolymath/deed-ecosystem@main':
Expand Down
27 changes: 0 additions & 27 deletions .github/workflows/rust-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,32 +17,5 @@ permissions:
contents: read

jobs:
# Auto-format before the reusable Rust CI so PRs don't fail on fmt alone.
fmt:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v7.0.1
with:
ref: ${{ github.head_ref }}
token: ${{ secrets.GITHUB_TOKEN }}
- uses: dtolnay/rust-toolchain@v1
with:
toolchain: stable
components: rustfmt
- uses: Swatinem/rust-cache@v2.9.2
- run: cargo fmt --all
- run: |
git config user.name "github-actions"
git config user.email "github-actions@github.com"
if ! git diff --quiet; then
git add -A
git commit -m "fmt: cargo fmt --all"
git push
else
echo "no fmt changes"
fi
rust-ci:
needs: fmt
uses: hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@84355587cb2a1f86e6882de83514a32db2646e7a # re-pinned 2026-07-01: toolchain-required-input fix (standards#439)
6 changes: 2 additions & 4 deletions crates/presswerk-core/src/provenance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -528,16 +528,14 @@ impl From<FormProvenanceLegacy> for FfpRecord {
filled_at: None,
appearances_generated: None,
})
} else if let Some(p) = &legacy.producer {
Some(FfpDeclared {
} else {
legacy.producer.as_ref().map(|p| FfpDeclared {
filled_by: "machine".to_string(),
tool: Some(p.clone()),
tool_version: None,
filled_at: None,
appearances_generated: None,
})
} else {
None
};
let mut evidence = Vec::new();
if legacy.need_appearances {
Expand Down
21 changes: 11 additions & 10 deletions crates/presswerk-document/src/pdf/writer.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,27 @@
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
//
// PDF writer — create new PDF documents from text or images using `printpdf` 0.8.
// PDF writer — create new PDF documents from text or images using `printpdf` 0.9.
//
// printpdf 0.8 uses a data-oriented API: documents are built by constructing
// printpdf 0.9 uses a data-oriented API: documents are built by constructing
// `PdfPage` structs containing `Vec<Op>` operation lists, then serialised via
// `PdfDocument::save()`.
// `PdfDocument::save()`. Text is emitted with the 1:1 PDF operators
// `Op::SetFont` (`Tf`) and `Op::ShowText` (`Tj`/`TJ`); the 0.8-era
// `SetFontSizeBuiltinFont` / `WriteTextBuiltinFont` ops were removed upstream.

use std::path::Path;

use presswerk_core::PaperSize;
use presswerk_core::error::PresswerkError;
use printpdf::{
BuiltinFont, Mm, Op, PdfDocument, PdfPage, PdfSaveOptions, PdfWarnMsg, Point, Pt, RawImage,
RawImageData, RawImageFormat, TextItem, XObjectTransform,
BuiltinFont, Mm, Op, PdfDocument, PdfFontHandle, PdfPage, PdfSaveOptions, PdfWarnMsg, Point,
Pt, RawImage, RawImageData, RawImageFormat, TextItem, XObjectTransform,
};
use tracing::{debug, info, instrument};

/// Creates new PDF documents from text content or raster images.
///
/// Uses `printpdf` 0.8 for generation, producing standards-compliant PDF output
/// Uses `printpdf` 0.9 for generation, producing standards-compliant PDF output
/// suitable for printing.
pub struct PdfWriter {
/// Paper size for page creation.
Expand Down Expand Up @@ -119,13 +121,12 @@ impl PdfWriter {
y: Pt(y_pt),
},
});
ops.push(Op::SetFontSizeBuiltinFont {
ops.push(Op::SetFont {
font: PdfFontHandle::Builtin(BuiltinFont::Helvetica),
size: Pt(font_size_pt),
font: BuiltinFont::Helvetica,
});
ops.push(Op::WriteTextBuiltinFont {
ops.push(Op::ShowText {
items: vec![TextItem::Text(line.clone())],
font: BuiltinFont::Helvetica,
});
ops.push(Op::EndTextSection);

Expand Down
98 changes: 49 additions & 49 deletions crates/presswerk-document/src/provenance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,54 +81,54 @@ pub fn classify_document(document: &Document, raw: &[u8]) -> FfpRecord {
let mut declared_filled_by: Option<String> = None;

// Step 2 — declared marker (XMP in catalog Metadata)
if let Ok(catalog) = document.catalog() {
if let Ok(meta_obj) = catalog.get(b"Metadata") {
let payload = get_metadata_payload(document, meta_obj);
if payload.is_empty() {
evidence.insert("FFP-E-XMP-UNREADABLE".to_string());
} else if contains_bytes(&payload, FFP_NS) {
// Namespace present — extract properties
let filled_by = xmp_extract(&payload, "filledBy");
let tool = xmp_extract(&payload, "tool");
let tool_version = xmp_extract(&payload, "toolVersion");
let filled_at = xmp_extract(&payload, "filledAt");
let ap_gen_str = xmp_extract(&payload, "appearancesGenerated");
let ap_gen = ap_gen_str
.as_deref()
.map(|s| s.eq_ignore_ascii_case("true"));
if let Ok(catalog) = document.catalog()
&& let Ok(meta_obj) = catalog.get(b"Metadata")
{
let payload = get_metadata_payload(document, meta_obj);
if payload.is_empty() {
evidence.insert("FFP-E-XMP-UNREADABLE".to_string());
} else if contains_bytes(&payload, FFP_NS) {
// Namespace present — extract properties
let filled_by = xmp_extract(&payload, "filledBy");
let tool = xmp_extract(&payload, "tool");
let tool_version = xmp_extract(&payload, "toolVersion");
let filled_at = xmp_extract(&payload, "filledAt");
let ap_gen_str = xmp_extract(&payload, "appearancesGenerated");
let ap_gen = ap_gen_str
.as_deref()
.map(|s| s.eq_ignore_ascii_case("true"));

if let Some(fb) = filled_by {
let fb_trim = fb.trim().to_string();
if fb_trim == "machine" {
evidence.insert("FFP-E-DECL-MACHINE".to_string());
declared_filled_by = Some(fb_trim.clone());
declared = Some(FfpDeclared {
filled_by: fb_trim,
tool: tool.filter(|s| !s.trim().is_empty()),
tool_version: tool_version.filter(|s| !s.trim().is_empty()),
filled_at: filled_at.filter(|s| !s.trim().is_empty()),
appearances_generated: ap_gen,
});
} else {
evidence.insert("FFP-E-DECL-UNRECOGNISED".to_string());
// Still record declared verbatim per spec
declared = Some(FfpDeclared {
filled_by: fb_trim.clone(),
tool: tool.filter(|s| !s.trim().is_empty()),
tool_version: tool_version.filter(|s| !s.trim().is_empty()),
filled_at: filled_at.filter(|s| !s.trim().is_empty()),
appearances_generated: ap_gen,
});
declared_filled_by = Some(fb_trim);
// For classification, unrecognised does not count as machine
}
if let Some(fb) = filled_by {
let fb_trim = fb.trim().to_string();
if fb_trim == "machine" {
evidence.insert("FFP-E-DECL-MACHINE".to_string());
declared_filled_by = Some(fb_trim.clone());
declared = Some(FfpDeclared {
filled_by: fb_trim,
tool: tool.filter(|s| !s.trim().is_empty()),
tool_version: tool_version.filter(|s| !s.trim().is_empty()),
filled_at: filled_at.filter(|s| !s.trim().is_empty()),
appearances_generated: ap_gen,
});
} else {
evidence.insert("FFP-E-DECL-UNRECOGNISED".to_string());
// Namespace present but filledBy absent
// Still record declared verbatim per spec
declared = Some(FfpDeclared {
filled_by: fb_trim.clone(),
tool: tool.filter(|s| !s.trim().is_empty()),
tool_version: tool_version.filter(|s| !s.trim().is_empty()),
filled_at: filled_at.filter(|s| !s.trim().is_empty()),
appearances_generated: ap_gen,
});
declared_filled_by = Some(fb_trim);
// For classification, unrecognised does not count as machine
}
} else {
// Metadata present but no FFP namespace — no declaration evidence
evidence.insert("FFP-E-DECL-UNRECOGNISED".to_string());
// Namespace present but filledBy absent
}
} else {
// Metadata present but no FFP namespace — no declaration evidence
}
}
// If Metadata not present, no XMP evidence.
Expand Down Expand Up @@ -580,7 +580,8 @@ fn is_meaningful(document: &Document, ft: &str, v_obj: Option<&Object>, v_raw: &
return !name.eq_ignore_ascii_case(b"Off") && !name.is_empty();
}
Object::String(bytes, _) => {
return !bytes.eq_ignore_ascii_case(b"Off") && !bytes.is_empty();
return !bytes.eq_ignore_ascii_case(b"Off")
&& !bytes.is_empty();
}
_ => return false,
}
Expand Down Expand Up @@ -613,12 +614,11 @@ fn is_meaningful(document: &Document, ft: &str, v_obj: Option<&Object>, v_raw: &
if is_string_object_meaningful(document, item) {
return true;
}
if let Object::Reference(id) = item {
if let Ok(res) = document.get_object(*id) {
if is_string_object_meaningful(document, res) {
return true;
}
}
if let Object::Reference(id) = item
&& let Ok(res) = document.get_object(*id)
&& is_string_object_meaningful(document, res)
{
return true;
}
}
return false;
Expand Down
7 changes: 6 additions & 1 deletion crates/presswerk-security/src/audit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,9 +199,14 @@ impl AuditLog {

/// Return the total number of entries in the audit log.
pub fn count(&self) -> Result<u64, PresswerkError> {
// SQLite integers are signed 64-bit; rusqlite has no `FromSql` impl
// for `u64`, so read as `i64` and widen (COUNT(*) is never negative).
self.conn
.query_row("SELECT COUNT(*) FROM audit_log", [], |row| row.get(0))
.query_row("SELECT COUNT(*) FROM audit_log", [], |row| {
row.get::<_, i64>(0)
})
.map_err(db_err)
.map(|n| n as u64)
}
}

Expand Down
Loading