MoreRSS

site iconMayx修改

兴趣范围从技术到网络开发,还对编程、DIY和设计感兴趣。
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

Mayx的 RSS 预览

在Google杀死XSLT之后的XML美化方案

2026-02-08 00:00:00

即使没有了XSLT,也不能让读者看到光秃秃的XML!

起因

在半年前,我写了一篇用XSLT美化博客XML文件的文章,自从那以后,每次我在浏览其他人博客的时候,都会看一眼对方博客有没有给自己的订阅文件做美化。不过就在前段时间,我在浏览某个博客的时候,发现他博客的订阅文件,甚至连最基本的XML文档树都没有显示出来。这时候我打开开发者工具看了一眼源代码,发现他也并没有使用xml-stylesheet之类的指令……而且控制台貌似报了些错,好像是出现了什么CSP错误……于是我就想,浏览器显示XML文档树的本质,会不会其实也是一种XSLT?之所以报错也有可能是浏览器在自动引用内置的XSLT时违反了CSP。所以我就问了问谷歌AI,结果似乎真的是这样,比如火狐浏览器就内置了一份XSLT文件,IE浏览器也有。正当我为XSLT的功能感到强大时,谷歌AI随后提到,Chrome浏览器决定弃用XSLT,所以以后不要再用XSLT了😰……
我给我的订阅文件加美化功能才半年,怎么就要不能用了?XSLT出现这么多年都还能用,结果等我加上就要废弃了?当时为了增加这个功能,还是费了不少劲的,怎么能让谷歌说没就没?于是我就开始对这件事进行了调查。

Google杀死了XSLT

从上面Chrome的弃用XSLT文档中,可以发现,这件事的始作俑者是Mason Freed,他在WHATWG中发起了一个Issue,因为XSLT用的人很少,以及实现XSLT的库很老而且容易出漏洞,所以建议把XSLT从Web标准中删除。在这个Issue中可以发现,有很多人表示不满,毕竟这个功能对想要给自己订阅做美化的博主来说还是很有用的。为了对抗谷歌,还有人做了个网站: https://xslt.rip
而且XSLT虽然用的人占比也许不高,但从总量上应该还是挺多的,除了用XSLT美化博客订阅的,甚至还有用XSLT作为博客框架的,另外还有一些人提出一部分政府网站也有使用XSLT
不过Freed看起来对这件事早有准备,他做了一个Polyfill库,通过WASM的方式让XSLT可以正常工作,为了方便大家使用这个库,我顺手给CDNJS发了个PR,以后可以用CDN引用它了。不过使用这个库的前提是需要在订阅中加一段引用JS的代码,像我博客中的Atom订阅,用的是jekyll-feed插件,里面的格式都是写死的,就用不了了……
只不过现在已经没办法阻止谷歌了……而且其他浏览器也表示会跟进,看来我们唯一能做的就是去适应了。

没有XSLT之后的美化方案

纯CSS

虽然XSLT不能用,但不代表xml-stylesheet指令就不能用了,除了XSLT之外,xml-stylesheet同样可以引用CSS。只是似乎完全没见过用CSS美化订阅源的,也许是因为光用CSS能做到的事比较少吧,想用CSS给XML文档加链接之类的估计就做不到了。
但目前能选择的也不多了,既然大家都没写过用CSS美化订阅源,那就让我来写一个吧!然而我并不会写😅……那就只好让AI来写了,我把需求说清楚之后,AI就写出来了:feed.css。试了一下效果还挺不错的,我让AI写的这个版本无论是RSS还是Atom都可以使用,如果有人感兴趣可以拿去用。可惜我的Atom订阅因为用的是插件的原因用不了😭,只能加到用纯Liquid实现的RSS订阅上了。
但用纯CSS的缺点也很明显,没办法操作文档的内容,像修改日期格式的就做不了了,而且也不能添加超链接……XML的标签本身对浏览器来说并没有内建的语义,正常情况下也没法让浏览器把某个标签当作超链接。那难道就没办法了吗?

混合XHTML

如果完全不能修改XML内容,那确实就没有办法了,但如果能修改XML的内容那还是有办法的,简单来说就是混入XHTML,事实上Freed编写的Polyfill库原理上也是利用了XHTML,只要在能作为XHTML的标签中添加XHTML的命名空间,那么浏览器就可以理解它的语义并渲染,像刚刚用纯CSS美化的订阅没有链接,那就可以在根元素中添加命名空间:xmlns:xhtml="http://www.w3.org/1999/xhtml",然后在合适的位置写:

<xhtml:a href="https://example.com">Read more -&gt;</xhtml:a>

就可以了。只是这样有个缺点,这样写的订阅文件不够“纯粹”,用验证器验证会显示“Misplaced XHTML content”警告。对有洁癖的人来说可能会有点难受😆。
不过如果能接受这种“不纯粹”,那么其实xml-stylesheet指令也没必要了,link标签一样可以用,包括script也是,所以有人写了一个不使用XSLT美化XML的库。
只不过这种方法和XSLT相比还是有一些缺陷,要知道XSLT的本质是转换,是把XML转换为HTML,也就是说转出来的文档本质是HTML,所有的DOM操作都和操作HTML是完全相同的,但是在XML里混入XHTML标签就不一样了,它的本质依然是XML文档,只是嵌入了XHTML命名空间下的元素,所以相应的DOM操作会有一些不同。如果是自己写的纯JS可能还好,如果是用了jQuery之类假定DOM为HTML的库就会出现问题了,因此这也就是那个Polyfill库的局限性,用正常的XSLT执行document.constructor会显示HTMLDocument,而用这个Polyfill库执行完则是显示XMLDocument。因此,直接套用为浏览器原生XSLT编写的旧样式文件,就有可能会出问题,但如果要考虑改XSLT的话那还不如重新写JS,然后用XHTML引入呢。

感想

