Skip to content

feat(html): implement atob and btoa - #272

Open
Sonic-Y3k wants to merge 2 commits into
gost-dom:mainfrom
Sonic-Y3k:feat/atob-btoa
Open

feat(html): implement atob and btoa#272
Sonic-Y3k wants to merge 2 commits into
gost-dom:mainfrom
Sonic-Y3k:feat/atob-btoa

Conversation

@Sonic-Y3k

Copy link
Copy Markdown

V8 does not provide atob/btoa (they are Web APIs, not part of ECMAScript), so they were missing. This implements them natively on WindowOrWorkerGlobalScope with correct binary-string semantics and WHATWG forgiving-base64 decoding. Implemented natively (rather than as a JS polyfill) so Function.prototype.toString reports [native code], matching browsers.

Testing: added TestBase64 (round-trips, binary strings, native-code check).


AI disclosure: This change was developed with the help of an AI coding assistant. I've reviewed and tested it myself; it follows the existing conventions and the full test suite (main module, v8engine, sobekengine) passes locally.

V8 provides no atob/btoa (they are Web APIs, not ECMAScript). Implemented
natively on WindowOrWorkerGlobalScope with correct binary-string semantics and
forgiving-base64 decoding.
@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

This PR adds btoa and atob support for WindowOrWorkerGlobalScope. It introduces a ByteSlice Go type, adds codegen rules and codec mappings for binary-string conversion, implements Btoa, Atob, and forgiving base64 decoding, wires the generated HTML wrappers, and adds a base64 script test suite.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 26.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title directly names the main change: adding atob and btoa support to html.
Description check ✅ Passed The description matches the change set, describing native atob/btoa support, binary-string semantics, forgiving base64 decoding, and tests.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Warning

⚠️ This pull request shows signs of AI-generated slop (description_diff_mismatch). It has been flagged by CodeRabbit slop detection and should be reviewed carefully.

Comment thread scripting/internal/html/base64.go Outdated
"outside of the Latin1 range.")
}
bytes = append(bytes, byte(r))
}

@stroiman stroiman Jun 22, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The implementation threw me off, but after checking the MDN documentation; it makes sense.

However, I think it would make sense to create a dedicated decoder for this, e.g., DecodeBinaryString, allowing this implementation to be reduced to

	bytes, err := js.ConsumeArgument(cbCtx, "data", nil, codec.DecodeBinaryString)
	if err != nil {
		return nil, err
	}
	return cbCtx.NewString(base64.StdEncoding.EncodeToString(bytes)), nil

It would only be used this once, so far, so not crucial; just seem to be separate concerns better.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure! I added codec.DecodeBinaryString, and btoa's argument decodes straight to []byte. As part of this it's all generated from the Web IDL now: native Btoa([]byte) string / Atob(string) ([]byte, error) live on windowOrWorkerGlobalScope in package html.

Comment thread scripting/v8engine/base64_test.go Outdated
eq("round-trip binary", `atob(btoa("\x00\x01\xfe\xff")) === "\x00\x01\xfe\xff"`, true)
// Implemented natively, so Function.prototype.toString reports native code.
eq("atob is native", `/\[native code\]/.test(atob.toString())`, true)
eq("btoa is native", `/\[native code\]/.test(btoa.toString())`, true)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Testing that toString() on the functions doesn't seem to make sense.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah sure! :D Fixed ;-)

@stroiman

Copy link
Copy Markdown
Member

Hey.

I actually ran into issues, wishing I had this par implemented :D

However, there are some thing I'd like to move around ;)

Separate implementation from JS bindings

Many of the types align with web IDL specifications, and the two functions are defined on WindowOrWorkerGlobalScope

interface mixin WindowOrWorkerGlobalScope {
  // ...
  DOMString btoa(DOMString data);
  ByteString atob(DOMString data);

This is also where e.g., setTimeout and friends are defined. In Go; the native Go implementations using sensible Go types exist on windowOrWorkerGlobalScope in package html. Converting to/from JavaScript types is largely auto-generated from Web IDL specs.

In internal/code-gen/customrules/html_rules.go, these methods are specifically excluded:

