mirror of
https://github.com/NanashiTheNameless/FixTheGulf.git
synced 2025-07-05 14:10:38 -04:00
Initial commit
This commit is contained in:
commit
650d1ef2fe
4 changed files with 243 additions and 0 deletions
7
README.md
Normal file
7
README.md
Normal 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
221
gulf.js
Normal 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
BIN
icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.9 KiB |
15
manifest.json
Normal file
15
manifest.json
Normal 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"
|
||||||
|
}]
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue