mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-14 10:34:06 +02:00
V1 merge (#5193)
# Description of Changes <!-- Please provide a summary of the changes, including: - What was changed - Why the change was made - Any challenges encountered Closes #(issue_number) --> --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --------- Signed-off-by: dependabot[bot] <[email protected]> Signed-off-by: Balázs Szücs <[email protected]> Signed-off-by: stirlingbot[bot] <stirlingbot[bot]@users.noreply.github.com> Co-authored-by: ConnorYoh <[email protected]> Co-authored-by: Connor Yoh <[email protected]> Co-authored-by: OUNZAR Aymane <[email protected]> Co-authored-by: YAOU Reda <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com> Co-authored-by: Balázs Szücs <[email protected]> Co-authored-by: Ludy <[email protected]> Co-authored-by: tkymmm <[email protected]> Co-authored-by: Peter Dave Hello <[email protected]> Co-authored-by: albanobattistella <[email protected]> Co-authored-by: PingLin8888 <[email protected]> Co-authored-by: FdaSilvaYY <[email protected]> Co-authored-by: Copilot <[email protected]> Co-authored-by: OteJlo <[email protected]> Co-authored-by: Angel <[email protected]> Co-authored-by: Ricardo Catarino <[email protected]> Co-authored-by: Luis Antonio Argüelles González <[email protected]> Co-authored-by: Dawid Urbański <[email protected]> Co-authored-by: Stephan Paternotte <[email protected]> Co-authored-by: Leonardo Santos Paulucio <[email protected]> Co-authored-by: hamza khalem <[email protected]> Co-authored-by: IT Creativity + Art Team <[email protected]> Co-authored-by: Reece Browne <[email protected]> Co-authored-by: James Brunton <[email protected]> Co-authored-by: Victor Villarreal <[email protected]>
This commit is contained in:
co-authored by
ConnorYoh
Connor Yoh
OUNZAR Aymane
YAOU Reda
dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
Balázs Szücs
Ludy
tkymmm
Peter Dave Hello
albanobattistella
PingLin8888
FdaSilvaYY
Copilot
OteJlo
Angel
Ricardo Catarino
Luis Antonio Argüelles González
Dawid Urbański
Stephan Paternotte
Leonardo Santos Paulucio
hamza khalem
IT Creativity + Art Team
Reece Browne
James Brunton
Victor Villarreal
parent
a5dcdd5bd9
commit
68ed54e398
+17
-13
@@ -6,22 +6,27 @@ app: &app
|
|||||||
- app/(common|core|proprietary)/src/main/java/**
|
- app/(common|core|proprietary)/src/main/java/**
|
||||||
|
|
||||||
openapi: &openapi
|
openapi: &openapi
|
||||||
- build.gradle
|
- *build
|
||||||
- app/(common|core|proprietary)/build.gradle
|
- *app
|
||||||
- app/(common|core|proprietary)/src/main/java/**
|
|
||||||
|
|
||||||
project: &project
|
docker: &docker
|
||||||
- app/(common|core|proprietary)/src/(main|test)/java/**
|
|
||||||
- app/(common|core|proprietary)/build.gradle
|
|
||||||
- 'app/(common|core|proprietary)/src/(main|test)/resources/**/!(messages_*.properties|*.md)*'
|
|
||||||
- exampleYmlFiles/**
|
|
||||||
- gradle/**
|
|
||||||
- libs/**
|
|
||||||
- testing/**
|
|
||||||
- build.gradle
|
|
||||||
- Dockerfile
|
- Dockerfile
|
||||||
- Dockerfile.fat
|
- Dockerfile.fat
|
||||||
- Dockerfile.ultra-lite
|
- Dockerfile.ultra-lite
|
||||||
|
- ".github/workflows/build.yml"
|
||||||
|
- scripts/init.sh
|
||||||
|
- scripts/init-without-ocr.sh
|
||||||
|
- exampleYmlFiles/**
|
||||||
|
|
||||||
|
project: &project
|
||||||
|
- app/(common|core|proprietary)/src/(main|test)/java/**
|
||||||
|
- *build
|
||||||
|
- "app/(common|core|proprietary)/src/(main|test)/resources/**/!(messages_*.properties|*.md)*"
|
||||||
|
- exampleYmlFiles/**
|
||||||
|
- gradle/**
|
||||||
|
- libs/**
|
||||||
|
- "testing/**/!(requirements*.txt|requirements*.in)*"
|
||||||
|
- *docker
|
||||||
- gradle.properties
|
- gradle.properties
|
||||||
- gradlew
|
- gradlew
|
||||||
- gradlew.bat
|
- gradlew.bat
|
||||||
@@ -29,7 +34,6 @@ project: &project
|
|||||||
- settings.gradle
|
- settings.gradle
|
||||||
- frontend/**
|
- frontend/**
|
||||||
- docker/**
|
- docker/**
|
||||||
- testing/**
|
|
||||||
|
|
||||||
frontend: &frontend
|
frontend: &frontend
|
||||||
- frontend/**
|
- frontend/**
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
{
|
{
|
||||||
|
"label_changer": [
|
||||||
|
"Frooodle",
|
||||||
|
"Ludy87",
|
||||||
|
"balazs-szucs"
|
||||||
|
],
|
||||||
"repo_devs": [
|
"repo_devs": [
|
||||||
"Frooodle",
|
"Frooodle",
|
||||||
"sf298",
|
"sf298",
|
||||||
|
|||||||
@@ -184,3 +184,6 @@
|
|||||||
- name: "codex"
|
- name: "codex"
|
||||||
color: "ededed"
|
color: "ededed"
|
||||||
description: "chatgpt AI generated code"
|
description: "chatgpt AI generated code"
|
||||||
|
- name: "break-change"
|
||||||
|
color: "FF0000"
|
||||||
|
description: "This PR introduces a breaking API change."
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ Closes #(issue_number)
|
|||||||
- [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed)
|
- [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed)
|
||||||
- [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only)
|
- [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only)
|
||||||
|
|
||||||
|
### Translations (if applicable)
|
||||||
|
|
||||||
|
- [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)
|
||||||
|
|
||||||
### UI Changes (if applicable)
|
### UI Changes (if applicable)
|
||||||
|
|
||||||
- [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR)
|
- [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR)
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
changelog:
|
changelog:
|
||||||
categories:
|
categories:
|
||||||
|
- title: Breaking Changes
|
||||||
|
labels:
|
||||||
|
- break-change
|
||||||
|
|
||||||
- title: Bug Fixes
|
- title: Bug Fixes
|
||||||
labels:
|
labels:
|
||||||
- Bug
|
- Bug
|
||||||
|
|||||||
@@ -6,3 +6,4 @@ pillow
|
|||||||
unoserver
|
unoserver
|
||||||
opencv-python-headless
|
opencv-python-headless
|
||||||
pre-commit
|
pre-commit
|
||||||
|
brotli @ git+https://github.com/google/brotli.git@028fb5a23661f123017c060daa546b55cf4bde29
|
||||||
|
|||||||
@@ -4,201 +4,98 @@
|
|||||||
#
|
#
|
||||||
# pip-compile --allow-unsafe --generate-hashes --output-file='.github\scripts\requirements_dev.txt' --strip-extras '.github\scripts\requirements_dev.in'
|
# pip-compile --allow-unsafe --generate-hashes --output-file='.github\scripts\requirements_dev.txt' --strip-extras '.github\scripts\requirements_dev.in'
|
||||||
#
|
#
|
||||||
brotli==1.1.0 \
|
# WARNING: pip install will require the following package to be hashed.
|
||||||
--hash=sha256:03d20af184290887bdea3f0f78c4f737d126c74dc2f3ccadf07e54ceca3bf208 \
|
# Consider using a hashable URL like https://github.com/jazzband/pip-tools/archive/SOMECOMMIT.zip
|
||||||
--hash=sha256:0541e747cce78e24ea12d69176f6a7ddb690e62c425e01d31cc065e69ce55b48 \
|
# CVE-2025-6176 mitigation: pin brotli to a specific commit
|
||||||
--hash=sha256:069a121ac97412d1fe506da790b3e69f52254b9df4eb665cd42460c837193354 \
|
brotli @ git+https://github.com/google/brotli.git@028fb5a23661f123017c060daa546b55cf4bde29
|
||||||
--hash=sha256:0737ddb3068957cf1b054899b0883830bb1fec522ec76b1098f9b6e0f02d9419 \
|
# via
|
||||||
--hash=sha256:0b63b949ff929fbc2d6d3ce0e924c9b93c9785d877a21a1b678877ffbbc4423a \
|
# -r .github/scripts/requirements_dev.in
|
||||||
--hash=sha256:0c6244521dda65ea562d5a69b9a26120769b7a9fb3db2fe9545935ed6735b128 \
|
# fonttools
|
||||||
--hash=sha256:11d00ed0a83fa22d29bc6b64ef636c4552ebafcef57154b4ddd132f5638fbd1c \
|
cffi==2.0.0 \
|
||||||
--hash=sha256:141bd4d93984070e097521ed07e2575b46f817d08f9fa42b16b9b5f27b5ac088 \
|
--hash=sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb \
|
||||||
--hash=sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9 \
|
--hash=sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b \
|
||||||
--hash=sha256:1ab4fbee0b2d9098c74f3057b2bc055a8bd92ccf02f65944a241b4349229185a \
|
--hash=sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f \
|
||||||
--hash=sha256:1ae56aca0402a0f9a3431cddda62ad71666ca9d4dc3a10a142b9dce2e3c0cda3 \
|
--hash=sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9 \
|
||||||
--hash=sha256:1b2c248cd517c222d89e74669a4adfa5577e06ab68771a529060cf5a156e9757 \
|
--hash=sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44 \
|
||||||
--hash=sha256:1e9a65b5736232e7a7f91ff3d02277f11d339bf34099a56cdab6a8b3410a02b2 \
|
--hash=sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2 \
|
||||||
--hash=sha256:224e57f6eac61cc449f498cc5f0e1725ba2071a3d4f48d5d9dffba42db196438 \
|
--hash=sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c \
|
||||||
--hash=sha256:22fc2a8549ffe699bfba2256ab2ed0421a7b8fadff114a3d201794e45a9ff578 \
|
--hash=sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75 \
|
||||||
--hash=sha256:23032ae55523cc7bccb4f6a0bf368cd25ad9bcdcc1990b64a647e7bbcce9cb5b \
|
--hash=sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65 \
|
||||||
--hash=sha256:2333e30a5e00fe0fe55903c8832e08ee9c3b1382aacf4db26664a16528d51b4b \
|
--hash=sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e \
|
||||||
--hash=sha256:2954c1c23f81c2eaf0b0717d9380bd348578a94161a65b3a2afc62c86467dd68 \
|
--hash=sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a \
|
||||||
--hash=sha256:2a24c50840d89ded6c9a8fdc7b6ed3692ed4e86f1c4a4a938e1e92def92933e0 \
|
--hash=sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e \
|
||||||
--hash=sha256:2de9d02f5bda03d27ede52e8cfe7b865b066fa49258cbab568720aa5be80a47d \
|
--hash=sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25 \
|
||||||
--hash=sha256:2feb1d960f760a575dbc5ab3b1c00504b24caaf6986e2dc2b01c09c87866a943 \
|
--hash=sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a \
|
||||||
--hash=sha256:30924eb4c57903d5a7526b08ef4a584acc22ab1ffa085faceb521521d2de32dd \
|
--hash=sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe \
|
||||||
--hash=sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409 \
|
--hash=sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b \
|
||||||
--hash=sha256:32d95b80260d79926f5fab3c41701dbb818fde1c9da590e77e571eefd14abe28 \
|
--hash=sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91 \
|
||||||
--hash=sha256:38025d9f30cf4634f8309c6874ef871b841eb3c347e90b0851f63d1ded5212da \
|
--hash=sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592 \
|
||||||
--hash=sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50 \
|
--hash=sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187 \
|
||||||
--hash=sha256:3c0ef38c7a7014ffac184db9e04debe495d317cc9c6fb10071f7fefd93100a4f \
|
--hash=sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c \
|
||||||
--hash=sha256:3d7954194c36e304e1523f55d7042c59dc53ec20dd4e9ea9d151f1b62b4415c0 \
|
--hash=sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1 \
|
||||||
--hash=sha256:3ee8a80d67a4334482d9712b8e83ca6b1d9bc7e351931252ebef5d8f7335a547 \
|
--hash=sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94 \
|
||||||
--hash=sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180 \
|
--hash=sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba \
|
||||||
--hash=sha256:43395e90523f9c23a3d5bdf004733246fba087f2948f87ab28015f12359ca6a0 \
|
--hash=sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb \
|
||||||
--hash=sha256:43ce1b9935bfa1ede40028054d7f48b5469cd02733a365eec8a329ffd342915d \
|
--hash=sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165 \
|
||||||
--hash=sha256:4410f84b33374409552ac9b6903507cdb31cd30d2501fc5ca13d18f73548444a \
|
--hash=sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529 \
|
||||||
--hash=sha256:494994f807ba0b92092a163a0a283961369a65f6cbe01e8891132b7a320e61eb \
|
--hash=sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca \
|
||||||
--hash=sha256:4d4a848d1837973bf0f4b5e54e3bec977d99be36a7895c61abb659301b02c112 \
|
--hash=sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c \
|
||||||
--hash=sha256:4ed11165dd45ce798d99a136808a794a748d5dc38511303239d4e2363c0695dc \
|
--hash=sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6 \
|
||||||
--hash=sha256:4f3607b129417e111e30637af1b56f24f7a49e64763253bbc275c75fa887d4b2 \
|
--hash=sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c \
|
||||||
--hash=sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265 \
|
--hash=sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0 \
|
||||||
--hash=sha256:524f35912131cc2cabb00edfd8d573b07f2d9f21fa824bd3fb19725a9cf06327 \
|
--hash=sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743 \
|
||||||
--hash=sha256:587ca6d3cef6e4e868102672d3bd9dc9698c309ba56d41c2b9c85bbb903cdb95 \
|
--hash=sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63 \
|
||||||
--hash=sha256:58d4b711689366d4a03ac7957ab8c28890415e267f9b6589969e74b6e42225ec \
|
--hash=sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5 \
|
||||||
--hash=sha256:5b3cc074004d968722f51e550b41a27be656ec48f8afaeeb45ebf65b561481dd \
|
--hash=sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5 \
|
||||||
--hash=sha256:5dab0844f2cf82be357a0eb11a9087f70c5430b2c241493fc122bb6f2bb0917c \
|
--hash=sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4 \
|
||||||
--hash=sha256:5e55da2c8724191e5b557f8e18943b1b4839b8efc3ef60d65985bcf6f587dd38 \
|
--hash=sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d \
|
||||||
--hash=sha256:5eeb539606f18a0b232d4ba45adccde4125592f3f636a6182b4a8a436548b914 \
|
--hash=sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b \
|
||||||
--hash=sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0 \
|
--hash=sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93 \
|
||||||
--hash=sha256:5fb2ce4b8045c78ebbc7b8f3c15062e435d47e7393cc57c25115cfd49883747a \
|
--hash=sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205 \
|
||||||
--hash=sha256:6172447e1b368dcbc458925e5ddaf9113477b0ed542df258d84fa28fc45ceea7 \
|
--hash=sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27 \
|
||||||
--hash=sha256:6967ced6730aed543b8673008b5a391c3b1076d834ca438bbd70635c73775368 \
|
--hash=sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512 \
|
||||||
--hash=sha256:6974f52a02321b36847cd19d1b8e381bf39939c21efd6ee2fc13a28b0d99348c \
|
--hash=sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d \
|
||||||
--hash=sha256:6c3020404e0b5eefd7c9485ccf8393cfb75ec38ce75586e046573c9dc29967a0 \
|
--hash=sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c \
|
||||||
--hash=sha256:6c6e0c425f22c1c719c42670d561ad682f7bfeeef918edea971a79ac5252437f \
|
--hash=sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037 \
|
||||||
--hash=sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451 \
|
--hash=sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26 \
|
||||||
--hash=sha256:7905193081db9bfa73b1219140b3d315831cbff0d8941f22da695832f0dd188f \
|
--hash=sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322 \
|
||||||
--hash=sha256:7bc37c4d6b87fb1017ea28c9508b36bbcb0c3d18b4260fcdf08b200c74a6aee8 \
|
--hash=sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb \
|
||||||
--hash=sha256:7c4855522edb2e6ae7fdb58e07c3ba9111e7621a8956f481c68d5d979c93032e \
|
--hash=sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c \
|
||||||
--hash=sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248 \
|
--hash=sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8 \
|
||||||
--hash=sha256:7eedaa5d036d9336c95915035fb57422054014ebdeb6f3b42eac809928e40d0c \
|
--hash=sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4 \
|
||||||
--hash=sha256:7f4bf76817c14aa98cc6697ac02f3972cb8c3da93e9ef16b9c66573a68014f91 \
|
--hash=sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414 \
|
||||||
--hash=sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724 \
|
--hash=sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9 \
|
||||||
--hash=sha256:832436e59afb93e1836081a20f324cb185836c617659b07b129141a8426973c7 \
|
--hash=sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664 \
|
||||||
--hash=sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966 \
|
--hash=sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9 \
|
||||||
--hash=sha256:87a3044c3a35055527ac75e419dfa9f4f3667a1e887ee80360589eb8c90aabb9 \
|
--hash=sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775 \
|
||||||
--hash=sha256:890b5a14ce214389b2cc36ce82f3093f96f4cc730c1cffdbefff77a7c71f2a97 \
|
--hash=sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739 \
|
||||||
--hash=sha256:89f4988c7203739d48c6f806f1e87a1d96e0806d44f0fba61dba81392c9e474d \
|
--hash=sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc \
|
||||||
--hash=sha256:8bf32b98b75c13ec7cf774164172683d6e7891088f6316e54425fde1efc276d5 \
|
--hash=sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062 \
|
||||||
--hash=sha256:8dadd1314583ec0bf2d1379f7008ad627cd6336625d6679cf2f8e67081b83acf \
|
--hash=sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe \
|
||||||
--hash=sha256:901032ff242d479a0efa956d853d16875d42157f98951c0230f69e69f9c09bac \
|
--hash=sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9 \
|
||||||
--hash=sha256:9011560a466d2eb3f5a6e4929cf4a09be405c64154e12df0dd72713f6500e32b \
|
--hash=sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92 \
|
||||||
--hash=sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951 \
|
--hash=sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5 \
|
||||||
--hash=sha256:919e32f147ae93a09fe064d77d5ebf4e35502a8df75c29fb05788528e330fe74 \
|
--hash=sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13 \
|
||||||
--hash=sha256:91d7cc2a76b5567591d12c01f019dd7afce6ba8cba6571187e21e2fc418ae648 \
|
--hash=sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d \
|
||||||
--hash=sha256:929811df5462e182b13920da56c6e0284af407d1de637d8e536c5cd00a7daf60 \
|
--hash=sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26 \
|
||||||
--hash=sha256:949f3b7c29912693cee0afcf09acd6ebc04c57af949d9bf77d6101ebb61e388c \
|
--hash=sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f \
|
||||||
--hash=sha256:a090ca607cbb6a34b0391776f0cb48062081f5f60ddcce5d11838e67a01928d1 \
|
--hash=sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495 \
|
||||||
--hash=sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8 \
|
--hash=sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b \
|
||||||
--hash=sha256:a37b8f0391212d29b3a91a799c8e4a2855e0576911cdfb2515487e30e322253d \
|
--hash=sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6 \
|
||||||
--hash=sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc \
|
--hash=sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c \
|
||||||
--hash=sha256:a469274ad18dc0e4d316eefa616d1d0c2ff9da369af19fa6f3daa4f09671fd61 \
|
--hash=sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef \
|
||||||
--hash=sha256:a599669fd7c47233438a56936988a2478685e74854088ef5293802123b5b2460 \
|
--hash=sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5 \
|
||||||
--hash=sha256:a743e5a28af5f70f9c080380a5f908d4d21d40e8f0e0c8901604d15cfa9ba751 \
|
--hash=sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18 \
|
||||||
--hash=sha256:a77def80806c421b4b0af06f45d65a136e7ac0bdca3c09d9e2ea4e515367c7e9 \
|
--hash=sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad \
|
||||||
--hash=sha256:a7e53012d2853a07a4a79c00643832161a910674a893d296c9f1259859a289d2 \
|
--hash=sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3 \
|
||||||
--hash=sha256:a93dde851926f4f2678e704fadeb39e16c35d8baebd5252c9fd94ce8ce68c4a0 \
|
--hash=sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7 \
|
||||||
--hash=sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1 \
|
--hash=sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5 \
|
||||||
--hash=sha256:ae15b066e5ad21366600ebec29a7ccbc86812ed267e4b28e860b8ca16a2bc474 \
|
--hash=sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534 \
|
||||||
--hash=sha256:aea440a510e14e818e67bfc4027880e2fb500c2ccb20ab21c7a7c8b5b4703d75 \
|
--hash=sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49 \
|
||||||
--hash=sha256:af6fa6817889314555aede9a919612b23739395ce767fe7fcbea9a80bf140fe5 \
|
--hash=sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2 \
|
||||||
--hash=sha256:b760c65308ff1e462f65d69c12e4ae085cff3b332d894637f6273a12a482d09f \
|
--hash=sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5 \
|
||||||
--hash=sha256:be36e3d172dc816333f33520154d708a2657ea63762ec16b62ece02ab5e4daf2 \
|
--hash=sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453 \
|
||||||
--hash=sha256:c247dd99d39e0338a604f8c2b3bc7061d5c2e9e2ac7ba9cc1be5a69cb6cd832f \
|
--hash=sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf
|
||||||
--hash=sha256:c5529b34c1c9d937168297f2c1fde7ebe9ebdd5e121297ff9c043bdb2ae3d6fb \
|
|
||||||
--hash=sha256:c8146669223164fc87a7e3de9f81e9423c67a79d6b3447994dfb9c95da16e2d6 \
|
|
||||||
--hash=sha256:c8fd5270e906eef71d4a8d19b7c6a43760c6abcfcc10c9101d14eb2357418de9 \
|
|
||||||
--hash=sha256:ca63e1890ede90b2e4454f9a65135a4d387a4585ff8282bb72964fab893f2111 \
|
|
||||||
--hash=sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2 \
|
|
||||||
--hash=sha256:cb1dac1770878ade83f2ccdf7d25e494f05c9165f5246b46a621cc849341dc01 \
|
|
||||||
--hash=sha256:cdad5b9014d83ca68c25d2e9444e28e967ef16e80f6b436918c700c117a85467 \
|
|
||||||
--hash=sha256:cdbc1fc1bc0bff1cef838eafe581b55bfbffaed4ed0318b724d0b71d4d377619 \
|
|
||||||
--hash=sha256:ceb64bbc6eac5a140ca649003756940f8d6a7c444a68af170b3187623b43bebf \
|
|
||||||
--hash=sha256:d0c5516f0aed654134a2fc936325cc2e642f8a0e096d075209672eb321cff408 \
|
|
||||||
--hash=sha256:d143fd47fad1db3d7c27a1b1d66162e855b5d50a89666af46e1679c496e8e579 \
|
|
||||||
--hash=sha256:d192f0f30804e55db0d0e0a35d83a9fead0e9a359a9ed0285dbacea60cc10a84 \
|
|
||||||
--hash=sha256:d2b35ca2c7f81d173d2fadc2f4f31e88cc5f7a39ae5b6db5513cf3383b0e0ec7 \
|
|
||||||
--hash=sha256:d342778ef319e1026af243ed0a07c97acf3bad33b9f29e7ae6a1f68fd083e90c \
|
|
||||||
--hash=sha256:d487f5432bf35b60ed625d7e1b448e2dc855422e87469e3f450aa5552b0eb284 \
|
|
||||||
--hash=sha256:d7702622a8b40c49bffb46e1e3ba2e81268d5c04a34f460978c6b5517a34dd52 \
|
|
||||||
--hash=sha256:db85ecf4e609a48f4b29055f1e144231b90edc90af7481aa731ba2d059226b1b \
|
|
||||||
--hash=sha256:de6551e370ef19f8de1807d0a9aa2cdfdce2e85ce88b122fe9f6b2b076837e59 \
|
|
||||||
--hash=sha256:e1140c64812cb9b06c922e77f1c26a75ec5e3f0fb2bf92cc8c58720dec276752 \
|
|
||||||
--hash=sha256:e4fe605b917c70283db7dfe5ada75e04561479075761a0b3866c081d035b01c1 \
|
|
||||||
--hash=sha256:e6a904cb26bfefc2f0a6f240bdf5233be78cd2488900a2f846f3c3ac8489ab80 \
|
|
||||||
--hash=sha256:e79e6520141d792237c70bcd7a3b122d00f2613769ae0cb61c52e89fd3443839 \
|
|
||||||
--hash=sha256:e84799f09591700a4154154cab9787452925578841a94321d5ee8fb9a9a328f0 \
|
|
||||||
--hash=sha256:e93dfc1a1165e385cc8239fab7c036fb2cd8093728cbd85097b284d7b99249a2 \
|
|
||||||
--hash=sha256:efa8b278894b14d6da122a72fefcebc28445f2d3f880ac59d46c90f4c13be9a3 \
|
|
||||||
--hash=sha256:f0d8a7a6b5983c2496e364b969f0e526647a06b075d034f3297dc66f3b360c64 \
|
|
||||||
--hash=sha256:f0db75f47be8b8abc8d9e31bc7aad0547ca26f24a54e6fd10231d623f183d089 \
|
|
||||||
--hash=sha256:f296c40e23065d0d6650c4aefe7470d2a25fffda489bcc3eb66083f3ac9f6643 \
|
|
||||||
--hash=sha256:f31859074d57b4639318523d6ffdca586ace54271a73ad23ad021acd807eb14b \
|
|
||||||
--hash=sha256:f66b5337fa213f1da0d9000bc8dc0cb5b896b726eefd9c6046f699b169c41b9e \
|
|
||||||
--hash=sha256:f733d788519c7e3e71f0855c96618720f5d3d60c3cb829d8bbb722dddce37985 \
|
|
||||||
--hash=sha256:fce1473f3ccc4187f75b4690cfc922628aed4d3dd013d047f95a9b3919a86596 \
|
|
||||||
--hash=sha256:fd5f17ff8f14003595ab414e45fce13d073e0762394f957182e69035c9f3d7c2 \
|
|
||||||
--hash=sha256:fdc3ff3bfccdc6b9cc7c342c03aa2400683f0cb891d46e94b64a197910dc4064
|
|
||||||
# via fonttools
|
|
||||||
cffi==1.17.1 \
|
|
||||||
--hash=sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8 \
|
|
||||||
--hash=sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2 \
|
|
||||||
--hash=sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1 \
|
|
||||||
--hash=sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15 \
|
|
||||||
--hash=sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36 \
|
|
||||||
--hash=sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824 \
|
|
||||||
--hash=sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8 \
|
|
||||||
--hash=sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36 \
|
|
||||||
--hash=sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17 \
|
|
||||||
--hash=sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf \
|
|
||||||
--hash=sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc \
|
|
||||||
--hash=sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3 \
|
|
||||||
--hash=sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed \
|
|
||||||
--hash=sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702 \
|
|
||||||
--hash=sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1 \
|
|
||||||
--hash=sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8 \
|
|
||||||
--hash=sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903 \
|
|
||||||
--hash=sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6 \
|
|
||||||
--hash=sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d \
|
|
||||||
--hash=sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b \
|
|
||||||
--hash=sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e \
|
|
||||||
--hash=sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be \
|
|
||||||
--hash=sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c \
|
|
||||||
--hash=sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683 \
|
|
||||||
--hash=sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9 \
|
|
||||||
--hash=sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c \
|
|
||||||
--hash=sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8 \
|
|
||||||
--hash=sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1 \
|
|
||||||
--hash=sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4 \
|
|
||||||
--hash=sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655 \
|
|
||||||
--hash=sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67 \
|
|
||||||
--hash=sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595 \
|
|
||||||
--hash=sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0 \
|
|
||||||
--hash=sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65 \
|
|
||||||
--hash=sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41 \
|
|
||||||
--hash=sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6 \
|
|
||||||
--hash=sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401 \
|
|
||||||
--hash=sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6 \
|
|
||||||
--hash=sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3 \
|
|
||||||
--hash=sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16 \
|
|
||||||
--hash=sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93 \
|
|
||||||
--hash=sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e \
|
|
||||||
--hash=sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4 \
|
|
||||||
--hash=sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964 \
|
|
||||||
--hash=sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c \
|
|
||||||
--hash=sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576 \
|
|
||||||
--hash=sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0 \
|
|
||||||
--hash=sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3 \
|
|
||||||
--hash=sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662 \
|
|
||||||
--hash=sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3 \
|
|
||||||
--hash=sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff \
|
|
||||||
--hash=sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5 \
|
|
||||||
--hash=sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd \
|
|
||||||
--hash=sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f \
|
|
||||||
--hash=sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5 \
|
|
||||||
--hash=sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14 \
|
|
||||||
--hash=sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d \
|
|
||||||
--hash=sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9 \
|
|
||||||
--hash=sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7 \
|
|
||||||
--hash=sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382 \
|
|
||||||
--hash=sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a \
|
|
||||||
--hash=sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e \
|
|
||||||
--hash=sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a \
|
|
||||||
--hash=sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4 \
|
|
||||||
--hash=sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99 \
|
|
||||||
--hash=sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87 \
|
|
||||||
--hash=sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b
|
|
||||||
# via weasyprint
|
# via weasyprint
|
||||||
cfgv==3.4.0 \
|
cfgv==3.4.0 \
|
||||||
--hash=sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9 \
|
--hash=sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9 \
|
||||||
@@ -212,57 +109,73 @@ distlib==0.4.0 \
|
|||||||
--hash=sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 \
|
--hash=sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 \
|
||||||
--hash=sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d
|
--hash=sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d
|
||||||
# via virtualenv
|
# via virtualenv
|
||||||
filelock==3.18.0 \
|
filelock==3.20.0 \
|
||||||
--hash=sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2 \
|
--hash=sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2 \
|
||||||
--hash=sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de
|
--hash=sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4
|
||||||
# via virtualenv
|
# via virtualenv
|
||||||
fonttools==4.59.0 \
|
fonttools==4.60.1 \
|
||||||
--hash=sha256:052444a5d0151878e87e3e512a1aa1a0ab35ee4c28afde0a778e23b0ace4a7de \
|
--hash=sha256:022beaea4b73a70295b688f817ddc24ed3e3418b5036ffcd5658141184ef0d0c \
|
||||||
--hash=sha256:169b99a2553a227f7b5fea8d9ecd673aa258617f466b2abc6091fe4512a0dcd0 \
|
--hash=sha256:026290e4ec76583881763fac284aca67365e0be9f13a7fb137257096114cb3bc \
|
||||||
--hash=sha256:209b75943d158f610b78320eacb5539aa9e920bee2c775445b2846c65d20e19d \
|
--hash=sha256:0b0835ed15dd5b40d726bb61c846a688f5b4ce2208ec68779bc81860adb5851a \
|
||||||
--hash=sha256:21e606b2d38fed938dde871c5736822dd6bda7a4631b92e509a1f5cd1b90c5df \
|
--hash=sha256:0eae96373e4b7c9e45d099d7a523444e3554360927225c1cdae221a58a45b856 \
|
||||||
--hash=sha256:241313683afd3baacb32a6bd124d0bce7404bc5280e12e291bae1b9bba28711d \
|
--hash=sha256:122e1a8ada290423c493491d002f622b1992b1ab0b488c68e31c413390dc7eb2 \
|
||||||
--hash=sha256:26731739daa23b872643f0e4072d5939960237d540c35c14e6a06d47d71ca8fe \
|
--hash=sha256:1410155d0e764a4615774e5c2c6fc516259fe3eca5882f034eb9bfdbee056259 \
|
||||||
--hash=sha256:2e7cf8044ce2598bb87e44ba1d2c6e45d7a8decf56055b92906dc53f67c76d64 \
|
--hash=sha256:145daa14bf24824b677b9357c5e44fd8895c2a8f53596e1b9ea3496081dc692c \
|
||||||
--hash=sha256:31003b6a10f70742a63126b80863ab48175fb8272a18ca0846c0482968f0588e \
|
--hash=sha256:1525796c3ffe27bb6268ed2a1bb0dcf214d561dfaf04728abf01489eb5339dce \
|
||||||
--hash=sha256:332bfe685d1ac58ca8d62b8d6c71c2e52a6c64bc218dc8f7825c9ea51385aa01 \
|
--hash=sha256:154cb6ee417e417bf5f7c42fe25858c9140c26f647c7347c06f0cc2d47eff003 \
|
||||||
--hash=sha256:37c377f7cb2ab2eca8a0b319c68146d34a339792f9420fca6cd49cf28d370705 \
|
--hash=sha256:2299df884c11162617a66b7c316957d74a18e3758c0274762d2cc87df7bc0272 \
|
||||||
--hash=sha256:37e01c6ec0c98599778c2e688350d624fa4770fbd6144551bd5e032f1199171c \
|
--hash=sha256:2409d5fb7b55fd70f715e6d34e7a6e4f7511b8ad29a49d6df225ee76da76dd77 \
|
||||||
--hash=sha256:401b1941ce37e78b8fd119b419b617277c65ae9417742a63282257434fd68ea2 \
|
--hash=sha256:268ecda8ca6cb5c4f044b1fb9b3b376e8cd1b361cef275082429dc4174907038 \
|
||||||
--hash=sha256:4536f2695fe5c1ffb528d84a35a7d3967e5558d2af58b4775e7ab1449d65767b \
|
--hash=sha256:282dafa55f9659e8999110bd8ed422ebe1c8aecd0dc396550b038e6c9a08b8ea \
|
||||||
--hash=sha256:4c908a7036f0f3677f8afa577bcd973e3e20ddd2f7c42a33208d18bee95cdb6f \
|
--hash=sha256:2ee06fc57512144d8b0445194c2da9f190f61ad51e230f14836286470c99f854 \
|
||||||
--hash=sha256:51ab1ff33c19e336c02dee1e9fd1abd974a4ca3d8f7eef2a104d0816a241ce97 \
|
--hash=sha256:3630e86c484263eaac71d117085d509cbcf7b18f677906824e4bace598fb70d2 \
|
||||||
--hash=sha256:524133c1be38445c5c0575eacea42dbd44374b310b1ffc4b60ff01d881fabb96 \
|
--hash=sha256:398447f3d8c0c786cbf1209711e79080a40761eb44b27cdafffb48f52bcec258 \
|
||||||
--hash=sha256:57bb7e26928573ee7c6504f54c05860d867fd35e675769f3ce01b52af38d48e2 \
|
--hash=sha256:4ba4bd646e86de16160f0fb72e31c3b9b7d0721c3e5b26b9fa2fc931dfdb2652 \
|
||||||
--hash=sha256:60f6665579e909b618282f3c14fa0b80570fbf1ee0e67678b9a9d43aa5d67a37 \
|
--hash=sha256:5664fd1a9ea7f244487ac8f10340c4e37664675e8667d6fee420766e0fb3cf08 \
|
||||||
--hash=sha256:62224a9bb85b4b66d1b46d45cbe43d71cbf8f527d332b177e3b96191ffbc1e64 \
|
--hash=sha256:583b7f8e3c49486e4d489ad1deacfb8d5be54a8ef34d6df824f6a171f8511d99 \
|
||||||
--hash=sha256:6770d7da00f358183d8fd5c4615436189e4f683bdb6affb02cad3d221d7bb757 \
|
--hash=sha256:596ecaca36367027d525b3b426d8a8208169d09edcf8c7506aceb3a38bfb55c7 \
|
||||||
--hash=sha256:6801aeddb6acb2c42eafa45bc1cb98ba236871ae6f33f31e984670b749a8e58e \
|
--hash=sha256:5c1015318e4fec75dd4943ad5f6a206d9727adf97410d58b7e32ab644a807914 \
|
||||||
--hash=sha256:70d6b3ceaa9cc5a6ac52884f3b3d9544e8e231e95b23f138bdb78e6d4dc0eae3 \
|
--hash=sha256:66929e2ea2810c6533a5184f938502cfdaea4bc3efb7130d8cc02e1c1b4108d6 \
|
||||||
--hash=sha256:78813b49d749e1bb4db1c57f2d4d7e6db22c253cb0a86ad819f5dc197710d4b2 \
|
--hash=sha256:6ec722ee589e89a89f5b7574f5c45604030aa6ae24cb2c751e2707193b466fed \
|
||||||
--hash=sha256:841b2186adce48903c0fef235421ae21549020eca942c1da773ac380b056ab3c \
|
--hash=sha256:6f68576bb4bbf6060c7ab047b1574a1ebe5c50a17de62830079967b211059ebb \
|
||||||
--hash=sha256:84fc186980231a287b28560d3123bd255d3c6b6659828c642b4cf961e2b923d0 \
|
--hash=sha256:7473a8ed9ed09aeaa191301244a5a9dbe46fe0bf54f9d6cd21d83044c3321217 \
|
||||||
--hash=sha256:885bde7d26e5b40e15c47bd5def48b38cbd50830a65f98122a8fb90962af7cd1 \
|
--hash=sha256:7b0c6d57ab00dae9529f3faf187f2254ea0aa1e04215cf2f1a8ec277c96661bc \
|
||||||
--hash=sha256:8b4309a2775e4feee7356e63b163969a215d663399cce1b3d3b65e7ec2d9680e \
|
--hash=sha256:7b4c32e232a71f63a5d00259ca3d88345ce2a43295bb049d21061f338124246f \
|
||||||
--hash=sha256:8d77f92438daeaddc05682f0f3dac90c5b9829bcac75b57e8ce09cb67786073c \
|
--hash=sha256:8177ec9676ea6e1793c8a084a90b65a9f778771998eb919d05db6d4b1c0b114c \
|
||||||
--hash=sha256:902425f5afe28572d65d2bf9c33edd5265c612ff82c69e6f83ea13eafc0dcbea \
|
--hash=sha256:839565cbf14645952d933853e8ade66a463684ed6ed6c9345d0faf1f0e868877 \
|
||||||
--hash=sha256:9bcc1e77fbd1609198966ded6b2a9897bd6c6bcbd2287a2fc7d75f1a254179c5 \
|
--hash=sha256:875cb7764708b3132637f6c5fb385b16eeba0f7ac9fa45a69d35e09b47045801 \
|
||||||
--hash=sha256:a408c3c51358c89b29cfa5317cf11518b7ce5de1717abb55c5ae2d2921027de6 \
|
--hash=sha256:8a44788d9d91df72d1a5eac49b31aeb887a5f4aab761b4cffc4196c74907ea85 \
|
||||||
--hash=sha256:a9bf8adc9e1f3012edc8f09b08336272aec0c55bc677422273e21280db748f7c \
|
--hash=sha256:8b4eb332f9501cb1cd3d4d099374a1e1306783ff95489a1026bde9eb02ccc34a \
|
||||||
--hash=sha256:b818db35879d2edf7f46c7e729c700a0bce03b61b9412f5a7118406687cb151d \
|
--hash=sha256:906306ac7afe2156fcf0042173d6ebbb05416af70f6b370967b47f8f00103bbb \
|
||||||
--hash=sha256:b8974b2a266b54c96709bd5e239979cddfd2dbceed331aa567ea1d7c4a2202db \
|
--hash=sha256:992775c9fbe2cf794786fa0ffca7f09f564ba3499b8fe9f2f80bd7197db60383 \
|
||||||
--hash=sha256:be392ec3529e2f57faa28709d60723a763904f71a2b63aabe14fee6648fe3b14 \
|
--hash=sha256:996a4d1834524adbb423385d5a629b868ef9d774670856c63c9a0408a3063401 \
|
||||||
--hash=sha256:d3972b13148c1d1fbc092b27678a33b3080d1ac0ca305742b0119b75f9e87e38 \
|
--hash=sha256:9a52f254ce051e196b8fe2af4634c2d2f02c981756c6464dc192f1b6050b4e28 \
|
||||||
--hash=sha256:d40dcf533ca481355aa7b682e9e079f766f35715defa4929aeb5597f9604272e \
|
--hash=sha256:9d0ced62b59e0430b3690dbc5373df1c2aa7585e9a8ce38eff87f0fd993c5b01 \
|
||||||
--hash=sha256:e93df708c69a193fc7987192f94df250f83f3851fda49413f02ba5dded639482 \
|
--hash=sha256:a140761c4ff63d0cb9256ac752f230460ee225ccef4ad8f68affc723c88e2036 \
|
||||||
--hash=sha256:efd7e6660674e234e29937bc1481dceb7e0336bfae75b856b4fb272b5093c5d4 \
|
--hash=sha256:a184b2ea57b13680ab6d5fbde99ccef152c95c06746cb7718c583abd8f945ccc \
|
||||||
--hash=sha256:f9b3a78f69dcbd803cf2fb3f972779875b244c1115481dfbdd567b2c22b31f6b \
|
--hash=sha256:a3db56f153bd4c5c2b619ab02c5db5192e222150ce5a1bc10f16164714bc39ac \
|
||||||
--hash=sha256:fa39475eaccb98f9199eccfda4298abaf35ae0caec676ffc25b3a5e224044464 \
|
--hash=sha256:a46b2f450bc79e06ef3b6394f0c68660529ed51692606ad7f953fc2e448bc903 \
|
||||||
--hash=sha256:fbce6dae41b692a5973d0f2158f782b9ad05babc2c2019a970a1094a23909b1b
|
--hash=sha256:a884aef09d45ba1206712c7dbda5829562d3fea7726935d3289d343232ecb0d3 \
|
||||||
|
--hash=sha256:b2cf105cee600d2de04ca3cfa1f74f1127f8455b71dbad02b9da6ec266e116d6 \
|
||||||
|
--hash=sha256:b33a7884fabd72bdf5f910d0cf46be50dce86a0362a65cfc746a4168c67eb96c \
|
||||||
|
--hash=sha256:b42d86938e8dda1cd9a1a87a6d82f1818eaf933348429653559a458d027446da \
|
||||||
|
--hash=sha256:b6379e7546ba4ae4b18f8ae2b9bc5960936007a1c0e30b342f662577e8bc3299 \
|
||||||
|
--hash=sha256:c7420a2696a44650120cdd269a5d2e56a477e2bfa9d95e86229059beb1c19e15 \
|
||||||
|
--hash=sha256:c8651e0d4b3bdeda6602b85fdc2abbefc1b41e573ecb37b6779c4ca50753a199 \
|
||||||
|
--hash=sha256:d066ea419f719ed87bc2c99a4a4bfd77c2e5949cb724588b9dd58f3fd90b92bf \
|
||||||
|
--hash=sha256:e6c58beb17380f7c2ea181ea11e7db8c0ceb474c9dd45f48e71e2cb577d146a1 \
|
||||||
|
--hash=sha256:e852d9dda9f93ad3651ae1e3bb770eac544ec93c3807888798eccddf84596537 \
|
||||||
|
--hash=sha256:ec3681a0cb34c255d76dd9d865a55f260164adb9fa02628415cdc2d43ee2c05d \
|
||||||
|
--hash=sha256:ee0c0b3b35b34f782afc673d503167157094a16f442ace7c6c5e0ca80b08f50c \
|
||||||
|
--hash=sha256:eedacb5c5d22b7097482fa834bda0dafa3d914a4e829ec83cdea2a01f8c813c4 \
|
||||||
|
--hash=sha256:ef00af0439ebfee806b25f24c8f92109157ff3fac5731dc7867957812e87b8d9 \
|
||||||
|
--hash=sha256:f0e8817c7d1a0c2eedebf57ef9a9896f3ea23324769a9a2061a80fe8852705ed \
|
||||||
|
--hash=sha256:f3d5be054c461d6a2268831f04091dc82753176f6ea06dc6047a5e168265a987 \
|
||||||
|
--hash=sha256:f4b5c37a5f40e4d733d3bbaaef082149bee5a5ea3156a785ff64d949bd1353fa
|
||||||
# via weasyprint
|
# via weasyprint
|
||||||
identify==2.6.13 \
|
identify==2.6.15 \
|
||||||
--hash=sha256:60381139b3ae39447482ecc406944190f690d4a2997f2584062089848361b33b \
|
--hash=sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757 \
|
||||||
--hash=sha256:da8d6c828e773620e13bfa86ea601c5a5310ba4bcd65edf378198b56a1f9fb32
|
--hash=sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf
|
||||||
# via pre-commit
|
# via pre-commit
|
||||||
nodeenv==1.9.1 \
|
nodeenv==1.9.1 \
|
||||||
--hash=sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f \
|
--hash=sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f \
|
||||||
@@ -338,128 +251,113 @@ pdf2image==1.17.0 \
|
|||||||
--hash=sha256:eaa959bc116b420dd7ec415fcae49b98100dda3dd18cd2fdfa86d09f112f6d57 \
|
--hash=sha256:eaa959bc116b420dd7ec415fcae49b98100dda3dd18cd2fdfa86d09f112f6d57 \
|
||||||
--hash=sha256:ecdd58d7afb810dffe21ef2b1bbc057ef434dabbac6c33778a38a3f7744a27e2
|
--hash=sha256:ecdd58d7afb810dffe21ef2b1bbc057ef434dabbac6c33778a38a3f7744a27e2
|
||||||
# via -r .github/scripts/requirements_dev.in
|
# via -r .github/scripts/requirements_dev.in
|
||||||
pillow==11.3.0 \
|
pillow==12.0.0 \
|
||||||
--hash=sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2 \
|
--hash=sha256:0869154a2d0546545cde61d1789a6524319fc1897d9ee31218eae7a60ccc5643 \
|
||||||
--hash=sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214 \
|
--hash=sha256:09f2d0abef9e4e2f349305a4f8cc784a8a6c2f58a8c4892eea13b10a943bd26e \
|
||||||
--hash=sha256:040a5b691b0713e1f6cbe222e0f4f74cd233421e105850ae3b3c0ceda520f42e \
|
--hash=sha256:0b817e7035ea7f6b942c13aa03bb554fc44fea70838ea21f8eb31c638326584e \
|
||||||
--hash=sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59 \
|
--hash=sha256:0fd00cac9c03256c8b2ff58f162ebcd2587ad3e1f2e397eab718c47e24d231cc \
|
||||||
--hash=sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50 \
|
--hash=sha256:110486b79f2d112cf6add83b28b627e369219388f64ef2f960fef9ebaf54c642 \
|
||||||
--hash=sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632 \
|
--hash=sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6 \
|
||||||
--hash=sha256:092c80c76635f5ecb10f3f83d76716165c96f5229addbd1ec2bdbbda7d496e06 \
|
--hash=sha256:1ac11e8ea4f611c3c0147424eae514028b5e9077dd99ab91e1bd7bc33ff145e1 \
|
||||||
--hash=sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a \
|
--hash=sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b \
|
||||||
--hash=sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51 \
|
--hash=sha256:1ee80a59f6ce048ae13cda1abf7fbd2a34ab9ee7d401c46be3ca685d1999a399 \
|
||||||
--hash=sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced \
|
--hash=sha256:21f241bdd5080a15bc86d3466a9f6074a9c2c2b314100dd896ac81ee6db2f1ba \
|
||||||
--hash=sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f \
|
--hash=sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad \
|
||||||
--hash=sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12 \
|
--hash=sha256:26d9f7d2b604cd23aba3e9faf795787456ac25634d82cd060556998e39c6fa47 \
|
||||||
--hash=sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8 \
|
--hash=sha256:27f95b12453d165099c84f8a8bfdfd46b9e4bda9e0e4b65f0635430027f55739 \
|
||||||
--hash=sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6 \
|
--hash=sha256:2c54c1a783d6d60595d3514f0efe9b37c8808746a66920315bfd34a938d7994b \
|
||||||
--hash=sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580 \
|
--hash=sha256:2fa5f0b6716fc88f11380b88b31fe591a06c6315e955c096c35715788b339e3f \
|
||||||
--hash=sha256:19d2ff547c75b8e3ff46f4d9ef969a06c30ab2d4263a9e287733aa8b2429ce8f \
|
--hash=sha256:32ed80ea8a90ee3e6fa08c21e2e091bba6eda8eccc83dbc34c95169507a91f10 \
|
||||||
--hash=sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac \
|
--hash=sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52 \
|
||||||
--hash=sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860 \
|
--hash=sha256:38df9b4bfd3db902c9c2bd369bcacaf9d935b2fff73709429d95cc41554f7b3d \
|
||||||
--hash=sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd \
|
--hash=sha256:3adfb466bbc544b926d50fe8f4a4e6abd8c6bffd28a26177594e6e9b2b76572b \
|
||||||
--hash=sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722 \
|
--hash=sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a \
|
||||||
--hash=sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8 \
|
--hash=sha256:4078242472387600b2ce8d93ade8899c12bf33fa89e55ec89fe126e9d6d5d9e9 \
|
||||||
--hash=sha256:23cff760a9049c502721bdb743a7cb3e03365fafcdfc2ef9784610714166e5a4 \
|
--hash=sha256:455247ac8a4cfb7b9bc45b7e432d10421aea9fc2e74d285ba4072688a74c2e9d \
|
||||||
--hash=sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673 \
|
--hash=sha256:4cc6b3b2efff105c6a1656cfe59da4fdde2cda9af1c5e0b58529b24525d0a098 \
|
||||||
--hash=sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788 \
|
--hash=sha256:4cf7fed4b4580601c4345ceb5d4cbf5a980d030fd5ad07c4d2ec589f95f09905 \
|
||||||
--hash=sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542 \
|
--hash=sha256:5193fde9a5f23c331ea26d0cf171fbf67e3f247585f50c08b3e205c7aeb4589b \
|
||||||
--hash=sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e \
|
--hash=sha256:5269cc1caeedb67e6f7269a42014f381f45e2e7cd42d834ede3c703a1d915fe3 \
|
||||||
--hash=sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd \
|
--hash=sha256:53561a4ddc36facb432fae7a9d8afbfaf94795414f5cdc5fc52f28c1dca90371 \
|
||||||
--hash=sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8 \
|
--hash=sha256:55f818bd74fe2f11d4d7cbc65880a843c4075e0ac7226bc1a23261dbea531953 \
|
||||||
--hash=sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523 \
|
--hash=sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01 \
|
||||||
--hash=sha256:3cee80663f29e3843b68199b9d6f4f54bd1d4a6b59bdd91bceefc51238bcb967 \
|
--hash=sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca \
|
||||||
--hash=sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809 \
|
--hash=sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e \
|
||||||
--hash=sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477 \
|
--hash=sha256:6ace95230bfb7cd79ef66caa064bbe2f2a1e63d93471c3a2e1f1348d9f22d6b7 \
|
||||||
--hash=sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027 \
|
--hash=sha256:6e51b71417049ad6ab14c49608b4a24d8fb3fe605e5dfabfe523b58064dc3d27 \
|
||||||
--hash=sha256:4445fa62e15936a028672fd48c4c11a66d641d2c05726c7ec1f8ba6a572036ae \
|
--hash=sha256:71db6b4c1653045dacc1585c1b0d184004f0d7e694c7b34ac165ca70c0838082 \
|
||||||
--hash=sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b \
|
--hash=sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e \
|
||||||
--hash=sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c \
|
--hash=sha256:759de84a33be3b178a64c8ba28ad5c135900359e85fb662bc6e403ad4407791d \
|
||||||
--hash=sha256:48d254f8a4c776de343051023eb61ffe818299eeac478da55227d96e241de53f \
|
--hash=sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8 \
|
||||||
--hash=sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e \
|
--hash=sha256:7d87ef5795da03d742bf49439f9ca4d027cde49c82c5371ba52464aee266699a \
|
||||||
--hash=sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b \
|
--hash=sha256:7dfb439562f234f7d57b1ac6bc8fe7f838a4bd49c79230e0f6a1da93e82f1fad \
|
||||||
--hash=sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7 \
|
--hash=sha256:7fa22993bac7b77b78cae22bad1e2a987ddf0d9015c63358032f84a53f23cdc3 \
|
||||||
--hash=sha256:527b37216b6ac3a12d7838dc3bd75208ec57c1c6d11ef01902266a5a0c14fc27 \
|
--hash=sha256:805ebf596939e48dbb2e4922a1d3852cfc25c38160751ce02da93058b48d252a \
|
||||||
--hash=sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361 \
|
--hash=sha256:82240051c6ca513c616f7f9da06e871f61bfd7805f566275841af15015b8f98d \
|
||||||
--hash=sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae \
|
--hash=sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353 \
|
||||||
--hash=sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d \
|
--hash=sha256:8d8ca2b210ada074d57fcee40c30446c9562e542fc46aedc19baf758a93532ee \
|
||||||
--hash=sha256:6359a3bc43f57d5b375d1ad54a0074318a0844d11b76abccf478c37c986d3cfc \
|
--hash=sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b \
|
||||||
--hash=sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58 \
|
--hash=sha256:90387104ee8400a7b4598253b4c406f8958f59fcf983a6cea2b50d59f7d63d0b \
|
||||||
--hash=sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad \
|
--hash=sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a \
|
||||||
--hash=sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6 \
|
--hash=sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7 \
|
||||||
--hash=sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024 \
|
--hash=sha256:99a7f72fb6249302aa62245680754862a44179b545ded638cf1fef59befb57ef \
|
||||||
--hash=sha256:6a418691000f2a418c9135a7cf0d797c1bb7d9a485e61fe8e7722845b95ef978 \
|
--hash=sha256:9f0b04c6b8584c2c193babcccc908b38ed29524b29dd464bc8801bf10d746a3a \
|
||||||
--hash=sha256:6abdbfd3aea42be05702a8dd98832329c167ee84400a1d1f61ab11437f1717eb \
|
--hash=sha256:9fe611163f6303d1619bbcb653540a4d60f9e55e622d60a3108be0d5b441017a \
|
||||||
--hash=sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d \
|
--hash=sha256:a3475b96f5908b3b16c47533daaa87380c491357d197564e0ba34ae75c0f3257 \
|
||||||
--hash=sha256:7107195ddc914f656c7fc8e4a5e1c25f32e9236ea3ea860f257b0436011fddd0 \
|
--hash=sha256:a6597ff2b61d121172f5844b53f21467f7082f5fb385a9a29c01414463f93b07 \
|
||||||
--hash=sha256:71f511f6b3b91dd543282477be45a033e4845a40278fa8dcdbfdb07109bf18f9 \
|
--hash=sha256:a7921c5a6d31b3d756ec980f2f47c0cfdbce0fc48c22a39347a895f41f4a6ea4 \
|
||||||
--hash=sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f \
|
--hash=sha256:aa5129de4e174daccbc59d0a3b6d20eaf24417d59851c07ebb37aeb02947987c \
|
||||||
--hash=sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874 \
|
--hash=sha256:aeaefa96c768fc66818730b952a862235d68825c178f1b3ffd4efd7ad2edcb7c \
|
||||||
--hash=sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa \
|
--hash=sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4 \
|
||||||
--hash=sha256:7aee118e30a4cf54fdd873bd3a29de51e29105ab11f9aad8c32123f58c8f8081 \
|
--hash=sha256:aff9e4d82d082ff9513bdd6acd4f5bd359f5b2c870907d2b0a9c5e10d40c88fe \
|
||||||
--hash=sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149 \
|
--hash=sha256:b22bd8c974942477156be55a768f7aa37c46904c175be4e158b6a86e3a6b7ca8 \
|
||||||
--hash=sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6 \
|
--hash=sha256:b290fd8aa38422444d4b50d579de197557f182ef1068b75f5aa8558638b8d0a5 \
|
||||||
--hash=sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d \
|
--hash=sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6 \
|
||||||
--hash=sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd \
|
--hash=sha256:b583dc9070312190192631373c6c8ed277254aa6e6084b74bdd0a6d3b221608e \
|
||||||
--hash=sha256:819931d25e57b513242859ce1876c58c59dc31587847bf74cfe06b2e0cb22d2f \
|
--hash=sha256:b87843e225e74576437fd5b6a4c2205d422754f84a06942cfaf1dc32243e45a8 \
|
||||||
--hash=sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c \
|
--hash=sha256:bc91a56697869546d1b8f0a3ff35224557ae7f881050e99f615e0119bf934b4e \
|
||||||
--hash=sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31 \
|
--hash=sha256:bd87e140e45399c818fac4247880b9ce719e4783d767e030a883a970be632275 \
|
||||||
--hash=sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e \
|
--hash=sha256:bde737cff1a975b70652b62d626f7785e0480918dece11e8fef3c0cf057351c3 \
|
||||||
--hash=sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db \
|
--hash=sha256:bdee52571a343d721fb2eb3b090a82d959ff37fc631e3f70422e0c2e029f3e76 \
|
||||||
--hash=sha256:89bd777bc6624fe4115e9fac3352c79ed60f3bb18651420635f26e643e3dd1f6 \
|
--hash=sha256:bee2a6db3a7242ea309aa7ee8e2780726fed67ff4e5b40169f2c940e7eb09227 \
|
||||||
--hash=sha256:8dc70ca24c110503e16918a658b869019126ecfe03109b754c402daff12b3d9f \
|
--hash=sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9 \
|
||||||
--hash=sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494 \
|
--hash=sha256:c50f36a62a22d350c96e49ad02d0da41dbd17ddc2e29750dbdba4323f85eb4a5 \
|
||||||
--hash=sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69 \
|
--hash=sha256:c607c90ba67533e1b2355b821fef6764d1dd2cbe26b8c1005ae84f7aea25ff79 \
|
||||||
--hash=sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94 \
|
--hash=sha256:c7b2a63fd6d5246349f3d3f37b14430d73ee7e8173154461785e43036ffa96ca \
|
||||||
--hash=sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77 \
|
--hash=sha256:c828a1ae702fc712978bda0320ba1b9893d99be0badf2647f693cc01cf0f04fa \
|
||||||
--hash=sha256:97afb3a00b65cc0804d1c7abddbf090a81eaac02768af58cbdcaaa0a931e0b6d \
|
--hash=sha256:c85de1136429c524e55cfa4e033b4a7940ac5c8ee4d9401cc2d1bf48154bbc7b \
|
||||||
--hash=sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7 \
|
--hash=sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e \
|
||||||
--hash=sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a \
|
--hash=sha256:cae81479f77420d217def5f54b5b9d279804d17e982e0f2fa19b1d1e14ab5197 \
|
||||||
--hash=sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438 \
|
--hash=sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab \
|
||||||
--hash=sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288 \
|
--hash=sha256:d120c38a42c234dc9a8c5de7ceaaf899cf33561956acb4941653f8bdc657aa79 \
|
||||||
--hash=sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b \
|
--hash=sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2 \
|
||||||
--hash=sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635 \
|
--hash=sha256:d49e2314c373f4c2b39446fb1a45ed333c850e09d0c59ac79b72eb3b95397363 \
|
||||||
--hash=sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3 \
|
--hash=sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0 \
|
||||||
--hash=sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d \
|
--hash=sha256:d64317d2587c70324b79861babb9c09f71fbb780bad212018874b2c013d8600e \
|
||||||
--hash=sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe \
|
--hash=sha256:d77153e14b709fd8b8af6f66a3afbb9ed6e9fc5ccf0b6b7e1ced7b036a228782 \
|
||||||
--hash=sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0 \
|
--hash=sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925 \
|
||||||
--hash=sha256:b5f56c3f344f2ccaf0dd875d3e180f631dc60a51b314295a3e681fe8cf851fbe \
|
--hash=sha256:dd333073e0cacdc3089525c7df7d39b211bcdf31fc2824e49d01c6b6187b07d0 \
|
||||||
--hash=sha256:be5463ac478b623b9dd3937afd7fb7ab3d79dd290a28e2b6df292dc75063eb8a \
|
--hash=sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b \
|
||||||
--hash=sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805 \
|
--hash=sha256:f135c702ac42262573fe9714dfe99c944b4ba307af5eb507abef1667e2cbbced \
|
||||||
--hash=sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8 \
|
--hash=sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c \
|
||||||
--hash=sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36 \
|
--hash=sha256:f4f1231b7dec408e8670264ce63e9c71409d9583dd21d32c163e25213ee2a344 \
|
||||||
--hash=sha256:cadc9e0ea0a2431124cde7e1697106471fc4c1da01530e679b2391c37d3fbb3a \
|
--hash=sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9 \
|
||||||
--hash=sha256:cc3e831b563b3114baac7ec2ee86819eb03caa1a2cef0b481a5675b59c4fe23b \
|
--hash=sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1
|
||||||
--hash=sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e \
|
|
||||||
--hash=sha256:d000f46e2917c705e9fb93a3606ee4a819d1e3aa7a9b442f6444f07e77cf5e25 \
|
|
||||||
--hash=sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12 \
|
|
||||||
--hash=sha256:e5c5858ad8ec655450a7c7df532e9842cf8df7cc349df7225c60d5d348c8aada \
|
|
||||||
--hash=sha256:e67d793d180c9df62f1f40aee3accca4829d3794c95098887edc18af4b8b780c \
|
|
||||||
--hash=sha256:ea944117a7974ae78059fcc1800e5d3295172bb97035c0c1d9345fca1419da71 \
|
|
||||||
--hash=sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d \
|
|
||||||
--hash=sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c \
|
|
||||||
--hash=sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6 \
|
|
||||||
--hash=sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1 \
|
|
||||||
--hash=sha256:f1f182ebd2303acf8c380a54f615ec883322593320a9b00438eb842c1f37ae50 \
|
|
||||||
--hash=sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653 \
|
|
||||||
--hash=sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c \
|
|
||||||
--hash=sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4 \
|
|
||||||
--hash=sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3
|
|
||||||
# via
|
# via
|
||||||
# -r .github/scripts/requirements_dev.in
|
# -r .github/scripts/requirements_dev.in
|
||||||
# pdf2image
|
# pdf2image
|
||||||
# weasyprint
|
# weasyprint
|
||||||
platformdirs==4.3.8 \
|
platformdirs==4.5.0 \
|
||||||
--hash=sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc \
|
--hash=sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312 \
|
||||||
--hash=sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4
|
--hash=sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3
|
||||||
# via virtualenv
|
# via virtualenv
|
||||||
pre-commit==4.3.0 \
|
pre-commit==4.3.0 \
|
||||||
--hash=sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8 \
|
--hash=sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8 \
|
||||||
--hash=sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16
|
--hash=sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16
|
||||||
# via -r .github/scripts/requirements_dev.in
|
# via -r .github/scripts/requirements_dev.in
|
||||||
pycparser==2.22 \
|
pycparser==2.23 \
|
||||||
--hash=sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6 \
|
--hash=sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2 \
|
||||||
--hash=sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc
|
--hash=sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934
|
||||||
# via cffi
|
# via cffi
|
||||||
pydyf==0.11.0 \
|
pydyf==0.11.0 \
|
||||||
--hash=sha256:0aaf9e2ebbe786ec7a78ec3fbffa4cdcecde53fd6f563221d53c6bc1328848a3 \
|
--hash=sha256:0aaf9e2ebbe786ec7a78ec3fbffa4cdcecde53fd6f563221d53c6bc1328848a3 \
|
||||||
@@ -469,60 +367,80 @@ pyphen==0.17.2 \
|
|||||||
--hash=sha256:3a07fb017cb2341e1d9ff31b8634efb1ae4dc4b130468c7c39dd3d32e7c3affd \
|
--hash=sha256:3a07fb017cb2341e1d9ff31b8634efb1ae4dc4b130468c7c39dd3d32e7c3affd \
|
||||||
--hash=sha256:f60647a9c9b30ec6c59910097af82bc5dd2d36576b918e44148d8b07ef3b4aa3
|
--hash=sha256:f60647a9c9b30ec6c59910097af82bc5dd2d36576b918e44148d8b07ef3b4aa3
|
||||||
# via weasyprint
|
# via weasyprint
|
||||||
pyyaml==6.0.2 \
|
pyyaml==6.0.3 \
|
||||||
--hash=sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff \
|
--hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \
|
||||||
--hash=sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48 \
|
--hash=sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a \
|
||||||
--hash=sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086 \
|
--hash=sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3 \
|
||||||
--hash=sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e \
|
--hash=sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956 \
|
||||||
--hash=sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133 \
|
--hash=sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6 \
|
||||||
--hash=sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5 \
|
--hash=sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c \
|
||||||
--hash=sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484 \
|
--hash=sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65 \
|
||||||
--hash=sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee \
|
--hash=sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a \
|
||||||
--hash=sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5 \
|
--hash=sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0 \
|
||||||
--hash=sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68 \
|
--hash=sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b \
|
||||||
--hash=sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a \
|
--hash=sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1 \
|
||||||
--hash=sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf \
|
--hash=sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6 \
|
||||||
--hash=sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99 \
|
--hash=sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7 \
|
||||||
--hash=sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8 \
|
--hash=sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e \
|
||||||
--hash=sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85 \
|
--hash=sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007 \
|
||||||
--hash=sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19 \
|
--hash=sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310 \
|
||||||
--hash=sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc \
|
--hash=sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4 \
|
||||||
--hash=sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a \
|
--hash=sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9 \
|
||||||
--hash=sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1 \
|
--hash=sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295 \
|
||||||
--hash=sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317 \
|
--hash=sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea \
|
||||||
--hash=sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c \
|
--hash=sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0 \
|
||||||
--hash=sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631 \
|
--hash=sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e \
|
||||||
--hash=sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d \
|
--hash=sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac \
|
||||||
--hash=sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652 \
|
--hash=sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9 \
|
||||||
--hash=sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5 \
|
--hash=sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7 \
|
||||||
--hash=sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e \
|
--hash=sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35 \
|
||||||
--hash=sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b \
|
--hash=sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb \
|
||||||
--hash=sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8 \
|
--hash=sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b \
|
||||||
--hash=sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476 \
|
--hash=sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69 \
|
||||||
--hash=sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706 \
|
--hash=sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5 \
|
||||||
--hash=sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563 \
|
--hash=sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b \
|
||||||
--hash=sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237 \
|
--hash=sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c \
|
||||||
--hash=sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b \
|
--hash=sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369 \
|
||||||
--hash=sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083 \
|
--hash=sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd \
|
||||||
--hash=sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180 \
|
--hash=sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824 \
|
||||||
--hash=sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425 \
|
--hash=sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198 \
|
||||||
--hash=sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e \
|
--hash=sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065 \
|
||||||
--hash=sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f \
|
--hash=sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c \
|
||||||
--hash=sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725 \
|
--hash=sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c \
|
||||||
--hash=sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183 \
|
--hash=sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764 \
|
||||||
--hash=sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab \
|
--hash=sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196 \
|
||||||
--hash=sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774 \
|
--hash=sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b \
|
||||||
--hash=sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725 \
|
--hash=sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00 \
|
||||||
--hash=sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e \
|
--hash=sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac \
|
||||||
--hash=sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5 \
|
--hash=sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8 \
|
||||||
--hash=sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d \
|
--hash=sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e \
|
||||||
--hash=sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290 \
|
--hash=sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28 \
|
||||||
--hash=sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44 \
|
--hash=sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3 \
|
||||||
--hash=sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed \
|
--hash=sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5 \
|
||||||
--hash=sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4 \
|
--hash=sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4 \
|
||||||
--hash=sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba \
|
--hash=sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b \
|
||||||
--hash=sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12 \
|
--hash=sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf \
|
||||||
--hash=sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4
|
--hash=sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5 \
|
||||||
|
--hash=sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702 \
|
||||||
|
--hash=sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8 \
|
||||||
|
--hash=sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788 \
|
||||||
|
--hash=sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da \
|
||||||
|
--hash=sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d \
|
||||||
|
--hash=sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc \
|
||||||
|
--hash=sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c \
|
||||||
|
--hash=sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba \
|
||||||
|
--hash=sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f \
|
||||||
|
--hash=sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917 \
|
||||||
|
--hash=sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5 \
|
||||||
|
--hash=sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26 \
|
||||||
|
--hash=sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f \
|
||||||
|
--hash=sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b \
|
||||||
|
--hash=sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be \
|
||||||
|
--hash=sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c \
|
||||||
|
--hash=sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3 \
|
||||||
|
--hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6 \
|
||||||
|
--hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \
|
||||||
|
--hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0
|
||||||
# via pre-commit
|
# via pre-commit
|
||||||
tinycss2==1.4.0 \
|
tinycss2==1.4.0 \
|
||||||
--hash=sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7 \
|
--hash=sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7 \
|
||||||
@@ -534,13 +452,13 @@ tinyhtml5==2.0.0 \
|
|||||||
--hash=sha256:086f998833da24c300c414d9fe81d9b368fd04cb9d2596a008421cbc705fcfcc \
|
--hash=sha256:086f998833da24c300c414d9fe81d9b368fd04cb9d2596a008421cbc705fcfcc \
|
||||||
--hash=sha256:13683277c5b176d070f82d099d977194b7a1e26815b016114f581a74bbfbf47e
|
--hash=sha256:13683277c5b176d070f82d099d977194b7a1e26815b016114f581a74bbfbf47e
|
||||||
# via weasyprint
|
# via weasyprint
|
||||||
unoserver==3.3.2 \
|
unoserver==3.4 \
|
||||||
--hash=sha256:1eeb7467cf6b56b8eff3b576e2d1b2b2ff4e0eb2052e995ac80a1456de300639 \
|
--hash=sha256:3dcf2204013def1d1ddd3671f38b11346bdf349fef9728277462666a8a634419 \
|
||||||
--hash=sha256:87e144f903ee21951b2e06a97549450c13ed7eca5bcebad942d3352d4e882616
|
--hash=sha256:64c24d33d4f65d680a2d9f676518cb28e7fd6c1f9d9a745c33e4a4cb59afdfcd
|
||||||
# via -r .github/scripts/requirements_dev.in
|
# via -r .github/scripts/requirements_dev.in
|
||||||
virtualenv==20.33.1 \
|
virtualenv==20.35.4 \
|
||||||
--hash=sha256:07c19bc66c11acab6a5958b815cbcee30891cd1c2ccf53785a28651a0d8d8a67 \
|
--hash=sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c \
|
||||||
--hash=sha256:1b44478d9e261b3fb8baa5e74a0ca3bc0e05f21aa36167bf9cbf850e542765b8
|
--hash=sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b
|
||||||
# via pre-commit
|
# via pre-commit
|
||||||
weasyprint==66.0 \
|
weasyprint==66.0 \
|
||||||
--hash=sha256:82b0783b726fcd318e2c977dcdddca76515b30044bc7a830cc4fbe717582a6d0 \
|
--hash=sha256:82b0783b726fcd318e2c977dcdddca76515b30044bc7a830cc4fbe717582a6d0 \
|
||||||
@@ -628,9 +546,9 @@ zopfli==0.2.3.post1 \
|
|||||||
# via fonttools
|
# via fonttools
|
||||||
|
|
||||||
# The following packages are considered to be unsafe in a requirements file:
|
# The following packages are considered to be unsafe in a requirements file:
|
||||||
pip==25.2 \
|
pip==25.3 \
|
||||||
--hash=sha256:578283f006390f85bb6282dffb876454593d637f5d1be494b5202ce4877e71f2 \
|
--hash=sha256:8d0538dbbd7babbd207f261ed969c65de439f6bc9e5dbd3b3b9a77f25d95f343 \
|
||||||
--hash=sha256:6d67a2b4e7f14d8b31b8b52648866fa717f45a1eb70e83002f4331d07e953717
|
--hash=sha256:9655943313a94722b7774661c21049070f6bbb0a1516bf02f7c8d5d9201514cd
|
||||||
# via -r .github/scripts/requirements_dev.in
|
# via -r .github/scripts/requirements_dev.in
|
||||||
setuptools==80.9.0 \
|
setuptools==80.9.0 \
|
||||||
--hash=sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 \
|
--hash=sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 \
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ distlib==0.4.0 \
|
|||||||
--hash=sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 \
|
--hash=sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 \
|
||||||
--hash=sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d
|
--hash=sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d
|
||||||
# via virtualenv
|
# via virtualenv
|
||||||
filelock==3.19.1 \
|
filelock==3.20.0 \
|
||||||
--hash=sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58 \
|
--hash=sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2 \
|
||||||
--hash=sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d
|
--hash=sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4
|
||||||
# via virtualenv
|
# via virtualenv
|
||||||
identify==2.6.15 \
|
identify==2.6.15 \
|
||||||
--hash=sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757 \
|
--hash=sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757 \
|
||||||
@@ -24,9 +24,9 @@ nodeenv==1.9.1 \
|
|||||||
--hash=sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f \
|
--hash=sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f \
|
||||||
--hash=sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9
|
--hash=sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9
|
||||||
# via pre-commit
|
# via pre-commit
|
||||||
platformdirs==4.4.0 \
|
platformdirs==4.5.0 \
|
||||||
--hash=sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85 \
|
--hash=sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312 \
|
||||||
--hash=sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf
|
--hash=sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3
|
||||||
# via virtualenv
|
# via virtualenv
|
||||||
pre-commit==4.3.0 \
|
pre-commit==4.3.0 \
|
||||||
--hash=sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8 \
|
--hash=sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8 \
|
||||||
@@ -107,7 +107,7 @@ pyyaml==6.0.3 \
|
|||||||
--hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \
|
--hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \
|
||||||
--hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0
|
--hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0
|
||||||
# via pre-commit
|
# via pre-commit
|
||||||
virtualenv==20.34.0 \
|
virtualenv==20.35.4 \
|
||||||
--hash=sha256:341f5afa7eee943e4984a9207c025feedd768baff6753cd660c857ceb3e36026 \
|
--hash=sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c \
|
||||||
--hash=sha256:44815b2c9dee7ed86e387b842a84f20b93f7f417f95886ca1996a72a4138eb1a
|
--hash=sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b
|
||||||
# via pre-commit
|
# via pre-commit
|
||||||
|
|||||||
@@ -41,12 +41,12 @@ jobs:
|
|||||||
enable_enterprise: ${{ steps.check-pro-flag.outputs.enable_enterprise }}
|
enable_enterprise: ${{ steps.check-pro-flag.outputs.enable_enterprise }}
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: Checkout PR
|
- name: Checkout PR
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||||
|
|
||||||
- name: Setup GitHub App Bot
|
- name: Setup GitHub App Bot
|
||||||
if: github.actor != 'dependabot[bot]'
|
if: github.actor != 'dependabot[bot]'
|
||||||
@@ -129,12 +129,12 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: Checkout PR
|
- name: Checkout PR
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||||
|
|
||||||
- name: Setup GitHub App Bot
|
- name: Setup GitHub App Bot
|
||||||
if: github.actor != 'dependabot[bot]'
|
if: github.actor != 'dependabot[bot]'
|
||||||
@@ -146,7 +146,7 @@ jobs:
|
|||||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
- name: Checkout PR
|
- name: Checkout PR
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||||
with:
|
with:
|
||||||
ref: refs/pull/${{ needs.check-comment.outputs.pr_number }}/merge
|
ref: refs/pull/${{ needs.check-comment.outputs.pr_number }}/merge
|
||||||
token: ${{ steps.setup-bot.outputs.token }}
|
token: ${{ steps.setup-bot.outputs.token }}
|
||||||
@@ -357,3 +357,149 @@ jobs:
|
|||||||
rm -f ../private.key docker-compose.yml
|
rm -f ../private.key docker-compose.yml
|
||||||
echo "Cleanup complete."
|
echo "Cleanup complete."
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
|
|
||||||
|
handle-label-commands:
|
||||||
|
if: ${{ github.event.issue.pull_request != null }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Harden Runner
|
||||||
|
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
|
- name: Check out the repository
|
||||||
|
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||||
|
|
||||||
|
- name: Setup GitHub App Bot
|
||||||
|
id: setup-bot
|
||||||
|
uses: ./.github/actions/setup-bot
|
||||||
|
with:
|
||||||
|
app-id: ${{ secrets.GH_APP_ID }}
|
||||||
|
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
|
- name: Apply label commands
|
||||||
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||||
|
with:
|
||||||
|
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||||
|
script: |
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const { comment, issue } = context.payload;
|
||||||
|
const commentBody = comment?.body ?? '';
|
||||||
|
if (!commentBody.includes('::label::')) {
|
||||||
|
core.info('No label commands detected in comment.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const configPath = path.join(process.env.GITHUB_WORKSPACE, '.github', 'config', 'repo_devs.json');
|
||||||
|
const repoDevsConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
||||||
|
const label_changer = (repoDevsConfig.label_changer || []).map((login) => login.toLowerCase());
|
||||||
|
|
||||||
|
const commenter = (comment?.user?.login || '').toLowerCase();
|
||||||
|
if (!label_changer.includes(commenter)) {
|
||||||
|
core.info(`User ${commenter} is not authorized to manage labels.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const labelsConfigPath = path.join(process.env.GITHUB_WORKSPACE, '.github', 'labels.yml');
|
||||||
|
const labelsFile = fs.readFileSync(labelsConfigPath, 'utf8');
|
||||||
|
|
||||||
|
const labelNameMap = new Map();
|
||||||
|
for (const match of labelsFile.matchAll(/-\s+name:\s*(?:"([^"]+)"|'([^']+)'|([^\n]+))/g)) {
|
||||||
|
const labelName = (match[1] ?? match[2] ?? match[3] ?? '').trim();
|
||||||
|
|
||||||
|
if (!labelName) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const normalized = labelName.toLowerCase();
|
||||||
|
if (!labelNameMap.has(normalized)) {
|
||||||
|
labelNameMap.set(normalized, labelName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!labelNameMap.size) {
|
||||||
|
core.warning('No labels could be read from .github/labels.yml; aborting label commands.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let allowedLabelNames = new Set(labelNameMap.values());
|
||||||
|
|
||||||
|
const labelsToAdd = new Set();
|
||||||
|
const labelsToRemove = new Set();
|
||||||
|
const commandRegex = /^(\w+)::(label)::"([^"]+)"/gim;
|
||||||
|
let match;
|
||||||
|
while ((match = commandRegex.exec(commentBody)) !== null) {
|
||||||
|
core.info(`Found label command: ${match[0]} (action: ${match[1]}, label: ${match[2]}, labelName: ${match[3]})`);
|
||||||
|
const action = match[1].toLowerCase();
|
||||||
|
const labelName = match[3].trim();
|
||||||
|
|
||||||
|
if (!labelName) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalized = labelName.toLowerCase();
|
||||||
|
const resolvedLabelName = labelNameMap.get(normalized);
|
||||||
|
if (action === 'add') {
|
||||||
|
if (!resolvedLabelName) {
|
||||||
|
core.warning(`Label "${labelName}" is not defined in .github/labels.yml and cannot be added.`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!allowedLabelNames.has(resolvedLabelName)) {
|
||||||
|
core.warning(`Label "${resolvedLabelName}" is not allowed for add commands and will be skipped.`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
labelsToAdd.add(resolvedLabelName);
|
||||||
|
} else if (action === 'rm') {
|
||||||
|
const labelToRemove = resolvedLabelName ?? labelName;
|
||||||
|
if (!resolvedLabelName) {
|
||||||
|
core.warning(`Label "${labelName}" is not defined in .github/labels.yml; attempting to remove as provided.`);
|
||||||
|
}
|
||||||
|
labelsToRemove.add(labelToRemove);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const addLabels = Array.from(labelsToAdd);
|
||||||
|
const removeLabels = Array.from(labelsToRemove);
|
||||||
|
|
||||||
|
if (!addLabels.length && !removeLabels.length) {
|
||||||
|
core.info('No valid label commands found after parsing.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const issueParams = {
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: issue.number,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (addLabels.length) {
|
||||||
|
core.info(`Adding labels: ${addLabels.join(', ')}`);
|
||||||
|
await github.rest.issues.addLabels({
|
||||||
|
...issueParams,
|
||||||
|
labels: addLabels,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const labelName of removeLabels) {
|
||||||
|
core.info(`Removing label: ${labelName}`);
|
||||||
|
try {
|
||||||
|
await github.rest.issues.removeLabel({
|
||||||
|
...issueParams,
|
||||||
|
name: labelName,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error.status === 404) {
|
||||||
|
core.warning(`Label "${labelName}" was not present on the pull request.`);
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await github.rest.issues.deleteComment({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
comment_id: comment.id,
|
||||||
|
});
|
||||||
|
core.info('Processed label commands and deleted the comment.');
|
||||||
|
|||||||
@@ -21,12 +21,12 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: Checkout PR
|
- name: Checkout PR
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||||
|
|
||||||
- name: Setup GitHub App Bot
|
- name: Setup GitHub App Bot
|
||||||
if: github.actor != 'dependabot[bot]'
|
if: github.actor != 'dependabot[bot]'
|
||||||
|
|||||||
@@ -19,11 +19,11 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ name: "Auto Pull Request Labeler V2"
|
|||||||
on:
|
on:
|
||||||
pull_request_target:
|
pull_request_target:
|
||||||
types: [opened, synchronize]
|
types: [opened, synchronize]
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- V2
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
@@ -13,11 +16,11 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||||
|
|
||||||
- name: Setup GitHub App Bot
|
- name: Setup GitHub App Bot
|
||||||
id: setup-bot
|
id: setup-bot
|
||||||
|
|||||||
@@ -25,12 +25,12 @@ jobs:
|
|||||||
pull-requests: write # Allow writing to pull requests
|
pull-requests: write # Allow writing to pull requests
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: Checkout main branch first
|
- name: Checkout main branch first
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||||
|
|
||||||
- name: Setup GitHub App Bot
|
- name: Setup GitHub App Bot
|
||||||
id: setup-bot
|
id: setup-bot
|
||||||
|
|||||||
@@ -17,13 +17,13 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: "Checkout Repository"
|
- name: "Checkout Repository"
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||||
- name: "Dependency Review"
|
- name: "Dependency Review"
|
||||||
uses: actions/dependency-review-action@56339e523c0409420f6c2c9a2f4292bbb3c07dd3 # v4.8.0
|
uses: actions/dependency-review-action@3c4e3dcb1aa7874d2c16be7d79418e9b7efd6261 # v4.8.2
|
||||||
with:
|
with:
|
||||||
config-file: './.github/config/dependency-review-config.yml'
|
config-file: './.github/config/dependency-review-config.yml'
|
||||||
|
|||||||
@@ -31,12 +31,12 @@ jobs:
|
|||||||
repository-projects: write # Required for enabling automerge
|
repository-projects: write # Required for enabling automerge
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload artifact on failure
|
- name: Upload artifact on failure
|
||||||
if: failure()
|
if: failure()
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: dependencies-without-allowed-license.json
|
name: dependencies-without-allowed-license.json
|
||||||
path: build/reports/dependency-license/dependencies-without-allowed-license.json
|
path: build/reports/dependency-license/dependencies-without-allowed-license.json
|
||||||
@@ -82,7 +82,7 @@ jobs:
|
|||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
id: cpr
|
id: cpr
|
||||||
if: env.CHANGES_DETECTED == 'true'
|
if: env.CHANGES_DETECTED == 'true'
|
||||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
|
uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7.0.9
|
||||||
with:
|
with:
|
||||||
token: ${{ steps.setup-bot.outputs.token }}
|
token: ${{ steps.setup-bot.outputs.token }}
|
||||||
commit-message: "Update 3rd Party Licenses"
|
commit-message: "Update 3rd Party Licenses"
|
||||||
|
|||||||
@@ -15,12 +15,12 @@ jobs:
|
|||||||
issues: write
|
issues: write
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: Check out the repository
|
- name: Check out the repository
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||||
|
|
||||||
- name: Run Labeler
|
- name: Run Labeler
|
||||||
uses: crazy-max/ghaction-github-labeler@24d110aa46a59976b8a7f35518cb7f14f434c916 # v5.3.0
|
uses: crazy-max/ghaction-github-labeler@24d110aa46a59976b8a7f35518cb7f14f434c916 # v5.3.0
|
||||||
|
|||||||
@@ -38,14 +38,14 @@ jobs:
|
|||||||
version: ${{ steps.versionNumber.outputs.versionNumber }}
|
version: ${{ steps.versionNumber.outputs.versionNumber }}
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||||
|
|
||||||
- name: Set up JDK 21
|
- name: Set up JDK 21
|
||||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||||
with:
|
with:
|
||||||
java-version: "21"
|
java-version: "21"
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
@@ -106,14 +106,14 @@ jobs:
|
|||||||
file_suffix: "-server"
|
file_suffix: "-server"
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||||
|
|
||||||
- name: Set up JDK 21
|
- name: Set up JDK 21
|
||||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||||
with:
|
with:
|
||||||
java-version: "21"
|
java-version: "21"
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
@@ -145,7 +145,7 @@ jobs:
|
|||||||
cp app/core/build/libs/stirling-pdf-${{ needs.determine-matrix.outputs.version }}.jar ./jar-dist/Stirling-PDF${{ matrix.variant.file_suffix }}.jar
|
cp app/core/build/libs/stirling-pdf-${{ needs.determine-matrix.outputs.version }}.jar ./jar-dist/Stirling-PDF${{ matrix.variant.file_suffix }}.jar
|
||||||
|
|
||||||
- name: Upload JAR artifacts
|
- name: Upload JAR artifacts
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: jar${{ matrix.variant.file_suffix }}
|
name: jar${{ matrix.variant.file_suffix }}
|
||||||
path: ./jar-dist/*.jar
|
path: ./jar-dist/*.jar
|
||||||
@@ -189,7 +189,7 @@ jobs:
|
|||||||
targets: ${{ (matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel') && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
|
targets: ${{ (matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel') && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
|
||||||
|
|
||||||
- name: Set up JDK 21
|
- name: Set up JDK 21
|
||||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||||
with:
|
with:
|
||||||
java-version: "21"
|
java-version: "21"
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
@@ -516,7 +516,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Upload build artifacts
|
- name: Upload build artifacts
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: Stirling-PDF-${{ matrix.name }}
|
name: Stirling-PDF-${{ matrix.name }}
|
||||||
path: ./dist/*
|
path: ./dist/*
|
||||||
@@ -530,30 +530,30 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: Download all Tauri artifacts
|
- name: Download all Tauri artifacts
|
||||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||||
with:
|
with:
|
||||||
pattern: Stirling-PDF-*
|
pattern: Stirling-PDF-*
|
||||||
path: ./artifacts/tauri
|
path: ./artifacts/tauri
|
||||||
|
|
||||||
- name: Download JAR artifact (default)
|
- name: Download JAR artifact (default)
|
||||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||||
with:
|
with:
|
||||||
name: jar
|
name: jar
|
||||||
path: ./artifacts/jars
|
path: ./artifacts/jars
|
||||||
|
|
||||||
- name: Download JAR artifact (with login)
|
- name: Download JAR artifact (with login)
|
||||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||||
with:
|
with:
|
||||||
name: jar-with-login
|
name: jar-with-login
|
||||||
path: ./artifacts/jars
|
path: ./artifacts/jars
|
||||||
|
|
||||||
- name: Download JAR artifact (server only)
|
- name: Download JAR artifact (server only)
|
||||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||||
with:
|
with:
|
||||||
name: jar-server
|
name: jar-server
|
||||||
path: ./artifacts/jars
|
path: ./artifacts/jars
|
||||||
@@ -562,7 +562,7 @@ jobs:
|
|||||||
run: ls -R ./artifacts
|
run: ls -R ./artifacts
|
||||||
|
|
||||||
- name: Upload binaries to Release
|
- name: Upload binaries to Release
|
||||||
uses: softprops/action-gh-release@62c96d0c4e8a889135c1f3a25910db8dbe0e85f7 # v2.3.4
|
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2
|
||||||
with:
|
with:
|
||||||
tag_name: v${{ needs.determine-matrix.outputs.version }}
|
tag_name: v${{ needs.determine-matrix.outputs.version }}
|
||||||
generate_release_notes: true
|
generate_release_notes: true
|
||||||
|
|||||||
@@ -21,12 +21,12 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
if: env.CHANGES_DETECTED == 'true'
|
if: env.CHANGES_DETECTED == 'true'
|
||||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
|
uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7.0.9
|
||||||
with:
|
with:
|
||||||
token: ${{ steps.setup-bot.outputs.token }}
|
token: ${{ steps.setup-bot.outputs.token }}
|
||||||
commit-message: ":file_folder: pre-commit"
|
commit-message: ":file_folder: pre-commit"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- V2-master
|
- V2-master
|
||||||
- alljavadocker
|
- V1_V2_merge
|
||||||
|
|
||||||
# cancel in-progress jobs if a new job is triggered
|
# cancel in-progress jobs if a new job is triggered
|
||||||
# This is useful to avoid running multiple builds for the same branch if a new commit is pushed
|
# This is useful to avoid running multiple builds for the same branch if a new commit is pushed
|
||||||
@@ -95,10 +95,10 @@ jobs:
|
|||||||
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}
|
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}
|
||||||
type=raw,value=latest
|
type=raw,value=latest
|
||||||
|
|
||||||
- name: Generate tags for latest (alljavadocker branch - test)
|
- name: Generate tags for latest (V1_V2_merge branch - test)
|
||||||
id: meta-test
|
id: meta-test
|
||||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
|
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
|
||||||
if: github.ref == 'refs/heads/alljavadocker'
|
if: github.ref == 'refs/heads/V1_V2_merge'
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
ghcr.io/stirling-tools/stirling-pdf-test
|
ghcr.io/stirling-tools/stirling-pdf-test
|
||||||
@@ -151,10 +151,10 @@ jobs:
|
|||||||
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-fat
|
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-fat
|
||||||
type=raw,value=latest-fat
|
type=raw,value=latest-fat
|
||||||
|
|
||||||
- name: Generate tags for latest-fat (alljavadocker branch - test)
|
- name: Generate tags for latest-fat (V1_V2_merge branch - test)
|
||||||
id: meta-fat-test
|
id: meta-fat-test
|
||||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
|
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
|
||||||
if: github.ref == 'refs/heads/alljavadocker'
|
if: github.ref == 'refs/heads/V1_V2_merge'
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
ghcr.io/stirling-tools/stirling-pdf-test
|
ghcr.io/stirling-tools/stirling-pdf-test
|
||||||
@@ -205,10 +205,10 @@ jobs:
|
|||||||
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-ultra-lite
|
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-ultra-lite
|
||||||
type=raw,value=latest-ultra-lite
|
type=raw,value=latest-ultra-lite
|
||||||
|
|
||||||
- name: Generate tags for ultra-lite (alljavadocker branch - test)
|
- name: Generate tags for ultra-lite (V1_V2_merge branch - test)
|
||||||
id: meta-lite-test
|
id: meta-lite-test
|
||||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
|
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
|
||||||
if: github.ref == 'refs/heads/alljavadocker'
|
if: github.ref == 'refs/heads/V1_V2_merge'
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
ghcr.io/stirling-tools/stirling-pdf-test
|
ghcr.io/stirling-tools/stirling-pdf-test
|
||||||
|
|||||||
@@ -31,11 +31,11 @@ jobs:
|
|||||||
id-token: write
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||||
|
|
||||||
- name: Set up JDK 17
|
- name: Set up JDK 17
|
||||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||||
@@ -55,7 +55,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Install cosign
|
- name: Install cosign
|
||||||
if: github.ref == 'refs/heads/master'
|
if: github.ref == 'refs/heads/master'
|
||||||
uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0
|
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
|
||||||
with:
|
with:
|
||||||
cosign-release: "v2.4.1"
|
cosign-release: "v2.4.1"
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ jobs:
|
|||||||
password: ${{ github.token }}
|
password: ${{ github.token }}
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
|
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||||
|
|
||||||
- name: Convert repository owner to lowercase
|
- name: Convert repository owner to lowercase
|
||||||
id: repoowner
|
id: repoowner
|
||||||
@@ -89,7 +89,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Generate tags
|
- name: Generate tags
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
|
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5.9.0
|
||||||
if: github.ref != 'refs/heads/main'
|
if: github.ref != 'refs/heads/main'
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
@@ -135,7 +135,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Generate tags ultra-lite
|
- name: Generate tags ultra-lite
|
||||||
id: meta2
|
id: meta2
|
||||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
|
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5.9.0
|
||||||
if: github.ref != 'refs/heads/main'
|
if: github.ref != 'refs/heads/main'
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
@@ -166,7 +166,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Generate tags fat
|
- name: Generate tags fat
|
||||||
id: meta3
|
id: meta3
|
||||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
|
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5.9.0
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ jobs:
|
|||||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||||
version: ${{ steps.versionNumber.outputs.versionNumber }}
|
version: ${{ steps.versionNumber.outputs.versionNumber }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||||
|
|
||||||
- name: Get version number
|
- name: Get version number
|
||||||
id: versionNumber
|
id: versionNumber
|
||||||
@@ -68,12 +68,12 @@ jobs:
|
|||||||
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
|
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||||
|
|
||||||
- name: Install dependencies (ubuntu only)
|
- name: Install dependencies (ubuntu only)
|
||||||
if: matrix.platform == 'ubuntu-22.04'
|
if: matrix.platform == 'ubuntu-22.04'
|
||||||
@@ -95,7 +95,7 @@ jobs:
|
|||||||
targets: ${{ (matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel') && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
|
targets: ${{ (matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel') && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
|
||||||
|
|
||||||
- name: Set up JDK 21
|
- name: Set up JDK 21
|
||||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||||
with:
|
with:
|
||||||
java-version: "21"
|
java-version: "21"
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
@@ -448,7 +448,7 @@ jobs:
|
|||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: Download build artifacts
|
- name: Download build artifacts
|
||||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||||
with:
|
with:
|
||||||
name: Stirling-PDF-${{ matrix.name }}
|
name: Stirling-PDF-${{ matrix.name }}
|
||||||
|
|
||||||
@@ -517,7 +517,7 @@ jobs:
|
|||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: Download all signed artifacts
|
- name: Download all signed artifacts
|
||||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||||
with:
|
with:
|
||||||
pattern: Stirling-PDF-*-signed
|
pattern: Stirling-PDF-*-signed
|
||||||
path: ./artifacts
|
path: ./artifacts
|
||||||
@@ -526,7 +526,7 @@ jobs:
|
|||||||
run: ls -R ./artifacts
|
run: ls -R ./artifacts
|
||||||
|
|
||||||
- name: Create GitHub Release
|
- name: Create GitHub Release
|
||||||
uses: softprops/action-gh-release@62c96d0c4e8a889135c1f3a25910db8dbe0e85f7 # v2.3.4
|
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2
|
||||||
with:
|
with:
|
||||||
tag_name: v${{ needs.determine-matrix.outputs.version }}
|
tag_name: v${{ needs.determine-matrix.outputs.version }}
|
||||||
generate_release_notes: true
|
generate_release_notes: true
|
||||||
|
|||||||
@@ -35,12 +35,12 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: "Checkout code"
|
- name: "Checkout code"
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ jobs:
|
|||||||
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
|
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
|
||||||
# format to the repository Actions tab.
|
# format to the repository Actions tab.
|
||||||
- name: "Upload artifact"
|
- name: "Upload artifact"
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: SARIF file
|
name: SARIF file
|
||||||
path: results.sarif
|
path: results.sarif
|
||||||
@@ -75,6 +75,6 @@ jobs:
|
|||||||
|
|
||||||
# Upload the results to GitHub's code scanning dashboard.
|
# Upload the results to GitHub's code scanning dashboard.
|
||||||
- name: "Upload to code-scanning"
|
- name: "Upload to code-scanning"
|
||||||
uses: github/codeql-action/upload-sarif@64d10c13136e1c5bce3e5fbde8d4906eeaafc885 # v3.29.5
|
uses: github/codeql-action/upload-sarif@fdbfb4d2750291e159f0156def62b853c2798ca2 # v3.29.5
|
||||||
with:
|
with:
|
||||||
sarif_file: results.sarif
|
sarif_file: results.sarif
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
name: Run Sonarqube
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
pull_request_target:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
# cancel in-progress jobs if a new job is triggered
|
|
||||||
# This is useful to avoid running multiple builds for the same branch if a new commit is pushed
|
|
||||||
# or a pull request is updated.
|
|
||||||
# It helps to save resources and time by ensuring that only the latest commit is built and tested
|
|
||||||
# This is particularly useful for long-running jobs that may take a while to complete.
|
|
||||||
# The `group` is set to a combination of the workflow name, event name, and branch name.
|
|
||||||
# This ensures that jobs are grouped by the workflow and branch, allowing for cancellation of
|
|
||||||
# in-progress jobs when a new commit is pushed to the same branch or a new pull request is opened.
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref_name || github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
pull-requests: read
|
|
||||||
actions: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
sonarqube:
|
|
||||||
if: ${{ vars.CI_PROFILE != 'lite' }}
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Harden Runner
|
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
|
||||||
with:
|
|
||||||
egress-policy: audit
|
|
||||||
|
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Setup Gradle
|
|
||||||
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
|
||||||
|
|
||||||
- name: Build and analyze with Gradle
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
|
||||||
DISABLE_ADDITIONAL_FEATURES: false
|
|
||||||
STIRLING_PDF_DESKTOP_UI: true
|
|
||||||
run: |
|
|
||||||
./gradlew clean build sonar \
|
|
||||||
-Dsonar.projectKey=Stirling-Tools_Stirling-PDF \
|
|
||||||
-Dsonar.organization=stirling-tools \
|
|
||||||
-Dsonar.host.url=https://sonarcloud.io \
|
|
||||||
-Dsonar.login=${SONAR_TOKEN} \
|
|
||||||
-Dsonar.log.level=DEBUG \
|
|
||||||
--info
|
|
||||||
|
|
||||||
- name: Upload Problems Report on Failure
|
|
||||||
if: failure()
|
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
|
||||||
with:
|
|
||||||
name: gradle-problems-report
|
|
||||||
path: build/reports/problems/problems-report.html
|
|
||||||
retention-days: 7
|
|
||||||
|
|
||||||
- name: Upload Sonar Logs on Failure
|
|
||||||
if: failure()
|
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
|
||||||
with:
|
|
||||||
name: sonar-logs
|
|
||||||
path: |
|
|
||||||
.scannerwork/report-task.txt
|
|
||||||
build/sonar/
|
|
||||||
retention-days: 7
|
|
||||||
@@ -17,7 +17,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|||||||
@@ -27,11 +27,11 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||||
|
|
||||||
- name: Set up JDK 17
|
- name: Set up JDK 17
|
||||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||||
|
|||||||
@@ -33,11 +33,11 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||||
|
|
||||||
- name: Setup GitHub App Bot
|
- name: Setup GitHub App Bot
|
||||||
id: setup-bot
|
id: setup-bot
|
||||||
@@ -47,7 +47,7 @@ jobs:
|
|||||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
python-version: "3.12"
|
||||||
cache: "pip" # caching pip dependencies
|
cache: "pip" # caching pip dependencies
|
||||||
|
|||||||
@@ -25,12 +25,12 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||||
|
|
||||||
- name: Set up JDK
|
- name: Set up JDK
|
||||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||||
@@ -125,7 +125,7 @@ jobs:
|
|||||||
outputs:
|
outputs:
|
||||||
frontend: ${{ steps.changes.outputs.frontend }}
|
frontend: ${{ steps.changes.outputs.frontend }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||||
|
|
||||||
- name: Check for file changes
|
- name: Check for file changes
|
||||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||||
@@ -140,14 +140,14 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||||
|
|
||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||||
with:
|
with:
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
cache-dependency-path: frontend/package-lock.json
|
cache-dependency-path: frontend/package-lock.json
|
||||||
@@ -176,7 +176,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.12.7
|
rev: v0.14.2
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
args:
|
args:
|
||||||
@@ -26,7 +26,7 @@ repos:
|
|||||||
hooks:
|
hooks:
|
||||||
- id: gitleaks
|
- id: gitleaks
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v5.0.0
|
rev: v6.0.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: end-of-file-fixer
|
- id: end-of-file-fixer
|
||||||
files: ^.*(\.js|\.java|\.py|\.yml)$
|
files: ^.*(\.js|\.java|\.py|\.yml)$
|
||||||
|
|||||||
@@ -33,16 +33,16 @@ dependencies {
|
|||||||
api 'com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20240325.1'
|
api 'com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20240325.1'
|
||||||
api 'com.fathzer:javaluator:3.0.6'
|
api 'com.fathzer:javaluator:3.0.6'
|
||||||
api 'com.posthog.java:posthog:1.2.0'
|
api 'com.posthog.java:posthog:1.2.0'
|
||||||
api 'org.apache.commons:commons-lang3:3.19.0'
|
api 'org.apache.commons:commons-lang3:3.20.0'
|
||||||
api 'com.drewnoakes:metadata-extractor:2.19.0' // Image metadata extractor
|
api 'com.drewnoakes:metadata-extractor:2.19.0' // Image metadata extractor
|
||||||
api 'com.vladsch.flexmark:flexmark-html2md-converter:0.64.8'
|
api 'com.vladsch.flexmark:flexmark-html2md-converter:0.64.8'
|
||||||
api "org.apache.pdfbox:pdfbox:$pdfboxVersion"
|
api "org.apache.pdfbox:pdfbox:$pdfboxVersion"
|
||||||
api "org.apache.pdfbox:xmpbox:$pdfboxVersion"
|
api "org.apache.pdfbox:xmpbox:$pdfboxVersion"
|
||||||
api "org.apache.pdfbox:preflight:$pdfboxVersion"
|
api "org.apache.pdfbox:preflight:$pdfboxVersion"
|
||||||
api 'com.github.junrar:junrar:7.5.5' // RAR archive support for CBR files
|
api 'com.github.junrar:junrar:7.5.7' // RAR archive support for CBR files
|
||||||
api 'jakarta.servlet:jakarta.servlet-api:6.1.0'
|
api 'jakarta.servlet:jakarta.servlet-api:6.1.0'
|
||||||
api 'org.snakeyaml:snakeyaml-engine:2.10'
|
api 'org.snakeyaml:snakeyaml-engine:2.10'
|
||||||
api "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.13"
|
api "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.14"
|
||||||
api 'jakarta.mail:jakarta.mail-api:2.1.5'
|
api 'jakarta.mail:jakarta.mail-api:2.1.5'
|
||||||
runtimeOnly 'org.eclipse.angus:angus-mail:2.0.5'
|
runtimeOnly 'org.eclipse.angus:angus-mail:2.0.5'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -334,6 +334,9 @@ public class EndpointConfiguration {
|
|||||||
addEndpointToGroup("Convert", "pdf-to-csv");
|
addEndpointToGroup("Convert", "pdf-to-csv");
|
||||||
addEndpointToGroup("Convert", "pdf-to-markdown");
|
addEndpointToGroup("Convert", "pdf-to-markdown");
|
||||||
addEndpointToGroup("Convert", "eml-to-pdf");
|
addEndpointToGroup("Convert", "eml-to-pdf");
|
||||||
|
addEndpointToGroup("Convert", "pdf-to-vector");
|
||||||
|
addEndpointToGroup("Convert", "vector-to-pdf");
|
||||||
|
addEndpointToGroup("Convert", "pdf-to-video");
|
||||||
addEndpointToGroup("Convert", "cbz-to-pdf");
|
addEndpointToGroup("Convert", "cbz-to-pdf");
|
||||||
addEndpointToGroup("Convert", "pdf-to-cbz");
|
addEndpointToGroup("Convert", "pdf-to-cbz");
|
||||||
addEndpointToGroup("Convert", "pdf-to-json");
|
addEndpointToGroup("Convert", "pdf-to-json");
|
||||||
@@ -490,6 +493,10 @@ public class EndpointConfiguration {
|
|||||||
/* Ghostscript */
|
/* Ghostscript */
|
||||||
addEndpointToGroup("Ghostscript", "repair");
|
addEndpointToGroup("Ghostscript", "repair");
|
||||||
addEndpointToGroup("Ghostscript", "compress-pdf");
|
addEndpointToGroup("Ghostscript", "compress-pdf");
|
||||||
|
addEndpointToGroup("Ghostscript", "crop");
|
||||||
|
addEndpointToGroup("Ghostscript", "replace-invert-pdf");
|
||||||
|
addEndpointToGroup("Ghostscript", "pdf-to-vector");
|
||||||
|
addEndpointToGroup("Ghostscript", "vector-to-pdf");
|
||||||
|
|
||||||
/* ImageMagick */
|
/* ImageMagick */
|
||||||
addEndpointToGroup("ImageMagick", "compress-pdf");
|
addEndpointToGroup("ImageMagick", "compress-pdf");
|
||||||
@@ -554,7 +561,7 @@ public class EndpointConfiguration {
|
|||||||
disableGroup("enterprise");
|
disableGroup("enterprise");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!applicationProperties.getSystem().getEnableUrlToPDF()) {
|
if (!applicationProperties.getSystem().isEnableUrlToPDF()) {
|
||||||
disableEndpoint("url-to-pdf");
|
disableEndpoint("url-to-pdf");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ public class AutoJobAspect {
|
|||||||
|
|
||||||
@Around("@annotation(autoJobPostMapping)")
|
@Around("@annotation(autoJobPostMapping)")
|
||||||
public Object wrapWithJobExecution(
|
public Object wrapWithJobExecution(
|
||||||
ProceedingJoinPoint joinPoint, AutoJobPostMapping autoJobPostMapping) {
|
ProceedingJoinPoint joinPoint, AutoJobPostMapping autoJobPostMapping) throws Exception {
|
||||||
// This aspect will run before any audit aspects due to @Order(0)
|
// This aspect will run before any audit aspects due to @Order(0)
|
||||||
// Extract parameters from the request and annotation
|
// Extract parameters from the request and annotation
|
||||||
boolean async = Boolean.parseBoolean(request.getParameter("async"));
|
boolean async = Boolean.parseBoolean(request.getParameter("async"));
|
||||||
@@ -81,6 +81,12 @@ public class AutoJobAspect {
|
|||||||
"AutoJobAspect caught exception during job execution: {}",
|
"AutoJobAspect caught exception during job execution: {}",
|
||||||
ex.getMessage(),
|
ex.getMessage(),
|
||||||
ex);
|
ex);
|
||||||
|
// Rethrow RuntimeException as-is to preserve exception type
|
||||||
|
if (ex instanceof RuntimeException) {
|
||||||
|
throw (RuntimeException) ex;
|
||||||
|
}
|
||||||
|
// Wrap checked exceptions - GlobalExceptionHandler will unwrap
|
||||||
|
// BaseAppException
|
||||||
throw new RuntimeException(ex);
|
throw new RuntimeException(ex);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -109,7 +115,8 @@ public class AutoJobAspect {
|
|||||||
int maxRetries,
|
int maxRetries,
|
||||||
boolean trackProgress,
|
boolean trackProgress,
|
||||||
boolean queueable,
|
boolean queueable,
|
||||||
int resourceWeight) {
|
int resourceWeight)
|
||||||
|
throws Exception {
|
||||||
|
|
||||||
// Keep jobId reference for progress tracking in TaskManager
|
// Keep jobId reference for progress tracking in TaskManager
|
||||||
AtomicReference<String> jobIdRef = new AtomicReference<>();
|
AtomicReference<String> jobIdRef = new AtomicReference<>();
|
||||||
@@ -207,6 +214,12 @@ public class AutoJobAspect {
|
|||||||
|
|
||||||
// If we get here, all retries failed
|
// If we get here, all retries failed
|
||||||
if (lastException != null) {
|
if (lastException != null) {
|
||||||
|
// Rethrow RuntimeException as-is to preserve exception type
|
||||||
|
if (lastException instanceof RuntimeException) {
|
||||||
|
throw (RuntimeException) lastException;
|
||||||
|
}
|
||||||
|
// Wrap checked exceptions - GlobalExceptionHandler will unwrap
|
||||||
|
// BaseAppException
|
||||||
throw new RuntimeException(
|
throw new RuntimeException(
|
||||||
"Job failed after "
|
"Job failed after "
|
||||||
+ maxRetries
|
+ maxRetries
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ public class AppConfig {
|
|||||||
|
|
||||||
@Bean(name = "loginEnabled")
|
@Bean(name = "loginEnabled")
|
||||||
public boolean loginEnabled() {
|
public boolean loginEnabled() {
|
||||||
return applicationProperties.getSecurity().getEnableLogin();
|
return applicationProperties.getSecurity().isEnableLogin();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean(name = "appName")
|
@Bean(name = "appName")
|
||||||
@@ -120,9 +120,7 @@ public class AppConfig {
|
|||||||
|
|
||||||
@Bean(name = "enableAlphaFunctionality")
|
@Bean(name = "enableAlphaFunctionality")
|
||||||
public boolean enableAlphaFunctionality() {
|
public boolean enableAlphaFunctionality() {
|
||||||
return applicationProperties.getSystem().getEnableAlphaFunctionality() != null
|
return applicationProperties.getSystem().isEnableAlphaFunctionality();
|
||||||
? applicationProperties.getSystem().getEnableAlphaFunctionality()
|
|
||||||
: false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean(name = "rateLimit")
|
@Bean(name = "rateLimit")
|
||||||
@@ -265,9 +263,14 @@ public class AppConfig {
|
|||||||
return "NORMAL";
|
return "NORMAL";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean(name = "disablePixel")
|
@Bean(name = "scarfEnabled")
|
||||||
public boolean disablePixel() {
|
public boolean scarfEnabled() {
|
||||||
return Boolean.parseBoolean(env.getProperty("DISABLE_PIXEL", "false"));
|
return applicationProperties.getSystem().isScarfEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean(name = "posthogEnabled")
|
||||||
|
public boolean posthogEnabled() {
|
||||||
|
return applicationProperties.getSystem().isPosthogEnabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean(name = "machineType")
|
@Bean(name = "machineType")
|
||||||
|
|||||||
+2
-1
@@ -2,6 +2,7 @@ package stirling.software.common.configuration;
|
|||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
@@ -61,7 +62,7 @@ public class InstallationPathConfig {
|
|||||||
|
|
||||||
private static String initializeBasePath() {
|
private static String initializeBasePath() {
|
||||||
if (Boolean.parseBoolean(System.getProperty("STIRLING_PDF_DESKTOP_UI", "false"))) {
|
if (Boolean.parseBoolean(System.getProperty("STIRLING_PDF_DESKTOP_UI", "false"))) {
|
||||||
String os = System.getProperty("os.name").toLowerCase();
|
String os = System.getProperty("os.name").toLowerCase(Locale.ROOT);
|
||||||
if (os.contains("win")) {
|
if (os.contains("win")) {
|
||||||
return Paths.get(
|
return Paths.get(
|
||||||
System.getenv("APPDATA"), // parent path
|
System.getenv("APPDATA"), // parent path
|
||||||
|
|||||||
+40
-2
@@ -10,8 +10,10 @@ import lombok.Getter;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.common.model.ApplicationProperties;
|
import stirling.software.common.model.ApplicationProperties;
|
||||||
|
import stirling.software.common.model.ApplicationProperties.CustomPaths;
|
||||||
import stirling.software.common.model.ApplicationProperties.CustomPaths.Operations;
|
import stirling.software.common.model.ApplicationProperties.CustomPaths.Operations;
|
||||||
import stirling.software.common.model.ApplicationProperties.CustomPaths.Pipeline;
|
import stirling.software.common.model.ApplicationProperties.CustomPaths.Pipeline;
|
||||||
|
import stirling.software.common.model.ApplicationProperties.System;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Configuration
|
@Configuration
|
||||||
@@ -19,8 +21,16 @@ import stirling.software.common.model.ApplicationProperties.CustomPaths.Pipeline
|
|||||||
public class RuntimePathConfig {
|
public class RuntimePathConfig {
|
||||||
private final ApplicationProperties properties;
|
private final ApplicationProperties properties;
|
||||||
private final String basePath;
|
private final String basePath;
|
||||||
|
|
||||||
|
// Operation paths
|
||||||
private final String weasyPrintPath;
|
private final String weasyPrintPath;
|
||||||
private final String unoConvertPath;
|
private final String unoConvertPath;
|
||||||
|
private final String calibrePath;
|
||||||
|
private final String ocrMyPdfPath;
|
||||||
|
private final String sOfficePath;
|
||||||
|
|
||||||
|
// Tesseract data path
|
||||||
|
private final String tessDataPath;
|
||||||
|
|
||||||
// Pipeline paths
|
// Pipeline paths
|
||||||
private final String pipelineWatchedFoldersPath;
|
private final String pipelineWatchedFoldersPath;
|
||||||
@@ -37,7 +47,10 @@ public class RuntimePathConfig {
|
|||||||
String defaultFinishedFolders = Path.of(this.pipelinePath, "finishedFolders").toString();
|
String defaultFinishedFolders = Path.of(this.pipelinePath, "finishedFolders").toString();
|
||||||
String defaultWebUIConfigs = Path.of(this.pipelinePath, "defaultWebUIConfigs").toString();
|
String defaultWebUIConfigs = Path.of(this.pipelinePath, "defaultWebUIConfigs").toString();
|
||||||
|
|
||||||
Pipeline pipeline = properties.getSystem().getCustomPaths().getPipeline();
|
System system = properties.getSystem();
|
||||||
|
CustomPaths customPaths = system.getCustomPaths();
|
||||||
|
|
||||||
|
Pipeline pipeline = customPaths.getPipeline();
|
||||||
|
|
||||||
this.pipelineWatchedFoldersPath =
|
this.pipelineWatchedFoldersPath =
|
||||||
resolvePath(
|
resolvePath(
|
||||||
@@ -57,8 +70,11 @@ public class RuntimePathConfig {
|
|||||||
// Initialize Operation paths
|
// Initialize Operation paths
|
||||||
String defaultWeasyPrintPath = isDocker ? "/opt/venv/bin/weasyprint" : "weasyprint";
|
String defaultWeasyPrintPath = isDocker ? "/opt/venv/bin/weasyprint" : "weasyprint";
|
||||||
String defaultUnoConvertPath = isDocker ? "/opt/venv/bin/unoconvert" : "unoconvert";
|
String defaultUnoConvertPath = isDocker ? "/opt/venv/bin/unoconvert" : "unoconvert";
|
||||||
|
String defaultCalibrePath = isDocker ? "/opt/calibre/ebook-convert" : "ebook-convert";
|
||||||
|
String defaultOcrMyPdfPath = isDocker ? "/usr/bin/ocrmypdf" : "ocrmypdf";
|
||||||
|
String defaultSOfficePath = isDocker ? "/usr/bin/soffice" : "soffice";
|
||||||
|
|
||||||
Operations operations = properties.getSystem().getCustomPaths().getOperations();
|
Operations operations = customPaths.getOperations();
|
||||||
this.weasyPrintPath =
|
this.weasyPrintPath =
|
||||||
resolvePath(
|
resolvePath(
|
||||||
defaultWeasyPrintPath,
|
defaultWeasyPrintPath,
|
||||||
@@ -67,6 +83,28 @@ public class RuntimePathConfig {
|
|||||||
resolvePath(
|
resolvePath(
|
||||||
defaultUnoConvertPath,
|
defaultUnoConvertPath,
|
||||||
operations != null ? operations.getUnoconvert() : null);
|
operations != null ? operations.getUnoconvert() : null);
|
||||||
|
this.calibrePath =
|
||||||
|
resolvePath(
|
||||||
|
defaultCalibrePath, operations != null ? operations.getCalibre() : null);
|
||||||
|
this.ocrMyPdfPath =
|
||||||
|
resolvePath(
|
||||||
|
defaultOcrMyPdfPath, operations != null ? operations.getOcrmypdf() : null);
|
||||||
|
this.sOfficePath =
|
||||||
|
resolvePath(
|
||||||
|
defaultSOfficePath, operations != null ? operations.getSoffice() : null);
|
||||||
|
|
||||||
|
// Initialize Tesseract data path
|
||||||
|
String defaultTessDataPath =
|
||||||
|
isDocker ? "/usr/share/tesseract-ocr/5/tessdata" : "/usr/share/tessdata";
|
||||||
|
|
||||||
|
String tessPath = system.getTessdataDir();
|
||||||
|
String tessdataDir = java.lang.System.getenv("TESSDATA_PREFIX");
|
||||||
|
|
||||||
|
this.tessDataPath =
|
||||||
|
resolvePath(
|
||||||
|
defaultTessDataPath,
|
||||||
|
(tessPath != null && !tessPath.isEmpty()) ? tessPath : tessdataDir);
|
||||||
|
log.info("Using Tesseract data path: {}", this.tessDataPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String resolvePath(String defaultPath, String customPath) {
|
private String resolvePath(String defaultPath, String customPath) {
|
||||||
|
|||||||
+47
-14
@@ -12,6 +12,7 @@ import java.util.ArrayList;
|
|||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
@@ -152,7 +153,7 @@ public class ApplicationProperties {
|
|||||||
|
|
||||||
@Data
|
@Data
|
||||||
public static class Security {
|
public static class Security {
|
||||||
private Boolean enableLogin;
|
private boolean enableLogin;
|
||||||
private InitialLogin initialLogin = new InitialLogin();
|
private InitialLogin initialLogin = new InitialLogin();
|
||||||
private OAUTH2 oauth2 = new OAUTH2();
|
private OAUTH2 oauth2 = new OAUTH2();
|
||||||
private SAML2 saml2 = new SAML2();
|
private SAML2 saml2 = new SAML2();
|
||||||
@@ -327,7 +328,7 @@ public class ApplicationProperties {
|
|||||||
private KeycloakProvider keycloak = new KeycloakProvider();
|
private KeycloakProvider keycloak = new KeycloakProvider();
|
||||||
|
|
||||||
public Provider get(String registrationId) throws UnsupportedProviderException {
|
public Provider get(String registrationId) throws UnsupportedProviderException {
|
||||||
return switch (registrationId.toLowerCase()) {
|
return switch (registrationId.toLowerCase(Locale.ROOT)) {
|
||||||
case "google" -> getGoogle();
|
case "google" -> getGoogle();
|
||||||
case "github" -> getGithub();
|
case "github" -> getGithub();
|
||||||
case "keycloak" -> getKeycloak();
|
case "keycloak" -> getKeycloak();
|
||||||
@@ -335,8 +336,8 @@ public class ApplicationProperties {
|
|||||||
throw new UnsupportedProviderException(
|
throw new UnsupportedProviderException(
|
||||||
"Logout from the provider "
|
"Logout from the provider "
|
||||||
+ registrationId
|
+ registrationId
|
||||||
+ " is not supported. "
|
+ " is not supported. Report it at"
|
||||||
+ "Report it at https://github.com/Stirling-Tools/Stirling-PDF/issues");
|
+ " https://github.com/Stirling-Tools/Stirling-PDF/issues");
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -389,20 +390,20 @@ public class ApplicationProperties {
|
|||||||
@Data
|
@Data
|
||||||
public static class System {
|
public static class System {
|
||||||
private String defaultLocale;
|
private String defaultLocale;
|
||||||
private Boolean googlevisibility;
|
private boolean googlevisibility;
|
||||||
private boolean showUpdate;
|
private boolean showUpdate;
|
||||||
private Boolean showUpdateOnlyAdmin;
|
private boolean showUpdateOnlyAdmin;
|
||||||
private boolean customHTMLFiles;
|
private boolean customHTMLFiles;
|
||||||
private String tessdataDir;
|
private String tessdataDir;
|
||||||
private Boolean enableAlphaFunctionality;
|
private boolean enableAlphaFunctionality;
|
||||||
private Boolean enableAnalytics;
|
private Boolean enableAnalytics;
|
||||||
private Boolean enablePosthog;
|
private Boolean enablePosthog;
|
||||||
private Boolean enableScarf;
|
private Boolean enableScarf;
|
||||||
private Boolean enableDesktopInstallSlide;
|
private Boolean enableDesktopInstallSlide;
|
||||||
private Datasource datasource;
|
private Datasource datasource;
|
||||||
private Boolean disableSanitize;
|
private boolean disableSanitize;
|
||||||
private int maxDPI;
|
private int maxDPI;
|
||||||
private Boolean enableUrlToPDF;
|
private boolean enableUrlToPDF;
|
||||||
private Html html = new Html();
|
private Html html = new Html();
|
||||||
private CustomPaths customPaths = new CustomPaths();
|
private CustomPaths customPaths = new CustomPaths();
|
||||||
private String fileUploadLimit;
|
private String fileUploadLimit;
|
||||||
@@ -454,6 +455,9 @@ public class ApplicationProperties {
|
|||||||
public static class Operations {
|
public static class Operations {
|
||||||
private String weasyprint;
|
private String weasyprint;
|
||||||
private String unoconvert;
|
private String unoconvert;
|
||||||
|
private String calibre;
|
||||||
|
private String ocrmypdf;
|
||||||
|
private String soffice;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -536,10 +540,10 @@ public class ApplicationProperties {
|
|||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return """
|
return """
|
||||||
Driver {
|
Driver {
|
||||||
driverName='%s'
|
driverName='%s'
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
.formatted(driverName);
|
.formatted(driverName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -571,7 +575,7 @@ public class ApplicationProperties {
|
|||||||
|
|
||||||
@Data
|
@Data
|
||||||
public static class Metrics {
|
public static class Metrics {
|
||||||
private Boolean enabled;
|
private boolean enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@@ -668,6 +672,25 @@ public class ApplicationProperties {
|
|||||||
public static class EnterpriseFeatures {
|
public static class EnterpriseFeatures {
|
||||||
private PersistentMetrics persistentMetrics = new PersistentMetrics();
|
private PersistentMetrics persistentMetrics = new PersistentMetrics();
|
||||||
private Audit audit = new Audit();
|
private Audit audit = new Audit();
|
||||||
|
private DatabaseNotifications databaseNotifications = new DatabaseNotifications();
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class DatabaseNotifications {
|
||||||
|
private Backup backups = new Backup();
|
||||||
|
private Imports imports = new Imports();
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class Backup {
|
||||||
|
private boolean successful = false;
|
||||||
|
private boolean failed = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class Imports {
|
||||||
|
private boolean successful = false;
|
||||||
|
private boolean failed = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public static class Audit {
|
public static class Audit {
|
||||||
@@ -702,6 +725,7 @@ public class ApplicationProperties {
|
|||||||
private int tesseractSessionLimit;
|
private int tesseractSessionLimit;
|
||||||
private int ghostscriptSessionLimit;
|
private int ghostscriptSessionLimit;
|
||||||
private int ocrMyPdfSessionLimit;
|
private int ocrMyPdfSessionLimit;
|
||||||
|
private int ffmpegSessionLimit;
|
||||||
|
|
||||||
public int getQpdfSessionLimit() {
|
public int getQpdfSessionLimit() {
|
||||||
return qpdfSessionLimit > 0 ? qpdfSessionLimit : 2;
|
return qpdfSessionLimit > 0 ? qpdfSessionLimit : 2;
|
||||||
@@ -746,6 +770,10 @@ public class ApplicationProperties {
|
|||||||
public int getOcrMyPdfSessionLimit() {
|
public int getOcrMyPdfSessionLimit() {
|
||||||
return ocrMyPdfSessionLimit > 0 ? ocrMyPdfSessionLimit : 2;
|
return ocrMyPdfSessionLimit > 0 ? ocrMyPdfSessionLimit : 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getFfmpegSessionLimit() {
|
||||||
|
return ffmpegSessionLimit > 0 ? ffmpegSessionLimit : 2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@@ -774,6 +802,7 @@ public class ApplicationProperties {
|
|||||||
private long qpdfTimeoutMinutes;
|
private long qpdfTimeoutMinutes;
|
||||||
private long ghostscriptTimeoutMinutes;
|
private long ghostscriptTimeoutMinutes;
|
||||||
private long ocrMyPdfTimeoutMinutes;
|
private long ocrMyPdfTimeoutMinutes;
|
||||||
|
private long ffmpegTimeoutMinutes;
|
||||||
|
|
||||||
public long getTesseractTimeoutMinutes() {
|
public long getTesseractTimeoutMinutes() {
|
||||||
return tesseractTimeoutMinutes > 0 ? tesseractTimeoutMinutes : 30;
|
return tesseractTimeoutMinutes > 0 ? tesseractTimeoutMinutes : 30;
|
||||||
@@ -818,6 +847,10 @@ public class ApplicationProperties {
|
|||||||
public long getOcrMyPdfTimeoutMinutes() {
|
public long getOcrMyPdfTimeoutMinutes() {
|
||||||
return ocrMyPdfTimeoutMinutes > 0 ? ocrMyPdfTimeoutMinutes : 30;
|
return ocrMyPdfTimeoutMinutes > 0 ? ocrMyPdfTimeoutMinutes : 30;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long getFfmpegTimeoutMinutes() {
|
||||||
|
return ffmpegTimeoutMinutes > 0 ? ffmpegTimeoutMinutes : 30;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,11 +30,11 @@ public class FileInfo {
|
|||||||
// Formats the file size into a human-readable string.
|
// Formats the file size into a human-readable string.
|
||||||
public String getFormattedFileSize() {
|
public String getFormattedFileSize() {
|
||||||
if (fileSize >= 1024 * 1024 * 1024) {
|
if (fileSize >= 1024 * 1024 * 1024) {
|
||||||
return String.format(Locale.US, "%.2f GB", fileSize / (1024.0 * 1024 * 1024));
|
return String.format(Locale.ROOT, "%.2f GB", fileSize / (1024.0 * 1024 * 1024));
|
||||||
} else if (fileSize >= 1024 * 1024) {
|
} else if (fileSize >= 1024 * 1024) {
|
||||||
return String.format(Locale.US, "%.2f MB", fileSize / (1024.0 * 1024));
|
return String.format(Locale.ROOT, "%.2f MB", fileSize / (1024.0 * 1024));
|
||||||
} else if (fileSize >= 1024) {
|
} else if (fileSize >= 1024) {
|
||||||
return String.format(Locale.US, "%.2f KB", fileSize / 1024.0);
|
return String.format(Locale.ROOT, "%.2f KB", fileSize / 1024.0);
|
||||||
} else {
|
} else {
|
||||||
return String.format("%d Bytes", fileSize);
|
return String.format("%d Bytes", fileSize);
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-7
@@ -7,6 +7,7 @@ import java.io.InputStream;
|
|||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.StandardCopyOption;
|
import java.nio.file.StandardCopyOption;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
|
||||||
import org.apache.pdfbox.Loader;
|
import org.apache.pdfbox.Loader;
|
||||||
@@ -249,7 +250,7 @@ public class CustomPDFDocumentFactory {
|
|||||||
log.debug(
|
log.debug(
|
||||||
"Memory status - Free: {}MB ({}%), Used: {}MB, Max: {}MB",
|
"Memory status - Free: {}MB ({}%), Used: {}MB, Max: {}MB",
|
||||||
actualFreeMemory / (1024 * 1024),
|
actualFreeMemory / (1024 * 1024),
|
||||||
String.format("%.2f", freeMemoryPercent),
|
String.format(Locale.ROOT, "%.2f", freeMemoryPercent),
|
||||||
usedMemory / (1024 * 1024),
|
usedMemory / (1024 * 1024),
|
||||||
maxMemory / (1024 * 1024));
|
maxMemory / (1024 * 1024));
|
||||||
|
|
||||||
@@ -258,7 +259,7 @@ public class CustomPDFDocumentFactory {
|
|||||||
|| actualFreeMemory < MIN_FREE_MEMORY_BYTES) {
|
|| actualFreeMemory < MIN_FREE_MEMORY_BYTES) {
|
||||||
log.debug(
|
log.debug(
|
||||||
"Low memory detected ({}%), forcing file-based cache",
|
"Low memory detected ({}%), forcing file-based cache",
|
||||||
String.format("%.2f", freeMemoryPercent));
|
String.format(Locale.ROOT, "%.2f", freeMemoryPercent));
|
||||||
return createScratchFileCacheFunction(MemoryUsageSetting.setupTempFileOnly());
|
return createScratchFileCacheFunction(MemoryUsageSetting.setupTempFileOnly());
|
||||||
} else if (contentSize < SMALL_FILE_THRESHOLD) {
|
} else if (contentSize < SMALL_FILE_THRESHOLD) {
|
||||||
log.debug("Using memory-only cache for small document ({}KB)", contentSize / 1024);
|
log.debug("Using memory-only cache for small document ({}KB)", contentSize / 1024);
|
||||||
@@ -477,11 +478,6 @@ public class CustomPDFDocumentFactory {
|
|||||||
return file;
|
return file;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Create a uniquely named temporary directory */
|
|
||||||
private Path createTempDirectory(String prefix) throws IOException {
|
|
||||||
return Files.createTempDirectory(prefix + tempCounter.incrementAndGet() + "-");
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Create new document bytes based on an existing document */
|
/** Create new document bytes based on an existing document */
|
||||||
public byte[] createNewBytesBasedOnOldDocument(byte[] oldDocument) throws IOException {
|
public byte[] createNewBytesBasedOnOldDocument(byte[] oldDocument) throws IOException {
|
||||||
try (PDDocument document = load(oldDocument)) {
|
try (PDDocument document = load(oldDocument)) {
|
||||||
|
|||||||
@@ -253,6 +253,22 @@ public class JobExecutorService {
|
|||||||
log.error("Synchronous job timed out after {} ms", timeoutToUse);
|
log.error("Synchronous job timed out after {} ms", timeoutToUse);
|
||||||
return ResponseEntity.internalServerError()
|
return ResponseEntity.internalServerError()
|
||||||
.body(Map.of("error", "Job timed out after " + timeoutToUse + " ms"));
|
.body(Map.of("error", "Job timed out after " + timeoutToUse + " ms"));
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
// Check if this is a wrapped typed exception that should be handled by
|
||||||
|
// GlobalExceptionHandler
|
||||||
|
Throwable cause = e.getCause();
|
||||||
|
if (cause instanceof stirling.software.common.util.ExceptionUtils.BaseAppException
|
||||||
|
|| cause
|
||||||
|
instanceof
|
||||||
|
stirling.software.common.util.ExceptionUtils
|
||||||
|
.BaseValidationException) {
|
||||||
|
// Rethrow so GlobalExceptionHandler can handle with proper HTTP status codes
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
// Handle other RuntimeExceptions as generic errors
|
||||||
|
log.error("Error executing synchronous job: {}", e.getMessage(), e);
|
||||||
|
return ResponseEntity.internalServerError()
|
||||||
|
.body(Map.of("error", "Job failed: " + e.getMessage()));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Error executing synchronous job: {}", e.getMessage(), e);
|
log.error("Error executing synchronous job: {}", e.getMessage(), e);
|
||||||
// Construct a JSON error response
|
// Construct a JSON error response
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ public class PostHogService {
|
|||||||
addIfNotEmpty(
|
addIfNotEmpty(
|
||||||
properties,
|
properties,
|
||||||
"security_enableLogin",
|
"security_enableLogin",
|
||||||
applicationProperties.getSecurity().getEnableLogin());
|
applicationProperties.getSecurity().isEnableLogin());
|
||||||
addIfNotEmpty(properties, "security_csrfDisabled", true);
|
addIfNotEmpty(properties, "security_csrfDisabled", true);
|
||||||
addIfNotEmpty(
|
addIfNotEmpty(
|
||||||
properties,
|
properties,
|
||||||
@@ -299,13 +299,13 @@ public class PostHogService {
|
|||||||
addIfNotEmpty(
|
addIfNotEmpty(
|
||||||
properties,
|
properties,
|
||||||
"system_googlevisibility",
|
"system_googlevisibility",
|
||||||
applicationProperties.getSystem().getGooglevisibility());
|
applicationProperties.getSystem().isGooglevisibility());
|
||||||
addIfNotEmpty(
|
addIfNotEmpty(
|
||||||
properties, "system_showUpdate", applicationProperties.getSystem().isShowUpdate());
|
properties, "system_showUpdate", applicationProperties.getSystem().isShowUpdate());
|
||||||
addIfNotEmpty(
|
addIfNotEmpty(
|
||||||
properties,
|
properties,
|
||||||
"system_showUpdateOnlyAdmin",
|
"system_showUpdateOnlyAdmin",
|
||||||
applicationProperties.getSystem().getShowUpdateOnlyAdmin());
|
applicationProperties.getSystem().isShowUpdateOnlyAdmin());
|
||||||
addIfNotEmpty(
|
addIfNotEmpty(
|
||||||
properties,
|
properties,
|
||||||
"system_customHTMLFiles",
|
"system_customHTMLFiles",
|
||||||
@@ -317,7 +317,7 @@ public class PostHogService {
|
|||||||
addIfNotEmpty(
|
addIfNotEmpty(
|
||||||
properties,
|
properties,
|
||||||
"system_enableAlphaFunctionality",
|
"system_enableAlphaFunctionality",
|
||||||
applicationProperties.getSystem().getEnableAlphaFunctionality());
|
applicationProperties.getSystem().isEnableAlphaFunctionality());
|
||||||
addIfNotEmpty(
|
addIfNotEmpty(
|
||||||
properties,
|
properties,
|
||||||
"system_enableAnalytics",
|
"system_enableAnalytics",
|
||||||
@@ -337,7 +337,7 @@ public class PostHogService {
|
|||||||
|
|
||||||
// Capture Metrics properties
|
// Capture Metrics properties
|
||||||
addIfNotEmpty(
|
addIfNotEmpty(
|
||||||
properties, "metrics_enabled", applicationProperties.getMetrics().getEnabled());
|
properties, "metrics_enabled", applicationProperties.getMetrics().isEnabled());
|
||||||
|
|
||||||
// Capture EnterpriseEdition properties
|
// Capture EnterpriseEdition properties
|
||||||
addIfNotEmpty(
|
addIfNotEmpty(
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import java.lang.management.MemoryMXBean;
|
|||||||
import java.lang.management.OperatingSystemMXBean;
|
import java.lang.management.OperatingSystemMXBean;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
@@ -173,8 +174,8 @@ public class ResourceMonitor {
|
|||||||
log.info("System resource status changed from {} to {}", oldStatus, newStatus);
|
log.info("System resource status changed from {} to {}", oldStatus, newStatus);
|
||||||
log.info(
|
log.info(
|
||||||
"Current metrics - CPU: {}%, Memory: {}%, Free Memory: {} MB",
|
"Current metrics - CPU: {}%, Memory: {}%, Free Memory: {} MB",
|
||||||
String.format("%.1f", cpuUsage * 100),
|
String.format(Locale.ROOT, "%.1f", cpuUsage * 100),
|
||||||
String.format("%.1f", memoryUsage * 100),
|
String.format(Locale.ROOT, "%.1f", memoryUsage * 100),
|
||||||
freeMemory / (1024 * 1024));
|
freeMemory / (1024 * 1024));
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|||||||
+8
-5
@@ -5,6 +5,7 @@ import java.net.Inet6Address;
|
|||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.UnknownHostException;
|
import java.net.UnknownHostException;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -83,7 +84,7 @@ public class SsrfProtectionService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return config.getAllowedDomains().contains(host.toLowerCase());
|
return config.getAllowedDomains().contains(host.toLowerCase(Locale.ROOT));
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.debug("Failed to parse URL for MAX security check: {}", url, e);
|
log.debug("Failed to parse URL for MAX security check: {}", url, e);
|
||||||
@@ -101,7 +102,7 @@ public class SsrfProtectionService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
String hostLower = host.toLowerCase();
|
String hostLower = host.toLowerCase(Locale.ROOT);
|
||||||
|
|
||||||
// Check explicit blocked domains
|
// Check explicit blocked domains
|
||||||
if (config.getBlockedDomains().contains(hostLower)) {
|
if (config.getBlockedDomains().contains(hostLower)) {
|
||||||
@@ -111,7 +112,7 @@ public class SsrfProtectionService {
|
|||||||
|
|
||||||
// Check internal TLD patterns
|
// Check internal TLD patterns
|
||||||
for (String tld : config.getInternalTlds()) {
|
for (String tld : config.getInternalTlds()) {
|
||||||
if (hostLower.endsWith(tld.toLowerCase())) {
|
if (hostLower.endsWith(tld.toLowerCase(Locale.ROOT))) {
|
||||||
log.debug("URL blocked by internal TLD pattern '{}': {}", tld, url);
|
log.debug("URL blocked by internal TLD pattern '{}': {}", tld, url);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -123,9 +124,11 @@ public class SsrfProtectionService {
|
|||||||
config.getAllowedDomains().stream()
|
config.getAllowedDomains().stream()
|
||||||
.anyMatch(
|
.anyMatch(
|
||||||
domain ->
|
domain ->
|
||||||
hostLower.equals(domain.toLowerCase())
|
hostLower.equals(domain.toLowerCase(Locale.ROOT))
|
||||||
|| hostLower.endsWith(
|
|| hostLower.endsWith(
|
||||||
"." + domain.toLowerCase()));
|
"."
|
||||||
|
+ domain.toLowerCase(
|
||||||
|
Locale.ROOT)));
|
||||||
|
|
||||||
if (!isAllowed) {
|
if (!isAllowed) {
|
||||||
log.debug("URL not in allowed domains list: {}", url);
|
log.debug("URL not in allowed domains list: {}", url);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import java.time.LocalDateTime;
|
|||||||
import java.time.temporal.ChronoUnit;
|
import java.time.temporal.ChronoUnit;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
@@ -101,14 +102,16 @@ public class TaskManager {
|
|||||||
if (!extractedFiles.isEmpty()) {
|
if (!extractedFiles.isEmpty()) {
|
||||||
jobResult.completeWithFiles(extractedFiles);
|
jobResult.completeWithFiles(extractedFiles);
|
||||||
log.debug(
|
log.debug(
|
||||||
"Set multiple file results for job ID: {} with {} files extracted from ZIP",
|
"Set multiple file results for job ID: {} with {} files extracted from"
|
||||||
|
+ " ZIP",
|
||||||
jobId,
|
jobId,
|
||||||
extractedFiles.size());
|
extractedFiles.size());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn(
|
log.warn(
|
||||||
"Failed to extract ZIP file for job {}: {}. Falling back to single file result.",
|
"Failed to extract ZIP file for job {}: {}. Falling back to single file"
|
||||||
|
+ " result.",
|
||||||
jobId,
|
jobId,
|
||||||
e.getMessage());
|
e.getMessage());
|
||||||
}
|
}
|
||||||
@@ -342,12 +345,12 @@ public class TaskManager {
|
|||||||
/** Check if a file is a ZIP file based on content type and filename */
|
/** Check if a file is a ZIP file based on content type and filename */
|
||||||
private boolean isZipFile(String contentType, String fileName) {
|
private boolean isZipFile(String contentType, String fileName) {
|
||||||
if (contentType != null
|
if (contentType != null
|
||||||
&& (contentType.equals("application/zip")
|
&& ("application/zip".equals(contentType)
|
||||||
|| contentType.equals("application/x-zip-compressed"))) {
|
|| "application/x-zip-compressed".equals(contentType))) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fileName != null && fileName.toLowerCase().endsWith(".zip")) {
|
if (fileName != null && fileName.toLowerCase(Locale.ROOT).endsWith(".zip")) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,7 +417,7 @@ public class TaskManager {
|
|||||||
return MediaType.APPLICATION_OCTET_STREAM_VALUE;
|
return MediaType.APPLICATION_OCTET_STREAM_VALUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
String lowerName = fileName.toLowerCase();
|
String lowerName = fileName.toLowerCase(Locale.ROOT);
|
||||||
if (lowerName.endsWith(".pdf")) {
|
if (lowerName.endsWith(".pdf")) {
|
||||||
return MediaType.APPLICATION_PDF_VALUE;
|
return MediaType.APPLICATION_PDF_VALUE;
|
||||||
} else if (lowerName.endsWith(".txt")) {
|
} else if (lowerName.endsWith(".txt")) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import java.io.InputStream;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
import org.apache.commons.io.FilenameUtils;
|
import org.apache.commons.io.FilenameUtils;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
@@ -58,31 +59,24 @@ public class CbrUtils {
|
|||||||
log.warn(
|
log.warn(
|
||||||
"Failed to open CBR/RAR archive due to corrupt header: {}",
|
"Failed to open CBR/RAR archive due to corrupt header: {}",
|
||||||
e.getMessage());
|
e.getMessage());
|
||||||
throw ExceptionUtils.createIllegalArgumentException(
|
throw ExceptionUtils.createCbrInvalidFormatException(null);
|
||||||
"error.invalidFormat",
|
|
||||||
"Invalid or corrupted CBR/RAR archive. "
|
|
||||||
+ "The file may be corrupted, use an unsupported RAR format (RAR5+), "
|
|
||||||
+ "or may not be a valid RAR archive. "
|
|
||||||
+ "Please ensure the file is a valid RAR archive.");
|
|
||||||
} catch (RarException e) {
|
} catch (RarException e) {
|
||||||
log.warn("Failed to open CBR/RAR archive: {}", e.getMessage());
|
log.warn("Failed to open CBR/RAR archive: {}", e.getMessage());
|
||||||
String errorMessage;
|
|
||||||
String exMessage = e.getMessage() != null ? e.getMessage() : "";
|
String exMessage = e.getMessage() != null ? e.getMessage() : "";
|
||||||
|
|
||||||
if (exMessage.contains("encrypted")) {
|
if (exMessage.contains("encrypted")) {
|
||||||
errorMessage = "Encrypted CBR/RAR archives are not supported.";
|
throw ExceptionUtils.createCbrEncryptedException();
|
||||||
} else if (exMessage.isEmpty()) {
|
} else if (exMessage.isEmpty()) {
|
||||||
errorMessage =
|
throw ExceptionUtils.createCbrInvalidFormatException(
|
||||||
"Invalid CBR/RAR archive. "
|
"Invalid CBR/RAR archive. The file may be encrypted, corrupted, or"
|
||||||
+ "The file may be encrypted, corrupted, or use an unsupported format.";
|
+ " use an unsupported format.");
|
||||||
} else {
|
} else {
|
||||||
errorMessage =
|
throw ExceptionUtils.createCbrInvalidFormatException(
|
||||||
"Invalid CBR/RAR archive: "
|
"Invalid CBR/RAR archive: "
|
||||||
+ exMessage
|
+ exMessage
|
||||||
+ ". The file may be encrypted, corrupted, or use an unsupported format.";
|
+ ". The file may be encrypted, corrupted, or use an"
|
||||||
|
+ " unsupported format.");
|
||||||
}
|
}
|
||||||
throw ExceptionUtils.createIllegalArgumentException(
|
|
||||||
"error.invalidFormat", errorMessage);
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
log.warn("IO error reading CBR/RAR archive: {}", e.getMessage());
|
log.warn("IO error reading CBR/RAR archive: {}", e.getMessage());
|
||||||
throw ExceptionUtils.createFileProcessingException("CBR extraction", e);
|
throw ExceptionUtils.createFileProcessingException("CBR extraction", e);
|
||||||
@@ -121,7 +115,8 @@ public class CbrUtils {
|
|||||||
if (imageEntries.isEmpty()) {
|
if (imageEntries.isEmpty()) {
|
||||||
throw ExceptionUtils.createIllegalArgumentException(
|
throw ExceptionUtils.createIllegalArgumentException(
|
||||||
"error.fileProcessing",
|
"error.fileProcessing",
|
||||||
"No valid images found in the CBR file. The archive may be empty or contain no supported image formats.");
|
"No valid images found in the CBR file. The archive may be empty or"
|
||||||
|
+ " contain no supported image formats.");
|
||||||
}
|
}
|
||||||
|
|
||||||
for (ImageEntryData imageEntry : imageEntries) {
|
for (ImageEntryData imageEntry : imageEntries) {
|
||||||
@@ -146,7 +141,8 @@ public class CbrUtils {
|
|||||||
if (document.getNumberOfPages() == 0) {
|
if (document.getNumberOfPages() == 0) {
|
||||||
throw ExceptionUtils.createIllegalArgumentException(
|
throw ExceptionUtils.createIllegalArgumentException(
|
||||||
"error.fileProcessing",
|
"error.fileProcessing",
|
||||||
"No images could be processed from the CBR file. All images may be corrupted or in unsupported formats.");
|
"No images could be processed from the CBR file. All images may be"
|
||||||
|
+ " corrupted or in unsupported formats.");
|
||||||
}
|
}
|
||||||
|
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
@@ -159,7 +155,6 @@ public class CbrUtils {
|
|||||||
return GeneralUtils.optimizePdfWithGhostscript(pdfBytes);
|
return GeneralUtils.optimizePdfWithGhostscript(pdfBytes);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
log.warn("Ghostscript optimization failed, returning unoptimized PDF", e);
|
log.warn("Ghostscript optimization failed, returning unoptimized PDF", e);
|
||||||
return pdfBytes;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,17 +165,17 @@ public class CbrUtils {
|
|||||||
|
|
||||||
private void validateCbrFile(MultipartFile file) {
|
private void validateCbrFile(MultipartFile file) {
|
||||||
if (file == null || file.isEmpty()) {
|
if (file == null || file.isEmpty()) {
|
||||||
throw new IllegalArgumentException("File cannot be null or empty");
|
throw ExceptionUtils.createFileNullOrEmptyException();
|
||||||
}
|
}
|
||||||
|
|
||||||
String filename = file.getOriginalFilename();
|
String filename = file.getOriginalFilename();
|
||||||
if (filename == null) {
|
if (filename == null) {
|
||||||
throw new IllegalArgumentException("File must have a name");
|
throw ExceptionUtils.createFileNoNameException();
|
||||||
}
|
}
|
||||||
|
|
||||||
String extension = FilenameUtils.getExtension(filename).toLowerCase();
|
String extension = FilenameUtils.getExtension(filename).toLowerCase(Locale.ROOT);
|
||||||
if (!"cbr".equals(extension) && !"rar".equals(extension)) {
|
if (!"cbr".equals(extension) && !"rar".equals(extension)) {
|
||||||
throw new IllegalArgumentException("File must be a CBR or RAR archive");
|
throw ExceptionUtils.createNotCbrFileException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,7 +185,7 @@ public class CbrUtils {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
String extension = FilenameUtils.getExtension(filename).toLowerCase();
|
String extension = FilenameUtils.getExtension(filename).toLowerCase(Locale.ROOT);
|
||||||
return "cbr".equals(extension) || "rar".equals(extension);
|
return "cbr".equals(extension) || "rar".equals(extension);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import java.util.ArrayList;
|
|||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.Enumeration;
|
import java.util.Enumeration;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipFile;
|
import java.util.zip.ZipFile;
|
||||||
import java.util.zip.ZipInputStream;
|
import java.util.zip.ZipInputStream;
|
||||||
@@ -55,10 +56,10 @@ public class CbzUtils {
|
|||||||
new java.io.FileInputStream(tempFile.getFile()));
|
new java.io.FileInputStream(tempFile.getFile()));
|
||||||
ZipInputStream zis = new ZipInputStream(bis)) {
|
ZipInputStream zis = new ZipInputStream(bis)) {
|
||||||
if (zis.getNextEntry() == null) {
|
if (zis.getNextEntry() == null) {
|
||||||
throw new IllegalArgumentException("Archive is empty or invalid ZIP");
|
throw ExceptionUtils.createCbzEmptyException();
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new IllegalArgumentException("Invalid CBZ/ZIP archive", e);
|
throw ExceptionUtils.createCbzInvalidFormatException(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
try (PDDocument document = pdfDocumentFactory.createNewDocument();
|
try (PDDocument document = pdfDocumentFactory.createNewDocument();
|
||||||
@@ -83,7 +84,7 @@ public class CbzUtils {
|
|||||||
Comparator.comparing(ImageEntryData::name, new NaturalOrderComparator()));
|
Comparator.comparing(ImageEntryData::name, new NaturalOrderComparator()));
|
||||||
|
|
||||||
if (imageEntries.isEmpty()) {
|
if (imageEntries.isEmpty()) {
|
||||||
throw new IllegalArgumentException("No valid images found in the CBZ file");
|
throw ExceptionUtils.createCbzNoImagesException();
|
||||||
}
|
}
|
||||||
|
|
||||||
for (ImageEntryData imageEntry : imageEntries) {
|
for (ImageEntryData imageEntry : imageEntries) {
|
||||||
@@ -106,8 +107,7 @@ public class CbzUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (document.getNumberOfPages() == 0) {
|
if (document.getNumberOfPages() == 0) {
|
||||||
throw new IllegalArgumentException(
|
throw ExceptionUtils.createCbzCorruptedImagesException();
|
||||||
"No images could be processed from the CBZ file");
|
|
||||||
}
|
}
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
document.save(baos);
|
document.save(baos);
|
||||||
@@ -119,7 +119,6 @@ public class CbzUtils {
|
|||||||
return GeneralUtils.optimizePdfWithGhostscript(pdfBytes);
|
return GeneralUtils.optimizePdfWithGhostscript(pdfBytes);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
log.warn("Ghostscript optimization failed, returning unoptimized PDF", e);
|
log.warn("Ghostscript optimization failed, returning unoptimized PDF", e);
|
||||||
return pdfBytes;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,17 +129,17 @@ public class CbzUtils {
|
|||||||
|
|
||||||
private void validateCbzFile(MultipartFile file) {
|
private void validateCbzFile(MultipartFile file) {
|
||||||
if (file == null || file.isEmpty()) {
|
if (file == null || file.isEmpty()) {
|
||||||
throw new IllegalArgumentException("File cannot be null or empty");
|
throw ExceptionUtils.createFileNullOrEmptyException();
|
||||||
}
|
}
|
||||||
|
|
||||||
String filename = file.getOriginalFilename();
|
String filename = file.getOriginalFilename();
|
||||||
if (filename == null) {
|
if (filename == null) {
|
||||||
throw new IllegalArgumentException("File must have a name");
|
throw ExceptionUtils.createFileNoNameException();
|
||||||
}
|
}
|
||||||
|
|
||||||
String extension = FilenameUtils.getExtension(filename).toLowerCase();
|
String extension = FilenameUtils.getExtension(filename).toLowerCase(Locale.ROOT);
|
||||||
if (!"cbz".equals(extension) && !"zip".equals(extension)) {
|
if (!"cbz".equals(extension) && !"zip".equals(extension)) {
|
||||||
throw new IllegalArgumentException("File must be a CBZ or ZIP archive");
|
throw ExceptionUtils.createNotCbzFileException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,7 +149,7 @@ public class CbzUtils {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
String extension = FilenameUtils.getExtension(filename).toLowerCase();
|
String extension = FilenameUtils.getExtension(filename).toLowerCase(Locale.ROOT);
|
||||||
return "cbz".equals(extension) || "zip".equals(extension);
|
return "cbz".equals(extension) || "zip".equals(extension);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,7 +159,7 @@ public class CbzUtils {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
String extension = FilenameUtils.getExtension(filename).toLowerCase();
|
String extension = FilenameUtils.getExtension(filename).toLowerCase(Locale.ROOT);
|
||||||
return "cbz".equals(extension)
|
return "cbz".equals(extension)
|
||||||
|| "zip".equals(extension)
|
|| "zip".equals(extension)
|
||||||
|| "cbr".equals(extension)
|
|| "cbr".equals(extension)
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ public class CheckProgramInstall {
|
|||||||
private static final List<String> PYTHON_COMMANDS = Arrays.asList("python3", "python");
|
private static final List<String> PYTHON_COMMANDS = Arrays.asList("python3", "python");
|
||||||
private static boolean pythonAvailableChecked = false;
|
private static boolean pythonAvailableChecked = false;
|
||||||
private static String availablePythonCommand = null;
|
private static String availablePythonCommand = null;
|
||||||
|
private static boolean ffmpegAvailableChecked = false;
|
||||||
|
private static boolean ffmpegAvailable = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks which Python command is available and returns it.
|
* Checks which Python command is available and returns it.
|
||||||
@@ -56,4 +58,25 @@ public class CheckProgramInstall {
|
|||||||
public static boolean isPythonAvailable() {
|
public static boolean isPythonAvailable() {
|
||||||
return getAvailablePythonCommand() != null;
|
return getAvailablePythonCommand() != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if FFmpeg is available on the system.
|
||||||
|
*
|
||||||
|
* @return true if FFmpeg is installed and accessible, false otherwise.
|
||||||
|
*/
|
||||||
|
public static boolean isFfmpegAvailable() {
|
||||||
|
if (!ffmpegAvailableChecked) {
|
||||||
|
try {
|
||||||
|
ProcessExecutorResult result =
|
||||||
|
ProcessExecutor.getInstance(ProcessExecutor.Processes.FFMPEG)
|
||||||
|
.runCommandWithOutputHandling(Arrays.asList("ffmpeg", "-version"));
|
||||||
|
ffmpegAvailable = true;
|
||||||
|
} catch (IOException | InterruptedException e) {
|
||||||
|
ffmpegAvailable = false;
|
||||||
|
} finally {
|
||||||
|
ffmpegAvailableChecked = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ffmpegAvailable;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,14 +58,11 @@ public class ChecksumUtils {
|
|||||||
* @throws IOException if reading from the stream fails
|
* @throws IOException if reading from the stream fails
|
||||||
*/
|
*/
|
||||||
public static String checksum(InputStream is, String algorithm) throws IOException {
|
public static String checksum(InputStream is, String algorithm) throws IOException {
|
||||||
switch (algorithm.toUpperCase(Locale.ROOT)) {
|
return switch (algorithm.toUpperCase(Locale.ROOT)) {
|
||||||
case "CRC32":
|
case "CRC32" -> checksumChecksum(is, new CRC32());
|
||||||
return checksumChecksum(is, new CRC32());
|
case "ADLER32" -> checksumChecksum(is, new Adler32());
|
||||||
case "ADLER32":
|
default -> toHex(checksumBytes(is, algorithm));
|
||||||
return checksumChecksum(is, new Adler32());
|
};
|
||||||
default:
|
|
||||||
return toHex(checksumBytes(is, algorithm));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -98,14 +95,13 @@ public class ChecksumUtils {
|
|||||||
* @throws IOException if reading from the stream fails
|
* @throws IOException if reading from the stream fails
|
||||||
*/
|
*/
|
||||||
public static String checksumBase64(InputStream is, String algorithm) throws IOException {
|
public static String checksumBase64(InputStream is, String algorithm) throws IOException {
|
||||||
switch (algorithm.toUpperCase(Locale.ROOT)) {
|
return switch (algorithm.toUpperCase(Locale.ROOT)) {
|
||||||
case "CRC32":
|
case "CRC32" ->
|
||||||
return Base64.getEncoder().encodeToString(checksumChecksumBytes(is, new CRC32()));
|
Base64.getEncoder().encodeToString(checksumChecksumBytes(is, new CRC32()));
|
||||||
case "ADLER32":
|
case "ADLER32" ->
|
||||||
return Base64.getEncoder().encodeToString(checksumChecksumBytes(is, new Adler32()));
|
Base64.getEncoder().encodeToString(checksumChecksumBytes(is, new Adler32()));
|
||||||
default:
|
default -> Base64.getEncoder().encodeToString(checksumBytes(is, algorithm));
|
||||||
return Base64.getEncoder().encodeToString(checksumBytes(is, algorithm));
|
};
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -179,7 +175,7 @@ public class ChecksumUtils {
|
|||||||
for (Map.Entry<String, Checksum> entry : checksums.entrySet()) {
|
for (Map.Entry<String, Checksum> entry : checksums.entrySet()) {
|
||||||
// Keep value as long and mask to ensure unsigned hex formatting.
|
// Keep value as long and mask to ensure unsigned hex formatting.
|
||||||
long unsigned32 = entry.getValue().getValue() & UNSIGNED_32_BIT_MASK;
|
long unsigned32 = entry.getValue().getValue() & UNSIGNED_32_BIT_MASK;
|
||||||
results.put(entry.getKey(), String.format("%08x", unsigned32));
|
results.put(entry.getKey(), String.format(Locale.ROOT, "%08x", unsigned32));
|
||||||
}
|
}
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
@@ -258,7 +254,7 @@ public class ChecksumUtils {
|
|||||||
}
|
}
|
||||||
// Keep as long and mask to ensure correct unsigned representation.
|
// Keep as long and mask to ensure correct unsigned representation.
|
||||||
long unsigned32 = checksum.getValue() & UNSIGNED_32_BIT_MASK;
|
long unsigned32 = checksum.getValue() & UNSIGNED_32_BIT_MASK;
|
||||||
return String.format("%08x", unsigned32);
|
return String.format(Locale.ROOT, "%08x", unsigned32);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -294,7 +290,7 @@ public class ChecksumUtils {
|
|||||||
private static String toHex(byte[] hash) {
|
private static String toHex(byte[] hash) {
|
||||||
StringBuilder sb = new StringBuilder(hash.length * 2);
|
StringBuilder sb = new StringBuilder(hash.length * 2);
|
||||||
for (byte b : hash) {
|
for (byte b : hash) {
|
||||||
sb.append(String.format("%02x", b));
|
sb.append(String.format(Locale.ROOT, "%02x", b));
|
||||||
}
|
}
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,8 +62,7 @@ public class CustomHtmlSanitizer {
|
|||||||
.and(new HtmlPolicyBuilder().disallowElements("noscript").toFactory());
|
.and(new HtmlPolicyBuilder().disallowElements("noscript").toFactory());
|
||||||
|
|
||||||
public String sanitize(String html) {
|
public String sanitize(String html) {
|
||||||
boolean disableSanitize =
|
boolean disableSanitize = applicationProperties.getSystem().isDisableSanitize();
|
||||||
Boolean.TRUE.equals(applicationProperties.getSystem().getDisableSanitize());
|
|
||||||
return disableSanitize ? html : POLICY.sanitize(html);
|
return disableSanitize ? html : POLICY.sanitize(html);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import java.time.ZonedDateTime;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.Properties;
|
import java.util.Properties;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
@@ -229,7 +230,8 @@ public class EmlParser {
|
|||||||
Method getContentType = message.getClass().getMethod("getContentType");
|
Method getContentType = message.getClass().getMethod("getContentType");
|
||||||
String contentType = (String) getContentType.invoke(message);
|
String contentType = (String) getContentType.invoke(message);
|
||||||
|
|
||||||
if (contentType != null && contentType.toLowerCase().contains(TEXT_HTML)) {
|
if (contentType != null
|
||||||
|
&& contentType.toLowerCase(Locale.ROOT).contains(TEXT_HTML)) {
|
||||||
content.setHtmlBody(stringContent);
|
content.setHtmlBody(stringContent);
|
||||||
} else {
|
} else {
|
||||||
content.setTextBody(stringContent);
|
content.setTextBody(stringContent);
|
||||||
@@ -296,7 +298,7 @@ public class EmlParser {
|
|||||||
String contentType = (String) getContentType.invoke(part);
|
String contentType = (String) getContentType.invoke(part);
|
||||||
|
|
||||||
String normalizedDisposition =
|
String normalizedDisposition =
|
||||||
disposition != null ? ((String) disposition).toLowerCase() : null;
|
disposition != null ? ((String) disposition).toLowerCase(Locale.ROOT) : null;
|
||||||
|
|
||||||
if ((Boolean) isMimeType.invoke(part, TEXT_PLAIN) && normalizedDisposition == null) {
|
if ((Boolean) isMimeType.invoke(part, TEXT_PLAIN) && normalizedDisposition == null) {
|
||||||
Object partContent = getContent.invoke(part);
|
Object partContent = getContent.invoke(part);
|
||||||
@@ -422,7 +424,7 @@ public class EmlParser {
|
|||||||
RegexPatternUtils.getInstance().getNewlineSplitPattern().split(emlContent);
|
RegexPatternUtils.getInstance().getNewlineSplitPattern().split(emlContent);
|
||||||
for (int i = 0; i < lines.length; i++) {
|
for (int i = 0; i < lines.length; i++) {
|
||||||
String line = lines[i];
|
String line = lines[i];
|
||||||
if (line.toLowerCase().startsWith(headerName.toLowerCase())) {
|
if (line.toLowerCase(Locale.ROOT).startsWith(headerName.toLowerCase(Locale.ROOT))) {
|
||||||
StringBuilder value =
|
StringBuilder value =
|
||||||
new StringBuilder(line.substring(headerName.length()).trim());
|
new StringBuilder(line.substring(headerName.length()).trim());
|
||||||
for (int j = i + 1; j < lines.length; j++) {
|
for (int j = i + 1; j < lines.length; j++) {
|
||||||
@@ -444,7 +446,7 @@ public class EmlParser {
|
|||||||
|
|
||||||
private static String extractHtmlBody(String emlContent) {
|
private static String extractHtmlBody(String emlContent) {
|
||||||
try {
|
try {
|
||||||
String lowerContent = emlContent.toLowerCase();
|
String lowerContent = emlContent.toLowerCase(Locale.ROOT);
|
||||||
int htmlStart = lowerContent.indexOf(HEADER_CONTENT_TYPE + " " + TEXT_HTML);
|
int htmlStart = lowerContent.indexOf(HEADER_CONTENT_TYPE + " " + TEXT_HTML);
|
||||||
if (htmlStart == -1) return null;
|
if (htmlStart == -1) return null;
|
||||||
|
|
||||||
@@ -463,7 +465,7 @@ public class EmlParser {
|
|||||||
|
|
||||||
private static String extractTextBody(String emlContent) {
|
private static String extractTextBody(String emlContent) {
|
||||||
try {
|
try {
|
||||||
String lowerContent = emlContent.toLowerCase();
|
String lowerContent = emlContent.toLowerCase(Locale.ROOT);
|
||||||
int textStart = lowerContent.indexOf(HEADER_CONTENT_TYPE + " " + TEXT_PLAIN);
|
int textStart = lowerContent.indexOf(HEADER_CONTENT_TYPE + " " + TEXT_PLAIN);
|
||||||
if (textStart == -1) {
|
if (textStart == -1) {
|
||||||
int bodyStart = emlContent.indexOf("\r\n\r\n");
|
int bodyStart = emlContent.indexOf("\r\n\r\n");
|
||||||
@@ -516,7 +518,7 @@ public class EmlParser {
|
|||||||
String currentEncoding = "";
|
String currentEncoding = "";
|
||||||
|
|
||||||
for (String line : lines) {
|
for (String line : lines) {
|
||||||
String lowerLine = line.toLowerCase().trim();
|
String lowerLine = line.toLowerCase(Locale.ROOT).trim();
|
||||||
|
|
||||||
if (line.trim().isEmpty()) {
|
if (line.trim().isEmpty()) {
|
||||||
inHeaders = false;
|
inHeaders = false;
|
||||||
@@ -554,9 +556,12 @@ public class EmlParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isAttachment(String disposition, String filename, String contentType) {
|
private static boolean isAttachment(String disposition, String filename, String contentType) {
|
||||||
return (disposition.toLowerCase().contains(DISPOSITION_ATTACHMENT) && !filename.isEmpty())
|
return (disposition.toLowerCase(Locale.ROOT).contains(DISPOSITION_ATTACHMENT)
|
||||||
|| (!filename.isEmpty() && !contentType.toLowerCase().startsWith("text/"))
|
&& !filename.isEmpty())
|
||||||
|| (contentType.toLowerCase().contains("application/") && !filename.isEmpty());
|
|| (!filename.isEmpty()
|
||||||
|
&& !contentType.toLowerCase(Locale.ROOT).startsWith("text/"))
|
||||||
|
|| (contentType.toLowerCase(Locale.ROOT).contains("application/")
|
||||||
|
&& !filename.isEmpty());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String extractFilenameFromDisposition(String disposition) {
|
private static String extractFilenameFromDisposition(String disposition) {
|
||||||
@@ -565,8 +570,8 @@ public class EmlParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle filename*= (RFC 2231 encoded filename)
|
// Handle filename*= (RFC 2231 encoded filename)
|
||||||
if (disposition.toLowerCase().contains("filename*=")) {
|
if (disposition.toLowerCase(Locale.ROOT).contains("filename*=")) {
|
||||||
int filenameStarStart = disposition.toLowerCase().indexOf("filename*=") + 10;
|
int filenameStarStart = disposition.toLowerCase(Locale.ROOT).indexOf("filename*=") + 10;
|
||||||
int filenameStarEnd = disposition.indexOf(";", filenameStarStart);
|
int filenameStarEnd = disposition.indexOf(";", filenameStarStart);
|
||||||
if (filenameStarEnd == -1) filenameStarEnd = disposition.length();
|
if (filenameStarEnd == -1) filenameStarEnd = disposition.length();
|
||||||
String extendedFilename =
|
String extendedFilename =
|
||||||
@@ -586,7 +591,7 @@ public class EmlParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle regular filename=
|
// Handle regular filename=
|
||||||
int filenameStart = disposition.toLowerCase().indexOf("filename=") + 9;
|
int filenameStart = disposition.toLowerCase(Locale.ROOT).indexOf("filename=") + 9;
|
||||||
int filenameEnd = disposition.indexOf(";", filenameStart);
|
int filenameEnd = disposition.indexOf(";", filenameStart);
|
||||||
if (filenameEnd == -1) filenameEnd = disposition.length();
|
if (filenameEnd == -1) filenameEnd = disposition.length();
|
||||||
String filename = disposition.substring(filenameStart, filenameEnd).trim();
|
String filename = disposition.substring(filenameStart, filenameEnd).trim();
|
||||||
|
|||||||
@@ -48,11 +48,11 @@ public class EmlProcessingUtils {
|
|||||||
|
|
||||||
public static void validateEmlInput(byte[] emlBytes) {
|
public static void validateEmlInput(byte[] emlBytes) {
|
||||||
if (emlBytes == null || emlBytes.length == 0) {
|
if (emlBytes == null || emlBytes.length == 0) {
|
||||||
throw new IllegalArgumentException("EML file is empty or null");
|
throw ExceptionUtils.createEmlEmptyException();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isInvalidEmlFormat(emlBytes)) {
|
if (isInvalidEmlFormat(emlBytes)) {
|
||||||
throw new IllegalArgumentException("Invalid EML file format");
|
throw ExceptionUtils.createEmlInvalidFormatException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,12 +109,13 @@ public class EmlProcessingUtils {
|
|||||||
|
|
||||||
html.append(
|
html.append(
|
||||||
String.format(
|
String.format(
|
||||||
|
Locale.ROOT,
|
||||||
"""
|
"""
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en"><head><meta charset="UTF-8">
|
<html lang="en"><head><meta charset="UTF-8">
|
||||||
<title>%s</title>
|
<title>%s</title>
|
||||||
<style>
|
<style>
|
||||||
""",
|
""",
|
||||||
sanitizeText(content.getSubject(), customHtmlSanitizer)));
|
sanitizeText(content.getSubject(), customHtmlSanitizer)));
|
||||||
|
|
||||||
appendEnhancedStyles(html);
|
appendEnhancedStyles(html);
|
||||||
@@ -127,14 +128,15 @@ public class EmlProcessingUtils {
|
|||||||
|
|
||||||
html.append(
|
html.append(
|
||||||
String.format(
|
String.format(
|
||||||
|
Locale.ROOT,
|
||||||
"""
|
"""
|
||||||
<div class="email-container">
|
<div class="email-container">
|
||||||
<div class="email-header">
|
<div class="email-header">
|
||||||
<h1>%s</h1>
|
<h1>%s</h1>
|
||||||
<div class="email-meta">
|
<div class="email-meta">
|
||||||
<div><strong>From:</strong> %s</div>
|
<div><strong>From:</strong> %s</div>
|
||||||
<div><strong>To:</strong> %s</div>
|
<div><strong>To:</strong> %s</div>
|
||||||
""",
|
""",
|
||||||
sanitizeText(content.getSubject(), customHtmlSanitizer),
|
sanitizeText(content.getSubject(), customHtmlSanitizer),
|
||||||
sanitizeText(content.getFrom(), customHtmlSanitizer),
|
sanitizeText(content.getFrom(), customHtmlSanitizer),
|
||||||
sanitizeText(content.getTo(), customHtmlSanitizer)));
|
sanitizeText(content.getTo(), customHtmlSanitizer)));
|
||||||
@@ -142,6 +144,7 @@ public class EmlProcessingUtils {
|
|||||||
if (content.getCc() != null && !content.getCc().trim().isEmpty()) {
|
if (content.getCc() != null && !content.getCc().trim().isEmpty()) {
|
||||||
html.append(
|
html.append(
|
||||||
String.format(
|
String.format(
|
||||||
|
Locale.ROOT,
|
||||||
"<div><strong>CC:</strong> %s</div>\n",
|
"<div><strong>CC:</strong> %s</div>\n",
|
||||||
sanitizeText(content.getCc(), customHtmlSanitizer)));
|
sanitizeText(content.getCc(), customHtmlSanitizer)));
|
||||||
}
|
}
|
||||||
@@ -149,6 +152,7 @@ public class EmlProcessingUtils {
|
|||||||
if (content.getBcc() != null && !content.getBcc().trim().isEmpty()) {
|
if (content.getBcc() != null && !content.getBcc().trim().isEmpty()) {
|
||||||
html.append(
|
html.append(
|
||||||
String.format(
|
String.format(
|
||||||
|
Locale.ROOT,
|
||||||
"<div><strong>BCC:</strong> %s</div>\n",
|
"<div><strong>BCC:</strong> %s</div>\n",
|
||||||
sanitizeText(content.getBcc(), customHtmlSanitizer)));
|
sanitizeText(content.getBcc(), customHtmlSanitizer)));
|
||||||
}
|
}
|
||||||
@@ -156,11 +160,13 @@ public class EmlProcessingUtils {
|
|||||||
if (content.getDate() != null) {
|
if (content.getDate() != null) {
|
||||||
html.append(
|
html.append(
|
||||||
String.format(
|
String.format(
|
||||||
|
Locale.ROOT,
|
||||||
"<div><strong>Date:</strong> %s</div>\n",
|
"<div><strong>Date:</strong> %s</div>\n",
|
||||||
PdfAttachmentHandler.formatEmailDate(content.getDate())));
|
PdfAttachmentHandler.formatEmailDate(content.getDate())));
|
||||||
} else if (content.getDateString() != null && !content.getDateString().trim().isEmpty()) {
|
} else if (content.getDateString() != null && !content.getDateString().trim().isEmpty()) {
|
||||||
html.append(
|
html.append(
|
||||||
String.format(
|
String.format(
|
||||||
|
Locale.ROOT,
|
||||||
"<div><strong>Date:</strong> %s</div>\n",
|
"<div><strong>Date:</strong> %s</div>\n",
|
||||||
sanitizeText(content.getDateString(), customHtmlSanitizer)));
|
sanitizeText(content.getDateString(), customHtmlSanitizer)));
|
||||||
}
|
}
|
||||||
@@ -175,6 +181,7 @@ public class EmlProcessingUtils {
|
|||||||
} else if (content.getTextBody() != null && !content.getTextBody().trim().isEmpty()) {
|
} else if (content.getTextBody() != null && !content.getTextBody().trim().isEmpty()) {
|
||||||
html.append(
|
html.append(
|
||||||
String.format(
|
String.format(
|
||||||
|
Locale.ROOT,
|
||||||
"<div class=\"text-body\">%s</div>",
|
"<div class=\"text-body\">%s</div>",
|
||||||
convertTextToHtml(content.getTextBody(), customHtmlSanitizer)));
|
convertTextToHtml(content.getTextBody(), customHtmlSanitizer)));
|
||||||
} else {
|
} else {
|
||||||
@@ -234,14 +241,16 @@ public class EmlProcessingUtils {
|
|||||||
.getUrlLinkPattern()
|
.getUrlLinkPattern()
|
||||||
.matcher(html)
|
.matcher(html)
|
||||||
.replaceAll(
|
.replaceAll(
|
||||||
"<a href=\"$1\" style=\"color: #1a73e8; text-decoration: underline;\">$1</a>");
|
"<a href=\"$1\" style=\"color: #1a73e8; text-decoration:"
|
||||||
|
+ " underline;\">$1</a>");
|
||||||
|
|
||||||
html =
|
html =
|
||||||
RegexPatternUtils.getInstance()
|
RegexPatternUtils.getInstance()
|
||||||
.getEmailLinkPattern()
|
.getEmailLinkPattern()
|
||||||
.matcher(html)
|
.matcher(html)
|
||||||
.replaceAll(
|
.replaceAll(
|
||||||
"<a href=\"mailto:$1\" style=\"color: #1a73e8; text-decoration: underline;\">$1</a>");
|
"<a href=\"mailto:$1\" style=\"color: #1a73e8; text-decoration:"
|
||||||
|
+ " underline;\">$1</a>");
|
||||||
|
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
@@ -249,127 +258,128 @@ public class EmlProcessingUtils {
|
|||||||
private static void appendEnhancedStyles(StringBuilder html) {
|
private static void appendEnhancedStyles(StringBuilder html) {
|
||||||
String css =
|
String css =
|
||||||
String.format(
|
String.format(
|
||||||
|
Locale.ROOT,
|
||||||
"""
|
"""
|
||||||
body {
|
body {
|
||||||
font-family: %s;
|
font-family: %s;
|
||||||
font-size: %dpx;
|
font-size: %dpx;
|
||||||
line-height: %s;
|
line-height: %s;
|
||||||
color: %s;
|
color: %s;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
background-color: %s;
|
background-color: %s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.email-container {
|
.email-container {
|
||||||
width: 100%%;
|
width: 100%%;
|
||||||
max-width: 100%%;
|
max-width: 100%%;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.email-header {
|
.email-header {
|
||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
border-bottom: 1px solid %s;
|
border-bottom: 1px solid %s;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.email-header h1 {
|
.email-header h1 {
|
||||||
margin: 0 0 10px 0;
|
margin: 0 0 10px 0;
|
||||||
font-size: %dpx;
|
font-size: %dpx;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.email-meta div {
|
.email-meta div {
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
font-size: %dpx;
|
font-size: %dpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.email-body {
|
.email-body {
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.attachment-section {
|
.attachment-section {
|
||||||
margin-top: 15px;
|
margin-top: 15px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
background-color: %s;
|
background-color: %s;
|
||||||
border: 1px solid %s;
|
border: 1px solid %s;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.attachment-section h3 {
|
.attachment-section h3 {
|
||||||
margin: 0 0 8px 0;
|
margin: 0 0 8px 0;
|
||||||
font-size: %dpx;
|
font-size: %dpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.attachment-item {
|
.attachment-item {
|
||||||
padding: 5px 0;
|
padding: 5px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.attachment-icon {
|
.attachment-icon {
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.attachment-details, .attachment-type {
|
.attachment-details, .attachment-type {
|
||||||
font-size: %dpx;
|
font-size: %dpx;
|
||||||
color: #555555;
|
color: #555555;
|
||||||
}
|
}
|
||||||
|
|
||||||
.attachment-inclusion-note, .attachment-info-note {
|
.attachment-inclusion-note, .attachment-info-note {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
font-size: %dpx;
|
font-size: %dpx;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.attachment-inclusion-note {
|
.attachment-inclusion-note {
|
||||||
background-color: #e6ffed;
|
background-color: #e6ffed;
|
||||||
border: 1px solid #d4f7dc;
|
border: 1px solid #d4f7dc;
|
||||||
color: #006420;
|
color: #006420;
|
||||||
}
|
}
|
||||||
|
|
||||||
.attachment-info-note {
|
.attachment-info-note {
|
||||||
background-color: #fff9e6;
|
background-color: #fff9e6;
|
||||||
border: 1px solid #fff0c2;
|
border: 1px solid #fff0c2;
|
||||||
color: #664d00;
|
color: #664d00;
|
||||||
}
|
}
|
||||||
|
|
||||||
.attachment-link-container {
|
.attachment-link-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
background-color: #f8f9fa;
|
background-color: #f8f9fa;
|
||||||
border: 1px solid #dee2e6;
|
border: 1px solid #dee2e6;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
margin: 4px 0;
|
margin: 4px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.attachment-link-container:hover {
|
.attachment-link-container:hover {
|
||||||
background-color: #e9ecef;
|
background-color: #e9ecef;
|
||||||
}
|
}
|
||||||
|
|
||||||
.attachment-note {
|
.attachment-note {
|
||||||
font-size: %dpx;
|
font-size: %dpx;
|
||||||
color: #6c757d;
|
color: #6c757d;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-content {
|
.no-content {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #666;
|
color: #666;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-body {
|
.text-body {
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
max-width: 100%%;
|
max-width: 100%%;
|
||||||
height: auto;
|
height: auto;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
""",
|
""",
|
||||||
DEFAULT_FONT_FAMILY,
|
DEFAULT_FONT_FAMILY,
|
||||||
DEFAULT_FONT_SIZE,
|
DEFAULT_FONT_SIZE,
|
||||||
DEFAULT_LINE_HEIGHT,
|
DEFAULT_LINE_HEIGHT,
|
||||||
@@ -420,13 +430,14 @@ public class EmlProcessingUtils {
|
|||||||
String attachmentId = "attachment_" + i;
|
String attachmentId = "attachment_" + i;
|
||||||
html.append(
|
html.append(
|
||||||
String.format(
|
String.format(
|
||||||
|
Locale.ROOT,
|
||||||
"""
|
"""
|
||||||
<div class="attachment-item" id="%s">
|
<div class="attachment-item" id="%s">
|
||||||
<span class="attachment-icon" data-filename="%s">@</span>
|
<span class="attachment-icon" data-filename="%s">@</span>
|
||||||
<span class="attachment-name">%s</span>
|
<span class="attachment-name">%s</span>
|
||||||
<span class="attachment-details">(%s%s)</span>
|
<span class="attachment-details">(%s%s)</span>
|
||||||
</div>
|
</div>
|
||||||
""",
|
""",
|
||||||
attachmentId,
|
attachmentId,
|
||||||
escapeHtml(embeddedFilename),
|
escapeHtml(embeddedFilename),
|
||||||
escapeHtml(EmlParser.safeMimeDecode(attachment.getFilename())),
|
escapeHtml(EmlParser.safeMimeDecode(attachment.getFilename())),
|
||||||
@@ -470,7 +481,7 @@ public class EmlProcessingUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (filename != null) {
|
if (filename != null) {
|
||||||
String lowerFilename = filename.toLowerCase();
|
String lowerFilename = filename.toLowerCase(Locale.ROOT);
|
||||||
for (Map.Entry<String, String> entry : EXTENSION_TO_MIME_TYPE.entrySet()) {
|
for (Map.Entry<String, String> entry : EXTENSION_TO_MIME_TYPE.entrySet()) {
|
||||||
if (lowerFilename.endsWith(entry.getKey())) {
|
if (lowerFilename.endsWith(entry.getKey())) {
|
||||||
return entry.getValue();
|
return entry.getValue();
|
||||||
@@ -516,7 +527,7 @@ public class EmlProcessingUtils {
|
|||||||
result.append(processedText, lastEnd, matcher.start());
|
result.append(processedText, lastEnd, matcher.start());
|
||||||
|
|
||||||
String charset = matcher.group(1);
|
String charset = matcher.group(1);
|
||||||
String encoding = matcher.group(2).toUpperCase();
|
String encoding = matcher.group(2).toUpperCase(Locale.ROOT);
|
||||||
String encodedValue = matcher.group(3);
|
String encodedValue = matcher.group(3);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -5,13 +5,11 @@ import java.io.FileOutputStream;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.UncheckedIOException;
|
import java.io.UncheckedIOException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.FileVisitResult;
|
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.SimpleFileVisitor;
|
|
||||||
import java.nio.file.attribute.BasicFileAttributes;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipInputStream;
|
import java.util.zip.ZipInputStream;
|
||||||
@@ -37,9 +35,11 @@ public class FileToPdf {
|
|||||||
try (TempFile tempInputFile =
|
try (TempFile tempInputFile =
|
||||||
new TempFile(
|
new TempFile(
|
||||||
tempFileManager,
|
tempFileManager,
|
||||||
fileName.toLowerCase().endsWith(".html") ? ".html" : ".zip")) {
|
fileName.toLowerCase(Locale.ROOT).endsWith(".html")
|
||||||
|
? ".html"
|
||||||
|
: ".zip")) {
|
||||||
|
|
||||||
if (fileName.toLowerCase().endsWith(".html")) {
|
if (fileName.toLowerCase(Locale.ROOT).endsWith(".html")) {
|
||||||
String sanitizedHtml =
|
String sanitizedHtml =
|
||||||
sanitizeHtmlContent(
|
sanitizeHtmlContent(
|
||||||
new String(fileBytes, StandardCharsets.UTF_8),
|
new String(fileBytes, StandardCharsets.UTF_8),
|
||||||
@@ -47,7 +47,7 @@ public class FileToPdf {
|
|||||||
Files.write(
|
Files.write(
|
||||||
tempInputFile.getPath(),
|
tempInputFile.getPath(),
|
||||||
sanitizedHtml.getBytes(StandardCharsets.UTF_8));
|
sanitizedHtml.getBytes(StandardCharsets.UTF_8));
|
||||||
} else if (fileName.toLowerCase().endsWith(".zip")) {
|
} else if (fileName.toLowerCase(Locale.ROOT).endsWith(".zip")) {
|
||||||
Files.write(tempInputFile.getPath(), fileBytes);
|
Files.write(tempInputFile.getPath(), fileBytes);
|
||||||
sanitizeHtmlFilesInZip(
|
sanitizeHtmlFilesInZip(
|
||||||
tempInputFile.getPath(), tempFileManager, customHtmlSanitizer);
|
tempInputFile.getPath(), tempFileManager, customHtmlSanitizer);
|
||||||
@@ -102,8 +102,8 @@ public class FileToPdf {
|
|||||||
tempUnzippedDir.getPath().resolve(sanitizeZipFilename(entry.getName()));
|
tempUnzippedDir.getPath().resolve(sanitizeZipFilename(entry.getName()));
|
||||||
if (!entry.isDirectory()) {
|
if (!entry.isDirectory()) {
|
||||||
Files.createDirectories(filePath.getParent());
|
Files.createDirectories(filePath.getParent());
|
||||||
if (entry.getName().toLowerCase().endsWith(".html")
|
if (entry.getName().toLowerCase(Locale.ROOT).endsWith(".html")
|
||||||
|| entry.getName().toLowerCase().endsWith(".htm")) {
|
|| entry.getName().toLowerCase(Locale.ROOT).endsWith(".htm")) {
|
||||||
String content =
|
String content =
|
||||||
new String(zipIn.readAllBytes(), StandardCharsets.UTF_8);
|
new String(zipIn.readAllBytes(), StandardCharsets.UTF_8);
|
||||||
String sanitizedContent =
|
String sanitizedContent =
|
||||||
@@ -145,64 +145,6 @@ public class FileToPdf {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void deleteDirectory(Path dir) throws IOException {
|
|
||||||
Files.walkFileTree(
|
|
||||||
dir,
|
|
||||||
new SimpleFileVisitor<Path>() {
|
|
||||||
@Override
|
|
||||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
|
|
||||||
throws IOException {
|
|
||||||
Files.delete(file);
|
|
||||||
return FileVisitResult.CONTINUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public FileVisitResult postVisitDirectory(Path dir, IOException exc)
|
|
||||||
throws IOException {
|
|
||||||
Files.delete(dir);
|
|
||||||
return FileVisitResult.CONTINUE;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Path unzipAndGetMainHtml(byte[] fileBytes) throws IOException {
|
|
||||||
Path tempDirectory = Files.createTempDirectory("unzipped_");
|
|
||||||
try (ZipInputStream zipIn =
|
|
||||||
ZipSecurity.createHardenedInputStream(new ByteArrayInputStream(fileBytes))) {
|
|
||||||
ZipEntry entry = zipIn.getNextEntry();
|
|
||||||
while (entry != null) {
|
|
||||||
Path filePath = tempDirectory.resolve(sanitizeZipFilename(entry.getName()));
|
|
||||||
if (entry.isDirectory()) {
|
|
||||||
Files.createDirectories(filePath); // Explicitly create the directory structure
|
|
||||||
} else {
|
|
||||||
Files.createDirectories(
|
|
||||||
filePath.getParent()); // Create parent directories if they don't exist
|
|
||||||
Files.copy(zipIn, filePath);
|
|
||||||
}
|
|
||||||
zipIn.closeEntry();
|
|
||||||
entry = zipIn.getNextEntry();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search for the main HTML file.
|
|
||||||
try (Stream<Path> walk = Files.walk(tempDirectory)) {
|
|
||||||
List<Path> htmlFiles = walk.filter(file -> file.toString().endsWith(".html")).toList();
|
|
||||||
|
|
||||||
if (htmlFiles.isEmpty()) {
|
|
||||||
throw new IOException("No HTML files found in the unzipped directory.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prioritize 'index.html' if it exists, otherwise use the first .html file
|
|
||||||
for (Path htmlFile : htmlFiles) {
|
|
||||||
if ("index.html".equals(htmlFile.getFileName().toString())) {
|
|
||||||
return htmlFile;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return htmlFiles.get(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static String sanitizeZipFilename(String entryName) {
|
static String sanitizeZipFilename(String entryName) {
|
||||||
if (entryName == null || entryName.trim().isEmpty()) {
|
if (entryName == null || entryName.trim().isEmpty()) {
|
||||||
return "";
|
return "";
|
||||||
|
|||||||
+9
-4
@@ -1,4 +1,4 @@
|
|||||||
package stirling.software.proprietary.util;
|
package stirling.software.common.util;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@@ -23,9 +23,13 @@ import org.apache.pdfbox.pdmodel.interactive.form.PDTerminalField;
|
|||||||
import lombok.experimental.UtilityClass;
|
import lombok.experimental.UtilityClass;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class for copying and transforming PDF form fields during page operations. Used by
|
||||||
|
* multi-page layout and other page manipulation operations that need to preserve form fields.
|
||||||
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@UtilityClass
|
@UtilityClass
|
||||||
public class FormCopyUtils {
|
public class GeneralFormCopyUtils {
|
||||||
|
|
||||||
public boolean hasAnyRotatedPage(PDDocument document) {
|
public boolean hasAnyRotatedPage(PDDocument document) {
|
||||||
try {
|
try {
|
||||||
@@ -188,7 +192,8 @@ public class FormCopyUtils {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
FormFieldTypeSupport handler = FormFieldTypeSupport.forField(terminalField);
|
GeneralFormFieldTypeSupport handler =
|
||||||
|
GeneralFormFieldTypeSupport.forField(terminalField);
|
||||||
if (handler == null) {
|
if (handler == null) {
|
||||||
log.debug(
|
log.debug(
|
||||||
"Skipping unsupported field type '{}' for widget '{}'",
|
"Skipping unsupported field type '{}' for widget '{}'",
|
||||||
@@ -222,7 +227,7 @@ public class FormCopyUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void copyFieldUsingHandler(
|
private void copyFieldUsingHandler(
|
||||||
FormFieldTypeSupport handler,
|
GeneralFormFieldTypeSupport handler,
|
||||||
PDTerminalField sourceField,
|
PDTerminalField sourceField,
|
||||||
PDAcroForm newAcroForm,
|
PDAcroForm newAcroForm,
|
||||||
PDPage destinationPage,
|
PDPage destinationPage,
|
||||||
+165
@@ -0,0 +1,165 @@
|
|||||||
|
package stirling.software.common.util;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
|
||||||
|
import org.apache.pdfbox.pdmodel.interactive.form.PDCheckBox;
|
||||||
|
import org.apache.pdfbox.pdmodel.interactive.form.PDComboBox;
|
||||||
|
import org.apache.pdfbox.pdmodel.interactive.form.PDField;
|
||||||
|
import org.apache.pdfbox.pdmodel.interactive.form.PDListBox;
|
||||||
|
import org.apache.pdfbox.pdmodel.interactive.form.PDPushButton;
|
||||||
|
import org.apache.pdfbox.pdmodel.interactive.form.PDRadioButton;
|
||||||
|
import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField;
|
||||||
|
import org.apache.pdfbox.pdmodel.interactive.form.PDTerminalField;
|
||||||
|
import org.apache.pdfbox.pdmodel.interactive.form.PDTextField;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simplified form field type support for general PDF operations. This is a subset of the full
|
||||||
|
* proprietary FormFieldTypeSupport, containing only what's needed for basic form field copying
|
||||||
|
* during page operations.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public enum GeneralFormFieldTypeSupport {
|
||||||
|
TEXT("text", "textField", PDTextField.class) {
|
||||||
|
@Override
|
||||||
|
PDTerminalField createField(PDAcroForm acroForm) {
|
||||||
|
PDTextField textField = new PDTextField(acroForm);
|
||||||
|
textField.setDefaultAppearance("/Helv 12 Tf 0 g");
|
||||||
|
return textField;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void copyFromOriginal(PDTerminalField source, PDTerminalField target) throws IOException {
|
||||||
|
PDTextField src = (PDTextField) source;
|
||||||
|
PDTextField dst = (PDTextField) target;
|
||||||
|
String value = src.getValueAsString();
|
||||||
|
if (value != null) {
|
||||||
|
dst.setValue(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
CHECKBOX("checkbox", "checkBox", PDCheckBox.class) {
|
||||||
|
@Override
|
||||||
|
PDTerminalField createField(PDAcroForm acroForm) {
|
||||||
|
return new PDCheckBox(acroForm);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void copyFromOriginal(PDTerminalField source, PDTerminalField target) throws IOException {
|
||||||
|
PDCheckBox src = (PDCheckBox) source;
|
||||||
|
PDCheckBox dst = (PDCheckBox) target;
|
||||||
|
if (src.isChecked()) {
|
||||||
|
dst.check();
|
||||||
|
} else {
|
||||||
|
dst.unCheck();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
RADIO("radio", "radioButton", PDRadioButton.class) {
|
||||||
|
@Override
|
||||||
|
PDTerminalField createField(PDAcroForm acroForm) {
|
||||||
|
return new PDRadioButton(acroForm);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void copyFromOriginal(PDTerminalField source, PDTerminalField target) throws IOException {
|
||||||
|
PDRadioButton src = (PDRadioButton) source;
|
||||||
|
PDRadioButton dst = (PDRadioButton) target;
|
||||||
|
if (src.getExportValues() != null) {
|
||||||
|
dst.setExportValues(src.getExportValues());
|
||||||
|
}
|
||||||
|
if (src.getValue() != null) {
|
||||||
|
dst.setValue(src.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
COMBOBOX("combobox", "comboBox", PDComboBox.class) {
|
||||||
|
@Override
|
||||||
|
PDTerminalField createField(PDAcroForm acroForm) {
|
||||||
|
return new PDComboBox(acroForm);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void copyFromOriginal(PDTerminalField source, PDTerminalField target) throws IOException {
|
||||||
|
PDComboBox src = (PDComboBox) source;
|
||||||
|
PDComboBox dst = (PDComboBox) target;
|
||||||
|
if (src.getOptions() != null) {
|
||||||
|
dst.setOptions(src.getOptions());
|
||||||
|
}
|
||||||
|
if (src.getValue() != null && !src.getValue().isEmpty()) {
|
||||||
|
dst.setValue(src.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
LISTBOX("listbox", "listBox", PDListBox.class) {
|
||||||
|
@Override
|
||||||
|
PDTerminalField createField(PDAcroForm acroForm) {
|
||||||
|
return new PDListBox(acroForm);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void copyFromOriginal(PDTerminalField source, PDTerminalField target) throws IOException {
|
||||||
|
PDListBox src = (PDListBox) source;
|
||||||
|
PDListBox dst = (PDListBox) target;
|
||||||
|
if (src.getOptions() != null) {
|
||||||
|
dst.setOptions(src.getOptions());
|
||||||
|
}
|
||||||
|
if (src.getValue() != null && !src.getValue().isEmpty()) {
|
||||||
|
dst.setValue(src.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
SIGNATURE("signature", "signature", PDSignatureField.class) {
|
||||||
|
@Override
|
||||||
|
PDTerminalField createField(PDAcroForm acroForm) {
|
||||||
|
return new PDSignatureField(acroForm);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
BUTTON("button", "pushButton", PDPushButton.class) {
|
||||||
|
@Override
|
||||||
|
PDTerminalField createField(PDAcroForm acroForm) {
|
||||||
|
return new PDPushButton(acroForm);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private final String typeName;
|
||||||
|
private final String fallbackWidgetName;
|
||||||
|
private final Class<? extends PDTerminalField> fieldClass;
|
||||||
|
|
||||||
|
GeneralFormFieldTypeSupport(
|
||||||
|
String typeName,
|
||||||
|
String fallbackWidgetName,
|
||||||
|
Class<? extends PDTerminalField> fieldClass) {
|
||||||
|
this.typeName = typeName;
|
||||||
|
this.fallbackWidgetName = fallbackWidgetName;
|
||||||
|
this.fieldClass = fieldClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static GeneralFormFieldTypeSupport forField(PDField field) {
|
||||||
|
if (field == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (GeneralFormFieldTypeSupport handler : values()) {
|
||||||
|
if (handler.fieldClass.isInstance(field)) {
|
||||||
|
return handler;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String typeName() {
|
||||||
|
return typeName;
|
||||||
|
}
|
||||||
|
|
||||||
|
String fallbackWidgetName() {
|
||||||
|
return fallbackWidgetName;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract PDTerminalField createField(PDAcroForm acroForm);
|
||||||
|
|
||||||
|
void copyFromOriginal(PDTerminalField source, PDTerminalField target) throws IOException {
|
||||||
|
// default no-op
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,6 +33,11 @@ import stirling.software.common.configuration.InstallationPathConfig;
|
|||||||
@UtilityClass
|
@UtilityClass
|
||||||
public class GeneralUtils {
|
public class GeneralUtils {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum number of resolved DNS addresses allowed for a host before it is considered unsafe.
|
||||||
|
*/
|
||||||
|
private static final int MAX_DNS_ADDRESSES = 20;
|
||||||
|
|
||||||
private final Set<String> DEFAULT_VALID_SCRIPTS = Set.of("png_to_webp.py", "split_photos.py");
|
private final Set<String> DEFAULT_VALID_SCRIPTS = Set.of("png_to_webp.py", "split_photos.py");
|
||||||
private final Set<String> DEFAULT_VALID_PIPELINE =
|
private final Set<String> DEFAULT_VALID_PIPELINE =
|
||||||
Set.of(
|
Set.of(
|
||||||
@@ -90,32 +95,6 @@ public class GeneralUtils {
|
|||||||
return tempFile;
|
return tempFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* Gets the configured temporary directory, creating it if necessary.
|
|
||||||
*
|
|
||||||
* @return Path to the temporary directory
|
|
||||||
* @throws IOException if directory creation fails
|
|
||||||
*/
|
|
||||||
private Path getTempDirectory() throws IOException {
|
|
||||||
String customTempDir = System.getenv("STIRLING_TEMPFILES_DIRECTORY");
|
|
||||||
if (customTempDir == null || customTempDir.isEmpty()) {
|
|
||||||
customTempDir = System.getProperty("stirling.tempfiles.directory");
|
|
||||||
}
|
|
||||||
|
|
||||||
Path tempDir;
|
|
||||||
if (customTempDir != null && !customTempDir.isEmpty()) {
|
|
||||||
tempDir = Path.of(customTempDir);
|
|
||||||
} else {
|
|
||||||
tempDir = Path.of(System.getProperty("java.io.tmpdir"), "stirling-pdf");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Files.exists(tempDir)) {
|
|
||||||
Files.createDirectories(tempDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
return tempDir;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Remove file extension
|
* Remove file extension
|
||||||
*
|
*
|
||||||
@@ -154,7 +133,7 @@ public class GeneralUtils {
|
|||||||
return matcher.find() ? matcher.replaceFirst("") : filename;
|
return matcher.find() ? matcher.replaceFirst("") : filename;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/**
|
||||||
* Append suffix to base name with null safety.
|
* Append suffix to base name with null safety.
|
||||||
*
|
*
|
||||||
* @param baseName the base filename, null becomes "default"
|
* @param baseName the base filename, null becomes "default"
|
||||||
@@ -165,7 +144,7 @@ public class GeneralUtils {
|
|||||||
return (baseName == null ? "default" : baseName) + (suffix != null ? suffix : "");
|
return (baseName == null ? "default" : baseName) + (suffix != null ? suffix : "");
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/**
|
||||||
* Generate a PDF filename by removing extension from first file and adding suffix.
|
* Generate a PDF filename by removing extension from first file and adding suffix.
|
||||||
*
|
*
|
||||||
* <p>High-level utility method for common PDF naming scenarios. Handles null safety and uses
|
* <p>High-level utility method for common PDF naming scenarios. Handles null safety and uses
|
||||||
@@ -180,7 +159,7 @@ public class GeneralUtils {
|
|||||||
return appendSuffix(baseName, suffix);
|
return appendSuffix(baseName, suffix);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/**
|
||||||
* Process a list of filenames by removing extensions and adding suffix.
|
* Process a list of filenames by removing extensions and adding suffix.
|
||||||
*
|
*
|
||||||
* <p>Efficiently processes multiple filenames using streaming operations and bulk operations
|
* <p>Efficiently processes multiple filenames using streaming operations and bulk operations
|
||||||
@@ -201,7 +180,7 @@ public class GeneralUtils {
|
|||||||
.forEach(processor);
|
.forEach(processor);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/**
|
||||||
* Extract title from filename by removing extension, with fallback handling.
|
* Extract title from filename by removing extension, with fallback handling.
|
||||||
*
|
*
|
||||||
* <p>Returns "Untitled" for null or empty filenames, otherwise removes the extension using the
|
* <p>Returns "Untitled" for null or empty filenames, otherwise removes the extension using the
|
||||||
@@ -269,6 +248,12 @@ public class GeneralUtils {
|
|||||||
.getResources(pattern);
|
.getResources(pattern);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates URL syntax and disallows common-infrastructure targets to reduce SSRF risk.
|
||||||
|
*
|
||||||
|
* @param urlStr a URL string to validate
|
||||||
|
* @return {@code true} if the URL is syntactically valid and allowed; {@code false} otherwise
|
||||||
|
*/
|
||||||
public boolean isValidURL(String urlStr) {
|
public boolean isValidURL(String urlStr) {
|
||||||
try {
|
try {
|
||||||
Urls.create(
|
Urls.create(
|
||||||
@@ -279,7 +264,7 @@ public class GeneralUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/**
|
||||||
* Checks if a URL is reachable with proper timeout configuration and error handling.
|
* Checks if a URL is reachable with proper timeout configuration and error handling.
|
||||||
*
|
*
|
||||||
* @param urlStr the URL string to check
|
* @param urlStr the URL string to check
|
||||||
@@ -289,15 +274,17 @@ public class GeneralUtils {
|
|||||||
return isURLReachable(urlStr, 5000, 5000);
|
return isURLReachable(urlStr, 5000, 5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/**
|
||||||
* Checks if a URL is reachable with configurable timeouts.
|
* Checks whether a URL is reachable using configurable timeouts. Only {@code http} and {@code
|
||||||
|
* https} protocols are permitted, and local/private/multicast ranges are blocked.
|
||||||
*
|
*
|
||||||
* @param urlStr the URL string to check
|
* @param urlStr the URL to probe
|
||||||
* @param connectTimeout connection timeout in milliseconds
|
* @param connectTimeout connection timeout in milliseconds
|
||||||
* @param readTimeout read timeout in milliseconds
|
* @param readTimeout read timeout in milliseconds
|
||||||
* @return true if URL is reachable, false otherwise
|
* @return {@code true} if a HEAD request returns a 2xx or 3xx status; {@code false} otherwise
|
||||||
*/
|
*/
|
||||||
public boolean isURLReachable(String urlStr, int connectTimeout, int readTimeout) {
|
public boolean isURLReachable(String urlStr, int connectTimeout, int readTimeout) {
|
||||||
|
HttpURLConnection connection = null;
|
||||||
try {
|
try {
|
||||||
// Parse the URL
|
// Parse the URL
|
||||||
URL url = URI.create(urlStr).toURL();
|
URL url = URI.create(urlStr).toURL();
|
||||||
@@ -308,14 +295,17 @@ public class GeneralUtils {
|
|||||||
return false; // Disallow other protocols
|
return false; // Disallow other protocols
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the host is a local address
|
|
||||||
String host = url.getHost();
|
String host = url.getHost();
|
||||||
if (isLocalAddress(host)) {
|
if (host == null || host.isBlank()) {
|
||||||
return false; // Exclude local addresses
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDisallowedNetworkLocation(host)) {
|
||||||
|
return false; // Exclude local, private or otherwise sensitive addresses
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the URL is reachable
|
// Check if the URL is reachable
|
||||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
connection = (HttpURLConnection) url.openConnection();
|
||||||
connection.setRequestMethod("HEAD");
|
connection.setRequestMethod("HEAD");
|
||||||
connection.setConnectTimeout(connectTimeout);
|
connection.setConnectTimeout(connectTimeout);
|
||||||
connection.setReadTimeout(readTimeout);
|
connection.setReadTimeout(readTimeout);
|
||||||
@@ -326,29 +316,173 @@ public class GeneralUtils {
|
|||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.debug("URL {} is not reachable: {}", urlStr, e.getMessage());
|
log.debug("URL {} is not reachable: {}", urlStr, e.getMessage());
|
||||||
return false; // Return false in case of any exception
|
return false; // Return false in case of any exception
|
||||||
|
} finally {
|
||||||
|
if (connection != null) {
|
||||||
|
connection.disconnect();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isLocalAddress(String host) {
|
/**
|
||||||
|
* Determines whether the specified host resolves to a disallowed network location, such as
|
||||||
|
* local, private, multicast, or reserved ranges. Excessive DNS results are also blocked.
|
||||||
|
*
|
||||||
|
* @param host the hostname to resolve
|
||||||
|
* @return {@code true} if the host should be considered unsafe
|
||||||
|
*/
|
||||||
|
private boolean isDisallowedNetworkLocation(String host) {
|
||||||
|
// Resolution is delegated to the JVM/OS resolver which already applies system
|
||||||
|
// configured query limits and timeouts. We only need the resolved addresses here so
|
||||||
|
// that we can enforce the MAX_DNS_ADDRESSES limit and perform the sensitive range
|
||||||
|
// checks below.
|
||||||
try {
|
try {
|
||||||
// Resolve DNS to IP address
|
InetAddress[] addresses = InetAddress.getAllByName(host);
|
||||||
InetAddress address = InetAddress.getByName(host);
|
if (addresses.length > MAX_DNS_ADDRESSES) {
|
||||||
|
log.debug(
|
||||||
// Check for local addresses
|
"Blocking URL to host {} due to excessive DNS records (>{})",
|
||||||
return address.isAnyLocalAddress()
|
host,
|
||||||
|| // Matches 0.0.0.0 or similar
|
MAX_DNS_ADDRESSES);
|
||||||
address.isLoopbackAddress()
|
return true;
|
||||||
|| // Matches 127.0.0.1 or ::1
|
}
|
||||||
address.isSiteLocalAddress()
|
for (InetAddress address : addresses) {
|
||||||
|| // Matches private IPv4 ranges: 192.168.x.x, 10.x.x.x, 172.16.x.x to
|
if (address == null || isSensitiveAddress(address)) {
|
||||||
// 172.31.x.x
|
log.debug("Blocking URL to host {} resolved to {}", host, address);
|
||||||
address.getHostAddress()
|
return true;
|
||||||
.startsWith("fe80:"); // Matches link-local IPv6 addresses
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return false; // Return false for invalid or unresolved addresses
|
log.debug("Unable to resolve host {}: {}", host, e.getMessage());
|
||||||
|
return true; // Treat resolution issues as unsafe to avoid SSRF
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the given IP address lies within ranges that should not be contacted by the
|
||||||
|
* server (loopback, link-local, private, multicast, etc.). IPv6 ULA and IPv4-mapped addresses
|
||||||
|
* are handled.
|
||||||
|
*
|
||||||
|
* @param address the resolved address
|
||||||
|
* @return {@code true} if the address is considered sensitive
|
||||||
|
*/
|
||||||
|
private boolean isSensitiveAddress(InetAddress address) {
|
||||||
|
if (address.isAnyLocalAddress()
|
||||||
|
|| address.isLoopbackAddress()
|
||||||
|
|| address.isLinkLocalAddress()
|
||||||
|
|| address.isSiteLocalAddress()
|
||||||
|
|| address.isMulticastAddress()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] rawAddress = address.getAddress();
|
||||||
|
if (address instanceof Inet4Address) {
|
||||||
|
return isPrivateOrReservedIPv4(rawAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (address instanceof Inet6Address inet6Address) {
|
||||||
|
if (isUniqueLocalIPv6(rawAddress)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (isIPv4MappedAddress(rawAddress) || inet6Address.isIPv4CompatibleAddress()) {
|
||||||
|
byte[] ipv4 =
|
||||||
|
Arrays.copyOfRange(rawAddress, rawAddress.length - 4, rawAddress.length);
|
||||||
|
return isPrivateOrReservedIPv4(ipv4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether an IPv4 address is private or reserved. Any malformed input defaults to {@code
|
||||||
|
* true} (conservative) to avoid misuse.
|
||||||
|
*
|
||||||
|
* @param address 4-byte IPv4 address
|
||||||
|
* @return {@code true} if private/reserved
|
||||||
|
*/
|
||||||
|
private boolean isPrivateOrReservedIPv4(byte[] address) {
|
||||||
|
// IPv4 addresses must be exactly 4 bytes. Treat null or unexpected lengths as
|
||||||
|
// sensitive to avoid processing malformed input.
|
||||||
|
if (address == null || address.length != 4) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int first = Byte.toUnsignedInt(address[0]);
|
||||||
|
int second = Byte.toUnsignedInt(address[1]);
|
||||||
|
|
||||||
|
if (first == 0 || first == 127) {
|
||||||
|
return true; // 0.0.0.0/8 and 127.0.0.0/8
|
||||||
|
}
|
||||||
|
if (first == 100 && second >= 64 && second <= 127) {
|
||||||
|
return true; // 100.64.0.0/10 Carrier-grade NAT
|
||||||
|
}
|
||||||
|
if (first == 169 && second == 254) {
|
||||||
|
return true; // 169.254.0.0/16 Link-local
|
||||||
|
}
|
||||||
|
if (first == 172 && second >= 16 && second <= 31) {
|
||||||
|
return true; // 172.16.0.0/12 Private
|
||||||
|
}
|
||||||
|
if (first == 192 && second == 0 && Byte.toUnsignedInt(address[2]) == 0) {
|
||||||
|
return true; // 192.0.0.0/24 IETF Protocol Assignments
|
||||||
|
}
|
||||||
|
if (first == 192 && second == 0 && Byte.toUnsignedInt(address[2]) == 2) {
|
||||||
|
return true; // 192.0.2.0/24 TEST-NET-1
|
||||||
|
}
|
||||||
|
if (first == 192 && second == 168) {
|
||||||
|
return true; // 192.168.0.0/16 Private
|
||||||
|
}
|
||||||
|
if (first == 198 && (second == 18 || second == 19)) {
|
||||||
|
return true; // 198.18.0.0/15 Benchmark tests
|
||||||
|
}
|
||||||
|
if (first == 198 && second == 51 && Byte.toUnsignedInt(address[2]) == 100) {
|
||||||
|
return true; // 198.51.100.0/24 TEST-NET-2
|
||||||
|
}
|
||||||
|
if (first == 203 && second == 0 && Byte.toUnsignedInt(address[2]) == 113) {
|
||||||
|
return true; // 203.0.113.0/24 TEST-NET-3
|
||||||
|
}
|
||||||
|
if (first == 10) {
|
||||||
|
return true; // 10.0.0.0/8 Private
|
||||||
|
}
|
||||||
|
if (first >= 224) {
|
||||||
|
return true; // 224.0.0.0/4 Multicast and 240.0.0.0/4 Reserved for future use
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether an IPv6 address is a Unique Local Address (ULA, fc00::/7). Any malformed input
|
||||||
|
* defaults to {@code true} (conservative) to avoid misuse.
|
||||||
|
*
|
||||||
|
* @param address 16-byte IPv6 address
|
||||||
|
* @return {@code true} if ULA
|
||||||
|
*/
|
||||||
|
private boolean isUniqueLocalIPv6(byte[] address) {
|
||||||
|
if (address == null || address.length != 16) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
int first = Byte.toUnsignedInt(address[0]);
|
||||||
|
return (first & 0xFE) == 0xFC; // fc00::/7 Unique local addresses
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether an IPv6 address is an IPv4-mapped address (::ffff:0:0/96). Any malformed input
|
||||||
|
* defaults to {@code false} (conservative) to avoid misuse.
|
||||||
|
*
|
||||||
|
* @param address 16-byte IPv6 address
|
||||||
|
* @return {@code true} if IPv4-mapped
|
||||||
|
*/
|
||||||
|
private boolean isIPv4MappedAddress(byte[] address) {
|
||||||
|
if (address == null || address.length != 16) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
if (address[i] != 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return address[10] == (byte) 0xFF && address[11] == (byte) 0xFF;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Improved multipart file conversion using the shared helper method.
|
* Improved multipart file conversion using the shared helper method.
|
||||||
*
|
*
|
||||||
@@ -386,7 +520,7 @@ public class GeneralUtils {
|
|||||||
throw new IllegalArgumentException("Invalid default unit: " + defaultUnit);
|
throw new IllegalArgumentException("Invalid default unit: " + defaultUnit);
|
||||||
}
|
}
|
||||||
|
|
||||||
sizeStr = sizeStr.trim().toUpperCase();
|
sizeStr = sizeStr.trim().toUpperCase(Locale.ROOT);
|
||||||
sizeStr = sizeStr.replace(",", ".").replace(" ", "");
|
sizeStr = sizeStr.replace(",", ".").replace(" ", "");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -415,7 +549,7 @@ public class GeneralUtils {
|
|||||||
return Long.parseLong(sizeStr.substring(0, sizeStr.length() - 1));
|
return Long.parseLong(sizeStr.substring(0, sizeStr.length() - 1));
|
||||||
} else {
|
} else {
|
||||||
// Use provided default unit or fall back to MB
|
// Use provided default unit or fall back to MB
|
||||||
String unit = defaultUnit != null ? defaultUnit.toUpperCase() : "MB";
|
String unit = defaultUnit != null ? defaultUnit.toUpperCase(Locale.ROOT) : "MB";
|
||||||
double value = Double.parseDouble(sizeStr);
|
double value = Double.parseDouble(sizeStr);
|
||||||
return switch (unit) {
|
return switch (unit) {
|
||||||
case "TB" -> (long) (value * 1024L * 1024L * 1024L * 1024L);
|
case "TB" -> (long) (value * 1024L * 1024L * 1024L * 1024L);
|
||||||
@@ -457,13 +591,14 @@ public class GeneralUtils {
|
|||||||
if (bytes < 1024) {
|
if (bytes < 1024) {
|
||||||
return bytes + " B";
|
return bytes + " B";
|
||||||
} else if (bytes < 1024L * 1024L) {
|
} else if (bytes < 1024L * 1024L) {
|
||||||
return String.format(Locale.US, "%.2f KB", bytes / 1024.0);
|
return String.format(Locale.ROOT, "%.2f KB", bytes / 1024.0);
|
||||||
} else if (bytes < 1024L * 1024L * 1024L) {
|
} else if (bytes < 1024L * 1024L * 1024L) {
|
||||||
return String.format(Locale.US, "%.2f MB", bytes / (1024.0 * 1024.0));
|
return String.format(Locale.ROOT, "%.2f MB", bytes / (1024.0 * 1024.0));
|
||||||
} else if (bytes < 1024L * 1024L * 1024L * 1024L) {
|
} else if (bytes < 1024L * 1024L * 1024L * 1024L) {
|
||||||
return String.format(Locale.US, "%.2f GB", bytes / (1024.0 * 1024.0 * 1024.0));
|
return String.format(Locale.ROOT, "%.2f GB", bytes / (1024.0 * 1024.0 * 1024.0));
|
||||||
} else {
|
} else {
|
||||||
return String.format(Locale.US, "%.2f TB", bytes / (1024.0 * 1024.0 * 1024.0 * 1024.0));
|
return String.format(
|
||||||
|
Locale.ROOT, "%.2f TB", bytes / (1024.0 * 1024.0 * 1024.0 * 1024.0));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -723,7 +858,7 @@ public class GeneralUtils {
|
|||||||
byte[] mac = net.getHardwareAddress();
|
byte[] mac = net.getHardwareAddress();
|
||||||
if (mac != null && mac.length > 0) {
|
if (mac != null && mac.length > 0) {
|
||||||
for (byte b : mac) {
|
for (byte b : mac) {
|
||||||
sb.append(String.format("%02X", b));
|
sb.append(String.format(Locale.ROOT, "%02X", b));
|
||||||
}
|
}
|
||||||
break; // Use the first valid network interface
|
break; // Use the first valid network interface
|
||||||
}
|
}
|
||||||
@@ -733,7 +868,7 @@ public class GeneralUtils {
|
|||||||
byte[] mac = network.getHardwareAddress();
|
byte[] mac = network.getHardwareAddress();
|
||||||
if (mac != null) {
|
if (mac != null) {
|
||||||
for (byte b : mac) {
|
for (byte b : mac) {
|
||||||
sb.append(String.format("%02X", b));
|
sb.append(String.format(Locale.ROOT, "%02X", b));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -940,17 +1075,29 @@ public class GeneralUtils {
|
|||||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT)
|
ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT)
|
||||||
.runCommandWithOutputHandling(command);
|
.runCommandWithOutputHandling(command);
|
||||||
|
|
||||||
|
ExceptionUtils.GhostscriptException detectedError =
|
||||||
|
ExceptionUtils.detectGhostscriptCriticalError(result.getMessages());
|
||||||
|
if (detectedError != null) {
|
||||||
|
log.warn(
|
||||||
|
"Ghostscript ebook optimization reported a critical error: {}",
|
||||||
|
detectedError.getMessage());
|
||||||
|
throw detectedError;
|
||||||
|
}
|
||||||
|
|
||||||
if (result.getRc() != 0) {
|
if (result.getRc() != 0) {
|
||||||
log.warn(
|
log.warn(
|
||||||
"Ghostscript ebook optimization failed with return code: {}",
|
"Ghostscript ebook optimization failed with return code: {}",
|
||||||
result.getRc());
|
result.getRc());
|
||||||
throw ExceptionUtils.createGhostscriptCompressionException();
|
throw ExceptionUtils.createGhostscriptCompressionException(result.getMessages());
|
||||||
}
|
}
|
||||||
|
|
||||||
return Files.readAllBytes(tempOutput);
|
return Files.readAllBytes(tempOutput);
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("Ghostscript ebook optimization failed", e);
|
log.warn("Ghostscript ebook optimization failed", e);
|
||||||
|
if (e instanceof ExceptionUtils.GhostscriptException ghostscriptException) {
|
||||||
|
throw ghostscriptException;
|
||||||
|
}
|
||||||
throw ExceptionUtils.createGhostscriptCompressionException(e);
|
throw ExceptionUtils.createGhostscriptCompressionException(e);
|
||||||
} finally {
|
} finally {
|
||||||
if (tempInput != null) {
|
if (tempInput != null) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import java.io.IOException;
|
|||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
import javax.imageio.ImageIO;
|
import javax.imageio.ImageIO;
|
||||||
import javax.imageio.ImageReader;
|
import javax.imageio.ImageReader;
|
||||||
@@ -118,7 +119,7 @@ public class ImageProcessingUtils {
|
|||||||
BufferedImage image = null;
|
BufferedImage image = null;
|
||||||
String filename = file.getOriginalFilename();
|
String filename = file.getOriginalFilename();
|
||||||
|
|
||||||
if (filename != null && filename.toLowerCase().endsWith(".psd")) {
|
if (filename != null && filename.toLowerCase(Locale.ROOT).endsWith(".psd")) {
|
||||||
// For PSD files, try explicit ImageReader
|
// For PSD files, try explicit ImageReader
|
||||||
Iterator<ImageReader> readers = ImageIO.getImageReadersByFormatName("PSD");
|
Iterator<ImageReader> readers = ImageIO.getImageReadersByFormatName("PSD");
|
||||||
if (readers.hasNext()) {
|
if (readers.hasNext()) {
|
||||||
@@ -134,7 +135,8 @@ public class ImageProcessingUtils {
|
|||||||
throw new IOException(
|
throw new IOException(
|
||||||
"Unable to read image from file: "
|
"Unable to read image from file: "
|
||||||
+ filename
|
+ filename
|
||||||
+ ". Supported PSD formats: RGB/CMYK/Gray 8-32 bit, RLE/ZIP compression");
|
+ ". Supported PSD formats: RGB/CMYK/Gray 8-32 bit, RLE/ZIP"
|
||||||
|
+ " compression");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// For non-PSD files, use standard ImageIO
|
// For non-PSD files, use standard ImageIO
|
||||||
@@ -142,7 +144,7 @@ public class ImageProcessingUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (image == null) {
|
if (image == null) {
|
||||||
throw new IOException("Unable to read image from file: " + filename);
|
throw ExceptionUtils.createImageReadException(filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
double orientation = extractImageOrientation(file.getInputStream());
|
double orientation = extractImageOrientation(file.getInputStream());
|
||||||
|
|||||||
@@ -27,15 +27,22 @@ import io.github.pixee.security.Filenames;
|
|||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import stirling.software.common.configuration.RuntimePathConfig;
|
||||||
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
|
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class PDFToFile {
|
public class PDFToFile {
|
||||||
|
|
||||||
private final TempFileManager tempFileManager;
|
private final TempFileManager tempFileManager;
|
||||||
|
private final RuntimePathConfig runtimePathConfig;
|
||||||
|
|
||||||
public PDFToFile(TempFileManager tempFileManager) {
|
public PDFToFile(TempFileManager tempFileManager) {
|
||||||
|
this(tempFileManager, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PDFToFile(TempFileManager tempFileManager, RuntimePathConfig runtimePathConfig) {
|
||||||
this.tempFileManager = tempFileManager;
|
this.tempFileManager = tempFileManager;
|
||||||
|
this.runtimePathConfig = runtimePathConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ResponseEntity<byte[]> processPdfToMarkdown(MultipartFile inputFile)
|
public ResponseEntity<byte[]> processPdfToMarkdown(MultipartFile inputFile)
|
||||||
@@ -241,31 +248,65 @@ public class PDFToFile {
|
|||||||
byte[] fileBytes;
|
byte[] fileBytes;
|
||||||
String fileName;
|
String fileName;
|
||||||
|
|
||||||
|
Path libreOfficeProfile = null;
|
||||||
try (TempFile inputFileTemp = new TempFile(tempFileManager, ".pdf");
|
try (TempFile inputFileTemp = new TempFile(tempFileManager, ".pdf");
|
||||||
TempDirectory outputDirTemp = new TempDirectory(tempFileManager)) {
|
TempDirectory outputDirTemp = new TempDirectory(tempFileManager)) {
|
||||||
|
|
||||||
Path tempInputFile = inputFileTemp.getPath();
|
Path tempInputFile = inputFileTemp.getPath();
|
||||||
Path tempOutputDir = outputDirTemp.getPath();
|
Path tempOutputDir = outputDirTemp.getPath();
|
||||||
|
Path unoOutputFile =
|
||||||
|
tempOutputDir.resolve(
|
||||||
|
pdfBaseName + "." + resolvePrimaryExtension(outputFormat));
|
||||||
|
|
||||||
// Save the uploaded file to a temporary location
|
// Save the uploaded file to a temporary location
|
||||||
inputFile.transferTo(tempInputFile);
|
inputFile.transferTo(tempInputFile);
|
||||||
|
|
||||||
// Run the LibreOffice command
|
// Run the LibreOffice command
|
||||||
List<String> command =
|
ProcessExecutorResult returnCode = null;
|
||||||
new ArrayList<>(
|
IOException unoconvertException = null;
|
||||||
Arrays.asList(
|
|
||||||
"soffice",
|
if (isUnoConvertEnabled()) {
|
||||||
"--headless",
|
try {
|
||||||
"--nologo",
|
List<String> unoCommand =
|
||||||
"--infilter=" + libreOfficeFilter,
|
buildUnoConvertCommand(
|
||||||
"--convert-to",
|
tempInputFile, unoOutputFile, outputFormat, libreOfficeFilter);
|
||||||
outputFormat,
|
returnCode =
|
||||||
"--outdir",
|
ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE)
|
||||||
tempOutputDir.toString(),
|
.runCommandWithOutputHandling(unoCommand);
|
||||||
tempInputFile.toString()));
|
} catch (IOException e) {
|
||||||
ProcessExecutorResult returnCode =
|
unoconvertException = e;
|
||||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE)
|
log.warn(
|
||||||
.runCommandWithOutputHandling(command);
|
"Unoconvert command failed ({}). Falling back to soffice command.",
|
||||||
|
e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (returnCode == null) {
|
||||||
|
// Run the LibreOffice command as a fallback
|
||||||
|
libreOfficeProfile = Files.createTempDirectory("libreoffice_profile_");
|
||||||
|
List<String> command = new ArrayList<>();
|
||||||
|
command.add(runtimePathConfig.getSOfficePath());
|
||||||
|
command.add("-env:UserInstallation=" + libreOfficeProfile.toUri().toString());
|
||||||
|
command.add("--headless");
|
||||||
|
command.add("--nologo");
|
||||||
|
command.add("--infilter=" + libreOfficeFilter);
|
||||||
|
command.add("--convert-to");
|
||||||
|
command.add(outputFormat);
|
||||||
|
command.add("--outdir");
|
||||||
|
command.add(tempOutputDir.toString());
|
||||||
|
command.add(tempInputFile.toString());
|
||||||
|
|
||||||
|
try {
|
||||||
|
returnCode =
|
||||||
|
ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE)
|
||||||
|
.runCommandWithOutputHandling(command);
|
||||||
|
} catch (IOException e) {
|
||||||
|
if (unoconvertException != null) {
|
||||||
|
e.addSuppressed(unoconvertException);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Get output files
|
// Get output files
|
||||||
List<File> outputFiles = Arrays.asList(tempOutputDir.toFile().listFiles());
|
List<File> outputFiles = Arrays.asList(tempOutputDir.toFile().listFiles());
|
||||||
@@ -300,8 +341,42 @@ public class PDFToFile {
|
|||||||
|
|
||||||
fileBytes = byteArrayOutputStream.toByteArray();
|
fileBytes = byteArrayOutputStream.toByteArray();
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
if (libreOfficeProfile != null) {
|
||||||
|
FileUtils.deleteQuietly(libreOfficeProfile.toFile());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return WebResponseUtils.bytesToWebResponse(
|
return WebResponseUtils.bytesToWebResponse(
|
||||||
fileBytes, fileName, MediaType.APPLICATION_OCTET_STREAM);
|
fileBytes, fileName, MediaType.APPLICATION_OCTET_STREAM);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isUnoConvertEnabled() {
|
||||||
|
return runtimePathConfig != null
|
||||||
|
&& runtimePathConfig.getUnoConvertPath() != null
|
||||||
|
&& !runtimePathConfig.getUnoConvertPath().isBlank();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> buildUnoConvertCommand(
|
||||||
|
Path inputFile, Path outputFile, String outputFormat, String libreOfficeFilter) {
|
||||||
|
List<String> command = new ArrayList<>();
|
||||||
|
command.add(runtimePathConfig.getUnoConvertPath());
|
||||||
|
command.add("--port");
|
||||||
|
command.add("2003");
|
||||||
|
command.add("--convert-to");
|
||||||
|
command.add(outputFormat);
|
||||||
|
if (libreOfficeFilter != null && !libreOfficeFilter.isBlank()) {
|
||||||
|
command.add("--input-filter=" + libreOfficeFilter);
|
||||||
|
}
|
||||||
|
command.add(inputFile.toString());
|
||||||
|
command.add(outputFile.toString());
|
||||||
|
return command;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolvePrimaryExtension(String outputFormat) {
|
||||||
|
if (outputFormat == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
int colonIndex = outputFormat.indexOf(':');
|
||||||
|
return colonIndex > 0 ? outputFormat.substring(0, colonIndex) : outputFormat;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -318,7 +318,7 @@ public class PdfAttachmentHandler {
|
|||||||
|
|
||||||
private static String normalizeFilename(String filename) {
|
private static String normalizeFilename(String filename) {
|
||||||
if (filename == null) return "";
|
if (filename == null) return "";
|
||||||
String normalized = filename.toLowerCase().trim();
|
String normalized = filename.toLowerCase(Locale.ROOT).trim();
|
||||||
normalized =
|
normalized =
|
||||||
RegexPatternUtils.getInstance()
|
RegexPatternUtils.getInstance()
|
||||||
.getWhitespacePattern()
|
.getWhitespacePattern()
|
||||||
@@ -560,7 +560,7 @@ public class PdfAttachmentHandler {
|
|||||||
@Override
|
@Override
|
||||||
protected void writeString(String string, List<TextPosition> textPositions)
|
protected void writeString(String string, List<TextPosition> textPositions)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
String lowerString = string.toLowerCase();
|
String lowerString = string.toLowerCase(Locale.ROOT);
|
||||||
|
|
||||||
if (ATTACHMENT_SECTION_PATTERN.matcher(lowerString).find()) {
|
if (ATTACHMENT_SECTION_PATTERN.matcher(lowerString).find()) {
|
||||||
isInAttachmentSection = true;
|
isInAttachmentSection = true;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import java.nio.file.Path;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
import javax.imageio.ImageIO;
|
import javax.imageio.ImageIO;
|
||||||
|
|
||||||
@@ -34,7 +35,7 @@ public class PdfToCbrUtils {
|
|||||||
|
|
||||||
try (PDDocument document = pdfDocumentFactory.load(pdfFile)) {
|
try (PDDocument document = pdfDocumentFactory.load(pdfFile)) {
|
||||||
if (document.getNumberOfPages() == 0) {
|
if (document.getNumberOfPages() == 0) {
|
||||||
throw new IllegalArgumentException("PDF file contains no pages");
|
throw ExceptionUtils.createPdfNoPages();
|
||||||
}
|
}
|
||||||
|
|
||||||
return createCbrFromPdf(document, dpi);
|
return createCbrFromPdf(document, dpi);
|
||||||
@@ -43,17 +44,17 @@ public class PdfToCbrUtils {
|
|||||||
|
|
||||||
private static void validatePdfFile(MultipartFile file) {
|
private static void validatePdfFile(MultipartFile file) {
|
||||||
if (file == null || file.isEmpty()) {
|
if (file == null || file.isEmpty()) {
|
||||||
throw new IllegalArgumentException("File cannot be null or empty");
|
throw ExceptionUtils.createFileNullOrEmptyException();
|
||||||
}
|
}
|
||||||
|
|
||||||
String filename = file.getOriginalFilename();
|
String filename = file.getOriginalFilename();
|
||||||
if (filename == null) {
|
if (filename == null) {
|
||||||
throw new IllegalArgumentException("File must have a name");
|
throw ExceptionUtils.createFileNoNameException();
|
||||||
}
|
}
|
||||||
|
|
||||||
String extension = FilenameUtils.getExtension(filename).toLowerCase();
|
String extension = FilenameUtils.getExtension(filename).toLowerCase(Locale.ROOT);
|
||||||
if (!"pdf".equals(extension)) {
|
if (!"pdf".equals(extension)) {
|
||||||
throw new IllegalArgumentException("File must be a PDF");
|
throw ExceptionUtils.createPdfFileRequiredException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,27 +67,36 @@ public class PdfToCbrUtils {
|
|||||||
int totalPages = document.getNumberOfPages();
|
int totalPages = document.getNumberOfPages();
|
||||||
|
|
||||||
for (int pageIndex = 0; pageIndex < totalPages; pageIndex++) {
|
for (int pageIndex = 0; pageIndex < totalPages; pageIndex++) {
|
||||||
|
final int currentPage = pageIndex;
|
||||||
try {
|
try {
|
||||||
BufferedImage image =
|
BufferedImage image =
|
||||||
pdfRenderer.renderImageWithDPI(pageIndex, dpi, ImageType.RGB);
|
ExceptionUtils.handleOomRendering(
|
||||||
|
currentPage + 1,
|
||||||
|
dpi,
|
||||||
|
() ->
|
||||||
|
pdfRenderer.renderImageWithDPI(
|
||||||
|
currentPage, dpi, ImageType.RGB));
|
||||||
|
|
||||||
String imageFilename = String.format("page_%03d.png", pageIndex + 1);
|
String imageFilename =
|
||||||
|
String.format(Locale.ROOT, "page_%03d.png", currentPage + 1);
|
||||||
Path imagePath = tempDir.resolve(imageFilename);
|
Path imagePath = tempDir.resolve(imageFilename);
|
||||||
|
|
||||||
ImageIO.write(image, "PNG", imagePath.toFile());
|
ImageIO.write(image, "PNG", imagePath.toFile());
|
||||||
generatedImages.add(imagePath);
|
generatedImages.add(imagePath);
|
||||||
|
|
||||||
|
} catch (ExceptionUtils.OutOfMemoryDpiException e) {
|
||||||
|
// Re-throw OOM exceptions without wrapping
|
||||||
|
throw e;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
log.warn("Error processing page {}: {}", pageIndex + 1, e.getMessage());
|
// Wrap other IOExceptions with context
|
||||||
} catch (OutOfMemoryError e) {
|
throw ExceptionUtils.createFileProcessingException(
|
||||||
throw ExceptionUtils.createOutOfMemoryDpiException(pageIndex + 1, dpi, e);
|
"CBR creation for page " + (currentPage + 1), e);
|
||||||
} catch (NegativeArraySizeException e) {
|
|
||||||
throw ExceptionUtils.createOutOfMemoryDpiException(pageIndex + 1, dpi, e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (generatedImages.isEmpty()) {
|
if (generatedImages.isEmpty()) {
|
||||||
throw new IOException("Failed to render any pages to images for CBR conversion");
|
throw ExceptionUtils.createFileProcessingException(
|
||||||
|
"CBR conversion", new IOException("No pages were successfully rendered"));
|
||||||
}
|
}
|
||||||
|
|
||||||
return createRarArchive(tempDir, generatedImages);
|
return createRarArchive(tempDir, generatedImages);
|
||||||
@@ -115,15 +125,18 @@ public class PdfToCbrUtils {
|
|||||||
ProcessExecutorResult result =
|
ProcessExecutorResult result =
|
||||||
executor.runCommandWithOutputHandling(command, tempDir.toFile());
|
executor.runCommandWithOutputHandling(command, tempDir.toFile());
|
||||||
if (result.getRc() != 0) {
|
if (result.getRc() != 0) {
|
||||||
throw new IOException("RAR command failed: " + result.getMessages());
|
throw ExceptionUtils.createFileProcessingException(
|
||||||
|
"RAR archive creation",
|
||||||
|
new IOException("RAR command failed with code " + result.getRc()));
|
||||||
}
|
}
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
throw new IOException("RAR command interrupted", e);
|
throw ExceptionUtils.createProcessingInterruptedException("RAR creation", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Files.exists(rarFile)) {
|
if (!Files.exists(rarFile)) {
|
||||||
throw new IOException("RAR file was not created");
|
throw ExceptionUtils.createFileProcessingException(
|
||||||
|
"RAR archive creation", new IOException("RAR file was not created"));
|
||||||
}
|
}
|
||||||
|
|
||||||
try (FileInputStream fis = new FileInputStream(rarFile.toFile());
|
try (FileInputStream fis = new FileInputStream(rarFile.toFile());
|
||||||
@@ -167,7 +180,7 @@ public class PdfToCbrUtils {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
String extension = FilenameUtils.getExtension(filename).toLowerCase();
|
String extension = FilenameUtils.getExtension(filename).toLowerCase(Locale.ROOT);
|
||||||
return "pdf".equals(extension);
|
return "pdf".equals(extension);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package stirling.software.common.util;
|
|||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipOutputStream;
|
import java.util.zip.ZipOutputStream;
|
||||||
|
|
||||||
@@ -29,7 +30,7 @@ public class PdfToCbzUtils {
|
|||||||
|
|
||||||
try (PDDocument document = pdfDocumentFactory.load(pdfFile)) {
|
try (PDDocument document = pdfDocumentFactory.load(pdfFile)) {
|
||||||
if (document.getNumberOfPages() == 0) {
|
if (document.getNumberOfPages() == 0) {
|
||||||
throw new IllegalArgumentException("PDF file contains no pages");
|
throw ExceptionUtils.createPdfNoPages();
|
||||||
}
|
}
|
||||||
|
|
||||||
return createCbzFromPdf(document, dpi);
|
return createCbzFromPdf(document, dpi);
|
||||||
@@ -38,17 +39,17 @@ public class PdfToCbzUtils {
|
|||||||
|
|
||||||
private static void validatePdfFile(MultipartFile file) {
|
private static void validatePdfFile(MultipartFile file) {
|
||||||
if (file == null || file.isEmpty()) {
|
if (file == null || file.isEmpty()) {
|
||||||
throw new IllegalArgumentException("File cannot be null or empty");
|
throw ExceptionUtils.createFileNullOrEmptyException();
|
||||||
}
|
}
|
||||||
|
|
||||||
String filename = file.getOriginalFilename();
|
String filename = file.getOriginalFilename();
|
||||||
if (filename == null) {
|
if (filename == null) {
|
||||||
throw new IllegalArgumentException("File must have a name");
|
throw ExceptionUtils.createFileNoNameException();
|
||||||
}
|
}
|
||||||
|
|
||||||
String extension = FilenameUtils.getExtension(filename).toLowerCase();
|
String extension = FilenameUtils.getExtension(filename).toLowerCase(Locale.ROOT);
|
||||||
if (!"pdf".equals(extension)) {
|
if (!"pdf".equals(extension)) {
|
||||||
throw new IllegalArgumentException("File must be a PDF");
|
throw ExceptionUtils.createPdfFileRequiredException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,24 +62,31 @@ public class PdfToCbzUtils {
|
|||||||
int totalPages = document.getNumberOfPages();
|
int totalPages = document.getNumberOfPages();
|
||||||
|
|
||||||
for (int pageIndex = 0; pageIndex < totalPages; pageIndex++) {
|
for (int pageIndex = 0; pageIndex < totalPages; pageIndex++) {
|
||||||
|
final int currentPage = pageIndex;
|
||||||
try {
|
try {
|
||||||
BufferedImage image =
|
BufferedImage image =
|
||||||
pdfRenderer.renderImageWithDPI(pageIndex, dpi, ImageType.RGB);
|
ExceptionUtils.handleOomRendering(
|
||||||
|
currentPage + 1,
|
||||||
String imageFilename = String.format("page_%03d.png", pageIndex + 1);
|
dpi,
|
||||||
|
() ->
|
||||||
|
pdfRenderer.renderImageWithDPI(
|
||||||
|
currentPage, dpi, ImageType.RGB));
|
||||||
|
|
||||||
|
String imageFilename =
|
||||||
|
String.format(Locale.ROOT, "page_%03d.png", currentPage + 1);
|
||||||
ZipEntry zipEntry = new ZipEntry(imageFilename);
|
ZipEntry zipEntry = new ZipEntry(imageFilename);
|
||||||
zipOut.putNextEntry(zipEntry);
|
zipOut.putNextEntry(zipEntry);
|
||||||
|
|
||||||
ImageIO.write(image, "PNG", zipOut);
|
ImageIO.write(image, "PNG", zipOut);
|
||||||
zipOut.closeEntry();
|
zipOut.closeEntry();
|
||||||
|
|
||||||
|
} catch (ExceptionUtils.OutOfMemoryDpiException e) {
|
||||||
|
// Re-throw OOM exceptions without wrapping
|
||||||
|
throw e;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
log.warn("Error processing page {}: {}", pageIndex + 1, e.getMessage());
|
// Wrap other IOExceptions with context
|
||||||
} catch (OutOfMemoryError e) {
|
throw ExceptionUtils.createFileProcessingException(
|
||||||
throw ExceptionUtils.createOutOfMemoryDpiException(pageIndex + 1, dpi, e);
|
"CBZ creation for page " + (currentPage + 1), e);
|
||||||
} catch (NegativeArraySizeException e) {
|
|
||||||
throw ExceptionUtils.createOutOfMemoryDpiException(pageIndex + 1, dpi, e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,7 +101,7 @@ public class PdfToCbzUtils {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
String extension = FilenameUtils.getExtension(filename).toLowerCase();
|
String extension = FilenameUtils.getExtension(filename).toLowerCase(Locale.ROOT);
|
||||||
return "pdf".equals(extension);
|
return "pdf".equals(extension);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import java.io.IOException;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
@@ -50,36 +51,18 @@ public class PdfUtils {
|
|||||||
|
|
||||||
public PDRectangle textToPageSize(String size) {
|
public PDRectangle textToPageSize(String size) {
|
||||||
|
|
||||||
switch (size.toUpperCase()) {
|
return switch (size.toUpperCase(Locale.ROOT)) {
|
||||||
case "A0" -> {
|
case "A0" -> PDRectangle.A0;
|
||||||
return PDRectangle.A0;
|
case "A1" -> PDRectangle.A1;
|
||||||
}
|
case "A2" -> PDRectangle.A2;
|
||||||
case "A1" -> {
|
case "A3" -> PDRectangle.A3;
|
||||||
return PDRectangle.A1;
|
case "A4" -> PDRectangle.A4;
|
||||||
}
|
case "A5" -> PDRectangle.A5;
|
||||||
case "A2" -> {
|
case "A6" -> PDRectangle.A6;
|
||||||
return PDRectangle.A2;
|
case "LETTER" -> PDRectangle.LETTER;
|
||||||
}
|
case "LEGAL" -> PDRectangle.LEGAL;
|
||||||
case "A3" -> {
|
|
||||||
return PDRectangle.A3;
|
|
||||||
}
|
|
||||||
case "A4" -> {
|
|
||||||
return PDRectangle.A4;
|
|
||||||
}
|
|
||||||
case "A5" -> {
|
|
||||||
return PDRectangle.A5;
|
|
||||||
}
|
|
||||||
case "A6" -> {
|
|
||||||
return PDRectangle.A6;
|
|
||||||
}
|
|
||||||
case "LETTER" -> {
|
|
||||||
return PDRectangle.LETTER;
|
|
||||||
}
|
|
||||||
case "LEGAL" -> {
|
|
||||||
return PDRectangle.LEGAL;
|
|
||||||
}
|
|
||||||
default -> throw ExceptionUtils.createInvalidPageSizeException(size);
|
default -> throw ExceptionUtils.createInvalidPageSizeException(size);
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<RenderedImage> getAllImages(PDResources resources) throws IOException {
|
public List<RenderedImage> getAllImages(PDResources resources) throws IOException {
|
||||||
@@ -182,8 +165,8 @@ public class PdfUtils {
|
|||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
|
||||||
if (singleImage) {
|
if (singleImage) {
|
||||||
if ("tiff".equals(imageType.toLowerCase())
|
if ("tiff".equals(imageType.toLowerCase(Locale.ROOT))
|
||||||
|| "tif".equals(imageType.toLowerCase())) {
|
|| "tif".equals(imageType.toLowerCase(Locale.ROOT))) {
|
||||||
// Write the images to the output stream as a TIFF with multiple frames
|
// Write the images to the output stream as a TIFF with multiple frames
|
||||||
ImageWriter writer = ImageIO.getImageWritersByFormatName("tiff").next();
|
ImageWriter writer = ImageIO.getImageWritersByFormatName("tiff").next();
|
||||||
ImageWriteParam param = writer.getDefaultWriteParam();
|
ImageWriteParam param = writer.getDefaultWriteParam();
|
||||||
@@ -196,9 +179,20 @@ public class PdfUtils {
|
|||||||
writer.prepareWriteSequence(null);
|
writer.prepareWriteSequence(null);
|
||||||
|
|
||||||
for (int i = 0; i < pageCount; ++i) {
|
for (int i = 0; i < pageCount; ++i) {
|
||||||
|
final int pageIndex = i;
|
||||||
BufferedImage image;
|
BufferedImage image;
|
||||||
try {
|
try {
|
||||||
image = pdfRenderer.renderImageWithDPI(i, DPI, colorType);
|
// Validate dimensions before rendering
|
||||||
|
ExceptionUtils.validateRenderingDimensions(
|
||||||
|
document.getPage(pageIndex), pageIndex + 1, DPI);
|
||||||
|
|
||||||
|
image =
|
||||||
|
ExceptionUtils.handleOomRendering(
|
||||||
|
pageIndex + 1,
|
||||||
|
DPI,
|
||||||
|
() ->
|
||||||
|
pdfRenderer.renderImageWithDPI(
|
||||||
|
pageIndex, DPI, colorType));
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
if (e.getMessage() != null
|
if (e.getMessage() != null
|
||||||
&& e.getMessage()
|
&& e.getMessage()
|
||||||
@@ -212,10 +206,6 @@ public class PdfUtils {
|
|||||||
DPI);
|
DPI);
|
||||||
}
|
}
|
||||||
throw e;
|
throw e;
|
||||||
} catch (OutOfMemoryError e) {
|
|
||||||
throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, DPI, e);
|
|
||||||
} catch (NegativeArraySizeException e) {
|
|
||||||
throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, DPI, e);
|
|
||||||
}
|
}
|
||||||
writer.writeToSequence(new IIOImage(image, null, null), param);
|
writer.writeToSequence(new IIOImage(image, null, null), param);
|
||||||
}
|
}
|
||||||
@@ -239,6 +229,7 @@ public class PdfUtils {
|
|||||||
HashMap<PdfRenderSettingsKey, PdfImageDimensionValue> pageSizes =
|
HashMap<PdfRenderSettingsKey, PdfImageDimensionValue> pageSizes =
|
||||||
new HashMap<>();
|
new HashMap<>();
|
||||||
for (int i = 0; i < pageCount; ++i) {
|
for (int i = 0; i < pageCount; ++i) {
|
||||||
|
final int pageIndex = i;
|
||||||
PDPage page = document.getPage(i);
|
PDPage page = document.getPage(i);
|
||||||
PDRectangle mediaBox = page.getMediaBox();
|
PDRectangle mediaBox = page.getMediaBox();
|
||||||
int rotation = page.getRotation();
|
int rotation = page.getRotation();
|
||||||
@@ -249,7 +240,17 @@ public class PdfUtils {
|
|||||||
if (dimension == null) {
|
if (dimension == null) {
|
||||||
// Render the image to get the dimensions
|
// Render the image to get the dimensions
|
||||||
try {
|
try {
|
||||||
pdfSizeImage = pdfRenderer.renderImageWithDPI(i, DPI, colorType);
|
// Validate dimensions before rendering
|
||||||
|
ExceptionUtils.validateRenderingDimensions(
|
||||||
|
page, pageIndex + 1, DPI);
|
||||||
|
|
||||||
|
pdfSizeImage =
|
||||||
|
ExceptionUtils.handleOomRendering(
|
||||||
|
pageIndex + 1,
|
||||||
|
DPI,
|
||||||
|
() ->
|
||||||
|
pdfRenderer.renderImageWithDPI(
|
||||||
|
pageIndex, DPI, colorType));
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
if (e.getMessage() != null
|
if (e.getMessage() != null
|
||||||
&& e.getMessage()
|
&& e.getMessage()
|
||||||
@@ -264,10 +265,6 @@ public class PdfUtils {
|
|||||||
DPI);
|
DPI);
|
||||||
}
|
}
|
||||||
throw e;
|
throw e;
|
||||||
} catch (OutOfMemoryError e) {
|
|
||||||
throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, DPI, e);
|
|
||||||
} catch (NegativeArraySizeException e) {
|
|
||||||
throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, DPI, e);
|
|
||||||
}
|
}
|
||||||
pdfSizeImageIndex = i;
|
pdfSizeImageIndex = i;
|
||||||
dimension =
|
dimension =
|
||||||
@@ -293,11 +290,22 @@ public class PdfUtils {
|
|||||||
boolean firstImageAlreadyRendered = pdfSizeImageIndex == 0;
|
boolean firstImageAlreadyRendered = pdfSizeImageIndex == 0;
|
||||||
|
|
||||||
for (int i = 0; i < pageCount; ++i) {
|
for (int i = 0; i < pageCount; ++i) {
|
||||||
|
final int pageIndex = i;
|
||||||
if (firstImageAlreadyRendered && i == 0) {
|
if (firstImageAlreadyRendered && i == 0) {
|
||||||
pageImage = pdfSizeImage;
|
pageImage = pdfSizeImage;
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
pageImage = pdfRenderer.renderImageWithDPI(i, DPI, colorType);
|
// Validate dimensions before rendering
|
||||||
|
ExceptionUtils.validateRenderingDimensions(
|
||||||
|
document.getPage(pageIndex), pageIndex + 1, DPI);
|
||||||
|
|
||||||
|
pageImage =
|
||||||
|
ExceptionUtils.handleOomRendering(
|
||||||
|
pageIndex + 1,
|
||||||
|
DPI,
|
||||||
|
() ->
|
||||||
|
pdfRenderer.renderImageWithDPI(
|
||||||
|
pageIndex, DPI, colorType));
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
if (e.getMessage() != null
|
if (e.getMessage() != null
|
||||||
&& e.getMessage()
|
&& e.getMessage()
|
||||||
@@ -311,10 +319,6 @@ public class PdfUtils {
|
|||||||
DPI);
|
DPI);
|
||||||
}
|
}
|
||||||
throw e;
|
throw e;
|
||||||
} catch (OutOfMemoryError e) {
|
|
||||||
throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, DPI, e);
|
|
||||||
} catch (NegativeArraySizeException e) {
|
|
||||||
throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, DPI, e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,9 +339,20 @@ public class PdfUtils {
|
|||||||
// Zip the images and return as byte array
|
// Zip the images and return as byte array
|
||||||
try (ZipOutputStream zos = new ZipOutputStream(baos)) {
|
try (ZipOutputStream zos = new ZipOutputStream(baos)) {
|
||||||
for (int i = 0; i < pageCount; ++i) {
|
for (int i = 0; i < pageCount; ++i) {
|
||||||
|
final int pageIndex = i;
|
||||||
BufferedImage image;
|
BufferedImage image;
|
||||||
try {
|
try {
|
||||||
image = pdfRenderer.renderImageWithDPI(i, DPI, colorType);
|
// Validate dimensions before rendering
|
||||||
|
ExceptionUtils.validateRenderingDimensions(
|
||||||
|
document.getPage(pageIndex), pageIndex + 1, DPI);
|
||||||
|
|
||||||
|
image =
|
||||||
|
ExceptionUtils.handleOomRendering(
|
||||||
|
pageIndex + 1,
|
||||||
|
DPI,
|
||||||
|
() ->
|
||||||
|
pdfRenderer.renderImageWithDPI(
|
||||||
|
pageIndex, DPI, colorType));
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
if (e.getMessage() != null
|
if (e.getMessage() != null
|
||||||
&& e.getMessage().contains("Maximum size of image exceeded")) {
|
&& e.getMessage().contains("Maximum size of image exceeded")) {
|
||||||
@@ -349,10 +364,6 @@ public class PdfUtils {
|
|||||||
DPI);
|
DPI);
|
||||||
}
|
}
|
||||||
throw e;
|
throw e;
|
||||||
} catch (OutOfMemoryError e) {
|
|
||||||
throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, DPI, e);
|
|
||||||
} catch (NegativeArraySizeException e) {
|
|
||||||
throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, DPI, e);
|
|
||||||
}
|
}
|
||||||
try (ByteArrayOutputStream baosImage = new ByteArrayOutputStream()) {
|
try (ByteArrayOutputStream baosImage = new ByteArrayOutputStream()) {
|
||||||
ImageIO.write(image, imageType, baosImage);
|
ImageIO.write(image, imageType, baosImage);
|
||||||
@@ -361,9 +372,10 @@ public class PdfUtils {
|
|||||||
zos.putNextEntry(
|
zos.putNextEntry(
|
||||||
new ZipEntry(
|
new ZipEntry(
|
||||||
String.format(
|
String.format(
|
||||||
|
Locale.ROOT,
|
||||||
filename + "_%d.%s",
|
filename + "_%d.%s",
|
||||||
i + 1,
|
i + 1,
|
||||||
imageType.toLowerCase())));
|
imageType.toLowerCase(Locale.ROOT))));
|
||||||
zos.write(baosImage.toByteArray());
|
zos.write(baosImage.toByteArray());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -391,6 +403,7 @@ public class PdfUtils {
|
|||||||
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
||||||
pdfRenderer.setSubsamplingAllowed(true);
|
pdfRenderer.setSubsamplingAllowed(true);
|
||||||
for (int page = 0; page < document.getNumberOfPages(); ++page) {
|
for (int page = 0; page < document.getNumberOfPages(); ++page) {
|
||||||
|
final int pageIndex = page;
|
||||||
BufferedImage bim;
|
BufferedImage bim;
|
||||||
|
|
||||||
// Use global maximum DPI setting, fallback to 300 if not set
|
// Use global maximum DPI setting, fallback to 300 if not set
|
||||||
@@ -400,9 +413,16 @@ public class PdfUtils {
|
|||||||
if (properties != null && properties.getSystem() != null) {
|
if (properties != null && properties.getSystem() != null) {
|
||||||
renderDpi = properties.getSystem().getMaxDPI();
|
renderDpi = properties.getSystem().getMaxDPI();
|
||||||
}
|
}
|
||||||
|
final int dpi = renderDpi;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
bim = pdfRenderer.renderImageWithDPI(page, renderDpi, ImageType.RGB);
|
bim =
|
||||||
|
ExceptionUtils.handleOomRendering(
|
||||||
|
pageIndex + 1,
|
||||||
|
dpi,
|
||||||
|
() ->
|
||||||
|
pdfRenderer.renderImageWithDPI(
|
||||||
|
pageIndex, dpi, ImageType.RGB));
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
if (e.getMessage() != null
|
if (e.getMessage() != null
|
||||||
&& e.getMessage().contains("Maximum size of image exceeded")) {
|
&& e.getMessage().contains("Maximum size of image exceeded")) {
|
||||||
@@ -411,13 +431,9 @@ public class PdfUtils {
|
|||||||
"PDF page {0} is too large to render at 300 DPI. The resulting image"
|
"PDF page {0} is too large to render at 300 DPI. The resulting image"
|
||||||
+ " would exceed Java's maximum array size. Please use a lower DPI"
|
+ " would exceed Java's maximum array size. Please use a lower DPI"
|
||||||
+ " value for PDF-to-image conversion.",
|
+ " value for PDF-to-image conversion.",
|
||||||
page + 1);
|
pageIndex + 1);
|
||||||
}
|
}
|
||||||
throw e;
|
throw e;
|
||||||
} catch (OutOfMemoryError e) {
|
|
||||||
throw ExceptionUtils.createOutOfMemoryDpiException(page + 1, 300, e);
|
|
||||||
} catch (NegativeArraySizeException e) {
|
|
||||||
throw ExceptionUtils.createOutOfMemoryDpiException(page + 1, 300, e);
|
|
||||||
}
|
}
|
||||||
PDPage originalPage = document.getPage(page);
|
PDPage originalPage = document.getPage(page);
|
||||||
|
|
||||||
@@ -463,8 +479,8 @@ public class PdfUtils {
|
|||||||
String contentType = file.getContentType();
|
String contentType = file.getContentType();
|
||||||
String originalFilename = Filenames.toSimpleFileName(file.getOriginalFilename());
|
String originalFilename = Filenames.toSimpleFileName(file.getOriginalFilename());
|
||||||
if (originalFilename != null
|
if (originalFilename != null
|
||||||
&& (originalFilename.toLowerCase().endsWith(".tiff")
|
&& (originalFilename.toLowerCase(Locale.ROOT).endsWith(".tiff")
|
||||||
|| originalFilename.toLowerCase().endsWith(".tif"))) {
|
|| originalFilename.toLowerCase(Locale.ROOT).endsWith(".tif"))) {
|
||||||
ImageReader reader = ImageIO.getImageReadersByFormatName("tiff").next();
|
ImageReader reader = ImageIO.getImageReadersByFormatName("tiff").next();
|
||||||
reader.setInput(ImageIO.createImageInputStream(file.getInputStream()));
|
reader.setInput(ImageIO.createImageInputStream(file.getInputStream()));
|
||||||
int numPages = reader.getNumImages(true);
|
int numPages = reader.getNumImages(true);
|
||||||
@@ -567,7 +583,7 @@ public class PdfUtils {
|
|||||||
PDImageXObject image = PDImageXObject.createFromByteArray(document, imageBytes, "");
|
PDImageXObject image = PDImageXObject.createFromByteArray(document, imageBytes, "");
|
||||||
// Draw the image onto the page at the specified x and y coordinates
|
// Draw the image onto the page at the specified x and y coordinates
|
||||||
contentStream.drawImage(image, x, y);
|
contentStream.drawImage(image, x, y);
|
||||||
log.info("Image successfully overlayed onto PDF");
|
log.info("Image successfully overlaid onto PDF");
|
||||||
if (!everyPage && i == 0) {
|
if (!everyPage && i == 0) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -631,7 +647,7 @@ public class PdfUtils {
|
|||||||
int actualPageCount = pdfDocument.getNumberOfPages();
|
int actualPageCount = pdfDocument.getNumberOfPages();
|
||||||
pdfDocument.close();
|
pdfDocument.close();
|
||||||
|
|
||||||
return switch (comparator.toLowerCase()) {
|
return switch (comparator.toLowerCase(Locale.ROOT)) {
|
||||||
case "greater" -> actualPageCount > pageCount;
|
case "greater" -> actualPageCount > pageCount;
|
||||||
case "equal" -> actualPageCount == pageCount;
|
case "equal" -> actualPageCount == pageCount;
|
||||||
case "less" -> actualPageCount < pageCount;
|
case "less" -> actualPageCount < pageCount;
|
||||||
|
|||||||
@@ -102,6 +102,11 @@ public class ProcessExecutor {
|
|||||||
.getSessionLimit()
|
.getSessionLimit()
|
||||||
.getOcrMyPdfSessionLimit();
|
.getOcrMyPdfSessionLimit();
|
||||||
case CFF_CONVERTER -> 1;
|
case CFF_CONVERTER -> 1;
|
||||||
|
case FFMPEG ->
|
||||||
|
applicationProperties
|
||||||
|
.getProcessExecutor()
|
||||||
|
.getSessionLimit()
|
||||||
|
.getFfmpegSessionLimit();
|
||||||
};
|
};
|
||||||
|
|
||||||
long timeoutMinutes =
|
long timeoutMinutes =
|
||||||
@@ -162,6 +167,11 @@ public class ProcessExecutor {
|
|||||||
.getTimeoutMinutes()
|
.getTimeoutMinutes()
|
||||||
.getOcrMyPdfTimeoutMinutes();
|
.getOcrMyPdfTimeoutMinutes();
|
||||||
case CFF_CONVERTER -> 5L;
|
case CFF_CONVERTER -> 5L;
|
||||||
|
case FFMPEG ->
|
||||||
|
applicationProperties
|
||||||
|
.getProcessExecutor()
|
||||||
|
.getTimeoutMinutes()
|
||||||
|
.getFfmpegTimeoutMinutes();
|
||||||
};
|
};
|
||||||
return new ProcessExecutor(semaphoreLimit, liveUpdates, timeoutMinutes);
|
return new ProcessExecutor(semaphoreLimit, liveUpdates, timeoutMinutes);
|
||||||
});
|
});
|
||||||
@@ -316,7 +326,8 @@ public class ProcessExecutor {
|
|||||||
QPDF,
|
QPDF,
|
||||||
GHOSTSCRIPT,
|
GHOSTSCRIPT,
|
||||||
OCR_MY_PDF,
|
OCR_MY_PDF,
|
||||||
CFF_CONVERTER
|
CFF_CONVERTER,
|
||||||
|
FFMPEG
|
||||||
}
|
}
|
||||||
|
|
||||||
@Setter
|
@Setter
|
||||||
|
|||||||
@@ -325,6 +325,11 @@ public final class RegexPatternUtils {
|
|||||||
return getPattern("\\+");
|
return getPattern("\\+");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Pattern for splitting on pipe delimiter (used for hint lists in i18n messages) */
|
||||||
|
public Pattern getPipeDelimiterPattern() {
|
||||||
|
return getPattern("\\|");
|
||||||
|
}
|
||||||
|
|
||||||
/** Pattern for username validation */
|
/** Pattern for username validation */
|
||||||
public Pattern getUsernameValidationPattern() {
|
public Pattern getUsernameValidationPattern() {
|
||||||
return getPattern("^[a-zA-Z0-9](?!.*[-@._+]{2,})[a-zA-Z0-9@._+-]{1,48}[a-zA-Z0-9]$");
|
return getPattern("^[a-zA-Z0-9](?!.*[-@._+]{2,})[a-zA-Z0-9@._+-]{1,48}[a-zA-Z0-9]$");
|
||||||
@@ -567,6 +572,16 @@ public final class RegexPatternUtils {
|
|||||||
return getPattern("^[a-zA-Z0-9]{2,4}$", Pattern.CASE_INSENSITIVE);
|
return getPattern("^[a-zA-Z0-9]{2,4}$", Pattern.CASE_INSENSITIVE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Pattern for splitting on line breaks (Unicode line separator) */
|
||||||
|
public Pattern getLineSeparatorPattern() {
|
||||||
|
return getPattern("\\R");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pattern for removing leading asterisks and whitespace */
|
||||||
|
public Pattern getLeadingAsterisksWhitespacePattern() {
|
||||||
|
return getPattern("^[*\\s]+");
|
||||||
|
}
|
||||||
|
|
||||||
private record PatternKey(String regex, int flags) {
|
private record PatternKey(String regex, int flags) {
|
||||||
// Record automatically provides equals, hashCode, and toString
|
// Record automatically provides equals, hashCode, and toString
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,17 @@ public class TempFileManager {
|
|||||||
private final TempFileRegistry registry;
|
private final TempFileRegistry registry;
|
||||||
private final ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a managed temporary file that will be tracked by the TempFileManager.
|
||||||
|
*
|
||||||
|
* @param suffix The suffix for the temporary file
|
||||||
|
* @return The created temporary file wrapper
|
||||||
|
* @throws IOException If an I/O error occurs
|
||||||
|
*/
|
||||||
|
public TempFile createManagedTempFile(String suffix) throws IOException {
|
||||||
|
return new TempFile(this, suffix);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a temporary file with the Stirling-PDF prefix. The file is automatically registered
|
* Create a temporary file with the Stirling-PDF prefix. The file is automatically registered
|
||||||
* with the registry.
|
* with the registry.
|
||||||
@@ -130,7 +141,6 @@ public class TempFileManager {
|
|||||||
return deleted;
|
return deleted;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
log.warn("Failed to delete temp file: {}", path.toString(), e);
|
log.warn("Failed to delete temp file: {}", path.toString(), e);
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
package stirling.software.common.util;
|
|
||||||
|
|
||||||
import java.util.Collection;
|
|
||||||
|
|
||||||
public class ValidationUtil {
|
|
||||||
|
|
||||||
public static boolean isStringEmpty(String input) {
|
|
||||||
return input == null || input.isBlank();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isCollectionEmpty(Collection<String> input) {
|
|
||||||
return input == null || input.isEmpty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+7
-9
@@ -56,16 +56,14 @@ public class InvertFullColorStrategy extends ReplaceAndInvertColorStrategy {
|
|||||||
if (properties != null && properties.getSystem() != null) {
|
if (properties != null && properties.getSystem() != null) {
|
||||||
renderDpi = properties.getSystem().getMaxDPI();
|
renderDpi = properties.getSystem().getMaxDPI();
|
||||||
}
|
}
|
||||||
|
final int dpi = renderDpi;
|
||||||
|
final int pageNum = page;
|
||||||
|
|
||||||
try {
|
image =
|
||||||
image =
|
ExceptionUtils.handleOomRendering(
|
||||||
pdfRenderer.renderImageWithDPI(
|
pageNum + 1,
|
||||||
page, renderDpi); // Render page with global DPI setting
|
dpi,
|
||||||
} catch (OutOfMemoryError e) {
|
() -> pdfRenderer.renderImageWithDPI(pageNum, dpi));
|
||||||
throw ExceptionUtils.createOutOfMemoryDpiException(page + 1, renderDpi, e);
|
|
||||||
} catch (NegativeArraySizeException e) {
|
|
||||||
throw ExceptionUtils.createOutOfMemoryDpiException(page + 1, renderDpi, e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Invert the colors
|
// Invert the colors
|
||||||
invertImageColors(image);
|
invertImageColors(image);
|
||||||
|
|||||||
+3
-11
@@ -1,8 +1,6 @@
|
|||||||
package stirling.software.common.annotations;
|
package stirling.software.common.annotations;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.anyBoolean;
|
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||||
import static org.mockito.ArgumentMatchers.anyInt;
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
@@ -31,8 +29,6 @@ import stirling.software.common.aop.AutoJobAspect;
|
|||||||
import stirling.software.common.model.api.PDFFile;
|
import stirling.software.common.model.api.PDFFile;
|
||||||
import stirling.software.common.service.FileStorage;
|
import stirling.software.common.service.FileStorage;
|
||||||
import stirling.software.common.service.JobExecutorService;
|
import stirling.software.common.service.JobExecutorService;
|
||||||
import stirling.software.common.service.JobQueue;
|
|
||||||
import stirling.software.common.service.ResourceMonitor;
|
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
class AutoJobPostMappingIntegrationTest {
|
class AutoJobPostMappingIntegrationTest {
|
||||||
@@ -45,10 +41,6 @@ class AutoJobPostMappingIntegrationTest {
|
|||||||
|
|
||||||
@Mock private FileStorage fileStorage;
|
@Mock private FileStorage fileStorage;
|
||||||
|
|
||||||
@Mock private ResourceMonitor resourceMonitor;
|
|
||||||
|
|
||||||
@Mock private JobQueue jobQueue;
|
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
autoJobAspect = new AutoJobAspect(jobExecutorService, request, fileStorage);
|
autoJobAspect = new AutoJobAspect(jobExecutorService, request, fileStorage);
|
||||||
@@ -73,7 +65,7 @@ class AutoJobPostMappingIntegrationTest {
|
|||||||
// Given
|
// Given
|
||||||
PDFFile pdfFile = new PDFFile();
|
PDFFile pdfFile = new PDFFile();
|
||||||
pdfFile.setFileId("test-file-id");
|
pdfFile.setFileId("test-file-id");
|
||||||
Object[] args = new Object[] {pdfFile};
|
Object[] args = {pdfFile};
|
||||||
|
|
||||||
when(joinPoint.getArgs()).thenReturn(args);
|
when(joinPoint.getArgs()).thenReturn(args);
|
||||||
when(request.getParameter("async")).thenReturn("true");
|
when(request.getParameter("async")).thenReturn("true");
|
||||||
@@ -153,7 +145,7 @@ class AutoJobPostMappingIntegrationTest {
|
|||||||
// Given
|
// Given
|
||||||
PDFFile pdfFile = new PDFFile();
|
PDFFile pdfFile = new PDFFile();
|
||||||
pdfFile.setFileInput(mock(MultipartFile.class));
|
pdfFile.setFileInput(mock(MultipartFile.class));
|
||||||
Object[] args = new Object[] {pdfFile};
|
Object[] args = {pdfFile};
|
||||||
|
|
||||||
when(joinPoint.getArgs()).thenReturn(args);
|
when(joinPoint.getArgs()).thenReturn(args);
|
||||||
when(request.getParameter("async")).thenReturn("true");
|
when(request.getParameter("async")).thenReturn("true");
|
||||||
|
|||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
package stirling.software.common.configuration.interfaces;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class ShowAdminInterfaceTest {
|
||||||
|
|
||||||
|
// Create a simple implementation for testing
|
||||||
|
static class TestImpl implements ShowAdminInterface {}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getShowUpdateOnlyAdmins_returnsTrueByDefault() {
|
||||||
|
ShowAdminInterface instance = new TestImpl();
|
||||||
|
assertTrue(instance.getShowUpdateOnlyAdmins(), "Default should return true");
|
||||||
|
}
|
||||||
|
}
|
||||||
+9
-9
@@ -18,17 +18,17 @@ class ApplicationPropertiesDynamicYamlPropertySourceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void loads_yaml_into_environment() throws Exception {
|
void loads_yaml_into_environment() throws Exception {
|
||||||
// YAML-Config in Temp-Datei schreiben
|
|
||||||
String yaml =
|
String yaml =
|
||||||
""
|
"""
|
||||||
+ "ui:\n"
|
\
|
||||||
+ " appName: \"My App\"\n"
|
ui:
|
||||||
+ "system:\n"
|
appName: "My App"
|
||||||
+ " enableAnalytics: true\n";
|
system:
|
||||||
|
enableAnalytics: true
|
||||||
|
""";
|
||||||
Path tmp = Files.createTempFile("spdf-settings-", ".yml");
|
Path tmp = Files.createTempFile("spdf-settings-", ".yml");
|
||||||
Files.writeString(tmp, yaml);
|
Files.writeString(tmp, yaml);
|
||||||
|
|
||||||
// Pfad per statischem Mock liefern
|
|
||||||
try (MockedStatic<InstallationPathConfig> mocked =
|
try (MockedStatic<InstallationPathConfig> mocked =
|
||||||
Mockito.mockStatic(InstallationPathConfig.class)) {
|
Mockito.mockStatic(InstallationPathConfig.class)) {
|
||||||
mocked.when(InstallationPathConfig::getSettingsPath).thenReturn(tmp.toString());
|
mocked.when(InstallationPathConfig::getSettingsPath).thenReturn(tmp.toString());
|
||||||
@@ -36,7 +36,7 @@ class ApplicationPropertiesDynamicYamlPropertySourceTest {
|
|||||||
ConfigurableEnvironment env = new StandardEnvironment();
|
ConfigurableEnvironment env = new StandardEnvironment();
|
||||||
ApplicationProperties props = new ApplicationProperties();
|
ApplicationProperties props = new ApplicationProperties();
|
||||||
|
|
||||||
props.dynamicYamlPropertySource(env); // fügt PropertySource an erster Stelle ein
|
props.dynamicYamlPropertySource(env);
|
||||||
|
|
||||||
assertEquals("My App", env.getProperty("ui.appName"));
|
assertEquals("My App", env.getProperty("ui.appName"));
|
||||||
assertEquals("true", env.getProperty("system.enableAnalytics"));
|
assertEquals("true", env.getProperty("system.enableAnalytics"));
|
||||||
@@ -44,7 +44,7 @@ class ApplicationPropertiesDynamicYamlPropertySourceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void throws_when_settings_file_missing() throws Exception {
|
void throws_when_settings_file_missing() {
|
||||||
String missing = "/path/does/not/exist/spdf.yml";
|
String missing = "/path/does/not/exist/spdf.yml";
|
||||||
try (MockedStatic<InstallationPathConfig> mocked =
|
try (MockedStatic<InstallationPathConfig> mocked =
|
||||||
Mockito.mockStatic(InstallationPathConfig.class)) {
|
Mockito.mockStatic(InstallationPathConfig.class)) {
|
||||||
|
|||||||
+2
-2
@@ -61,7 +61,7 @@ class ApplicationPropertiesSaml2HttpTest {
|
|||||||
Resource r = s.getSpCert();
|
Resource r = s.getSpCert();
|
||||||
|
|
||||||
assertNotNull(r);
|
assertNotNull(r);
|
||||||
assertTrue(r instanceof FileSystemResource, "Expected FileSystemResource for FS path");
|
assertInstanceOf(FileSystemResource.class, r, "Expected FileSystemResource for FS path");
|
||||||
assertTrue(r.exists(), "Temp file should exist");
|
assertTrue(r.exists(), "Temp file should exist");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ class ApplicationPropertiesSaml2HttpTest {
|
|||||||
Resource r = s.getIdpCert();
|
Resource r = s.getIdpCert();
|
||||||
|
|
||||||
assertNotNull(r);
|
assertNotNull(r);
|
||||||
assertTrue(r instanceof FileSystemResource, "Expected FileSystemResource for FS path");
|
assertInstanceOf(FileSystemResource.class, r, "Expected FileSystemResource for FS path");
|
||||||
assertFalse(r.exists(), "Resource should not exist for missing file");
|
assertFalse(r.exists(), "Resource should not exist for missing file");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
package stirling.software.common.model;
|
package stirling.software.common.model;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
import java.io.File;
|
import java.nio.file.Path;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.params.ParameterizedTest;
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
import org.junit.jupiter.params.provider.CsvSource;
|
import org.junit.jupiter.params.provider.CsvSource;
|
||||||
|
|
||||||
public class FileInfoTest {
|
public class FileInfoTest {
|
||||||
|
|
||||||
|
private static final LocalDateTime FIXED_NOW = LocalDateTime.of(2025, 11, 1, 12, 0, 0);
|
||||||
|
|
||||||
@ParameterizedTest(name = "{index}: fileSize={0}")
|
@ParameterizedTest(name = "{index}: fileSize={0}")
|
||||||
@CsvSource({
|
@CsvSource({
|
||||||
"0, '0 Bytes'",
|
"0, '0 Bytes'",
|
||||||
@@ -25,87 +29,146 @@ public class FileInfoTest {
|
|||||||
FileInfo fileInfo =
|
FileInfo fileInfo =
|
||||||
new FileInfo(
|
new FileInfo(
|
||||||
"example.txt",
|
"example.txt",
|
||||||
File.separator
|
"/path/to/example.txt",
|
||||||
+ "path"
|
FIXED_NOW,
|
||||||
+ File.separator
|
|
||||||
+ "to"
|
|
||||||
+ File.separator
|
|
||||||
+ "example.txt",
|
|
||||||
LocalDateTime.now(),
|
|
||||||
fileSize,
|
fileSize,
|
||||||
LocalDateTime.now().minusDays(1));
|
FIXED_NOW.minusDays(1));
|
||||||
|
|
||||||
assertEquals(expectedFormattedSize, fileInfo.getFormattedFileSize());
|
assertEquals(expectedFormattedSize, fileInfo.getFormattedFileSize());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Nested
|
||||||
void testGetFilePathAsPath() {
|
@DisplayName("getFilePathAsPath")
|
||||||
FileInfo fileInfo =
|
class GetFilePathAsPathTests {
|
||||||
new FileInfo(
|
@Test
|
||||||
"test.pdf",
|
@DisplayName("Should convert filePath string into a Path instance")
|
||||||
File.separator + "tmp" + File.separator + "test.pdf",
|
void shouldConvertStringToPath() {
|
||||||
LocalDateTime.now(),
|
FileInfo fi =
|
||||||
1234,
|
new FileInfo(
|
||||||
LocalDateTime.now().minusDays(2));
|
"example.txt",
|
||||||
assertEquals(
|
"/path/to/example.txt",
|
||||||
File.separator + "tmp" + File.separator + "test.pdf",
|
FIXED_NOW,
|
||||||
fileInfo.getFilePathAsPath().toString());
|
123,
|
||||||
|
FIXED_NOW.minusDays(1));
|
||||||
|
|
||||||
|
Path path = fi.getFilePathAsPath();
|
||||||
|
|
||||||
|
// Basic sanity checks
|
||||||
|
assertNotNull(path, "Path should not be null");
|
||||||
|
assertEquals(
|
||||||
|
Path.of("/path/to/example.txt"),
|
||||||
|
path,
|
||||||
|
"Converted Path should match input string");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Nested
|
||||||
void testGetFormattedModificationDate() {
|
@DisplayName("Date formatting")
|
||||||
LocalDateTime modDate = LocalDateTime.of(2024, 6, 1, 15, 30, 45);
|
class DateFormattingTests {
|
||||||
FileInfo fileInfo =
|
@Test
|
||||||
new FileInfo(
|
@DisplayName("Should format modificationDate as 'yyyy-MM-dd HH:mm:ss'")
|
||||||
"file.txt",
|
void shouldFormatModificationDate() {
|
||||||
File.separator + "file.txt",
|
LocalDateTime mod = LocalDateTime.of(2025, 8, 10, 15, 30, 45);
|
||||||
modDate,
|
FileInfo fi =
|
||||||
100,
|
new FileInfo(
|
||||||
LocalDateTime.of(2024, 5, 31, 10, 0, 0));
|
"example.txt",
|
||||||
assertEquals("2024-06-01 15:30:45", fileInfo.getFormattedModificationDate());
|
"/path/to/example.txt",
|
||||||
|
mod,
|
||||||
|
1,
|
||||||
|
LocalDateTime.of(2024, 1, 1, 0, 0, 0));
|
||||||
|
|
||||||
|
assertEquals("2025-08-10 15:30:45", fi.getFormattedModificationDate());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should format creationDate as 'yyyy-MM-dd HH:mm:ss'")
|
||||||
|
void shouldFormatCreationDate() {
|
||||||
|
LocalDateTime created = LocalDateTime.of(2024, 12, 31, 23, 59, 59);
|
||||||
|
FileInfo fi =
|
||||||
|
new FileInfo(
|
||||||
|
"example.txt",
|
||||||
|
"/path/to/example.txt",
|
||||||
|
LocalDateTime.of(2025, 1, 1, 0, 0, 0),
|
||||||
|
1,
|
||||||
|
created);
|
||||||
|
|
||||||
|
assertEquals("2024-12-31 23:59:59", fi.getFormattedCreationDate());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should throw NPE when modificationDate is null (current behavior)")
|
||||||
|
void shouldThrowWhenModificationDateNull() {
|
||||||
|
// Assumption: Current implementation does not guard null -> NPE is expected.
|
||||||
|
FileInfo fi =
|
||||||
|
new FileInfo(
|
||||||
|
"example.txt",
|
||||||
|
"/path/to/example.txt",
|
||||||
|
null, // modificationDate null
|
||||||
|
1,
|
||||||
|
FIXED_NOW);
|
||||||
|
|
||||||
|
assertThrows(
|
||||||
|
NullPointerException.class,
|
||||||
|
fi::getFormattedModificationDate,
|
||||||
|
"Formatting a null modificationDate should throw NPE with current"
|
||||||
|
+ " implementation");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should throw NPE when creationDate is null (current behavior)")
|
||||||
|
void shouldThrowWhenCreationDateNull() {
|
||||||
|
// Assumption: Current implementation does not guard null -> NPE is expected.
|
||||||
|
FileInfo fi =
|
||||||
|
new FileInfo(
|
||||||
|
"example.txt",
|
||||||
|
"/path/to/example.txt",
|
||||||
|
FIXED_NOW,
|
||||||
|
1,
|
||||||
|
null); // creationDate null
|
||||||
|
|
||||||
|
assertThrows(
|
||||||
|
NullPointerException.class,
|
||||||
|
fi::getFormattedCreationDate,
|
||||||
|
"Formatting a null creationDate should throw NPE with current implementation");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Nested
|
||||||
void testGetFormattedCreationDate() {
|
@DisplayName("Additional size formatting cases")
|
||||||
LocalDateTime creationDate = LocalDateTime.of(2023, 12, 25, 8, 15, 0);
|
class AdditionalSizeFormattingTests {
|
||||||
FileInfo fileInfo =
|
|
||||||
new FileInfo(
|
|
||||||
"holiday.txt",
|
|
||||||
File.separator + "holiday.txt",
|
|
||||||
LocalDateTime.of(2024, 1, 1, 0, 0, 0),
|
|
||||||
500,
|
|
||||||
creationDate);
|
|
||||||
assertEquals("2023-12-25 08:15:00", fileInfo.getFormattedCreationDate());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testGettersAndSetters() {
|
@DisplayName("Should round to two decimals for KB (e.g., 1536 B -> 1.50 KB)")
|
||||||
LocalDateTime now = LocalDateTime.now();
|
void shouldRoundKbToTwoDecimals() {
|
||||||
FileInfo fileInfo =
|
FileInfo fi =
|
||||||
new FileInfo(
|
new FileInfo(
|
||||||
"doc.pdf",
|
"example.txt",
|
||||||
File.separator + "docs" + File.separator + "doc.pdf",
|
"/path/to/example.txt",
|
||||||
now,
|
FIXED_NOW,
|
||||||
2048,
|
1536, // 1.5 KB
|
||||||
now.minusDays(1));
|
FIXED_NOW.minusDays(1));
|
||||||
// Test getters
|
|
||||||
assertEquals("doc.pdf", fileInfo.getFileName());
|
|
||||||
assertEquals(File.separator + "docs" + File.separator + "doc.pdf", fileInfo.getFilePath());
|
|
||||||
assertEquals(now, fileInfo.getModificationDate());
|
|
||||||
assertEquals(2048, fileInfo.getFileSize());
|
|
||||||
assertEquals(now.minusDays(1), fileInfo.getCreationDate());
|
|
||||||
|
|
||||||
// Test setters
|
assertEquals("1.50 KB", fi.getFormattedFileSize());
|
||||||
fileInfo.setFileName("new.pdf");
|
}
|
||||||
fileInfo.setFilePath(File.separator + "new" + File.separator + "new.pdf");
|
|
||||||
fileInfo.setModificationDate(now.plusDays(1));
|
|
||||||
fileInfo.setFileSize(4096);
|
|
||||||
fileInfo.setCreationDate(now.minusDays(2));
|
|
||||||
|
|
||||||
assertEquals("new.pdf", fileInfo.getFileName());
|
@Test
|
||||||
assertEquals(File.separator + "new" + File.separator + "new.pdf", fileInfo.getFilePath());
|
@DisplayName("Values above 1 TB are still represented in GB (design choice)")
|
||||||
assertEquals(now.plusDays(1), fileInfo.getModificationDate());
|
void shouldRepresentTerabytesInGb() {
|
||||||
assertEquals(4096, fileInfo.getFileSize());
|
// 2 TB = 2 * 1024 GB -> 2 * 1024 * 1024^3 bytes
|
||||||
assertEquals(now.minusDays(2), fileInfo.getCreationDate());
|
long twoTB = 2L * 1024 * 1024 * 1024 * 1024; // 2 * 2^40
|
||||||
|
FileInfo fi =
|
||||||
|
new FileInfo(
|
||||||
|
"example.txt",
|
||||||
|
"/path/to/example.txt",
|
||||||
|
FIXED_NOW,
|
||||||
|
twoTB,
|
||||||
|
FIXED_NOW.minusDays(1));
|
||||||
|
|
||||||
|
// 2 TB equals 2048.00 GB with current implementation
|
||||||
|
assertEquals(
|
||||||
|
"2048.00 GB",
|
||||||
|
fi.getFormattedFileSize(),
|
||||||
|
"Current implementation caps at GB and shows TB in GB units");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
package stirling.software.common.model.exception;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
@DisplayName("Tests for UnsupportedClaimException")
|
||||||
|
class UnsupportedClaimExceptionTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should store message passed to constructor")
|
||||||
|
void shouldStoreMessageFromConstructor() {
|
||||||
|
String expectedMessage = "This claim is not supported";
|
||||||
|
UnsupportedClaimException exception = new UnsupportedClaimException(expectedMessage);
|
||||||
|
|
||||||
|
// Verify the stored message
|
||||||
|
assertEquals(
|
||||||
|
expectedMessage,
|
||||||
|
exception.getMessage(),
|
||||||
|
"Constructor should correctly store the provided message");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should allow null message without throwing exception")
|
||||||
|
void shouldAllowNullMessage() {
|
||||||
|
UnsupportedClaimException exception = new UnsupportedClaimException(null);
|
||||||
|
|
||||||
|
// Null message should be stored as null
|
||||||
|
assertNull(
|
||||||
|
exception.getMessage(),
|
||||||
|
"Constructor should accept null message and store it as null");
|
||||||
|
}
|
||||||
|
}
|
||||||
+52
-59
@@ -1,7 +1,6 @@
|
|||||||
package stirling.software.common.service;
|
package stirling.software.common.service;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
|
||||||
import static org.mockito.Mockito.*;
|
import static org.mockito.Mockito.*;
|
||||||
import static stirling.software.common.service.SpyPDFDocumentFactory.*;
|
import static stirling.software.common.service.SpyPDFDocumentFactory.*;
|
||||||
|
|
||||||
@@ -22,7 +21,6 @@ import org.springframework.http.MediaType;
|
|||||||
import org.springframework.mock.web.MockMultipartFile;
|
import org.springframework.mock.web.MockMultipartFile;
|
||||||
|
|
||||||
import stirling.software.common.model.api.PDFFile;
|
import stirling.software.common.model.api.PDFFile;
|
||||||
import stirling.software.common.service.SpyPDFDocumentFactory.StrategyType;
|
|
||||||
|
|
||||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
||||||
@@ -43,58 +41,7 @@ class CustomPDFDocumentFactoryTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedTest
|
private static byte[] inflatePdf(byte[] input, int sizeInMB) throws IOException {
|
||||||
@CsvSource({"5,MEMORY_ONLY", "20,MIXED", "60,TEMP_FILE"})
|
|
||||||
void testStrategy_FileInput(int sizeMB, StrategyType expected) throws IOException {
|
|
||||||
File file = writeTempFile(inflatePdf(basePdfBytes, sizeMB));
|
|
||||||
try (PDDocument doc = factory.load(file)) {
|
|
||||||
Assertions.assertEquals(expected, factory.lastStrategyUsed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@CsvSource({"5,MEMORY_ONLY", "20,MIXED", "60,TEMP_FILE"})
|
|
||||||
void testStrategy_ByteArray(int sizeMB, StrategyType expected) throws IOException {
|
|
||||||
byte[] inflated = inflatePdf(basePdfBytes, sizeMB);
|
|
||||||
try (PDDocument doc = factory.load(inflated)) {
|
|
||||||
Assertions.assertEquals(expected, factory.lastStrategyUsed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@CsvSource({"5,MEMORY_ONLY", "20,MIXED", "60,TEMP_FILE"})
|
|
||||||
void testStrategy_InputStream(int sizeMB, StrategyType expected) throws IOException {
|
|
||||||
byte[] inflated = inflatePdf(basePdfBytes, sizeMB);
|
|
||||||
try (PDDocument doc = factory.load(new ByteArrayInputStream(inflated))) {
|
|
||||||
Assertions.assertEquals(expected, factory.lastStrategyUsed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@CsvSource({"5,MEMORY_ONLY", "20,MIXED", "60,TEMP_FILE"})
|
|
||||||
void testStrategy_MultipartFile(int sizeMB, StrategyType expected) throws IOException {
|
|
||||||
byte[] inflated = inflatePdf(basePdfBytes, sizeMB);
|
|
||||||
MockMultipartFile multipart =
|
|
||||||
new MockMultipartFile("file", "doc.pdf", MediaType.APPLICATION_PDF_VALUE, inflated);
|
|
||||||
try (PDDocument doc = factory.load(multipart)) {
|
|
||||||
Assertions.assertEquals(expected, factory.lastStrategyUsed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
|
||||||
@CsvSource({"5,MEMORY_ONLY", "20,MIXED", "60,TEMP_FILE"})
|
|
||||||
void testStrategy_PDFFile(int sizeMB, StrategyType expected) throws IOException {
|
|
||||||
byte[] inflated = inflatePdf(basePdfBytes, sizeMB);
|
|
||||||
MockMultipartFile multipart =
|
|
||||||
new MockMultipartFile("file", "doc.pdf", MediaType.APPLICATION_PDF_VALUE, inflated);
|
|
||||||
PDFFile pdfFile = new PDFFile();
|
|
||||||
pdfFile.setFileInput(multipart);
|
|
||||||
try (PDDocument doc = factory.load(pdfFile)) {
|
|
||||||
Assertions.assertEquals(expected, factory.lastStrategyUsed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private byte[] inflatePdf(byte[] input, int sizeInMB) throws IOException {
|
|
||||||
try (PDDocument doc = Loader.loadPDF(input)) {
|
try (PDDocument doc = Loader.loadPDF(input)) {
|
||||||
byte[] largeData = new byte[sizeInMB * 1024 * 1024];
|
byte[] largeData = new byte[sizeInMB * 1024 * 1024];
|
||||||
Arrays.fill(largeData, (byte) 'A');
|
Arrays.fill(largeData, (byte) 'A');
|
||||||
@@ -113,6 +60,46 @@ class CustomPDFDocumentFactoryTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static File writeTempFile(byte[] content) throws IOException {
|
||||||
|
File file = Files.createTempFile("pdf-test-", ".pdf").toFile();
|
||||||
|
Files.write(file.toPath(), content);
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@CsvSource({"5,MEMORY_ONLY", "20,MIXED", "60,TEMP_FILE"})
|
||||||
|
void testStrategy_FileInput(int sizeMB, StrategyType expected) throws IOException {
|
||||||
|
File file = writeTempFile(inflatePdf(basePdfBytes, sizeMB));
|
||||||
|
factory.load(file);
|
||||||
|
Assertions.assertEquals(expected, factory.lastStrategyUsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@CsvSource({"5,MEMORY_ONLY", "20,MIXED", "60,TEMP_FILE"})
|
||||||
|
void testStrategy_ByteArray(int sizeMB, StrategyType expected) throws IOException {
|
||||||
|
byte[] inflated = inflatePdf(basePdfBytes, sizeMB);
|
||||||
|
factory.load(inflated);
|
||||||
|
Assertions.assertEquals(expected, factory.lastStrategyUsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@CsvSource({"5,MEMORY_ONLY", "20,MIXED", "60,TEMP_FILE"})
|
||||||
|
void testStrategy_InputStream(int sizeMB, StrategyType expected) throws IOException {
|
||||||
|
byte[] inflated = inflatePdf(basePdfBytes, sizeMB);
|
||||||
|
factory.load(new ByteArrayInputStream(inflated));
|
||||||
|
Assertions.assertEquals(expected, factory.lastStrategyUsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@CsvSource({"5,MEMORY_ONLY", "20,MIXED", "60,TEMP_FILE"})
|
||||||
|
void testStrategy_MultipartFile(int sizeMB, StrategyType expected) throws IOException {
|
||||||
|
byte[] inflated = inflatePdf(basePdfBytes, sizeMB);
|
||||||
|
MockMultipartFile multipart =
|
||||||
|
new MockMultipartFile("file", "doc.pdf", MediaType.APPLICATION_PDF_VALUE, inflated);
|
||||||
|
factory.load(multipart);
|
||||||
|
Assertions.assertEquals(expected, factory.lastStrategyUsed);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testLoadFromPath() throws IOException {
|
void testLoadFromPath() throws IOException {
|
||||||
File file = writeTempFile(inflatePdf(basePdfBytes, 5));
|
File file = writeTempFile(inflatePdf(basePdfBytes, 5));
|
||||||
@@ -130,7 +117,7 @@ class CustomPDFDocumentFactoryTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// neeed to add password pdf
|
// need to add password pdf
|
||||||
// @Test
|
// @Test
|
||||||
// void testLoadPasswordProtectedPdfFromInputStream() throws IOException {
|
// void testLoadPasswordProtectedPdfFromInputStream() throws IOException {
|
||||||
// try (InputStream is = getClass().getResourceAsStream("/protected.pdf")) {
|
// try (InputStream is = getClass().getResourceAsStream("/protected.pdf")) {
|
||||||
@@ -212,10 +199,16 @@ class CustomPDFDocumentFactoryTest {
|
|||||||
assertTrue(newBytes.length > 0);
|
assertTrue(newBytes.length > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private File writeTempFile(byte[] content) throws IOException {
|
@ParameterizedTest
|
||||||
File file = Files.createTempFile("pdf-test-", ".pdf").toFile();
|
@CsvSource({"5,MEMORY_ONLY", "20,MIXED", "60,TEMP_FILE"})
|
||||||
Files.write(file.toPath(), content);
|
void testStrategy_PDFFile(int sizeMB, StrategyType expected) throws IOException {
|
||||||
return file;
|
byte[] inflated = inflatePdf(basePdfBytes, sizeMB);
|
||||||
|
MockMultipartFile multipart =
|
||||||
|
new MockMultipartFile("file", "doc.pdf", MediaType.APPLICATION_PDF_VALUE, inflated);
|
||||||
|
PDFFile pdfFile = new PDFFile();
|
||||||
|
pdfFile.setFileInput(multipart);
|
||||||
|
factory.load(pdfFile);
|
||||||
|
Assertions.assertEquals(expected, factory.lastStrategyUsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
package stirling.software.common.service;
|
package stirling.software.common.service;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
import static org.mockito.AdditionalAnswers.*;
|
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
|
||||||
import static org.mockito.Mockito.*;
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
@@ -86,7 +82,7 @@ class FileStorageTest {
|
|||||||
void testRetrieveFile() throws IOException {
|
void testRetrieveFile() throws IOException {
|
||||||
// Arrange
|
// Arrange
|
||||||
byte[] fileContent = "Test PDF content".getBytes();
|
byte[] fileContent = "Test PDF content".getBytes();
|
||||||
String fileId = UUID.randomUUID().toString();
|
String fileId = "test-file-1";
|
||||||
Path filePath = tempDir.resolve(fileId);
|
Path filePath = tempDir.resolve(fileId);
|
||||||
Files.write(filePath, fileContent);
|
Files.write(filePath, fileContent);
|
||||||
|
|
||||||
@@ -106,7 +102,7 @@ class FileStorageTest {
|
|||||||
void testRetrieveBytes() throws IOException {
|
void testRetrieveBytes() throws IOException {
|
||||||
// Arrange
|
// Arrange
|
||||||
byte[] fileContent = "Test PDF content".getBytes();
|
byte[] fileContent = "Test PDF content".getBytes();
|
||||||
String fileId = UUID.randomUUID().toString();
|
String fileId = "test-file-2";
|
||||||
Path filePath = tempDir.resolve(fileId);
|
Path filePath = tempDir.resolve(fileId);
|
||||||
Files.write(filePath, fileContent);
|
Files.write(filePath, fileContent);
|
||||||
|
|
||||||
@@ -139,7 +135,7 @@ class FileStorageTest {
|
|||||||
void testDeleteFile() throws IOException {
|
void testDeleteFile() throws IOException {
|
||||||
// Arrange
|
// Arrange
|
||||||
byte[] fileContent = "Test PDF content".getBytes();
|
byte[] fileContent = "Test PDF content".getBytes();
|
||||||
String fileId = UUID.randomUUID().toString();
|
String fileId = "test-file-3";
|
||||||
Path filePath = tempDir.resolve(fileId);
|
Path filePath = tempDir.resolve(fileId);
|
||||||
Files.write(filePath, fileContent);
|
Files.write(filePath, fileContent);
|
||||||
|
|
||||||
@@ -167,7 +163,7 @@ class FileStorageTest {
|
|||||||
void testFileExists() throws IOException {
|
void testFileExists() throws IOException {
|
||||||
// Arrange
|
// Arrange
|
||||||
byte[] fileContent = "Test PDF content".getBytes();
|
byte[] fileContent = "Test PDF content".getBytes();
|
||||||
String fileId = UUID.randomUUID().toString();
|
String fileId = "test-file-4";
|
||||||
Path filePath = tempDir.resolve(fileId);
|
Path filePath = tempDir.resolve(fileId);
|
||||||
Files.write(filePath, fileContent);
|
Files.write(filePath, fileContent);
|
||||||
|
|
||||||
|
|||||||
+11
-10
@@ -1,6 +1,7 @@
|
|||||||
package stirling.software.common.service;
|
package stirling.software.common.service;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
@@ -105,7 +106,7 @@ class JobExecutorServiceTest {
|
|||||||
|
|
||||||
// Then
|
// Then
|
||||||
assertEquals(HttpStatus.OK, response.getStatusCode());
|
assertEquals(HttpStatus.OK, response.getStatusCode());
|
||||||
assertTrue(response.getBody() instanceof JobResponse);
|
assertInstanceOf(JobResponse.class, response.getBody());
|
||||||
JobResponse<?> jobResponse = (JobResponse<?>) response.getBody();
|
JobResponse<?> jobResponse = (JobResponse<?>) response.getBody();
|
||||||
assertTrue(jobResponse.isAsync());
|
assertTrue(jobResponse.isAsync());
|
||||||
assertNotNull(jobResponse.getJobId());
|
assertNotNull(jobResponse.getJobId());
|
||||||
@@ -134,7 +135,7 @@ class JobExecutorServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldQueueJobWhenResourcesLimited() {
|
void shouldQueueJobWhenResourcesLimited() throws Exception {
|
||||||
// Given
|
// Given
|
||||||
Supplier<Object> work = () -> "test-result";
|
Supplier<Object> work = () -> "test-result";
|
||||||
CompletableFuture<ResponseEntity<?>> future = new CompletableFuture<>();
|
CompletableFuture<ResponseEntity<?>> future = new CompletableFuture<>();
|
||||||
@@ -150,7 +151,7 @@ class JobExecutorServiceTest {
|
|||||||
|
|
||||||
// Then
|
// Then
|
||||||
assertEquals(HttpStatus.OK, response.getStatusCode());
|
assertEquals(HttpStatus.OK, response.getStatusCode());
|
||||||
assertTrue(response.getBody() instanceof JobResponse);
|
assertInstanceOf(JobResponse.class, response.getBody());
|
||||||
|
|
||||||
// Verify job was queued
|
// Verify job was queued
|
||||||
verify(jobQueue).queueJob(anyString(), eq(80), any(), eq(5000L));
|
verify(jobQueue).queueJob(anyString(), eq(80), any(), eq(5000L));
|
||||||
@@ -184,13 +185,13 @@ class JobExecutorServiceTest {
|
|||||||
// Given
|
// Given
|
||||||
Supplier<Object> work =
|
Supplier<Object> work =
|
||||||
() -> {
|
() -> {
|
||||||
try {
|
// Simulate long-running job without actual sleep
|
||||||
Thread.sleep(100); // Simulate long-running job
|
// Use a loop to consume time instead of Thread.sleep
|
||||||
return "test-result";
|
long startTime = System.nanoTime();
|
||||||
} catch (InterruptedException e) {
|
while (System.nanoTime() - startTime < 100_000_000) { // 100ms in nanoseconds
|
||||||
Thread.currentThread().interrupt();
|
// Busy wait to simulate work without Thread.sleep
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
}
|
||||||
|
return "test-result";
|
||||||
};
|
};
|
||||||
|
|
||||||
// Use reflection to access the private executeWithTimeout method
|
// Use reflection to access the private executeWithTimeout method
|
||||||
@@ -203,7 +204,7 @@ class JobExecutorServiceTest {
|
|||||||
try {
|
try {
|
||||||
executeMethod.invoke(jobExecutorService, work, 1L); // Very short timeout
|
executeMethod.invoke(jobExecutorService, work, 1L); // Very short timeout
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
assertTrue(e.getCause() instanceof TimeoutException);
|
assertInstanceOf(TimeoutException.class, e.getCause());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
package stirling.software.common.service;
|
package stirling.software.common.service;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
||||||
|
|
||||||
import java.lang.management.MemoryMXBean;
|
import java.lang.management.MemoryMXBean;
|
||||||
import java.lang.management.OperatingSystemMXBean;
|
import java.lang.management.OperatingSystemMXBean;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
@@ -33,11 +32,11 @@ class ResourceMonitorTest {
|
|||||||
@Mock private MemoryMXBean memoryMXBean;
|
@Mock private MemoryMXBean memoryMXBean;
|
||||||
|
|
||||||
@Spy
|
@Spy
|
||||||
private AtomicReference<ResourceStatus> currentStatus =
|
private final AtomicReference<ResourceStatus> currentStatus =
|
||||||
new AtomicReference<>(ResourceStatus.OK);
|
new AtomicReference<>(ResourceStatus.OK);
|
||||||
|
|
||||||
@Spy
|
@Spy
|
||||||
private AtomicReference<ResourceMetrics> latestMetrics =
|
private final AtomicReference<ResourceMetrics> latestMetrics =
|
||||||
new AtomicReference<>(new ResourceMetrics());
|
new AtomicReference<>(new ResourceMetrics());
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
@@ -118,18 +117,24 @@ class ResourceMonitorTest {
|
|||||||
shouldQueue,
|
shouldQueue,
|
||||||
result,
|
result,
|
||||||
String.format(
|
String.format(
|
||||||
|
Locale.ROOT,
|
||||||
"For weight %d and status %s, shouldQueue should be %s",
|
"For weight %d and status %s, shouldQueue should be %s",
|
||||||
weight, status, shouldQueue));
|
weight,
|
||||||
|
status,
|
||||||
|
shouldQueue));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void resourceMetricsShouldDetectStaleState() {
|
void resourceMetricsShouldDetectStaleState() {
|
||||||
|
// Capture test time at the beginning for deterministic calculations
|
||||||
|
final Instant testTime = Instant.now();
|
||||||
|
|
||||||
// Given
|
// Given
|
||||||
Instant now = Instant.now();
|
Instant pastInstant =
|
||||||
Instant pastInstant = now.minusMillis(6000);
|
testTime.minusMillis(6000); // 6 seconds ago (relative to test start time)
|
||||||
|
|
||||||
ResourceMetrics staleMetrics = new ResourceMetrics(0.5, 0.5, 1024, 2048, 4096, pastInstant);
|
ResourceMetrics staleMetrics = new ResourceMetrics(0.5, 0.5, 1024, 2048, 4096, pastInstant);
|
||||||
ResourceMetrics freshMetrics = new ResourceMetrics(0.5, 0.5, 1024, 2048, 4096, now);
|
ResourceMetrics freshMetrics = new ResourceMetrics(0.5, 0.5, 1024, 2048, 4096, testTime);
|
||||||
|
|
||||||
// When/Then
|
// When/Then
|
||||||
assertTrue(
|
assertTrue(
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import static org.mockito.Mockito.*;
|
|||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
@@ -42,7 +41,7 @@ class TaskManagerTest {
|
|||||||
@Test
|
@Test
|
||||||
void testCreateTask() {
|
void testCreateTask() {
|
||||||
// Act
|
// Act
|
||||||
String jobId = UUID.randomUUID().toString();
|
String jobId = "test-job-1";
|
||||||
taskManager.createTask(jobId);
|
taskManager.createTask(jobId);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
@@ -56,7 +55,7 @@ class TaskManagerTest {
|
|||||||
@Test
|
@Test
|
||||||
void testSetResult() {
|
void testSetResult() {
|
||||||
// Arrange
|
// Arrange
|
||||||
String jobId = UUID.randomUUID().toString();
|
String jobId = "test-job-2";
|
||||||
taskManager.createTask(jobId);
|
taskManager.createTask(jobId);
|
||||||
Object resultObject = "Test result";
|
Object resultObject = "Test result";
|
||||||
|
|
||||||
@@ -74,7 +73,7 @@ class TaskManagerTest {
|
|||||||
@Test
|
@Test
|
||||||
void testSetFileResult() throws Exception {
|
void testSetFileResult() throws Exception {
|
||||||
// Arrange
|
// Arrange
|
||||||
String jobId = UUID.randomUUID().toString();
|
String jobId = "test-job-3";
|
||||||
taskManager.createTask(jobId);
|
taskManager.createTask(jobId);
|
||||||
String fileId = "file-id";
|
String fileId = "file-id";
|
||||||
String originalFileName = "test.pdf";
|
String originalFileName = "test.pdf";
|
||||||
@@ -108,7 +107,7 @@ class TaskManagerTest {
|
|||||||
@Test
|
@Test
|
||||||
void testSetError() {
|
void testSetError() {
|
||||||
// Arrange
|
// Arrange
|
||||||
String jobId = UUID.randomUUID().toString();
|
String jobId = "test-job-4";
|
||||||
taskManager.createTask(jobId);
|
taskManager.createTask(jobId);
|
||||||
String errorMessage = "Test error";
|
String errorMessage = "Test error";
|
||||||
|
|
||||||
@@ -126,7 +125,7 @@ class TaskManagerTest {
|
|||||||
@Test
|
@Test
|
||||||
void testSetComplete_WithExistingResult() {
|
void testSetComplete_WithExistingResult() {
|
||||||
// Arrange
|
// Arrange
|
||||||
String jobId = UUID.randomUUID().toString();
|
String jobId = "test-job-5";
|
||||||
taskManager.createTask(jobId);
|
taskManager.createTask(jobId);
|
||||||
Object resultObject = "Test result";
|
Object resultObject = "Test result";
|
||||||
taskManager.setResult(jobId, resultObject);
|
taskManager.setResult(jobId, resultObject);
|
||||||
@@ -144,7 +143,7 @@ class TaskManagerTest {
|
|||||||
@Test
|
@Test
|
||||||
void testSetComplete_WithoutExistingResult() {
|
void testSetComplete_WithoutExistingResult() {
|
||||||
// Arrange
|
// Arrange
|
||||||
String jobId = UUID.randomUUID().toString();
|
String jobId = "test-job-6";
|
||||||
taskManager.createTask(jobId);
|
taskManager.createTask(jobId);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -160,7 +159,7 @@ class TaskManagerTest {
|
|||||||
@Test
|
@Test
|
||||||
void testIsComplete() {
|
void testIsComplete() {
|
||||||
// Arrange
|
// Arrange
|
||||||
String jobId = UUID.randomUUID().toString();
|
String jobId = "test-job-7";
|
||||||
taskManager.createTask(jobId);
|
taskManager.createTask(jobId);
|
||||||
|
|
||||||
// Assert - not complete initially
|
// Assert - not complete initially
|
||||||
@@ -215,7 +214,9 @@ class TaskManagerTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testCleanupOldJobs() throws Exception {
|
void testCleanupOldJobs() {
|
||||||
|
// Capture test time at the beginning for deterministic calculations
|
||||||
|
final LocalDateTime testTime = LocalDateTime.now();
|
||||||
// Arrange
|
// Arrange
|
||||||
// 1. Create a recent completed job
|
// 1. Create a recent completed job
|
||||||
String recentJobId = "recent-job";
|
String recentJobId = "recent-job";
|
||||||
@@ -227,8 +228,9 @@ class TaskManagerTest {
|
|||||||
taskManager.createTask(oldJobId);
|
taskManager.createTask(oldJobId);
|
||||||
JobResult oldJob = taskManager.getJobResult(oldJobId);
|
JobResult oldJob = taskManager.getJobResult(oldJobId);
|
||||||
|
|
||||||
// Manually set the completion time to be older than the expiry
|
// Manually set the completion time to be older than the expiry (relative to test start
|
||||||
LocalDateTime oldTime = LocalDateTime.now().minusHours(1);
|
// time)
|
||||||
|
LocalDateTime oldTime = testTime.minusHours(1);
|
||||||
ReflectionTestUtils.setField(oldJob, "completedAt", oldTime);
|
ReflectionTestUtils.setField(oldJob, "completedAt", oldTime);
|
||||||
ReflectionTestUtils.setField(oldJob, "complete", true);
|
ReflectionTestUtils.setField(oldJob, "complete", true);
|
||||||
|
|
||||||
@@ -253,6 +255,7 @@ class TaskManagerTest {
|
|||||||
taskManager.createTask(activeJobId);
|
taskManager.createTask(activeJobId);
|
||||||
|
|
||||||
// Verify all jobs are in the map
|
// Verify all jobs are in the map
|
||||||
|
assertNotNull(jobResultsMap);
|
||||||
assertTrue(jobResultsMap.containsKey(recentJobId));
|
assertTrue(jobResultsMap.containsKey(recentJobId));
|
||||||
assertTrue(jobResultsMap.containsKey(oldJobId));
|
assertTrue(jobResultsMap.containsKey(oldJobId));
|
||||||
assertTrue(jobResultsMap.containsKey(activeJobId));
|
assertTrue(jobResultsMap.containsKey(activeJobId));
|
||||||
@@ -268,7 +271,7 @@ class TaskManagerTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testShutdown() throws Exception {
|
void testShutdown() {
|
||||||
// This mainly tests that the shutdown method doesn't throw exceptions
|
// This mainly tests that the shutdown method doesn't throw exceptions
|
||||||
taskManager.shutdown();
|
taskManager.shutdown();
|
||||||
|
|
||||||
@@ -279,7 +282,7 @@ class TaskManagerTest {
|
|||||||
@Test
|
@Test
|
||||||
void testAddNote() {
|
void testAddNote() {
|
||||||
// Arrange
|
// Arrange
|
||||||
String jobId = UUID.randomUUID().toString();
|
String jobId = "test-job-8";
|
||||||
taskManager.createTask(jobId);
|
taskManager.createTask(jobId);
|
||||||
String note = "Test note";
|
String note = "Test note";
|
||||||
|
|
||||||
|
|||||||
+49
-41
@@ -1,8 +1,6 @@
|
|||||||
package stirling.software.common.service;
|
package stirling.software.common.service;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
|
||||||
import static org.mockito.ArgumentMatchers.anyLong;
|
|
||||||
import static org.mockito.Mockito.*;
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@@ -133,6 +131,9 @@ public class TempFileCleanupServiceTest {
|
|||||||
|
|
||||||
// Use MockedStatic to mock Files operations
|
// Use MockedStatic to mock Files operations
|
||||||
try (MockedStatic<Files> mockedFiles = mockStatic(Files.class)) {
|
try (MockedStatic<Files> mockedFiles = mockStatic(Files.class)) {
|
||||||
|
// Capture test time at the beginning for deterministic calculations
|
||||||
|
final long testTime = System.currentTimeMillis();
|
||||||
|
|
||||||
// Mock Files.list for each directory we'll process
|
// Mock Files.list for each directory we'll process
|
||||||
mockedFiles
|
mockedFiles
|
||||||
.when(() -> Files.list(eq(systemTempDir)))
|
.when(() -> Files.list(eq(systemTempDir)))
|
||||||
@@ -177,18 +178,17 @@ public class TempFileCleanupServiceTest {
|
|||||||
// maxAgeMillis
|
// maxAgeMillis
|
||||||
if (fileName.contains("old")) {
|
if (fileName.contains("old")) {
|
||||||
return FileTime.fromMillis(
|
return FileTime.fromMillis(
|
||||||
System.currentTimeMillis() - 5000000);
|
testTime - 5000000); // ~1.4 hours ago
|
||||||
}
|
}
|
||||||
// For empty.tmp file, return a timestamp older than 5 minutes (for
|
// For empty.tmp file, return a timestamp older than 5 minutes (for
|
||||||
// empty file test)
|
// empty file test)
|
||||||
else if (fileName.equals("empty.tmp")) {
|
else if ("empty.tmp".equals(fileName)) {
|
||||||
return FileTime.fromMillis(
|
return FileTime.fromMillis(
|
||||||
System.currentTimeMillis() - 6 * 60 * 1000);
|
testTime - 6 * 60 * 1000); // 6 minutes ago
|
||||||
}
|
}
|
||||||
// For all other files, return a recent timestamp
|
// For all other files, return a recent timestamp
|
||||||
else {
|
else {
|
||||||
return FileTime.fromMillis(
|
return FileTime.fromMillis(testTime - 60000); // 1 minute ago
|
||||||
System.currentTimeMillis() - 60000); // 1 minute ago
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -201,7 +201,7 @@ public class TempFileCleanupServiceTest {
|
|||||||
String fileName = path.getFileName().toString();
|
String fileName = path.getFileName().toString();
|
||||||
|
|
||||||
// Return 0 bytes for the empty file
|
// Return 0 bytes for the empty file
|
||||||
if (fileName.equals("empty.tmp")) {
|
if ("empty.tmp".equals(fileName)) {
|
||||||
return 0L;
|
return 0L;
|
||||||
}
|
}
|
||||||
// Return normal size for all other files
|
// Return normal size for all other files
|
||||||
@@ -221,9 +221,9 @@ public class TempFileCleanupServiceTest {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Act - set containerMode to false for this test
|
// Act - set containerMode to false for this test
|
||||||
invokeCleanupDirectoryStreaming(systemTempDir, false, 0, 3600000);
|
invokeCleanupDirectoryStreaming(systemTempDir, false, 3600000);
|
||||||
invokeCleanupDirectoryStreaming(customTempDir, false, 0, 3600000);
|
invokeCleanupDirectoryStreaming(customTempDir, false, 3600000);
|
||||||
invokeCleanupDirectoryStreaming(libreOfficeTempDir, false, 0, 3600000);
|
invokeCleanupDirectoryStreaming(libreOfficeTempDir, false, 3600000);
|
||||||
|
|
||||||
// Assert - Only old temp files and empty files should be deleted
|
// Assert - Only old temp files and empty files should be deleted
|
||||||
assertTrue(deletedFiles.contains(oldTempFile), "Old temp file should be deleted");
|
assertTrue(deletedFiles.contains(oldTempFile), "Old temp file should be deleted");
|
||||||
@@ -276,6 +276,9 @@ public class TempFileCleanupServiceTest {
|
|||||||
|
|
||||||
// Use MockedStatic to mock Files operations
|
// Use MockedStatic to mock Files operations
|
||||||
try (MockedStatic<Files> mockedFiles = mockStatic(Files.class)) {
|
try (MockedStatic<Files> mockedFiles = mockStatic(Files.class)) {
|
||||||
|
// Capture test time at the beginning for deterministic calculations
|
||||||
|
final long testTime = System.currentTimeMillis();
|
||||||
|
|
||||||
// Mock Files.list for systemTempDir
|
// Mock Files.list for systemTempDir
|
||||||
mockedFiles
|
mockedFiles
|
||||||
.when(() -> Files.list(eq(systemTempDir)))
|
.when(() -> Files.list(eq(systemTempDir)))
|
||||||
@@ -290,9 +293,7 @@ public class TempFileCleanupServiceTest {
|
|||||||
// Configure Files.getLastModifiedTime to return recent timestamps
|
// Configure Files.getLastModifiedTime to return recent timestamps
|
||||||
mockedFiles
|
mockedFiles
|
||||||
.when(() -> Files.getLastModifiedTime(any(Path.class)))
|
.when(() -> Files.getLastModifiedTime(any(Path.class)))
|
||||||
.thenReturn(
|
.thenReturn(FileTime.fromMillis(testTime - 60000)); // 1 minute ago
|
||||||
FileTime.fromMillis(
|
|
||||||
System.currentTimeMillis() - 60000)); // 1 minute ago
|
|
||||||
|
|
||||||
// Configure Files.size to return normal size
|
// Configure Files.size to return normal size
|
||||||
mockedFiles.when(() -> Files.size(any(Path.class))).thenReturn(1024L); // 1 KB
|
mockedFiles.when(() -> Files.size(any(Path.class))).thenReturn(1024L); // 1 KB
|
||||||
@@ -308,7 +309,7 @@ public class TempFileCleanupServiceTest {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Act - set containerMode to true and maxAgeMillis to 0 for container startup cleanup
|
// Act - set containerMode to true and maxAgeMillis to 0 for container startup cleanup
|
||||||
invokeCleanupDirectoryStreaming(systemTempDir, true, 0, 0);
|
invokeCleanupDirectoryStreaming(systemTempDir, true, 0);
|
||||||
|
|
||||||
// Assert - In container mode, both our temp files and system temp files should be
|
// Assert - In container mode, both our temp files and system temp files should be
|
||||||
// deleted
|
// deleted
|
||||||
@@ -337,6 +338,9 @@ public class TempFileCleanupServiceTest {
|
|||||||
|
|
||||||
// Use MockedStatic to mock Files operations
|
// Use MockedStatic to mock Files operations
|
||||||
try (MockedStatic<Files> mockedFiles = mockStatic(Files.class)) {
|
try (MockedStatic<Files> mockedFiles = mockStatic(Files.class)) {
|
||||||
|
// Capture test time at the beginning for deterministic calculations
|
||||||
|
final long testTime = System.currentTimeMillis();
|
||||||
|
|
||||||
// Mock Files.list for systemTempDir
|
// Mock Files.list for systemTempDir
|
||||||
mockedFiles
|
mockedFiles
|
||||||
.when(() -> Files.list(eq(systemTempDir)))
|
.when(() -> Files.list(eq(systemTempDir)))
|
||||||
@@ -356,14 +360,14 @@ public class TempFileCleanupServiceTest {
|
|||||||
Path path = invocation.getArgument(0);
|
Path path = invocation.getArgument(0);
|
||||||
String fileName = path.getFileName().toString();
|
String fileName = path.getFileName().toString();
|
||||||
|
|
||||||
if (fileName.equals("empty.tmp")) {
|
if ("empty.tmp".equals(fileName)) {
|
||||||
// More than 5 minutes old
|
// More than 5 minutes old
|
||||||
return FileTime.fromMillis(
|
return FileTime.fromMillis(
|
||||||
System.currentTimeMillis() - 6 * 60 * 1000);
|
testTime - 6 * 60 * 1000); // 6 minutes ago
|
||||||
} else {
|
} else {
|
||||||
// Less than 5 minutes old
|
// Less than 5 minutes old
|
||||||
return FileTime.fromMillis(
|
return FileTime.fromMillis(
|
||||||
System.currentTimeMillis() - 2 * 60 * 1000);
|
testTime - 2 * 60 * 1000); // 2 minutes ago
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -381,7 +385,7 @@ public class TempFileCleanupServiceTest {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
invokeCleanupDirectoryStreaming(systemTempDir, false, 0, 3600000);
|
invokeCleanupDirectoryStreaming(systemTempDir, false, 3600000);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
assertTrue(
|
assertTrue(
|
||||||
@@ -412,14 +416,25 @@ public class TempFileCleanupServiceTest {
|
|||||||
|
|
||||||
// Use MockedStatic to mock Files operations
|
// Use MockedStatic to mock Files operations
|
||||||
try (MockedStatic<Files> mockedFiles = mockStatic(Files.class)) {
|
try (MockedStatic<Files> mockedFiles = mockStatic(Files.class)) {
|
||||||
|
// Capture test time at the beginning for deterministic calculations
|
||||||
|
final long testTime = System.currentTimeMillis();
|
||||||
|
|
||||||
// Mock Files.list for each directory
|
// Mock Files.list for each directory
|
||||||
mockedFiles.when(() -> Files.list(eq(systemTempDir))).thenReturn(Stream.of(dir1));
|
mockedFiles
|
||||||
|
.when(() -> Files.list(eq(systemTempDir)))
|
||||||
|
.thenAnswer(invocation -> Stream.of(dir1));
|
||||||
|
|
||||||
mockedFiles.when(() -> Files.list(eq(dir1))).thenReturn(Stream.of(tempFile1, dir2));
|
mockedFiles
|
||||||
|
.when(() -> Files.list(eq(dir1)))
|
||||||
|
.thenAnswer(invocation -> Stream.of(tempFile1, dir2));
|
||||||
|
|
||||||
mockedFiles.when(() -> Files.list(eq(dir2))).thenReturn(Stream.of(tempFile2, dir3));
|
mockedFiles
|
||||||
|
.when(() -> Files.list(eq(dir2)))
|
||||||
|
.thenAnswer(invocation -> Stream.of(tempFile2, dir3));
|
||||||
|
|
||||||
mockedFiles.when(() -> Files.list(eq(dir3))).thenReturn(Stream.of(tempFile3));
|
mockedFiles
|
||||||
|
.when(() -> Files.list(eq(dir3)))
|
||||||
|
.thenAnswer(invocation -> Stream.of(tempFile3));
|
||||||
|
|
||||||
// Configure Files.isDirectory for each path
|
// Configure Files.isDirectory for each path
|
||||||
mockedFiles.when(() -> Files.isDirectory(eq(dir1))).thenReturn(true);
|
mockedFiles.when(() -> Files.isDirectory(eq(dir1))).thenReturn(true);
|
||||||
@@ -432,6 +447,9 @@ public class TempFileCleanupServiceTest {
|
|||||||
// Configure Files.exists to return true for all paths
|
// Configure Files.exists to return true for all paths
|
||||||
mockedFiles.when(() -> Files.exists(any(Path.class))).thenReturn(true);
|
mockedFiles.when(() -> Files.exists(any(Path.class))).thenReturn(true);
|
||||||
|
|
||||||
|
// Configure Files.size to return 0 for all files (ensure they're not empty)
|
||||||
|
mockedFiles.when(() -> Files.size(any(Path.class))).thenReturn(1024L);
|
||||||
|
|
||||||
// Configure Files.getLastModifiedTime to return different times based on file names
|
// Configure Files.getLastModifiedTime to return different times based on file names
|
||||||
mockedFiles
|
mockedFiles
|
||||||
.when(() -> Files.getLastModifiedTime(any(Path.class)))
|
.when(() -> Files.getLastModifiedTime(any(Path.class)))
|
||||||
@@ -441,19 +459,14 @@ public class TempFileCleanupServiceTest {
|
|||||||
String fileName = path.getFileName().toString();
|
String fileName = path.getFileName().toString();
|
||||||
|
|
||||||
if (fileName.contains("old")) {
|
if (fileName.contains("old")) {
|
||||||
// Old file
|
// Old file - very old timestamp (older than 1 hour)
|
||||||
return FileTime.fromMillis(
|
return FileTime.fromMillis(testTime - 7200000); // 2 hours ago
|
||||||
System.currentTimeMillis() - 5000000);
|
|
||||||
} else {
|
} else {
|
||||||
// Recent file
|
// Recent file - very recent timestamp (less than 1 hour)
|
||||||
return FileTime.fromMillis(System.currentTimeMillis() - 60000);
|
return FileTime.fromMillis(testTime - 60000); // 1 minute ago
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Configure Files.size to return normal size
|
|
||||||
mockedFiles.when(() -> Files.size(any(Path.class))).thenReturn(1024L);
|
|
||||||
|
|
||||||
// For deleteIfExists, track which files would be deleted
|
|
||||||
mockedFiles
|
mockedFiles
|
||||||
.when(() -> Files.deleteIfExists(any(Path.class)))
|
.when(() -> Files.deleteIfExists(any(Path.class)))
|
||||||
.thenAnswer(
|
.thenAnswer(
|
||||||
@@ -463,12 +476,8 @@ public class TempFileCleanupServiceTest {
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Act
|
// Act - pass maxAgeMillis = 3600000 (1 hour)
|
||||||
invokeCleanupDirectoryStreaming(systemTempDir, false, 0, 3600000);
|
invokeCleanupDirectoryStreaming(systemTempDir, false, 3600000);
|
||||||
|
|
||||||
// Debug - print what was deleted
|
|
||||||
System.out.println("Deleted files: " + deletedFiles);
|
|
||||||
System.out.println("Looking for: " + tempFile3);
|
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
assertFalse(deletedFiles.contains(tempFile1), "Recent temp file should be preserved");
|
assertFalse(deletedFiles.contains(tempFile1), "Recent temp file should be preserved");
|
||||||
@@ -481,8 +490,7 @@ public class TempFileCleanupServiceTest {
|
|||||||
|
|
||||||
/** Helper method to invoke the private cleanupDirectoryStreaming method using reflection */
|
/** Helper method to invoke the private cleanupDirectoryStreaming method using reflection */
|
||||||
private void invokeCleanupDirectoryStreaming(
|
private void invokeCleanupDirectoryStreaming(
|
||||||
Path directory, boolean containerMode, int depth, long maxAgeMillis)
|
Path directory, boolean containerMode, long maxAgeMillis) {
|
||||||
throws IOException {
|
|
||||||
try {
|
try {
|
||||||
// Create a consumer that tracks deleted files
|
// Create a consumer that tracks deleted files
|
||||||
AtomicInteger deleteCount = new AtomicInteger(0);
|
AtomicInteger deleteCount = new AtomicInteger(0);
|
||||||
@@ -505,7 +513,7 @@ public class TempFileCleanupServiceTest {
|
|||||||
cleanupService,
|
cleanupService,
|
||||||
directory,
|
directory,
|
||||||
containerMode,
|
containerMode,
|
||||||
depth,
|
0,
|
||||||
maxAgeMillis,
|
maxAgeMillis,
|
||||||
false,
|
false,
|
||||||
deleteCallback);
|
deleteCallback);
|
||||||
|
|||||||
+58
-7
@@ -1,9 +1,6 @@
|
|||||||
package stirling.software.common.util;
|
package stirling.software.common.util;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
||||||
import static org.mockito.ArgumentMatchers.anyList;
|
import static org.mockito.ArgumentMatchers.anyList;
|
||||||
import static org.mockito.Mockito.mockStatic;
|
import static org.mockito.Mockito.mockStatic;
|
||||||
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.times;
|
||||||
@@ -26,6 +23,7 @@ class CheckProgramInstallTest {
|
|||||||
|
|
||||||
private MockedStatic<ProcessExecutor> mockProcessExecutor;
|
private MockedStatic<ProcessExecutor> mockProcessExecutor;
|
||||||
private ProcessExecutor mockExecutor;
|
private ProcessExecutor mockExecutor;
|
||||||
|
private ProcessExecutor mockFfmpegExecutor;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() throws Exception {
|
void setUp() throws Exception {
|
||||||
@@ -34,10 +32,14 @@ class CheckProgramInstallTest {
|
|||||||
|
|
||||||
// Set up mock for ProcessExecutor
|
// Set up mock for ProcessExecutor
|
||||||
mockExecutor = Mockito.mock(ProcessExecutor.class);
|
mockExecutor = Mockito.mock(ProcessExecutor.class);
|
||||||
|
mockFfmpegExecutor = Mockito.mock(ProcessExecutor.class);
|
||||||
mockProcessExecutor = mockStatic(ProcessExecutor.class);
|
mockProcessExecutor = mockStatic(ProcessExecutor.class);
|
||||||
mockProcessExecutor
|
mockProcessExecutor
|
||||||
.when(() -> ProcessExecutor.getInstance(ProcessExecutor.Processes.PYTHON_OPENCV))
|
.when(() -> ProcessExecutor.getInstance(ProcessExecutor.Processes.PYTHON_OPENCV))
|
||||||
.thenReturn(mockExecutor);
|
.thenReturn(mockExecutor);
|
||||||
|
mockProcessExecutor
|
||||||
|
.when(() -> ProcessExecutor.getInstance(ProcessExecutor.Processes.FFMPEG))
|
||||||
|
.thenReturn(mockFfmpegExecutor);
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterEach
|
@AfterEach
|
||||||
@@ -49,7 +51,7 @@ class CheckProgramInstallTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Reset static fields in the CheckProgramInstall class using reflection */
|
/** Reset static fields in the CheckProgramInstall class using reflection */
|
||||||
private void resetStaticFields() throws Exception {
|
private static void resetStaticFields() throws Exception {
|
||||||
Field pythonAvailableCheckedField =
|
Field pythonAvailableCheckedField =
|
||||||
CheckProgramInstall.class.getDeclaredField("pythonAvailableChecked");
|
CheckProgramInstall.class.getDeclaredField("pythonAvailableChecked");
|
||||||
pythonAvailableCheckedField.setAccessible(true);
|
pythonAvailableCheckedField.setAccessible(true);
|
||||||
@@ -59,6 +61,15 @@ class CheckProgramInstallTest {
|
|||||||
CheckProgramInstall.class.getDeclaredField("availablePythonCommand");
|
CheckProgramInstall.class.getDeclaredField("availablePythonCommand");
|
||||||
availablePythonCommandField.setAccessible(true);
|
availablePythonCommandField.setAccessible(true);
|
||||||
availablePythonCommandField.set(null, null);
|
availablePythonCommandField.set(null, null);
|
||||||
|
|
||||||
|
Field ffmpegAvailableCheckedField =
|
||||||
|
CheckProgramInstall.class.getDeclaredField("ffmpegAvailableChecked");
|
||||||
|
ffmpegAvailableCheckedField.setAccessible(true);
|
||||||
|
ffmpegAvailableCheckedField.set(null, false);
|
||||||
|
|
||||||
|
Field ffmpegAvailableField = CheckProgramInstall.class.getDeclaredField("ffmpegAvailable");
|
||||||
|
ffmpegAvailableField.setAccessible(true);
|
||||||
|
ffmpegAvailableField.set(null, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -108,8 +119,7 @@ class CheckProgramInstallTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testGetAvailablePythonCommand_WhenPythonReturnsNonZeroExitCode()
|
void testGetAvailablePythonCommand_WhenPythonReturnsNonZeroExitCode() throws Exception {
|
||||||
throws IOException, InterruptedException, Exception {
|
|
||||||
// Arrange
|
// Arrange
|
||||||
// Reset the static fields again to ensure clean state
|
// Reset the static fields again to ensure clean state
|
||||||
resetStaticFields();
|
resetStaticFields();
|
||||||
@@ -205,4 +215,45 @@ class CheckProgramInstallTest {
|
|||||||
// Verify getAvailablePythonCommand was called internally
|
// Verify getAvailablePythonCommand was called internally
|
||||||
verify(mockExecutor).runCommandWithOutputHandling(Arrays.asList("python3", "--version"));
|
verify(mockExecutor).runCommandWithOutputHandling(Arrays.asList("python3", "--version"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testIsFfmpegAvailable_WhenInstalled() throws Exception {
|
||||||
|
resetStaticFields();
|
||||||
|
ProcessExecutorResult result = Mockito.mock(ProcessExecutorResult.class);
|
||||||
|
when(mockFfmpegExecutor.runCommandWithOutputHandling(Arrays.asList("ffmpeg", "-version")))
|
||||||
|
.thenReturn(result);
|
||||||
|
|
||||||
|
assertTrue(CheckProgramInstall.isFfmpegAvailable());
|
||||||
|
verify(mockFfmpegExecutor)
|
||||||
|
.runCommandWithOutputHandling(Arrays.asList("ffmpeg", "-version"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testIsFfmpegAvailable_WhenNotInstalled() throws Exception {
|
||||||
|
resetStaticFields();
|
||||||
|
when(mockFfmpegExecutor.runCommandWithOutputHandling(Arrays.asList("ffmpeg", "-version")))
|
||||||
|
.thenThrow(new IOException("Command not found"));
|
||||||
|
|
||||||
|
assertFalse(CheckProgramInstall.isFfmpegAvailable());
|
||||||
|
verify(mockFfmpegExecutor)
|
||||||
|
.runCommandWithOutputHandling(Arrays.asList("ffmpeg", "-version"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testIsFfmpegAvailable_CachesResult() throws Exception {
|
||||||
|
resetStaticFields();
|
||||||
|
ProcessExecutorResult result = Mockito.mock(ProcessExecutorResult.class);
|
||||||
|
when(mockFfmpegExecutor.runCommandWithOutputHandling(Arrays.asList("ffmpeg", "-version")))
|
||||||
|
.thenReturn(result);
|
||||||
|
|
||||||
|
assertTrue(CheckProgramInstall.isFfmpegAvailable());
|
||||||
|
|
||||||
|
when(mockFfmpegExecutor.runCommandWithOutputHandling(Arrays.asList("ffmpeg", "-version")))
|
||||||
|
.thenThrow(new IOException("Command not found"));
|
||||||
|
|
||||||
|
assertTrue(CheckProgramInstall.isFfmpegAvailable());
|
||||||
|
|
||||||
|
verify(mockFfmpegExecutor, times(1))
|
||||||
|
.runCommandWithOutputHandling(Arrays.asList("ffmpeg", "-version"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ public class ChecksumUtilsTest {
|
|||||||
@Test
|
@Test
|
||||||
void crc32_unsignedFormatting_highBitSet() throws Exception {
|
void crc32_unsignedFormatting_highBitSet() throws Exception {
|
||||||
// CRC32 of single zero byte (0x00) is 0xD202EF8D (>= 0x8000_0000)
|
// CRC32 of single zero byte (0x00) is 0xD202EF8D (>= 0x8000_0000)
|
||||||
byte[] data = new byte[] {0x00};
|
byte[] data = {0x00};
|
||||||
|
|
||||||
// Hex (unsigned, 8 chars, lowercase)
|
// Hex (unsigned, 8 chars, lowercase)
|
||||||
try (InputStream is = new ByteArrayInputStream(data)) {
|
try (InputStream is = new ByteArrayInputStream(data)) {
|
||||||
|
|||||||
+163
-38
@@ -1,41 +1,44 @@
|
|||||||
package stirling.software.common.util;
|
package stirling.software.common.util;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.mockito.ArgumentMatchers.argThat;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.*;
|
||||||
import static org.mockito.Mockito.when;
|
|
||||||
|
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import org.junit.jupiter.params.ParameterizedTest;
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
import org.junit.jupiter.params.provider.Arguments;
|
import org.junit.jupiter.params.provider.Arguments;
|
||||||
import org.junit.jupiter.params.provider.MethodSource;
|
import org.junit.jupiter.params.provider.MethodSource;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
import stirling.software.common.model.ApplicationProperties;
|
||||||
import stirling.software.common.service.SsrfProtectionService;
|
import stirling.software.common.service.SsrfProtectionService;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
class CustomHtmlSanitizerTest {
|
class CustomHtmlSanitizerTest {
|
||||||
|
|
||||||
|
@Mock private SsrfProtectionService ssrfProtectionService;
|
||||||
|
@Mock private ApplicationProperties applicationProperties;
|
||||||
|
@Mock private ApplicationProperties.System systemProperties;
|
||||||
|
|
||||||
private CustomHtmlSanitizer customHtmlSanitizer;
|
private CustomHtmlSanitizer customHtmlSanitizer;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
SsrfProtectionService mockSsrfProtectionService = mock(SsrfProtectionService.class);
|
// Default behavior: allow all URLs and enable sanitization. Lenient stubs avoid
|
||||||
stirling.software.common.model.ApplicationProperties mockApplicationProperties =
|
// strict-stubbing failures when individual tests bypass certain branches.
|
||||||
mock(stirling.software.common.model.ApplicationProperties.class);
|
lenient().when(ssrfProtectionService.isUrlAllowed(anyString())).thenReturn(true);
|
||||||
stirling.software.common.model.ApplicationProperties.System mockSystem =
|
lenient().when(applicationProperties.getSystem()).thenReturn(systemProperties);
|
||||||
mock(stirling.software.common.model.ApplicationProperties.System.class);
|
lenient().when(systemProperties.isDisableSanitize()).thenReturn(false);
|
||||||
|
|
||||||
// Allow all URLs by default for basic tests
|
customHtmlSanitizer = new CustomHtmlSanitizer(ssrfProtectionService, applicationProperties);
|
||||||
when(mockSsrfProtectionService.isUrlAllowed(org.mockito.ArgumentMatchers.anyString()))
|
|
||||||
.thenReturn(true);
|
|
||||||
when(mockApplicationProperties.getSystem()).thenReturn(mockSystem);
|
|
||||||
when(mockSystem.getDisableSanitize()).thenReturn(false); // Enable sanitization for tests
|
|
||||||
|
|
||||||
customHtmlSanitizer =
|
|
||||||
new CustomHtmlSanitizer(mockSsrfProtectionService, mockApplicationProperties);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@@ -56,10 +59,11 @@ class CustomHtmlSanitizerTest {
|
|||||||
"<p>This is <strong>valid</strong> HTML with <em>formatting</em>.</p>",
|
"<p>This is <strong>valid</strong> HTML with <em>formatting</em>.</p>",
|
||||||
new String[] {"<p>", "<strong>", "<em>"}),
|
new String[] {"<p>", "<strong>", "<em>"}),
|
||||||
Arguments.of(
|
Arguments.of(
|
||||||
"<p>Text with <b>bold</b>, <i>italic</i>, <u>underline</u>, "
|
"<p>Text with <b>bold</b>, <i>italic</i>, <u>underline</u>,"
|
||||||
+ "<em>emphasis</em>, <strong>strong</strong>, <strike>strikethrough</strike>, "
|
+ " <em>emphasis</em>, <strong>strong</strong>,"
|
||||||
+ "<s>strike</s>, <sub>subscript</sub>, <sup>superscript</sup>, "
|
+ " <strike>strikethrough</strike>, <s>strike</s>,"
|
||||||
+ "<tt>teletype</tt>, <code>code</code>, <big>big</big>, <small>small</small>.</p>",
|
+ " <sub>subscript</sub>, <sup>superscript</sup>, <tt>teletype</tt>,"
|
||||||
|
+ " <code>code</code>, <big>big</big>, <small>small</small>.</p>",
|
||||||
new String[] {
|
new String[] {
|
||||||
"<b>bold</b>",
|
"<b>bold</b>",
|
||||||
"<i>italic</i>",
|
"<i>italic</i>",
|
||||||
@@ -163,7 +167,7 @@ class CustomHtmlSanitizerTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testSanitizeAllowsImages() {
|
void testSanitizeAllowsImages() {
|
||||||
// Arrange - Testing Sanitizers.IMAGES
|
// Arrange - Testing custom images policy with SSRF check
|
||||||
String htmlWithImage =
|
String htmlWithImage =
|
||||||
"<img src=\"image.jpg\" alt=\"An image\" width=\"100\" height=\"100\">";
|
"<img src=\"image.jpg\" alt=\"An image\" width=\"100\" height=\"100\">";
|
||||||
|
|
||||||
@@ -182,7 +186,16 @@ class CustomHtmlSanitizerTest {
|
|||||||
void testSanitizeDisallowsDataUrlImages() {
|
void testSanitizeDisallowsDataUrlImages() {
|
||||||
// Arrange
|
// Arrange
|
||||||
String htmlWithDataUrlImage =
|
String htmlWithDataUrlImage =
|
||||||
"<img src=\"data:image/svg+xml;base64,PHN2ZyBvbmxvYWQ9ImFsZXJ0KDEpIj48L3N2Zz4=\" alt=\"SVG with XSS\">";
|
"<img src=\"data:image/svg+xml;base64,PHN2ZyBvbmxvYWQ9ImFsZXJ0KDEpIj48L3N2Zz4=\""
|
||||||
|
+ " alt=\"SVG with XSS\">";
|
||||||
|
|
||||||
|
// Changed: Explicitly tell SSRF service to reject data: URLs so the custom AttributePolicy
|
||||||
|
// drops the src attribute. Without this, a permissive SSRF mock might allow data: URLs.
|
||||||
|
lenient()
|
||||||
|
.when(
|
||||||
|
ssrfProtectionService.isUrlAllowed(
|
||||||
|
argThat(v -> v != null && v.startsWith("data:"))))
|
||||||
|
.thenReturn(false);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
String sanitizedHtml = customHtmlSanitizer.sanitize(htmlWithDataUrlImage);
|
String sanitizedHtml = customHtmlSanitizer.sanitize(htmlWithDataUrlImage);
|
||||||
@@ -257,9 +270,9 @@ class CustomHtmlSanitizerTest {
|
|||||||
void testSanitizeRemovesObjectAndEmbed() {
|
void testSanitizeRemovesObjectAndEmbed() {
|
||||||
// Arrange
|
// Arrange
|
||||||
String htmlWithObjects =
|
String htmlWithObjects =
|
||||||
"<p>Safe content</p>"
|
"<p>Safe content</p><object data=\"data.swf\""
|
||||||
+ "<object data=\"data.swf\" type=\"application/x-shockwave-flash\"></object>"
|
+ " type=\"application/x-shockwave-flash\"></object><embed src=\"embed.swf\""
|
||||||
+ "<embed src=\"embed.swf\" type=\"application/x-shockwave-flash\">";
|
+ " type=\"application/x-shockwave-flash\">";
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
String sanitizedHtml = customHtmlSanitizer.sanitize(htmlWithObjects);
|
String sanitizedHtml = customHtmlSanitizer.sanitize(htmlWithObjects);
|
||||||
@@ -295,17 +308,12 @@ class CustomHtmlSanitizerTest {
|
|||||||
void testSanitizeHandlesComplexHtml() {
|
void testSanitizeHandlesComplexHtml() {
|
||||||
// Arrange
|
// Arrange
|
||||||
String complexHtml =
|
String complexHtml =
|
||||||
"<div class=\"container\">"
|
"<div class=\"container\"> <h1 style=\"color: blue;\">Welcome</h1> <p>This is a"
|
||||||
+ " <h1 style=\"color: blue;\">Welcome</h1>"
|
+ " <strong>test</strong> with <a href=\"https://example.com\">link</a>.</p> "
|
||||||
+ " <p>This is a <strong>test</strong> with <a href=\"https://example.com\">link</a>.</p>"
|
+ " <table> <tr><th>Name</th><th>Value</th></tr> <tr><td>Item"
|
||||||
+ " <table>"
|
+ " 1</td><td>100</td></tr> </table> <img src=\"image.jpg\" alt=\"Test"
|
||||||
+ " <tr><th>Name</th><th>Value</th></tr>"
|
+ " image\"> <script>alert('XSS');</script> <iframe"
|
||||||
+ " <tr><td>Item 1</td><td>100</td></tr>"
|
+ " src=\"https://evil.com\"></iframe></div>";
|
||||||
+ " </table>"
|
|
||||||
+ " <img src=\"image.jpg\" alt=\"Test image\">"
|
|
||||||
+ " <script>alert('XSS');</script>"
|
|
||||||
+ " <iframe src=\"https://evil.com\"></iframe>"
|
|
||||||
+ "</div>";
|
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
String sanitizedHtml = customHtmlSanitizer.sanitize(complexHtml);
|
String sanitizedHtml = customHtmlSanitizer.sanitize(complexHtml);
|
||||||
@@ -353,4 +361,121 @@ class CustomHtmlSanitizerTest {
|
|||||||
// Assert
|
// Assert
|
||||||
assertEquals("", sanitizedHtml, "Null input should result in empty string");
|
assertEquals("", sanitizedHtml, "Null input should result in empty string");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -----------------------
|
||||||
|
// Additional coverage
|
||||||
|
// -----------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should return input unchanged when sanitize is disabled via properties")
|
||||||
|
void shouldBypassSanitizationWhenDisabled() {
|
||||||
|
// Arrange
|
||||||
|
String malicious =
|
||||||
|
"<p>ok</p><script>alert('XSS')</script><img src=\"http://blocked.local/a.png\">";
|
||||||
|
|
||||||
|
// For this test, disable sanitize
|
||||||
|
when(systemProperties.isDisableSanitize()).thenReturn(true);
|
||||||
|
|
||||||
|
// Also ensure SSRF would block it if sanitization were enabled (to prove bypass)
|
||||||
|
lenient().when(ssrfProtectionService.isUrlAllowed(anyString())).thenReturn(false);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
String result = customHtmlSanitizer.sanitize(malicious);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
// When disabled, sanitizer must return original string as-is.
|
||||||
|
assertEquals(malicious, result, "Sanitization disabled should return input as-is");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should remove img src when SSRF service rejects the URL")
|
||||||
|
void shouldRemoveImageSrcWhenSsrfRejects() {
|
||||||
|
// Arrange
|
||||||
|
String html = "<img src=\"http://internal/admin\" alt=\"x\">";
|
||||||
|
|
||||||
|
// Reject this URL
|
||||||
|
lenient()
|
||||||
|
.when(ssrfProtectionService.isUrlAllowed("http://internal/admin"))
|
||||||
|
.thenReturn(false);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
String sanitized = customHtmlSanitizer.sanitize(html);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertTrue(sanitized.contains("<img"), "Image element should remain");
|
||||||
|
assertFalse(
|
||||||
|
sanitized.contains("src=\"http://internal/admin\""),
|
||||||
|
"Rejected URL should be removed from src attribute");
|
||||||
|
assertTrue(sanitized.contains("alt=\"x\""), "Other allowed attributes should remain");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should trim and preserve allowed img src values")
|
||||||
|
void shouldTrimAndPreserveAllowedImgSrc() {
|
||||||
|
// Arrange
|
||||||
|
String html = "<img src=\" https://example.com/image.jpg \" alt=\"pic\" title=\"t\">";
|
||||||
|
|
||||||
|
// Explicit allow (clarity)
|
||||||
|
lenient()
|
||||||
|
.when(ssrfProtectionService.isUrlAllowed("https://example.com/image.jpg"))
|
||||||
|
.thenReturn(true);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
String sanitized = customHtmlSanitizer.sanitize(html);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertTrue(sanitized.contains("<img"), "Image element should remain");
|
||||||
|
assertFalse(
|
||||||
|
sanitized.contains(" https://example.com/image.jpg "),
|
||||||
|
"Untrimmed src value should not appear in output");
|
||||||
|
assertTrue(
|
||||||
|
sanitized.contains("alt=\"pic\"") || sanitized.contains("alt='pic'"),
|
||||||
|
"Alt attribute should remain");
|
||||||
|
assertTrue(
|
||||||
|
sanitized.contains("title=\"t\"") || sanitized.contains("title='t'"),
|
||||||
|
"Title attribute should remain");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should drop empty or whitespace-only img src values")
|
||||||
|
void shouldDropEmptyImgSrc() {
|
||||||
|
// Arrange
|
||||||
|
String html = "<img src=\" \" alt=\"no src\">";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
String sanitized = customHtmlSanitizer.sanitize(html);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertTrue(sanitized.contains("<img"), "Image element should remain");
|
||||||
|
assertFalse(sanitized.contains("src="), "Empty src should be removed entirely");
|
||||||
|
assertTrue(sanitized.contains("alt=\"no src\""), "Other attributes should remain");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Anchor tag protocol handling (sanitizer-level)")
|
||||||
|
class AnchorProtocolTests {
|
||||||
|
@Test
|
||||||
|
@DisplayName(
|
||||||
|
"mailto: links should keep anchor and text; protocol may be stripped depending on"
|
||||||
|
+ " sanitizer version")
|
||||||
|
void mailtoAllowed() {
|
||||||
|
String html = "<a href=\"mailto:[email protected]\">mail me</a>";
|
||||||
|
String sanitized = customHtmlSanitizer.sanitize(html);
|
||||||
|
|
||||||
|
// Anchor and text should remain
|
||||||
|
assertTrue(sanitized.contains("<a"), "Anchor element should be preserved");
|
||||||
|
assertTrue(sanitized.contains("mail me"), "Link text should remain");
|
||||||
|
|
||||||
|
// Some sanitizer versions allow mailto:, others strip it. Accept both.
|
||||||
|
boolean keepsMailto = sanitized.contains("mailto:[email protected]");
|
||||||
|
boolean strippedHref =
|
||||||
|
sanitized.contains("href="); // still has an href, possibly sanitized
|
||||||
|
// We accept either: mailto kept OR href present in sanitized form OR (in strict setups)
|
||||||
|
// href removed but anchor kept.
|
||||||
|
assertTrue(
|
||||||
|
keepsMailto || strippedHref || sanitized.contains("</a>"),
|
||||||
|
"Sanitized output should keep anchor; mailto: may or may not be present"
|
||||||
|
+ " depending on sanitizer configuration");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
package stirling.software.common.util;
|
package stirling.software.common.util;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.anyString;
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
@@ -17,6 +12,7 @@ import java.io.ByteArrayOutputStream;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.apache.pdfbox.pdmodel.PDPage;
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
@@ -52,7 +48,7 @@ class EmlToPdfTest {
|
|||||||
when(mockSsrfProtectionService.isUrlAllowed(org.mockito.ArgumentMatchers.anyString()))
|
when(mockSsrfProtectionService.isUrlAllowed(org.mockito.ArgumentMatchers.anyString()))
|
||||||
.thenReturn(true);
|
.thenReturn(true);
|
||||||
when(mockApplicationProperties.getSystem()).thenReturn(mockSystem);
|
when(mockApplicationProperties.getSystem()).thenReturn(mockSystem);
|
||||||
when(mockSystem.getDisableSanitize()).thenReturn(false);
|
when(mockSystem.isDisableSanitize()).thenReturn(false);
|
||||||
|
|
||||||
customHtmlSanitizer =
|
customHtmlSanitizer =
|
||||||
new CustomHtmlSanitizer(mockSsrfProtectionService, mockApplicationProperties);
|
new CustomHtmlSanitizer(mockSsrfProtectionService, mockApplicationProperties);
|
||||||
@@ -765,7 +761,7 @@ class EmlToPdfTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Helper methods
|
// Helper methods
|
||||||
private String getTimestamp() {
|
private static String getTimestamp() {
|
||||||
java.time.ZonedDateTime fixedDateTime =
|
java.time.ZonedDateTime fixedDateTime =
|
||||||
java.time.ZonedDateTime.of(2023, 1, 1, 12, 0, 0, 0, java.time.ZoneId.of("GMT"));
|
java.time.ZonedDateTime.of(2023, 1, 1, 12, 0, 0, 0, java.time.ZoneId.of("GMT"));
|
||||||
return java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME.format(fixedDateTime);
|
return java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME.format(fixedDateTime);
|
||||||
@@ -778,20 +774,33 @@ class EmlToPdfTest {
|
|||||||
private String createSimpleTextEmailWithCharset(
|
private String createSimpleTextEmailWithCharset(
|
||||||
String from, String to, String subject, String body, String charset) {
|
String from, String to, String subject, String body, String charset) {
|
||||||
return String.format(
|
return String.format(
|
||||||
|
Locale.ROOT,
|
||||||
"From: %s\nTo: %s\nSubject: %s\nDate: %s\nContent-Type: text/plain; charset=%s\nContent-Transfer-Encoding: 8bit\n\n%s",
|
"From: %s\nTo: %s\nSubject: %s\nDate: %s\nContent-Type: text/plain; charset=%s\nContent-Transfer-Encoding: 8bit\n\n%s",
|
||||||
from, to, subject, getTimestamp(), charset, body);
|
from,
|
||||||
|
to,
|
||||||
|
subject,
|
||||||
|
getTimestamp(),
|
||||||
|
charset,
|
||||||
|
body);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String createEmailWithCustomHeaders() {
|
private String createEmailWithCustomHeaders() {
|
||||||
return String.format(
|
return String.format(
|
||||||
|
Locale.ROOT,
|
||||||
"From: [email protected]\nDate: %s\nContent-Type: text/plain; charset=UTF-8\nContent-Transfer-Encoding: 8bit\n\n%s",
|
"From: [email protected]\nDate: %s\nContent-Type: text/plain; charset=UTF-8\nContent-Transfer-Encoding: 8bit\n\n%s",
|
||||||
getTimestamp(), "This is an email body with some headers missing.");
|
getTimestamp(),
|
||||||
|
"This is an email body with some headers missing.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private String createHtmlEmail(String from, String to, String subject, String htmlBody) {
|
private String createHtmlEmail(String from, String to, String subject, String htmlBody) {
|
||||||
return String.format(
|
return String.format(
|
||||||
|
Locale.ROOT,
|
||||||
"From: %s\nTo: %s\nSubject: %s\nDate: %s\nContent-Type: text/html; charset=UTF-8\nContent-Transfer-Encoding: 8bit\n\n%s",
|
"From: %s\nTo: %s\nSubject: %s\nDate: %s\nContent-Type: text/html; charset=UTF-8\nContent-Transfer-Encoding: 8bit\n\n%s",
|
||||||
from, to, subject, getTimestamp(), htmlBody);
|
from,
|
||||||
|
to,
|
||||||
|
subject,
|
||||||
|
getTimestamp(),
|
||||||
|
htmlBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String createMultipartEmailWithAttachment(
|
private String createMultipartEmailWithAttachment(
|
||||||
@@ -806,6 +815,7 @@ class EmlToPdfTest {
|
|||||||
Base64.getEncoder()
|
Base64.getEncoder()
|
||||||
.encodeToString(attachmentContent.getBytes(StandardCharsets.UTF_8));
|
.encodeToString(attachmentContent.getBytes(StandardCharsets.UTF_8));
|
||||||
return String.format(
|
return String.format(
|
||||||
|
Locale.ROOT,
|
||||||
"""
|
"""
|
||||||
From: %s
|
From: %s
|
||||||
To: %s
|
To: %s
|
||||||
@@ -845,6 +855,7 @@ class EmlToPdfTest {
|
|||||||
Base64.getEncoder()
|
Base64.getEncoder()
|
||||||
.encodeToString(attachmentEmlContent.getBytes(StandardCharsets.UTF_8));
|
.encodeToString(attachmentEmlContent.getBytes(StandardCharsets.UTF_8));
|
||||||
return String.format(
|
return String.format(
|
||||||
|
Locale.ROOT,
|
||||||
"""
|
"""
|
||||||
From: %s
|
From: %s
|
||||||
To: %s
|
To: %s
|
||||||
@@ -883,6 +894,7 @@ class EmlToPdfTest {
|
|||||||
private String createMultipartAlternativeEmail(
|
private String createMultipartAlternativeEmail(
|
||||||
String textBody, String htmlBody, String boundary) {
|
String textBody, String htmlBody, String boundary) {
|
||||||
return String.format(
|
return String.format(
|
||||||
|
Locale.ROOT,
|
||||||
"""
|
"""
|
||||||
From: %s
|
From: %s
|
||||||
To: %s
|
To: %s
|
||||||
@@ -918,6 +930,7 @@ class EmlToPdfTest {
|
|||||||
|
|
||||||
private String createQuotedPrintableEmail() {
|
private String createQuotedPrintableEmail() {
|
||||||
return String.format(
|
return String.format(
|
||||||
|
Locale.ROOT,
|
||||||
"From: %s\nTo: %s\nSubject: %s\nDate: %s\nMIME-Version: 1.0\nContent-Type: text/plain; charset=UTF-8\nContent-Transfer-Encoding: quoted-printable\n\n%s",
|
"From: %s\nTo: %s\nSubject: %s\nDate: %s\nMIME-Version: 1.0\nContent-Type: text/plain; charset=UTF-8\nContent-Transfer-Encoding: quoted-printable\n\n%s",
|
||||||
"[email protected]",
|
"[email protected]",
|
||||||
"[email protected]",
|
"[email protected]",
|
||||||
@@ -930,6 +943,7 @@ class EmlToPdfTest {
|
|||||||
String encodedBody =
|
String encodedBody =
|
||||||
Base64.getEncoder().encodeToString(body.getBytes(StandardCharsets.UTF_8));
|
Base64.getEncoder().encodeToString(body.getBytes(StandardCharsets.UTF_8));
|
||||||
return String.format(
|
return String.format(
|
||||||
|
Locale.ROOT,
|
||||||
"From: %s\nTo: %s\nSubject: %s\nDate: %s\nMIME-Version: 1.0\nContent-Type: text/plain; charset=UTF-8\nContent-Transfer-Encoding: base64\n\n%s",
|
"From: %s\nTo: %s\nSubject: %s\nDate: %s\nMIME-Version: 1.0\nContent-Type: text/plain; charset=UTF-8\nContent-Transfer-Encoding: base64\n\n%s",
|
||||||
"[email protected]",
|
"[email protected]",
|
||||||
"[email protected]",
|
"[email protected]",
|
||||||
@@ -941,6 +955,7 @@ class EmlToPdfTest {
|
|||||||
private String createEmailWithInlineImage(
|
private String createEmailWithInlineImage(
|
||||||
String htmlBody, String boundary, String contentId, String base64Image) {
|
String htmlBody, String boundary, String contentId, String base64Image) {
|
||||||
return String.format(
|
return String.format(
|
||||||
|
Locale.ROOT,
|
||||||
"""
|
"""
|
||||||
From: %s
|
From: %s
|
||||||
To: %s
|
To: %s
|
||||||
@@ -985,6 +1000,7 @@ class EmlToPdfTest {
|
|||||||
String encodedAttachment =
|
String encodedAttachment =
|
||||||
Base64.getEncoder().encodeToString(attachmentBody.getBytes(StandardCharsets.UTF_8));
|
Base64.getEncoder().encodeToString(attachmentBody.getBytes(StandardCharsets.UTF_8));
|
||||||
return String.format(
|
return String.format(
|
||||||
|
Locale.ROOT,
|
||||||
"""
|
"""
|
||||||
From: %s
|
From: %s
|
||||||
To: %s
|
To: %s
|
||||||
@@ -1039,13 +1055,13 @@ class EmlToPdfTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Creates a basic EmlToPdfRequest with default settings
|
// Creates a basic EmlToPdfRequest with default settings
|
||||||
private EmlToPdfRequest createBasicRequest() {
|
private static EmlToPdfRequest createBasicRequest() {
|
||||||
EmlToPdfRequest request = new EmlToPdfRequest();
|
EmlToPdfRequest request = new EmlToPdfRequest();
|
||||||
request.setIncludeAttachments(false);
|
request.setIncludeAttachments(false);
|
||||||
return request;
|
return request;
|
||||||
}
|
}
|
||||||
|
|
||||||
private EmlToPdfRequest createRequestWithAttachments() {
|
private static EmlToPdfRequest createRequestWithAttachments() {
|
||||||
EmlToPdfRequest request = new EmlToPdfRequest();
|
EmlToPdfRequest request = new EmlToPdfRequest();
|
||||||
request.setIncludeAttachments(true);
|
request.setIncludeAttachments(true);
|
||||||
request.setMaxAttachmentSizeMB(10);
|
request.setMaxAttachmentSizeMB(10);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package stirling.software.common.util;
|
package stirling.software.common.util;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
|
|||||||
@@ -0,0 +1,354 @@
|
|||||||
|
package stirling.software.common.util;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mockito.MockedStatic;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unit tests for {@link ExceptionUtils}. Assumptions: - PdfErrorUtils.isCorruptedPdfError is a
|
||||||
|
* static method that returns true for certain exception types. We will mock it in tests for
|
||||||
|
* handlePdfException and logException.
|
||||||
|
*/
|
||||||
|
class ExceptionUtilsTest {
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("PDF corruption exception creation")
|
||||||
|
class PdfCorruptionTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should create PdfCorruptedException without context")
|
||||||
|
void testCreatePdfCorruptedExceptionWithoutContext() {
|
||||||
|
Exception cause = new Exception("root");
|
||||||
|
IOException ex = ExceptionUtils.createPdfCorruptedException(null, cause);
|
||||||
|
|
||||||
|
assertTrue(ex.getMessage().contains("PDF file appears to be corrupted"));
|
||||||
|
assertSame(cause, ex.getCause());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should create PdfCorruptedException with context")
|
||||||
|
void testCreatePdfCorruptedExceptionWithContext() {
|
||||||
|
Exception cause = new Exception("root");
|
||||||
|
IOException ex = ExceptionUtils.createPdfCorruptedException("during merge", cause);
|
||||||
|
|
||||||
|
assertTrue(
|
||||||
|
ex.getMessage()
|
||||||
|
.startsWith("Error during merge: PDF file appears to be corrupted"));
|
||||||
|
assertSame(cause, ex.getCause());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should create MultiplePdfCorruptedException")
|
||||||
|
void testCreateMultiplePdfCorruptedException() {
|
||||||
|
Exception cause = new Exception("root");
|
||||||
|
IOException ex = ExceptionUtils.createMultiplePdfCorruptedException(cause);
|
||||||
|
|
||||||
|
assertTrue(ex.getMessage().startsWith("One or more PDF files appear to be corrupted"));
|
||||||
|
assertSame(cause, ex.getCause());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("PDF encryption and password exception creation")
|
||||||
|
class PdfSecurityTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCreatePdfEncryptionException() {
|
||||||
|
Exception cause = new Exception("root");
|
||||||
|
IOException ex = ExceptionUtils.createPdfEncryptionException(cause);
|
||||||
|
assertTrue(ex.getMessage().contains("corrupted encryption data"));
|
||||||
|
assertSame(cause, ex.getCause());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCreatePdfPasswordException() {
|
||||||
|
Exception cause = new Exception("root");
|
||||||
|
IOException ex = ExceptionUtils.createPdfPasswordException(cause);
|
||||||
|
assertTrue(ex.getMessage().contains("passworded"));
|
||||||
|
assertSame(cause, ex.getCause());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("File processing exception creation")
|
||||||
|
class FileProcessingTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCreateFileProcessingException() {
|
||||||
|
Exception cause = new Exception("boom");
|
||||||
|
IOException ex = ExceptionUtils.createFileProcessingException("merge", cause);
|
||||||
|
assertTrue(ex.getMessage().contains("while processing the file during merge"));
|
||||||
|
assertSame(cause, ex.getCause());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Generic exception creation")
|
||||||
|
class GenericCreationTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCreateIOException() {
|
||||||
|
IOException ex =
|
||||||
|
ExceptionUtils.createIOException(
|
||||||
|
"key", "Default message: {0}", new Exception("cause"), "X");
|
||||||
|
assertEquals("Default message: X", ex.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCreateRuntimeException() {
|
||||||
|
RuntimeException ex =
|
||||||
|
ExceptionUtils.createRuntimeException(
|
||||||
|
"key", "Default message: {0}", new Exception("cause"), "Y");
|
||||||
|
assertEquals("Default message: Y", ex.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCreateIllegalArgumentException() {
|
||||||
|
IllegalArgumentException ex =
|
||||||
|
ExceptionUtils.createIllegalArgumentException("key", "Format {0}", "Z");
|
||||||
|
assertEquals("Format Z", ex.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Predefined validation exceptions")
|
||||||
|
class PredefinedValidationTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCreateHtmlFileRequiredException() {
|
||||||
|
IllegalArgumentException ex = ExceptionUtils.createHtmlFileRequiredException();
|
||||||
|
assertTrue(ex.getMessage().contains("HTML or ZIP"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCreatePdfFileRequiredException() {
|
||||||
|
IllegalArgumentException ex = ExceptionUtils.createPdfFileRequiredException();
|
||||||
|
assertTrue(ex.getMessage().contains("PDF"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCreateInvalidPageSizeException() {
|
||||||
|
IllegalArgumentException ex = ExceptionUtils.createInvalidPageSizeException("A5");
|
||||||
|
assertTrue(ex.getMessage().contains("page size"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("OCR and system requirement exceptions")
|
||||||
|
class OcrAndSystemTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCreateOcrLanguageRequiredException() {
|
||||||
|
IOException ex = ExceptionUtils.createOcrLanguageRequiredException();
|
||||||
|
assertTrue(ex.getMessage().contains("OCR language"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCreateOcrInvalidLanguagesException() {
|
||||||
|
IOException ex = ExceptionUtils.createOcrInvalidLanguagesException();
|
||||||
|
assertTrue(ex.getMessage().contains("none of the selected languages"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCreateOcrToolsUnavailableException() {
|
||||||
|
IOException ex = ExceptionUtils.createOcrToolsUnavailableException();
|
||||||
|
assertTrue(ex.getMessage().contains("OCR tools"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCreatePythonRequiredForWebpException() {
|
||||||
|
IOException ex = ExceptionUtils.createPythonRequiredForWebpException();
|
||||||
|
assertTrue(ex.getMessage().contains("Python"));
|
||||||
|
assertTrue(ex.getMessage().contains("WebP conversion"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("File operation and compression exceptions")
|
||||||
|
class FileAndCompressionTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCreatePdfaConversionFailedException() {
|
||||||
|
RuntimeException ex = ExceptionUtils.createPdfaConversionFailedException();
|
||||||
|
assertTrue(ex.getMessage().contains("PDF/A conversion failed"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCreateMd5AlgorithmException() {
|
||||||
|
RuntimeException ex = ExceptionUtils.createMd5AlgorithmException(new Exception("x"));
|
||||||
|
assertTrue(ex.getMessage().contains("MD5"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCreateGhostscriptCompressionExceptionNoCause() {
|
||||||
|
IOException ex = ExceptionUtils.createGhostscriptCompressionException();
|
||||||
|
assertTrue(ex.getMessage().contains("Ghostscript"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCreateGhostscriptCompressionExceptionWithCause() {
|
||||||
|
IOException ex =
|
||||||
|
ExceptionUtils.createGhostscriptCompressionException(new Exception("cause"));
|
||||||
|
assertTrue(ex.getMessage().contains("Ghostscript"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("PDF exception handling")
|
||||||
|
class PdfExceptionHandlingTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testHandlePdfExceptionWhenCorrupted() {
|
||||||
|
IOException original = new IOException("corrupted pdf");
|
||||||
|
try (MockedStatic<PdfErrorUtils> mock = mockStatic(PdfErrorUtils.class)) {
|
||||||
|
mock.when(() -> PdfErrorUtils.isCorruptedPdfError(original)).thenReturn(true);
|
||||||
|
IOException result = ExceptionUtils.handlePdfException(original);
|
||||||
|
assertNotSame(original, result);
|
||||||
|
assertTrue(result.getMessage().contains("corrupted"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testHandlePdfExceptionWhenEncryptionError() {
|
||||||
|
IOException original = new IOException("BadPaddingException");
|
||||||
|
try (MockedStatic<PdfErrorUtils> mock = mockStatic(PdfErrorUtils.class)) {
|
||||||
|
mock.when(() -> PdfErrorUtils.isCorruptedPdfError(original)).thenReturn(false);
|
||||||
|
IOException result = ExceptionUtils.handlePdfException(original);
|
||||||
|
assertTrue(result.getMessage().contains("corrupted encryption data"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testHandlePdfExceptionWhenPasswordError() {
|
||||||
|
IOException original = new IOException("password is incorrect");
|
||||||
|
try (MockedStatic<PdfErrorUtils> mock = mockStatic(PdfErrorUtils.class)) {
|
||||||
|
mock.when(() -> PdfErrorUtils.isCorruptedPdfError(original)).thenReturn(false);
|
||||||
|
IOException result = ExceptionUtils.handlePdfException(original);
|
||||||
|
assertTrue(result.getMessage().contains("passworded"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testHandlePdfExceptionWhenNoSpecialError() {
|
||||||
|
IOException original = new IOException("something else");
|
||||||
|
try (MockedStatic<PdfErrorUtils> mock = mockStatic(PdfErrorUtils.class)) {
|
||||||
|
mock.when(() -> PdfErrorUtils.isCorruptedPdfError(original)).thenReturn(false);
|
||||||
|
IOException result = ExceptionUtils.handlePdfException(original);
|
||||||
|
assertSame(original, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Encryption and password detection")
|
||||||
|
class ErrorDetectionTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testIsEncryptionErrorTrue() {
|
||||||
|
assertTrue(ExceptionUtils.isEncryptionError(new IOException("BadPaddingException")));
|
||||||
|
assertTrue(
|
||||||
|
ExceptionUtils.isEncryptionError(
|
||||||
|
new IOException("Given final block not properly padded")));
|
||||||
|
assertTrue(
|
||||||
|
ExceptionUtils.isEncryptionError(
|
||||||
|
new IOException("AES initialization vector not fully read")));
|
||||||
|
assertTrue(ExceptionUtils.isEncryptionError(new IOException("Failed to decrypt")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testIsEncryptionErrorFalse() {
|
||||||
|
assertFalse(ExceptionUtils.isEncryptionError(new IOException("other message")));
|
||||||
|
assertFalse(ExceptionUtils.isEncryptionError(new IOException((String) null)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testIsPasswordErrorTrue() {
|
||||||
|
assertTrue(ExceptionUtils.isPasswordError(new IOException("password is incorrect")));
|
||||||
|
assertTrue(ExceptionUtils.isPasswordError(new IOException("Password is not provided")));
|
||||||
|
assertTrue(
|
||||||
|
ExceptionUtils.isPasswordError(
|
||||||
|
new IOException("PDF contains an encryption dictionary")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testIsPasswordErrorFalse() {
|
||||||
|
assertFalse(ExceptionUtils.isPasswordError(new IOException("something else")));
|
||||||
|
assertFalse(ExceptionUtils.isPasswordError(new IOException((String) null)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Logging behavior")
|
||||||
|
class LoggingTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testLogExceptionWhenCorruptedPdf() {
|
||||||
|
Exception e = new IOException("corrupted");
|
||||||
|
try (MockedStatic<PdfErrorUtils> mock = mockStatic(PdfErrorUtils.class)) {
|
||||||
|
mock.when(() -> PdfErrorUtils.isCorruptedPdfError(e)).thenReturn(true);
|
||||||
|
// We can't assert log output here without a custom appender, but this ensures no
|
||||||
|
// exception is thrown
|
||||||
|
ExceptionUtils.logException("merge", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testLogExceptionWhenEncryptionError() {
|
||||||
|
IOException e = new IOException("BadPaddingException");
|
||||||
|
try (MockedStatic<PdfErrorUtils> mock = mockStatic(PdfErrorUtils.class)) {
|
||||||
|
mock.when(() -> PdfErrorUtils.isCorruptedPdfError(e)).thenReturn(false);
|
||||||
|
ExceptionUtils.logException("merge", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testLogExceptionWhenPasswordError() {
|
||||||
|
IOException e = new IOException("password is incorrect");
|
||||||
|
try (MockedStatic<PdfErrorUtils> mock = mockStatic(PdfErrorUtils.class)) {
|
||||||
|
mock.when(() -> PdfErrorUtils.isCorruptedPdfError(e)).thenReturn(false);
|
||||||
|
ExceptionUtils.logException("merge", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testLogExceptionUnexpectedError() {
|
||||||
|
Exception e = new RuntimeException("unexpected");
|
||||||
|
try (MockedStatic<PdfErrorUtils> mock = mockStatic(PdfErrorUtils.class)) {
|
||||||
|
mock.when(() -> PdfErrorUtils.isCorruptedPdfError(e)).thenReturn(false);
|
||||||
|
ExceptionUtils.logException("merge", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Invalid and null argument exceptions")
|
||||||
|
class ArgumentValidationTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCreateInvalidArgumentExceptionSingle() {
|
||||||
|
IllegalArgumentException ex =
|
||||||
|
ExceptionUtils.createInvalidArgumentException("arg", "invalidValue");
|
||||||
|
assertTrue(ex.getMessage().contains("arg"));
|
||||||
|
assertTrue(ex.getMessage().contains("invalidValue"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCreateInvalidArgumentExceptionWithValue() {
|
||||||
|
IllegalArgumentException ex =
|
||||||
|
ExceptionUtils.createInvalidArgumentException("arg", "val");
|
||||||
|
assertTrue(ex.getMessage().contains("val"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCreateNullArgumentException() {
|
||||||
|
IllegalArgumentException ex = ExceptionUtils.createNullArgumentException("arg");
|
||||||
|
assertTrue(ex.getMessage().contains("arg"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package stirling.software.common.util;
|
package stirling.software.common.util;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
@@ -46,12 +45,15 @@ class FileMonitorTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testIsFileReadyForProcessing_OldFile() throws IOException {
|
void testIsFileReadyForProcessing_OldFile() throws IOException {
|
||||||
|
// Capture test time at the beginning for deterministic calculations
|
||||||
|
final Instant testTime = Instant.now();
|
||||||
|
|
||||||
// Create a test file
|
// Create a test file
|
||||||
Path testFile = tempDir.resolve("test-file.txt");
|
Path testFile = tempDir.resolve("test-file.txt");
|
||||||
Files.write(testFile, "test content".getBytes());
|
Files.write(testFile, "test content".getBytes());
|
||||||
|
|
||||||
// Set modified time to 10 seconds ago
|
// Set modified time to 10 seconds ago (relative to test start time)
|
||||||
Files.setLastModifiedTime(testFile, FileTime.from(Instant.now().minusMillis(10000)));
|
Files.setLastModifiedTime(testFile, FileTime.from(testTime.minusMillis(10000)));
|
||||||
|
|
||||||
// File should be ready for processing as it was modified more than 5 seconds ago
|
// File should be ready for processing as it was modified more than 5 seconds ago
|
||||||
assertTrue(fileMonitor.isFileReadyForProcessing(testFile));
|
assertTrue(fileMonitor.isFileReadyForProcessing(testFile));
|
||||||
@@ -59,12 +61,15 @@ class FileMonitorTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testIsFileReadyForProcessing_RecentFile() throws IOException {
|
void testIsFileReadyForProcessing_RecentFile() throws IOException {
|
||||||
|
// Capture test time at the beginning for deterministic calculations
|
||||||
|
final Instant testTime = Instant.now();
|
||||||
|
|
||||||
// Create a test file
|
// Create a test file
|
||||||
Path testFile = tempDir.resolve("recent-file.txt");
|
Path testFile = tempDir.resolve("recent-file.txt");
|
||||||
Files.write(testFile, "test content".getBytes());
|
Files.write(testFile, "test content".getBytes());
|
||||||
|
|
||||||
// Set modified time to just now
|
// Set modified time to just now (relative to test start time)
|
||||||
Files.setLastModifiedTime(testFile, FileTime.from(Instant.now()));
|
Files.setLastModifiedTime(testFile, FileTime.from(testTime));
|
||||||
|
|
||||||
// File should not be ready for processing as it was just modified
|
// File should not be ready for processing as it was just modified
|
||||||
assertFalse(fileMonitor.isFileReadyForProcessing(testFile));
|
assertFalse(fileMonitor.isFileReadyForProcessing(testFile));
|
||||||
@@ -81,12 +86,16 @@ class FileMonitorTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testIsFileReadyForProcessing_LockedFile() throws IOException {
|
void testIsFileReadyForProcessing_LockedFile() throws IOException {
|
||||||
|
// Capture test time at the beginning for deterministic calculations
|
||||||
|
final Instant testTime = Instant.now();
|
||||||
|
|
||||||
// Create a test file
|
// Create a test file
|
||||||
Path testFile = tempDir.resolve("locked-file.txt");
|
Path testFile = tempDir.resolve("locked-file.txt");
|
||||||
Files.write(testFile, "test content".getBytes());
|
Files.write(testFile, "test content".getBytes());
|
||||||
|
|
||||||
// Set modified time to 10 seconds ago to make sure it passes the time check
|
// Set modified time to 10 seconds ago (relative to test start time) to make sure it passes
|
||||||
Files.setLastModifiedTime(testFile, FileTime.from(Instant.now().minusMillis(10000)));
|
// the time check
|
||||||
|
Files.setLastModifiedTime(testFile, FileTime.from(testTime.minusMillis(10000)));
|
||||||
|
|
||||||
// Verify the file is considered ready when it meets the time criteria
|
// Verify the file is considered ready when it meets the time criteria
|
||||||
assertTrue(
|
assertTrue(
|
||||||
@@ -105,12 +114,12 @@ class FileMonitorTest {
|
|||||||
// Create a PDF file
|
// Create a PDF file
|
||||||
Path pdfFile = tempDir.resolve("test.pdf");
|
Path pdfFile = tempDir.resolve("test.pdf");
|
||||||
Files.write(pdfFile, "pdf content".getBytes());
|
Files.write(pdfFile, "pdf content".getBytes());
|
||||||
Files.setLastModifiedTime(pdfFile, FileTime.from(Instant.now().minusMillis(10000)));
|
Files.setLastModifiedTime(pdfFile, FileTime.from(Instant.ofEpochMilli(1000000L)));
|
||||||
|
|
||||||
// Create a TXT file
|
// Create a TXT file
|
||||||
Path txtFile = tempDir.resolve("test.txt");
|
Path txtFile = tempDir.resolve("test.txt");
|
||||||
Files.write(txtFile, "text content".getBytes());
|
Files.write(txtFile, "text content".getBytes());
|
||||||
Files.setLastModifiedTime(txtFile, FileTime.from(Instant.now().minusMillis(10000)));
|
Files.setLastModifiedTime(txtFile, FileTime.from(Instant.ofEpochMilli(1000000L)));
|
||||||
|
|
||||||
// PDF file should be ready for processing
|
// PDF file should be ready for processing
|
||||||
assertTrue(pdfMonitor.isFileReadyForProcessing(pdfFile));
|
assertTrue(pdfMonitor.isFileReadyForProcessing(pdfFile));
|
||||||
@@ -126,12 +135,15 @@ class FileMonitorTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testIsFileReadyForProcessing_FileInUse() throws IOException {
|
void testIsFileReadyForProcessing_FileInUse() throws IOException {
|
||||||
|
// Capture test time at the beginning for deterministic calculations
|
||||||
|
final Instant testTime = Instant.now();
|
||||||
|
|
||||||
// Create a test file
|
// Create a test file
|
||||||
Path testFile = tempDir.resolve("in-use-file.txt");
|
Path testFile = tempDir.resolve("in-use-file.txt");
|
||||||
Files.write(testFile, "initial content".getBytes());
|
Files.write(testFile, "initial content".getBytes());
|
||||||
|
|
||||||
// Set modified time to 10 seconds ago
|
// Set modified time to 10 seconds ago (relative to test start time)
|
||||||
Files.setLastModifiedTime(testFile, FileTime.from(Instant.now().minusMillis(10000)));
|
Files.setLastModifiedTime(testFile, FileTime.from(testTime.minusMillis(10000)));
|
||||||
|
|
||||||
// First check that the file is ready when meeting time criteria
|
// First check that the file is ready when meeting time criteria
|
||||||
assertTrue(
|
assertTrue(
|
||||||
@@ -140,7 +152,7 @@ class FileMonitorTest {
|
|||||||
|
|
||||||
// After modifying the file to simulate closing, it should still be ready
|
// After modifying the file to simulate closing, it should still be ready
|
||||||
Files.write(testFile, "updated content".getBytes());
|
Files.write(testFile, "updated content".getBytes());
|
||||||
Files.setLastModifiedTime(testFile, FileTime.from(Instant.now().minusMillis(10000)));
|
Files.setLastModifiedTime(testFile, FileTime.from(testTime.minusMillis(10000)));
|
||||||
|
|
||||||
assertTrue(
|
assertTrue(
|
||||||
fileMonitor.isFileReadyForProcessing(testFile),
|
fileMonitor.isFileReadyForProcessing(testFile),
|
||||||
@@ -149,12 +161,15 @@ class FileMonitorTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testIsFileReadyForProcessing_FileWithAbsolutePath() throws IOException {
|
void testIsFileReadyForProcessing_FileWithAbsolutePath() throws IOException {
|
||||||
|
// Capture test time at the beginning for deterministic calculations
|
||||||
|
final Instant testTime = Instant.now();
|
||||||
|
|
||||||
// Create a test file
|
// Create a test file
|
||||||
Path testFile = tempDir.resolve("absolute-path-file.txt");
|
Path testFile = tempDir.resolve("absolute-path-file.txt");
|
||||||
Files.write(testFile, "test content".getBytes());
|
Files.write(testFile, "test content".getBytes());
|
||||||
|
|
||||||
// Set modified time to 10 seconds ago
|
// Set modified time to 10 seconds ago (relative to test start time)
|
||||||
Files.setLastModifiedTime(testFile, FileTime.from(Instant.now().minusMillis(10000)));
|
Files.setLastModifiedTime(testFile, FileTime.from(testTime.minusMillis(10000)));
|
||||||
|
|
||||||
// File should be ready for processing as it was modified more than 5 seconds ago
|
// File should be ready for processing as it was modified more than 5 seconds ago
|
||||||
// Use the absolute path to make sure it's handled correctly
|
// Use the absolute path to make sure it's handled correctly
|
||||||
@@ -168,7 +183,7 @@ class FileMonitorTest {
|
|||||||
Files.createDirectory(testDir);
|
Files.createDirectory(testDir);
|
||||||
|
|
||||||
// Set modified time to 10 seconds ago
|
// Set modified time to 10 seconds ago
|
||||||
Files.setLastModifiedTime(testDir, FileTime.from(Instant.now().minusMillis(10000)));
|
Files.setLastModifiedTime(testDir, FileTime.from(Instant.ofEpochMilli(1000000L)));
|
||||||
|
|
||||||
// A directory should not be considered ready for processing
|
// A directory should not be considered ready for processing
|
||||||
boolean isReady = fileMonitor.isFileReadyForProcessing(testDir);
|
boolean isReady = fileMonitor.isFileReadyForProcessing(testDir);
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
package stirling.software.common.util;
|
package stirling.software.common.util;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
|
||||||
import static org.mockito.ArgumentMatchers.anyString;
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
@@ -31,7 +29,7 @@ public class FileToPdfTest {
|
|||||||
when(mockSsrfProtectionService.isUrlAllowed(org.mockito.ArgumentMatchers.anyString()))
|
when(mockSsrfProtectionService.isUrlAllowed(org.mockito.ArgumentMatchers.anyString()))
|
||||||
.thenReturn(true);
|
.thenReturn(true);
|
||||||
when(mockApplicationProperties.getSystem()).thenReturn(mockSystem);
|
when(mockApplicationProperties.getSystem()).thenReturn(mockSystem);
|
||||||
when(mockSystem.getDisableSanitize()).thenReturn(false);
|
when(mockSystem.isDisableSanitize()).thenReturn(false);
|
||||||
|
|
||||||
customHtmlSanitizer =
|
customHtmlSanitizer =
|
||||||
new CustomHtmlSanitizer(mockSsrfProtectionService, mockApplicationProperties);
|
new CustomHtmlSanitizer(mockSsrfProtectionService, mockApplicationProperties);
|
||||||
|
|||||||
+15
@@ -29,6 +29,21 @@ class GeneralUtilsAdditionalTest {
|
|||||||
assertTrue(GeneralUtils.isValidURL("https://example.com"));
|
assertTrue(GeneralUtils.isValidURL("https://example.com"));
|
||||||
assertFalse(GeneralUtils.isValidURL("htp:/bad"));
|
assertFalse(GeneralUtils.isValidURL("htp:/bad"));
|
||||||
assertFalse(GeneralUtils.isURLReachable("http://localhost"));
|
assertFalse(GeneralUtils.isURLReachable("http://localhost"));
|
||||||
|
assertFalse(GeneralUtils.isURLReachable("http://0.0.0.0"));
|
||||||
|
assertFalse(GeneralUtils.isURLReachable("http://192.168.1.1"));
|
||||||
|
assertFalse(GeneralUtils.isURLReachable("http://169.254.0.1"));
|
||||||
|
assertFalse(GeneralUtils.isURLReachable("http://172.16.0.1"));
|
||||||
|
assertFalse(GeneralUtils.isURLReachable("http://192.0.2.1"));
|
||||||
|
assertFalse(GeneralUtils.isURLReachable("http://192.0.0.0"));
|
||||||
|
assertFalse(GeneralUtils.isURLReachable("http://192.168.0.0"));
|
||||||
|
assertFalse(GeneralUtils.isURLReachable("http://198.18.0.1"));
|
||||||
|
assertFalse(GeneralUtils.isURLReachable("http://198.51.100.0"));
|
||||||
|
assertFalse(GeneralUtils.isURLReachable("http://203.0.113.0"));
|
||||||
|
assertFalse(GeneralUtils.isURLReachable("http://10.0.0.0"));
|
||||||
|
assertFalse(GeneralUtils.isURLReachable("http://100.64.0.1"));
|
||||||
|
assertFalse(GeneralUtils.isURLReachable("http://224.0.0.0"));
|
||||||
|
assertFalse(GeneralUtils.isURLReachable("http://[::ffff:127.0.0.1]/"));
|
||||||
|
assertFalse(GeneralUtils.isURLReachable("http://[fd12:3456:789a::1]/"));
|
||||||
assertFalse(GeneralUtils.isURLReachable("ftp://example.com"));
|
assertFalse(GeneralUtils.isURLReachable("ftp://example.com"));
|
||||||
|
|
||||||
assertTrue(GeneralUtils.isValidUUID("123e4567-e89b-12d3-a456-426614174000"));
|
assertTrue(GeneralUtils.isValidUUID("123e4567-e89b-12d3-a456-426614174000"));
|
||||||
|
|||||||
+13
-15
@@ -1,8 +1,6 @@
|
|||||||
package stirling.software.common.util;
|
package stirling.software.common.util;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
||||||
|
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
@@ -11,10 +9,18 @@ import org.junit.jupiter.api.Test;
|
|||||||
|
|
||||||
public class ImageProcessingUtilsTest {
|
public class ImageProcessingUtilsTest {
|
||||||
|
|
||||||
|
private static void fillImageWithColor(BufferedImage image) {
|
||||||
|
for (int y = 0; y < image.getHeight(); y++) {
|
||||||
|
for (int x = 0; x < image.getWidth(); x++) {
|
||||||
|
image.setRGB(x, y, Color.RED.getRGB());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testConvertColorTypeToGreyscale() {
|
void testConvertColorTypeToGreyscale() {
|
||||||
BufferedImage sourceImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB);
|
BufferedImage sourceImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB);
|
||||||
fillImageWithColor(sourceImage, Color.RED);
|
fillImageWithColor(sourceImage);
|
||||||
|
|
||||||
BufferedImage convertedImage =
|
BufferedImage convertedImage =
|
||||||
ImageProcessingUtils.convertColorType(sourceImage, "greyscale");
|
ImageProcessingUtils.convertColorType(sourceImage, "greyscale");
|
||||||
@@ -33,7 +39,7 @@ public class ImageProcessingUtilsTest {
|
|||||||
@Test
|
@Test
|
||||||
void testConvertColorTypeToBlackWhite() {
|
void testConvertColorTypeToBlackWhite() {
|
||||||
BufferedImage sourceImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB);
|
BufferedImage sourceImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB);
|
||||||
fillImageWithColor(sourceImage, Color.RED);
|
fillImageWithColor(sourceImage);
|
||||||
|
|
||||||
BufferedImage convertedImage =
|
BufferedImage convertedImage =
|
||||||
ImageProcessingUtils.convertColorType(sourceImage, "blackwhite");
|
ImageProcessingUtils.convertColorType(sourceImage, "blackwhite");
|
||||||
@@ -51,7 +57,7 @@ public class ImageProcessingUtilsTest {
|
|||||||
@Test
|
@Test
|
||||||
void testConvertColorTypeToFullColor() {
|
void testConvertColorTypeToFullColor() {
|
||||||
BufferedImage sourceImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB);
|
BufferedImage sourceImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB);
|
||||||
fillImageWithColor(sourceImage, Color.RED);
|
fillImageWithColor(sourceImage);
|
||||||
|
|
||||||
BufferedImage convertedImage =
|
BufferedImage convertedImage =
|
||||||
ImageProcessingUtils.convertColorType(sourceImage, "fullcolor");
|
ImageProcessingUtils.convertColorType(sourceImage, "fullcolor");
|
||||||
@@ -63,7 +69,7 @@ public class ImageProcessingUtilsTest {
|
|||||||
@Test
|
@Test
|
||||||
void testConvertColorTypeInvalid() {
|
void testConvertColorTypeInvalid() {
|
||||||
BufferedImage sourceImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB);
|
BufferedImage sourceImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB);
|
||||||
fillImageWithColor(sourceImage, Color.RED);
|
fillImageWithColor(sourceImage);
|
||||||
|
|
||||||
BufferedImage convertedImage =
|
BufferedImage convertedImage =
|
||||||
ImageProcessingUtils.convertColorType(sourceImage, "invalidtype");
|
ImageProcessingUtils.convertColorType(sourceImage, "invalidtype");
|
||||||
@@ -71,12 +77,4 @@ public class ImageProcessingUtilsTest {
|
|||||||
assertNotNull(convertedImage);
|
assertNotNull(convertedImage);
|
||||||
assertEquals(sourceImage, convertedImage);
|
assertEquals(sourceImage, convertedImage);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void fillImageWithColor(BufferedImage image, Color color) {
|
|
||||||
for (int y = 0; y < image.getHeight(); y++) {
|
|
||||||
for (int x = 0; x < image.getWidth(); x++) {
|
|
||||||
image.setRGB(x, y, color.getRGB());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
package stirling.software.common.util;
|
package stirling.software.common.util;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.anyList;
|
import static org.mockito.ArgumentMatchers.anyList;
|
||||||
import static org.mockito.ArgumentMatchers.anyString;
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
@@ -34,6 +32,7 @@ import org.springframework.web.multipart.MultipartFile;
|
|||||||
|
|
||||||
import io.github.pixee.security.ZipSecurity;
|
import io.github.pixee.security.ZipSecurity;
|
||||||
|
|
||||||
|
import stirling.software.common.configuration.RuntimePathConfig;
|
||||||
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
|
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -50,6 +49,7 @@ class PDFToFileTest {
|
|||||||
@Mock private ProcessExecutor mockProcessExecutor;
|
@Mock private ProcessExecutor mockProcessExecutor;
|
||||||
@Mock private ProcessExecutorResult mockExecutorResult;
|
@Mock private ProcessExecutorResult mockExecutorResult;
|
||||||
@Mock private TempFileManager mockTempFileManager;
|
@Mock private TempFileManager mockTempFileManager;
|
||||||
|
@Mock private RuntimePathConfig mockRuntimePathConfig;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() throws IOException {
|
void setUp() throws IOException {
|
||||||
@@ -63,7 +63,9 @@ class PDFToFileTest {
|
|||||||
.when(mockTempFileManager.createTempDirectory())
|
.when(mockTempFileManager.createTempDirectory())
|
||||||
.thenAnswer(invocation -> Files.createTempDirectory("test"));
|
.thenAnswer(invocation -> Files.createTempDirectory("test"));
|
||||||
|
|
||||||
pdfToFile = new PDFToFile(mockTempFileManager);
|
lenient().when(mockRuntimePathConfig.getSOfficePath()).thenReturn("/usr/bin/soffice");
|
||||||
|
|
||||||
|
pdfToFile = new PDFToFile(mockTempFileManager, mockRuntimePathConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -329,12 +331,10 @@ class PDFToFileTest {
|
|||||||
boolean foundImage = false;
|
boolean foundImage = false;
|
||||||
|
|
||||||
while ((entry = zipStream.getNextEntry()) != null) {
|
while ((entry = zipStream.getNextEntry()) != null) {
|
||||||
if ("test.html".equals(entry.getName())) {
|
switch (entry.getName()) {
|
||||||
foundMainHtml = true;
|
case "test.html" -> foundMainHtml = true;
|
||||||
} else if ("test_ind.html".equals(entry.getName())) {
|
case "test_ind.html" -> foundIndexHtml = true;
|
||||||
foundIndexHtml = true;
|
case "test_img.png" -> foundImage = true;
|
||||||
} else if ("test_img.png".equals(entry.getName())) {
|
|
||||||
foundImage = true;
|
|
||||||
}
|
}
|
||||||
zipStream.closeEntry();
|
zipStream.closeEntry();
|
||||||
}
|
}
|
||||||
@@ -367,7 +367,8 @@ class PDFToFileTest {
|
|||||||
when(mockProcessExecutor.runCommandWithOutputHandling(
|
when(mockProcessExecutor.runCommandWithOutputHandling(
|
||||||
argThat(
|
argThat(
|
||||||
args ->
|
args ->
|
||||||
args.contains("--convert-to")
|
args != null
|
||||||
|
&& args.contains("--convert-to")
|
||||||
&& args.contains("docx"))))
|
&& args.contains("docx"))))
|
||||||
.thenAnswer(
|
.thenAnswer(
|
||||||
invocation -> {
|
invocation -> {
|
||||||
@@ -382,6 +383,7 @@ class PDFToFileTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create output file
|
// Create output file
|
||||||
|
assertNotNull(outDir);
|
||||||
Files.write(
|
Files.write(
|
||||||
Path.of(outDir, "document.docx"),
|
Path.of(outDir, "document.docx"),
|
||||||
"Fake DOCX content".getBytes());
|
"Fake DOCX content".getBytes());
|
||||||
@@ -427,7 +429,11 @@ class PDFToFileTest {
|
|||||||
.thenReturn(mockProcessExecutor);
|
.thenReturn(mockProcessExecutor);
|
||||||
|
|
||||||
when(mockProcessExecutor.runCommandWithOutputHandling(
|
when(mockProcessExecutor.runCommandWithOutputHandling(
|
||||||
argThat(args -> args.contains("--convert-to") && args.contains("odp"))))
|
argThat(
|
||||||
|
args ->
|
||||||
|
args != null
|
||||||
|
&& args.contains("--convert-to")
|
||||||
|
&& args.contains("odp"))))
|
||||||
.thenAnswer(
|
.thenAnswer(
|
||||||
invocation -> {
|
invocation -> {
|
||||||
// When command is executed, find the output directory argument
|
// When command is executed, find the output directory argument
|
||||||
@@ -442,6 +448,7 @@ class PDFToFileTest {
|
|||||||
|
|
||||||
// Create multiple output files (simulating a presentation with
|
// Create multiple output files (simulating a presentation with
|
||||||
// multiple files)
|
// multiple files)
|
||||||
|
assertNotNull(outDir);
|
||||||
Files.write(
|
Files.write(
|
||||||
Path.of(outDir, "document.odp"),
|
Path.of(outDir, "document.odp"),
|
||||||
"Fake ODP content".getBytes());
|
"Fake ODP content".getBytes());
|
||||||
@@ -515,7 +522,8 @@ class PDFToFileTest {
|
|||||||
when(mockProcessExecutor.runCommandWithOutputHandling(
|
when(mockProcessExecutor.runCommandWithOutputHandling(
|
||||||
argThat(
|
argThat(
|
||||||
args ->
|
args ->
|
||||||
args.contains("--convert-to")
|
args != null
|
||||||
|
&& args.contains("--convert-to")
|
||||||
&& args.contains("txt:Text"))))
|
&& args.contains("txt:Text"))))
|
||||||
.thenAnswer(
|
.thenAnswer(
|
||||||
invocation -> {
|
invocation -> {
|
||||||
@@ -530,6 +538,7 @@ class PDFToFileTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create text output file
|
// Create text output file
|
||||||
|
assertNotNull(outDir);
|
||||||
Files.write(
|
Files.write(
|
||||||
Path.of(outDir, "document.txt"),
|
Path.of(outDir, "document.txt"),
|
||||||
"Extracted text content".getBytes());
|
"Extracted text content".getBytes());
|
||||||
@@ -587,6 +596,7 @@ class PDFToFileTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create output file - uses default name
|
// Create output file - uses default name
|
||||||
|
assertNotNull(outDir);
|
||||||
Files.write(
|
Files.write(
|
||||||
Path.of(outDir, "output.docx"),
|
Path.of(outDir, "output.docx"),
|
||||||
"Fake DOCX content".getBytes());
|
"Fake DOCX content".getBytes());
|
||||||
@@ -611,4 +621,110 @@ class PDFToFileTest {
|
|||||||
.contains("output.docx"));
|
.contains("output.docx"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testProcessPdfToOfficeFormat_UsesUnoconvertWhenConfigured()
|
||||||
|
throws IOException, InterruptedException {
|
||||||
|
when(mockRuntimePathConfig.getUnoConvertPath()).thenReturn("/custom/unoconvert");
|
||||||
|
PDFToFile pdfToFileWithUno = new PDFToFile(mockTempFileManager, mockRuntimePathConfig);
|
||||||
|
|
||||||
|
try (MockedStatic<ProcessExecutor> mockedStaticProcessExecutor =
|
||||||
|
mockStatic(ProcessExecutor.class)) {
|
||||||
|
MultipartFile pdfFile =
|
||||||
|
new MockMultipartFile(
|
||||||
|
"file",
|
||||||
|
"document.pdf",
|
||||||
|
MediaType.APPLICATION_PDF_VALUE,
|
||||||
|
"Fake PDF content".getBytes());
|
||||||
|
|
||||||
|
mockedStaticProcessExecutor
|
||||||
|
.when(() -> ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE))
|
||||||
|
.thenReturn(mockProcessExecutor);
|
||||||
|
|
||||||
|
when(mockProcessExecutor.runCommandWithOutputHandling(
|
||||||
|
argThat(args -> args != null && args.contains("/custom/unoconvert"))))
|
||||||
|
.thenAnswer(
|
||||||
|
invocation -> {
|
||||||
|
List<String> args = invocation.getArgument(0);
|
||||||
|
String outputPath = args.get(args.size() - 1);
|
||||||
|
Files.write(Path.of(outputPath), "Fake DOCX content".getBytes());
|
||||||
|
return mockExecutorResult;
|
||||||
|
});
|
||||||
|
|
||||||
|
ResponseEntity<byte[]> response =
|
||||||
|
pdfToFileWithUno.processPdfToOfficeFormat(pdfFile, "docx", "writer_pdf_import");
|
||||||
|
|
||||||
|
assertEquals(HttpStatus.OK, response.getStatusCode());
|
||||||
|
assertNotNull(response.getBody());
|
||||||
|
assertTrue(response.getBody().length > 0);
|
||||||
|
assertTrue(
|
||||||
|
response.getHeaders()
|
||||||
|
.getContentDisposition()
|
||||||
|
.toString()
|
||||||
|
.contains("document.docx"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testProcessPdfToOfficeFormat_FallsBackWhenUnoconvertFails()
|
||||||
|
throws IOException, InterruptedException {
|
||||||
|
when(mockRuntimePathConfig.getUnoConvertPath()).thenReturn("/custom/unoconvert");
|
||||||
|
PDFToFile pdfToFileWithUno = new PDFToFile(mockTempFileManager, mockRuntimePathConfig);
|
||||||
|
|
||||||
|
try (MockedStatic<ProcessExecutor> mockedStaticProcessExecutor =
|
||||||
|
mockStatic(ProcessExecutor.class)) {
|
||||||
|
MultipartFile pdfFile =
|
||||||
|
new MockMultipartFile(
|
||||||
|
"file",
|
||||||
|
"document.pdf",
|
||||||
|
MediaType.APPLICATION_PDF_VALUE,
|
||||||
|
"Fake PDF content".getBytes());
|
||||||
|
|
||||||
|
mockedStaticProcessExecutor
|
||||||
|
.when(() -> ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE))
|
||||||
|
.thenReturn(mockProcessExecutor);
|
||||||
|
|
||||||
|
when(mockProcessExecutor.runCommandWithOutputHandling(
|
||||||
|
argThat(args -> args != null && args.contains("/custom/unoconvert"))))
|
||||||
|
.thenThrow(new IOException("Conversion failed"));
|
||||||
|
|
||||||
|
when(mockProcessExecutor.runCommandWithOutputHandling(
|
||||||
|
argThat(
|
||||||
|
args ->
|
||||||
|
args != null
|
||||||
|
&& args.stream()
|
||||||
|
.anyMatch(
|
||||||
|
arg ->
|
||||||
|
arg.contains(
|
||||||
|
"soffice")))))
|
||||||
|
.thenAnswer(
|
||||||
|
invocation -> {
|
||||||
|
List<String> args = invocation.getArgument(0);
|
||||||
|
String outDir = null;
|
||||||
|
for (int i = 0; i < args.size(); i++) {
|
||||||
|
if ("--outdir".equals(args.get(i)) && i + 1 < args.size()) {
|
||||||
|
outDir = args.get(i + 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertNotNull(outDir);
|
||||||
|
Files.write(
|
||||||
|
Path.of(outDir, "document.docx"),
|
||||||
|
"Fallback DOCX content".getBytes());
|
||||||
|
return mockExecutorResult;
|
||||||
|
});
|
||||||
|
|
||||||
|
ResponseEntity<byte[]> response =
|
||||||
|
pdfToFileWithUno.processPdfToOfficeFormat(pdfFile, "docx", "writer_pdf_import");
|
||||||
|
|
||||||
|
assertEquals(HttpStatus.OK, response.getStatusCode());
|
||||||
|
assertNotNull(response.getBody());
|
||||||
|
assertTrue(response.getBody().length > 0);
|
||||||
|
assertTrue(
|
||||||
|
response.getHeaders()
|
||||||
|
.getContentDisposition()
|
||||||
|
.toString()
|
||||||
|
.contains("document.docx"));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,45 @@
|
|||||||
package stirling.software.common.util;
|
package stirling.software.common.util;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.mockStatic;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
import java.awt.Color;
|
import java.awt.Color;
|
||||||
import java.awt.Graphics2D;
|
import java.awt.Graphics2D;
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.awt.image.ColorModel;
|
||||||
|
import java.awt.image.RenderedImage;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Collections;
|
import java.util.Arrays;
|
||||||
import java.util.HashSet;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipInputStream;
|
||||||
|
|
||||||
|
import javax.imageio.ImageIO;
|
||||||
|
|
||||||
import org.apache.pdfbox.cos.COSName;
|
import org.apache.pdfbox.cos.COSName;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.apache.pdfbox.pdmodel.PDPage;
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPageContentStream.AppendMode;
|
||||||
import org.apache.pdfbox.pdmodel.PDResources;
|
import org.apache.pdfbox.pdmodel.PDResources;
|
||||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||||
|
import org.apache.pdfbox.pdmodel.font.PDType1Font;
|
||||||
|
import org.apache.pdfbox.pdmodel.font.Standard14Fonts;
|
||||||
|
import org.apache.pdfbox.pdmodel.graphics.PDXObject;
|
||||||
|
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
||||||
|
import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory;
|
||||||
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
|
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
|
||||||
|
import org.apache.pdfbox.rendering.ImageType;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.mockito.Mockito;
|
import org.mockito.MockedStatic;
|
||||||
|
import org.springframework.mock.web.MockMultipartFile;
|
||||||
|
|
||||||
import stirling.software.common.model.ApplicationProperties;
|
import stirling.software.common.model.ApplicationProperties;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
@@ -31,32 +49,77 @@ public class PdfUtilsTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testTextToPageSize() {
|
void testTextToPageSize() {
|
||||||
|
assertEquals(PDRectangle.A0, PdfUtils.textToPageSize("A0"));
|
||||||
|
assertEquals(PDRectangle.A1, PdfUtils.textToPageSize("A1"));
|
||||||
|
assertEquals(PDRectangle.A2, PdfUtils.textToPageSize("A2"));
|
||||||
|
assertEquals(PDRectangle.A3, PdfUtils.textToPageSize("A3"));
|
||||||
assertEquals(PDRectangle.A4, PdfUtils.textToPageSize("A4"));
|
assertEquals(PDRectangle.A4, PdfUtils.textToPageSize("A4"));
|
||||||
|
assertEquals(PDRectangle.A5, PdfUtils.textToPageSize("A5"));
|
||||||
|
assertEquals(PDRectangle.A6, PdfUtils.textToPageSize("A6"));
|
||||||
assertEquals(PDRectangle.LETTER, PdfUtils.textToPageSize("LETTER"));
|
assertEquals(PDRectangle.LETTER, PdfUtils.textToPageSize("LETTER"));
|
||||||
|
assertEquals(PDRectangle.LEGAL, PdfUtils.textToPageSize("LEGAL"));
|
||||||
assertThrows(IllegalArgumentException.class, () -> PdfUtils.textToPageSize("INVALID"));
|
assertThrows(IllegalArgumentException.class, () -> PdfUtils.textToPageSize("INVALID"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testHasImagesOnPage() throws IOException {
|
void testGetAllImages() throws Exception {
|
||||||
// Mock a PDPage and its resources
|
// Root resources
|
||||||
PDPage page = Mockito.mock(PDPage.class);
|
PDResources root = mock(PDResources.class);
|
||||||
PDResources resources = Mockito.mock(PDResources.class);
|
|
||||||
Mockito.when(page.getResources()).thenReturn(resources);
|
|
||||||
|
|
||||||
// Case 1: No images in resources
|
COSName im1 = COSName.getPDFName("Im1");
|
||||||
Mockito.when(resources.getXObjectNames()).thenReturn(Collections.emptySet());
|
COSName form1 = COSName.getPDFName("Form1");
|
||||||
assertFalse(PdfUtils.hasImagesOnPage(page));
|
COSName other1 = COSName.getPDFName("Other1");
|
||||||
|
when(root.getXObjectNames()).thenReturn(Arrays.asList(im1, form1, other1));
|
||||||
|
|
||||||
// Case 2: Resources with an image
|
// Direct image at root
|
||||||
Set<COSName> xObjectNames = new HashSet<>();
|
PDImageXObject imgXObj1 = mock(PDImageXObject.class);
|
||||||
COSName cosName = Mockito.mock(COSName.class);
|
BufferedImage img1 = new BufferedImage(2, 2, BufferedImage.TYPE_INT_ARGB);
|
||||||
xObjectNames.add(cosName);
|
when(imgXObj1.getImage()).thenReturn(img1);
|
||||||
|
when(root.getXObject(im1)).thenReturn(imgXObj1);
|
||||||
|
|
||||||
PDImageXObject imageXObject = Mockito.mock(PDImageXObject.class);
|
// "Other" XObject that should be ignored
|
||||||
Mockito.when(resources.getXObjectNames()).thenReturn(xObjectNames);
|
PDXObject otherXObj = mock(PDXObject.class);
|
||||||
Mockito.when(resources.getXObject(cosName)).thenReturn(imageXObject);
|
when(root.getXObject(other1)).thenReturn(otherXObj);
|
||||||
|
|
||||||
assertTrue(PdfUtils.hasImagesOnPage(page));
|
// Form XObject with its own resources
|
||||||
|
PDFormXObject formXObj = mock(PDFormXObject.class);
|
||||||
|
PDResources formRes = mock(PDResources.class);
|
||||||
|
when(formXObj.getResources()).thenReturn(formRes);
|
||||||
|
when(root.getXObject(form1)).thenReturn(formXObj);
|
||||||
|
|
||||||
|
// Inside the form: one image and a nested form
|
||||||
|
COSName im2 = COSName.getPDFName("Im2");
|
||||||
|
COSName nestedForm = COSName.getPDFName("NestedForm");
|
||||||
|
when(formRes.getXObjectNames()).thenReturn(Arrays.asList(im2, nestedForm));
|
||||||
|
|
||||||
|
PDImageXObject imgXObj2 = mock(PDImageXObject.class);
|
||||||
|
BufferedImage img2 = new BufferedImage(3, 3, BufferedImage.TYPE_INT_RGB);
|
||||||
|
when(imgXObj2.getImage()).thenReturn(img2);
|
||||||
|
when(formRes.getXObject(im2)).thenReturn(imgXObj2);
|
||||||
|
|
||||||
|
PDFormXObject nestedFormXObj = mock(PDFormXObject.class);
|
||||||
|
PDResources nestedRes = mock(PDResources.class);
|
||||||
|
when(nestedFormXObj.getResources()).thenReturn(nestedRes);
|
||||||
|
when(formRes.getXObject(nestedForm)).thenReturn(nestedFormXObj);
|
||||||
|
|
||||||
|
// Deep nest: another image
|
||||||
|
COSName im3 = COSName.getPDFName("Im3");
|
||||||
|
when(nestedRes.getXObjectNames()).thenReturn(List.of(im3));
|
||||||
|
|
||||||
|
PDImageXObject imgXObj3 = mock(PDImageXObject.class);
|
||||||
|
BufferedImage img3 = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
|
||||||
|
when(imgXObj3.getImage()).thenReturn(img3);
|
||||||
|
when(nestedRes.getXObject(im3)).thenReturn(imgXObj3);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
List<RenderedImage> result = PdfUtils.getAllImages(root);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertEquals(
|
||||||
|
3, result.size(), "It should find exactly 3 images (root + form + nested form).");
|
||||||
|
assertTrue(
|
||||||
|
result.containsAll(List.of(img1, img2, img3)),
|
||||||
|
"All expected images must be present.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -107,7 +170,7 @@ public class PdfUtilsTest {
|
|||||||
g.fillRect(0, 0, 10, 10);
|
g.fillRect(0, 0, 10, 10);
|
||||||
g.dispose();
|
g.dispose();
|
||||||
ByteArrayOutputStream imgOut = new ByteArrayOutputStream();
|
ByteArrayOutputStream imgOut = new ByteArrayOutputStream();
|
||||||
javax.imageio.ImageIO.write(image, "png", imgOut);
|
ImageIO.write(image, "png", imgOut);
|
||||||
|
|
||||||
PdfMetadataService meta =
|
PdfMetadataService meta =
|
||||||
new PdfMetadataService(new ApplicationProperties(), "label", false, null);
|
new PdfMetadataService(new ApplicationProperties(), "label", false, null);
|
||||||
@@ -120,4 +183,554 @@ public class PdfUtilsTest {
|
|||||||
assertEquals(1, resultDoc.getNumberOfPages());
|
assertEquals(1, resultDoc.getNumberOfPages());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===============================================================
|
||||||
|
// Additional tests (added without modifying existing ones)
|
||||||
|
// ===============================================================
|
||||||
|
|
||||||
|
/* Helper: create a colored test image */
|
||||||
|
private static BufferedImage createImage(int w, int h, Color color) {
|
||||||
|
BufferedImage img = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
|
||||||
|
Graphics2D g = img.createGraphics();
|
||||||
|
g.setColor(color);
|
||||||
|
g.fillRect(0, 0, w, h);
|
||||||
|
g.dispose();
|
||||||
|
return img;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Helper: create a factory like in existing tests */
|
||||||
|
private static CustomPDFDocumentFactory factory() {
|
||||||
|
PdfMetadataService meta =
|
||||||
|
new PdfMetadataService(new ApplicationProperties(), "label", false, null);
|
||||||
|
return new CustomPDFDocumentFactory(meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("convertPdfToPdfImage: creates image-PDF with same page count")
|
||||||
|
void convertPdfToPdfImage_shouldCreateImagePdfWithSamePageCount() throws IOException {
|
||||||
|
try (PDDocument doc = new PDDocument()) {
|
||||||
|
PDPage p1 = new PDPage(PDRectangle.A4);
|
||||||
|
doc.addPage(p1);
|
||||||
|
try (PDPageContentStream cs =
|
||||||
|
new PDPageContentStream(doc, p1, AppendMode.APPEND, true, true)) {
|
||||||
|
cs.beginText();
|
||||||
|
cs.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 12);
|
||||||
|
cs.newLineAtOffset(50, 750);
|
||||||
|
cs.showText("Hello PDF");
|
||||||
|
cs.endText();
|
||||||
|
}
|
||||||
|
PDPage p2 = new PDPage(PDRectangle.A4);
|
||||||
|
doc.addPage(p2);
|
||||||
|
|
||||||
|
PDDocument out = PdfUtils.convertPdfToPdfImage(doc);
|
||||||
|
assertNotNull(out);
|
||||||
|
assertEquals(2, out.getNumberOfPages(), "Page count should be preserved");
|
||||||
|
out.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("imageToPdf: PNG -> single-page PDF (static ImageProcessingUtils mocked)")
|
||||||
|
void imageToPdf_shouldCreatePdfFromPng() throws Exception {
|
||||||
|
BufferedImage img = createImage(320, 200, Color.RED);
|
||||||
|
ByteArrayOutputStream pngOut = new ByteArrayOutputStream();
|
||||||
|
ImageIO.write(img, "png", pngOut);
|
||||||
|
|
||||||
|
MockMultipartFile file =
|
||||||
|
new MockMultipartFile("files", "test.png", "image/png", pngOut.toByteArray());
|
||||||
|
|
||||||
|
try (MockedStatic<ImageProcessingUtils> mocked = mockStatic(ImageProcessingUtils.class)) {
|
||||||
|
// Assume: loadImageWithExifOrientation/convertColorType exist – static mock
|
||||||
|
mocked.when(() -> ImageProcessingUtils.loadImageWithExifOrientation(any()))
|
||||||
|
.thenReturn(img);
|
||||||
|
mocked.when(
|
||||||
|
() ->
|
||||||
|
ImageProcessingUtils.convertColorType(
|
||||||
|
any(BufferedImage.class), anyString()))
|
||||||
|
.thenAnswer(inv -> inv.getArgument(0, BufferedImage.class));
|
||||||
|
|
||||||
|
byte[] pdfBytes =
|
||||||
|
PdfUtils.imageToPdf(
|
||||||
|
new MockMultipartFile[] {file},
|
||||||
|
"maintainAspectRatio",
|
||||||
|
true,
|
||||||
|
"RGB",
|
||||||
|
factory());
|
||||||
|
|
||||||
|
try (PDDocument result = factory().load(pdfBytes)) {
|
||||||
|
assertEquals(1, result.getNumberOfPages());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("imageToPdf: JPEG -> single-page PDF (JPEGFactory path)")
|
||||||
|
void imageToPdf_shouldCreatePdfFromJpeg_UsingJpegFactory() throws Exception {
|
||||||
|
BufferedImage img = createImage(640, 360, Color.BLUE);
|
||||||
|
ByteArrayOutputStream jpgOut = new ByteArrayOutputStream();
|
||||||
|
ImageIO.write(img, "jpg", jpgOut);
|
||||||
|
|
||||||
|
MockMultipartFile file =
|
||||||
|
new MockMultipartFile("files", "photo.jpg", "image/jpeg", jpgOut.toByteArray());
|
||||||
|
|
||||||
|
try (MockedStatic<ImageProcessingUtils> mocked = mockStatic(ImageProcessingUtils.class)) {
|
||||||
|
mocked.when(() -> ImageProcessingUtils.loadImageWithExifOrientation(any()))
|
||||||
|
.thenReturn(img);
|
||||||
|
mocked.when(
|
||||||
|
() ->
|
||||||
|
ImageProcessingUtils.convertColorType(
|
||||||
|
any(BufferedImage.class), anyString()))
|
||||||
|
.thenAnswer(inv -> inv.getArgument(0, BufferedImage.class));
|
||||||
|
|
||||||
|
byte[] pdfBytes =
|
||||||
|
PdfUtils.imageToPdf(
|
||||||
|
new MockMultipartFile[] {file}, "fillPage", false, "RGB", factory());
|
||||||
|
|
||||||
|
try (PDDocument result = factory().load(pdfBytes)) {
|
||||||
|
assertEquals(1, result.getNumberOfPages());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("addImageToDocument: fitDocumentToImage -> page size = image size")
|
||||||
|
void addImageToDocument_shouldUseImageSizeForPage_whenFitDocumentToImage() throws IOException {
|
||||||
|
try (PDDocument doc = new PDDocument()) {
|
||||||
|
BufferedImage img = createImage(300, 500, Color.GREEN);
|
||||||
|
PDImageXObject ximg = LosslessFactory.createFromImage(doc, img);
|
||||||
|
|
||||||
|
PdfUtils.addImageToDocument(doc, ximg, "fitDocumentToImage", false);
|
||||||
|
|
||||||
|
assertEquals(1, doc.getNumberOfPages());
|
||||||
|
PDRectangle box = doc.getPage(0).getMediaBox();
|
||||||
|
assertEquals(300, (int) box.getWidth());
|
||||||
|
assertEquals(500, (int) box.getHeight());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("addImageToDocument: autoRotate rotates A4 for landscape image")
|
||||||
|
void addImageToDocument_shouldRotateA4_whenAutoRotateAndLandscape() throws IOException {
|
||||||
|
try (PDDocument doc = new PDDocument()) {
|
||||||
|
BufferedImage img = createImage(800, 400, Color.ORANGE); // Landscape
|
||||||
|
PDImageXObject ximg = LosslessFactory.createFromImage(doc, img);
|
||||||
|
|
||||||
|
PdfUtils.addImageToDocument(doc, ximg, "maintainAspectRatio", true);
|
||||||
|
|
||||||
|
assertEquals(1, doc.getNumberOfPages());
|
||||||
|
PDRectangle box = doc.getPage(0).getMediaBox();
|
||||||
|
assertTrue(
|
||||||
|
box.getWidth() > box.getHeight(),
|
||||||
|
"A4 should be landscape when auto-rotate + landscape");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("addImageToDocument: fillPage runs without errors")
|
||||||
|
void addImageToDocument_fillPage_executes() throws IOException {
|
||||||
|
try (PDDocument doc = new PDDocument()) {
|
||||||
|
BufferedImage img = createImage(200, 200, Color.MAGENTA);
|
||||||
|
PDImageXObject ximg = LosslessFactory.createFromImage(doc, img);
|
||||||
|
|
||||||
|
PdfUtils.addImageToDocument(doc, ximg, "fillPage", false);
|
||||||
|
|
||||||
|
assertEquals(1, doc.getNumberOfPages());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("overlayImage: everyPage=true overlays all pages")
|
||||||
|
void overlayImage_shouldOverlayAllPages_whenEveryPageTrue() throws IOException {
|
||||||
|
CustomPDFDocumentFactory factory = factory();
|
||||||
|
|
||||||
|
// Create PDF with 2 pages
|
||||||
|
byte[] basePdf;
|
||||||
|
try (PDDocument doc = factory.createNewDocument()) {
|
||||||
|
doc.addPage(new PDPage(PDRectangle.A4));
|
||||||
|
doc.addPage(new PDPage(PDRectangle.A4));
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
doc.save(baos);
|
||||||
|
basePdf = baos.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create image bytes
|
||||||
|
BufferedImage img = createImage(50, 50, Color.BLACK);
|
||||||
|
ByteArrayOutputStream pngOut = new ByteArrayOutputStream();
|
||||||
|
ImageIO.write(img, "png", pngOut);
|
||||||
|
|
||||||
|
byte[] result = PdfUtils.overlayImage(factory, basePdf, pngOut.toByteArray(), 10, 10, true);
|
||||||
|
|
||||||
|
try (PDDocument out = factory.load(result)) {
|
||||||
|
assertEquals(2, out.getNumberOfPages(), "Page count remains identical");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Helper function: document with text on page1/page2 */
|
||||||
|
private static PDDocument createDocWithText(String p1, String p2) throws IOException {
|
||||||
|
PDDocument doc = new PDDocument();
|
||||||
|
|
||||||
|
PDPage page1 = new PDPage(PDRectangle.A4);
|
||||||
|
doc.addPage(page1);
|
||||||
|
try (PDPageContentStream cs =
|
||||||
|
new PDPageContentStream(doc, page1, AppendMode.APPEND, true, true)) {
|
||||||
|
cs.beginText();
|
||||||
|
cs.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 12);
|
||||||
|
cs.newLineAtOffset(50, 750);
|
||||||
|
cs.showText(p1);
|
||||||
|
cs.endText();
|
||||||
|
}
|
||||||
|
|
||||||
|
PDPage page2 = new PDPage(PDRectangle.A4);
|
||||||
|
doc.addPage(page2);
|
||||||
|
try (PDPageContentStream cs =
|
||||||
|
new PDPageContentStream(doc, page2, AppendMode.APPEND, true, true)) {
|
||||||
|
cs.beginText();
|
||||||
|
cs.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 12);
|
||||||
|
cs.newLineAtOffset(50, 750);
|
||||||
|
cs.showText(p2);
|
||||||
|
cs.endText();
|
||||||
|
}
|
||||||
|
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("containsTextInFile: pagesToCheck='all' finds text")
|
||||||
|
void containsTextInFile_allPages_true() throws IOException {
|
||||||
|
try (PDDocument doc = createDocWithText("alpha", "beta")) {
|
||||||
|
assertTrue(PdfUtils.containsTextInFile(doc, "beta", "all"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("containsTextInFile: single page '2' finds text")
|
||||||
|
void containsTextInFile_singlePage_two_true() throws IOException {
|
||||||
|
try (PDDocument doc = createDocWithText("alpha", "beta")) {
|
||||||
|
assertTrue(PdfUtils.containsTextInFile(doc, "beta", "2"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("containsTextInFile: range '1-1' finds text on page 1")
|
||||||
|
void containsTextInFile_range_oneToOne_true() throws IOException {
|
||||||
|
try (PDDocument doc = createDocWithText("findme", "other")) {
|
||||||
|
assertTrue(PdfUtils.containsTextInFile(doc, "findme", "1-1"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("containsTextInFile: list '1,2' finds text (whitespace robust)")
|
||||||
|
void containsTextInFile_list_pages_true() throws IOException {
|
||||||
|
try (PDDocument doc = createDocWithText("foo", "bar")) {
|
||||||
|
assertTrue(PdfUtils.containsTextInFile(doc, "bar", " 1 , 2 "));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("containsTextInFile: text not present -> false")
|
||||||
|
void containsTextInFile_textNotPresent_false() throws IOException {
|
||||||
|
try (PDDocument doc = createDocWithText("xxx", "yyy")) {
|
||||||
|
assertFalse(PdfUtils.containsTextInFile(doc, "zzz", "all"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("pageSize: different size returns false")
|
||||||
|
void pageSize_shouldReturnFalse_whenSizeDoesNotMatch() throws IOException {
|
||||||
|
try (PDDocument doc = new PDDocument()) {
|
||||||
|
doc.addPage(new PDPage(PDRectangle.A4));
|
||||||
|
assertFalse(PdfUtils.pageSize(doc, "600x842"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== New: convertFromPdf – coverage =====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("convertFromPdf: singleImage=true creates combined PNG file (readable)")
|
||||||
|
void convertFromPdf_singleImagePng_combinedReadable() throws Exception {
|
||||||
|
// Create two-page PDF
|
||||||
|
byte[] pdfBytes;
|
||||||
|
PdfMetadataService meta =
|
||||||
|
new PdfMetadataService(new ApplicationProperties(), "label", false, null);
|
||||||
|
CustomPDFDocumentFactory factory = new CustomPDFDocumentFactory(meta);
|
||||||
|
try (PDDocument doc = new PDDocument()) {
|
||||||
|
doc.addPage(new PDPage(PDRectangle.A4));
|
||||||
|
doc.addPage(new PDPage(PDRectangle.A4));
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
doc.save(baos);
|
||||||
|
pdfBytes = baos.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] imageBytes =
|
||||||
|
PdfUtils.convertFromPdf(
|
||||||
|
factory, pdfBytes, "png", ImageType.RGB, true, 72, "test.pdf", false);
|
||||||
|
|
||||||
|
// Should be readable as a single combined PNG image
|
||||||
|
BufferedImage img = ImageIO.read(new java.io.ByteArrayInputStream(imageBytes));
|
||||||
|
assertNotNull(img, "PNG should be readable");
|
||||||
|
assertTrue(img.getWidth() > 0 && img.getHeight() > 0, "Image dimensions > 0");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName(
|
||||||
|
"convertFromPdf: singleImage=false returns ZIP with PNG entries (first image readable)")
|
||||||
|
void convertFromPdf_multiImagePng_firstReadable() throws Exception {
|
||||||
|
// Create two-page PDF
|
||||||
|
byte[] pdfBytes;
|
||||||
|
PdfMetadataService meta =
|
||||||
|
new PdfMetadataService(new ApplicationProperties(), "label", false, null);
|
||||||
|
CustomPDFDocumentFactory factory = new CustomPDFDocumentFactory(meta);
|
||||||
|
try (PDDocument doc = new PDDocument()) {
|
||||||
|
doc.addPage(new PDPage(PDRectangle.A4));
|
||||||
|
doc.addPage(new PDPage(PDRectangle.A4));
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
doc.save(baos);
|
||||||
|
pdfBytes = baos.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Act: singleImage=false -> ZIP with separate images
|
||||||
|
byte[] zipBytes =
|
||||||
|
PdfUtils.convertFromPdf(
|
||||||
|
factory, pdfBytes, "png", ImageType.RGB, false, 72, "test.pdf", false);
|
||||||
|
|
||||||
|
// Assert: open ZIP, read first entry as PNG
|
||||||
|
try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(zipBytes))) {
|
||||||
|
ZipEntry entry = zis.getNextEntry();
|
||||||
|
assertNotNull(entry, "ZIP should contain at least one entry");
|
||||||
|
|
||||||
|
ByteArrayOutputStream imgOut = new ByteArrayOutputStream();
|
||||||
|
zis.transferTo(imgOut);
|
||||||
|
BufferedImage first = ImageIO.read(new ByteArrayInputStream(imgOut.toByteArray()));
|
||||||
|
|
||||||
|
assertNotNull(first, "First PNG entry should be readable");
|
||||||
|
assertTrue(first.getWidth() > 0 && first.getHeight() > 0, "Image dimensions > 0");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("hasText: detects phrase on selected pages ('1', '2', 'all')")
|
||||||
|
void hasText_shouldDetectPhrase_onSelectedPages() throws Exception {
|
||||||
|
// Arrange: PDF with 2 pages and text
|
||||||
|
try (PDDocument doc = new PDDocument()) {
|
||||||
|
PDPage p1 = new PDPage(PDRectangle.A4);
|
||||||
|
PDPage p2 = new PDPage(PDRectangle.A4);
|
||||||
|
doc.addPage(p1);
|
||||||
|
doc.addPage(p2);
|
||||||
|
|
||||||
|
try (PDPageContentStream cs =
|
||||||
|
new PDPageContentStream(doc, p1, AppendMode.APPEND, true, true)) {
|
||||||
|
cs.beginText();
|
||||||
|
cs.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 12);
|
||||||
|
cs.newLineAtOffset(50, 750);
|
||||||
|
cs.showText("alpha on page 1");
|
||||||
|
cs.endText();
|
||||||
|
}
|
||||||
|
try (PDPageContentStream cs =
|
||||||
|
new PDPageContentStream(doc, p2, AppendMode.APPEND, true, true)) {
|
||||||
|
cs.beginText();
|
||||||
|
cs.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 12);
|
||||||
|
cs.newLineAtOffset(50, 750);
|
||||||
|
cs.showText("beta on page 2");
|
||||||
|
cs.endText();
|
||||||
|
}
|
||||||
|
|
||||||
|
assertTrue(PdfUtils.hasText(doc, "1", "alpha"), "Page 1 should contain 'alpha'");
|
||||||
|
}
|
||||||
|
|
||||||
|
// For further checks, create new doc with identical content
|
||||||
|
try (PDDocument doc = new PDDocument()) {
|
||||||
|
PDPage p1 = new PDPage(PDRectangle.A4);
|
||||||
|
PDPage p2 = new PDPage(PDRectangle.A4);
|
||||||
|
doc.addPage(p1);
|
||||||
|
doc.addPage(p2);
|
||||||
|
|
||||||
|
try (PDPageContentStream cs =
|
||||||
|
new PDPageContentStream(doc, p1, AppendMode.APPEND, true, true)) {
|
||||||
|
cs.beginText();
|
||||||
|
cs.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 12);
|
||||||
|
cs.newLineAtOffset(50, 750);
|
||||||
|
cs.showText("alpha on page 1");
|
||||||
|
cs.endText();
|
||||||
|
}
|
||||||
|
try (PDPageContentStream cs =
|
||||||
|
new PDPageContentStream(doc, p2, AppendMode.APPEND, true, true)) {
|
||||||
|
cs.beginText();
|
||||||
|
cs.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 12);
|
||||||
|
cs.newLineAtOffset(50, 750);
|
||||||
|
cs.showText("beta on page 2");
|
||||||
|
cs.endText();
|
||||||
|
}
|
||||||
|
|
||||||
|
assertTrue(PdfUtils.hasText(doc, "2", "beta"), "Page 2 should contain 'beta'");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Third doc for 'all'
|
||||||
|
try (PDDocument doc = new PDDocument()) {
|
||||||
|
PDPage p1 = new PDPage(PDRectangle.A4);
|
||||||
|
PDPage p2 = new PDPage(PDRectangle.A4);
|
||||||
|
doc.addPage(p1);
|
||||||
|
doc.addPage(p2);
|
||||||
|
|
||||||
|
try (PDPageContentStream cs =
|
||||||
|
new PDPageContentStream(doc, p1, AppendMode.APPEND, true, true)) {
|
||||||
|
cs.beginText();
|
||||||
|
cs.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 12);
|
||||||
|
cs.newLineAtOffset(50, 750);
|
||||||
|
cs.showText("gamma");
|
||||||
|
cs.endText();
|
||||||
|
}
|
||||||
|
assertTrue(PdfUtils.hasText(doc, "all", "gamma"), "'all' should find text on page 1");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("hasTextOnPage: true if page contains phrase, else false")
|
||||||
|
void hasTextOnPage_shouldReturnTrueOnlyForPagesWithPhrase() throws Exception {
|
||||||
|
try (PDDocument doc = new PDDocument()) {
|
||||||
|
PDPage p1 = new PDPage(PDRectangle.A4);
|
||||||
|
PDPage p2 = new PDPage(PDRectangle.A4);
|
||||||
|
doc.addPage(p1);
|
||||||
|
doc.addPage(p2);
|
||||||
|
|
||||||
|
try (PDPageContentStream cs =
|
||||||
|
new PDPageContentStream(doc, p1, AppendMode.APPEND, true, true)) {
|
||||||
|
cs.beginText();
|
||||||
|
cs.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 12);
|
||||||
|
cs.newLineAtOffset(50, 750);
|
||||||
|
cs.showText("needle");
|
||||||
|
cs.endText();
|
||||||
|
}
|
||||||
|
|
||||||
|
assertTrue(PdfUtils.hasTextOnPage(p1, "needle"));
|
||||||
|
assertTrue(!PdfUtils.hasTextOnPage(p2, "needle"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("hasImages: detects images on selected pages and 'all'")
|
||||||
|
void hasImages_shouldDetectImages_onSelectedPages() throws Exception {
|
||||||
|
// Case 1: Page 1 without image (but resources set) -> false
|
||||||
|
try (PDDocument doc = new PDDocument()) {
|
||||||
|
PDPage p1 = new PDPage(PDRectangle.A4);
|
||||||
|
PDPage p2 = new PDPage(PDRectangle.A4);
|
||||||
|
p1.setResources(new PDResources());
|
||||||
|
p2.setResources(new PDResources());
|
||||||
|
doc.addPage(p1);
|
||||||
|
doc.addPage(p2);
|
||||||
|
|
||||||
|
// Image only on page 2
|
||||||
|
BufferedImage bi = new BufferedImage(20, 20, BufferedImage.TYPE_INT_RGB);
|
||||||
|
Graphics2D g = bi.createGraphics();
|
||||||
|
g.setColor(Color.GREEN);
|
||||||
|
g.fillRect(0, 0, 20, 20);
|
||||||
|
g.dispose();
|
||||||
|
|
||||||
|
PDImageXObject ximg = LosslessFactory.createFromImage(doc, bi);
|
||||||
|
try (PDPageContentStream cs =
|
||||||
|
new PDPageContentStream(doc, p2, AppendMode.APPEND, true, true)) {
|
||||||
|
cs.drawImage(ximg, 50, 700, 20, 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
assertTrue(!PdfUtils.hasImages(doc, "1"), "Page 1 should have no image");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case 2: Page 2 with image -> true
|
||||||
|
try (PDDocument doc = new PDDocument()) {
|
||||||
|
PDPage p1 = new PDPage(PDRectangle.A4);
|
||||||
|
PDPage p2 = new PDPage(PDRectangle.A4);
|
||||||
|
p1.setResources(new PDResources());
|
||||||
|
p2.setResources(new PDResources());
|
||||||
|
doc.addPage(p1);
|
||||||
|
doc.addPage(p2);
|
||||||
|
|
||||||
|
BufferedImage bi = new BufferedImage(20, 20, BufferedImage.TYPE_INT_RGB);
|
||||||
|
Graphics2D g = bi.createGraphics();
|
||||||
|
g.setColor(Color.BLUE);
|
||||||
|
g.fillRect(0, 0, 20, 20);
|
||||||
|
g.dispose();
|
||||||
|
|
||||||
|
PDImageXObject ximg = LosslessFactory.createFromImage(doc, bi);
|
||||||
|
try (PDPageContentStream cs =
|
||||||
|
new PDPageContentStream(doc, p2, AppendMode.APPEND, true, true)) {
|
||||||
|
cs.drawImage(ximg, 50, 700, 20, 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
assertTrue(PdfUtils.hasImages(doc, "2"), "Page 2 should have an image");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case 3: 'all' detects image
|
||||||
|
try (PDDocument doc = new PDDocument()) {
|
||||||
|
PDPage p = new PDPage(PDRectangle.A4);
|
||||||
|
p.setResources(new PDResources());
|
||||||
|
doc.addPage(p);
|
||||||
|
|
||||||
|
BufferedImage bi = new BufferedImage(10, 10, BufferedImage.TYPE_INT_RGB);
|
||||||
|
PDImageXObject ximg = LosslessFactory.createFromImage(doc, bi);
|
||||||
|
try (PDPageContentStream cs =
|
||||||
|
new PDPageContentStream(doc, p, AppendMode.APPEND, true, true)) {
|
||||||
|
cs.drawImage(ximg, 20, 730, 10, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
assertTrue(PdfUtils.hasImages(doc, "all"), "'all' should detect the image");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("hasImagesOnPage: true if page contains an image, else false")
|
||||||
|
void hasImagesOnPage_shouldReturnTrueOnlyForPagesWithImage() throws Exception {
|
||||||
|
try (PDDocument doc = new PDDocument()) {
|
||||||
|
PDPage p1 = new PDPage(PDRectangle.A4);
|
||||||
|
PDPage p2 = new PDPage(PDRectangle.A4);
|
||||||
|
p1.setResources(new PDResources());
|
||||||
|
p2.setResources(new PDResources());
|
||||||
|
doc.addPage(p1);
|
||||||
|
doc.addPage(p2);
|
||||||
|
|
||||||
|
BufferedImage bi = new BufferedImage(12, 12, BufferedImage.TYPE_INT_RGB);
|
||||||
|
Graphics2D g = bi.createGraphics();
|
||||||
|
g.setColor(Color.RED);
|
||||||
|
g.fillRect(0, 0, 12, 12);
|
||||||
|
g.dispose();
|
||||||
|
|
||||||
|
PDImageXObject ximg = LosslessFactory.createFromImage(doc, bi);
|
||||||
|
try (PDPageContentStream cs =
|
||||||
|
new PDPageContentStream(doc, p1, AppendMode.APPEND, true, true)) {
|
||||||
|
cs.drawImage(ximg, 40, 720, 12, 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
assertTrue(PdfUtils.hasImagesOnPage(p1));
|
||||||
|
assertTrue(!PdfUtils.hasImagesOnPage(p2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("convertFromPdf: singleImage=true with JPG -> no alpha, white background")
|
||||||
|
void convertFromPdf_singleImageJpg_noAlphaWhiteBackground() throws Exception {
|
||||||
|
// small 1-page PDF
|
||||||
|
byte[] pdfBytes;
|
||||||
|
PdfMetadataService meta =
|
||||||
|
new PdfMetadataService(new ApplicationProperties(), "label", false, null);
|
||||||
|
CustomPDFDocumentFactory factory = new CustomPDFDocumentFactory(meta);
|
||||||
|
try (PDDocument doc = new PDDocument()) {
|
||||||
|
PDPage p = new PDPage(PDRectangle.A4);
|
||||||
|
doc.addPage(p);
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
doc.save(baos);
|
||||||
|
pdfBytes = baos.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] jpgBytes =
|
||||||
|
PdfUtils.convertFromPdf(
|
||||||
|
factory, pdfBytes, "jpg", ImageType.RGB, true, 72, "sample.pdf", false);
|
||||||
|
|
||||||
|
BufferedImage img = ImageIO.read(new ByteArrayInputStream(jpgBytes));
|
||||||
|
assertNotNull(img, "JPG should be readable");
|
||||||
|
|
||||||
|
ColorModel cm = img.getColorModel();
|
||||||
|
assertFalse(cm.hasAlpha(), "JPG output should have no alpha channel");
|
||||||
|
|
||||||
|
// JPG background should be white (approximate check)
|
||||||
|
int rgb = img.getRGB(img.getWidth() / 2, img.getHeight() / 2) & 0x00FFFFFF;
|
||||||
|
assertEquals(0xFFFFFF, rgb, "Background pixel should be white");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
package stirling.software.common.util;
|
package stirling.software.common.util;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -48,9 +45,7 @@ public class ProcessExecutorTest {
|
|||||||
IOException thrown =
|
IOException thrown =
|
||||||
assertThrows(
|
assertThrows(
|
||||||
IOException.class,
|
IOException.class,
|
||||||
() -> {
|
() -> processExecutor.runCommandWithOutputHandling(command));
|
||||||
processExecutor.runCommandWithOutputHandling(command);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check the exception message to ensure it indicates the command was not found
|
// Check the exception message to ensure it indicates the command was not found
|
||||||
String errorMessage = thrown.getMessage();
|
String errorMessage = thrown.getMessage();
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package stirling.software.common.util;
|
package stirling.software.common.util;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -22,7 +23,7 @@ public class PropertyConfigsTest {
|
|||||||
boolean result = PropertyConfigs.getBooleanValue(keys, defaultValue);
|
boolean result = PropertyConfigs.getBooleanValue(keys, defaultValue);
|
||||||
|
|
||||||
// Verify the result
|
// Verify the result
|
||||||
assertEquals(true, result);
|
assertTrue(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -51,7 +52,7 @@ public class PropertyConfigsTest {
|
|||||||
boolean result = PropertyConfigs.getBooleanValue(key, defaultValue);
|
boolean result = PropertyConfigs.getBooleanValue(key, defaultValue);
|
||||||
|
|
||||||
// Verify the result
|
// Verify the result
|
||||||
assertEquals(true, result);
|
assertTrue(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -62,17 +62,11 @@ public class RegexPatternUtilsTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testNullRegexHandling() {
|
void testNullRegexHandling() {
|
||||||
assertThrows(
|
assertThrows(IllegalArgumentException.class, () -> utils.getPattern(null));
|
||||||
IllegalArgumentException.class,
|
|
||||||
() -> {
|
|
||||||
utils.getPattern(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
assertThrows(
|
assertThrows(
|
||||||
IllegalArgumentException.class,
|
IllegalArgumentException.class,
|
||||||
() -> {
|
() -> utils.getPattern(null, Pattern.CASE_INSENSITIVE));
|
||||||
utils.getPattern(null, Pattern.CASE_INSENSITIVE);
|
|
||||||
});
|
|
||||||
|
|
||||||
assertFalse(utils.isCached(null));
|
assertFalse(utils.isCached(null));
|
||||||
assertFalse(utils.removeFromCache(null));
|
assertFalse(utils.removeFromCache(null));
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package stirling.software.common.util;
|
package stirling.software.common.util;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.params.ParameterizedTest;
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
|||||||
@@ -57,8 +57,7 @@ class SpringContextHolderTest {
|
|||||||
void testGetBean_BeanNotFound() {
|
void testGetBean_BeanNotFound() {
|
||||||
// Arrange
|
// Arrange
|
||||||
contextHolder.setApplicationContext(mockApplicationContext);
|
contextHolder.setApplicationContext(mockApplicationContext);
|
||||||
when(mockApplicationContext.getBean(TestBean.class))
|
when(mockApplicationContext.getBean(TestBean.class)).thenThrow(new MyBeansException());
|
||||||
.thenThrow(new org.springframework.beans.BeansException("Bean not found") {});
|
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
TestBean result = SpringContextHolder.getBean(TestBean.class);
|
TestBean result = SpringContextHolder.getBean(TestBean.class);
|
||||||
@@ -69,4 +68,10 @@ class SpringContextHolderTest {
|
|||||||
|
|
||||||
// Simple test class
|
// Simple test class
|
||||||
private static class TestBean {}
|
private static class TestBean {}
|
||||||
|
|
||||||
|
private static class MyBeansException extends org.springframework.beans.BeansException {
|
||||||
|
public MyBeansException() {
|
||||||
|
super("Bean not found");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
package stirling.software.common.util;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unit tests for {@link TempDirectory}. Assumption: TempFileManager has methods
|
||||||
|
* createTempDirectory() and deleteTempDirectory(Path).
|
||||||
|
*/
|
||||||
|
class TempDirectoryTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should create temp directory and return correct path info")
|
||||||
|
void shouldReturnCorrectPathInfo() throws IOException {
|
||||||
|
TempFileManager manager = mock(TempFileManager.class);
|
||||||
|
Path tempPath = Files.createTempDirectory("testDir");
|
||||||
|
when(manager.createTempDirectory()).thenReturn(tempPath);
|
||||||
|
|
||||||
|
try (TempDirectory tempDir = new TempDirectory(manager)) {
|
||||||
|
assertEquals(
|
||||||
|
tempPath,
|
||||||
|
tempDir.getPath(),
|
||||||
|
"getPath should return the created directory path");
|
||||||
|
assertEquals(
|
||||||
|
tempPath.toAbsolutePath().toString(),
|
||||||
|
tempDir.getAbsolutePath(),
|
||||||
|
"getAbsolutePath should return absolute path");
|
||||||
|
assertTrue(tempDir.exists(), "exists should return true when directory exists");
|
||||||
|
assertTrue(
|
||||||
|
tempDir.toString().contains(tempPath.toAbsolutePath().toString()),
|
||||||
|
"toString should include the absolute path");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should call deleteTempDirectory on close")
|
||||||
|
void shouldDeleteTempDirectoryOnClose() throws IOException {
|
||||||
|
TempFileManager manager = mock(TempFileManager.class);
|
||||||
|
Path tempPath = Files.createTempDirectory("testDir");
|
||||||
|
when(manager.createTempDirectory()).thenReturn(tempPath);
|
||||||
|
|
||||||
|
try (TempDirectory tempDir = new TempDirectory(manager)) {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
ArgumentCaptor<Path> captor = ArgumentCaptor.forClass(Path.class);
|
||||||
|
verify(manager, times(1)).deleteTempDirectory(captor.capture());
|
||||||
|
assertEquals(
|
||||||
|
tempPath,
|
||||||
|
captor.getValue(),
|
||||||
|
"deleteTempDirectory should be called with the created path");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should handle multiple close calls without exception")
|
||||||
|
void shouldHandleMultipleCloseCalls() throws IOException {
|
||||||
|
TempFileManager manager = mock(TempFileManager.class);
|
||||||
|
Path tempPath = Files.createTempDirectory("testDir");
|
||||||
|
when(manager.createTempDirectory()).thenReturn(tempPath);
|
||||||
|
|
||||||
|
TempDirectory tempDir = new TempDirectory(manager);
|
||||||
|
tempDir.close();
|
||||||
|
assertDoesNotThrow(tempDir::close, "Second close should not throw exception");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should return false for exists if directory does not exist")
|
||||||
|
void shouldReturnFalseIfDirectoryDoesNotExist() throws IOException {
|
||||||
|
TempFileManager manager = mock(TempFileManager.class);
|
||||||
|
Path tempPath = Files.createTempDirectory("testDir");
|
||||||
|
Files.delete(tempPath); // delete immediately
|
||||||
|
when(manager.createTempDirectory()).thenReturn(tempPath);
|
||||||
|
|
||||||
|
try (TempDirectory tempDir = new TempDirectory(manager)) {
|
||||||
|
assertFalse(tempDir.exists(), "exists should return false when directory is missing");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should throw IOException if createTempDirectory fails")
|
||||||
|
void shouldThrowIfCreateTempDirectoryFails() throws IOException {
|
||||||
|
TempFileManager manager = mock(TempFileManager.class);
|
||||||
|
when(manager.createTempDirectory()).thenThrow(new IOException("Disk full"));
|
||||||
|
|
||||||
|
IOException ex = assertThrows(IOException.class, () -> new TempDirectory(manager));
|
||||||
|
assertEquals("Disk full", ex.getMessage(), "Exception message should be propagated");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
package stirling.software.common.util;
|
package stirling.software.common.util;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
|
||||||
import static org.mockito.ArgumentMatchers.anyInt;
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
import static org.mockito.Mockito.mockStatic;
|
import static org.mockito.Mockito.mockStatic;
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
package stirling.software.common.util;
|
package stirling.software.common.util;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
package stirling.software.common.util;
|
package stirling.software.common.util;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|
||||||
import static org.junit.jupiter.api.Assertions.fail;
|
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|||||||
+2
-3
@@ -1,7 +1,6 @@
|
|||||||
package stirling.software.common.util.misc;
|
package stirling.software.common.util.misc;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
@@ -106,7 +105,7 @@ class CustomColorReplaceStrategyTest {
|
|||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// If we get here, the test failed
|
// If we get here, the test failed
|
||||||
org.junit.jupiter.api.Assertions.fail("Exception occurred: " + e.getMessage());
|
fail("Exception occurred: " + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-2
@@ -1,7 +1,6 @@
|
|||||||
package stirling.software.common.util.misc;
|
package stirling.software.common.util.misc;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user