CDN Hashes

From RoAPI

Some endpoints, like the imageUrl provided by https://thumbnails.roblox.com/v1/users/avatar-3d?userId=1, don't provide a full CDN URL and only provide raw hashes, like this: bbdb80c2b573bf222da3e92f5f148330.
We need to turn this into a full CDN URL. A CDN URL looks like https://tX.rbxcdn.com/bbdb80c2b573bf222da3e92f5f148330 where X is the CDN number. The CDN number ranges from 0 to 7, so you might be tempted to send a request to t0, then t1, and keep going until you reach the one containing the object. This works, but it's quite wasteful as you send up to 8 requests for just one object.
There's a better way to do this. We can define a variable as 31, loop through the first 32 characters in the string, and in each iteration set the variable to itself bitwise XORed against the integer representation of that character (or, alternatively, the integer version of the hex value)

Examples[edit | edit source]

fold_dyad =: adverb define
  acc =. x
  for_item. y do. acc =. acc u item end.
  acc
)

get_cdn_url =: monad define
    'https://t' , (":8 | 31 (XOR fold_dyad) a.i.y) , '.rbxcdn.com/' , y
)
import Data.Bits (xor)
import Data.Char (ord)
import Text.Printf (printf)

getCdnUrl :: [Char] -> [Char]
getCdnUrl hash =
  let t = foldl (\acc char -> xor acc $ ord char) 31 hash `mod` 8
  in printf "https://t%d.rbxcdn.com/%s" t hash
function get_cdn_url(hash)
  t = reduce((acc, char) -> xor(acc, codepoint(char)), hash, init=31)
  "https://t$(t % 8).rbxcdn.com/$hash"
end
def get_cdn_url(hash):
    i = 31
    for char in hash[:32]:
        i ^= ord(char)  # i ^= int(char, 16) also works
    return f"https://t{i%8}.rbxcdn.com/{hash}"

# alternatively:
from functools import reduce

def get_cdn_url(hash):
    t = reduce(lambda last_code, char: last_code ^ ord(char), hash, 31)
    
    return f"https://t{t % 8}.rbxcdn.com/{hash}"
package pkg

import "fmt"

func GetCdnHash(hash string) string {
	// If url is null or empty, throw an error
	if hash == "" {
		panic("url is empty")
	}

	var i int = 31

	for _, char := range hash {
		// Do a bitwise XOR operation on the character and the index
		// to get the hash
		i = i ^ int(char)
	}

	return fmt.Sprintf("https://t%d.rbxcdn.com/%s", i%8, hash)
}
defmodule CDN do
  @spec get_cdn_url(String.t()) :: integer()
  def get_cdn_url(hash) do
    t = hash
    |> String.to_charlist
    |> Enum.reduce(31, fn char, last_code -> Bitwise.bxor(last_code, char) end)

    "https://t#{rem(t, 8)}.rbxcdn.com/#{hash}"
  end
end
const getCdnUrl = (hash) => {
    const t = [...hash].reduce((lastCode, char) => lastCode ^ char.charCodeAt(0), 31)

    return `https://t${t % 8}.rbxcdn.com/${hash}`;
}
using System;
using System.Linq;

string GetCdnUrl(string hash) {
     int t =  hash.ToCharArray().Aggregate(31, (lastCode, character) => lastCode ^ (int)character);
 
     return $"https://t{t % 8}.rbxcdn.com/{hash}";
}
def get_cdn_url(hash)
  t = hash.codepoints.reduce(31) { |last_code, code| last_code ^ code }
  "https://t#{t % 8}.rbxcdn.com/#{hash}"
