Compare commits

..

4 Commits

Author SHA1 Message Date
mayx
7f6baa0c8d Update 2 files
- /_tools/serv00_post-receive
- /_posts/2026-10-01-quine.md
2026-10-07 06:16:54 +00:00
mayx
1052b66270 Update file default.html 2026-10-03 08:14:38 +00:00
mayx
8cb782d1ff Update 12 files
- /vendor/libarchive/LICENSE
- /vendor/libarchive/worker-bundle.js
- /vendor/libarchive/wasm-gen/libarchive.js
- /vendor/libarchive/wasm-gen/libarchive.wasm
- /sw.js
- /offline.md
- /_layouts/default.html
- /_tools/serv00_post-receive
- /_tools/envs_post-receive
- /_data/proxylist.yml
- /assets/js/offline-page.js
- /assets/js/offline-unpack.js
2026-10-03 07:57:00 +00:00
mayx
7659d3e9d5 Update 7 files
- /images/GEDSC AIRWAY BILL TO CHINA.jpg
- /images/MOS_Demo.png
- /images/MOS_Demo.webp
- /images/GEDSC AIRWAY BILL TO CHINA.webp
- /images/2020.04.06.zip
- /_posts/2018-10-28-mos-reporter.md
- /_posts/2019-06-10-cheat.md
2026-10-02 16:38:55 +00:00
20 changed files with 1175 additions and 7 deletions

View File

@ -17,7 +17,6 @@ mirrors:
- https://mayx.vercel.app/
- https://mayx.netlify.app/
- https://mayx.gitnet.page/
- https://mayx.stormkit.dev/
- https://mayx.grebedoc.dev/
- https://mayx.codeberg.page/
- https://mayx.tildepages.org/

View File

@ -98,7 +98,8 @@ layout: xslt_container
<li><a href="{{ site.github.tar_url }}">Download <strong>TAR Ball</strong></a></li>
<li><a href="{{ site.github.repository_url }}">View On <strong>GitHub</strong></a></li>
{% else %}
<li style="width: 270px; border-right: none;"><a href="/MayxBlog.7z">Download <strong>7-Zip File</strong></a></li>
<li style="width: 134px;"><a href="/MayxBlog.7z">Download <strong>7-Zip File</strong></a></li>
<li style="width: 134px; border-right: none;"><a href="/offline.html">Manage <strong>Offline Mode</strong></a></li>
{% endif %}
</ul>
</header>

View File

