kivikakk.ee

you should hand-write a parser

I found myself adding attribute support to Comrak on the weekend. Attributes in Markdown (as they’re often supported; there’s nothing for them in CommonMark) look like this:

Henlo [world](https://agave.syrup){.delicious}

This would, in most dialects that support it, attach a “class” attribute called delicious, which, in most dialects that then render these to HTML, would produce something like:

Henlo <a href="https://agave.syrup" class="delicious">world</a>

You can also attach IDs with #bweh and arbitrary key-pairs with wan=nyan, not to mention double-quote and escaping support, leading to {#monstrosities .somewhat like=this}. But y’know, it’s fine, and it’s even helpful for attaching things like height and width to images without having to resort to HTML. GitLab happens to use them for this purpose, though it implements it with a Very Special Post-Processing Step.

So how do we parse these fun strings? Ignoring the actual situating-them-in-the-document bit — assume we get to write a function that gets called whenever a { is found in a place where it might be an attribute (like right after a link or image, say), and it’s given what follows, and we need to (a) decide if it’s actually an attribute, and if so, (b) return the structured data from it, and how long that was (so the parent parser knows how much to skip).

I don’t know what your first impulse is, but it should probably be to just hand-write a state machine driven parser. Here’s mine! Consider reading the parse function closely enough that it makes sense to you, if your impulse is to reach for a regular expression, lexer/parser generator, or otherwise.

Some notes:

  • Locally defined state enums are fun!
  • Other than the result (which is random-write, as attributes may arrive in any order), all state is stored in the state enum member; this is why Value embeds Kind and Quote, and why Kind embeds Pair. I have to shuffle these around in some states, but it’s cleaner than having more function locals which are effectively “globals” that only apply sometimes, and which you can forget to reinit.
  • We use str::char_indices because the input is Unicode, we really do prefer to enumerate characters and not bytes, but also we return the (byte) index to the parent parser so it knows how many to skip (since a character count would necessitate O(n) through the string, thank’s UTF-8). Note we fearlessly calculate things like i + 1 when we know the character under i is single-byte (i.e. '}').
  • None is returned if the input didn’t have a valid attribute set as a prefix, i.e. any parse error whatsoever; we take advantage of this to call ci.next()?, since it’ll bubble None up if we hit the end of the string without the parse finishing.
  • The double loop is a side-effect of the particular way I’ve chosen to deal with a certain property of many state machines, including this one: most state/character combinations result in a break (which means we repeat the outer loop and go to the next character), but some, like seeing a whitespace or } while reading a value, instead change the state back to some parent one (because we know the value has ended), and then let the new state handle the specifics of the character.
    • There’s more than one way to handle this kind of thing, where you want to transition to a state and let it handle that same character too, this was just least-effort. Some setups mean you’ll be able to only ‘consume’ the input token in branches that you want to (i.e. everywhere I break), others let you “unput” it back onto the input so it gets read again next loop, you can be creative. :)

    • Concretely, to make our one cleaner, you could DIY unput with something like:

      // ...
      let mut unput: Option<(usize, char)> = None;
      loop {
      let (i, c) = unput.take().or_else(|| ci.next())?;
      // ...

      Then you can drop the inner loop, replace each point that doesn’t break with unput = Some((i, c));, and remove all the breaks! PRs welcome :D

That’s basically all I have to mention. Hand-write a parser! You can do really silly things like the function that follows and even do a partial parse backwards, if your input syntax is sympathetic to it :)

pintado

Ever-declining sleep quality lately has blessed me with a preponderance of dreams turning lucid; most recently, I was lying on a grassy, featureless field, and decided I wanted to see a magpie. I painted one on the ground in front of me, with big brush strokes of my mind, and it started to sing; I could reach out and just barely touch it, feeling it light on the back of my fingers like aether. As I continued to paint, my intention settled that it should be younger, and its song turned into a baby’s meeps.

degeneration

Some months back, I stopped being able to sleep (or comfortably lie at all) on my left side. Like most of the slowly encroaching issues of my body since getting COVID, something circulation-related happened, this time in my shoulder, and lying on it causes my heart to beat hard and it’s impossible to sleep, or feel like it’s a good idea remaining in that position at all. I keep trying it, just in case, and it doesn’t get better. This is the same shoulder that gave me a whole lot of pain last year, so, I dunno, guess it’s related.

This was disquieting but practically speaking not a huge deal; I’m a side sleeper but don’t prefer sleeping on my left side anyway, and I still had lying on my back to switch between.

