1/* 2 * Copyright (c) 2014, 2016, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. 8 * 9 * This code is distributed in the hope that it will be useful, but WITHOUT 10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 11 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 12 * version 2 for more details (a copy is included in the LICENSE file that 13 * accompanied this code). 14 * 15 * You should have received a copy of the GNU General Public License version 16 * 2 along with this work; if not, write to the Free Software Foundation, 17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 18 * 19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 20 * or visit www.oracle.com if you need additional information or have any 21 * questions. 22 */ 23package jdk.incubator.http.internal.hpack; 24 25import org.testng.annotations.Test; 26 27import java.nio.Buffer; 28import java.nio.ByteBuffer; 29import java.util.ArrayList; 30import java.util.Collections; 31import java.util.Iterator; 32import java.util.LinkedList; 33import java.util.List; 34import java.util.function.Consumer; 35import java.util.function.Function; 36 37import static jdk.incubator.http.internal.hpack.BuffersTestingKit.concat; 38import static jdk.incubator.http.internal.hpack.BuffersTestingKit.forEachSplit; 39import static jdk.incubator.http.internal.hpack.SpecHelper.toHexdump; 40import static jdk.incubator.http.internal.hpack.TestHelper.assertVoidThrows; 41import static java.util.Arrays.asList; 42import static org.testng.Assert.assertEquals; 43import static org.testng.Assert.assertTrue; 44 45// TODO: map textual representation of commands from the spec to actual 46// calls to encoder (actually, this is a good idea for decoder as well) 47public final class EncoderTest { 48 49 // 50 // http://tools.ietf.org/html/rfc7541#appendix-C.2.1 51 // 52 @Test 53 public void example1() { 54 55 Encoder e = newCustomEncoder(256); 56 drainInitialUpdate(e); 57 58 e.literalWithIndexing("custom-key", false, "custom-header", false); 59 // @formatter:off 60 test(e, 61 62 "400a 6375 7374 6f6d 2d6b 6579 0d63 7573\n" + 63 "746f 6d2d 6865 6164 6572", 64 65 "[ 1] (s = 55) custom-key: custom-header\n" + 66 " Table size: 55"); 67 // @formatter:on 68 } 69 70 // 71 // http://tools.ietf.org/html/rfc7541#appendix-C.2.2 72 // 73 @Test 74 public void example2() { 75 76 Encoder e = newCustomEncoder(256); 77 drainInitialUpdate(e); 78 79 e.literal(4, "/sample/path", false); 80 // @formatter:off 81 test(e, 82 83 "040c 2f73 616d 706c 652f 7061 7468", 84 85 "empty."); 86 // @formatter:on 87 } 88 89 // 90 // http://tools.ietf.org/html/rfc7541#appendix-C.2.3 91 // 92 @Test 93 public void example3() { 94 95 Encoder e = newCustomEncoder(256); 96 drainInitialUpdate(e); 97 98 e.literalNeverIndexed("password", false, "secret", false); 99 // @formatter:off 100 test(e, 101 102 "1008 7061 7373 776f 7264 0673 6563 7265\n" + 103 "74", 104 105 "empty."); 106 // @formatter:on 107 } 108 109 // 110 // http://tools.ietf.org/html/rfc7541#appendix-C.2.4 111 // 112 @Test 113 public void example4() { 114 115 Encoder e = newCustomEncoder(256); 116 drainInitialUpdate(e); 117 118 e.indexed(2); 119 // @formatter:off 120 test(e, 121 122 "82", 123 124 "empty."); 125 // @formatter:on 126 } 127 128 // 129 // http://tools.ietf.org/html/rfc7541#appendix-C.3 130 // 131 @Test 132 public void example5() { 133 Encoder e = newCustomEncoder(256); 134 drainInitialUpdate(e); 135 136 ByteBuffer output = ByteBuffer.allocate(64); 137 e.indexed(2); 138 e.encode(output); 139 e.indexed(6); 140 e.encode(output); 141 e.indexed(4); 142 e.encode(output); 143 e.literalWithIndexing(1, "www.example.com", false); 144 e.encode(output); 145 146 output.flip(); 147 148 // @formatter:off 149 test(e, output, 150 151 "8286 8441 0f77 7777 2e65 7861 6d70 6c65\n" + 152 "2e63 6f6d", 153 154 "[ 1] (s = 57) :authority: www.example.com\n" + 155 " Table size: 57"); 156 157 output.clear(); 158 159 e.indexed( 2); 160 e.encode(output); 161 e.indexed( 6); 162 e.encode(output); 163 e.indexed( 4); 164 e.encode(output); 165 e.indexed(62); 166 e.encode(output); 167 e.literalWithIndexing(24, "no-cache", false); 168 e.encode(output); 169 170 output.flip(); 171 172 test(e, output, 173 174 "8286 84be 5808 6e6f 2d63 6163 6865", 175 176 "[ 1] (s = 53) cache-control: no-cache\n" + 177 "[ 2] (s = 57) :authority: www.example.com\n" + 178 " Table size: 110"); 179 180 output.clear(); 181 182 e.indexed( 2); 183 e.encode(output); 184 e.indexed( 7); 185 e.encode(output); 186 e.indexed( 5); 187 e.encode(output); 188 e.indexed(63); 189 e.encode(output); 190 e.literalWithIndexing("custom-key", false, "custom-value", false); 191 e.encode(output); 192 193 output.flip(); 194 195 test(e, output, 196 197 "8287 85bf 400a 6375 7374 6f6d 2d6b 6579\n" + 198 "0c63 7573 746f 6d2d 7661 6c75 65", 199 200 "[ 1] (s = 54) custom-key: custom-value\n" + 201 "[ 2] (s = 53) cache-control: no-cache\n" + 202 "[ 3] (s = 57) :authority: www.example.com\n" + 203 " Table size: 164"); 204 // @formatter:on 205 } 206 207 @Test 208 public void example5AllSplits() { 209 210 List<Consumer<Encoder>> actions = new LinkedList<>(); 211 actions.add(e -> e.indexed(2)); 212 actions.add(e -> e.indexed(6)); 213 actions.add(e -> e.indexed(4)); 214 actions.add(e -> e.literalWithIndexing(1, "www.example.com", false)); 215 216 encodeAllSplits( 217 actions, 218 219 "8286 8441 0f77 7777 2e65 7861 6d70 6c65\n" + 220 "2e63 6f6d", 221 222 "[ 1] (s = 57) :authority: www.example.com\n" + 223 " Table size: 57"); 224 } 225 226 private static void encodeAllSplits(Iterable<Consumer<Encoder>> consumers, 227 String expectedHexdump, 228 String expectedTableState) { 229 ByteBuffer buffer = SpecHelper.toBytes(expectedHexdump); 230 erase(buffer); // Zeroed buffer of size needed to hold the encoding 231 forEachSplit(buffer, iterable -> { 232 List<ByteBuffer> copy = new LinkedList<>(); 233 iterable.forEach(b -> copy.add(ByteBuffer.allocate(b.remaining()))); 234 Iterator<ByteBuffer> output = copy.iterator(); 235 if (!output.hasNext()) { 236 throw new IllegalStateException("No buffers to encode to"); 237 } 238 Encoder e = newCustomEncoder(256); // FIXME: pull up (as a parameter) 239 drainInitialUpdate(e); 240 boolean encoded; 241 ByteBuffer b = output.next(); 242 for (Consumer<Encoder> c : consumers) { 243 c.accept(e); 244 do { 245 encoded = e.encode(b); 246 if (!encoded) { 247 if (output.hasNext()) { 248 b = output.next(); 249 } else { 250 throw new IllegalStateException("No room for encoding"); 251 } 252 } 253 } 254 while (!encoded); 255 } 256 copy.forEach(Buffer::flip); 257 ByteBuffer data = concat(copy); 258 test(e, data, expectedHexdump, expectedTableState); 259 }); 260 } 261 262 // 263 // http://tools.ietf.org/html/rfc7541#appendix-C.4 264 // 265 @Test 266 public void example6() { 267 Encoder e = newCustomEncoder(256); 268 drainInitialUpdate(e); 269 270 ByteBuffer output = ByteBuffer.allocate(64); 271 e.indexed(2); 272 e.encode(output); 273 e.indexed(6); 274 e.encode(output); 275 e.indexed(4); 276 e.encode(output); 277 e.literalWithIndexing(1, "www.example.com", true); 278 e.encode(output); 279 280 output.flip(); 281 282 // @formatter:off 283 test(e, output, 284 285 "8286 8441 8cf1 e3c2 e5f2 3a6b a0ab 90f4\n" + 286 "ff", 287 288 "[ 1] (s = 57) :authority: www.example.com\n" + 289 " Table size: 57"); 290 291 output.clear(); 292 293 e.indexed( 2); 294 e.encode(output); 295 e.indexed( 6); 296 e.encode(output); 297 e.indexed( 4); 298 e.encode(output); 299 e.indexed(62); 300 e.encode(output); 301 e.literalWithIndexing(24, "no-cache", true); 302 e.encode(output); 303 304 output.flip(); 305 306 test(e, output, 307 308 "8286 84be 5886 a8eb 1064 9cbf", 309 310 "[ 1] (s = 53) cache-control: no-cache\n" + 311 "[ 2] (s = 57) :authority: www.example.com\n" + 312 " Table size: 110"); 313 314 output.clear(); 315 316 e.indexed( 2); 317 e.encode(output); 318 e.indexed( 7); 319 e.encode(output); 320 e.indexed( 5); 321 e.encode(output); 322 e.indexed(63); 323 e.encode(output); 324 e.literalWithIndexing("custom-key", true, "custom-value", true); 325 e.encode(output); 326 327 output.flip(); 328 329 test(e, output, 330 331 "8287 85bf 4088 25a8 49e9 5ba9 7d7f 8925\n" + 332 "a849 e95b b8e8 b4bf", 333 334 "[ 1] (s = 54) custom-key: custom-value\n" + 335 "[ 2] (s = 53) cache-control: no-cache\n" + 336 "[ 3] (s = 57) :authority: www.example.com\n" + 337 " Table size: 164"); 338 // @formatter:on 339 } 340 341 // 342 // http://tools.ietf.org/html/rfc7541#appendix-C.5 343 // 344 @Test 345 public void example7() { 346 Encoder e = newCustomEncoder(256); 347 drainInitialUpdate(e); 348 349 ByteBuffer output = ByteBuffer.allocate(128); 350 // @formatter:off 351 e.literalWithIndexing( 8, "302", false); 352 e.encode(output); 353 e.literalWithIndexing(24, "private", false); 354 e.encode(output); 355 e.literalWithIndexing(33, "Mon, 21 Oct 2013 20:13:21 GMT", false); 356 e.encode(output); 357 e.literalWithIndexing(46, "https://www.example.com", false); 358 e.encode(output); 359 360 output.flip(); 361 362 test(e, output, 363 364 "4803 3330 3258 0770 7269 7661 7465 611d\n" + 365 "4d6f 6e2c 2032 3120 4f63 7420 3230 3133\n" + 366 "2032 303a 3133 3a32 3120 474d 546e 1768\n" + 367 "7474 7073 3a2f 2f77 7777 2e65 7861 6d70\n" + 368 "6c65 2e63 6f6d", 369 370 "[ 1] (s = 63) location: https://www.example.com\n" + 371 "[ 2] (s = 65) date: Mon, 21 Oct 2013 20:13:21 GMT\n" + 372 "[ 3] (s = 52) cache-control: private\n" + 373 "[ 4] (s = 42) :status: 302\n" + 374 " Table size: 222"); 375 376 output.clear(); 377 378 e.literalWithIndexing( 8, "307", false); 379 e.encode(output); 380 e.indexed(65); 381 e.encode(output); 382 e.indexed(64); 383 e.encode(output); 384 e.indexed(63); 385 e.encode(output); 386 387 output.flip(); 388 389 test(e, output, 390 391 "4803 3330 37c1 c0bf", 392 393 "[ 1] (s = 42) :status: 307\n" + 394 "[ 2] (s = 63) location: https://www.example.com\n" + 395 "[ 3] (s = 65) date: Mon, 21 Oct 2013 20:13:21 GMT\n" + 396 "[ 4] (s = 52) cache-control: private\n" + 397 " Table size: 222"); 398 399 output.clear(); 400 401 e.indexed( 8); 402 e.encode(output); 403 e.indexed(65); 404 e.encode(output); 405 e.literalWithIndexing(33, "Mon, 21 Oct 2013 20:13:22 GMT", false); 406 e.encode(output); 407 e.indexed(64); 408 e.encode(output); 409 e.literalWithIndexing(26, "gzip", false); 410 e.encode(output); 411 e.literalWithIndexing(55, "foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1", false); 412 e.encode(output); 413 414 output.flip(); 415 416 test(e, output, 417 418 "88c1 611d 4d6f 6e2c 2032 3120 4f63 7420\n" + 419 "3230 3133 2032 303a 3133 3a32 3220 474d\n" + 420 "54c0 5a04 677a 6970 7738 666f 6f3d 4153\n" + 421 "444a 4b48 514b 425a 584f 5157 454f 5049\n" + 422 "5541 5851 5745 4f49 553b 206d 6178 2d61\n" + 423 "6765 3d33 3630 303b 2076 6572 7369 6f6e\n" + 424 "3d31", 425 426 "[ 1] (s = 98) set-cookie: foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1\n" + 427 "[ 2] (s = 52) content-encoding: gzip\n" + 428 "[ 3] (s = 65) date: Mon, 21 Oct 2013 20:13:22 GMT\n" + 429 " Table size: 215"); 430 // @formatter:on 431 } 432 433 // 434 // http://tools.ietf.org/html/rfc7541#appendix-C.6 435 // 436 @Test 437 public void example8() { 438 Encoder e = newCustomEncoder(256); 439 drainInitialUpdate(e); 440 441 ByteBuffer output = ByteBuffer.allocate(128); 442 // @formatter:off 443 e.literalWithIndexing( 8, "302", true); 444 e.encode(output); 445 e.literalWithIndexing(24, "private", true); 446 e.encode(output); 447 e.literalWithIndexing(33, "Mon, 21 Oct 2013 20:13:21 GMT", true); 448 e.encode(output); 449 e.literalWithIndexing(46, "https://www.example.com", true); 450 e.encode(output); 451 452 output.flip(); 453 454 test(e, output, 455 456 "4882 6402 5885 aec3 771a 4b61 96d0 7abe\n" + 457 "9410 54d4 44a8 2005 9504 0b81 66e0 82a6\n" + 458 "2d1b ff6e 919d 29ad 1718 63c7 8f0b 97c8\n" + 459 "e9ae 82ae 43d3", 460 461 "[ 1] (s = 63) location: https://www.example.com\n" + 462 "[ 2] (s = 65) date: Mon, 21 Oct 2013 20:13:21 GMT\n" + 463 "[ 3] (s = 52) cache-control: private\n" + 464 "[ 4] (s = 42) :status: 302\n" + 465 " Table size: 222"); 466 467 output.clear(); 468 469 e.literalWithIndexing( 8, "307", true); 470 e.encode(output); 471 e.indexed(65); 472 e.encode(output); 473 e.indexed(64); 474 e.encode(output); 475 e.indexed(63); 476 e.encode(output); 477 478 output.flip(); 479 480 test(e, output, 481 482 "4883 640e ffc1 c0bf", 483 484 "[ 1] (s = 42) :status: 307\n" + 485 "[ 2] (s = 63) location: https://www.example.com\n" + 486 "[ 3] (s = 65) date: Mon, 21 Oct 2013 20:13:21 GMT\n" + 487 "[ 4] (s = 52) cache-control: private\n" + 488 " Table size: 222"); 489 490 output.clear(); 491 492 e.indexed( 8); 493 e.encode(output); 494 e.indexed(65); 495 e.encode(output); 496 e.literalWithIndexing(33, "Mon, 21 Oct 2013 20:13:22 GMT", true); 497 e.encode(output); 498 e.indexed(64); 499 e.encode(output); 500 e.literalWithIndexing(26, "gzip", true); 501 e.encode(output); 502 e.literalWithIndexing(55, "foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1", true); 503 e.encode(output); 504 505 output.flip(); 506 507 test(e, output, 508 509 "88c1 6196 d07a be94 1054 d444 a820 0595\n" + 510 "040b 8166 e084 a62d 1bff c05a 839b d9ab\n" + 511 "77ad 94e7 821d d7f2 e6c7 b335 dfdf cd5b\n" + 512 "3960 d5af 2708 7f36 72c1 ab27 0fb5 291f\n" + 513 "9587 3160 65c0 03ed 4ee5 b106 3d50 07", 514 515 "[ 1] (s = 98) set-cookie: foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1\n" + 516 "[ 2] (s = 52) content-encoding: gzip\n" + 517 "[ 3] (s = 65) date: Mon, 21 Oct 2013 20:13:22 GMT\n" + 518 " Table size: 215"); 519 // @formatter:on 520 } 521 522 @Test 523 public void initialSizeUpdateDefaultEncoder() { 524 Function<Integer, Encoder> e = Encoder::new; 525 testSizeUpdate(e, 1024, asList(), asList(0)); 526 testSizeUpdate(e, 1024, asList(1024), asList(0)); 527 testSizeUpdate(e, 1024, asList(1024, 1024), asList(0)); 528 testSizeUpdate(e, 1024, asList(1024, 512), asList(0)); 529 testSizeUpdate(e, 1024, asList(512, 1024), asList(0)); 530 testSizeUpdate(e, 1024, asList(512, 2048), asList(0)); 531 } 532 533 @Test 534 public void initialSizeUpdateCustomEncoder() { 535 Function<Integer, Encoder> e = EncoderTest::newCustomEncoder; 536 testSizeUpdate(e, 1024, asList(), asList(1024)); 537 testSizeUpdate(e, 1024, asList(1024), asList(1024)); 538 testSizeUpdate(e, 1024, asList(1024, 1024), asList(1024)); 539 testSizeUpdate(e, 1024, asList(1024, 512), asList(512)); 540 testSizeUpdate(e, 1024, asList(512, 1024), asList(1024)); 541 testSizeUpdate(e, 1024, asList(512, 2048), asList(2048)); 542 } 543 544 @Test 545 public void seriesOfSizeUpdatesDefaultEncoder() { 546 Function<Integer, Encoder> e = c -> { 547 Encoder encoder = new Encoder(c); 548 drainInitialUpdate(encoder); 549 return encoder; 550 }; 551 testSizeUpdate(e, 0, asList(0), asList()); 552 testSizeUpdate(e, 1024, asList(1024), asList()); 553 testSizeUpdate(e, 1024, asList(2048), asList()); 554 testSizeUpdate(e, 1024, asList(512), asList()); 555 testSizeUpdate(e, 1024, asList(1024, 1024), asList()); 556 testSizeUpdate(e, 1024, asList(1024, 2048), asList()); 557 testSizeUpdate(e, 1024, asList(2048, 1024), asList()); 558 testSizeUpdate(e, 1024, asList(1024, 512), asList()); 559 testSizeUpdate(e, 1024, asList(512, 1024), asList()); 560 } 561 562 // 563 // https://tools.ietf.org/html/rfc7541#section-4.2 564 // 565 @Test 566 public void seriesOfSizeUpdatesCustomEncoder() { 567 Function<Integer, Encoder> e = c -> { 568 Encoder encoder = newCustomEncoder(c); 569 drainInitialUpdate(encoder); 570 return encoder; 571 }; 572 testSizeUpdate(e, 0, asList(0), asList()); 573 testSizeUpdate(e, 1024, asList(1024), asList()); 574 testSizeUpdate(e, 1024, asList(2048), asList(2048)); 575 testSizeUpdate(e, 1024, asList(512), asList(512)); 576 testSizeUpdate(e, 1024, asList(1024, 1024), asList()); 577 testSizeUpdate(e, 1024, asList(1024, 2048), asList(2048)); 578 testSizeUpdate(e, 1024, asList(2048, 1024), asList()); 579 testSizeUpdate(e, 1024, asList(1024, 512), asList(512)); 580 testSizeUpdate(e, 1024, asList(512, 1024), asList(512, 1024)); 581 } 582 583 @Test 584 public void callSequenceViolations() { 585 { // Hasn't set up a header 586 Encoder e = new Encoder(0); 587 assertVoidThrows(IllegalStateException.class, () -> e.encode(ByteBuffer.allocate(16))); 588 } 589 { // Can't set up header while there's an unfinished encoding 590 Encoder e = new Encoder(0); 591 e.indexed(32); 592 assertVoidThrows(IllegalStateException.class, () -> e.indexed(32)); 593 } 594 { // Can't setMaxCapacity while there's an unfinished encoding 595 Encoder e = new Encoder(0); 596 e.indexed(32); 597 assertVoidThrows(IllegalStateException.class, () -> e.setMaxCapacity(512)); 598 } 599 { // Hasn't set up a header 600 Encoder e = new Encoder(0); 601 e.setMaxCapacity(256); 602 assertVoidThrows(IllegalStateException.class, () -> e.encode(ByteBuffer.allocate(16))); 603 } 604 { // Hasn't set up a header after the previous encoding 605 Encoder e = new Encoder(0); 606 e.indexed(0); 607 boolean encoded = e.encode(ByteBuffer.allocate(16)); 608 assertTrue(encoded); // assumption 609 assertVoidThrows(IllegalStateException.class, () -> e.encode(ByteBuffer.allocate(16))); 610 } 611 } 612 613 private static void test(Encoder encoder, 614 String expectedTableState, 615 String expectedHexdump) { 616 617 ByteBuffer b = ByteBuffer.allocate(128); 618 encoder.encode(b); 619 b.flip(); 620 test(encoder, b, expectedTableState, expectedHexdump); 621 } 622 623 private static void test(Encoder encoder, 624 ByteBuffer output, 625 String expectedHexdump, 626 String expectedTableState) { 627 628 String actualTableState = encoder.getHeaderTable().getStateString(); 629 assertEquals(actualTableState, expectedTableState); 630 631 String actualHexdump = toHexdump(output); 632 assertEquals(actualHexdump, expectedHexdump.replaceAll("\\n", " ")); 633 } 634 635 // initial size - the size encoder is constructed with 636 // updates - a sequence of values for consecutive calls to encoder.setMaxCapacity 637 // expected - a sequence of values expected to be decoded by a decoder 638 private void testSizeUpdate(Function<Integer, Encoder> encoder, 639 int initialSize, 640 List<Integer> updates, 641 List<Integer> expected) { 642 Encoder e = encoder.apply(initialSize); 643 updates.forEach(e::setMaxCapacity); 644 ByteBuffer b = ByteBuffer.allocate(64); 645 e.header("a", "b"); 646 e.encode(b); 647 b.flip(); 648 Decoder d = new Decoder(updates.isEmpty() ? initialSize : Collections.max(updates)); 649 List<Integer> actual = new ArrayList<>(); 650 d.decode(b, true, new DecodingCallback() { 651 @Override 652 public void onDecoded(CharSequence name, CharSequence value) { } 653 654 @Override 655 public void onSizeUpdate(int capacity) { 656 actual.add(capacity); 657 } 658 }); 659 assertEquals(actual, expected); 660 } 661 662 // 663 // Default encoder does not need any table, therefore a subclass that 664 // behaves differently is needed 665 // 666 private static Encoder newCustomEncoder(int maxCapacity) { 667 return new Encoder(maxCapacity) { 668 @Override 669 protected int calculateCapacity(int maxCapacity) { 670 return maxCapacity; 671 } 672 }; 673 } 674 675 private static void drainInitialUpdate(Encoder e) { 676 ByteBuffer b = ByteBuffer.allocate(4); 677 e.header("a", "b"); 678 boolean done; 679 do { 680 done = e.encode(b); 681 b.flip(); 682 } while (!done); 683 } 684 685 private static void erase(ByteBuffer buffer) { 686 buffer.clear(); 687 while (buffer.hasRemaining()) { 688 buffer.put((byte) 0); 689 } 690 buffer.clear(); 691 } 692} 693