js/src/vm/SavedStacks.cpp

Sat, 03 Jan 2015 20:18:00 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Sat, 03 Jan 2015 20:18:00 +0100
branch
TOR_BUG_3246
changeset 7
129ffea94266
permissions
-rw-r--r--

Conditionally enable double key logic according to:
private browsing mode or privacy.thirdparty.isolate preference and
implement in GetCookieStringCommon and FindCookie where it counts...
With some reservations of how to convince FindCookie users to test
condition and pass a nullptr when disabling double key logic.

michael@0 1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
michael@0 2 * vim: set ts=8 sts=4 et sw=4 tw=99:
michael@0 3 * This Source Code Form is subject to the terms of the Mozilla Public
michael@0 4 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 6
michael@0 7
michael@0 8 #include "vm/SavedStacks.h"
michael@0 9
michael@0 10 #include "jscompartment.h"
michael@0 11 #include "jsnum.h"
michael@0 12
michael@0 13 #include "vm/GlobalObject.h"
michael@0 14 #include "vm/StringBuffer.h"
michael@0 15
michael@0 16 #include "jsobjinlines.h"
michael@0 17
michael@0 18 using mozilla::AddToHash;
michael@0 19 using mozilla::HashString;
michael@0 20
michael@0 21 namespace js {
michael@0 22
michael@0 23 /* static */ HashNumber
michael@0 24 SavedFrame::HashPolicy::hash(const Lookup &lookup)
michael@0 25 {
michael@0 26 return AddToHash(HashString(lookup.source->chars(), lookup.source->length()),
michael@0 27 lookup.line,
michael@0 28 lookup.column,
michael@0 29 lookup.functionDisplayName,
michael@0 30 SavedFramePtrHasher::hash(lookup.parent),
michael@0 31 JSPrincipalsPtrHasher::hash(lookup.principals));
michael@0 32 }
michael@0 33
michael@0 34 /* static */ bool
michael@0 35 SavedFrame::HashPolicy::match(SavedFrame *existing, const Lookup &lookup)
michael@0 36 {
michael@0 37 if (existing->getLine() != lookup.line)
michael@0 38 return false;
michael@0 39
michael@0 40 if (existing->getColumn() != lookup.column)
michael@0 41 return false;
michael@0 42
michael@0 43 if (existing->getParent() != lookup.parent)
michael@0 44 return false;
michael@0 45
michael@0 46 if (existing->getPrincipals() != lookup.principals)
michael@0 47 return false;
michael@0 48
michael@0 49 JSAtom *source = existing->getSource();
michael@0 50 if (source->length() != lookup.source->length())
michael@0 51 return false;
michael@0 52 if (source != lookup.source)
michael@0 53 return false;
michael@0 54
michael@0 55 JSAtom *functionDisplayName = existing->getFunctionDisplayName();
michael@0 56 if (functionDisplayName) {
michael@0 57 if (!lookup.functionDisplayName)
michael@0 58 return false;
michael@0 59 if (functionDisplayName->length() != lookup.functionDisplayName->length())
michael@0 60 return false;
michael@0 61 if (0 != CompareAtoms(functionDisplayName, lookup.functionDisplayName))
michael@0 62 return false;
michael@0 63 } else if (lookup.functionDisplayName) {
michael@0 64 return false;
michael@0 65 }
michael@0 66
michael@0 67 return true;
michael@0 68 }
michael@0 69
michael@0 70 /* static */ void
michael@0 71 SavedFrame::HashPolicy::rekey(Key &key, const Key &newKey)
michael@0 72 {
michael@0 73 key = newKey;
michael@0 74 }
michael@0 75
michael@0 76 /* static */ const Class SavedFrame::class_ = {
michael@0 77 "SavedFrame",
michael@0 78 JSCLASS_HAS_PRIVATE | JSCLASS_IMPLEMENTS_BARRIERS |
michael@0 79 JSCLASS_HAS_RESERVED_SLOTS(SavedFrame::JSSLOT_COUNT),
michael@0 80
michael@0 81 JS_PropertyStub, // addProperty
michael@0 82 JS_DeletePropertyStub, // delProperty
michael@0 83 JS_PropertyStub, // getProperty
michael@0 84 JS_StrictPropertyStub, // setProperty
michael@0 85 JS_EnumerateStub, // enumerate
michael@0 86 JS_ResolveStub, // resolve
michael@0 87 JS_ConvertStub, // convert
michael@0 88
michael@0 89 SavedFrame::finalize // finalize
michael@0 90 };
michael@0 91
michael@0 92 /* static */ void
michael@0 93 SavedFrame::finalize(FreeOp *fop, JSObject *obj)
michael@0 94 {
michael@0 95 JSPrincipals *p = obj->as<SavedFrame>().getPrincipals();
michael@0 96 if (p) {
michael@0 97 JSRuntime *rt = obj->runtimeFromMainThread();
michael@0 98 JS_DropPrincipals(rt, p);
michael@0 99 }
michael@0 100 }
michael@0 101
michael@0 102 JSAtom *
michael@0 103 SavedFrame::getSource()
michael@0 104 {
michael@0 105 const Value &v = getReservedSlot(JSSLOT_SOURCE);
michael@0 106 JSString *s = v.toString();
michael@0 107 return &s->asAtom();
michael@0 108 }
michael@0 109
michael@0 110 size_t
michael@0 111 SavedFrame::getLine()
michael@0 112 {
michael@0 113 const Value &v = getReservedSlot(JSSLOT_LINE);
michael@0 114 return v.toInt32();
michael@0 115 }
michael@0 116
michael@0 117 size_t
michael@0 118 SavedFrame::getColumn()
michael@0 119 {
michael@0 120 const Value &v = getReservedSlot(JSSLOT_COLUMN);
michael@0 121 return v.toInt32();
michael@0 122 }
michael@0 123
michael@0 124 JSAtom *
michael@0 125 SavedFrame::getFunctionDisplayName()
michael@0 126 {
michael@0 127 const Value &v = getReservedSlot(JSSLOT_FUNCTIONDISPLAYNAME);
michael@0 128 if (v.isNull())
michael@0 129 return nullptr;
michael@0 130 JSString *s = v.toString();
michael@0 131 return &s->asAtom();
michael@0 132 }
michael@0 133
michael@0 134 SavedFrame *
michael@0 135 SavedFrame::getParent()
michael@0 136 {
michael@0 137 const Value &v = getReservedSlot(JSSLOT_PARENT);
michael@0 138 return v.isObject() ? &v.toObject().as<SavedFrame>() : nullptr;
michael@0 139 }
michael@0 140
michael@0 141 JSPrincipals *
michael@0 142 SavedFrame::getPrincipals()
michael@0 143 {
michael@0 144 const Value &v = getReservedSlot(JSSLOT_PRINCIPALS);
michael@0 145 if (v.isUndefined())
michael@0 146 return nullptr;
michael@0 147 return static_cast<JSPrincipals *>(v.toPrivate());
michael@0 148 }
michael@0 149
michael@0 150 void
michael@0 151 SavedFrame::initFromLookup(Lookup &lookup)
michael@0 152 {
michael@0 153 JS_ASSERT(lookup.source);
michael@0 154 JS_ASSERT(getReservedSlot(JSSLOT_SOURCE).isUndefined());
michael@0 155 setReservedSlot(JSSLOT_SOURCE, StringValue(lookup.source));
michael@0 156
michael@0 157 setReservedSlot(JSSLOT_LINE, NumberValue(lookup.line));
michael@0 158 setReservedSlot(JSSLOT_COLUMN, NumberValue(lookup.column));
michael@0 159 setReservedSlot(JSSLOT_FUNCTIONDISPLAYNAME,
michael@0 160 lookup.functionDisplayName
michael@0 161 ? StringValue(lookup.functionDisplayName)
michael@0 162 : NullValue());
michael@0 163 setReservedSlot(JSSLOT_PARENT, ObjectOrNullValue(lookup.parent));
michael@0 164 setReservedSlot(JSSLOT_PRIVATE_PARENT, PrivateValue(lookup.parent));
michael@0 165
michael@0 166 JS_ASSERT(getReservedSlot(JSSLOT_PRINCIPALS).isUndefined());
michael@0 167 if (lookup.principals)
michael@0 168 JS_HoldPrincipals(lookup.principals);
michael@0 169 setReservedSlot(JSSLOT_PRINCIPALS, PrivateValue(lookup.principals));
michael@0 170 }
michael@0 171
michael@0 172 bool
michael@0 173 SavedFrame::parentMoved()
michael@0 174 {
michael@0 175 const Value &v = getReservedSlot(JSSLOT_PRIVATE_PARENT);
michael@0 176 JSObject *p = static_cast<JSObject *>(v.toPrivate());
michael@0 177 return p == getParent();
michael@0 178 }
michael@0 179
michael@0 180 void
michael@0 181 SavedFrame::updatePrivateParent()
michael@0 182 {
michael@0 183 setReservedSlot(JSSLOT_PRIVATE_PARENT, PrivateValue(getParent()));
michael@0 184 }
michael@0 185
michael@0 186 bool
michael@0 187 SavedFrame::isSelfHosted()
michael@0 188 {
michael@0 189 JSAtom *source = getSource();
michael@0 190 return StringEqualsAscii(source, "self-hosted");
michael@0 191 }
michael@0 192
michael@0 193 /* static */ bool
michael@0 194 SavedFrame::construct(JSContext *cx, unsigned argc, Value *vp)
michael@0 195 {
michael@0 196 JS_ReportErrorNumber(cx, js_GetErrorMessage, nullptr, JSMSG_NO_CONSTRUCTOR,
michael@0 197 "SavedFrame");
michael@0 198 return false;
michael@0 199 }
michael@0 200
michael@0 201 /* static */ SavedFrame *
michael@0 202 SavedFrame::checkThis(JSContext *cx, CallArgs &args, const char *fnName)
michael@0 203 {
michael@0 204 const Value &thisValue = args.thisv();
michael@0 205
michael@0 206 if (!thisValue.isObject()) {
michael@0 207 JS_ReportErrorNumber(cx, js_GetErrorMessage, nullptr, JSMSG_NOT_NONNULL_OBJECT);
michael@0 208 return nullptr;
michael@0 209 }
michael@0 210
michael@0 211 JSObject &thisObject = thisValue.toObject();
michael@0 212 if (!thisObject.is<SavedFrame>()) {
michael@0 213 JS_ReportErrorNumber(cx, js_GetErrorMessage, nullptr, JSMSG_INCOMPATIBLE_PROTO,
michael@0 214 SavedFrame::class_.name, fnName, thisObject.getClass()->name);
michael@0 215 return nullptr;
michael@0 216 }
michael@0 217
michael@0 218 // Check for SavedFrame.prototype, which has the same class as SavedFrame
michael@0 219 // instances, however doesn't actually represent a captured stack frame. It
michael@0 220 // is the only object that is<SavedFrame>() but doesn't have a source.
michael@0 221 if (thisObject.getReservedSlot(JSSLOT_SOURCE).isNull()) {
michael@0 222 JS_ReportErrorNumber(cx, js_GetErrorMessage, nullptr, JSMSG_INCOMPATIBLE_PROTO,
michael@0 223 SavedFrame::class_.name, fnName, "prototype object");
michael@0 224 return nullptr;
michael@0 225 }
michael@0 226
michael@0 227 return &thisObject.as<SavedFrame>();
michael@0 228 }
michael@0 229
michael@0 230 // Get the SavedFrame * from the current this value and handle any errors that
michael@0 231 // might occur therein.
michael@0 232 //
michael@0 233 // These parameters must already exist when calling this macro:
michael@0 234 // - JSContext *cx
michael@0 235 // - unsigned argc
michael@0 236 // - Value *vp
michael@0 237 // - const char *fnName
michael@0 238 // These parameters will be defined after calling this macro:
michael@0 239 // - CallArgs args
michael@0 240 // - Rooted<SavedFrame *> frame (will be non-null)
michael@0 241 #define THIS_SAVEDFRAME(cx, argc, vp, fnName, args, frame) \
michael@0 242 CallArgs args = CallArgsFromVp(argc, vp); \
michael@0 243 Rooted<SavedFrame *> frame(cx, checkThis(cx, args, fnName)); \
michael@0 244 if (!frame) \
michael@0 245 return false
michael@0 246
michael@0 247 /* static */ bool
michael@0 248 SavedFrame::sourceProperty(JSContext *cx, unsigned argc, Value *vp)
michael@0 249 {
michael@0 250 THIS_SAVEDFRAME(cx, argc, vp, "(get source)", args, frame);
michael@0 251 args.rval().setString(frame->getSource());
michael@0 252 return true;
michael@0 253 }
michael@0 254
michael@0 255 /* static */ bool
michael@0 256 SavedFrame::lineProperty(JSContext *cx, unsigned argc, Value *vp)
michael@0 257 {
michael@0 258 THIS_SAVEDFRAME(cx, argc, vp, "(get line)", args, frame);
michael@0 259 uint32_t line = frame->getLine();
michael@0 260 args.rval().setNumber(line);
michael@0 261 return true;
michael@0 262 }
michael@0 263
michael@0 264 /* static */ bool
michael@0 265 SavedFrame::columnProperty(JSContext *cx, unsigned argc, Value *vp)
michael@0 266 {
michael@0 267 THIS_SAVEDFRAME(cx, argc, vp, "(get column)", args, frame);
michael@0 268 uint32_t column = frame->getColumn();
michael@0 269 args.rval().setNumber(column);
michael@0 270 return true;
michael@0 271 }
michael@0 272
michael@0 273 /* static */ bool
michael@0 274 SavedFrame::functionDisplayNameProperty(JSContext *cx, unsigned argc, Value *vp)
michael@0 275 {
michael@0 276 THIS_SAVEDFRAME(cx, argc, vp, "(get functionDisplayName)", args, frame);
michael@0 277 RootedAtom name(cx, frame->getFunctionDisplayName());
michael@0 278 if (name)
michael@0 279 args.rval().setString(name);
michael@0 280 else
michael@0 281 args.rval().setNull();
michael@0 282 return true;
michael@0 283 }
michael@0 284
michael@0 285 /* static */ bool
michael@0 286 SavedFrame::parentProperty(JSContext *cx, unsigned argc, Value *vp)
michael@0 287 {
michael@0 288 THIS_SAVEDFRAME(cx, argc, vp, "(get parent)", args, frame);
michael@0 289 JSSubsumesOp subsumes = cx->runtime()->securityCallbacks->subsumes;
michael@0 290 JSPrincipals *principals = cx->compartment()->principals;
michael@0 291
michael@0 292 do
michael@0 293 frame = frame->getParent();
michael@0 294 while (frame && principals && subsumes &&
michael@0 295 !subsumes(principals, frame->getPrincipals()));
michael@0 296
michael@0 297 args.rval().setObjectOrNull(frame);
michael@0 298 return true;
michael@0 299 }
michael@0 300
michael@0 301 /* static */ const JSPropertySpec SavedFrame::properties[] = {
michael@0 302 JS_PSG("source", SavedFrame::sourceProperty, 0),
michael@0 303 JS_PSG("line", SavedFrame::lineProperty, 0),
michael@0 304 JS_PSG("column", SavedFrame::columnProperty, 0),
michael@0 305 JS_PSG("functionDisplayName", SavedFrame::functionDisplayNameProperty, 0),
michael@0 306 JS_PSG("parent", SavedFrame::parentProperty, 0),
michael@0 307 JS_PS_END
michael@0 308 };
michael@0 309
michael@0 310 /* static */ bool
michael@0 311 SavedFrame::toStringMethod(JSContext *cx, unsigned argc, Value *vp)
michael@0 312 {
michael@0 313 THIS_SAVEDFRAME(cx, argc, vp, "toString", args, frame);
michael@0 314 StringBuffer sb(cx);
michael@0 315 JSSubsumesOp subsumes = cx->runtime()->securityCallbacks->subsumes;
michael@0 316 JSPrincipals *principals = cx->compartment()->principals;
michael@0 317
michael@0 318 do {
michael@0 319 if (principals && subsumes && !subsumes(principals, frame->getPrincipals()))
michael@0 320 continue;
michael@0 321 if (frame->isSelfHosted())
michael@0 322 continue;
michael@0 323
michael@0 324 RootedAtom name(cx, frame->getFunctionDisplayName());
michael@0 325 if ((name && !sb.append(name))
michael@0 326 || !sb.append('@')
michael@0 327 || !sb.append(frame->getSource())
michael@0 328 || !sb.append(':')
michael@0 329 || !NumberValueToStringBuffer(cx, NumberValue(frame->getLine()), sb)
michael@0 330 || !sb.append(':')
michael@0 331 || !NumberValueToStringBuffer(cx, NumberValue(frame->getColumn()), sb)
michael@0 332 || !sb.append('\n')) {
michael@0 333 return false;
michael@0 334 }
michael@0 335 } while ((frame = frame->getParent()));
michael@0 336
michael@0 337 args.rval().setString(sb.finishString());
michael@0 338 return true;
michael@0 339 }
michael@0 340
michael@0 341 /* static */ const JSFunctionSpec SavedFrame::methods[] = {
michael@0 342 JS_FN("toString", SavedFrame::toStringMethod, 0, 0),
michael@0 343 JS_FS_END
michael@0 344 };
michael@0 345
michael@0 346 bool
michael@0 347 SavedStacks::init()
michael@0 348 {
michael@0 349 return frames.init();
michael@0 350 }
michael@0 351
michael@0 352 bool
michael@0 353 SavedStacks::saveCurrentStack(JSContext *cx, MutableHandle<SavedFrame*> frame)
michael@0 354 {
michael@0 355 JS_ASSERT(initialized());
michael@0 356 JS_ASSERT(&cx->compartment()->savedStacks() == this);
michael@0 357
michael@0 358 ScriptFrameIter iter(cx);
michael@0 359 return insertFrames(cx, iter, frame);
michael@0 360 }
michael@0 361
michael@0 362 void
michael@0 363 SavedStacks::sweep(JSRuntime *rt)
michael@0 364 {
michael@0 365 if (frames.initialized()) {
michael@0 366 for (SavedFrame::Set::Enum e(frames); !e.empty(); e.popFront()) {
michael@0 367 JSObject *obj = static_cast<JSObject *>(e.front());
michael@0 368 JSObject *temp = obj;
michael@0 369
michael@0 370 if (IsObjectAboutToBeFinalized(&obj)) {
michael@0 371 e.removeFront();
michael@0 372 } else {
michael@0 373 SavedFrame *frame = &obj->as<SavedFrame>();
michael@0 374 bool parentMoved = frame->parentMoved();
michael@0 375
michael@0 376 if (parentMoved) {
michael@0 377 frame->updatePrivateParent();
michael@0 378 }
michael@0 379
michael@0 380 if (obj != temp || parentMoved) {
michael@0 381 Rooted<SavedFrame*> parent(rt, frame->getParent());
michael@0 382 e.rekeyFront(SavedFrame::Lookup(frame->getSource(),
michael@0 383 frame->getLine(),
michael@0 384 frame->getColumn(),
michael@0 385 frame->getFunctionDisplayName(),
michael@0 386 parent,
michael@0 387 frame->getPrincipals()),
michael@0 388 frame);
michael@0 389 }
michael@0 390 }
michael@0 391 }
michael@0 392 }
michael@0 393
michael@0 394 if (savedFrameProto && IsObjectAboutToBeFinalized(&savedFrameProto)) {
michael@0 395 savedFrameProto = nullptr;
michael@0 396 }
michael@0 397 }
michael@0 398
michael@0 399 uint32_t
michael@0 400 SavedStacks::count()
michael@0 401 {
michael@0 402 JS_ASSERT(initialized());
michael@0 403 return frames.count();
michael@0 404 }
michael@0 405
michael@0 406 void
michael@0 407 SavedStacks::clear()
michael@0 408 {
michael@0 409 frames.clear();
michael@0 410 }
michael@0 411
michael@0 412 size_t
michael@0 413 SavedStacks::sizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf)
michael@0 414 {
michael@0 415 return frames.sizeOfExcludingThis(mallocSizeOf);
michael@0 416 }
michael@0 417
michael@0 418 bool
michael@0 419 SavedStacks::insertFrames(JSContext *cx, ScriptFrameIter &iter, MutableHandle<SavedFrame*> frame)
michael@0 420 {
michael@0 421 if (iter.done()) {
michael@0 422 frame.set(nullptr);
michael@0 423 return true;
michael@0 424 }
michael@0 425
michael@0 426 ScriptFrameIter thisFrame(iter);
michael@0 427 Rooted<SavedFrame*> parentFrame(cx);
michael@0 428 if (!insertFrames(cx, ++iter, &parentFrame))
michael@0 429 return false;
michael@0 430
michael@0 431 RootedScript script(cx, thisFrame.script());
michael@0 432 RootedFunction callee(cx, thisFrame.maybeCallee());
michael@0 433 const char *filename = script->filename();
michael@0 434 RootedAtom source(cx, Atomize(cx, filename, strlen(filename)));
michael@0 435 if (!source)
michael@0 436 return false;
michael@0 437 uint32_t column;
michael@0 438 uint32_t line = PCToLineNumber(script, thisFrame.pc(), &column);
michael@0 439
michael@0 440 SavedFrame::Lookup lookup(source,
michael@0 441 line,
michael@0 442 column,
michael@0 443 callee ? callee->displayAtom() : nullptr,
michael@0 444 parentFrame,
michael@0 445 thisFrame.compartment()->principals);
michael@0 446
michael@0 447 frame.set(getOrCreateSavedFrame(cx, lookup));
michael@0 448 return frame.address() != nullptr;
michael@0 449 }
michael@0 450
michael@0 451 SavedFrame *
michael@0 452 SavedStacks::getOrCreateSavedFrame(JSContext *cx, SavedFrame::Lookup &lookup)
michael@0 453 {
michael@0 454 SavedFrame::Set::AddPtr p = frames.lookupForAdd(lookup);
michael@0 455 if (p)
michael@0 456 return *p;
michael@0 457
michael@0 458 Rooted<SavedFrame *> frame(cx, createFrameFromLookup(cx, lookup));
michael@0 459 if (!frame)
michael@0 460 return nullptr;
michael@0 461
michael@0 462 if (!frames.relookupOrAdd(p, lookup, frame))
michael@0 463 return nullptr;
michael@0 464
michael@0 465 return frame;
michael@0 466 }
michael@0 467
michael@0 468 JSObject *
michael@0 469 SavedStacks::getOrCreateSavedFramePrototype(JSContext *cx)
michael@0 470 {
michael@0 471 if (savedFrameProto)
michael@0 472 return savedFrameProto;
michael@0 473
michael@0 474 Rooted<GlobalObject *> global(cx, cx->compartment()->maybeGlobal());
michael@0 475 if (!global)
michael@0 476 return nullptr;
michael@0 477
michael@0 478 savedFrameProto = js_InitClass(cx, global, global->getOrCreateObjectPrototype(cx),
michael@0 479 &SavedFrame::class_, SavedFrame::construct, 0,
michael@0 480 SavedFrame::properties, SavedFrame::methods, nullptr, nullptr);
michael@0 481 // The only object with the SavedFrame::class_ that doesn't have a source
michael@0 482 // should be the prototype.
michael@0 483 savedFrameProto->setReservedSlot(SavedFrame::JSSLOT_SOURCE, NullValue());
michael@0 484 return savedFrameProto;
michael@0 485 }
michael@0 486
michael@0 487 SavedFrame *
michael@0 488 SavedStacks::createFrameFromLookup(JSContext *cx, SavedFrame::Lookup &lookup)
michael@0 489 {
michael@0 490 RootedObject proto(cx, getOrCreateSavedFramePrototype(cx));
michael@0 491 if (!proto)
michael@0 492 return nullptr;
michael@0 493
michael@0 494 JS_ASSERT(proto->compartment() == cx->compartment());
michael@0 495
michael@0 496 RootedObject global(cx, cx->compartment()->maybeGlobal());
michael@0 497 if (!global)
michael@0 498 return nullptr;
michael@0 499
michael@0 500 JS_ASSERT(global->compartment() == cx->compartment());
michael@0 501
michael@0 502 RootedObject frameObj(cx, NewObjectWithGivenProto(cx, &SavedFrame::class_, proto, global));
michael@0 503 if (!frameObj)
michael@0 504 return nullptr;
michael@0 505
michael@0 506 SavedFrame &f = frameObj->as<SavedFrame>();
michael@0 507 f.initFromLookup(lookup);
michael@0 508
michael@0 509 return &f;
michael@0 510 }
michael@0 511
michael@0 512 } /* namespace js */

mercurial