<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>datagirl.xyz</title>
        <description>Posts found on datagirl.xyz.</description>
        <link>https://datagirl.xyz/</link>
        <atom:link href="https://datagirl.xyz/feed.xml" rel="self" type="application/rss+xml" />
            <item>
                <title>Detecting DOSBox from within the Box</title>
                <pubDate>Mon, 15 Dec 2025 19:04:27 -0800</pubDate>
                <guid isPermaLink="false">urn:uuid:ef01506c-da2b-11f0-9187-ead9fe9152e8</guid>
                <link>https://datagirl.xyz/posts/dos_inside_the_box.html</link>
                <description><![CDATA[<p>If you're the sort of person who reads blogs, I assume you need no introduction to <a href="https://www.dosbox.com/">DOSBox</a>. It's an MS-DOS emulator, which necessitates it being a sort of x86 emulator. But unlike x86 emulators like <a href="https://86box.net/">86Box</a> or <a href="https://www.qemu.org/">QEMU</a>, the DOS parts are an inextricable part of it. There are BIOS interrupts and a POST, but not a BIOS in the sense of &quot;a ROM chip mapped into memory.&quot; There isn't even <em>really</em> a DOS, in the traditional sense. But when you're running inside DOSBox, you wouldn't know it. Almost any DOS API you can expect is available, and effort was put into making sure features like Long File Names don't appear if your reported version is too old to have supported it. So how can you detect that which seeks not to be detected?
</p><p>Most MS-DOS-likes aren't perfect replicas of MS-DOS, and you can usually use those quirks or extra functions to figure out what you're running on.<sup>[<a href="#fn:aard" id="fnref:aard">1</a>]</sup> And one would imagine DOSBox is the same! &quot;Quirks&quot; are more likely bugs waiting to be resolved, but commands like <code>MOUNT</code> and <code>VER</code> seem to have the ability to poke through to the outside world, so maybe there's an extra function somewhere?
</p><h2>Easy Mode: The Correct Way
</h2><p>Okay, I know you're screaming it at your screen: the simplest way <em>is</em> to just get the string at <code>FE00:0061</code>&mdash;which everybody knows is the common Award BIOS version string address<sup>[<a href="#fn:awbios" id="fnref:awbios">2</a>]</sup>&mdash;and check if it starts with <code>DOSBox</code>. But that's so brittle, y'know? I could just modify a non-DOSBox BIOS to have that version string, or modify DOSBox to have the model string be something else. There's even a comment in <a href="https://dosbox-x.com/">DOSBox-X</a> (<a href="https://github.com/joncampbell123/dosbox-x/blob/938f05cb28ee780423a298fcb32f587a0be5478c/src/ints/bios.cpp#L12606-L12609">source</a>) that alludes to this being a desirable change in the future:
</p><pre><code class="language-c">    /* TODO: *DO* allow dynamic relocation however if the dosbox-x.conf indicates that the user
     *       is not interested in IBM BIOS compatibility. Also, it would be really cool if
     *       dosbox-x.conf could override these strings and the user could enter custom BIOS
     *       version and ID strings. Heh heh heh.. :) */</code></pre><p>So of course I can't take this route! There are other easier ways, like checking the serial number of the Z: drive (or if it exists, for that matter). But these can all be faked pretty easily. No, we must find something that's an inherent part of the emulator. Something that proves this is DOSBox.
</p><h2>Inventing Instructions
</h2><p>Let's go back to how DOSBox can talk to the outside world with commands like <code>MOUNT.COM</code>. COM files are just machine code, meaning we can run it directly through a disassembler. So let's do that, with a copy of MOUNT.COM from DOSBox:
</p><pre><code>$ ndisasm MOUNT.COM
00000000  BC0004            mov sp,0x400
00000003  BB4000            mov bx,0x40
00000006  B44A              mov ah,0x4a
00000008  CD21              int byte 0x21
0000000A  FE                db 0xfe
0000000B  3805              cmp [di],al
0000000D  00B8004C          add [bx+si+0x4c00],bh
00000011  CD21              int byte 0x21
00000013  02                db 0x02</code></pre><p>The first four lines make sense: <code>INT 21h Function 4Ah</code> shrinks the stack to 0x40 paragraphs (128 bytes). But the next couple lines are... basically garbage. <code>db 0xfe</code> just means &quot;there's a byte here, <code>0xfe</code>&quot;, and your typical x86 CPU would balk at this and throw an Invalid Instruction exception.
</p><p>But when you're writing an x86 CPU, you can just invent your own instructions! Lo and behold, in the DOSBox sources:
</p><pre><code class="language-c">/* Snippet from src/cpu/core_normal/prefix_none.h */
CASE_B(0xfe)               /* GRP4 Eb */
    {
&#9;    GetRM;Bitu which=(rm&gt;&gt;3)&amp;7;
&#9;    switch (which) {
&#9;&#9;&#9;case 0x00:     /* INC Eb */
&#9;&#9;&#9;    RMEb(INCB);
&#9;&#9;&#9;    break;
&#9;&#9;&#9;case 0x01:     /* DEC Eb */
&#9;&#9;&#9;    RMEb(DECB);
&#9;&#9;&#9;    break;
&#9;&#9;&#9;case 0x07:     /* CallBack */
&#9;&#9;&#9;    {
&#9;&#9;&#9;        Bitu cb=Fetchw();
&#9;&#9;&#9;        FillFlags();SAVEIP;
&#9;&#9;&#9;        return cb;
&#9;&#9;&#9;    }
&#9;&#9;&#9;default:
&#9;&#9;&#9;&#9;E_Exit(&quot;Illegal GRP4 Call %d&quot;,(rm&gt;&gt;3) &amp; 7);
&#9;&#9;&#9;&#9;break;
&#9;    }
&#9;    break;
    }</code></pre><p>This is the code for decoding the <code>FE</code> group of opcodes. <code>0x00</code> is INC and <code>0x01</code> is DEC, both real opcodes on x86.
</p><p>But that last one, <code>0x07</code>, <em>that</em> is a DOSBox exclusive. The word after the opcode is used to say which callback should be called...back, and breaks out. So, to fix up the disassembly from earlier, it might look like this:
</p><pre><code>00000000  BC0004            mov sp,0x400
00000003  BB4000            mov bx,0x40
00000006  B44A              mov ah,0x4a
00000008  CD21              int byte 0x21
0000000A  FE380500          CallBack 0x0005
0000000E  B8004C            mov ax,0x4c00
00000011  CD21              int byte 0x21</code></pre><p><p></p>
</p><div class="aside">
<h3>Aside: Tripping and falling into the weeds of x86 Instruction Encoding
</h3><p>In the first draft of this, I wrote:
</p><blockquote><p> I'll try not to trip and fall into the weeds of x86 instruction coding [...]
</p></blockquote><p>But the way the callback opcode works is directly because of how x86 opcodes work. And I don't feel like it's fair to expect anyone to know how x86 instructions are encoded. I want my ramblings to be at least <em>somewhat</em> accessible, even if I've already thrown assembly code at you in the first half.
</p><p>If you already know or don't care how this works, feel free to skip this. If you're really curious, my primary source here is Volume 2 of the <em>Intel 64 and IA-32 Architectures Software Developer's Manual</em>, found <a href="https://cdrdv2.intel.com/v1/dl/getContent/671110">here</a>. I'll cite chapters in parentheses through the rest of this section.
</p><p>So. Machine code is split up into quite a few parts, with the opcode itself only being one-and-a-half. (2.1) For the sake of conciseness, we'll ignore everything but the opcode, ModR/M, and Immediate bytes, since that's what we're using here.
</p><p>Let's take a snippet of that earlier disassembly:
</p><pre><code>0000000A  FE380500          CallBack 0x0005
0000000E  B8004C            mov ax,0x4c00</code></pre><p>and turn it into hex:
</p><pre><code>FE 38 05 00    00 B8 00 4C</code></pre><p>Without a prefix of <code>0F</code>, we know the opcode is just <code>FE</code>. (A.3, Table A-2) But this is a group, &quot;INC/DEC Grp 4,&quot; which uses the Opcode bits of the next byte, the ModR/M byte, to actually determine the opcode. That byte is split up like this:
</p><pre><code>Byte:   00 111 000  (0x38)
Mod:    00          (0x00)
Opcode:    111      (0x07)
R/M:           000  (0x00)</code></pre><p>For our purposes, only the Opcode field matters. So this can be read as <code>FE /7</code>. According to the Opcode Extensions table, (A.4.2, Table A-6) this doesn't actually exist. Only <code>FE /0</code> and <code>FE /1</code> exist in this group. But we know DOSBox supports a secret <code>FE /7</code>, so we'll have to rely on its source code to know what to do next. And it does this:
</p><pre><code class="language-c">Bitu cb=Fetchw();
FillFlags();SAVEIP;
return cb;</code></pre><p>Importantly, <code>Fetchw()</code> fetches the next word and returns it (effectively, telling the machine &quot;call this callback&quot;). Since x86 is little-endian, <code>05 00</code> becomes <code>00 05</code>.
</p><p>Once the callback is complete, the next instruction is called. That'll be <code>B8 00 4C</code>. <code>B8</code> is <code>MOV AX,XXXX</code>. This instruction takes a 16-bit immediate, which is the <code>00 4C</code> value (<code>4c00</code> in little-endian). And so on and so forth.
</p>
</div>
<p>Anyway, here it is in the part of the code that generates virtual programs like MOUNT.COM:
</p><pre><code class="language-c">/* Snippet from src/misc/programs.cpp */
static Bit8u exe_block[]={
    0xbc,0x00,0x04,                 //MOV SP,0x400 decrease stack size
    0xbb,0x40,0x00,                 //MOV BX,0x040 for memory resize
    0xb4,0x4a,                      //MOV AH,0x4A   Resize memory block
    0xcd,0x21,                      //INT 0x21
//pos 12 is callback number
    0xFE,0x38,0x00,0x00,            //CALLBack number
    0xb8,0x00,0x4c,                 //Mov ax,4c00
    0xcd,0x21,                      //INT 0x21
};</code></pre><p>Conveniently, since callbacks are returned the same way as the general status, <code>FE 38 00 00</code> is effectively a four-byte NOP! On DOSBox, anyway.
</p><p>Other x86 CPUs won't have such fortune. Since the 80186, invalid instructions trigger a <code>#UD</code> (Undefined Opcode) exception, or Interrupt 06h. So we just need to write an exception handler. Something like this:
</p><pre><code class="language-x86">_catchUD:
&#9;; Current IP is at the top of the stack, so +4 after we push ax/bx
&#9;push bx
&#9;push ax
&#9;
&#9;mov bx, sp
&#9;mov bx, WORD [ss:bx+4]
&#9;mov ax, bx
&#9;
&#9;mov bx, WORD [cs:bx] ; will copy little-endian (i.e. 0x38fe)
&#9;and bh, 38h
&#9;cmp bx, 38feh
&#9;je .notDosbox
&#9;
&#9;; if we end up here, something went really wrong! clean up the IVT
&#9;; and IRET so the actual #UD handler is called.
&#9;; Since we don't modify the IP, it'll re-run the invalid opcode.
&#9;push es
&#9;xor ax, ax
&#9;mov es, ax
&#9;mov bx, [oldUDAddr] ; previous int 06h addr
&#9;mov [es:18h], bx ; 06h*4
&#9;mov bx, [oldUDSeg] ; previous int 06h segment
&#9;mov [es:20h], bx ; (06h*4)+2
&#9;pop es
&#9;pop ax
&#9;jmp .catchDone
&#9;
&#9;.notDosbox:
&#9;; Not DOSBox -- increment the IP and zero AX
&#9;; You can of course do whatever here, like setting a global
&#9;add ax, 4
&#9;mov bx, sp
&#9;mov WORD [ss:bx+4], ax
&#9;xor ax, ax
&#9;add sp, 2 ; AX unneeded
&#9;
&#9;.catchDone:
&#9;pop bx
&#9;iret</code></pre><p>which, once set up, could be tested like this:
</p><pre><code class="language-asm">&#9;mov ax, 42
&#9;db 0xfe, 0x38, 0x00, 0x00
&#9;; was the exception handler here?
&#9;cmp ax, 0
&#9;jz .notDosbox ; Not DOSBox!
&#9;; DOSBox-only code starts here!
&#9;.notDosbox:
&#9;; Non-DOSBox code starts here!</code></pre><p>Add in some extra instructions to reset the interrupt 06h vector once done, and we should have a pretty good check for DOSBox!
</p><h2>DEBUGging x86
</h2><p>At this point in writing, I decided it'd be a good time to test this on hardware. But my Pentium II systems are currently a bit buried, and it'd be hard to get good screenshots of them anyway... so I figured I'd use 86Box.
</p><p>This did not go according to plan:
</p><p><img alt="Screenshot of a DOS program saying, &quot;Yep, that is a DOSBox!&quot;" class="as-post" src="/assets/post-img/dosbox/dbt2_dos_fail.png">
</p><p>Importantly, this is not DOSBox. But that's okay, because we can just step through it with the <code>DEBUG</code> program in MS-DOS and see what's going wrong. It's not the most, er, friendly program, but it's enough to get the job done in a case like this.
</p><p>There's a command, <code>t</code>, which lets you step through the code one instruction at a time. (<a href="https://thestarman.pcministry.com/asm/debug/debug.htm#BBUG">Well, mostly</a>.) So we'll step through to the callback instruction, and we can see here DEBUG has no idea what's going on, even if it encodes it correctly enough... but then steps through it as though it's valid!?
</p><p><img alt="Screenshot of DOS DEBUG.COM" class="as-post" src="/assets/post-img/dosbox/dbt2_debug_wat.png">
</p><p>At this point, I was entirely confused. Is this some secret undocumented instruction? Does 86Box ignore invalid instructions for some reason? Do invalid instruction exceptions not work how I thought they do? Could a DOS driver somehow mask the interrupt?
</p><p>I'll save you the days of troubleshooting I spent on this: 86Box inherited a bug from PCem where any ModR/M opcode modifier other than 0 was treated as <code>FE /1</code>.<sup>[<a href="#fn:bug" id="fnref:bug">3</a>]</sup> So <code>FE /2</code>, <code>FE /4</code>, and <code>FE /7</code> all acted as DEC calls. Thankfully the fix was pretty simple, and <a href="https://github.com/86Box/86Box/pull/6561">it's already been merged upstream</a>.
</p><p>As mentioned in the PR, special thanks to <a href="https://linear.network/">linear</a> for testing this on actual hardware so we can be (at least somewhat) sure this isn't just an Intel documentation issue.
</p><h2>The Finished Product(?)
</h2><p>If you want to run the sample program I wrote for this, you can get it on my Git forge <a href="https://git.2ki.xyz/snow/dostests/src/branch/trunk/dosbox.asm">here</a>. You'll need <a href="https://www.nasm.us/">NASM</a> to compile it. It'll run fine on DOSBox and DOSBox-X, at least.
</p><p>While this was a fun project on its own, my intent wasn't just to detect DOSBox. It just happened to be the trickiest to figure out. NTVDM and the Win9x MS-DOS Prompt are easier to detect, basically just a single <code>INT 2Fh</code> call. There's another DOS emulator for linux, aptly named DOSEMU, which has... a surprising amount of callback APIs. They're all implemented as COM files (e.g. <code>UNIX.COM</code> lets you run arbitrary commands on the host system), so it's not like they're hidden features. Of course, none of these are quite as hard to spoof as a custom CPU instruction, but they're more liable to cause side effects than changing a BIOS string would.
</p><div id="footnotes"><hr><ol><li id="fn:aard"><a href="https://en.wikipedia.org/wiki/AARD_code">Just ask Microsoft!</a> <a href="#fnref:aard"><img alt="(back)" class="backbtn" src="/assets/img/return.gif"></a></li><li id="fn:awbios">Okay but actually, I struggled to find any definitive information on whether this originated with Award BIOS, or even official documentation on it. If you have any authoritative documentation on this that I've missed, please feel free to let me know! <a href="#fnref:awbios"><img alt="(back)" class="backbtn" src="/assets/img/return.gif"></a></li><li id="fn:bug">And to be clear, I don't blame the developers of either project for this bug sticking around this long. It's such a niche use case, I'd be surprised if anybody was doing this sort of thing! <a href="#fnref:bug"><img alt="(back)" class="backbtn" src="/assets/img/return.gif"></a></li></ol></div>]]></description>
            </item>
            <item>
                <title>Redesigned the site, again!</title>
                <pubDate>Mon, 30 Sep 2024 00:09:06 -0700</pubDate>
                <guid isPermaLink="false">urn:uuid:4db68de2-837b-11ef-be3b-c28dba6bae4f</guid>
                <description><![CDATA[Now with light and dark modes! Don't worry, it still works in Netscape 7.2. I'll probably write about some of the more fun learnings in a bit, but I wanted to get this out for the Cohost read-onlyening.]]></description>
            </item>
            <item>
                <title>So you want to write a KMail plugin?</title>
                <pubDate>Sun, 16 Jun 2024 19:27:47 -0700</pubDate>
                <guid isPermaLink="false">urn:uuid:4dc9e888-837b-11ef-a2b2-9711e46e4843</guid>
                <link>https://datagirl.xyz/posts/kontact_plugin_writing.html</link>
                <description><![CDATA[<p>Recently, I've been moving away from macOS to Linux, and have settled on using KDE Plasma as my desktop environment. For the most part I've been comfortable with the change, but it's always the small things that get me. For example, the Mail app built into macOS provides an &quot;Unsubscribe&quot; button for emails. It looks like this:
</p><p><img alt="Banner saying &quot;This message is from a mailing list.&quot; A button next to the text says &quot;Unsubscribe.&quot;" class="as-post" src="/assets/post-img/kontact/macos_unsub.png">
</p><p>You click &quot;Unsubscribe,&quot; and you're unsubscribed. No pleading taco emoji, no line of check boxes, just a plainly-labeled button at the top of the email.
</p><p>Apparently this is also supported in some webmail clients, but I'm not interested in accessing my email that way.<sup>[<a href="#fn:webmail" id="fnref:webmail">1</a>]</sup> Unfortunately, I haven't found an X11 or Wayland email client that supports this sort of functionality, so I decided to implement it myself. And anyway, I'm trying out Kontact for my mail at the moment, which supports plugins. So why not use this as an opportunity to build one?
</p><h2>What <em>is</em> Kontact?
</h2><p><a href="https://kontact.kde.org/">Kontact</a> is an all-in-one Personal Information Manager (PIM) developed for KDE. The actual &quot;Kontact&quot; program is more like a fancy frontend: Email is handled via <a href="https://apps.kde.org/kmail2/">KMail</a>, <a href="https://apps.kde.org/korganizer/">KOrganizer</a> provides to-do lists and calendars, and <a href="https://apps.kde.org/kaddressbook/">KAddressBook</a>... well, these names aren't the most original. All of these programs can run independently, or be embedded into Kontact as <a href="https://api.kde.org/frameworks/kparts/html/index.html"><code>KPart</code>s</a> and using the <a href="https://api.kde.org/kdepim/kontactinterface/html/classKontactInterface_1_1Plugin.html"><code>KontactInterface::Plugin</code></a> interface. These are called &quot;Kontact plugins,&quot; and are not what we want to build.
</p><p>However, there is another set of plugins. If you go to &quot;Configure KMail...&quot;, you can see them:
</p><p><img alt="&quot;Configure KMail&quot; dialog, on the Plugins tab. A list of various plugins are displayed." class="as-post" src="/assets/post-img/kontact/config_kmail.png">
</p><p>These are split up into different categories: Akonadi Agents, Composer Plugins, Editor Plugins, and a category named &quot;Message Viewer.&quot; Since we'd need to act on the message viewer, we'll want to write one of those. But there's no &quot;install plugin&quot; button. How do we start building one? If it's not a Kontact plugin, is it an Akonadi plugin? Or a secret third thing?
</p><h2>Akonadi and Messagelib
</h2><p><a href="https://userbase.kde.org/Akonadi">To quote KDE</a>:
</p><blockquote><p>The <strong>Akonadi</strong> framework is responsible for providing applications with a centralized database to store, index and retrieve the user's personal information. This includes the user's emails, contacts, calendars, events, journals, alarms, notes, etc.
</p></blockquote><p>In essence, this is the backend of Kontact (Akregator excluded). So we won't be building a plugin for Akonadi, but we will be interacting with it shortly.
</p><p>The plugins we see in the Configure window come from the <a href="https://invent.kde.org/pim/kdepim-addons/">PIM/kdepim-addons</a> repository. We'll use the &quot;Create Event&quot; plugin as our reference, and we can find it in <a href="https://invent.kde.org/pim/kdepim-addons/-/tree/bbbcb977647185722a322162b3c1c89fdbbd125f/plugins/messageviewerplugins/createeventplugin">plugins/messageviewerplugins/createeventplugin</a>. From there, we can find the interface being used:
</p><pre><code class="language-cpp">namespace MessageViewer
{
class ViewerPluginCreateevent : public MessageViewer::ViewerPlugin
{
    Q_OBJECT
public:
    explicit ViewerPluginCreateevent(QObject *parent = nullptr, const QList&lt;QVariant&gt; &amp; = QList&lt;QVariant&gt;());
    ViewerPluginInterface *createView(QWidget *parent, KActionCollection *ac) override;
    [[nodiscard]] QString viewerPluginName() const override;
};
}</code></pre><p><code>MessageViewer</code> isn't a direct part of KMail or Kontact. It's part of <a href="https://invent.kde.org/pim/messagelib/">Messagelib</a>, which contains various widgets used for displaying and composing emails. These widgets are then used by KMail.
</p><p>Okay great, we finally have the interface and library we need to start building a plugin! Now to pull up the docs for <a href="https://api.kde.org/kdepim/messagelib/html/classMessageViewer_1_1ViewerPlugin.html"><code>MessageViewer::ViewerPlugin</code></a>, and
</p><p><img alt="A screenshot of API documentation. Method and class names are listed, but no context or descriptions are given." class="as-post" src="/assets/post-img/kontact/crickets.png">
</p><p>oh
</p><h2>Working Backwards
</h2><p>Normally, I'd start by building code and set up the build system as I go. At least for smaller projects, it can be easier to hit the ground running. But since we have no idea what we're working with here, I'm going to set up the bare minimum to get a working plugin.
</p><p>Due to the way KDE Frameworks are, we're going to want to use CMake for this. The good news is we don't have to think <em>too</em> hard about it, thanks to <a href="https://api.kde.org/frameworks/kcoreaddons/html/index.html">KCoreAddons</a> and KDE's library of CMake Modules, known as, uh, <a href="https://api.kde.org/frameworks/extra-cmake-modules/html/index.html">Extra CMake Modules</a>.
</p><pre><code class="language-cmake"># I've been testing with KF6, but maybe this will work with KF5
set(KF_MIN_VERSION &quot;6.0.0&quot;)

# Extra CMake Modules (ECM) setup
find_package(ECM ${KF_MIN_VERSION} CONFIG REQUIRED)
set(CMAKE_MODULE_PATH ${ECM_MODULE_DIR} ${ECM_KDE_MODULE_DIR})
include(ECMQtDeclareLoggingCategory)
include(KDEInstallDirs)
include(KDECMakeSettings)

set(kmail_unsubscribe_SRCS
&#9;# C++ sources go here...
&#9;unsubscribeplugin.cpp
&#9;unsubscribeplugin.h
)

ecm_qt_declare_logging_category(
&#9;kmail_unsubscribe_SRCS
&#9;# What the header will be
&#9;HEADER &quot;unsubscribe_debug.h&quot;
&#9;# The object name
&#9;IDENTIFIER &quot;UnsubscribePlugin&quot;
&#9;# How to refer to it externally, e.g. by
&#9;# QT_LOGGING_RULES
&#9;CATEGORY_NAME &quot;xyz.datagirl.kpim.unsubscribe&quot;
&#9;DESCRIPTION &quot;Unsubscribe Plugin&quot;
&#9;DEFAULT_SEVERITY Info
&#9;EXPORT
)

# Build a .so, not .a
set(BUILD_SHARED_LIBS ON)

# Let the KCoreAddons macro make our target
kcoreaddons_add_plugin(kmail_unsubscribe
&#9;SOURCES ${kmail_unsubscribe_SRCS}
&#9;# Used when installed
&#9;INSTALL_NAMESPACE pim6/messageviewer/viewerplugin)
# Now we can link libraries, set properties, etc
target_link_libraries(kmail_unsubscribe
&#9;KPim6::PimCommon
&#9;#...
)</code></pre><p>Now we have to define the metadata in a JSON file. In my case, it's <code>kmail_unsubscribeplugin.json</code>:
</p><pre><code class="language-json">{
&#9;&quot;KPlugin&quot;: {
&#9;&#9;&quot;Description&quot;: &quot;Adds an Unsubscribe button to messages&quot;,
&#9;&#9;&quot;EnabledByDefault&quot;: true,
&#9;&#9;&quot;Name&quot;: &quot;Unsubscribe&quot;,
&#9;&#9;&quot;Version&quot;: &quot;2.0&quot;
&#9;}
}</code></pre><p>Then, integrate it with your plugin class. For example, I have in <code>unsubscribeplugin.cpp</code>:
</p><pre><code class="language-cpp">// UnsubscribePlugin is defined in this header
#include &quot;unsubscribeplugin.h&quot;

#include &lt;KPluginFactory&gt;

using namespace MessageViewer;
K_PLUGIN_CLASS_WITH_JSON(UnsubscribePlugin, &quot;kmail_unsubscribeplugin.json&quot;)</code></pre><p>Finally, we're getting to code! First, we'll build out the scaffolding for <code>UnsubscribePlugin</code> in the header:
</p><pre><code class="language-cpp">// Requires KPim6MessageViewer
#include &lt;MessageViewer/ViewerPlugin&gt;
#include &lt;QVariant&gt;

namespace MessageViewer {
&#9;class UnsubscribePlugin
&#9;&#9;: public MessageViewer::ViewerPlugin
&#9;{
&#9;&#9;Q_OBJECT
&#9;public:
&#9;&#9;explicit UnsubscribePlugin(QObject *parent = nullptr,
&#9;&#9;&#9;const QList&lt;QVariant&gt; &amp; = QList&lt;QVariant&gt;());

&#9;&#9;[[nodiscard]] ViewerPluginInterface *createView(QWidget *parent,
&#9;&#9;&#9;KActionCollection *ac) override;

&#9;&#9;[[nodiscard]] QString viewerPluginName() const override
&#9;&#9;{
&#9;&#9;&#9;return QStringLiteral(&quot;oneclick-unsubscribe&quot;);
&#9;&#9;};
&#9;};
}</code></pre><p>And in the source file:
</p><pre><code class="language-cpp">// Pass up parent, ignore QList
UnsubscribePlugin::UnsubscribePlugin(
&#9;QObject *parent,
&#9;const QList&lt;QVariant&gt; &amp;
) : MessageViewer::ViewerPlugin(parent)
{
}

// Create a new plugin interface when asked
ViewerPluginInterface *
UnsubscribePlugin::createView(
&#9;QWidget *parent,
&#9;KActionCollection &amp;ac
)
{
&#9;MessageViewer::ViewerPluginInterface *view =
&#9;&#9;new UnsubscribePluginInterface(ac, parent);
&#9;return view;
}

// Meta Object Code (MOC) with the plugin definition
#include &quot;unsubscribeplugin.moc&quot;
// MOC for the CPP file
#include &quot;moc_unsubscribeplugin.cpp&quot;</code></pre><h3>The Plugin Interface
</h3><p>Up until this point, we've been just getting the plugin structure setup. The real core of the plugin is the plugin interface class, defined in our case by <a href="https://api.kde.org/kdepim/messagelib/html/classMessageViewer_1_1ViewerPluginInterface.html"><code>ViewerPluginInterface</code></a>. While its API documentation is lacking, it'll help us define our subclass:
</p><pre><code class="language-cpp">#include &lt;MessageViewer/ViewerPluginInterface&gt;

namespace MessageViewer
{
&#9;class UnsubscribePluginInterface
&#9;&#9;: public MessageViewer::ViewerPluginInterface
&#9;{
&#9;&#9;Q_OBJECT
&#9;public:
&#9;&#9;explicit UnsubscribePluginInterface(
&#9;&#9;&#9;QWidgetParent *parent = nullptr,
&#9;&#9;&#9;KActionCollection *ac);
&#9;&#9;~UnsubscribePluginInterface() override;

&#9;&#9;// In our case, we'll be returning mActions (see below)
&#9;&#9;[[nodiscard]] QList&lt;QAction *&gt; actions() const override;

&#9;&#9;// We'll get to these four shortly
&#9;&#9;void updateAction(const Akonadi::Item &amp;item) override;
&#9;&#9;void setMessageItem(const Akonadi::Item &amp;item) override;
&#9;&#9;void execute() override;
&#9;&#9;void closePlugin() override;

&#9;&#9;// This defines what your plugin supports. More on that
&#9;&#9;// in a moment.
&#9;&#9;[[nodiscard]] ViewerPluginInterface::SpecificFeatureTypes
&#9;&#9;&#9;featureTypes() const override
&#9;&#9;{
&#9;&#9;&#9;return ViewerPluginInterface::NeedMessage;
&#9;&#9;}
&#9;private:
&#9;&#9;// What we'll be returning in actions()
&#9;&#9;QList&lt;QAction *&gt; mActions;
&#9;};
}</code></pre><p>For those who looked at the API docs, you might have noticed I'm skipping quite a few methods. The parent class, <code>ViewerPluginInterface</code>, defines all those methods as no-ops for us, so we don't need to implement what we don't care about.
</p><p>The <code>featureTypes()</code> method defines the places it makes sense for your plugin to appear. While I haven't been able to figure out the specifics, the general idea seems to be:
</p><ul><li>Define <code>ViewerPluginInterface::NeedMessage</code> when you're working with the whole message (ex., toolbar items on the message view)
</li><li>Define <code>ViewerPluginInterface::NeedText</code> when you'll be dealing with the currently-selected text in a message
</li><li>Define <code>ViewerPluginInterface::NeedUrl</code> when you'll be interfacing with a clicked URL
</li></ul><p>These can be combined with bitwise OR, if you need multiple. You can also just define <code>ViewerPluginInterface::None</code> if you have no need for any of those.
</p><p>Okay, enough with the class definitions. Let's put together the constructor:
</p><pre><code class="language-cpp">using namespace MessageViewer;

UnsubscribePluginInterface::UnsubscribePluginInterface(
&#9;QWidget *parent,
&#9;KActionCollection *ac
) : ViewerPluginInterface(parent)
{
&#9;if (ac)
&#9;{
&#9;&#9;// Create an action...
&#9;&#9;auto action = new QAction(this);
&#9;&#9;action-&gt;setIcon(QIcon::fromTheme(QStringLiteral(&quot;news-unsubscribe&quot;)));
&#9;&#9;action-&gt;setIconText(QStringLiteral(&quot;Unsubscribe&quot;));
&#9;&#9;action-&gt;setWhatsThis(QStringLiteral(&quot;Unsubscribe from the mailing list&quot;));
&#9;&#9;// ... and add it to the collection
&#9;&#9;ac-&gt;addAction(QStringLiteral(&quot;oneclick_unsubscribe&quot;), action);
&#9;&#9;// Lastly, connect the triggered signal to a slot defined
&#9;&#9;// upstream
&#9;&#9;connect(
&#9;&#9;&#9;action,
&#9;&#9;&#9;&amp;QAction::triggered,
&#9;&#9;&#9;this,
&#9;&#9;&#9;&amp;UnsubscribePluginInterface::slotActivatePlugin
&#9;&#9;);
&#9;&#9;// Finally, add to our internal actions list
&#9;&#9;mActions.append(action);
&#9;}
}</code></pre><p>This part is pretty straightforward: create a <a href="https://doc.qt.io/qt-6/qaction.html"><code>QAction</code></a>, add it to the application's collection, and hook up the signal that occurs when the action is activated (e.g. when clicked, or a relevant keyboard shortcut is pressed).
</p><p>That should be the last of our scaffolding. Finally, onto the interesting part!
</p><h3>The Interesting Part
</h3><p>Our plugin's journey truly starts when the user selects an email. When this happens, the Message Viewer calls our plugin interface's <code>updateAction(Akonadi::Item &amp;item)</code> method.
</p><p>While <a href="https://api.kde.org/kdepim/akonadi/html/classAkonadi_1_1Item.html"><code>Akonadi::Item</code></a>s are general objects that can contain a number of things, we'll only receive ones with a shared pointer to a <a href="https://api.kde.org/kdepim/kmime/html/classKMime_1_1Message.html"><code>KMime::Message</code></a>. Switching folders appears to cause an <code>updateAction</code> call with an <code>Akonadi::Item</code> that has a null payload, so you may want to guard against that:
</p><pre><code class="language-cpp">if (item.hasPayload&lt;KMime::Message::Ptr&gt;())
{
&#9;mMessage = item.payload&lt;KMime::Message::Ptr&gt;();
&#9;if (mMessage == nullptr)
&#9;{
&#9;&#9;// Couldn't get the KMime message, bail
&#9;&#9;return;&#9;
&#9;}

&#9;// Now we can work with mMessage!
}</code></pre><p>As might be evident by the method name, <code>updateAction</code> is to update our action for the email we're on. In the case of our unsubscribe plugin, we may want to disable the action if we determine the user can't automatically unsubscribe from this email.
</p><p>Once our action is triggered, the following bit of code <a href="https://invent.kde.org/pim/messagelib/-/blob/c9f097a4c6fe3b0e90344d4fa55448f5c0467fe7/messageviewer/src/viewer/viewer_p.cpp#L2465-2475">in Messagelib</a> is run:
</p><pre><code class="language-cpp">void ViewerPrivate::slotActivatePlugin(ViewerPluginInterface *interface)
{
    interface-&gt;setMessage(mMessage);
    interface-&gt;setMessageItem(mMessageItem);
    interface-&gt;setUrl(mClickedUrl);
    interface-&gt;setCurrentCollection(mMessageItem.parentCollection());
    const QString text = mViewer-&gt;selectedText();
    if (!text.isEmpty()) {
        interface-&gt;setText(text);
    }
    interface-&gt;execute();
}</code></pre><p>We won't need to implement all of these methods for our plugin, but since we're here anyway, here's my understanding of the process:
</p><ol><li><code>setMessage(mMessage)</code> provides us with the actual message pointer.
</li><li><code>setMessageItem(mMessageItem)</code> gives us the <code>Akonadi::Item</code> corresponding to the message.
</li><li>If a URL is selected, <code>setUrl(mClickedUrl)</code> would give us the URL that was clicked.
</li><li><code>setCurrentCollection(...)</code> gives us the parent <a href="https://api.kde.org/kdepim/akonadi/html/classAkonadi_1_1Collection.html"><code>Akonadi::Collection</code></a> for our given message. This would most often be the email's folder.
</li><li>If there's selected text, <code>setText(text)</code> passes that to us.
</li><li>Finally, <code>execute()</code> is our cue to start doing our thing!
</li></ol><p>There's one last important method: <code>closePlugin()</code>. This is called when the current email is closed, to tell plugins to flush the current state. This would of course include closing the window, but more importantly includes every time the user changes the active email. In such a case, <code>updateAction</code> is called almost immediately, and the cycle repeats.
</p><div class="aside">
<h2>Aside: One-Click Unsubscribe
</h2><p>Feels a bit silly to call this an aside, but the point of this post is to share my findings about this plugin API, not the thing I wanted to implement. <em>anyway</em>
</p><p>One-Click Unsubscribe is an actual standard, <a href="https://datatracker.ietf.org/doc/html/rfc8058">RFC 8058</a>. As far as we're concerned, the mail sender's requirements boil down to:
</p><ul><li>The email MUST have a <code>List-Unsubscribe</code> header with only one HTTPS URI (any number of other URIs is fine)
</li><li>The email MUST also have a <code>List-Unsubscribe-Post</code> header, which MUST have the value of <code>List-Unsubscribe=One-Click</code>
</li><li>The email MUST be signed with DKIM, and the <code>List-Unsubscribe</code> and <code>List-Unsubscribe-Post</code> headers MUST be in the list of signed headers
</li></ul><p>For our end as the receiver, we just have to send a POST request to the URI given by the headers, with a form field named <code>List-Unsubscribe</code> with the value <code>One-Click</code>. Easy!
</p><h3>Bonus Aside: DKIM
</h3><p>Okay, not so easy. That last bullet point mentions the requirement for DomainKeys Identified Mail (DKIM), a standard for authenticating email by cryptographically signing a defined set of headers and the body of an email. KMail supports DKIM, so we should be able to use whatever it uses, right?
</p><p>Mostly! There's <a href="https://api.kde.org/kdepim/messagelib/html/classMessageViewer_1_1DKIMManager.html"><code>MessageViewer::DKIMManager</code></a> which is used to check DKIM signatures. It also provides a singleton-like <code>::self()</code> method, so one might think we could just do something like:
</p><pre><code class="language-cpp">connect(DKIMManager::self(),
&#9;   &amp;DKIMManager::result,
&#9;   this,
&#9;   &amp;SomePluginInterface::slotDkimResult);
DKIMManager::self()-&gt;checkDKim(mMessageItem);</code></pre><p>Which <em>does</em> work, but we're not the only one connected to this instance. Doing this leads to an amusing case where, when <code>checkDKim()</code> is run, this little status bar item:
</p><p><img alt="DKIM: valid (signed by kde.org)" class="as-post" src="/assets/post-img/kontact/dkim_ok.png">
</p><p>Immediately changes to:
</p><p><img alt="DKIM: invalid" class="as-post" src="/assets/post-img/kontact/dkim_bad.png">
</p><p>This is because our message item might not contain the body, and since the body is also signed, the check fails. My solution is to just build my own <code>DKIMManager</code> instance. This is probably more &quot;correct,&quot; but I'm curious what the use case for using <code>DKIMManager::self()</code> would be.
</p>
</div>
<h2>And all the Rest
</h2><p>And now you have at least a vague idea of how to build a plugin for a specific component of a specific part of Kontact!
</p><p>&quot;But what if I want to build a really cool plugin for (email composing/KAddressBook/Akonadi)?&quot; I hear you ask. Well, dear reader, <em>I have no idea</em>. It looks like most other plugins are derived from <a href="https://api.kde.org/kdepim/pimcommon/html/classPimCommon_1_1GenericPluginInterface.html"><code>PimCommon::GenericPluginInterface</code></a>, which is independent of <code>MessageViewer::ViewerPluginInterface</code>.
</p><p>My recommendation would be to look at existing plugins that do something remotely adjacent to what you want to do and look at the interfaces it implements. All of the built-in plugins come from <a href="https://invent.kde.org/pim/kdepim-addons/">PIM/kdepim-addons</a>, so there's a good chance the repository is in there.
</p><p>If the <a href="https://api.kde.org">KDE API docs</a> are failing you and web searches don't pull anything up, you'll want to reverse engineer those interfaces. Try to make a barebones plugin derived from your &quot;sample,&quot; using <code>ecm_qt_declare_logging_category</code> to create a new logging category and a bunch of <code>qCDebug(YourCategory)</code> calls to trace the behavior of those methods. If all else fails, search the KDE PIM libraries' source code for where the interface would call your plugin!
</p><p>yes this was the most efficient way to get the functionality i wanted, why do you ask :)
</p><p>If you're interested in the project that kickstarted all of this, I have it available both <a href="https://git.2ki.xyz/snow/kmail_unsubscribe">on my Git server</a> and <a href="https://github.com/snowkat/kmail_unsubscribe">on GitHub</a>.
</p><div id="footnotes"><hr><ol><li id="fn:webmail">I'm very particular about what goes on in my web browser! I've got 50 tabs of the same search result page, five videos, and a hundred or so manual pages for projects I've since abandoned. The important stuff! <a href="#fnref:webmail"><img alt="(back)" class="backbtn" src="/assets/img/return.gif"></a></li></ol></div>]]></description>
            </item>
            <item>
                <title>Running WolfSSL and cURL on Windows 2000</title>
                <pubDate>Sun, 03 Mar 2024 14:04:43 -0800</pubDate>
                <guid isPermaLink="false">urn:uuid:4e1f61c8-837b-11ef-977f-b1ad637eab16</guid>
                <link>https://datagirl.xyz/posts/wolfssl_curl_w2k.html</link>
                <description><![CDATA[<p>As part of my current series of projects, I've been working on a screenshot tool intended for older Windows versions. The goal is to have something similar to <a href="https://getsharex.com/">ShareX</a>, a tool that lets you take a screenshot and post it online.
</p><p>These days, file sharing is done via the Hypertext Transfer Protocol (HTTP)<sup>[<a href="#fn:http" id="fnref:http">1</a>]</sup>, often using Transport Layer Security (TLS).<sup>[<a href="#fn:tls" id="fnref:tls">2</a>]</sup> Built into every version of Windows since Windows 95 is WinINet, which provides HTTP, FTP, and (in XP/2003 and earlier) Gopher. Obviously for older Windows versions, this library won't include support for recent technologies like TLS 1.3 or HTTP/2. cURL still <a href="https://github.com/curl/curl/tree/master/winbuild#readme">supports Visual C++ 6</a>, but that only provides the HTTP/FTP part. We'll need a TLS library to get connected to most modern systems.
</p><div class="aside caution"><p>Note: <strong>All patches and documentation here are experimental, and should not be considered secure.</strong> My focus was on running something compliant with modern Web protocols, not improving the security posture of the system. Even if my patches haven't compromised the code's security, the versions of Visual C++ runtime and Windows involved have documented vulnerabilities that can affect libcurl and WolfSSL.
</p><p>Don't do your banking on Windows 2000, whether via IE6 or cURL 8.6.0.
</p></div><h2>Prerequisites
</h2><p>While I'm currently getting things running on Windows 2000 Professional SP4, I'd like to get this working on Windows 95 and 98 too. To that end, I'll be using Visual Studio.NET 2003, which was the last version to support Windows 95. I'm also building everything <em>in</em> Windows 2000, but that part's optional.
</p><p>You'll also need a copy of the Platform SDK from the era. I'm using the February 2003 MSDN release, for no other reason than it sounded like a good fit for VS2003.
</p><p>And some general advice if you want to develop for old Windows versions: the MSDN Library that comes with VS2003 is <em>so</em> much more useful than the version on the Internet. Today's MSDN docs are a minefield of incorrect version information, and while I'm sure modern-day search engines are far better than WinHelp, the built-in search function is surprisingly useful.
</p><div class="aside"><h2>Just here for the code?
</h2><p>The source code to both WolfSSL and cURL with my changes are available on my Git server:
</p><ul><li><a href="https://git.2ki.xyz/snow/wolfssl-w32">https://git.2ki.xyz/snow/wolfssl-w32</a>
</li><li><a href="https://git.2ki.xyz/snow/curl-w32">https://git.2ki.xyz/snow/curl-w32</a>
</li></ul><p>To make things easy: any of my changes to WolfSSL are licensed under GPLv2, and cURL under the cURL license.
</p></div><h2>WolfSSL and Visual C++
</h2><p>Before we think about compiling cURL, we need a TLS library. In the past I've made hacks to OpenSSL to get it to build on pre-Vista Win32, but I'd really rather avoid that for something I share with others.
</p><p>Inspired by <a href="https://www.dialup.net/wingpt/tls.html">the post on Dialup.net</a> about building WolfSSL for Windows 3.11 for Workgroups, I decided that would be a better place to start.
</p><p>At first glance, I noticed a file named <code>wolfssl.vcproj</code>, indicating a pre-Visual C++ 2010 project. Good start, except the project was made in Visual Studio 2008.<sup>[<a href="#fn:vcvers" id="fnref:vcvers">3</a>]</sup>
</p><pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;Windows-1252&quot;?&gt;
&lt;VisualStudioProject
        ProjectType=&quot;Visual C++&quot;
        Version=&quot;9.00&quot;</code></pre><p>To make matters worse, the project was woefully outdated. It was missing several files required to link, meaning I had to cherry-pick files as I got errors.
</p><p>Unfortunately, there's not a great way to automate this. So began the process of painstakingly adding files to the project until it compiled, using the CMakeLists.txt and  VC2010+ .vcxproj files as references.
</p><h3>Configuring WolfSSL
</h3><p>As is common with software written in C, configuration is handled via <code>#define</code> statements. WolfSSL helpfully allows you to override the usual configuration process by defining <code>WOLFSSL_USER_SETTINGS</code>. This allows us to use the configuration already used by the .vcproj: <code>IDE/WIN/user_settings.h</code>.
</p><p>Figuring out the right defines took more troubleshooting than I care to describe, so here's a lightning round:
</p><ul><li>First, force C89 by defining <code>WOLF_C89</code> and <code>NO_WOLF_C99</code>. More on this in a bit.
</li><li>Disable <code>WOLFSSL_SP_X86_64</code>,since we won't be running this on an x86_64 CPU.
</li><li>We don't have <code>strntok</code> or <code>strtok_s</code>, so define <code>USE_WOLF_STRTOK</code>.
</li></ul><p>We also have to define a few functions, so add them:
</p><pre><code class="language-c">#define XSTRTOK wc_strtok
#define XVSNPRINTF _vsnprintf</code></pre><p>We also need to define <code>strcpy_s</code>. My workaround was to use <code>StringCchCopy</code>, which is close enough? Probably? You need to <code>#include &lt;Winerror.h&gt;</code> first though, so later <code>#include &lt;Windows.h&gt;</code> lines don't try to re-define the <code>SUCCEEDED</code> and <code>FAILED</code> macros. Win32 headers are annoying like that.
</p><div class="aside"><h3>Aside: What <em>is</em> a C standard, really?
</h3><p>A quick and inaccurate history: some guy at a phone company made a language named C.<sup>[<a href="#fn:chist" id="fnref:chist">4</a>]</sup> His coworkers were working on a bunch of different programs with it and thought it was useful, so a couple of them <a href="https://openlibrary.org/books/OL4558528M/The_C_programming_language">wrote a book</a> about it.
</p><p>Without a standard, companies decided to make fifty different versions with their own features. To combat that, the American National Standards Institute (ANSI) decided they should make a standard about it, and took half a decade hashing out C89, or C90 if you're more of an ISO person.<sup>[<a href="#fn:ansi1" id="fnref:ansi1">5</a>]</sup> Those guys from the phone company <a href="http://s3-us-west-2.amazonaws.com/belllabs-microsite-dritchie/cbook/index.html">rewrote their book</a> for that, too. The companies decided to follow at least those rules when making their fifty different versions.
</p><p>Then, C99 arrived a decade later, with a ton of great features that everyone loved.<sup>[<i>citation needed</i>]</sup> Boolean types, variadic macros, the ability to declare variables wherever you want; all the classics.
</p><p>VC2003 doesn't support any of those though, so. Oops.
</p></div><h3>Out with the new
</h3><p>As mentioned previously, WolfSSL helps us avoid most of the C99 stuff with the <code>WOLF_C89</code> and <code>NO_WOLF_C99</code> defines. Unfortunately, this doesn't seem to account for variadic macros. They were first supported with VC2005, so we'll have to work around it. They're all used as a way to NOP functions, so we can either patch simpler functions like so:
</p><pre><code class="language-c">// from ...
#define X509_check_purpose(...)     0
// ... to
#define X509_check_purpose(x)       0</code></pre><p>Or for more complex functions where I don't feel like faking ten parameters, we can define an <code>__inline</code> variadic function:
</p><pre><code class="language-c">// from...
#define SSL_CTX_add_server_custom_ext(...) 0
// ... to
__inline int SSL_CTX_add_server_custom_ext(SSL_CTX *ctx, ...) {
&#9;(void)(ctx); // avoid compiler complaints about unused parameters
&#9;return 0; // return 0
}</code></pre><p>Lastly, although not a C99 feature, we need to stop <code>wc_port.h</code> from including <code>&lt;intrin.h&gt;</code>, since VC2003 doesn't include that. My understanding is VC2005 does, so we just change:
</p><pre><code class="language-c">#elif defined(_MSC_VER)
     /* Use MSVC compiler intrinsics for atomic ops */
     #include &lt;intrin.h&gt;</code></pre><p>to:
</p><pre><code class="language-c">#elif _MSC_VER &gt;= 1400
     /* Use MSVC compiler intrinsics for atomic ops */
     #include &lt;intrin.h&gt;</code></pre><p>And <em>now</em> we're good. All done, ready to build.
</p><h3>Just kidding, I forgot about WinSock
</h3><p>There's one last function I couldn't just stub out: <a href="https://learn.microsoft.com/en-us/windows/win32/api/ws2tcpip/nf-ws2tcpip-inet_pton"><code>inet_pton</code></a>. The prototype is pretty simple:
</p><pre><code class="language-c">INT WSAAPI inet_pton(
  [in]  INT   Family,
  [in]  PCSTR pszAddrString,
  [out] PVOID pAddrBuf
);</code></pre><p>In this case, <code>Family</code> can be either <code>AF_INET</code> or <code>AF_INET6</code>. You feed in a string representing the address (e.g., <code>&quot;198.18.0.1&quot;</code>) and it writes either an <code>IN_ADDR</code> or <code>IN6_ADDR</code> struct, both more machine-readable than a C string. It's pretty convenient!
</p><p>It's also not a thing on old Windows versions. Windows 2000 only supports IPv6 as part of a <a href="https://web.archive.org/web/20030410212855/http://msdn.microsoft.com/downloads/sdks/platform/tpipv6.asp">technology preview</a>. And on older versions of Windows, you definitely aren't gonna <a href="https://www.kame.net/">see that turtle dance</a>. From what I can find, <code>inet_pton</code> was first supported in <a href="https://learn.microsoft.com/en-us/windows/win32/winsock/what-s-new-for-windows-sockets-2#updated-for-windows-vista">Windows Vista</a>. So we'll have to write our own.
</p><p>That's pretty easy with <a href="https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-inet_addr"><code>inet_addr</code></a>, which takes a C string and gives you an <code>unsigned long</code>.  So, let's just build our own:
</p><pre><code class="language-c">#ifndef InetPton

int __stdcall
InetPton(int Family,
         const char *pszAddrString,
         void *pAddrBuf)
{
        IN_ADDR *addr = pAddrBuf;
        unsigned long ulTmp;
        
        if (Family != AF_INET) {
&#9;&#9;        // Oops! We don't support that addr family
                WSASetLastError(WSAEAFNOSUPPORT);
                return -1;
        }

        if (!pAddrBuf || !pszAddrString ||
&#9;        pszAddrString[0] == '\0') {
                WSASetLastError(WSAEFAULT);
                return -1;
        }

&#9;&#9;// Technically we could just put the return val directly
&#9;&#9;// into addr-&gt;S_un.S_addr, but I don't like writing bad
&#9;&#9;// data unless absolutely necessary. So store it in a temp
&#9;&#9;// variable, first.
        ulTmp = inet_addr(pszAddrString);
        if (ulTmp == INADDR_NONE) {
&#9;&#9;        // Not an IP address
                WSASetLastError(WSAEFAULT);
                return -1;
        }

&#9;&#9;// S_un.S_addr is an unsigned long. Easy!
        addr-&gt;S_un.S_addr = ulTmp;
        return 0;
}

#endif /* !InetPton */</code></pre><p>Now, this does throw a warning during compilation (&quot;incompatible types - from 'PCWSTR' to 'const char *'&quot;), but WolfSSL is already casting an ASCII string to PCWSTR. So I think it's fine.
</p><p>Probably.
</p><h3>Deployment
</h3><p>Visual Studio doesn't provide a great way of deploying to a random prefix along the lines of your standard <code>make DESTDIR=whatever install</code>. So I tried to replicate that with a script called <code>install.js</code>. It performs the following steps:
</p><ol><li>Copies the include files in <code>wolfssl</code> to <code>%DESTDIR%\include\wolfssl</code>, excluding the files referenced by CMakeLists.txt;
</li><li>Writes a hardcoded set of options to <code>%DESTDIR%\include\wolfssl\options.h</code>, similar to how it would be handled by CMake; and
</li><li>Copies the wolfssl.lib file to <code>%DESTDIR%\include\wolfssl\lib</code>.
</li></ol><p>From the repository root, it should be as simple as running:
</p><pre><code>cscript install.js X:\whatever Release</code></pre><p>to &quot;install&quot; the Release build.
</p><h2>Step 2: cURL
</h2><p>Now that we're done with the gauntlet of WolfSSL, installing cURL should be pretty straightforward! I mean, they provide a Makefile that they call out as compatible with Visual C++ 6.0. The README has no reference to WolfSSL—just MbedTLS or OpenSSL—but we'll come back to that later. For now, I want to see if this builds without TLS support.
</p><div class="aside caution"><p>If you're following along, definitely read <code>winbuild\README.md</code> first. I'm only going to talk about the switches I used, and you need to set up the dependencies in a certain way for the build system to find them.
</p></div><p>According to the README, we'll need the following switches set in the command line:
</p><ul><li>I'm setting <code>mode=static</code> to make a static library, just to arbitrarily pick something.
</li><li><code>VC=7</code> is more for our reference, since it sets the &quot;full name&quot; of the library. It doesn't affect anything else that we care about.<sup>[<a href="#fn:vc6" id="fnref:vc6">6</a>]</sup>
</li><li><code>ENABLE_IDN=no</code> and <code>ENABLE_IPV6=no</code>, since our Windows versions don't support those.
</li><li><code>USE_SSPI=no</code>, because some of the features used require higher than Windows 2000.
</li></ul><p>So with that, we run the build:
</p><pre><code>&gt; nmake /f Makefile.vc mode=static VC=7 ENABLE_IDN=no ENABLE_IPV6=no USE_SSPI=no

Microsoft (R) Program Maintenance Utility Version 7.10.3077
Copyright (C) Microsoft Corporation.  All rights reserved.

configuration name: libcurl-vc7-x86-release-static
The input line is too long.
NMAKE : fatal error U1077: 'CALL' : return code '0xff'
Stop.
</code></pre><p>Ah.
</p><h3>A Quick Aside About Batch Files
</h3><p>no wait before you skip this, i promise it'll be quick
</p><p>The Windows NT Command Prompt (cmd.exe) has a maximum amount of characters per command you can write. UNIX-y shells have this limitation too, and you can see it by running <code>getconf ARG_MAX</code>. Per <a href="https://web.archive.org/web/20070422141150/http://support.microsoft.com/kb/830473">some old Microsoft docs</a>, the maximum in Windows 2000 (and NT 4.0) is 2047 characters. When you surpass this limit, you get <code>The input line is too long.</code> In modern technical terms, this is called an &quot;annoyingly small limitation.&quot;
</p><p>So, why are we hitting this limit? Makefile.vc is calling out to <code>gen_resp_file.bat</code>, which takes a bunch of files as input and dumps them out as an NMake <code>.inc</code> file to be imported later on. For example:
</p><pre><code>!INCLUDE &quot;../lib/Makefile.inc&quot;
LIBCURL_OBJS=$(CSOURCES:.c=.obj)

[...]
&#9;@SET DIROBJ=$(LIBCURL_DIROBJ)
&#9;@SET MACRO_NAME=LIBCURL_OBJS
&#9;@SET OUTFILE=LIBCURL_OBJS.inc
&#9;@CALL gen_resp_file.bat $(LIBCURL_OBJS)</code></pre><p>I won't copy the contents of <code>&quot;../lib/Makefile.inc&quot;</code>, but there's a lot of files in there. Enough that we run up against this limit, but not when the batch file is run. The culprit is this loop in the batch file:
</p><pre><code class="language-bat">for %%i in (%*) do echo         %DIROBJ%/%%i \&gt;&gt; %OUTFILE%</code></pre><p><code>%*</code> is a variable representing all the arguments passed to the batch file. Unlike shells like bash, for loops don't get special treatment—the variables get expanded before the command line is run, and we trip over the 2047 character limit before we can even get to looping.
</p><p>So I'll make my own for loop, with <code>SHIFT</code> and <code>GOTO</code>:
</p><pre><code class="language-bat">:loop
IF [%1] == [] GOTO done
echo            %DIROBJ%/%1 \&gt;&gt; %OUTFILE%
SHIFT
GOTO loop
:done</code></pre><p>In this case, we only need to expand one parameter at a time, avoiding the command line limitation altogether. It might be slower, but on my Pentium II @ 400MHz, I couldn't tell.
</p><h3>(Re)Configuring cURL
</h3><p>Of course, &quot;compatible with Visual C++ 6.0&quot; doesn't necessarily mean &quot;compatible with wildly outdated operating systems.&quot; So there are a few extra changes to make. All of these changes can be made in <code>lib\config-win32.h</code> in the source tree.
</p><p>Starting off: I cannot for the life of me figure out why <code>__fseeki64</code> is defined in some versions of Visual C++ 6.0 and above, but not in others. It seems like it's probably not supported in Windows 9x anyway? So, sorry if you wanted to upload files larger than 2GiB, but I'm enforcing <code>USE_WIN32_SMALL_FILES</code>, which reverts to the old <code>fseek</code> function.
</p><p>cURL wants a type named <code>ADDRESS_FAMILY</code>, but it never gets defined. I think this is also defined in later WinSock versions, but it also doesn't really matter since we can just <code>#define ADDRESS_FAMILY USHORT</code>.
</p><div class="aside"><h3>Bonus Round: SMB
</h3><p>cURL has SMB support using your TLS library of choice, which I'd like to keep. It does throw a couple errors that can be fixed, but the easiest workaround is defining <code>CURL_DISABLE_SMB</code> somewhere.
</p><p>If you, like me, care to stare into this abyss, you'll first have to return to WolfSSL and add the <code>WOLFSSL_DES_ECB</code> define to <code>user_settings</code> and the <code>DEFINES</code> array in <code>install.js</code>. Rebuild and reinstall, and you shouldn't see compile errors.
</p><p>For some reason, <code>lib/smb.c</code> defines <code>getpid</code> to <code>GetCurrentProcessId</code>, despite there already being a function in MSVC. This leads to a confusing error where <code>GetCurrentProcessId</code> gets &quot;re-declared&quot; in <code>&lt;process.h&gt;</code>. You can just remove this <code>#define</code> to resolve the issue.
</p></div><h3>Supporting WolfSSL
</h3><p>While we're already messing with build files, this seems like a good time to add WolfSSL support into the Makefile. You can already include MbedTLS with <code>WITH_MBEDTLS=&lt;dll/static&gt;</code>, so that seems like a good starting point. In <code>MakefileBuild.vc</code>:
</p><pre><code class="language-make">!IFDEF MBEDTLS_PATH
MBEDTLS_INC_DIR  = $(MBEDTLS_PATH)\include
MBEDTLS_LIB_DIR  = $(MBEDTLS_PATH)\lib
MBEDTLS_LFLAGS   = $(MBEDTLS_LFLAGS) &quot;/LIBPATH:$(MBEDTLS_LIB_DIR)&quot;
!ELSE
MBEDTLS_INC_DIR  = $(DEVEL_INCLUDE)
MBEDTLS_LIB_DIR  = $(DEVEL_LIB)
!ENDIF

[...]

!IF &quot;$(WITH_MBEDTLS)&quot;==&quot;dll&quot; || &quot;$(WITH_MBEDTLS)&quot;==&quot;static&quot;
USE_MBEDTLS    = true
MBEDTLS        = $(WITH_MBEDTLS)
MBEDTLS_CFLAGS = /DUSE_MBEDTLS /I&quot;$(MBEDTLS_INC_DIR)&quot;
MBEDTLS_LIBS   = mbedtls.lib mbedcrypto.lib mbedx509.lib
!ENDIF

!IF &quot;$(USE_MBEDTLS)&quot;==&quot;true&quot;
CFLAGS = $(CFLAGS) $(MBEDTLS_CFLAGS)
LFLAGS = $(LFLAGS) $(MBEDTLS_LFLAGS) $(MBEDTLS_LIBS)
!ENDIF</code></pre><p>For the most part, we can just copy and paste these lines, changing <code>MBEDTLS</code> to <code>WOLFSSL</code>. The only other line to worry about is updating the library files to be... well, <code>wolfssl.lib</code>:
</p><pre><code class="language-make">WOLFSSL_LIBS    = wolfssl.lib</code></pre><p>Also, now that we set <code>USE_WOLFSSL</code>, cURL will try to <code>#include &lt;stdint.h&gt;</code>. VC2003 doesn't provide that, so copy <a href="https://git.2ki.xyz/snow/curl32-build/raw/branch/trunk/stdint.h">this public domain version that I patched</a> into your <code>deps/include</code> folder. It's the same one that's been floating around the Internet for years, but I removed the <code>WCHAR_MIN</code> and <code>WCHAR_MAX</code> defines because they conflict with the ones in <code>&lt;Windows.h&gt;</code>.
</p><h3>Compiling cURL
</h3><pre><code>&gt; nmake /f mode=static VC=7 ENABLE_IDN=no ENABLE_IPV6=no WITH_WOLFSSL=static USE_SSPI=no</code></pre><p>Assuming no errors occur, the completed <code>curl.exe</code> should be in <code>builds\libcurl-vc7-x86-release-static\bin</code>. But does it work?
</p><pre><code>&gt; curl -kv https://bell.2ki.xyz
* Host bell.2ki.xyz:443 was resolved.
* IPv4: 209.251.245.54
*   Trying 209.251.245.54:443...
* Connected to bell.2ki.xyz (209.251.245.54) port 443
* SSL connection using TLSv1.3 / TLS13-AES128-GCM-SHA256
* using HTTP/1.x
&gt; GET / HTTP/1.1
&gt; Host: bell.2ki.xyz
&gt; User-Agent: curl/8.6.0
&gt; Accept: */*
&gt;
&lt; HTTP/1.1 200 OK
&lt; Server: nginx/1.24.0
&lt; Date: Sat, 02 Mar 2024 00:52:43 GMT
&lt; Content-Type: application/octet-stream
&lt; Content-Length: 16
&lt; Connection: keep-alive
&lt;
It works! (TLS)
* Connection #0 to host bell.2ki.xyz left intact</code></pre><p>At least on my system, it seems so!
</p><h2>What now?
</h2><p>There are a few projects other than the aforementioned screenshot project that I think could be useful. Providing a WinHTTP.dll shim library is my first thought, although I imagine most services using that library are already dead.
</p><p>As you'll see in the build files for both projects, my next plan is to try for Windows 9x support. I don't currently have a 95/98 workstation readily available, but I'll update this post when I do. If you give it a try before then, please let me know!
</p><div id="footnotes"><hr><ol><li id="fn:http">For more information, see RFC 2616, <em>Hypertext Transfer Protocol -- HTTP/1.1</em> (<a href="https://www.rfc-editor.org/rfc/rfc2616.html">https://www.rfc-editor.org/rfc/rfc2616.html</a>). <a href="#fnref:http"><img alt="(back)" class="backbtn" src="/assets/img/return.gif"></a></li><li id="fn:tls">For more information, scream into your nearest soft, sound-absorbing material. A pillow, blanket, or limb should do. <a href="#fnref:tls"><img alt="(back)" class="backbtn" src="/assets/img/return.gif"></a></li><li id="fn:vcvers">For context, since I'll vaguely reference it throughout this post: Visual Studio.NET 2003 is Version 7.1, 2005 is 8.0, and 2008 is 9.0. The &quot;Visual Studio.NET&quot; moniker was started with 2002 and dropped with 2003, with the next version being &quot;Visual Studio 2005.&quot; <a href="#fnref:vcvers"><img alt="(back)" class="backbtn" src="/assets/img/return.gif"></a></li><li id="fn:chist">See &quot;The Development of the C Language*&quot; by Dennis M. Ritchie (<a href="https://www.bell-labs.com/usr/dmr/www/chist.html">https://www.bell-labs.com/usr/dmr/www/chist.html</a>). <a href="#fnref:chist"><img alt="(back)" class="backbtn" src="/assets/img/return.gif"></a></li><li id="fn:ansi1">See &quot;The Origin of ANSI C and ISO C&quot; from the American National Standards Institute (<a href="https://blog.ansi.org/2017/09/origin-ansi-c-iso-c/">https://blog.ansi.org/2017/09/origin-ansi-c-iso-c/</a>). <a href="#fnref:ansi1"><img alt="(back)" class="backbtn" src="/assets/img/return.gif"></a></li><li id="fn:vc6"><em>Technically</em> it matters if you're using VC6, since it sets different compiler flags. But we're not, so I don't care. <a href="#fnref:vc6"><img alt="(back)" class="backbtn" src="/assets/img/return.gif"></a></li></ol></div>]]></description>
            </item>
            <item>
                <title>Updated 2ki page to add ML info</title>
                <pubDate>Wed, 07 Feb 2024 17:06:24 -0800</pubDate>
                <guid isPermaLink="false">urn:uuid:4ddd3596-837b-11ef-9690-afcf80f11336</guid>
                <description><![CDATA[Oops, haven't updated this site in a minute. Hopefully will have something more substantial soon!]]></description>
            </item>
            <item>
                <title>Added an RSS feed</title>
                <pubDate>Fri, 04 Mar 2022 20:45:15 -0800</pubDate>
                <guid isPermaLink="false">urn:uuid:4df1379e-837b-11ef-9d4b-dc9b55c156fe</guid>
                <description><![CDATA[Give me ten more years and we'll get to Atom]]></description>
            </item>
            <item>
                <title>New Site! and Browsers</title>
                <pubDate>Sun, 13 Feb 2022 16:27:00 -0800</pubDate>
                <guid isPermaLink="false">urn:uuid:4e0b9350-837b-11ef-86b1-8658a1844e83</guid>
                <link>https://datagirl.xyz/posts/system_start.html</link>
                <description><![CDATA[<p>I have a new site! It's not perfect, but it's something I feel better
about than <a href="https://datagirl.xyz/old/">the previous iteration</a>.
</p><h2>Generating the site
</h2><p>Despite being the part I spent the largest chunk of time on, I don't
have much to say on the scripts that assemble this site together. I
was interested in learning Perl and found the idea of using an existing
solution boring, so I made my own.
</p><p>I would not advise you use this for your own site<sup>[<a href="#fn:engine" id="fnref:engine">1</a>]</sup>, but if you
want to check out the source, it's available
<a href="https://git.2ki.xyz/snow/dg-x">here</a>.
</p><h2>Professional...ness
</h2><p>For years I've been plagued by a need to be &quot;professional&quot; with the
things I create online. This, in my mind, translates to an almost
sterile expression&mdash;anything outside of absolute plain-text or some
Twitter Bootstrap-y design is too far.
</p><p>However, any site I make that ends up so barren will just cause me to
lose any motivation to work with it. So, I'm trying something new! I'm
hoping to hit an early-to-mid '00s style with this, with some extra
handling for more modern devices. I don't feel it's really that far out
of the norm anyway, but it's the little things that count, right?
</p><p><span style="font-size: 0.75em;">Checking off &quot;moral justification for
putting all content into a single <code>&lt;table&gt;</code>&quot;...</span>
</p><p>That said, making a more complex design means having to worry about how
browser rendering engines work. That is, they work like a tower of
bricks liable to topple if one of your hyperlinks sneezes the wrong way.
</p><h2>Browser engines
</h2><p>For better or worse, the browser landscape is pretty boring these days.
Responsive design is more important with screens taking a myriad of
forms, from touch-screen phones to 8K 50-inch televisions. Despite the
variety, there are usually a few key assumptions you can make.
</p><p>With over 65% market share as of time of writing<sup>[<a href="#fn:market" id="fnref:market">2</a>]</sup>&mdash;not even including the
browsers using the same underlying renderer&mdash;Google Chrome and its
Blink engine are going to be the most common you'll see. Of
course there may be some small differences depending on the OS, but most
if not all of the features are consistent. And especially if you stick
to standards or keep an eye on sites like <a href="https://developer.mozilla.org/">MDN</a> and <a href="https://caniuse.com/">caniuse.com</a>,
supporting non-Blink browsers like Firefox and Safari won't be much of a
problem.
</p><p>But those aren't the only browsers still updated these days! Text-mode
browsers like <a href="https://lynx.invisible-island.net">lynx</a>, derivatives like <a href="https://www.palemoon.org/">Pale Moon</a> and <a href="https://www.seamonkey-project.org">SeaMonkey</a>, and
built-from-scratch GUI browsers like <a href="https://www.netsurf-browser.org/">NetSurf</a> are all used by a
multitude of people today. Not all these browsers are going to support
modern JavaScript, features like custom fonts, or even graphical
elements like images. This brings a lot of challenges, not all of which
are neatly solved.
</p><p>Just trying to support modern browsers, you might have to employ kludges
to get things to look as you hope. For example, say someone converts an
old bitmap font to a pixellated TTF format so you can further have that
2000s web design aesthetic for your site. You've taken a crowbar to the
anti-aliasing algorithm of every browser as much as you can (much to the
frustration of other web designers), and things are looking pretty
crisp. Then you get a message or five from others saying the font looks
broken. It's blurry, or it's stretched in weird ways.
</p><p>You're able to deduce the issue as being a rendering issue if the text
is aligned on a half-pixel<sup>[<a href="#fn:font" id="fnref:font">3</a>]</sup>. This weird quirk might be happening
because of how you center your content in the browser. Maybe something
like:
</p><pre><code class="language-css">    width: 500px;
    position: absolute;
    left: -250px;
    margin-left: 50%;</code></pre><p>So with a viewport size of 1920x960, <code>(1920 * 0.5) - 250 = 710px</code> and
everybody's happy. But resize your window to 1919x960 and you get
709.5px, causing the font renderer to become evil.<sup>[<em>citation needed</em>]</sup>
</p><p>So you might end up with this garbage, just to get the centering to
round to the nearest pixel:
</p><pre><code class="language-js">function fixMargin() {
    document.getElementById(&quot;main&quot;).style.marginLeft =
        Math.floor(window.innerWidth / 2).toString() + &quot;px&quot;;
}
window.addEventListener('resize', fixMargin, true);
window.addEventListener('DOMContentLoaded', fixMargin, true);</code></pre><p>But it works! Most of the time. And also only if your browser supports
JavaScript, but what browser <em>wouldn't</em> support JavaScript in 2022?
</p><h2>NetSurf
</h2><p>I have strong and complex opinions about JavaScript. They're nothing
that hasn't been discussed to death, but I'd prefer to avoid JavaScript
when possible and make sure that a curious user knows <em>why</em> I'm running
code on their computer.
</p><p>Even so, it'd be bad to assume my JavaScript will run at all.
</p><p>NetSurf is a small, fast web browser that supports HTML5 and CSS 2.1
in varying capacities, and has some preliminary support for JavaScript.
That said, support for JavaScript is disabled by default, and support
for other technologies is <a href="https://www.netsurf-browser.org/documentation/progress.html">a bit complicated</a>.
</p><p>I only started working on NetSurf support after a partner noted an
older version of the site's design looked broken in the browser. The
content was readable and most of the functionality worked fine, but it
wasn't very pretty. And that just won't do.
</p><p>First off was dealing with the CSS. Chrome, Safari, and Firefox support
multiple <code>background-image</code>s in succession, and show them right next to
each other. Something like:
</p><pre><code class="language-css">background-image: url(img/left.png), url(img/right.png);
background-position: top left, top left;
background-repeat: no-repeat, repeat-x;</code></pre><p>NetSurf refuses to even process any images if it sees them in a list
format like that. NetSurf also doesn't support <code>z-index</code> yet, so I had
to be careful about how I designed elements that'd be in &quot;layers,&quot; like
the snowflake in the site background.
</p><p>One hack I'm proud of is how I handle the &quot;subtitle&quot; seen on the index
page. In browsers with JavaScript enabled, you only see one of the
(currently) three subtitles, and can get another one by clicking on it:
</p><p><img alt="View of the subtitle element" class="as-post" src="/assets/post-img/jssubtitle.png">
</p><p>If you're on a browser that doesn't support JS or has it disabled
though, you should see the full list, like so:
</p><p><img alt="View of all subtitles in a list format" class="as-post" src="/assets/post-img/nojsst.png">
</p><p>This is accomplished by the use of a <code>&lt;noscript/&gt;</code> tag. If you have
something like:
</p><pre><code>    &lt;noscript&gt;
      &lt;p&gt;
        Hello, world!
        &lt;a href=&quot;http://example.com&quot;&gt;Click here.&lt;/a&gt;
      &lt;/p&gt;
    &lt;/noscript&gt;</code></pre><p>A browser with JavaScript will hide the element, and a browser without
JavaScript will show the element. Except it's not quite as simple as
setting <code>display: none;</code> on the tag. The browser with JavaScript won't
even attempt to process the HTML within the tag.
</p><p>So the script that handles the rotation of the subtitles grabs the
<code>&lt;noscript/&gt;</code> element, passes that to a <code>DOMParser</code> object, and uses
that to grab the DOM elements it needs.
</p><p>After a lot of tweaking and frustration, I had something that looked
mostly similar between all the browsers I was testing. There are still
some small differences (for example, NetSurf doesn't seem to support
custom fonts yet), but it was enough for me to sleep peacefully at
night.
</p><p>And then I came up with an absolutely awful idea.
</p><h2>Anyway, Here's Netscape
</h2><p>I got together a Windows 98 SE virtual machine with QEMU, connected it
to the Internet like your parents said you should never do, and
installed Netscape 7.2. Surprisingly, the dev site was in pretty good
shape. Probably the work put into NetSurf support paid off.
</p><p>The most notable issue was that the subtitle wasn't appearing at all.
Attempts to troubleshoot with Netscape's JavaScript console made me
appreciate Firefox's modern console a bit more.
</p><p><img alt="Netscape 7 JS console. It's not pretty." class="as-post" src="/assets/post-img/nsjscons.png">
</p><p>The majority of the errors were, as one might guess, features that just
didn't exist when Netscape 7 was created. As shown above, no <code>console</code>
object exists in Netscape for <code>console.log()</code> calls to log to the
console. This also kills the script, which makes sense but is also
annoying.
</p><p>The JavaScript debugger bundled with Netscape, named <a href="http://www.hacksrus.com/~ginda/venkman/">Venkman</a>, is
surprisingly nice for the time. From my experiences as a kid messing
around with Frontpage and Internet Explorer 6 or so, I was dreading it.
To the contrary, it almost felt like a proper IDE. I can definitely feel
where pieces of this ended up becoming the modern Firefox developer
tools.
</p><p><img alt="Screenshot of Venkman, saying &quot;Welcome to the JavaScript Debugger&quot;." class="as-post" src="/assets/post-img/venkman.png">
</p><p>And without further ado, here's a full screenshot of the site in
Netscape 7.2:
</p><p><a href="/assets/post-img/netscape2.png"><img alt="Screenshot of the main site in Netscape 7.2" class="as-post" src="/assets/post-img/netscape2.png" width="660"></a>
</p><p>It's got some issues with colors, but I'm happy with it.
</p><p>And in case you're curious, here's how it looks on Internet Explorer 5
on the same VM:
</p><p><a href="/assets/post-img/ie5bad.png"><img alt="Screenshot of the site in Internet Explorer 5" class="as-post" src="/assets/post-img/ie5bad.png" width="660"></a>
</p><p>I will not be fixing this in the near future, mainly because debugging
Internet Explorer is annoying and I don't even know where to begin with
whatever's happening here. Maybe someday I'll revisit it.
</p><h2>Moving forward
</h2><p>This site is still far from complete, so things will change over time.
Maybe I'll spontaneously find motivation to get the site presenting
properly in <a href="https://www.dillo.org/">Dillo</a>! Probably not.
</p><p>Honestly, the Netscape support was an &quot;extra credit&quot; exercise. I sought
to support NetSurf because multiple loved ones use it, and I'm at least
pretty confident that if my site can work in Netscape 7.x, it'll work in
later Gecko forks like Pale Moon.
</p><p>Also, I hate working with mobile browsers and will put off things I
dread until the last second. But I'm sure that's got nothing to do with
it.
</p><div id="footnotes"><hr><ol><li id="fn:engine">If you're looking for a static site generator, I haven't worked with <a href="https://jekyllrb.com/">Jekyll</a> personally but I've heard good things about it. <a href="#fnref:engine"><img alt="(back)" class="backbtn" src="/assets/img/return.gif"></a></li><li id="fn:market">Using <a href="https://gs.statcounter.com/browser-market-share/desktop/worldwide/#monthly-202110-202110-bar">StatCounter</a> for statistics here. I imagine all of the stat sites are ballpark estimates at best. <a href="#fnref:market"><img alt="(back)" class="backbtn" src="/assets/img/return.gif"></a></li><li id="fn:font">Don't worry, I barely understand what I'm talking about either. <a href="#fnref:font"><img alt="(back)" class="backbtn" src="/assets/img/return.gif"></a></li></ol></div>]]></description>
            </item>
    </channel>
</rss>