虽然有一些技术会因为各种各样的原因消失,但这不代表我们就要妥协一些东西,总有一些不同的技术可以解决相同的问题,所以我们只需要用其他的技术去实现就好了。不过这也是没办法的事情,毕竟没人能改变浏览器厂商们的决策啊😂。

年终总结

2026-01-01 00:00:00

0 error(s), ∞ warning(s)

2025年的状态

在2025年,感觉状态不如去年……由于没能做出正确的选择,还是有点糟糕。不过总的来说还没有引发关键性的错误,至少还能继续坚持下去。
在这一年中,感觉记忆和思考能力都有所下滑,看来是没把自己照顾好😂,不过看看这一年写的文章,看起来似乎比以前更流畅了,这也许是因为和AI聊得多了,以至于思维有点偏向AI了吧。
总的来说感觉自己的稳定性还是有点低了,但这可能不是我能独自解决的,也不知会有什么转机……

2025年发生的事情

回顾了一下去年的年终总结,发现自己还是没能做到知行合一,在这一年里全球各类资产突然开始大幅升值,也就是说钱真的开始不值钱了……那时候想着买黄金,这一年下来却没能下定决心,最终错过了资产保值的机会。至于现在,似乎什么也做不了了……当然这对我的生活并没有造成什么严重的打击,只是感受到环境对自己的影响罢了。
至于AI……依然是一天比一天强,而各个公司对AI的投入相比去年也是极大的提升,当然出来的效果也是非常强,那时候的AI还是挺容易出错,但是现在AI解决问题的能力已经可以替代很多人了,不只是文本生成模型,今年的图像与视频生成模型也真的是发展到了以往完全不能想象的地步,真的可以做到一句话想要什么就有什么了。
另外,今年写的博客内容过于围绕博客本身了,以至于似乎不太跟得上时代,虽然我的博客也确实有点老旧了😆。只是看看以前的文章,都还有一些面向未来的趋势,而今年就有点“考古”了。相比于考古,去展望未来显然是更有意义的事情,只不过……真的感觉脑子不太好使,未来会发生什么,已经完全无法预测了。

展望2026年

虽然不知道未来会发生什么,但毕竟还没有造成关键性的错误,还有修正的余地,只能希望未来能够做出正确的选择,不要让自己陷入危险的境地吧。

在浏览器中运行Linux的各种方法

2025-12-01 00:00:00

浏览器已经无所不能了!

起因

前段时间跟网友交流时,有人展示了他博客里的一个Linux终端模拟项目:jsnix,看起来挺有意思的,里面甚至还藏了一个CTF。不过我感觉他这个终端和博客本身并没有真正联动起来,本质上只是一个模拟了Linux Shell行为的交互界面。除此之外我还发现了另一个风格类似的个人主页,它虽然也走了终端风格,但功能更简单,还原度也不算高。不过它至少和博客内容做了一些基础联动——尽管目前也只是做到列出文章这种程度😂,当然有这类功能的博客应该也不少,只是我发现的不太多……于是我就想,不如我也给自己的博客加一个类似的“命令行访问”功能,应该会很有趣。当然如果真要做的话,我肯定不会满足于只实现几个模拟指令——既然要做,就要追求真实感,至少得在浏览器上运行真实的Linux终端,才不会让人觉得出戏吧😋。

在浏览器中运行Linux

虚拟机方案

纯JS虚拟机

要说到在浏览器上运行Linux,最先想到的应该就是Fabrice Bellard大神写的JSLinux吧,这可能是第一个在浏览器中实现的虚拟机(毕竟是最强虚拟机QEMU的作者编写的)。现在他的个人主页中展示的这个版本是WASM版本,而他最早写的是纯JS实现的。那个JS实现的版本现在在GitHub上有一个去混淆的版本可以用作学习和研究,于是我顺手Fork了一份在GitHub Pages上部署作为演示
作为纯JS实现的x86虚拟机,性能估计是最差的,但相应的兼容性也最好,在Bellard当年写JSLinux的时候,还没有WASM这种东西呢,所以即使是在不支持WASM的IE11中,也可以正常运行。假如我想把它作为终端用在我的博客上,似乎也是个不错的选择,即使我完全看不懂代码,不知道如何实现JS和虚拟机的通信,它也预留了一个剪贴板设备,可以让我轻松地做到类似的事情,比如我在里面写个Bash脚本,通过它和外面的JS脚本联动来读取我的文章列表和内容,那也挺不错。
当然Bellard用纯JS编写虚拟机也不是独一份,他实现了x86的虚拟机,相应的也有人用纯JS实现了RISC-V的虚拟机,比如ANGEL,看起来挺不错,所以同样也顺手搭了一份。只不过它似乎用了一些更先进的语法,至少IE11上不能运行。
另外还有一个比较知名的项目,叫做jor1k,它模拟的是OpenRISC架构。只是这个架构目前已经过时,基本上没什么人用了,不过这里面还内置了几个演示的小游戏,看起来还挺有意思。
除了这些之外,其实能在浏览器上运行的Linux也不一定是个网页,有一个叫做LinuxPDF的项目可以让Linux运行在PDF中,它的原理和JSLinux差不多,所以需要PDF阅读器支持JS,看它的介绍貌似只能在基于Chromium内核的浏览器中运行,而且因为安全问题在PDF中有很多功能不能用,所以它的速度甚至比JSLinux还要慢,功能还很少,因此它基本上只是个PoC,没什么太大的意义。

WASM虚拟机