A week ago, though, the same problem started on my right side. Like I don’t have enough trouble sleeping.

self-affirmations in the time of ai (c/o ikea)

Photo from the plant section of an IKEA. There’s a sign above a part of the section which says: “We are real.”, with smaller text: “Please be gentle with us.”

the LLM residential botnet scraping thing

People are sometimes sceptical that providers use extremely distributed residential botnets to scrape, but it’s very obvious, particularly once you have done every thing you’re “meant to” and several steps beyond: when all the robots.txt blanket disallows and Anubis aren’t enough, and then you have to proceed to straight-up disallowing almost every request from a non-logged in user? They just keep trying, only no longer from officially tagged sources.

Here is a screenshot of nóssa’s Shynet analytics. Something happened during May 21–22! Let’s zoom in on those days.

Something certainly began at 15:00 or so. 3,528 requests for https://nossa.ee/~talya/beffast/blob/main/fddbdce6962ce338e1a17827107a976f10fed921/README.md, and 3,051 requests for https://nossa.ee/~talya/nix-rosetta-builder/blob/t/97f242ec47d110bb5a2e4294595f44be7641258a/LICENSE. Both these URLs give a 403 if you’re not logged in, and outside me there are a whole two registered users: my wife, and an old Fedi friend. I don’t think either of them are doing this, so we can safely assume all the bots are getting are 403s.

Note the referrers, which correlate in session numbers very closely to the requested locations. We have https://nossa.ee/~talya/beffast/blob/main/fddbdce6962ce338e1a17827107a976f10fed921/README.md with 3,520 sessions and https://nossa.ee/~talya/nix-rosetta-builder/blob/t/97f242ec47d110bb5a2e4294595f44be7641258a/LICENSE with 3,047 sessions.

These, too, have given 403s for the last month. Hmmm. Did they all just go to sleep for a month and then retry?

Everyone loves desktop Chrome on Windows.

From roughly the middle of the period, requests from networks identified as “Sky UK Limited”, “CABLE-NET-1”, “TWC-20001-PACWEST”, “COMCAST-7922”, “CITYOFWILSONNC”, “EGIHOSTING”, “CHARTER-20115”, “Virgin Media Limited”, “TWC-11426-CAROLINAS”, “OWS”, “UUNET”, “ATT-INTERNET4”, “Shenzhen Katherine Heng Technology Information Co., Ltd.”, “CENTURYLINK-US-LEGACY-QWEST” …

… “BACOM”, “UUNET” again, “EGIHOSTING” again, “COMCAST-7922” again, “Shenzhen Katherine Heng Technology Information Co., Ltd.” again, “CABLE-NET-1” again, “OWS” again, “EGIHOSTING” again, “T-MOBILE-AS21928”, “MEDIACOM-ENTERPRISE-BUSINESS”, “SOCKET”, “GTT Communications Inc.”, “Telmex Colombia S.A.”, “Unknown” (!), “BHN-33363”, “COMCAST-7922” again …

… “OWS” again, “EGIHOSTING” again, “COMCAST-7922” again, “UUNET” again, “TOT Public Company Limited”, “TWC-11426-CAROLINAS” again, “SP-NYJ”, “TWC-11351-NORTHEAST”, “FRONTIER-FRTR”, “Shenzhen Katherine Heng Technology Information Co., Ltd.” again, “CHARTER-201155” again, “Unknown” again, “GTT Communications Inc.” again …

… “COMCAST-7922” again, “CHARTER-20115” again, “T-MOBILE-AS21928” again, “TWC-11426-CAROLINAS” again, “EGIHOSTING” again, “ATT-INTERNET4” again, “MEGAPATH2”, “VALLEYFIBER”, “Shenzhen Katherine Heng Technology Information Co., Ltd.” again, “BACOM” again, “TWC-10796-MIDWEST”, “SPRINTLINK”, “CABLE-NET-1” again, “AS-CMN”, “CENTURYLINK-LEGACY-SAVVIS” …

… zooming back in time about 21 hours, from “EGIHOSTING” again, “COMCAST-7922” again, “VIPNAS1”, “CABLE-NET-1” again, “EASTMSCONNECT-01”, “Shenzhen Katherine Heng Technology Information Co., Ltd.” again, “TWC-11426-CAROLINAS” again, “UUNET” again, “ATT-INTERNET4” again, “OWS” again, “GTT Communications.” again.

¯\_(ツ)_/¯