-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathshortener.cpp
More file actions
293 lines (247 loc) · 8.06 KB
/
shortener.cpp
File metadata and controls
293 lines (247 loc) · 8.06 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
#include <napi.h>
#include <string>
#include <array>
#include <algorithm>
#include <openssl/evp.h>
static constexpr const char *ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
static constexpr int BASE = 62;
static constexpr int DEFAULT_OUTPUT_LENGTH = 8;
static constexpr int MIN_OUTPUT_LENGTH = 1;
static constexpr int MAX_OUTPUT_LENGTH = 22;
static constexpr size_t MAX_INPUT_SIZE = 65536;
static constexpr size_t SHA256_HASH_SIZE = 32;
static constexpr size_t HASH_PREFIX_BYTES = 8;
// Portable 128-bit unsigned integer (works on MSVC, GCC, Clang)
struct uint128_t
{
uint64_t hi;
uint64_t lo;
uint128_t() : hi(0), lo(0) {}
uint128_t(uint64_t h, uint64_t l) : hi(h), lo(l) {}
bool isZero() const { return hi == 0 && lo == 0; }
// Left shift by 8 bits
uint128_t shl8() const
{
return uint128_t((hi << 8) | (lo >> 56), lo << 8);
}
// OR with a byte
uint128_t orByte(unsigned char b) const
{
return uint128_t(hi, lo | b);
}
// Divide by divisor, return quotient and remainder
int divmod(int divisor)
{
// Split into high and low parts for division
// hi:lo / divisor
uint64_t rhi = hi % static_cast<uint64_t>(divisor);
uint64_t qhi = hi / static_cast<uint64_t>(divisor);
// Combine remainder with lo: (rhi << 64 + lo) / divisor
// Process in two 32-bit steps to avoid overflow
uint64_t mid = (rhi << 32) | (lo >> 32);
uint64_t qmid = mid / static_cast<uint64_t>(divisor);
uint64_t rmid = mid % static_cast<uint64_t>(divisor);
uint64_t low = (rmid << 32) | (lo & 0xFFFFFFFF);
uint64_t qlow = low / static_cast<uint64_t>(divisor);
int remainder = static_cast<int>(low % static_cast<uint64_t>(divisor));
hi = qhi;
lo = (qmid << 32) | qlow;
return remainder;
}
};
// Compute SHA-256 and return raw 32-byte digest
static bool sha256(const std::string &input, std::array<unsigned char, SHA256_HASH_SIZE> &out)
{
unsigned int hashLen = 0;
EVP_MD_CTX *ctx = EVP_MD_CTX_new();
if (!ctx)
return false;
if (EVP_DigestInit_ex(ctx, EVP_sha256(), nullptr) != 1 ||
EVP_DigestUpdate(ctx, input.c_str(), input.length()) != 1 ||
EVP_DigestFinal_ex(ctx, out.data(), &hashLen) != 1)
{
EVP_MD_CTX_free(ctx);
return false;
}
EVP_MD_CTX_free(ctx);
return true;
}
// Encode raw hash bytes to Base62 string of given length
// Uses first N bytes of hash as big-endian uint64_t
// Mathematically equivalent to the old hex-parsing approach:
// 16 hex chars → (num << 4) per char = 8 bytes → (num << 8) per byte
static std::string encodeBase62(const std::array<unsigned char, SHA256_HASH_SIZE> &hash, int length)
{
// For length <= 10, 8 bytes (64 bits) of entropy is sufficient
// For length > 10, use 16 bytes (128 bits) for more entropy
std::string encoded;
if (length <= 10)
{
uint64_t num = 0;
for (size_t i = 0; i < HASH_PREFIX_BYTES; i++)
{
num = (num << 8) | hash[i];
}
while (num > 0)
{
encoded.push_back(ALPHABET[num % BASE]);
num /= BASE;
}
}
else
{
// Use 16 bytes for longer outputs (up to 22 base62 chars)
uint128_t num;
for (size_t i = 0; i < 16; i++)
{
num = num.shl8().orByte(hash[i]);
}
while (!num.isZero())
{
int remainder = num.divmod(BASE);
encoded.push_back(ALPHABET[remainder]);
}
}
// Reverse since we built least-significant digit first
std::reverse(encoded.begin(), encoded.end());
// Pad front to ensure minimum length
while (static_cast<int>(encoded.size()) < length)
{
encoded.insert(encoded.begin(), ALPHABET[0]);
}
// Truncate from the end to the requested length (keep MSB prefix)
return encoded.substr(0, length);
}
// Parse and validate the optional length argument
static int parseLength(const Napi::CallbackInfo &info)
{
if (info.Length() >= 2)
{
if (!info[1].IsNumber())
{
Napi::TypeError::New(info.Env(), "Length must be a number")
.ThrowAsJavaScriptException();
return -1;
}
int length = info[1].As<Napi::Number>().Int32Value();
if (length < MIN_OUTPUT_LENGTH || length > MAX_OUTPUT_LENGTH)
{
Napi::RangeError::New(info.Env(),
"Length must be between " + std::to_string(MIN_OUTPUT_LENGTH) +
" and " + std::to_string(MAX_OUTPUT_LENGTH))
.ThrowAsJavaScriptException();
return -1;
}
return length;
}
return DEFAULT_OUTPUT_LENGTH;
}
// Validate input string argument and return it
static std::string parseInput(const Napi::CallbackInfo &info)
{
Napi::Env env = info.Env();
if (info.Length() < 1)
{
Napi::TypeError::New(env, "Wrong number of arguments")
.ThrowAsJavaScriptException();
return "";
}
if (!info[0].IsString())
{
Napi::TypeError::New(env, "Input must be a string")
.ThrowAsJavaScriptException();
return "";
}
std::string input = info[0].As<Napi::String>().Utf8Value();
if (input.empty())
{
Napi::TypeError::New(env, "Invalid URL")
.ThrowAsJavaScriptException();
return "";
}
if (input.size() > MAX_INPUT_SIZE)
{
Napi::RangeError::New(env, "Input exceeds maximum size of " +
std::to_string(MAX_INPUT_SIZE) + " bytes")
.ThrowAsJavaScriptException();
return "";
}
return input;
}
// Synchronous gen(input, length?)
Napi::Value Gen(const Napi::CallbackInfo &info)
{
Napi::Env env = info.Env();
std::string input = parseInput(info);
if (env.IsExceptionPending())
return env.Undefined();
int length = parseLength(info);
if (env.IsExceptionPending())
return env.Undefined();
std::array<unsigned char, SHA256_HASH_SIZE> hash;
if (!sha256(input, hash))
{
Napi::Error::New(env, "SHA-256 computation failed")
.ThrowAsJavaScriptException();
return env.Undefined();
}
std::string result = encodeBase62(hash, length);
return Napi::String::New(env, result);
}
// Async worker for genAsync(input, length?)
class ShortenerAsyncWorker : public Napi::AsyncWorker
{
public:
ShortenerAsyncWorker(Napi::Env env, const std::string &input, int length)
: Napi::AsyncWorker(env),
deferred_(Napi::Promise::Deferred::New(env)),
input_(input),
length_(length) {}
Napi::Promise Promise() { return deferred_.Promise(); }
protected:
void Execute() override
{
if (!sha256(input_, hash_))
{
SetError("SHA-256 computation failed");
return;
}
result_ = encodeBase62(hash_, length_);
}
void OnOK() override
{
deferred_.Resolve(Napi::String::New(Env(), result_));
}
void OnError(const Napi::Error &error) override
{
deferred_.Reject(error.Value());
}
private:
Napi::Promise::Deferred deferred_;
std::string input_;
int length_;
std::array<unsigned char, SHA256_HASH_SIZE> hash_;
std::string result_;
};
// Async genAsync(input, length?)
Napi::Value GenAsync(const Napi::CallbackInfo &info)
{
Napi::Env env = info.Env();
std::string input = parseInput(info);
if (env.IsExceptionPending())
return env.Undefined();
int length = parseLength(info);
if (env.IsExceptionPending())
return env.Undefined();
auto *worker = new ShortenerAsyncWorker(env, input, length);
Napi::Promise promise = worker->Promise();
worker->Queue();
return promise;
}
Napi::Object Init(Napi::Env env, Napi::Object exports)
{
exports.Set("gen", Napi::Function::New(env, Gen, "gen"));
exports.Set("genAsync", Napi::Function::New(env, GenAsync, "genAsync"));
return exports;
}
NODE_API_MODULE(shortener, Init)