aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/sisudoc/ocda
diff options
context:
space:
mode:
Diffstat (limited to 'src/sisudoc/ocda')
-rw-r--r--src/sisudoc/ocda/abstraction/load.d7
-rw-r--r--src/sisudoc/ocda/io_in/read_zip_pod.d26
-rw-r--r--src/sisudoc/ocda/meta/rgx.d4
-rw-r--r--src/sisudoc/ocda/meta/rgx_files.d4
-rw-r--r--src/sisudoc/ocda/zstd.d195
5 files changed, 228 insertions, 8 deletions
diff --git a/src/sisudoc/ocda/abstraction/load.d b/src/sisudoc/ocda/abstraction/load.d
index ea387d1..2072aae 100644
--- a/src/sisudoc/ocda/abstraction/load.d
+++ b/src/sisudoc/ocda/abstraction/load.d
@@ -120,7 +120,12 @@ template spineAbstractionLoad() {
if (_path.endsWith(".ocda.db")) { return AbstractionSource.ocda_db; }
if (_path.endsWith(".ssp")) { return AbstractionSource.ssp; }
if (_path.endsWith(".sst") || _path.endsWith(".ssm")) { return AbstractionSource.markup; }
- if (_path.endsWith(".zip")) { return AbstractionSource.pod_zip; }
+ /+ ↓ .sisupod is the pod archive spine writes; .zip is what it wrote before,
+ and every pod already published carries that name, so both are read.
+ What is inside decides how it is opened, not the name.
+ +/
+ if (_path.endsWith(".sisupod") || _path.endsWith(".zip"))
+ { return AbstractionSource.pod_zip; }
if (_path.endsWith(".db")) { return AbstractionSource.ocda_db; }
return AbstractionSource.unknown;
}
diff --git a/src/sisudoc/ocda/io_in/read_zip_pod.d b/src/sisudoc/ocda/io_in/read_zip_pod.d
index 49429be..d0eb65a 100644
--- a/src/sisudoc/ocda/io_in/read_zip_pod.d
+++ b/src/sisudoc/ocda/io_in/read_zip_pod.d
@@ -63,6 +63,7 @@ template spineExtractZipPod() {
import std.regex;
import std.stdio;
import std.string : indexOf;
+ import sisudoc.ocda.zstd;
/+ security limits for zip extraction +/
enum size_t MAX_ENTRY_SIZE = 50 * 1024 * 1024; /+ 50 MB per entry +/
@@ -122,10 +123,29 @@ template spineExtractZipPod() {
}
/+ ↓ derive pod name from zip filename +/
string zip_basename = zip_path.baseName.stripExtension;
- /+ ↓ read and parse zip archive +/
+ /+ ↓ read and parse the pod archive.
+ a .sisupod is one zstd frame wrapping the archive spine writes, so
+ the bytes are unwrapped before the archive is parsed. Determined from
+ the first four bytes and not from the filename: a pod published as a
+ plain .zip, or one a reader has renamed, is read either way. Everything
+ after this point sees the same archive it always did, guards included.
+ +/
+ ubyte[] _archive_bytes;
+ try {
+ _archive_bytes = cast(ubyte[]) read(zip_path);
+ if (zstdIsFrame(_archive_bytes)) {
+ _archive_bytes = zstdDecompress(_archive_bytes, MAX_TOTAL_SIZE);
+ }
+ } catch (ZstdException ex) {
+ result.error_msg = "failed to decompress pod archive: " ~ zip_path ~ " - " ~ ex.msg;
+ return result;
+ } catch (Exception ex) {
+ result.error_msg = "error reading pod archive: " ~ zip_path ~ " - " ~ ex.msg;
+ return result;
+ }
ZipArchive zip;
try {
- zip = new ZipArchive(read(zip_path));
+ zip = new ZipArchive(_archive_bytes);
} catch (ZipException ex) {
result.error_msg = "failed to read zip archive: " ~ zip_path ~ " - " ~ ex.msg;
return result;
@@ -274,7 +294,7 @@ template spineExtractZipPod() {
its own, carrying its images. A .ssp is not here: it describes its
images without carrying them, so it would arrive without them.
+/
- static auto rgx_url_source = ctRegex!(`^https?://[a-zA-Z0-9._:/-]+([.]zip|[.]ocda[.]db)$`);
+ static auto rgx_url_source = ctRegex!(`^https?://[a-zA-Z0-9._:/-]+([.]sisupod|[.]zip|[.]ocda[.]db)$`);
struct DownloadResult {
string local_path; /+ path to downloaded temp file +/
diff --git a/src/sisudoc/ocda/meta/rgx.d b/src/sisudoc/ocda/meta/rgx.d
index 4cc40ae..d3c9aa1 100644
--- a/src/sisudoc/ocda/meta/rgx.d
+++ b/src/sisudoc/ocda/meta/rgx.d
@@ -211,8 +211,8 @@ static template spineRgxIn() {
static src_pth_sst_or_ssm = ctRegex!(`^(?P<path>[/]?(?:[a-zA-Z0-9._-]+/)*)(?P<filename>[a-zA-Z0-9._-]+[.](?P<extension>ss[tm]))$`);
static src_pth_pod_sst_or_ssm = ctRegex!(`^(?P<podpath>[/]?(?:[a-zA-Z0-9._-]+/)*)media/text/[a-z]{2}/(?P<filename>[a-zA-Z0-9][a-zA-Z0-9._-]*?[.]ss[tm])$`);
static src_pth_contents = ctRegex!(`^(?P<path>[/]?(?:[a-zA-Z0-9._-]+/)*)(?P<filename>[a-zA-Z0-9][a-zA-Z0-9._-]*)/pod[.]manifest$`);
- static src_pth_zip = ctRegex!(`^(?P<path>[/]?(?:[a-zA-Z0-9._-]+/)*)(?P<filename>[a-zA-Z0-9._-]+[.]zip)$`);
- static src_pth_types = ctRegex!(`^(?P<path>[/]?[a-zA-Z0-9._-]+/)*(?P<gotfile>(?P<filename>[a-zA-Z0-9._-]+[.]ss[tm])|(?P<filelist>[a-zA-Z0-9._-]+/pod[.]manifest)|(?P<filezip>[a-zA-Z0-9._-]+[.]zip))$`);
+ static src_pth_zip = ctRegex!(`^(?P<path>[/]?(?:[a-zA-Z0-9._-]+/)*)(?P<filename>[a-zA-Z0-9._-]+(?:[.]zip|[.]sisupod))$`);
+ static src_pth_types = ctRegex!(`^(?P<path>[/]?[a-zA-Z0-9._-]+/)*(?P<gotfile>(?P<filename>[a-zA-Z0-9._-]+[.]ss[tm])|(?P<filelist>[a-zA-Z0-9._-]+/pod[.]manifest)|(?P<filezip>[a-zA-Z0-9._-]+(?:[.]zip|[.]sisupod)))$`);
static src_fn = ctRegex!(`^([/]?(?:[a-zA-Z0-9._-]+/)*)(?P<fn_src>(?P<fn_base>[a-zA-Z0-9._-]+)[.](?P<fn_src_suffix>ss[tm]))$`);
static src_fn_master = ctRegex!(`^(?P<path>/?(?:[a-zA-Z0-9._-]+/)*)(?P<filename>[a-zA-Z0-9._-]+[.]ssm)$`);
static src_fn_find_inserts = ctRegex!(`^(?P<path>/?(?:[a-zA-Z0-9._-]+/)*)(?P<filename>[a-zA-Z0-9._-]+[.]ss[im])$`);
diff --git a/src/sisudoc/ocda/meta/rgx_files.d b/src/sisudoc/ocda/meta/rgx_files.d
index b26422f..13c7e46 100644
--- a/src/sisudoc/ocda/meta/rgx_files.d
+++ b/src/sisudoc/ocda/meta/rgx_files.d
@@ -57,8 +57,8 @@ static template spineRgxFiles() {
static src_pth_sst_or_ssm = ctRegex!(`^(?P<path>[/]?(?:[a-zA-Z0-9._-]+/)*)(?P<filename>[a-zA-Z0-9._-]+[.](?P<extension>ss[tm]))$`);
static src_pth_pod_sst_or_ssm = ctRegex!(`^(?P<podpath>[/]?(?:[a-zA-Z0-9._-]+/)*)media/text/[a-z]{2}/(?P<filename>[a-zA-Z0-9][a-zA-Z0-9._-]*?[.]ss[tm])$`);
static src_pth_contents = ctRegex!(`^(?P<path>[/]?(?:[a-zA-Z0-9._-]+/)*)(?P<filename>[a-zA-Z0-9][a-zA-Z0-9._-]*)/pod[.]manifest$`);
- static src_pth_zip = ctRegex!(`^(?P<path>[/]?(?:[a-zA-Z0-9._-]+/)*)(?P<filename>[a-zA-Z0-9._-]+[.]zip)$`);
- static src_pth_types = ctRegex!(`^(?P<path>[/]?[a-zA-Z0-9._-]+/)*(?P<gotfile>(?P<filename>[a-zA-Z0-9._-]+[.]ss[tm])|(?P<filelist>[a-zA-Z0-9._-]+/pod[.]manifest)|(?P<filezip>[a-zA-Z0-9._-]+[.]zip))$`);
+ static src_pth_zip = ctRegex!(`^(?P<path>[/]?(?:[a-zA-Z0-9._-]+/)*)(?P<filename>[a-zA-Z0-9._-]+(?:[.]zip|[.]sisupod))$`);
+ static src_pth_types = ctRegex!(`^(?P<path>[/]?[a-zA-Z0-9._-]+/)*(?P<gotfile>(?P<filename>[a-zA-Z0-9._-]+[.]ss[tm])|(?P<filelist>[a-zA-Z0-9._-]+/pod[.]manifest)|(?P<filezip>[a-zA-Z0-9._-]+(?:[.]zip|[.]sisupod)))$`);
static src_fn = ctRegex!(`^([/]?(?:[a-zA-Z0-9._-]+/)*)(?P<fn_src>(?P<fn_base>[a-zA-Z0-9._-]+)[.](?P<fn_src_suffix>ss[tm]))$`);
static src_fn_master = ctRegex!(`^(?P<path>/?(?:[a-zA-Z0-9._-]+/)*)(?P<filename>[a-zA-Z0-9._-]+[.]ssm)$`);
static src_fn_find_inserts = ctRegex!(`^(?P<path>/?(?:[a-zA-Z0-9._-]+/)*)(?P<filename>[a-zA-Z0-9._-]+[.]ss[im])$`);
diff --git a/src/sisudoc/ocda/zstd.d b/src/sisudoc/ocda/zstd.d
new file mode 100644
index 0000000..2c8186f
--- /dev/null
+++ b/src/sisudoc/ocda/zstd.d
@@ -0,0 +1,195 @@
+/+
+- Name: SisuDoc Spine, Doc Reform [a part of]
+ - Description: documents, structuring, processing, publishing, search
+ - static content generator
+
+ - Author: Ralph Amissah
+ [ralph.amissah@gmail.com]
+
+ - Copyright: (C) 2015 (continuously updated, current 2026) Ralph Amissah, All Rights Reserved.
+
+ - License: AGPL 3 or later:
+
+ Spine (SiSU), a framework for document structuring, publishing and
+ search
+
+ Copyright (C) Ralph Amissah
+
+ This program is free software: you can redistribute it and/or modify it
+ under the terms of the GNU AFERO General Public License as published by the
+ Free Software Foundation, either version 3 of the License, or (at your
+ option) any later version.
+
+ This program is distributed in the hope that it will be useful, but WITHOUT
+ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ more details.
+
+ You should have received a copy of the GNU General Public License along with
+ this program. If not, see [https://www.gnu.org/licenses/].
+
+ If you have Internet connection, the latest version of the AGPL should be
+ available at these locations:
+ [https://www.fsf.org/licensing/licenses/agpl.html]
+ [https://www.gnu.org/licenses/agpl.html]
+
+ - Spine (by Doc Reform, related to SiSU) uses standard:
+ - docReform markup syntax
+ - standard SiSU markup syntax with modified headers and minor modifications
+ - docReform object numbering
+ - standard SiSU object citation numbering & system
+
+ - Homepages:
+ [https://www.sisudoc.org]
+ [https://www.doc-reform.org]
+
+ - Git
+ [https://git.sisudoc.org/]
+
++/
+/+
+ module zstd;<BR>
+ - the few libzstd entry points spine uses, and a buffer in / buffer out
+ wrapper over each direction<BR>
+ - one frame per call: what is compressed is a whole pod archive, not a
+ stream
++/
+module sisudoc.ocda.zstd;
+@safe:
+/+ ↓ libzstd, declared rather than bound by a generated header
+ Provides the fifteen declarations that spine needs (there is nothing
+ here to generate and no vendored tree to track): the C library is linked
+ (dub "libs": ["zstd"], nix buildInputs pkgs.zstd) and these
+ prototypes say what is being called. That is the same arrangement as
+ sqlite3, which is declared in the vendored d2sqlite3 and linked from the
+ system.
+ .
+ It sits under ocda/ rather than outputs/ because the pod *reader* needs it,
+ and the reader (ocda/io_in/read_zip_pod.d) is in the abstraction part,
+ which the outputs depend on and not the other way round.
+ A compression primitive is not an abstraction concern; the dependency
+ direction is what puts it here.
++/
+extern (C) @nogc nothrow private {
+ size_t ZSTD_compressBound(size_t srcSize);
+ size_t ZSTD_compress(void* dst, size_t dstCapacity,
+ const(void)* src, size_t srcSize, int compressionLevel);
+ size_t ZSTD_decompress(void* dst, size_t dstCapacity,
+ const(void)* src, size_t compressedSize);
+ ulong ZSTD_getFrameContentSize(const(void)* src, size_t srcSize);
+ uint ZSTD_isError(size_t code);
+ const(char)* ZSTD_getErrorName(size_t code);
+ int ZSTD_minCLevel();
+ int ZSTD_maxCLevel();
+ uint ZSTD_versionNumber();
+}
+/+ ↓ what ZSTD_getFrameContentSize says when it cannot say +/
+private enum ulong ZSTD_CONTENTSIZE_UNKNOWN = ulong.max; /+ (0ULL - 1) +/
+private enum ulong ZSTD_CONTENTSIZE_ERROR = ulong.max - 1; /+ (0ULL - 2) +/
+/+ ↓ the four bytes that open every zstd frame, 0xFD2FB528 little endian.
+ A pod is recognised by this rather than by its name, so that a pod
+ already published as a plain zip keeps being read whatever it is called.
++/
+enum ubyte[4] zstd_frame_magic = [0x28, 0xB5, 0x2F, 0xFD];
+/+ ↓ the ceiling on what one frame may be allowed to become.
+ A frame declares its uncompressed size in its header and that number is
+ attacker-controlled: a .sisupod fetched over https could claim a size no
+ machine can allocate. The declared size is checked against this before a
+ buffer is asked for, so a hostile declaration is refused rather than
+ attempted. It matches MAX_TOTAL_SIZE in read_zip_pod.d, which bounds what
+ the archive inside may extract to.
++/
+enum size_t ZSTD_MAX_FRAME_CONTENT = 500 * 1024 * 1024;
+/+ ↓ thrown rather than returned: every caller here wants the whole buffer or
+ nothing, and there is no partial result worth handing back
++/
+class ZstdException : Exception {
+ this(string _msg, string _file = __FILE__, size_t _line = __LINE__) @safe pure nothrow {
+ super(_msg, _file, _line);
+ }
+}
+/+ ↓ does this begin a zstd frame? +/
+bool zstdIsFrame(const(ubyte)[] _bytes) {
+ if (_bytes.length < zstd_frame_magic.length) {
+ return false;
+ }
+ foreach (_i, _b; zstd_frame_magic) {
+ if (_bytes[_i] != _b) {
+ return false;
+ }
+ }
+ return true;
+}
+/+ ↓ the library's version, for a run that wants to record what compressed a
+ pod. The bytes of a frame are a function of the library and the level, so
+ two zstd versions may compress the same input differently; nothing is
+ promised about archive bytes
++/
+@trusted uint zstdVersion() {
+ return ZSTD_versionNumber();
+}
+/+ ↓ the level clamped to what this library actually supports +/
+@trusted int zstdLevelClamped(int _level) {
+ int _min = ZSTD_minCLevel();
+ int _max = ZSTD_maxCLevel();
+ if (_level < _min) { return _min; }
+ if (_level > _max) { return _max; }
+ return _level;
+}
+private @trusted string _zstdError(size_t _code) {
+ import std.string : fromStringz;
+ return ZSTD_getErrorName(_code).fromStringz.idup;
+}
+/+ ↓ one buffer to one frame +/
+@trusted ubyte[] zstdCompress(const(ubyte)[] _src, int _level = 19) {
+ size_t _bound = ZSTD_compressBound(_src.length);
+ auto _dst = new ubyte[_bound];
+ size_t _got = ZSTD_compress(
+ _dst.ptr, _dst.length,
+ _src.ptr, _src.length,
+ zstdLevelClamped(_level),
+ );
+ if (ZSTD_isError(_got)) {
+ throw new ZstdException("zstd could not compress: " ~ _zstdError(_got));
+ }
+ return _dst[0 .. _got];
+}
+/+ ↓ one frame back to one buffer, refusing a frame that will not say how
+ large it is or that says something absurd
++/
+@trusted ubyte[] zstdDecompress(const(ubyte)[] _src,
+ size_t _max = ZSTD_MAX_FRAME_CONTENT
+) {
+ import std.conv : to;
+ if (!(zstdIsFrame(_src))) {
+ throw new ZstdException("not a zstd frame");
+ }
+ ulong _declared = ZSTD_getFrameContentSize(_src.ptr, _src.length);
+ if (_declared == ZSTD_CONTENTSIZE_ERROR) {
+ throw new ZstdException("zstd frame header will not read");
+ }
+ if (_declared == ZSTD_CONTENTSIZE_UNKNOWN) {
+ /+ every frame spine writes is one whole buffer, so its size is always
+ declared; a frame without one was made by something else
+ +/
+ throw new ZstdException("zstd frame does not declare its size");
+ }
+ if (_declared > _max) {
+ throw new ZstdException("zstd frame declares "
+ ~ _declared.to!string ~ " bytes, over the "
+ ~ _max.to!string ~ " allowed");
+ }
+ auto _dst = new ubyte[_declared.to!size_t];
+ size_t _got = ZSTD_decompress(
+ _dst.ptr, _dst.length,
+ _src.ptr, _src.length,
+ );
+ if (ZSTD_isError(_got)) {
+ throw new ZstdException("zstd could not decompress: " ~ _zstdError(_got));
+ }
+ if (_got != _dst.length) {
+ throw new ZstdException("zstd frame gave " ~ _got.to!string
+ ~ " bytes where its header declared " ~ _dst.length.to!string);
+ }
+ return _dst;
+}