@ -20,7 +20,7 @@ tags: [Mayx,MOS]
MOS是一个用来解释我不能解释的东西的一个代替品,就如同UFO和人们定义的神一样,因为未知,而又需要一种解释的方法,于是出现了MOS。
***
## 正文
![MOS_DEMO](/images/MOS_Demo.png "MOS的大概效果图")
![MOS_DEMO](/images/MOS_Demo.webp "MOS的大概效果图")
```
MOS Log System has been Load.
MOS Analysis System is Ready.

View File

@ -139,7 +139,7 @@ Tel +34 612 520330
然后还非常用心的配了一张快递单:
![GEDSC AIRWAY BILL TO CHINA](/images/GEDSC%20AIRWAY%20BILL%20TO%20CHINA.jpg)
![GEDSC AIRWAY BILL TO CHINA](/images/GEDSC%20AIRWAY%20BILL%20TO%20CHINA.webp)
注:由于隐私原因,涉及隐私的地方已经被删除 ~~(尽管大家都知道我的手机号)~~
这个邮件当时也没细看,然后就给Lori Robinson说了我已经给他们发了我的信息,过了几天,她给我发:

View File

@ -190,6 +190,568 @@ class BlogQuine:
于是我第一时间就把原来的TGZ压缩命令换掉,换成了AI给我写的[blogquine.py](https://github.com/Mabbs/mabbs.github.io/blob/master/_tools/blogquine.py),现在就可以通过[这里](https://mayx.eu.org/MayxBlog.7z)下载到“完整”包含我博客所有内容的压缩包了。
不过唯一的问题就是这样做出来的压缩包并没有压缩😂,相当于给做成了普通的归档了。当然我的博客本身倒是不大,没压缩也多不了多少空间,但相比于能做出“完整”的效果来说,这点浪费的空间也是小问题了。
## 基于博客压缩包的离线模式(2026.10.03更新)
既然现在的AI什么都能做了,那上次我因为技术不足仅仅只是用[Service Worker做了个代理](/2025/08/01/sw-proxy.html),现在也可以升级成下载全站之后让全站离线的功能吧?不过之前做的压缩包并没有压缩,导致整个压缩包有接近20MiB的大小,后来我仔细想了一下我的需求,我只是想让下载全站压缩包的按钮不断链,并没有说解压后的文件一定就必须是它本身,所以我只要用相同的文件名再压缩一次应该就可以了,这样既能满足我的想法,又可以让其他人下载的时候不会花费太多的时间。
在我直接压缩之后,整个压缩包占用大概9MiB的空间,其实我已经很满意了,不过压之前是18MiB,也就是说压缩率是50%……虽然也不是不能接受,但我在想这是不是合理的结果呢?我让AI分析了一下50%的压缩率正不正常,它看了一眼之后告诉我文本部分的10MiB压缩完之后有1MiB,图片因为压不动所以直接占用了8MiB😅,这时候我才意识到原来我一直存着一堆占着大量空间的垃圾,而且还是用的PNG/JPG这种压缩率很低的格式……考虑到兼容性和压缩效果,我把占用比较大的图片转换为了WebP格式,最终整个压缩包就只占用了4MiB的空间,一口气又节省了50%的空间。
最后做完对压缩包的处理之后,我直接让AI给我基于那个压缩包制作离线模式,它轻轻松松就[做出来](https://mayx.eu.org/offline.html)了,最终使用的是[libarchive库](https://github.com/nika-begiashvili/libarchivejs),在启用离线模式之后整个网站的速度简直就是0延迟,点击就能瞬间跳转,效果算是相当不错了。
## 受大佬启发制作了能压缩的7z Quine(2026.10.05更新)
做完博客压缩包之后我突发奇想,虽然东西是AI做的,但也是在我的手中做出了世界上第一个LZMA2 Quine,那么如果我向全世界第一个制作ZIP Quine的人展示一下我的作品会怎么样呢?目前可以知道,世界上第一个制作ZIP Quine的人是[Erling Ellingsen](https://infosec.exchange/@steike),所以我向他发了个帖子[展示了一下](https://infosec.exchange/@mayx/117385801954308033)。大佬不愧是大佬,一眼就能看出理论上能压缩的7z Quine是可以做出来的,对我来说,我是完全不知道7-Zip居然还支持链式编解码,能可以通过“double lzma2”的方式做到既可以自包含、又能真正被压缩的压缩包。既然大佬说理论上可以,那我自然是相信的,于是就让AI给我使劲折腾想办法把它做出来。在折腾了一晚上之后AI终于做出来了,真是强得可怕:
```python
# ============================================================
# 双层 LZMA2 的 7z quine("double lzma2")
#
# F = S32 ‖ P ‖ H,folder 的 coder 链 = [LZMA2(外层), LZMA2(内层)],
# 解码管线:P --外层--> I --内层--> U。
#
# I = Cseg ‖ LBseg ‖ Mseg ‖ 0x00
# Cseg : 真实 LZMA2 对 content 的压缩流(内容真正的压缩数据 C)
# LBseg: 内层 store 链,载荷 = seed(seed 内含一份 C 的参照副本)
# Mseg : 内层 match 链,输出 quine 区(= F 的逐字节副本)
# U = content ‖ seed ‖ F
#
# 外层把 I 再压一遍:Cseg 用 store 链原样承载;seed 里的 C 参照副本与 Cseg
# 逐字节相同,被外层的 rep0 回溯匹配消掉,于是成品里只剩一份压缩数据。
# quine 区回引 P 的字节时,源全在 seed 里(C 副本 / 头样本表 / 机械串)。
# ============================================================
def lzma_chunk(tokens, out_pos):
"""编一个 LZMA chunk(0xC0: state+props reset,无 dict reset,无 end marker)。
match token 的编码字节只取决于 (dist,len,pos),与历史内容无关(不维护输出历史)。
解压长度用 21 位(控制字节低 5 位存高 5 位),故单 chunk 输出上限 2 MiB。"""
enc = LzmaEncoder()
enc.pos = out_pos
total = 0
for t in tokens:
if t[0] == "m":
enc.match(t[1], t[2])
total += t[2]
else:
enc.rep_match(t[1])
total += t[1]
data = enc.finish()
u = total - 1
c = len(data) - 1
assert 0 <= u < (1 << 21) and 0 <= c < 65536, (total, len(data))
hdr = bytes([0xC0 | ((u >> 16) & 0x1F)]) + (u & 0xFFFF).to_bytes(2, "big") + \
(c & 0xFFFF).to_bytes(2, "big") + bytes([PROPS_BYTE])
return hdr + data, total
LZ2_MAXU = 1 << 21 # LZMA2 单个 LZMA chunk 的解压上限
def lzma2_raw(data, dict_size, preset=6):
"""FORMAT_RAW + LZMA2 压缩;去掉结尾 0x00 便于后面继续追加 chunk。"""
c = lzma.LZMACompressor(
format=lzma.FORMAT_RAW,
filters=[{"id": lzma.FILTER_LZMA2, "dict_size": dict_size, "preset": preset}])
out = c.compress(data) + c.flush()
assert out and out[-1] == 0x00, "LZMA2 流应以 0x00 结尾"
return out[:-1]
def split_len(n, cap):
"""按 cap 上限切分 n 字节。末片若为 1 字节则从前一片借 1(match 最短为 2)。"""
Ls, rem = [], n
while rem > cap:
Ls.append(cap)
rem -= cap
if rem == 1 and Ls:
Ls[-1] -= 1
rem = 2
Ls.append(rem)
return Ls
class DoubleQuine(BlogQuine):
"""双层 LZMA2 版本:folder = [LZMA2(外层), LZMA2(内层)]。"""
def entries(self):
"""U 的顺序是 内容文件… ‖ seed ‖ quine,条目顺序必须与之对应。"""
ent = [{"name": r, "dir": isd} for r, isd in self.walk_entries]
ent.append({"name": self.seed_name, "dir": False})
ent.append({"name": self.quine_name, "dir": False})
return ent
# ---------- 头部:双 coder + BindPair ----------
def build_header(self, n, total, d, entries, sub_sizes, size_I):
n_sub = len(sub_sizes)
N = len(entries)
h = bytearray()
h += b"\x01\x04" # kHeader, kMainStreamsInfo
h += b"\x06" + varint(0) + varint(1) + b"\x09" + varint(n) + b"\x00"
h += b"\x07" # kUnpackInfo
h += b"\x0b" + varint(1) + b"\x00" # kFolder: 1, local
h += varint(2) # NumCoders = 2
h += b"\x21\x21" + varint(1) + bytes([self.dict_prop_out]) # coder0 = 外层
h += b"\x21\x21" + varint(1) + bytes([self.dict_prop_in]) # coder1 = 内层
h += varint(1) + varint(0) # BindPair = (InIndex=1, OutIndex=0)
h += b"\x0c" + varint(size_I) + varint(d + total) # 两个 coder 的输出大小
h += b"\x00"
h += b"\x08" # SubStreamsInfo
h += b"\x0d" + varint(n_sub)
h += b"\x09" + b"".join(varint(s) for s in sub_sizes[:-1])
h += b"\x0a" + b"\x01"
crc_base = len(h)
h += b"\x00" * (4 * n_sub)
h += b"\x00\x00"
h += b"\x05" + varint(N) # FilesInfo
bits = bytearray((N + 7) // 8)
for i, e in enumerate(entries):
if e["dir"]:
bits[i // 8] |= 1 << (7 - i % 8)
h += b"\x0e" + varint(len(bits)) + bytes(bits)
mt = b"\x01\x00" + struct.pack("<Q", self.mtime_ft) * N
h += b"\x14" + varint(len(mt)) + mt
names = bytearray(b"\x00")
for e in entries:
names += e["name"].encode("utf-16-le") + b"\x00\x00"
h += b"\x11" + varint(len(names)) + bytes(names)
attrs = b"".join(struct.pack("<I", 0x10 if e["dir"] else 0x20)
for e in entries)
h += b"\x15" + varint(2 + len(attrs)) + b"\x01\x00" + attrs
h += b"\x00\x00"
return bytes(h), crc_base
# ---------- 布局:三层坐标的不动点迭代 ----------
def layout(self):
content = self.content
dc = len(content)
dict_c = max(1 << 20, 1 << max(20, dc.bit_length()))
C = lzma2_raw(content, dict_c)
self._C = C # 内容压缩只做一次,finalize 复用
lc = len(C)
# 迭代变量:Y = plant 长度, H = 头部长度, M = Mseg 字节数
Y, H, M = 96, 256, 256
seen = set()
for _ in range(200):
ds = lc + 32 + H + Y # seed = C副本‖S32‖H‖plant
d = dc + ds
Ls_LB = split_len(ds, CHUNK)
lLB = 3 * len(Ls_LB) + ds
off_in = lc + lLB
M1 = M + 1 # Mseg ‖ 内层流终止符 0x00
Ls_M = split_len(M1, CHUNK)
Li = off_in + M1
self.dict_prop_out, _ = dict_prop_for(max(Li, 1 << 20))
self.dict_prop_in, _ = dict_prop_for(d + 32 + H + 8192)
st = self._build_streams(C, lc, ds, d, Ls_LB, lLB, off_in, Ls_M,
M1, H)
if st is None:
Y += 8
continue
mseg = self._build_mseg(st, d, lc, ds, H, M)
if mseg is None:
Y += 8
continue
M_new = len(mseg)
Y_new = len(st["hdr_bytes"]) + len(st["mech"]) + M_new + 2
total = st["n"] + 32 + H
file_sizes = [len(x) for _, x in self.files]
sub_sizes = file_sizes + [ds] + [total]
entries = self.entries()
hdr, crc_base = self.build_header(st["n"], total, d, entries,
sub_sizes, Li)
H_new = len(hdr)
key = (Y_new, H_new, M_new)
if key in seen: # 再次见到同一组值 = 已收敛
Y, H, M = Y_new, H_new, M_new
break
seen.add(key)
Y, H, M = Y_new, H_new, M_new
else:
raise RuntimeError("double layout 不收敛")
return {"Y": Y, "H": H, "M": M}
# ---------- 定稿:用收敛后的 (Y, H, M) 重算全部结构 ----------
def finalize(self, Y, H, M):
content = self.content
dc = len(content)
C = self._C # layout 里已压缩过
lc = len(C)
ds = lc + 32 + H + Y
d = dc + ds
Ls_LB = split_len(ds, CHUNK)
lLB = 3 * len(Ls_LB) + ds
off_in = lc + lLB
M1 = M + 1
Ls_M = split_len(M1, CHUNK)
Li = off_in + M1
self.dict_prop_out, ds_out = dict_prop_for(max(Li, 1 << 20))
self.dict_prop_in, ds_in = dict_prop_for(d + 32 + H + 8192)
st = self._build_streams(C, lc, ds, d, Ls_LB, lLB, off_in, Ls_M, M1, H)
mseg = self._build_mseg(st, d, lc, ds, H, M)
assert len(mseg) == M, "Mseg %d != %d" % (len(mseg), M)
total = st["n"] + 32 + H
file_sizes = [len(x) for _, x in self.files]
sub_sizes = file_sizes + [ds] + [total]
entries = self.entries()
hdr, crc_base = self.build_header(st["n"], total, d, entries,
sub_sizes, Li)
assert len(hdr) == H, "header %d != %d" % (len(hdr), H)
plant = st["hdr_bytes"] + bytes(st["mech"]) + mseg + b"\x00\x00"
assert len(plant) == Y, "plant %d != %d" % (len(plant), Y)
return {"C": C, "lc": lc, "dc": dc, "ds": ds, "d": d, "Li": Li,
"ochunks": st["ochunks"], "hdr_tab": st["hdr_tab"],
"mseg": mseg, "M": M, "H": H, "n": st["n"], "total": total,
"header": hdr, "crc_base": crc_base, "plant": plant,
"dict_size_in": ds_in, "dict_size_out": ds_out,
"n_sub": len(sub_sizes)}
# ---------- 外层流 P 的构造 ----------
def _build_streams(self, C, lc, ds, d, Ls_LB, lLB, off_in, Ls_M, M1, H):
hdr_tab, hdr_idx = [], {}
def hidx(b):
assert len(b) == 3 and b[0] in (0x01, 0x02), b.hex()
if b not in hdr_idx:
hdr_idx[b] = len(hdr_tab)
hdr_tab.append(b)
return hdr_idx[b]
ochunks = []
f, io = 32, 0
mech = bytearray()
def put_lzma(tokens, ioff):
blk, t = lzma_chunk(tokens, ioff)
ochunks.append({"kind": "lzma", "foff": f, "size": len(blk),
"ioff": ioff, "olen": t, "bytes": blk,
"mech": len(mech)})
mech.extend(blk)
return len(blk), t
def put_store(ioff, olen, hi, src):
ochunks.append({"kind": "store", "foff": f, "size": 3 + olen,
"ioff": ioff, "olen": olen, "hdr": hi, "src": src})
return 3 + olen
# 段 1:store 链承载 Cseg(I[0:lc] = C 的字节;源 = seed 的 C 参照副本)
Ls_C = split_len(lc, CHUNK)
c_off = 0
for L in Ls_C:
f += put_store(io, L, hidx(store_hdr(L, first=(c_off == 0))),
("seed", c_off))
io += L
c_off += L
assert io == lc
# 段 2:LBseg(I[lc : lc+lLB])—— 内层 store 链,载荷 = seed
lb_off = lc
seed_off = 0
for j, L in enumerate(Ls_LB):
lb_hdr = store_hdr(L, first=False)
if j == 0 or L != CHUNK:
# 没有可回引的相同样本,用外层 store chunk 原样携带这 3 字节
f += put_store(lb_off, 3, hidx(store_hdr(3, first=False)),
("tab", hidx(lb_hdr)))
io += 3
else:
# 与 chunk0 的头逐字节相同,直接从 I[lc:lc+3] 复制
n, t = put_lzma([("m", lb_off - lc, 3)], lb_off)
assert t == 3
f += n
io += 3
# 载荷:C 参照副本区走 rep0 回溯匹配(去重发生处),其余走 store
pay = lb_off + 3
i = 0
while i < L:
if seed_off + i < lc:
a = min(L - i, lc - (seed_off + i), LZ2_MAXU)
dist = (pay + i) - (seed_off + i)
n, t = put_lzma(matches_for(dist, a), pay + i)
assert t == a
f += n
io += a
i += a
else:
a = min(L - i, LZ2_MAXU)
f += put_store(pay + i, a, hidx(store_hdr(a, first=False)),
("seed", seed_off + i))
io += a
i += a
lb_off += 3 + L
seed_off += L
assert lb_off == lc + lLB and io == lc + lLB
# 段 3:store 链承载 Mseg ‖ 0x00(0x00 是内层流的终止符)
m_off = 0
for L in Ls_M:
f += put_store(off_in + m_off, L,
hidx(store_hdr(L, first=False)), ("mseg", m_off))
io += L
m_off += L
# 外层流自己的终止符 0x00
ochunks.append({"kind": "term", "foff": f, "size": 1, "ioff": io,
"olen": 0, "bytes": b"\x00"})
f += 1
return {"ochunks": ochunks, "mech": mech, "hdr_tab": hdr_tab,
"hdr_bytes": b"".join(hdr_tab), "n": f - 32}
# ---------- 内层 Mseg:把 quine 区的每个 F 区段映射成 match 链 ----------
def _build_mseg(self, st, d, lc, ds, H, M):
dc = d - ds
s_S32 = dc + lc # seed 里 S32 副本
s_H = dc + lc + 32 # seed 里 H 副本
s_tab = dc + lc + 32 + H # seed 里头样本表
s_mech = s_tab + len(st["hdr_bytes"]) # seed 里机械串
s_mseg = s_mech + len(st["mech"]) # seed 里 Mseg 副本
total = st["n"] + 32 + H
hoff = 32 + st["n"]
segs = [(0, 32, s_S32)] # F[0:32) 签名头
for c in st["ochunks"]:
if c["kind"] == "store":
segs.append((c["foff"], 3, s_tab + 3 * c["hdr"]))
src = c["src"]
if src[0] == "seed":
u = dc + src[1]
elif src[0] == "tab":
u = s_tab + 3 * src[1]
else: # ("mseg", off)
u = s_mseg + src[1]
segs.append((c["foff"] + 3, c["olen"], u))
elif c["kind"] == "lzma":
segs.append((c["foff"], c["size"], s_mech + c["mech"]))
else: # term:并入前一个载荷段
pf, pl, pu = segs[-1]
segs[-1] = (pf, pl + 1, pu)
segs.append((hoff, H, s_H)) # F[hoff:] 头部
pos = 0
for (fo, ln, u) in segs:
assert fo == pos, "段不连续: %d != %d" % (fo, pos)
assert ln >= 2, "段太短 %d @%d" % (ln, fo)
pos += ln
assert pos == total, "段总长 %d != total %d" % (pos, total)
# 生成 match 链,按 2 MiB 上限切成多个 0xC0 chunk
mseg = bytearray()
out_pos = d
cur, cur_len = [], 0
chunks = []
for (fo, ln, u) in segs:
dist = (d + fo) - u
assert 0 < dist, "dist=%d @%d" % (dist, fo)
toks = matches_for(dist, ln)
if cur_len + ln > LZ2_MAXU - MAX_MATCH:
chunks.append((cur, out_pos, cur_len))
out_pos += cur_len
cur, cur_len = [], 0
cur.extend(toks)
cur_len += ln
if cur:
chunks.append((cur, out_pos, cur_len))
out_pos += cur_len
assert out_pos == d + total, "Mseg 输出 %d != %d" % (out_pos, d + total)
for (toks, op, cl) in chunks:
blk, t = lzma_chunk(toks, op)
assert t == cl
mseg.extend(blk)
return bytes(mseg)
# ---------- F 中承载 seed[soff] 的那个字节的偏移 ----------
def _f_off_of_seed(self, lay, soff):
"""返回 (F 偏移, 该 store 载荷内从 soff 起还剩多少字节)。
seed_head 超过 64 KiB 时会被切成多片,所以调用方要循环推进。"""
for c in lay["ochunks"]:
if c["kind"] == "store" and c["src"][0] == "seed":
s0 = c["src"][1]
if s0 <= soff < s0 + c["olen"]:
return c["foff"] + 3 + (soff - s0), s0 + c["olen"] - soff
raise RuntimeError("seed 偏移 %d 未被 store 载荷覆盖" % soff)
def _f_pos_list(self, lay, soff, length):
"""seed[soff:soff+length] 在 F 中对应的字节位置列表(可能跨多片)。"""
res = []
while length > 0:
p, avail = self._f_off_of_seed(lay, soff)
a = min(length, avail)
res.extend(range(p, p + a))
soff += a
length -= a
return res
# ---------- 装配 ----------
def assemble(self, lay):
total, H = lay["total"], lay["H"]
hoff = total - H
C, plant = lay["C"], lay["plant"]
F = bytearray(total)
# pass A: 签名头 + 所有非 store 载荷字节
F[0:6] = SIG
F[6:8] = VER
struct.pack_into("<Q", F, 12, lay["n"])
struct.pack_into("<Q", F, 20, H)
for c in lay["ochunks"]:
if c["kind"] == "store":
F[c["foff"]:c["foff"] + 3] = lay["hdr_tab"][c["hdr"]]
else:
F[c["foff"]:c["foff"] + c["size"]] = c["bytes"]
F[hoff:hoff + H] = lay["header"]
# pass B: seed = C副本 ‖ S32副本 ‖ H副本 ‖ plant
seed = bytearray()
seed += C
seed += bytes(F[0:32])
seed += bytes(F[hoff:hoff + H])
seed += plant
assert len(seed) == lay["ds"], "seed %d != %d" % (len(seed), lay["ds"])
# pass C: 各 store 的载荷
msegz = lay["mseg"] + b"\x00"
for c in lay["ochunks"]:
if c["kind"] != "store":
continue
p = c["foff"] + 3
src, L = c["src"], c["olen"]
if src[0] == "seed":
F[p:p + L] = seed[src[1]:src[1] + L]
elif src[0] == "tab":
F[p:p + 3] = lay["hdr_tab"][src[1]]
else:
F[p:p + L] = msegz[src[1]:src[1] + L]
return F, bytes(seed)
# ---------- CRC 定点(128x128 GF(2):seed/quine/Next/Start 四个 CRC) ----------
def solve_crc(self, F, lay, C, plant):
H, hoff = lay["H"], lay["total"] - lay["H"]
n_sub, crc_base = lay["n_sub"], lay["crc_base"]
lc = lay["lc"]
# seed 里 S32 / H 副本在 F 中的镜像(可能跨多片,逐字节列出)
scopy0 = self._f_pos_list(lay, lc, 32)
Fv = memoryview(F)
def seed_crc():
v = crc32(C)
v = crc32(Fv[0:32], v) # S32 副本
v = crc32(Fv[hoff:hoff + H], v) # H 副本
return crc32(plant, v)
def targets():
return (seed_crc(), crc32(Fv), crc32(Fv[hoff:hoff + H]),
crc32(Fv[12:32]))
# 已知:各内容文件的 CRC,写本体 + seed 里 H 副本的镜像两处
known = [crc32(data) for _, data in self.files]
for i, e in enumerate(known):
slot = crc_base + 4 * i
b = struct.pack("<I", e)
for k in range(4):
F[hoff + slot + k] = b[k]
mir = self._f_pos_list(lay, lc + 32 + slot, 4)
assert len(mir) == 4
for k in range(4):
F[mir[k]] = b[k]
# 未知量:D_seed(seed 的 CRC)、D(quine 的 CRC)、N、S
# 每个未知量 4 字节,每字节有"本体 + seed 镜像"两个落点
qs = crc_base + 4 * len(known) # seed 子流的 CRC 槽
q = crc_base + 4 * (n_sub - 1) # quine 子流的 CRC 槽
groups = []
for slot in (qs, q):
groups.append([list(range(hoff + slot, hoff + slot + 4)),
self._f_pos_list(lay, lc + 32 + slot, 4)])
groups.append([list(range(28, 32)), scopy0[28:32]]) # NextHeaderCRC
groups.append([list(range(8, 12)), scopy0[8:12]]) # StartHeaderCRC
nb = 32 * len(groups)
base = targets()
basevec = 0
for i, v in enumerate(base):
basevec |= v << (32 * i)
cols = []
for g in range(len(groups)):
own, mir = groups[g]
for kb in range(32):
bi, bb = kb // 8, kb % 8
poss = [own[bi], mir[bi]]
for p in poss:
F[p] ^= (1 << bb)
t = targets()
for p in poss:
F[p] ^= (1 << bb)
delta = 0
for i, v in enumerate(t):
delta |= (v ^ base[i]) << (32 * i)
cols.append(delta)
eqs = []
for i in range(nb):
coeff = 0
for j in range(nb):
if (cols[j] >> i) & 1:
coeff |= 1 << j
coeff ^= 1 << i
eqs.append([coeff, (basevec >> i) & 1])
for j in range(nb):
p = next((i for i in range(j, nb) if (eqs[i][0] >> j) & 1), None)
if p is None:
raise RuntimeError("GF(2) 奇异 @bit%d" % j)
eqs[j], eqs[p] = eqs[p], eqs[j]
for i in range(nb):
if i != j and ((eqs[i][0] >> j) & 1):
eqs[i][0] ^= eqs[j][0]
eqs[i][1] ^= eqs[j][1]
x = 0
for i in range(nb):
assert eqs[i][0] == (1 << i)
if eqs[i][1]:
x |= 1 << i
vals = [(x >> (32 * i)) & 0xFFFFFFFF for i in range(len(groups))]
for g in range(len(groups)):
own, mir = groups[g]
b = struct.pack("<I", vals[g])
for k in range(4):
F[own[k]] = b[k]
F[mir[k]] = b[k]
t = targets()
assert t == tuple(vals), "CRC 定点失败: %s != %s" % (t, vals)
return vals
def build(self):
l0 = self.layout()
lay = self.finalize(l0["Y"], l0["H"], l0["M"])
F, seed = self.assemble(lay)
crcs = self.solve_crc(F, lay, lay["C"], lay["plant"])
# CRC 定点会改写 H / S32,而 seed 是它们的镜像,定点后需重建
hoff = lay["total"] - lay["H"]
seed = lay["C"] + bytes(F[0:32]) + bytes(F[hoff:hoff + lay["H"]]) \
+ lay["plant"]
return bytes(F), lay, seed, crcs
def verify_double(F, lay, files, seed, content):
"""串联两个原始 LZMA2 解码器还原 U,逐段校验。"""
d, total = lay["d"], lay["total"]
dec_o = lzma.LZMADecompressor(
format=lzma.FORMAT_RAW,
filters=[{"id": lzma.FILTER_LZMA2, "dict_size": lay["dict_size_out"]}])
I = dec_o.decompress(F[32:32 + lay["n"]])
ok = True
if len(I) != lay["Li"]:
print("[verify] 外层输出 %d != I %d" % (len(I), lay["Li"]))
return False
dec_i = lzma.LZMADecompressor(
format=lzma.FORMAT_RAW,
filters=[{"id": lzma.FILTER_LZMA2, "dict_size": lay["dict_size_in"]}])
U = dec_i.decompress(I)
exp = d + total
if len(U) != exp:
print("[verify] 内层输出 %d != %d" % (len(U), exp))
return False
dc, ds = lay["dc"], lay["ds"]
if U[:dc] != content:
print("[verify] 内容不匹配")
ok = False
pos = 0
for rel, data in files: # 内容文件排在 U 的最前面
if U[pos:pos + len(data)] != data:
print("[verify] 文件不匹配:", rel)
ok = False
pos += len(data)
assert pos == dc
if U[dc:dc + ds] != seed:
print("[verify] seed 不匹配")
ok = False
if U[d:] != F:
print("[verify] quine 自复制不匹配")
ok = False
print("[verify] 自解码: |I|=%d, |U|=%d, seed/文件/quine 全部匹配: %s"
% (len(I), len(U), ok))
return ok
```
仅仅是大佬的一句话,AI就能做出来,实在是太可怕了。最终我试了一下,官方的7-Zip可以正常解压,但是Windows资源管理器不行……Windows资源管理器使用的也是[libarchive](https://github.com/libarchive/libarchive),似乎是因为libarchive只支持2个解码器,并且[第二个解码器只能是像BCJ这样的过滤器](https://github.com/libarchive/libarchive/blob/8bb3bbdc7b117a1e22086a2260f2087aafa90687/libarchive/archive_read_support_format_7zip.c#L1475)……明明7-Zip官方是可以解压的,不知道如果把这个问题当作BUG报告给libarchive他们会不会解决?但目前存在兼容性问题的话我博客压缩包就先不用这个方案了吧🥲……
## 对TXZ格式的尝试
在做完7z格式的压缩包之后,我发现了一个问题,虽然7z确实很流行,但是在Linux下解压起来有点麻烦,7-Zip历史上主要面向Windows,Linux上长期以来更多依赖p7zip等第三方移植,因此生态集成度不如tar.xz,想要解压7z文件还得额外安装。
不过Linux下也有个支持LZMA2算法的压缩软件,那就是前些年出过[后门](https://tukaani.org/xz-backdoor/)的XZ Utils,配合tar就可以做出TXZ(tar.xz)文件,甚至用我[博客终端](https://mabbs.github.io/linux/)中的BusyBox也能解压。我想了一下反正有AI,干脆一句话让AI帮我把blogquine.py改成tar.xz格式的好了,结果倒也没费多少功夫,AI就这样写出来了:

View File

@ -7,7 +7,8 @@ mkdir Mabbs
curl -L -o Mabbs/README.md https://github.com/Mabbs/Mabbs/raw/main/README.md
bundle exec jekyll build -d ../public_html
python3 _tools/blogquine.py --src-dir blog --quine-dir blog ../public_html/ MayxBlog.7z
mv MayxBlog.7z ../public_html/
7z a -mx9 -bd -bb0 MayxBlog-new.7z MayxBlog.7z
mv MayxBlog-new.7z ../public_html/MayxBlog.7z
cd ../public_html/
unset GIT_DIR
git init

View File

@ -7,18 +7,21 @@ mkdir Mabbs
curl -L -o Mabbs/README.md https://github.com/Mabbs/Mabbs/raw/main/README.md
bundle exec jekyll build -d ../domains/mayx.serv00.net/public_html/
python3 _tools/blogquine.py --src-dir blog --quine-dir blog ../domains/mayx.serv00.net/public_html/ MayxBlog.7z
mv MayxBlog.7z ../domains/mayx.serv00.net/public_html/
7zz a -mx9 -bd -bb0 MayxBlog-new.7z MayxBlog.7z
mv MayxBlog-new.7z ../domains/mayx.serv00.net/public_html/MayxBlog.7z
rsync -avz --delete ../domains/mayx.serv00.net/public_html/ mayx@web.sourceforge.net:/home/project-web/mayx/htdocs/
cd ../domains/mayx.serv00.net/public_html/
unset GIT_DIR
git init
git lfs install
git lfs track "*.png"
git lfs track "*.webp"
git lfs track "*.moc"
git lfs track "*.tgz"
git lfs track "*.jpg"
git lfs track "*.zip"
git lfs track "*.7z"
git lfs track "*.wasm"
git branch -m main
echo "---
title: Mayx

119
assets/js/offline-page.js Normal file
View File

@ -0,0 +1,119 @@
/*
* Mayx的博客 · /offline.html 控制页控制器(classic script,PJAX 兼容)
*
* 本体是 IIFE,不含模块语法:PJAX 换页时 jQuery 会重新执行本文件,
* 每次执行都重新查询元素、重新绑定事件,天然幂等。真正的解包逻辑在
* ES module /assets/js/offline-unpack.js 中,通过动态 import() 加载
* (浏览器模块缓存保证 PJAX 反复进入本页也不会重复加载)。
* 注意:本文件经 Liquid 原样输出,不能使用模板语法字符。
*/
(function () {
var MODULE_URL = '/assets/js/offline-unpack.js';
// 压缩包大致量级(当前约 27MB),仅用于进度条比例,不影响正确性
var EXPECTED_BYTES = 27 * 1024 * 1024;
var busy = false;
function el(id) { return document.getElementById(id); }
function fmtMB(bytes) { return (bytes / 1048576).toFixed(1) + ' MB'; }
function setMessage(text) {
el('offline-message').textContent = text || '';
}
function showProgress(show) {
el('offline-progress-box').hidden = !show;
}
function render(meta) {
var online = !meta;
el('offline-state').textContent = online
? '在线模式(离线未启用)'
: '离线模式(优先从本地缓存加载)';
el('offline-version').textContent = online ? '-' : fmtMB(meta.payloadSize) + '(压缩包大小即版本号)';
el('offline-files').textContent = online ? '-' : meta.files + ' 个文件 / ' + fmtMB(meta.bytes);
el('offline-time').textContent = online ? '-' : new Date(meta.unpackedAt).toLocaleString();
el('offline-persist').textContent = online ? '-' : (meta.persisted ? '已授权(缓存不会被自动回收)' : '未授权(浏览器空间紧张时可能回收缓存)');
el('offline-enable').disabled = busy || !online;
el('offline-disable').disabled = busy || online;
}
function onProgress(info) {
if (info.done) return;
el('offline-progress').value = Math.min(100, (info.bytes / EXPECTED_BYTES) * 100);
el('offline-progress-text').textContent =
'已解包 ' + info.files + ' 个文件(' + fmtMB(info.bytes) + ')…';
}
function withModule(fn) {
return import(MODULE_URL).then(fn);
}
function refresh() {
if (!('serviceWorker' in navigator) || !('caches' in window)) {
el('offline-state').textContent = '当前浏览器不支持离线模式(需要 Service Worker)';
el('offline-enable').disabled = true;
el('offline-disable').disabled = true;
return;
}
withModule(function (mod) { return mod.ensureFresh(); }).then(function (fresh) {
render(fresh.ready ? fresh.meta : null);
if (fresh.stale) {
setMessage('检测到站点压缩包已更新,离线缓存已自动失效,当前处于在线模式。可重新启用离线模式。');
}
}).catch(function (e) {
setMessage('状态检测失败:' + (e && e.message ? e.message : e));
});
}
function onEnable() {
busy = true;
render(null);
showProgress(true);
setMessage('正在下载并解包站点压缩包,请保持网络连接…');
withModule(function (mod) { return mod.activateOffline({ onProgress: onProgress }); })
.then(function (meta) {
el('offline-progress').value = 100;
el('offline-progress-text').textContent = '解包完成';
setMessage('离线模式已启用。之后访问本博客将优先使用本地缓存,断网也能阅读。');
busy = false;
showProgress(false);
render(meta); // 立即展示离线状态,不必等后台刷新
})
.catch(function (e) {
setMessage('启用失败:' + (e && e.message ? e.message : e));
busy = false;
showProgress(false);
refresh();
});
}
function onDisable() {
busy = true;
setMessage('');
withModule(function (mod) { return mod.deactivateOffline(); })
.then(function () {
setMessage('已切回在线模式,之后页面将通过网络加载。');
busy = false;
render(null);
if (navigator.onLine) {
setTimeout(function () { location.reload(); }, 800);
}
})
.catch(function (e) {
setMessage('操作失败:' + (e && e.message ? e.message : e));
busy = false;
refresh();
});
}
function bind() {
var enable = el('offline-enable');
if (!enable) return; // 不在控制页(PJAX 容错)
enable.addEventListener('click', onEnable);
el('offline-disable').addEventListener('click', onDisable);
refresh();
}
bind();
})();

269
assets/js/offline-unpack.js Normal file
View File

@ -0,0 +1,269 @@
/*
* Mayx的博客 · 离线模式核心逻辑(/offline.html 控制页使用,ES module)
*
* - activateOffline():注册 SW,下载站点压缩包 /MayxBlog.7z,经 libarchive(WASM)
* 流式解包,把每个文件按站点路径写入 Cache Storage,最后写入就绪标记
* (清单 + payloadSize 版本号),自此进入离线模式。
* - ensureFresh():HEAD 探测线上压缩包大小,与标记中的 payloadSize 不一致
* 说明站点已更新 → 清空缓存,回退在线模式。
* - deactivateOffline():清空缓存,手动切回在线模式。
* - readStatus():读取当前离线状态,供控制页展示。
*
*/
const ARCHIVE_URL = '/MayxBlog.7z';
const ARCHIVE_ROOT = 'blog/';
const CACHE_NAME = 'mayxblog-offline-v1';
const READY_KEY = '/__offline_manifest__';
const WORKER_URL = '/vendor/libarchive/worker-bundle.js';
const MIME = {
html: 'text/html; charset=utf-8',
htm: 'text/html; charset=utf-8',
css: 'text/css; charset=utf-8',
js: 'application/javascript; charset=utf-8',
mjs: 'application/javascript; charset=utf-8',
json: 'application/json; charset=utf-8',
xml: 'application/xml; charset=utf-8',
xsl: 'text/xsl; charset=utf-8',
opml: 'text/x-opml; charset=utf-8',
txt: 'text/plain; charset=utf-8',
md: 'text/markdown; charset=utf-8',
csv: 'text/csv; charset=utf-8',
svg: 'image/svg+xml',
png: 'image/png',
jpg: 'image/jpeg',
jpeg: 'image/jpeg',
gif: 'image/gif',
webp: 'image/webp',
avif: 'image/avif',
ico: 'image/x-icon',
bmp: 'image/bmp',
woff: 'font/woff',
woff2: 'font/woff2',
ttf: 'font/ttf',
otf: 'font/otf',
eot: 'application/vnd.ms-fontobject',
mp3: 'audio/mpeg',
mp4: 'video/mp4',
webm: 'video/webm',
'7z': 'application/x-7z-compressed',
zip: 'application/zip',
wasm: 'application/wasm',
mtn: 'application/octet-stream',
moc: 'application/octet-stream',
pdf: 'application/pdf',
};
function mimeFor(pathname) {
const dot = pathname.lastIndexOf('.');
const ext = dot === -1 ? '' : pathname.slice(dot + 1).toLowerCase();
return MIME[ext] || 'application/octet-stream';
}
function sitePathFor(entryPath) {
const normalized = entryPath.replace(/\\/g, '/');
if (!normalized.startsWith(ARCHIVE_ROOT)) return null;
const relative = normalized.slice(ARCHIVE_ROOT.length);
if (!relative) return null;
const segments = [];
for (const segment of relative.split('/')) {
if (!segment || segment === '.') continue;
if (segment === '..') return null;
segments.push(segment);
}
if (!segments.length) return null;
return '/' + segments.join('/');
}
/*
* 在独立 worker 中打开一层压缩包并流式吐出条目;
* 返回其中根级嵌套压缩包(下一层包装)的原始字节。
*/
function extractLayer(file, onEntry) {
return new Promise((resolve, reject) => {
const worker = new Worker(WORKER_URL);
const nested = [];
let settled = false;
const finish = (error) => {
if (settled) return;
settled = true;
worker.terminate();
if (error) reject(error);
else resolve(nested);
};
worker.addEventListener('error', (event) => {
finish(new Error(event.message || 'libarchive worker failed'));
});
worker.addEventListener('message', ({ data }) => {
if (data.type === 'ERROR') {
finish(new Error(data.error && data.error.message ? data.error.message : 'libarchive failed'));
return;
}
if (data.type === 'READY') {
worker.postMessage({ type: 'OPEN', file });
return;
}
if (data.type === 'OPENED') {
worker.postMessage({ type: 'EXTRACT_FILES' });
return;
}
if (data.type === 'END') {
finish(null);
return;
}
if (data.type !== 'ENTRY') return;
const entry = data.entry;
if (entry.type !== 'FILE' || !entry.fileData) return;
// 根级压缩包是下一层包装,不是站点内容
if (entry.path.replace(/\\/g, '/') === 'MayxBlog.7z') {
nested.push(entry.fileData);
return;
}
onEntry(entry);
});
});
}
/* 几十 MB 的缓存是浏览器常见的回收对象,而重新解包代价高,尽量申请持久存储。 */
async function requestPersistence() {
if (!navigator.storage || !navigator.storage.persist) return false;
try {
if (await navigator.storage.persisted()) return true;
return await navigator.storage.persist();
} catch {
return false;
}
}
/* 读取就绪标记(= 离线模式状态);损坏或缺清单的标记视为不存在。 */
export async function readStatus() {
const cache = await caches.open(CACHE_NAME);
const hit = await cache.match(READY_KEY);
if (!hit) return null;
const meta = await hit.json().catch(() => null);
if (!meta || !Array.isArray(meta.keys) || !meta.keys.length) return null;
return meta;
}
/*
* 线上压缩包的大小即版本号。HEAD 不被 SW 拦截,拿到的一定是线上真实值;
* 网络失败返回 null,调用方选择信任现有缓存而不是误伤。
*/
async function payloadSize() {
try {
const response = await fetch(ARCHIVE_URL, { method: 'HEAD', cache: 'no-store' });
if (!response.ok) return null;
const size = Number(response.headers.get('content-length'));
return Number.isFinite(size) && size > 0 ? size : null;
} catch {
return null;
}
}
/*
* 控制页打开时调用:未启用 → {ready:false};已启用且版本一致 → {ready:true, meta};
* 版本不一致 → 清空缓存并返回 {ready:false, stale:true}(已回退在线模式)。
*/
export async function ensureFresh() {
const cache = await caches.open(CACHE_NAME);
const size = await payloadSize();
const meta = await readStatus();
if (!meta) return { ready: false, size, stale: false };
if (size && meta.payloadSize !== size) {
await caches.delete(CACHE_NAME);
return { ready: false, size, stale: true };
}
return { ready: true, size, stale: false, meta };
}
/*
* 启用离线模式:注册 SW → 清空旧缓存(同时清掉上次中断的残留,保证随后
* 的压缩包下载不会被旧缓存喂旧的 quine 副本)→ 下载并逐层解包 → 写标记。
*/
export async function activateOffline({ onProgress } = {}) {
if (!('serviceWorker' in navigator)) {
throw new Error('当前浏览器不支持 Service Worker,无法启用离线模式。');
}
await navigator.serviceWorker.register('/sw.js', { scope: '/' });
await navigator.serviceWorker.ready;
await caches.delete(CACHE_NAME);
const persisted = await requestPersistence();
// 写操作串行链,保证按到达顺序落盘且不产生无界并发
const cache = await caches.open(CACHE_NAME);
let writes = Promise.resolve();
const keys = new Set();
let files = 0;
let bytes = 0;
let skipped = 0;
const response = await fetch(ARCHIVE_URL, { cache: 'no-store' });
if (!response.ok) throw new Error(`无法下载站点压缩包:HTTP ${response.status}`);
const outer = await response.arrayBuffer();
const downloadedBytes = outer.byteLength;
let layer = [outer];
while (layer.length) {
const bytesThisLayer = layer.shift();
const file = new File([bytesThisLayer], 'MayxBlog.7z');
layer = await extractLayer(file, (entry) => {
const key = sitePathFor(entry.path);
if (!key) {
skipped += 1;
return;
}
const body = entry.fileData;
files += 1;
bytes += body.byteLength;
writes = writes.then(async () => {
await cache.put(key, new Response(body, {
status: 200,
headers: { 'Content-Type': mimeFor(key) },
}));
keys.add(key);
});
if (onProgress) onProgress({ done: false, files, bytes, skipped });
});
}
await writes;
const meta = {
files,
bytes,
skipped,
persisted,
payloadSize: downloadedBytes,
failed: files - keys.size,
keys: [...keys],
unpackedAt: new Date().toISOString(),
};
await cache.put(
READY_KEY,
new Response(JSON.stringify(meta), {
headers: { 'Content-Type': 'application/json; charset=utf-8' },
})
);
if (onProgress) onProgress({ done: true, cached: false, ...meta });
return meta;
}
/* 切回在线模式:删掉缓存(含标记),SW 随即恢复纯转发。返回之前是否处于离线模式。 */
export async function deactivateOffline() {
const had = await readStatus();
await caches.delete(CACHE_NAME);
return Boolean(had);
}

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 331 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 748 KiB

BIN
images/MOS_Demo.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

37
offline.md Normal file
View File

@ -0,0 +1,37 @@
---
layout: default
title: 离线模式
---
# 离线模式
本页用于管理本博客的离线阅读功能:
- **启用离线模式**:浏览器会下载整站压缩包(`MayxBlog.7z`)并解包到本地缓存,之后即使没有网络也能正常浏览博客;
- **自动回退**:当站点压缩包更新(博客重新部署)时,离线缓存会自动失效并回退到在线模式,下次重新启用即可获取最新内容;
- **手动切换**:离线模式下可以随时在本页切回在线模式,切回后页面将重新通过网络加载。
<table>
<tbody>
<tr><th>当前状态</th><td id="offline-state">检测中…</td></tr>
<tr><th>缓存版本</th><td id="offline-version">-</td></tr>
<tr><th>文件统计</th><td id="offline-files">-</td></tr>
<tr><th>解包时间</th><td id="offline-time">-</td></tr>
<tr><th>持久存储</th><td id="offline-persist">-</td></tr>
</tbody>
</table>
<p id="offline-progress-box" hidden="hidden">
<progress id="offline-progress" max="100" value="0"></progress>
<small id="offline-progress-text">准备中…</small>
</p>
<p><strong id="offline-message"></strong></p>
<p>
<button type="button" id="offline-enable">启用离线模式</button>
<button type="button" id="offline-disable">切换回在线模式</button>
</p>
<script src="/assets/js/offline-page.js"></script>

140
sw.js Normal file
View File

@ -0,0 +1,140 @@
/*
* Mayx的博客 · 离线模式 Service Worker
*
* 两种模式(由 Cache Storage 中是否存在就绪标记决定):
* - 在线模式(默认,无标记):所有请求原样转发网络,SW 形同虚设。
* - 离线模式(/offline.html 启用后写入标记):同源 GET 请求缓存优先,
* 缓存未命中且清单里没有的路径才回源网络,实现断网阅读。
*
* 版本控制:压缩包 /MayxBlog.7z 的 content-length 即版本号(每次重新部署
* 都会变化)。SW 激活时、以及导航请求按 RECHECK_INTERVAL 节流复核;
* 一旦发现线上压缩包与已缓存版本不一致,立即清空缓存 → 自动回退在线模式。
* 网络不可用时复核失败,保留缓存(离线阅读本来就是离线模式的目的)。
*/
const CACHE_NAME = 'mayxblog-offline-v1';
const READY_KEY = '/__offline_manifest__';
const ARCHIVE_URL = '/MayxBlog.7z';
const RECHECK_INTERVAL = 5 * 60 * 1000;
let lastRecheck = 0;
self.addEventListener('install', () => {
self.skipWaiting();
});
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys()
.then((keys) =>
Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k)))
)
.then(verifyPayloadVersion)
.then(() => self.clients.claim())
);
});
/*
* HEAD 请求不被下面的 fetch 处理器拦截,这里读到的一定是线上真实压缩包,
* 而不是缓存里的 quine 副本。网络失败返回 null,调用方选择信任现有缓存。
*/
async function payloadSize() {
try {
const response = await fetch(ARCHIVE_URL, { method: 'HEAD', cache: 'no-store' });
if (!response.ok) return null;
const size = Number(response.headers.get('content-length'));
return Number.isFinite(size) && size > 0 ? size : null;
} catch {
return null;
}
}
/*
* 就绪标记同时是清单:记录了解包写入的所有路径。没有它就无法区分
* “站点本来就没有这个页面”和“缓存条目被浏览器回收了”。
*/
async function readManifest(cache) {
const hit = await cache.match(READY_KEY);
if (!hit) return null;
const meta = await hit.json().catch(() => null);
if (!meta || !Array.isArray(meta.keys) || !meta.keys.length) return null;
return meta;
}
/* 压缩包更新 → 清空缓存(即回退在线模式);返回是否发生了失效。 */
async function verifyPayloadVersion() {
const cache = await caches.open(CACHE_NAME);
const meta = await readManifest(cache);
if (!meta) return false;
const size = await payloadSize();
if (size && meta.payloadSize !== size) {
await caches.delete(CACHE_NAME);
return true;
}
return false;
}
/* 服务器对目录索引的处理与 Jekyll 产物一致,这里补齐候选路径。 */
function candidatesFor(pathname) {
const keys = [pathname];
if (pathname.endsWith('/')) {
keys.push(pathname + 'index.html');
} else {
keys.push(pathname + '/index.html');
keys.push(pathname + '.html');
}
return keys;
}
self.addEventListener('fetch', (event) => {
const request = event.request;
if (request.method !== 'GET') return; // HEAD 等直通网络(版本探测依赖这一点)
const url = new URL(request.url);
if (url.origin !== self.location.origin) return; // 跨域请求不干预
// 显式 no-store 的请求(重新下载压缩包等)绕过缓存,避免吃到旧的 quine 副本
if (request.cache === 'no-store') return;
event.respondWith(respond(request, url, event));
});
async function respond(request, url, event) {
const cache = await caches.open(CACHE_NAME);
// 导航请求顺带做节流版本复核;失效发生在后台,本次响应不受影响,
// 下一次请求自然落入在线模式。
if (request.mode === 'navigate' && Date.now() - lastRecheck > RECHECK_INTERVAL) {
lastRecheck = Date.now();
event.waitUntil(verifyPayloadVersion());
}
const candidates = candidatesFor(url.pathname);
for (const key of candidates) {
const hit = await cache.match(key);
if (hit) return hit;
}
const meta = await readManifest(cache);
if (!meta) return fetch(request); // 在线模式:纯转发
// 清单承诺过这个路径但缓存里没有 → 条目被浏览器回收,缓存已不可信,
// 整体作废并回退在线模式。
if (candidates.some((key) => meta.keys.includes(key))) {
await caches.delete(CACHE_NAME);
return fetch(request);
}
// 清单之外的路径回源网络;断网时的导航请求用站点 404 页兜底。
try {
return await fetch(request);
} catch (error) {
if (request.mode === 'navigate') {
const notFound = await cache.match('/404.html');
if (notFound) {
return new Response(notFound.body, { status: 404, headers: notFound.headers });
}
}
throw error;
}
}

21
vendor/libarchive/LICENSE vendored Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 ნიკა
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

File diff suppressed because one or more lines are too long

Binary file not shown.

1
vendor/libarchive/worker-bundle.js vendored Normal file

File diff suppressed because one or more lines are too long