那还有别的方案吗?既然Bellard都选择放弃纯JS的JSLinux而选择了WASM,显然还有其他类似的项目,比如v86,这也是一个能在浏览器中运行的x86虚拟机,不过因为使用了WASM和JIT技术,所以效率要比纯JS的JSLinux高得多。另外作为虚拟机,自然是不止能运行Linux,其他的系统也能运行,在示例中除了Linux之外还有DOS和Windows之类的系统,功能还挺强大,如果能自己做个系统镜像在博客里运行,似乎也是不错的选择。
另外还有一个相对比较知名的叫WebVM,从效果上来说和v86几乎没有区别,同样使用了WASM和JIT技术,也都只支持32位x86,然而它的虚拟化引擎CheerpX是闭源产品,既然和v86都拉不开差距,不知道是谁给他们的信心把它作为闭源产品😅。不过看它的说明文档,其相比于v86的主要区别是实现了Linux系统调用,考虑到它不能运行其他操作系统,而且Linux内核也不能更换,那我想它可能是类似于WSL1的那种实现方案,也许性能上会比v86好一些吧……只不过毕竟是闭源产品,不太清楚具体实现了。
既然纯JS有RISC-V的虚拟机,WASM当然也有,比如WebCM。这个项目相比于其他的项目有个不太一样的地方,它把虚拟机、内核以及镜像打包成了一个单独的WASM文件……只是这样感觉并没有什么好处吧,改起来更加复杂了。
以上这些虚拟机方案各有不同,但是想做一个自己的镜像相对来说还是有点困难,于是我又发现了另一个项目:container2wasm,它可以让一个Docker镜像在浏览器中运行,当然实际实现其实和Docker并没有什么关系,本质还是虚拟机,只是制作镜像的时候可以直接用Docker镜像,方便了不少,但Docker镜像一般也都很大,所以第一次加载可能要下载很长时间。另外它还有一个优势,可以使用Bochs运行x86_64的镜像,不像v86和WebVM只能模拟32位的x86(虽然Bochs的运行效率可能会差一些),而且可以使用WASI直接访问网络,不像以上几个项目如果需要访问网络需要用到中继服务。当然访问网络这个还是要受浏览器本身的跨域策略限制。总之从项目本身来说感觉也算是相当成熟了,尤其能用Docker镜像的话……我甚至可以考虑直接用镜像在线演示我曾经的Mabbs项目😋。

纯WASM方案

其实想要在浏览器中运行Linux也不一定非得要用虚拟机,用虚拟机相当于是把其他指令集的机器码翻译为WASM,然后浏览器还得再翻译成宿主机CPU支持的指令集,然而WASM本身其实也算是一种指令集,各种编译型语言编写的程序也能编译出WASM的产物,比如FFmpeg。所以Linux内核也完全可以被编译成WASM,正好前段时间我看新闻说Joel Severin做了这么一个项目,对Linux内核做了一些修改使其可以被编译为WASM程序,我试了一下,貌似在Safari浏览器中不能正常工作……Chrome浏览器倒是没问题,不过即使这样用起来BUG也很多,随便执行几条命令就会冻结,体验不是很好。
沿着这个项目,我又找到一个由Thomas Stokes制作的项目,和Joel的项目差不多,但我测了一下可以在Safari上运行,感觉这个项目更完善,不过之前那个项目上了新闻,所以⭐️数比这个更高😂。
于是我把它复制了一份,在我的GitHub Pages上部署了,但直接用仓库中的源代码会显示“Error: not cross origin isolated”,然而在Thomas自己部署的网站中可以正常打开,我看了一眼貌似是因为在GitHub Pages中没有COOP和COEP响应头导致的。Linux作为多任务操作系统来说,当然要运行多个进程,而Linux要管理它们就需要跨线程(Web Worker)读取内存的能力,所以用到了SharedArrayBuffer对象。不过由于CPU曾经出过“幽灵”漏洞,导致现代浏览器默认禁止使用SharedArrayBuffer对象,除非在服务器中配置COOP和COEP响应头才可以用,但是Joel的项目也是在GitHub Pages上运行的啊,为什么可以正常运行?看了源代码后才发现原来可以用Service Worker作为反向代理来给请求的资源加上响应头,他使用的是coi-serviceworker这个项目,所以我也给我部署的代码中加上了这个脚本,总算是解决了这个问题。
部署好这个项目之后我试用了几下,虽然有些操作仍然会导致系统冻结,但相比Joel的版本来说已经好多了。很遗憾的是目前这个WASM Linux还不能和外界通信,所以作用不是很大,另外如果想在里面运行其他二进制程序还是相当困难,首先在WASM中不存在内存管理单元(MMU),不能实现隔离和分页的功能,另外以WASM作为指令集的环境下编译的产物也得是WASM,所以目前来说想用它做点什么还是不太合适。
以上的这两个将Linux内核编译为WASM的方案其实相当于给内核打补丁,然后把浏览器看作是虚拟机来运行,有点像Xen,不过还有一种让Linux原生运行在WASM的项目,它将Linux kernel library编译为了WASM。那么什么是LKL?简单来说它有点像Wine,就和我之前所说的OS模拟器差不多,可以提供一个环境,让程序以为自己在Linux下运行,所以说它和之前的实现有一些不一样,它不存在内核模式,更像是一个普通的程序,而不是系统了。
不过这个项目的体验也比较一般,它无论做什么都得按两次回车,看说明的意思貌似是因为没有实现异步信号传递,所以要手动打断read函数,而且也经常莫名其妙卡住,总体体验不如Thomas的项目。

模仿的Linux

其实如果只是想做到和Linux类似的功能,也有这样的项目,比如WebContainers,它没有运行Linux系统,但是模拟了一个环境,可以在浏览器中运行Node.js以及Python之类的脚本,而且让脚本以为自己在Linux中运行,除此之外它还能用Service Worker把环境中运行的端口映射给浏览器,可以算是真的把服务端跑在浏览器上了。这个技术还挺高级,不过想想也挺合理,毕竟有WASI,直接编译为WASM的程序也不需要操作系统就能运行,所以用WASM去运行Linux本来就有点多此一举了😂。不过很遗憾的是WebContainers也不是开源软件,要使用它只能引入StackBlitz的资源,而且全网完全没有开源的替代品……也许在浏览器上进行开发本来就是个伪需求,所以没什么人实现吧。
当然如果只是实现和WebContainers类似的功能,JupyterLite也可以实现,它可以在浏览器中像使用本地JupyterLab那样运行JS和Python,还能用Matplotlib、Numpy、Pandas进行数据处理,功能可以说非常强大,而且还是开源软件。只不过它没有模拟操作系统的环境,所以不能运行Node.js项目,也不能提供终端,所以不太符合我想要的效果……

总结

总的来说,如果想要在博客上搞Linux终端,目前来看似乎虚拟机方案会更靠谱一些,虽然相对来说效率可能比较低,但毕竟目前WASM方案的可靠性还是不够,而且考虑到还需要配置额外的响应头,感觉有点麻烦,当然我觉得WASM还是算未来可期的,如果成熟的话肯定还是比虚拟机要更好一些,毕竟没有转译性能肯定要好不少。至于WebContainers这种方案……等什么时候有开源替代再考虑吧,需要依赖其他服务感觉不够可靠。只是也许我的想法只需要模拟一个合适的文件系统,然后给WASM版的Busybox加个终端就够了?不过这样感觉Bug会更多😂。
至于打算什么时候给博客加上这个功能?应该也是未来可期吧😝,目前还没什么好的思路,仅仅是分享一下在浏览器中运行Linux的各种方法。

让博客永恒的探索

2025-11-01 00:00:00

Mayx Forever Project – Phase II

起因

在前段时间,我通过Ecosyste.ms: Repos找到了不少Git平台的实例,也在探索的过程中发现和了解了Tilde社区。当然仅仅是这样显然还不够,里面的实例太多了,显然还有一些其他值得探索的东西。
在我查看这里面的某些Gitea实例时,发现了一些奇怪的事情,有些实例的仓库数和用户数多得离谱,正常来说除了几个大的平台,绝大多数应该只有几十到几百个仓库,这就让我有点好奇了。于是当我点进去之后发现,里面有一大堆仓库都是空的,而且用户名和仓库名都非常有规律,看起来都是一组单词加4位数字命名的,显然这不是正常现象,应该是一种有组织的行为。

被SPAM滥用的Git实例

于是我就简单看了一下这些异常的仓库和用户的规律,可以发现每个用户都填了个人主页地址,然后个人简介里大都是一段广告词。另外这些个人主页的地址看起来很多都是利用公开可注册的服务,比如开源的有各种Git平台、Wiki,以及论坛,还有一些允许用户写个人主页的新闻网站。在这其中,Git平台大多都没有广告文章,基本上都是通过个人主页地址链接到网站,而Wiki之类的就会写一些篇幅比较长的广告文章。
另外这些平台但凡还在开放注册,就会被以大约每分钟一次的速度自动注册新账号……所以这种事情到底是谁在干呢?我翻了几个仓库,里面的广告多种多样,有些看起来还算正常,还有一些看起来有些黑产。其中我发现有一家叫做“悠闲羊驼SEO”的网站,看介绍主要是给加密货币、对冲基金和博彩网站提供SEO优化的,再加上这些被滥用的平台里也有不少类似的广告,所以我怀疑这些滥用的行为就是这家SEO公司做的(虽然没有证据😂)。

永恒的探索

看到这么多Git平台被滥用,我就有个想法,之前为了保证可靠性给博客加了不少镜像,除此之外也在互联网档案馆、Software Heritage、Git Protect等存档服务中上传了备份,而且也在IPFS和Arweave等Web3平台上有相应的副本,但是我觉得还不够,再大的平台也有可能会倒闭,IPFS不Pin还会被GC,至于Arweave前段时间看了一眼整个网络才几百个节点,感觉一点也不靠谱……所以我应该好好利用这些平台提高我博客的可靠性。
既然那些Spammer只是为了SEO去滥用这些平台,不如让我利用这些平台给我的博客进行镜像吧!至于使用哪个平台……显然用Git平台方便一些,所以接下来就该考虑一下怎么样分发了。

镜像的分发

在Git平台中也有很多选择,最知名的是GitLab,不过GitLab有点复杂,接口不太好用……而且很多实例没有开镜像仓库的功能,毕竟如果我每次更新都给一堆仓库推送太费时间了,我打算让各个平台主动从GitHub上拉取我的最新代码。正好Gogs系列的平台基本上都默认支持镜像仓库,不过在我实际使用的时候发现Gogs默认情况下注册要验证码……写识别验证码感觉又挺麻烦,而Gogs的两个分支——Gitea和Forgejo反倒没有……还挺奇怪,所以接下来我的目标主要就是Gitea和Forgejo的实例了。
既然决定好目标,我就得先发现它们了,那些Spammer在注册的时候会在个人主页里写不同的网站,其中也有一些类Gogs平台,那么我可以先找一个Gitea平台,用接口读取这些网站,然后再调类Gogs专属的接口来检测这些网站哪个是类Gogs平台,于是我就写了个脚本来找到它们。
找到这些平台之后就该注册了,还好Gitea和Forgejo默认没有验证码,注册起来也很简单,随便写了个函数实现了一下:

def register_account(session, url, email, username, password):
    try:
        resp = session.get(url + "/user/sign_up")
        soup = BeautifulSoup(resp.text, "html.parser")
        csrf_token = soup.find("input", {"name": "_csrf"}).get("value")

        payload = {
            "_csrf": csrf_token,
            "user_name": username,
            "email": email,
            "password": password,
            "retype": password,
        }
        headers = {"Content-Type": "application/x-www-form-urlencoded"}
        resp = session.post(url + "/user/sign_up", data=payload, headers=headers)
        if "flash-success" in resp.text:
            print(
                f"Successfully registered at {url} with username: {username}, email: {email}, password: {password}"
            )
            save_to_file(
                "instances_userinfo.csv", f"{url},{username},{email},{password}"
            )
            return True
        else:
            print(f"Failed to register at {url}.")
            return False
    except Exception as e:
        print(f"Error registering at {url}: {e}")
        return False

