From 650d1ef2fe900362e00eccf8623d66b3ac0ed4d6 Mon Sep 17 00:00:00 2001 From: Bryce Bostwick Date: Sat, 15 Feb 2025 16:32:40 -0800 Subject: [PATCH] Initial commit --- README.md | 7 ++ gulf.js | 221 ++++++++++++++++++++++++++++++++++++++++++++++++++ icon.png | Bin 0 -> 6055 bytes manifest.json | 15 ++++ 4 files changed, 243 insertions(+) create mode 100644 README.md create mode 100644 gulf.js create mode 100644 icon.png create mode 100644 manifest.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..254fc2a --- /dev/null +++ b/README.md @@ -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). + + diff --git a/gulf.js b/gulf.js new file mode 100644 index 0000000..e1f4c2b --- /dev/null +++ b/gulf.js @@ -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) + } + +})() diff --git a/icon.png b/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..8e6cf328c2527d7db3367e3f0968bff7287fbe5d GIT binary patch literal 6055 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 literal 0 HcmV?d00001 diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..bb18842 --- /dev/null +++ b/manifest.json @@ -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" + }] +}