end
std::string getCdnUrl(const std::string& hash)
{
    if (hash.empty()) throw std::exception("Hash cannot be empty");

    int i = 31;

    for (char const& c : hash)
    {
        i ^= (int)c;
    }

    char buff[100];
    snprintf(buff, sizeof(buff), "https://t%d.rbxcdn.com/%s", i % 8, hash.c_str());

    return std::string(buff);
}
void getCdnUrl(char *hash, char *buffer) {
    int i = 31;
    int hashLength = strlen(hash);
    for (int j = 0; j < hashLength; j++) {
        i ^= (int)hash[j];
    }

    snprintf(buffer, 55, "https://t%d.rbxcdn.com/%s", i % 8, hash);
}
fn get_cdn_url(hash: &str) -> String {
    let t = hash.as_bytes().iter().fold(31, |last_code, code| {
        last_code ^ code
    });
    
    format!("https://t{}.rbxcdn.com/{}", t % 8, hash)
}
local function getCdnUrl(hash)
    local i = 31
    for _, code in utf8.codes(hash) do
        i = i ~ code
        -- for Lua 5.2 and Luau, use the following instead:
        -- i = bit32.bxor(i, code)
    end

    return string.format("https://t%d.rbxcdn.com/%s", i % 8, hash)
end
String getCdnUrl(String hash) {
    int i = 31;
    for (char character : hash.toCharArray()) {
        i ^= (int) character;
    }
        
    return String.format("https://t%d.rbxcdn.com/%s", i % 8, hash);
}
fun getCdnUrl(hash: String): String {
    var i = 31
    hash.forEach({ character: Char ->
    	i = i xor character.toInt()
    });
    
    return "https://t${i % 8}.rbxcdn.com/${hash}"
}
def get_cdn_url(hash : String): String
  t = hash.codepoints.reduce(31) { |last_code, code| last_code ^ code }
  "https://t#{t % 8}.rbxcdn.com/#{hash}"
end
let getCdnUrl (hash: string) =
    let t = hash |> Seq.fold (fun lastCode char -> lastCode ^^^ (int)char) 31
    
    $"https://t{t % 8}.rbxcdn.com/{hash}"
using <"fx/internals/com.string.extensions">

com::string getCdnUrl(const com::string& hash)
{
    if (hash.isNullOrEmpty()) throw new com::exception("Hash cannot be empty");

    int i = 31;

    // Could use an i32.xor here for faster math;
    hash.forEach(typeof(char), [=](char c) { i ^= c; });

    // Could use an i32.mod here for faster math;
    return com::string::format("https://t%d.rbxcdn.com/%s", i % 8, hash);
}
function get-cdn-url {
    param (
        [string] $hash
    )
    
    if ([string]::IsNullOrEmpty($hash)) { throw [System.ArgumentNullException]::new("hash"); }

    [int] $i = 31;

    foreach ($c in $hash.ToCharArray()) {
        $i = $i -bxor $c;
    }

    return "https://t$($i % 8).rbxcdn.com/$($hash)"
}
#include "node/node.h"

namespace rbx 
{
    void get_cdn_url(const char* hash, char* buffer)
    {
        int i = 31;
        int hashLength = strlen(hash);
        for (int j = 0; j < hashLength; j++) 
        {
            i ^= (int)hash[j];
        }
        snprintf(buffer, 55, "https://t%d.rbxcdn.com/%s", i % 8, hash);
    }


    const std::string get_cdn_url(const std::string& hash)
    {
        if (hash.empty()) throw std::runtime_error("Hash cannot be empty.");
        char* buffer;
        rbx::get_cdn_url(hash.c_str(), buffer);
        return std::string(buffer);
    }

    v8::Local<v8::String> get_cdn_url(v8::Isolate* isolate, const v8::Local<v8::String>& hash)
    {
        if (hash.IsEmpty()) throw std::runtime_error("The hash cannot be empty.");
        std::string url = rbx::get_cdn_url(*v8::String::Utf8Value(isolate, hash));
        return v8::String::NewFromUtf8(isolate, url.c_str()).ToLocalChecked();
    }

    enum ex_kind { kind_default, kind_range, kind_reference, kind_syntax, kind_type, kind_wasm_compiler, kind_wasm_link, kind_wasm_runtime };