注册完之后就该导入仓库了,只是通过模拟前端发包的方式在Gitea和Forgejo中不同版本的表现可能不太一样,所以我想用API实现,但是API又得有API Key,生成API Key还得模拟前端发包😥……所以怎么都绕不过。
不过这个生成API Key还挺麻烦,有些版本不需要配权限范围,有些配权限的参数还不一样……不过我就是随便一写,凑合用吧,像那些专业的Spammer应该是有更强大的脚本判断各种情况。
最后我还是选择用API导入,又写了个函数:

def import_repos(token, url):
    try:
        response = requests.post(
            url=url + "/api/v1/repos/migrate",
            headers={
                "Authorization": "token " + token,
            },
            json={
                "repo_name": "blog",
                "mirror_interval": "1h",
                "mirror": True,
                "description": "Mayx's Home Page",
                "clone_addr": "https://github.com/Mabbs/mabbs.github.io",
            },
        )
        if response.status_code == 201:
            print("Repository import initiated successfully.")
            save_to_file("repo_list.txt", url + "/mayx/blog")
            return True
        else:
            print(f"Failed to initiate repository import. Status code: {response.status_code}")
            print(f"Response: {response.text}")
            return False
    except Exception as e:
        print(f"Error updating website: {e}")
        return False

脚本写好之后我就只需要重复扫描、注册、导入的步骤就行了,这样我的镜像就会越来越多,而且用类Gogs的实例还有一个好处就是不需要我手动推送,它会自动定时拉取我的仓库保持最新,这样也许只要人类文明存在我的博客就会在某处存在吧🤣。
最后我创建的Git镜像可以在这里看到,看起来还是挺壮观啊😋。只不过像这种会被Spammer随便注册的Git平台实例很难说它能活多久,如果没人管而且是云服务器也许到期就没了,有人管的话应该不会允许这么多Spam行为吧……

感想

不知道用“量”来确保博客的永恒更可靠……还是用“质”的方式更好呢?其实我觉得还得是活动的更好,就像我以前所说的,如果有僵尸网络,自动帮我执行发现并推送的操作,也许比等着这些实例逐渐消失更好吧……只不过那样可能就不太友好了😂。

一次找回GitHub上被删除仓库的经历

2025-10-12 00:00:00

在GitHub中寻找踪迹也许是非常简单的事情……

起因

前段时间,有人和我聊天的时候提到了Brainfuck语言,让我回想起了高中时写的演讲稿。那时候我在演讲时也介绍了Brainfuck语言。对于Brainfuck的解释器,各种语言都可以实现,不过我当时为了方便理解用了一个在GitHub Pages上的网站,用可视化的方式演示了它的运行过程,效果很不错。现在既然聊到了,自然就想分享一下这个演示的网站,但我正想打开时,发现网站已经404了😰。
在GitHub Pages上的网站都有对应的仓库,现在不仅原仓库消失了,连作者的首页都打不开,看样子是完全退出GitHub了……那么我想找到这个网站的想法就无法实现了吗?不过GitHub有些有意思的特性也许能帮助我找回这个网站。

GitHub的特性

在GitHub中,一个普通的仓库可能没有什么特别的,也许就是服务器上的一个文件夹。但是当仓库被其他人Fork的时候就不一样了,在执行Fork时,显然GitHub不会完整复制整个仓库。否则,同一个仓库在服务器上会占用双倍空间,这显然不合理。另外,想想Git的结构:它由提交对象和分支指针构成,每次提交都有唯一的Hash值且不会冲突。因此可以推测,GitHub在实现Fork时,所有被Fork的仓库可能共享同一个对象库,而每个用户仓库只保存指针,这样所有仓库只会占用增量空间,而不会存储重复内容。
但这样也会带来一个问题,首先因为很多人可能要共用一部分对象,所以也很难确认对象的所有权,而且也因为这个原因所有的对象要能被所有人访问。因此在整个Fork网络中,只要有一个仓库存在,GitHub就必须保留所有的对象,而且每个仓库都能访问这个网络中所有的对象。为了验证这一点,我们可以用最知名的Linux内核仓库做个示例。
首先对Linux仓库进行Fork,然后我们可以随便做一些改动,比如在README中写“Linux已经被我占领了😆”之类的内容,提交到自己的仓库,并且记下提交的Hash值,接下来就可以把自己的仓库删掉了。如果上面的猜想是正确的,那么在这个Fork网络中的任何一个仓库查看我刚刚的提交应该都可以,于是我直接在主仓库拼上了提交的Hash值(顺便一说只要值唯一,和其他的提交不冲突,短的Hash值也可以),果不其然能找到刚刚修改的内容,这样一来,只要GitHub和任意一个Linux仓库的Fork还存在,这个提交就永远存在了😝。

找回仓库

那么接下来找回之前网站的方案就很简单了,我只要找到网站仓库的任意一个Fork,然后只要知道最新的提交Hash,我就可以还原最新的仓库了。Fork倒是好找,随便搜一下就能找到一个。这个Fork的最新提交是2016年,但要想找到我当年演讲的版本至少到2018年之后。不过这个Hash值也不太好找,虽然理论上爆破短Hash值也可以,但是感觉太麻烦了,没有那个必要,所以我干脆直接去互联网档案馆看看能找到的最新的仓库页面吧,这样我就能找到它的Hash值了,然后我再把Fork仓库的地址和Hash拼到一起,就看得到最新代码了。
当然,仅仅看到代码还不够。我想Fork这个项目并在自己的GitHub Pages上部署一份。有没有什么好办法可以将我仓库的HEAD指针指向最新的提交呢?其实很简单,首先我要Fork这个Fork仓库,然后Clone我的仓库到本地。不过,此时Clone下来的仓库并不包含GitHub上完整的对象库,因此直接checkout或reset是不行的。这时Hash值就派上用场了,通过fetch拉取对应提交后,就可以进行上述操作。具体命令如下:

git fetch origin <commit-hash>
git reset --hard <commit-hash>
git push origin master

最终我就获得了包含最新代码Brainfuck可视化演示了🎉。

结局

后来我才知道,原来有一个专门的组织Software Heritage会保存所有代码,根本没必要搞这些花里胡哨的操作😂,像这个仓库也是能很轻易在上面找到,这下以后知道了,再遇到类似情况就可以直接去Software Heritage查找,而不必在互联网档案馆上找线索瞎折腾了🤣。

关于ZIP Quine与自产生程序的探索

2025-09-01 00:00:00

描述自己的代码……是一种什么样的感觉?

起因

前段时间我在折腾博客部署的时候,回顾起了好久以前写的部署脚本。对于全站打包的这个步骤,本来我打算利用这个压缩包结合Service Worker做离线浏览,但因为没有合适的方案所以放弃了。而现在对于这个压缩包,我又有了一个特别的想法。事实上在这个下载全站的压缩包中,里面的内容和实际的网站并不完全相同,因为在这个压缩包里缺少了压缩包本身。所以把这个压缩包解压之后直接当作网站打开,会发现下载压缩包的链接是无效的,除非在解压之后把压缩包移动到网站里才行……
于是我就在想有没有一种可能可以让压缩包解压之后里面又包含了这个压缩包本身?似乎是个不太可能的事情,但我以前听过类似的东西,也许并非不可能?所以这次就来探索一下吧。

自包含压缩包的探索

在很久之前,我见到过一个很知名的自包含压缩包(又称为ZIP Quine),叫做droste.zip,是由Erling Ellingsen在2005年制作出来的。当时我只知道它很神奇,原理什么的并不清楚,另外在网上也基本上找不到类似的压缩包。现在再回看时发现介绍里包含了一些相关的链接,甚至还有一篇能自己制作类似压缩包的论文,所以接下来就可以看一下这些链接来理解这种压缩包是如何制作的了。
关于原理方面,先看Will Greenberg制作的一个示例,在这里面有一个谜题,使用“print M”(原样输出接下来的M行输入内容)和“repeat M N”(从倒数第N行的输出内容开始,重复M行)这两个指令让最终执行的结果和输入的指令完全相同。这正是对DEFLATE压缩算法所使用的LZ77编码的一种简化模拟,也就是说只要解决了这个问题,就可以让压缩包在解压时原样输出自己了。
这个问题看起来还挺复杂,不过在仓库的Issues就有人给出了几种解法(当然,这个题目解法不唯一),所以在理论上应该是可行的,那么接下来就需要研究压缩文件的格式来实现它了。

实现ZIP Quine的探索

Russ Cox写的《Zip Files All The Way Down》文章中,同样说明了这个原理,而且给出了一个方案,让上述这两个命令除了能够对命令本身的重复以外,还可以添加一些额外数据,这样才能做到构建一个压缩包文件。按照文章的描述,如果用之前谜题的规则来说,我们设头和尾的内容都是“print 0”,那么Cox给出的方案如下:

print 0
print 2
print 0
print 2
repeat 2 2
print 1
repeat 2 2
print 1
print 1
print 4
repeat 2 2
print 1
print 1
print 4
repeat 4 4
print 4
repeat 4 4
print 4
repeat 4 4
print 4
repeat 4 4
print 4
repeat 4 4
print 0
print 0
print 2
repeat 4 4
print 0
print 0
print 2
repeat 2 2
print 0
repeat 2 2
print 0

我们把这些指令粘贴到quine.zip这个谜题中,就会发现输出和输入完全相同,以此就能验证Cox方案的正确性。除此之外作者还给出了生成的源代码:rgzip.go,只是代码里面到处都是用来构建压缩包的十六进制数字,完全看不懂😂。
另外这个方案是针对使用基于LZ77与哈夫曼编码的DEFLATE压缩算法,所以格式不重要。因此无论是ZIP,还是GZIP,以及TGZ(GZIP压缩后的TAR),其实都是一样的,因为他们都使用的是DEFLATE压缩算法。顺便一提,Matthew Barber写了一篇很棒的文章,通过动画演示并详细讲解了如何实现一个简单的GZIP版ZIP Quine,很值得一看。
还有一点,普通的TAR文件能否实现类似功能呢?从原理来说估计不行,因为TAR文件本身并没有压缩,也不包含指令,就单纯是一堆文件和元数据的拼接,所以就做不到自包含了。
这么来看既然TGZ可以,那是不是在我博客网站的压缩包里放一份和自己一模一样的压缩包是可行的?很遗憾按照这个方法来看是做不到的,由于压缩格式和编码的限制,这个方案在实际实现时发现操作码需要是5个字节,最后发现最多只有类似repeat 64 64这样的指令能够满足要求,因此头尾区最多只能放64-5=59个字节的数据,也就刚刚好能容纳压缩格式需要的内容,几乎没法塞更多东西进去……显然,这些限制导致这种方式对我来说意义就不大了,何况作者的代码我也看不懂……而且还要考虑压缩包还存在校验用的CRC32,需要找满足整个压缩包的CRC32正好在压缩包中的“不动点”。虽然从CRC32的原理来说应该有办法做到通过数学方式解决,但这篇文章的作者因为解决了自包含的问题之后累了,因此放弃继续研究,选择直接暴力破解,毕竟CRC32只有32位,估计思考的时间都要比爆破的时间长吧😂。但如果是这样,即使有方案能存下我博客的数据,也不能在每次网站构建的时候都制作一次了……
虽然Russ Cox写的文章看起来做不到包含更多内容了,但Erling Ellingsen制作的droste.zip却包含了一张图片,说明并不是没办法加入更多数据,只是没有找到正确的方法。在2024年Ruben Van Mello写了一篇论文《A Generator for Recursive Zip Files》,在这篇论文里他不仅解决了包含的额外数据过少的问题,还编写了一个通用工具,能让普通人也能生成这样的压缩包,而且他还创新性的做了一种像衔尾蛇一样的双层嵌套循环压缩包,非常的有意思,所以接下来我打算试试他的方案。
在这篇论文中,里面简述了之前Russ Cox写的内容,也提到了59字节的限制,于是作者对原有的结构进行了一些改动,让操作码可以超出5字节的限制,具体可以看论文的表6,从而解决了只能包含59字节额外数据的限制。但由于DEFLATE压缩格式本身的约束(16位存储块长度以及32KiB回溯窗口),即使能够添加文件,最多也只能额外容纳32763字节的数据(其中包括压缩包所需的文件头)……显然这点空间完全存不下我的博客😭,看来我只能打消这个想法了。但既然都研究了半天,也不一定要存我的博客嘛,可以看看还有没有别的东西可以存?在这之前先继续阅读论文,看完再说吧。