	IgnoreMembers(htmlRules,
		Overrides{"WindowOrWorkerGlobalScope": {
			Operations: []string{
				"atob",
				"btoa",
				// ...
			},
			// ...
		}})

So by removing these two lines; it'd almost work, but there's probably some customization necessary to tell it to decode strings to []byte.

The generator also takes care of exposing the functions in global scope.

Worker support out of the box.

I have an experimental branch with Worker support. By moving the implementation to windowOrWorker... and letting the code-gen take care of bindings, all necessary changes for worker suppoert would be auto-generated from web-idl specs.

I can do it later ...

IF you feel up to it; you are welcome to give it a shot; if not, I'll gladly take it as is, and refactor accordingly later.

The web-idl -> go codenerator isn't the ... cleanest part of the code base.

Comment thread scripting/internal/html/base64.go Outdated
s = strings.Map(func(r rune) rune {
switch r {
case ' ', '\t', '\n', '\r', '\f':
return -1

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume that -1 filters out the value. I wonder. Would it make sense to use unicode.IsSpace()? https://pkg.go.dev/unicode#IsSpace

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well the -1 makes strings.Map drop the rune. I kept the explicit set rather than unicode.IsSpace, though: WHATWG forgiving-base64 decode strips ASCII whitespace only (tab/LF/FF/CR/space), whereas unicode.IsSpace also matches non-ASCII whitespace (NBSP, the Unicode separators…), which the algorithm requires to be treated as invalid input rather than stripped. Added a comment + spec link. (Moved into forgivingBase64Decode in package html with the restructure.)

Move the native atob/btoa implementation onto windowOrWorkerGlobalScope in
package html (alongside setTimeout and friends) and generate the JavaScript
bindings from the Web IDL specification, rather than hand-wiring them in the
scripting layer.

To support the binary-string conversions the bindings need, add a []byte
GoType and a matching DecodeBinaryString/EncodeBinaryString codec pair: btoa
takes its argument as []byte and atob returns its result as []byte
(ByteString). The forgiving-base64 decode helper keeps the explicit ASCII
whitespace set required by the Infra standard.

Also fix the interface generator so a GoType in an empty (local) package
renders correctly as an argument type.

Move the atob/btoa test into the shared script-engine suite so it runs against
both the V8 and Sobek engines, and drop the Function.prototype.toString checks.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 31412e72-1ee8-4a9a-af55-e373cb01c161

📥 Commits

Reviewing files that changed from the base of the PR and between ac2cda7 and a7d08a7.

📒 Files selected for processing (13)
  • html/window_or_worker_global_scope.go
  • internal/code-gen/customrules/html_rules.go
  • internal/code-gen/gotypes/gotypes.go
  • internal/code-gen/interfaces/idl_interface.go
  • internal/code-gen/scripting/codecs.go
  • internal/code-gen/scripting/model/encoder.go
  • internal/code-gen/scripting/model/es_operation.go
  • internal/interfaces/html-interfaces/window_or_worker_global_scope_generated.go
  • scripting/internal/codec/decoders.go
  • scripting/internal/codec/encoders.go
  • scripting/internal/html/window_or_worker_global_scope_generated.go
  • scripting/internal/scripttests/base64_suite.go
  • scripting/internal/scripttests/suites.go

Comment on lines +46 to +50
if decoded, err := base64.StdEncoding.DecodeString(s); err == nil {
return decoded, nil
}
// Fall back to unpadded decoding for inputs missing trailing '='.
return base64.RawStdEncoding.DecodeString(strings.TrimRight(s, "="))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reject malformed padded input before the raw fallback.

If the padded decode fails, Line 50 strips all trailing = and retries as unpadded base64. That makes malformed inputs like Zg=== and == decode successfully instead of failing.

Suggested fix
-	if decoded, err := base64.StdEncoding.DecodeString(s); err == nil {
+	if decoded, err := base64.StdEncoding.DecodeString(s); err == nil {
 		return decoded, nil
+	} else if strings.ContainsRune(s, '=') {
+		return nil, err
 	}
-	// Fall back to unpadded decoding for inputs missing trailing '='.
-	return base64.RawStdEncoding.DecodeString(strings.TrimRight(s, "="))
+	// Fall back only for genuinely unpadded input.
+	return base64.RawStdEncoding.DecodeString(s)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if decoded, err := base64.StdEncoding.DecodeString(s); err == nil {
return decoded, nil
}
// Fall back to unpadded decoding for inputs missing trailing '='.
return base64.RawStdEncoding.DecodeString(strings.TrimRight(s, "="))
if decoded, err := base64.StdEncoding.DecodeString(s); err == nil {
return decoded, nil
} else if strings.ContainsRune(s, '=') {
return nil, err
}
// Fall back only for genuinely unpadded input.
return base64.RawStdEncoding.DecodeString(s)

Comment on lines +41 to +49
for _, r := range str {
if r > 0xFF {
return nil, fmt.Errorf(
"codec: binary string contains a character outside the Latin-1 range")
}
bytes = append(bytes, byte(r))
}
return bytes, nil
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

G115 (gosec) is a false positive here, but may still break CI.

The r > 0xFF guard bounds r to [0, 0xFF] before the byte(r) conversion (runes are non-negative; invalid UTF-8 decodes to 0xFFFD, which is also caught), so the conversion is safe. gosec doesn't perform this range analysis. If gosec is enforced in the pipeline, add a //nolint:gosec // bounded by the r > 0xFF guard to avoid a failing build.

🧰 Tools
🪛 golangci-lint (2.12.2)

[high] 46-46: G115: integer overflow conversion rune -> byte

(gosec)

Source: Linters/SAST tools

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants