Initial commit

This commit is contained in:
Bryce Bostwick 2025-02-15 16:32:40 -08:00
commit 650d1ef2fe
4 changed files with 243 additions and 0 deletions

7
README.md Normal file
View file

@ -0,0 +1,7 @@
# Fix The Gulf
A small Chrome extension that restores the Gulf of Mexico's name on Google Maps. Available for download [here](https://chromewebstore.google.com/detail/restore-the-gulf-of-mexic/lonhbfacjemgflooloncigacbgmdgfmo?authuser=0&hl=en).
A YouTube video covering the research into building this is available [here](https://youtu.be/F5m2JxplnXk).

221
gulf.js Normal file
View file

@ -0,0 +1,221 @@
(() => {
/**
* The functions we're patching are available globally on the variable named `_`,
* but they have computer-generated names that change over time
* when the script is updated, like `_.N8a` or `_.gd`.
*
* In order to make this script slightly more resiliant against these
* name changes, we look up these function names at runtime based
* on the actual contents of the function. This relies on calling
* `toString()` on each function and seeing if it matches a
* pre-defined version. This function returns the name of a function
* matching that pre-defined versoin.
*
* This sounds awful, and maybe is, but the functions we're patching
* are super short, and don't depend on any other computer-generated
* function names, and therefore should be fairly resistant to changes
* over time.
*
* If the function implementations actually change, then this script
* will need to be patched - but that's a good thing, as we'd rather
* fail to patch anything than break the entire site.
*
* @param {string} stringRepresentation the `toString()` representation
* of the function to look up
* @returns the name of the function in the global `_` namespace matching
* that string representation, if any
*/
const findFunction = (stringRepresentation) => {
return Object.keys(_).find(key => _[key] && _[key].toString && _[key].toString() === stringRepresentation)
}
/*
Look up the name of the first function to patch,
JSON-parsing related utility. This function
is used in a couple places, one of them being parsing
of JSON API requests. It's not the most direct place
to hook, but it is probably the most convinient
(meaning it is a global function that's close in
execution to the spot we want to modify, without
any other dependencies)
*/
const jsonParsingFunctionName = findFunction('function(a,b){const c=JSON.parse(a);if(Array.isArray(c))return new b(c);throw Error("U`"+a);}')
/*
Store a copy of the original JSON parsing function
*/
const originalJsonParsingFunction = _[jsonParsingFunctionName]
/*
Replace the JSON parsing function. This version
replaces 'Gulf of America' -> 'Gulf of Mexico'
indiscriminately in the JSON string being parsed,
and then calls out to the original function.
*/
_[jsonParsingFunctionName] = function(a, b) {
a = a.replaceAll('Gulf of America', 'Gulf of Mexico');
return originalJsonParsingFunction(a, b)
}
/*
Look up the name of the second function to patch,
a fun functional-programming utility that takes in
two parameters:
a = an array of functions; only the first item is used
b = another function
if we say A is the function at a[0], then
this overall function's impl is basically:
return b(A)
Like the first function we're hooking, this one is not
the most direct spot to hook (this one's not even)
directly text-processing-related, but it is the most convinient.
We hook this method in order to inspect the value returned
by one of its functions. This value contains binary data
that ends up being translated into labels to place on the map.
*/
const labelProcessingFunctionName = findFunction('(a,b)=>{if(a.length!==0)return b(a[0])}')
/*
Store a copy of the original processing function
*/
const originalLabelProcessingFunction = _[labelProcessingFunctionName]
/*
Replace the original processing function
*/
_[labelProcessingFunctionName] = (a, b)=>{
// We want to modify the value returned by function `a[0]`,
// so instead of passing `a` to the original function,
// we define our owh function to sit in the middle
const hookedFunction = function (...args) {
if (a.length == 0) {
return
}
// Call the original `a[0]` function with whatever
// args were passed in to our function
const data = a[0](...args)
// If that response contains a `labelGroupBytes`
// UInt8Array field, then call out to
// `patchLabelBytesIfNeeded` to do the heavy lifting
// of replacing references within it
if (data.labelGroupBytes && data.labelGroupBytes instanceof Uint8Array) {
patchLabelBytesIfNeeded(data.labelGroupBytes)
}
// Return the data, patched or not
return data
}
// Call the original function, injecting our
// own function as one of the parameters
originalLabelProcessingFunction([hookedFunction], b)
}
/**
* Looks for "Gulf of America" in the given byte array and patches any occurances
* in-place to say "Gulf of Mexico" (with a trailing null byte, to make the strings
* the same size).
*
* These byte arrays can contain unexpected characters at word/line breaks
* e.g., `Gulf of ߘ\x01\n\x0F\n\x07America`. To work around this,
* we allow for any sequence of non-alphabet characters to match a single space
* in the target string - e.g., ` ` matches `ߘ\x01\n\x0F\n\x07`.
*
* @param {Uint8Array} labelBytes An array of bytes containing label information.
*/
const patchLabelBytesIfNeeded = (labelBytes) => {
// Define the bytes we want to search for
const SEARCH_PATTERN_BYTES = [...'Gulf of America'].map(char => char.charCodeAt(0))
// Constants for special cases
const CHAR_CODE_SPACE = " ".charCodeAt(0)
const CHAR_CODE_CAPITAL_A = "A".charCodeAt(0)
const REPLACEMENT_BYTES = [..."Mexico\0"].map(char => char.charCodeAt(0))
// For every possible starting character in our `labelBytes` blob...
for(let labelByteStartingIndex = 0; labelByteStartingIndex < labelBytes.length; labelByteStartingIndex++) {
// Start by assuming this is a match, until proven otherwise
let foundMatch = true
// Because one search byte can match multiple target bytes
// (see this function's documentation),
// we keep track of our target byte index independently of
// our search byte index
let labelByteOffset = 0
// Start iterating through our search pattern and see if we have a match
for(let searchPatternIndex = 0; searchPatternIndex < SEARCH_PATTERN_BYTES.length; searchPatternIndex++) {
// We've run out of bytes to check; not a complete match
if (labelByteStartingIndex + labelByteOffset >= labelBytes.length) {
foundMatch = false
break
}
// Get the bytes we're comparing from the target & search string.
const labelByte = labelBytes[labelByteStartingIndex + labelByteOffset]
const searchByte = SEARCH_PATTERN_BYTES[searchPatternIndex]
// Special case: if the searchByte is a space, then
// we want to match potentially many characters
if(searchByte == CHAR_CODE_SPACE && !isAlphaChar(labelByte)) {
// Advance at least one character forward in the target bytes,
// and keep repeating as long as the next character is also a non-alphabet character.
do {
labelByteOffset++
} while(!isAlphaChar(labelBytes[labelByteStartingIndex + labelByteOffset]))
// We've consumed all the non-alphabet characters we can;
// move on to checking the next character
continue
}
// Normal case: if the bytes are equal, we can move forward
// and check the next one
if(labelByte == searchByte) {
labelByteOffset++
continue
}
// If we've made it this far, the current characters didn't match
foundMatch = false
break
}
if (foundMatch) {
// We found a match! Find the offset of the letter "A" within the match
// (we can't just add a fixed value because we don't know how long the
// match even is, thanks to variable space matching)
const americaStartIndex = labelBytes.indexOf(CHAR_CODE_CAPITAL_A, labelByteStartingIndex)
// Replace "America" with "Mexico\0"
for (let i = 0; i < REPLACEMENT_BYTES.length; i++) {
labelBytes[americaStartIndex + i] = REPLACEMENT_BYTES[i];
}
}
}
}
/**
* Returns whether an ascii character code represents an
* alphabet character (A-Z or a-z).
*
* @param {int} code Ascii code of the character to check
* @returns `true` if ascii code represents an alphabet character
*/
const isAlphaChar = (code) => {
return (code > 64 && code < 91) || (code > 96 && code < 123)
}
})()

BIN
icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

15
manifest.json Normal file
View file

@ -0,0 +1,15 @@
{
"name": "Restore Gulf of Mexico",
"description": "Updates Google Maps to show the Gulf of Mexico again.",
"version": "1.0",
"manifest_version": 3,
"icons": {
"128": "icon.png"
},
"content_scripts": [{
"world": "MAIN",
"js": ["gulf.js"],
"matches": ["https://www.google.com/maps/*"],
"run_at": "document_idle"
}]
}