制作一个嵌套循环的ZIP Quine

在实现了常规的ZIP Quine之后,接下来就是作者的创新点了(如果光是解决存储限制这点创新点估计还不够发论文吧😂)。作者接下来制作了一种循环压缩文件,在压缩包内包含文件A和压缩包A,而压缩包A中则包含文件B和最初的压缩包,从而形成一个循环递归的结构。看论文的描述所说如果把外层的压缩包和内层的压缩包的开头和结尾按照一定的规则交替混合,就可以看作是一个整体,然后按照之前做ZIP Quine那样处理就可以……具体实现的细节得看论文的表10。只不过既然是把两个压缩包看作一个整体的话,按照上面的限制,自然每个压缩包能容纳的数据量就更小了,每个最多只能容纳16376字节的数据……
另外既然这里面有两个压缩包,那么每个压缩包还有自己的CRC32校验和,理论上如果要爆破的话计算难度得是原来的平方,这样难度就太大了。不过作者发现如果把数据的CRC32值取反(即与“0xFFFFFFFF”取异或)然后和原始数据拼到一起,整个数据的CRC32校验和就会被重置为一个固定的值“0xFFFFFFFF”,看起来挺有意思,正常的哈希算法可没有这种特性。因此原本计算难度很大的爆破计算现在就可以和之前一样了…… 话说为什么不让两层的CRC32都这样计算(包括之前单层的ZIP Quine)?这样就不需要爆破了……貌似是因为在普通的ZIP Quine中满足条件的CRC32需要出现两次,所以不能用这个方案吧?
现在所有的理论都足够了,我需要挑一个文件来做这样嵌套循环的ZIP Quine,既然博客的大小不可以……要不然我就用我写过的第一个大项目——Mabbs吧,这个项目的主程序是22KiB,看起来似乎超出了嵌套循环ZIP Quine的限制?其实没有,它的限制指的是压缩后的大小,我这个程序压缩之后是8KiB左右,所以完全没问题。
接下来就该使用论文中提到的生成工具:zip-quine-generator,这是一个Kotlin编写的程序,从发布中可以下载预构建的程序,接下来只要按照README中的描述使用“--loop”参数就可以用这个程序创建嵌套循环的ZIP Quine了。不过它原本的代码不能修改里面生成的压缩包的名字,另外压缩后的文件属性是隐藏文件,还有生成的压缩包中文件的创建时间总是当前时间,以及给文件内填充额外数据的代码里面填的是作者的声明,表示文件是由他论文的所写的生成器生成的……这些情况让我感觉有点不爽,还是希望这些部分能自定义一下,所以我就小改了一下他的代码。顺便一说,Kotlin编译起来还挺简单,直接一句kotlinc src/main/kotlin -include-runtime -d output.jar就可以了,也不需要折腾Maven之类乱七八糟的东西。最终我修改并编译完程序之后就把文件丢到服务器上开始给我爆破CRC32了,花了10个小时就算出来了,倒是比想象中快😂。
(2025.09.26更新)在2025年9月15日的时候,Nate Choe给zip-quine-generator做了个重大贡献,他通过数学的方式让CRC32的值可以不需要通过爆破的方式算出来,现在想要再制作这样的压缩包就可以瞬间生成了……要是我再晚点做这个压缩包就不需要花那么长时间了吧🤣。
最终我给我的Mabbs项目创建了Infinite Mabbs这个发布,生成的文件也可以在这里下载,这也算是不枉我研究半天这个论文了😆。

自产生程序的探索

说起来自包含压缩包为什么叫做ZIP Quine?其中的Quine是什么意思呢?其实这是一位美国哲学家的名字,他提出了“自指”的理论概念,所以为了纪念他,有类似概念的东西就被称作Quine,具体为什么也可以去看维基百科的说明。现在提到Quine一般代表的就是自产生程序,而自包含压缩包因为实现的原理和自产生程序的原理差不多,所以叫做ZIP Quine。因此接下来我打算探索一下自产生程序,更深入地了解Quine。

实现Quine的探索

那么什么是自产生程序?简单来说就是程序的源代码和程序的输出完全相同的程序,而且通常来说不允许通过读取/输入源代码的方式实现。按照一般的想法,让程序输出自身就需要输出中有全部代码,整个代码就会变长,而更长的代码就要输出更多,然后代码就会越来越长……所以这么想来似乎成了个死胡同。但其实这种程序实现起来并不复杂,想想ZIP Quine的实现,关键在于指令还需要以数据的形式表现,并且能被引用,这样输出的时候就会连着指令一起输出了。比如用Python的Quine举例:

