From ed68874dc637bcd20ba1a78337297325a0e2863c Mon Sep 17 00:00:00 2001
From: waroxa <5514643+waroxa@users.noreply.github.com>
Date: Sun, 2 Aug 2026 17:57:40 -0400
Subject: [PATCH] fix: use UTF-16 offsets for description links
---
spec/invidious/videos/description_spec.cr | 56 +++++++++++++++++++++++
src/invidious/videos/description.cr | 13 +++---
2 files changed, 63 insertions(+), 6 deletions(-)
create mode 100644 spec/invidious/videos/description_spec.cr
diff --git a/spec/invidious/videos/description_spec.cr b/spec/invidious/videos/description_spec.cr
new file mode 100644
index 000000000..8766e2720
--- /dev/null
+++ b/spec/invidious/videos/description_spec.cr
@@ -0,0 +1,56 @@
+require "../../spec_helper"
+require "../../../src/invidious/videos/description"
+
+Spectator.describe "Video description parser" do
+ describe "#parse_description" do
+ it "uses UTF-16 offsets when an emoji precedes a command" do
+ description = JSON.parse(<<-JSON)
+ {
+ "content": "🚀 Visit example now",
+ "commandRuns": [{
+ "startIndex": 3,
+ "length": 5,
+ "onTap": {
+ "innertubeCommand": {
+ "urlEndpoint": {"url": "https://example.com"}
+ }
+ }
+ }]
+ }
+ JSON
+
+ expect(parse_description(description, "video-id")).to eq(
+ %(🚀 Visit example now)
+ )
+ end
+
+ it "uses UTF-16 lengths when an emoji is inside a command" do
+ description = JSON.parse(<<-JSON)
+ {
+ "content": "Open 😀 docs",
+ "commandRuns": [{
+ "startIndex": 0,
+ "length": 7,
+ "onTap": {
+ "innertubeCommand": {
+ "urlEndpoint": {"url": "https://example.com"}
+ }
+ }
+ }]
+ }
+ JSON
+
+ expect(parse_description(description, "video-id")).to eq(
+ %(Open 😀 docs)
+ )
+ end
+
+ it "escapes complete descriptions without command runs" do
+ description = JSON.parse(%({"content":"🚀 & complete"}))
+
+ expect(parse_description(description, "video-id")).to eq(
+ %(🚀 <safe> & complete)
+ )
+ end
+ end
+end
diff --git a/src/invidious/videos/description.cr b/src/invidious/videos/description.cr
index 18b4122eb..e0864f11a 100644
--- a/src/invidious/videos/description.cr
+++ b/src/invidious/videos/description.cr
@@ -1,9 +1,9 @@
require "json"
require "uri"
-private def copy_string(str : String::Builder, iter : Iterator, count : Int) : Int
+private def copy_string(str : String::Builder, iter : Iterator, count : Int? = nil) : Int
copied = 0
- while copied < count
+ while count.nil? || copied < count
cp = iter.next
break if cp.is_a?(Iterator::Stop)
@@ -21,7 +21,9 @@ private def copy_string(str : String::Builder, iter : Iterator, count : Int) : I
str << cp.chr
end
- copied += 1
+ # YouTube expresses command offsets in UTF-16 code units. Astral
+ # codepoints therefore count as two units instead of one.
+ copied += cp > 0xFFFF ? 2 : 1
end
return copied
@@ -38,7 +40,7 @@ def parse_description(desc, video_id : String) : String?
# Slightly faster than HTML.escape, as we're only doing one pass on
# the string instead of five for the standard library
return String.build do |str|
- copy_string(str, content.each_codepoint, content.size)
+ copy_string(str, content.each_codepoint)
end
end
@@ -70,7 +72,6 @@ def parse_description(desc, video_id : String) : String?
end
# Copy the end of the string (past the last command).
- remaining_length = content.size - index
- copy_string(str, iter, remaining_length) if remaining_length > 0
+ copy_string(str, iter)
end
end