content/base/test/test_fileapi.html

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

michael@0 1 <!DOCTYPE HTML>
michael@0 2 <html>
michael@0 3 <head>
michael@0 4 <!--
michael@0 5 https://bugzilla.mozilla.org/show_bug.cgi?id=414796
michael@0 6 -->
michael@0 7 <title>Test for Bug 414796</title>
michael@0 8 <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
michael@0 9 <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
michael@0 10 </head>
michael@0 11
michael@0 12 <body>
michael@0 13 <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=414796">Mozilla Bug 414796</a>
michael@0 14 <p id="display">
michael@0 15 <input id="fileList" type="file"></input>
michael@0 16 </p>
michael@0 17 <div id="content" style="display: none">
michael@0 18 </div>
michael@0 19
michael@0 20 <pre id="test">
michael@0 21 <script class="testbody" type="text/javascript">
michael@0 22
michael@0 23 // File constructors should not work from non-chrome code
michael@0 24 try {
michael@0 25 var file = File("/etc/passwd");
michael@0 26 ok(false, "Did not throw on unprivileged attempt to construct a File");
michael@0 27 } catch (e) {
michael@0 28 ok(true, "Threw on an unprivileged attempt to construct a File");
michael@0 29 }
michael@0 30
michael@0 31 const minFileSize = 20000;
michael@0 32 var fileNum = 1;
michael@0 33 var testRanCounter = 0;
michael@0 34 var expectedTestCount = 0;
michael@0 35 SimpleTest.waitForExplicitFinish();
michael@0 36
michael@0 37 is(FileReader.EMPTY, 0, "correct EMPTY value");
michael@0 38 is(FileReader.LOADING, 1, "correct LOADING value");
michael@0 39 is(FileReader.DONE, 2, "correct DONE value");
michael@0 40
michael@0 41 // Create strings containing data we'll test with. We'll want long
michael@0 42 // strings to ensure they span multiple buffers while loading
michael@0 43 var testTextData = "asd b\tlah\u1234w\u00a0r";
michael@0 44 while (testTextData.length < minFileSize) {
michael@0 45 testTextData = testTextData + testTextData;
michael@0 46 }
michael@0 47
michael@0 48 var testASCIIData = "abcdef 123456\n";
michael@0 49 while (testASCIIData.length < minFileSize) {
michael@0 50 testASCIIData = testASCIIData + testASCIIData;
michael@0 51 }
michael@0 52
michael@0 53 var testBinaryData = "";
michael@0 54 for (var i = 0; i < 256; i++) {
michael@0 55 testBinaryData += String.fromCharCode(i);
michael@0 56 }
michael@0 57 while (testBinaryData.length < minFileSize) {
michael@0 58 testBinaryData = testBinaryData + testBinaryData;
michael@0 59 }
michael@0 60
michael@0 61
michael@0 62 //Set up files for testing
michael@0 63 var asciiFile = createFileWithData(testASCIIData);
michael@0 64 var binaryFile = createFileWithData(testBinaryData);
michael@0 65
michael@0 66 var fileList = document.getElementById('fileList');
michael@0 67 SpecialPowers.wrap(fileList).value = "/none/existing/path/fileAPI/testing";
michael@0 68 var nonExistingFile = fileList.files[0];
michael@0 69
michael@0 70 // Test that plain reading works and fires events as expected, both
michael@0 71 // for text and binary reading
michael@0 72
michael@0 73 var onloadHasRunText = false;
michael@0 74 var onloadStartHasRunText = false;
michael@0 75 r = new FileReader();
michael@0 76 is(r.readyState, FileReader.EMPTY, "correct initial text readyState");
michael@0 77 r.onload = getLoadHandler(testASCIIData, testASCIIData.length, "plain reading");
michael@0 78 r.addEventListener("load", function() { onloadHasRunText = true }, false);
michael@0 79 r.addEventListener("loadstart", function() { onloadStartHasRunText = true }, false);
michael@0 80 r.readAsText(asciiFile);
michael@0 81 is(r.readyState, FileReader.LOADING, "correct loading text readyState");
michael@0 82 is(onloadHasRunText, false, "text loading must be async");
michael@0 83 is(onloadStartHasRunText, true, "text loadstart should fire sync");
michael@0 84 expectedTestCount++;
michael@0 85
michael@0 86 var onloadHasRunBinary = false;
michael@0 87 var onloadStartHasRunBinary = false;
michael@0 88 r = new FileReader();
michael@0 89 is(r.readyState, FileReader.EMPTY, "correct initial binary readyState");
michael@0 90 r.addEventListener("load", function() { onloadHasRunBinary = true }, false);
michael@0 91 r.addEventListener("loadstart", function() { onloadStartHasRunBinary = true }, false);
michael@0 92 r.readAsBinaryString(binaryFile);
michael@0 93 r.onload = getLoadHandler(testBinaryData, testBinaryData.length, "binary reading");
michael@0 94 is(r.readyState, FileReader.LOADING, "correct loading binary readyState");
michael@0 95 is(onloadHasRunBinary, false, "binary loading must be async");
michael@0 96 is(onloadStartHasRunBinary, true, "binary loadstart should fire sync");
michael@0 97 expectedTestCount++;
michael@0 98
michael@0 99 var onloadHasRunArrayBuffer = false;
michael@0 100 var onloadStartHasRunArrayBuffer = false;
michael@0 101 r = new FileReader();
michael@0 102 is(r.readyState, FileReader.EMPTY, "correct initial arrayBuffer readyState");
michael@0 103 r.addEventListener("load", function() { onloadHasRunArrayBuffer = true }, false);
michael@0 104 r.addEventListener("loadstart", function() { onloadStartHasRunArrayBuffer = true }, false);
michael@0 105 r.readAsArrayBuffer(binaryFile);
michael@0 106 r.onload = getLoadHandlerForArrayBuffer(testBinaryData, testBinaryData.length, "array buffer reading");
michael@0 107 is(r.readyState, FileReader.LOADING, "correct loading arrayBuffer readyState");
michael@0 108 is(onloadHasRunArrayBuffer, false, "arrayBuffer loading must be async");
michael@0 109 is(onloadStartHasRunArrayBuffer, true, "arrayBuffer loadstart should fire sync");
michael@0 110 expectedTestCount++;
michael@0 111
michael@0 112 // Test a variety of encodings, and make sure they work properly
michael@0 113 r = new FileReader();
michael@0 114 r.onload = getLoadHandler(testASCIIData, testASCIIData.length, "no encoding reading");
michael@0 115 r.readAsText(asciiFile, "");
michael@0 116 expectedTestCount++;
michael@0 117
michael@0 118 r = new FileReader();
michael@0 119 r.onload = getLoadHandler(testASCIIData, testASCIIData.length, "iso8859 reading");
michael@0 120 r.readAsText(asciiFile, "iso-8859-1");
michael@0 121 expectedTestCount++;
michael@0 122
michael@0 123 r = new FileReader();
michael@0 124 r.onload = getLoadHandler(testTextData,
michael@0 125 convertToUTF8(testTextData).length,
michael@0 126 "utf8 reading");
michael@0 127 r.readAsText(createFileWithData(convertToUTF8(testTextData)), "utf8");
michael@0 128 expectedTestCount++;
michael@0 129
michael@0 130 r = new FileReader();
michael@0 131 r.readAsText(createFileWithData(convertToUTF16(testTextData)), "utf-16");
michael@0 132 r.onload = getLoadHandler(testTextData,
michael@0 133 convertToUTF16(testTextData).length,
michael@0 134 "utf16 reading");
michael@0 135 expectedTestCount++;
michael@0 136
michael@0 137 // Test get result without reading
michael@0 138 r = new FileReader();
michael@0 139 is(r.readyState, FileReader.EMPTY,
michael@0 140 "readyState in test reader get result without reading");
michael@0 141 is(r.error, null,
michael@0 142 "no error in test reader get result without reading");
michael@0 143 is(r.result, null,
michael@0 144 "result in test reader get result without reading");
michael@0 145
michael@0 146 // Test loading an empty file works (and doesn't crash!)
michael@0 147 var emptyFile = createFileWithData("");
michael@0 148 dump("hello nurse");
michael@0 149 r = new FileReader();
michael@0 150 r.onload = getLoadHandler("", 0, "empty no encoding reading");
michael@0 151 r.readAsText(emptyFile, "");
michael@0 152 expectedTestCount++;
michael@0 153
michael@0 154 r = new FileReader();
michael@0 155 r.onload = getLoadHandler("", 0, "empty utf8 reading");
michael@0 156 r.readAsText(emptyFile, "utf8");
michael@0 157 expectedTestCount++;
michael@0 158
michael@0 159 r = new FileReader();
michael@0 160 r.onload = getLoadHandler("", 0, "empty utf16 reading");
michael@0 161 r.readAsText(emptyFile, "utf-16");
michael@0 162 expectedTestCount++;
michael@0 163
michael@0 164 r = new FileReader();
michael@0 165 r.onload = getLoadHandler("", 0, "empty binary string reading");
michael@0 166 r.readAsBinaryString(emptyFile);
michael@0 167 expectedTestCount++;
michael@0 168
michael@0 169 r = new FileReader();
michael@0 170 r.onload = getLoadHandlerForArrayBuffer("", 0, "empty array buffer reading");
michael@0 171 r.readAsArrayBuffer(emptyFile);
michael@0 172 expectedTestCount++;
michael@0 173
michael@0 174 r = new FileReader();
michael@0 175 r.onload = getLoadHandler(convertToDataURL(""), 0, "empt binary string reading");
michael@0 176 r.readAsDataURL(emptyFile);
michael@0 177 expectedTestCount++;
michael@0 178
michael@0 179 // Test reusing a FileReader to read multiple times
michael@0 180 r = new FileReader();
michael@0 181 r.onload = getLoadHandler(testASCIIData,
michael@0 182 testASCIIData.length,
michael@0 183 "to-be-reused reading text")
michael@0 184 var makeAnotherReadListener = function(event) {
michael@0 185 r = event.target;
michael@0 186 r.removeEventListener("load", makeAnotherReadListener, false);
michael@0 187 r.onload = getLoadHandler(testASCIIData,
michael@0 188 testASCIIData.length,
michael@0 189 "reused reading text");
michael@0 190 r.readAsText(asciiFile);
michael@0 191 };
michael@0 192 r.addEventListener("load", makeAnotherReadListener, false);
michael@0 193 r.readAsText(asciiFile);
michael@0 194 expectedTestCount += 2;
michael@0 195
michael@0 196 r = new FileReader();
michael@0 197 r.onload = getLoadHandler(testBinaryData,
michael@0 198 testBinaryData.length,
michael@0 199 "to-be-reused reading binary")
michael@0 200 var makeAnotherReadListener2 = function(event) {
michael@0 201 r = event.target;
michael@0 202 r.removeEventListener("load", makeAnotherReadListener2, false);
michael@0 203 r.onload = getLoadHandler(testBinaryData,
michael@0 204 testBinaryData.length,
michael@0 205 "reused reading binary");
michael@0 206 r.readAsBinaryString(binaryFile);
michael@0 207 };
michael@0 208 r.addEventListener("load", makeAnotherReadListener2, false);
michael@0 209 r.readAsBinaryString(binaryFile);
michael@0 210 expectedTestCount += 2;
michael@0 211
michael@0 212 r = new FileReader();
michael@0 213 r.onload = getLoadHandler(convertToDataURL(testBinaryData),
michael@0 214 testBinaryData.length,
michael@0 215 "to-be-reused reading data url")
michael@0 216 var makeAnotherReadListener3 = function(event) {
michael@0 217 r = event.target;
michael@0 218 r.removeEventListener("load", makeAnotherReadListener3, false);
michael@0 219 r.onload = getLoadHandler(convertToDataURL(testBinaryData),
michael@0 220 testBinaryData.length,
michael@0 221 "reused reading data url");
michael@0 222 r.readAsDataURL(binaryFile);
michael@0 223 };
michael@0 224 r.addEventListener("load", makeAnotherReadListener3, false);
michael@0 225 r.readAsDataURL(binaryFile);
michael@0 226 expectedTestCount += 2;
michael@0 227
michael@0 228 r = new FileReader();
michael@0 229 r.onload = getLoadHandlerForArrayBuffer(testBinaryData,
michael@0 230 testBinaryData.length,
michael@0 231 "to-be-reused reading arrayBuffer")
michael@0 232 var makeAnotherReadListener4 = function(event) {
michael@0 233 r = event.target;
michael@0 234 r.removeEventListener("load", makeAnotherReadListener4, false);
michael@0 235 r.onload = getLoadHandlerForArrayBuffer(testBinaryData,
michael@0 236 testBinaryData.length,
michael@0 237 "reused reading arrayBuffer");
michael@0 238 r.readAsArrayBuffer(binaryFile);
michael@0 239 };
michael@0 240 r.addEventListener("load", makeAnotherReadListener4, false);
michael@0 241 r.readAsArrayBuffer(binaryFile);
michael@0 242 expectedTestCount += 2;
michael@0 243
michael@0 244 // Test first reading as ArrayBuffer then read as something else
michael@0 245 // (BinaryString) and doesn't crash
michael@0 246 r = new FileReader();
michael@0 247 r.onload = getLoadHandlerForArrayBuffer(testBinaryData,
michael@0 248 testBinaryData.length,
michael@0 249 "to-be-reused reading arrayBuffer")
michael@0 250 var makeAnotherReadListener5 = function(event) {
michael@0 251 r = event.target;
michael@0 252 r.removeEventListener("load", makeAnotherReadListener5, false);
michael@0 253 r.onload = getLoadHandler(testBinaryData,
michael@0 254 testBinaryData.length,
michael@0 255 "reused reading binary string");
michael@0 256 r.readAsBinaryString(binaryFile);
michael@0 257 };
michael@0 258 r.addEventListener("load", makeAnotherReadListener5, false);
michael@0 259 r.readAsArrayBuffer(binaryFile);
michael@0 260 expectedTestCount += 2;
michael@0 261
michael@0 262 //Test data-URI encoding on differing file sizes
michael@0 263 dataurldata = testBinaryData.substr(0, testBinaryData.length -
michael@0 264 testBinaryData.length % 3);
michael@0 265 is(dataurldata.length % 3, 0, "Want to test data with length % 3 == 0");
michael@0 266 r = new FileReader();
michael@0 267 r.onload = getLoadHandler(convertToDataURL(dataurldata),
michael@0 268 dataurldata.length,
michael@0 269 "dataurl reading, %3 = 0");
michael@0 270 r.readAsDataURL(createFileWithData(dataurldata));
michael@0 271 expectedTestCount++;
michael@0 272
michael@0 273 dataurldata = testBinaryData.substr(0, testBinaryData.length - 2 -
michael@0 274 testBinaryData.length % 3);
michael@0 275 is(dataurldata.length % 3, 1, "Want to test data with length % 3 == 1");
michael@0 276 r = new FileReader();
michael@0 277 r.onload = getLoadHandler(convertToDataURL(dataurldata),
michael@0 278 dataurldata.length,
michael@0 279 "dataurl reading, %3 = 1");
michael@0 280 r.readAsDataURL(createFileWithData(dataurldata));
michael@0 281 expectedTestCount++;
michael@0 282
michael@0 283 dataurldata = testBinaryData.substr(0, testBinaryData.length - 1 -
michael@0 284 testBinaryData.length % 3);
michael@0 285 is(dataurldata.length % 3, 2, "Want to test data with length % 3 == 2");
michael@0 286 r = new FileReader();
michael@0 287 r.onload = getLoadHandler(convertToDataURL(dataurldata),
michael@0 288 dataurldata.length,
michael@0 289 "dataurl reading, %3 = 2");
michael@0 290 r.readAsDataURL(createFileWithData(dataurldata));
michael@0 291 expectedTestCount++;
michael@0 292
michael@0 293
michael@0 294 // Test abort()
michael@0 295 var abortHasRun = false;
michael@0 296 var loadEndHasRun = false;
michael@0 297 r = new FileReader();
michael@0 298 r.onabort = function (event) {
michael@0 299 is(abortHasRun, false, "abort should only fire once");
michael@0 300 is(loadEndHasRun, false, "loadend shouldn't have fired yet");
michael@0 301 abortHasRun = true;
michael@0 302 is(event.target.readyState, FileReader.DONE, "should be DONE while firing onabort");
michael@0 303 is(event.target.error.name, "AbortError", "error set to AbortError for aborted reads");
michael@0 304 is(event.target.result, null, "file data should be null on aborted reads");
michael@0 305 }
michael@0 306 r.onloadend = function (event) {
michael@0 307 is(abortHasRun, true, "abort should fire before loadend");
michael@0 308 is(loadEndHasRun, false, "loadend should only fire once");
michael@0 309 loadEndHasRun = true;
michael@0 310 is(event.target.readyState, FileReader.DONE, "should be DONE while firing onabort");
michael@0 311 is(event.target.error.name, "AbortError", "error set to AbortError for aborted reads");
michael@0 312 is(event.target.result, null, "file data should be null on aborted reads");
michael@0 313 }
michael@0 314 r.onload = function() { ok(false, "load should not fire for aborted reads") };
michael@0 315 r.onerror = function() { ok(false, "error should not fire for aborted reads") };
michael@0 316 r.onprogress = function() { ok(false, "progress should not fire for aborted reads") };
michael@0 317 var abortThrew = false;
michael@0 318 try {
michael@0 319 r.abort();
michael@0 320 } catch(e) {
michael@0 321 abortThrew = true;
michael@0 322 }
michael@0 323 is(abortThrew, true, "abort() must throw if not loading");
michael@0 324 is(abortHasRun, false, "abort() is a no-op unless loading");
michael@0 325 r.readAsText(asciiFile);
michael@0 326 r.abort();
michael@0 327 is(abortHasRun, true, "abort should fire sync");
michael@0 328 is(loadEndHasRun, true, "loadend should fire sync");
michael@0 329
michael@0 330 // Test calling readAsX to cause abort()
michael@0 331 var reuseAbortHasRun = false;
michael@0 332 r = new FileReader();
michael@0 333 r.onabort = function (event) {
michael@0 334 is(reuseAbortHasRun, false, "abort should only fire once");
michael@0 335 reuseAbortHasRun = true;
michael@0 336 is(event.target.readyState, FileReader.DONE, "should be DONE while firing onabort");
michael@0 337 is(event.target.error.name, "AbortError", "error set to AbortError for aborted reads");
michael@0 338 is(event.target.result, null, "file data should be null on aborted reads");
michael@0 339 }
michael@0 340 r.onload = function() { ok(false, "load should not fire for aborted reads") };
michael@0 341 var abortThrew = false;
michael@0 342 try {
michael@0 343 r.abort();
michael@0 344 } catch(e) {
michael@0 345 abortThrew = true;
michael@0 346 }
michael@0 347 is(abortThrew, true, "abort() must throw if not loading");
michael@0 348 is(reuseAbortHasRun, false, "abort() is a no-op unless loading");
michael@0 349 r.readAsText(asciiFile);
michael@0 350 r.readAsText(asciiFile);
michael@0 351 is(reuseAbortHasRun, true, "abort should fire sync");
michael@0 352 r.onload = getLoadHandler(testASCIIData, testASCIIData.length, "reuse-as-abort reading");
michael@0 353 expectedTestCount++;
michael@0 354
michael@0 355
michael@0 356 // Test reading from nonexistent files
michael@0 357 r = new FileReader();
michael@0 358 var didThrow = false;
michael@0 359 try {
michael@0 360 r.readAsDataURL(nonExistingFile);
michael@0 361 } catch(ex) {
michael@0 362 didThrow = true;
michael@0 363 }
michael@0 364 // Once this test passes, we should test that onerror gets called and
michael@0 365 // that the FileReader object is in the right state during that call.
michael@0 366 todo(!didThrow, "shouldn't throw when opening nonexistent file, should fire error instead");
michael@0 367
michael@0 368
michael@0 369 function getLoadHandler(expectedResult, expectedLength, testName) {
michael@0 370 return function (event) {
michael@0 371 is(event.target.readyState, FileReader.DONE,
michael@0 372 "readyState in test " + testName);
michael@0 373 is(event.target.error, null,
michael@0 374 "no error in test " + testName);
michael@0 375 is(event.target.result, expectedResult,
michael@0 376 "result in test " + testName);
michael@0 377 is(event.lengthComputable, true,
michael@0 378 "lengthComputable in test " + testName);
michael@0 379 is(event.loaded, expectedLength,
michael@0 380 "loaded in test " + testName);
michael@0 381 is(event.total, expectedLength,
michael@0 382 "total in test " + testName);
michael@0 383 testHasRun();
michael@0 384 }
michael@0 385 }
michael@0 386
michael@0 387 function getLoadHandlerForArrayBuffer(expectedResult, expectedLength, testName) {
michael@0 388 return function (event) {
michael@0 389 is(event.target.readyState, FileReader.DONE,
michael@0 390 "readyState in test " + testName);
michael@0 391 is(event.target.error, null,
michael@0 392 "no error in test " + testName);
michael@0 393 is(event.lengthComputable, true,
michael@0 394 "lengthComputable in test " + testName);
michael@0 395 is(event.loaded, expectedLength,
michael@0 396 "loaded in test " + testName);
michael@0 397 is(event.total, expectedLength,
michael@0 398 "total in test " + testName);
michael@0 399 is(event.target.result.byteLength, expectedLength,
michael@0 400 "array buffer size in test " + testName);
michael@0 401 var u8v = new Uint8Array(event.target.result);
michael@0 402 is(String.fromCharCode.apply(String, u8v), expectedResult,
michael@0 403 "array buffer contents in test " + testName);
michael@0 404 u8v = null;
michael@0 405 SpecialPowers.gc();
michael@0 406 is(event.target.result.byteLength, expectedLength,
michael@0 407 "array buffer size after gc in test " + testName);
michael@0 408 u8v = new Uint8Array(event.target.result);
michael@0 409 is(String.fromCharCode.apply(String, u8v), expectedResult,
michael@0 410 "array buffer contents after gc in test " + testName);
michael@0 411 testHasRun();
michael@0 412 }
michael@0 413 }
michael@0 414
michael@0 415 function testHasRun() {
michael@0 416 //alert(testRanCounter);
michael@0 417 ++testRanCounter;
michael@0 418 if (testRanCounter == expectedTestCount) {
michael@0 419 is(onloadHasRunText, true, "onload text should have fired by now");
michael@0 420 is(onloadHasRunBinary, true, "onload binary should have fired by now");
michael@0 421 SimpleTest.finish();
michael@0 422 }
michael@0 423 }
michael@0 424
michael@0 425 function createFileWithData(fileData) {
michael@0 426 var dirSvc = SpecialPowers.Cc["@mozilla.org/file/directory_service;1"].getService(SpecialPowers.Ci.nsIProperties);
michael@0 427 var testFile = dirSvc.get("ProfD", SpecialPowers.Ci.nsIFile);
michael@0 428 testFile.append("fileAPItestfile" + fileNum);
michael@0 429 fileNum++;
michael@0 430 var outStream = SpecialPowers.Cc["@mozilla.org/network/file-output-stream;1"].createInstance(SpecialPowers.Ci.nsIFileOutputStream);
michael@0 431 outStream.init(testFile, 0x02 | 0x08 | 0x20, // write, create, truncate
michael@0 432 0666, 0);
michael@0 433 outStream.write(fileData, fileData.length);
michael@0 434 outStream.close();
michael@0 435
michael@0 436 var fileList = document.getElementById('fileList');
michael@0 437 SpecialPowers.wrap(fileList).value = testFile.path;
michael@0 438
michael@0 439 return fileList.files[0];
michael@0 440 }
michael@0 441
michael@0 442 function convertToUTF16(s) {
michael@0 443 res = "";
michael@0 444 for (var i = 0; i < s.length; ++i) {
michael@0 445 c = s.charCodeAt(i);
michael@0 446 res += String.fromCharCode(c & 255, c >>> 8);
michael@0 447 }
michael@0 448 return res;
michael@0 449 }
michael@0 450
michael@0 451 function convertToUTF8(s) {
michael@0 452 return unescape(encodeURIComponent(s));
michael@0 453 }
michael@0 454
michael@0 455 function convertToDataURL(s) {
michael@0 456 return "data:application/octet-stream;base64," + btoa(s);
michael@0 457 }
michael@0 458
michael@0 459 </script>
michael@0 460 </pre>
michael@0 461 </body> </html>

mercurial