c = 'c = %r; print(c %% c)'; print(c % c)

这里的变量中就以数据的形式存储了程序的代码,而在输出的时候除了变量内的代码,又通过引用的方式又把变量的内容放回到赋值的地方,所以它的输出就和原本的代码一样了。
其实Quine的实现思路都差不多是这样,可以在Rosetta Code中找到各种语言实现的Quine,在这其中能够发现大多数高级语言的写法都是类似的,除了一些低级语言以及esolang……这些我也看不懂😂,主要是有些语言没有变量的概念,不知道是怎么区分代码和数据……除了那个网站,在这里还能找到更多由esolang编写的Quine,可以看出来基本上很难看懂,其中最令人望而生畏的还得是用Malbolge写的Quine,这个代码看起来不仅很长,而且像乱码一样。至于什么是Malbolge?这就是Malbolge程序:

D'<;_98=6Z43Wxx/.R?Pa

代码就像加了密似的,顺便一说这个执行的输出结果是“Mayx”,关于Malbolge的具体细节可以看它的规范,另外虽然这个语言写起来很复杂,但还是有人能用它编出程序的,甚至还有人用Malbolge Unshackled(Malbolge不限内存的变种)写过Lisp解释器,实在是恐怖如斯😨。

只能Quine的语言

其实想要做出Quine,还有一种更加无聊的方案,那就是设计一种只能Quine的语言🤣。根据Quine的定义,代码输出的结果就是它本身……所以我们可以把任何内容都看作代码,然后这种语言的行为就是输出所有代码……听起来是不是有点无聊?但是想想看如果把Linux中的cat命令当作解释器,就可以实现这种语言了,比如:

#!/bin/cat
Hello, world!

作为脚本执行的结果就是原样输出这段内容,不过把内容当作代码算不算作弊呢……如果看作是cat的输入显然是作弊,但如果是当作源代码的话应该就不算了吧😋……但这就不是能写出逻辑的语言了。所以说Quine的趣味并不在“能不能实现”,而在于如何在限制条件下实现。正是因为大多数语言不会直接“自我输出”,才会觉得那些精巧的Quine程序如此有意思。

Quine Relay的探索

还有一个更加复杂的Quine变种是“Quine接力”(Quine Relay),即一个程序输出另一个程序的源代码,另一个程序又输出下一个程序的源代码,最后回到原始程序,就和之前所说的嵌套循环ZIP Quine有点类似。最著名的例子是Yusuke Endoh(这位还是IOCCC的冠军之一)创建的quine-relay项目,它包含了128种编程语言的循环。
这种程序写起来会更复杂一些,不过原理都差不多,通常除了当前运行的部分是可执行代码外,其他的代码都需要以额外包含的数据形式(如字符串)存储在变量中。如果想自己做个类似简单的Quine Relay,除了去看维基百科之外,前段时间我还看到过一个不错的文章,里面就讲了如何用“笨办法”编写Quine和Quine Relay,通过把变量中的内容编码为16进制来避免不同语言可能存在的特殊字符转译问题,思路不错,对于理解如何编写这类程序的问题很有帮助。当然这只是个简单的方案,仅适用于一些常规的编程语言,像上面那个quine-relay项目中甚至还包含Brainfuck之类的esolang,这种估计得要想办法让相对高级一些的语言通过“生成”的方式得到输出下一种代码的代码,而不是简单的赋值了,所以只靠这点知识想去完全理解大佬的作品还是想多了😆。
顺便一说,quine-relay并不是那位大佬唯一的Quine作品,他还做过有冗余的Quine以及动态的Quine,真的是相当的厉害……

Polyglot Quine的探索

除了Quine Relay之外还有一种很复杂的Quine,叫做Polyglot Quine,与Quine Relay需要在程序执行后才能切换到其他语言接力不同,Polyglot Quine的源代码本身即可同时属于多种语言,而且用这些语言的解释器每个执行后的输出全都一样,都与源代码完全一致。由于不同的编程语言的格式既有些相同之处,也有很多不同之处,所以让同一份代码表示不同语言就会很容易产生歧义,这时候就只能想办法通过一些特别的方式(比如将可能会对当前语言产生干扰的代码看作是注释的方式)来规避语言之间的差异。
Quine本身就已经很困难了,再加上这些限制就变得更加复杂了,所以制作Polyglot Quine的编程语言基本上都得精挑细选,而且通常只有两种语言,比如这段代码就是C和Python的Polyglot Quine,它巧妙利用了C预处理器指令在Python中可视为注释的特性,使两种语言互不干扰,非常有趣。当然并不是说只能是两种语言,像这个项目甚至使用了五种语言(C、Perl、PHP、Python、Ruby),可以说是相当厉害了。除此之外更令人惊叹的则是PyZipQuine项目,在这其中LZ77编码也可以作为一种语言,所以既可以被当作压缩包,也可以作为Python2.7代码,而且二者都是Quine,实在是令人赞叹。

感想

虽然这次探索最终没能完成让包含博客所有内容的压缩包自包含,但是在探索的过程中我还是收获了不少,尤其是Ruben Van Mello制作的ZIP Quine生成工具,实在是太棒了。很久以前我见到droste.zip这个压缩包的时候,就想整一个属于自己的ZIP Quine,现在我不仅用那个生成工具做了一个,还是对我来说很有意义的第一个项目——Mabbs,而且更关键的还是生成的是比普通的ZIP Quine更高级的嵌套循环ZIP Quine,也算是圆了小时候的心愿了。
另外在探索自产生程序的时候,也发现了一些很有意思的网站,比如Rosetta Code以及Esolang wiki (虽然这个网站里被好多小学生写了一堆无聊的东西😂) ,里面有不少有趣的东西,也算是让我大开眼界了。
所以有的时候探索不一定要完成目标,在这个过程中也会收获到很多不错的东西吧😊。