├── LICENSE ├── README.md ├── gulf.js ├── icon.png └── manifest.json /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Bryce Bostwick 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fix The Gulf 2 | 3 | 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). 4 | 5 | A YouTube video covering the research into building this is available [here](https://youtu.be/F5m2JxplnXk). 6 | 7 | 8 | -------------------------------------------------------------------------------- /gulf.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | /** 3 | * The functions we're patching are available globally on the variable named `_`, 4 | * but they have computer-generated names that change over time 5 | * when the script is updated, like `_.N8a` or `_.gd`. 6 | * 7 | * In order to make this script slightly more resiliant against these 8 | * name changes, we look up these function names at runtime based 9 | * on the actual contents of the function. This relies on calling 10 | * `toString()` on each function and seeing if it matches a 11 | * pre-defined version. This function returns the name of a function 12 | * matching that pre-defined version. 13 | * 14 | * This sounds awful, and maybe is, but the functions we're patching 15 | * are super short, and don't depend on any other computer-generated 16 | * function names, and therefore should be fairly resistant to changes 17 | * over time. 18 | * 19 | * If the function implementations actually change, then this script 20 | * will need to be patched - but that's a good thing, as we'd rather 21 | * fail to patch anything than break the entire site. 22 | * 23 | * @param {string} stringRepresentation the `toString()` representation 24 | * of the function to look up 25 | * @returns the name of the function in the global `_` namespace matching 26 | * that string representation, if any 27 | */ 28 | const findFunction = (stringRepresentation) => { 29 | return Object.keys(_).find(key => _[key] && _[key].toString && _[key].toString() === stringRepresentation) 30 | } 31 | 32 | /* 33 | Look up the name of the first function to patch, 34 | JSON-parsing related utility. This function 35 | is used in a couple places, one of them being parsing 36 | of JSON API requests. It's not the most direct place 37 | to hook, but it is probably the most convenient 38 | (meaning it is a global function that's close in 39 | execution to the spot we want to modify, without 40 | any other dependencies) 41 | */ 42 | const jsonParsingFunctionName = findFunction('function(a,b){const c=JSON.parse(a);if(Array.isArray(c))return new b(c);throw Error("U`"+a);}') 43 | 44 | /* 45 | Store a copy of the original JSON parsing function 46 | */ 47 | const originalJsonParsingFunction = _[jsonParsingFunctionName] 48 | 49 | /* 50 | Replace the JSON parsing function. This version 51 | replaces 'Gulf of America' -> 'Gulf of Mexico' 52 | indiscriminately in the JSON string being parsed, 53 | and then calls out to the original function. 54 | */ 55 | _[jsonParsingFunctionName] = function(a, b) { 56 | a = a.replaceAll(' (Gulf of America)', "").replaceAll('Gulf of America', 'Gulf of Mexico') 57 | return originalJsonParsingFunction(a, b) 58 | } 59 | 60 | 61 | /* 62 | Look up the name of the second function to patch, 63 | a fun functional-programming utility that takes in 64 | two parameters: 65 | 66 | a = an array of functions; only the first item is used 67 | b = another function 68 | 69 | if we say A is the function at a[0], then 70 | this overall function's impl is basically: 71 | 72 | return b(A) 73 | 74 | Like the first function we're hooking, this one is not 75 | the most direct spot to hook (this one's not even) 76 | directly text-processing-related, but it is the most convenient. 77 | 78 | We hook this method in order to inspect the value returned 79 | by one of its functions. This value contains binary data 80 | that ends up being translated into labels to place on the map. 81 | */ 82 | const labelProcessingFunctionName = findFunction('(a,b)=>{if(a.length!==0)return b(a[0])}') 83 | 84 | /* 85 | Store a copy of the original processing function 86 | */ 87 | const originalLabelProcessingFunction = _[labelProcessingFunctionName] 88 | 89 | /* 90 | Replace the original processing function 91 | */ 92 | _[labelProcessingFunctionName] = (a, b)=>{ 93 | // We want to modify the value returned by function `a[0]`, 94 | // so instead of passing `a` to the original function, 95 | // we define our owh function to sit in the middle 96 | const hookedFunction = function (...args) { 97 | if (a.length == 0) { 98 | return 99 | } 100 | 101 | // Call the original `a[0]` function with whatever 102 | // args were passed in to our function 103 | const data = a[0](...args) 104 | 105 | // If that response contains a `labelGroupBytes` 106 | // UInt8Array field, then call out to 107 | // `patchLabelBytesIfNeeded` to do the heavy lifting 108 | // of replacing references within it 109 | if (data.labelGroupBytes && data.labelGroupBytes instanceof Uint8Array) { 110 | patchLabelBytesIfNeeded(data.labelGroupBytes) 111 | } 112 | 113 | // Return the data, patched or not 114 | return data 115 | } 116 | 117 | // Call the original function, injecting our 118 | // own function as one of the parameters 119 | originalLabelProcessingFunction([hookedFunction], b) 120 | } 121 | 122 | /** 123 | * Looks for "Gulf of America" in the given byte array and patches any occurrences 124 | * in-place to say "Gulf of Mexico" (with a trailing null byte, to make the strings 125 | * the same size). 126 | * 127 | * These byte arrays can contain unexpected characters at word/line breaks — 128 | * e.g., `Gulf of ߘ\x01\n\x0F\n\x07America`. To work around this, 129 | * we allow for any sequence of non-alphabet characters to match a single space 130 | * in the target string - e.g., ` ` matches `ߘ\x01\n\x0F\n\x07`. 131 | * 132 | * @param {Uint8Array} labelBytes An array of bytes containing label information. 133 | */ 134 | const patchLabelBytesIfNeeded = (labelBytes) => { 135 | // Define the bytes we want to search for 136 | const SEARCH_PATTERN_BYTES = [...'Gulf of America'].map(char => char.charCodeAt(0)) 137 | 138 | // Constants for special cases 139 | const CHAR_CODE_SPACE = " ".charCodeAt(0) 140 | const CHAR_CODE_CAPITAL_A = "A".charCodeAt(0) 141 | const CHAR_CODE_PARENTH = '('.charCodeAt(0) 142 | const CHAR_CODE_CAPITAL_G = 'G'.charCodeAt(0) 143 | // \u200B is a zero-width space character. We add it to make the strings the same length 144 | const REPLACEMENT_BYTES = [..."Mexico\u200B"].map(char => char.charCodeAt(0)) 145 | 146 | // For every possible starting character in our `labelBytes` blob... 147 | for(let labelByteStartingIndex = 0; labelByteStartingIndex < labelBytes.length; labelByteStartingIndex++) { 148 | 149 | // Start by assuming this is a match, until proven otherwise 150 | let foundMatch = true 151 | 152 | // Because one search byte can match multiple target bytes 153 | // (see this function's documentation), 154 | // we keep track of our target byte index independently of 155 | // our search byte index 156 | let labelByteOffset = 0 157 | 158 | // Start iterating through our search pattern and see if we have a match 159 | for(let searchPatternIndex = 0; searchPatternIndex < SEARCH_PATTERN_BYTES.length; searchPatternIndex++) { 160 | 161 | // We've run out of bytes to check; not a complete match 162 | if (labelByteStartingIndex + labelByteOffset >= labelBytes.length) { 163 | foundMatch = false 164 | break 165 | } 166 | 167 | // Get the bytes we're comparing from the target & search string. 168 | const labelByte = labelBytes[labelByteStartingIndex + labelByteOffset] 169 | const searchByte = SEARCH_PATTERN_BYTES[searchPatternIndex] 170 | 171 | // Special case: if the searchByte is a space, then 172 | // we want to match potentially many characters 173 | if(searchByte == CHAR_CODE_SPACE && !isAlphaChar(labelByte)) { 174 | // Advance at least one character forward in the target bytes, 175 | // and keep repeating as long as the next character is also a non-alphabet character. 176 | do { 177 | labelByteOffset++ 178 | } while(!isAlphaChar(labelBytes[labelByteStartingIndex + labelByteOffset])) 179 | 180 | // We've consumed all the non-alphabet characters we can; 181 | // move on to checking the next character 182 | continue 183 | } 184 | 185 | // Normal case: if the bytes are equal, we can move forward 186 | // and check the next one 187 | if(labelByte == searchByte) { 188 | labelByteOffset++ 189 | continue 190 | } 191 | 192 | // If we've made it this far, the current characters didn't match 193 | foundMatch = false 194 | break 195 | } 196 | 197 | if (foundMatch) { 198 | // We found a match! Find the offset of the letter "A" within the match 199 | // (we can't just add a fixed value because we don't know how long the 200 | // match even is, thanks to variable space matching) 201 | const americaStartIndex = labelBytes.indexOf(CHAR_CODE_CAPITAL_A, labelByteStartingIndex) 202 | let parenthStartIndex = -1; 203 | // Check if the label is `Gulf of Mexico (Gulf of America)` 204 | for (let i = 0; i < labelBytes.length; i++) { 205 | if (labelBytes[i] == CHAR_CODE_PARENTH && labelBytes[i + 1] == CHAR_CODE_CAPITAL_G) { 206 | parenthStartIndex = i 207 | break 208 | } 209 | } 210 | if (parenthStartIndex > -1) { 211 | // Replace "(Gulf of" with zero-width spaces 212 | for (let i = 0; i < 8; i++) { 213 | labelBytes[parenthStartIndex + i] = '\u200B'.charCodeAt(0) 214 | } 215 | // Replace "America)" with zero-width spaces 216 | for (let i = 0; i < 8; i++) { 217 | labelBytes[americaStartIndex + i] = '\u200B'.charCodeAt(0) 218 | } 219 | } else { 220 | // Replace "America" with "Mexico\u200B" 221 | for (let i = 0; i < REPLACEMENT_BYTES.length; i++) { 222 | labelBytes[americaStartIndex + i] = REPLACEMENT_BYTES[i] 223 | } 224 | } 225 | } 226 | 227 | } 228 | } 229 | 230 | /** 231 | * Returns whether an ascii character code represents an 232 | * alphabet character (A-Z or a-z). 233 | * 234 | * @param {int} code Ascii code of the character to check 235 | * @returns `true` if ascii code represents an alphabet character 236 | */ 237 | const isAlphaChar = (code) => { 238 | return (code > 64 && code < 91) || (code > 96 && code < 123) 239 | } 240 | 241 | })() 242 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brycebostwick/FixTheGulf/b40ed628a0d4b2e7d150d33c46cfe5b8272b2a7a/icon.png -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Restore Gulf of Mexico", 3 | "description": "Updates Google Maps to show the Gulf of Mexico again.", 4 | "version": "1.0", 5 | "manifest_version": 3, 6 | "icons": { 7 | "128": "icon.png" 8 | }, 9 | "content_scripts": [{ 10 | "world": "MAIN", 11 | "js": ["gulf.js"], 12 | "matches": ["https://www.google.com/maps/*"], 13 | "run_at": "document_idle" 14 | }] 15 | } 16 | --------------------------------------------------------------------------------