    template <ex_kind T_Kind = ex_kind::kind_default>
    void throw_exception(v8::Isolate* isolate, const char* message)
    {
        v8::Local<v8::String> msg = v8::String::NewFromUtf8(isolate, message).ToLocalChecked();

        v8::Local<v8::Value> ex = v8::Exception::Error(msg);
        switch (T_Kind)
        {
        case kind_range:
            ex = v8::Exception::RangeError(msg);
            break;
        case kind_reference:
            ex = v8::Exception::ReferenceError(msg);
            break;
        case kind_syntax:
            ex = v8::Exception::SyntaxError(msg);
            break;
        case kind_type:
            ex = v8::Exception::TypeError(msg);
            break;
        case kind_wasm_compiler:
            ex = v8::Exception::WasmCompileError(msg);
            break;
        case kind_wasm_link:
            ex = v8::Exception::WasmLinkError(msg);
            break;
        case kind_wasm_runtime:
            ex = v8::Exception::WasmRuntimeError(msg);
            break;
        case kind_default:
        default:
            ex = v8::Exception::Error(msg);
            break;
        }

        isolate->ThrowException(ex);
    }

    void get_cdn_url(const v8::FunctionCallbackInfo<v8::Value>& args)
    {
        v8::Isolate* isolate = args.GetIsolate();

        if (args.Length() < 1) 
        {
            rbx::throw_exception<rbx::kind_type>(isolate, "The hash cannot be undefined");
            return;
        }

        if (!args[0]->IsString())
        {
            rbx::throw_exception<rbx::kind_type>(isolate, "The hash has to be of type string.");
            return;
        }

        args.GetReturnValue().Set(rbx::get_cdn_url(isolate, args[0].As<v8::String>()));
    }

    void Init(v8::Local<v8::Object> exports, v8::Local<v8::Object> module)
    {
        NODE_SET_METHOD(module, "exports", get_cdn_url);
    }

    NODE_MODULE(NODE_GYP_MODULE_NAME, Init);
}

/*
const addon = require("./build/Release/addon");

console.log(addon("HASH"));
*/
; nasm -felf64 cdn_hash.asm -o cdn_hash.o
; gcc -m64 -o cdn_hash cdn_hash.o -no-pie
; ./cdn_hash
extern printf, snprintf

section .text
    global main

get_cdn_url:
    push rdi
    push rsi
    push rdx
    push rcx
    push r8
    push rax
    
    ; rdi is the accumulator
    mov rdi, 31

    jmp .is_at_end

.xor_t:
    push rax
    ; rax is 8 bytes, get the first byte by AND'ing it by 255
    mov rax, [rax]
    and rax, 0xFF
    xor rdi, rax
    pop rax

    ; increment hash pointer
    inc rax

.is_at_end:
    cmp byte[rax], 0
    jne .xor_t

.fmt_cdn_url:
    mov rax, rdi
    xor rdx, rdx
    mov rsi, 8
    div rsi

    ; t
    mov rcx, rdx
    ; buffer
    lea rdi, cdn_url
    ; buffer size
    mov rsi, 55
    ; format
    lea rdx, url_fmt
    ; hash
    pop rax
    mov r8, rax
    push rax
    xor rax, rax
    call snprintf

    pop rax
    pop r8
    pop rcx
    pop rdx
    pop rsi
    pop rdi
    
    ret

main:
    lea rax, hash
    call get_cdn_url

    lea rdi, s_fmt
    lea rsi, cdn_url
    xor rax, rax
    call printf

    mov rax, 60
    mov rdi, 0
    syscall

section .data
    cdn_url: times 55 db 0
    s_fmt: db "%s", 10, 0
    url_fmt: db "https://t%d.rbxcdn.com/%s", 0
    hash: db "bbdb80c2b573bf222da3e92f5f148330", 0