1 /* 2 restinio 3 */ 4 5 /*! 6 Sendfile. 7 */ 8 9 #include <catch2/catch.hpp> 10 11 #include <restinio/all.hpp> 12 13 #include <restinio/utils/at_scope_exit.hpp> 14 15 #include <test/common/utest_logger.hpp> 16 #include <test/common/pub.hpp> 17 18 using logger_to_use_t = restinio::null_logger_t; 19 //using logger_to_use_t = utest_logger_t; 20 21 TEST_CASE( "sendfile offset/size representation" , "[sendfile]" ) 22 { 23 REQUIRE( 8 == sizeof( restinio::file_offset_t ) ); 24 REQUIRE( 8 == sizeof( restinio::file_size_t ) ); 25 } 26 27 TEST_CASE( "simple sendfile" , "[sendfile]" ) 28 { 29 using http_server_t = 30 restinio::http_server_t< 31 restinio::traits_t< 32 restinio::asio_timer_manager_t, 33 logger_to_use_t > >; 34 35 http_server_t http_server{ 36 restinio::own_io_context(), __anoned0e0aff0102( )37 []( auto & settings ){ 38 settings 39 .port( utest_default_port() ) 40 .address( "127.0.0.1" ) 41 .request_handler( 42 []( auto req ){ 43 if( restinio::http_method_get() == req->header().method() ) 44 { 45 req->create_response() 46 .append_header( "Server", "RESTinio utest server" ) 47 .append_header_date_field() 48 .append_header( "Content-Type", "text/plain; charset=utf-8" ) 49 .set_body( restinio::sendfile( "test/sendfile/f1.dat" ) ) 50 .done(); 51 return restinio::request_accepted(); 52 } 53 54 return restinio::request_rejected(); 55 } ); 56 } 57 }; 58 59 other_work_thread_for_server_t<http_server_t> other_thread{ http_server }; 60 other_thread.run(); 61 62 const std::string request{ 63 "GET / HTTP/1.0\r\n" 64 "From: unit-test\r\n" 65 "User-Agent: unit-test\r\n" 66 "Content-Type: application/x-www-form-urlencoded\r\n" 67 "Connection: close\r\n" 68 "\r\n" 69 }; 70 std::string response; 71 72 REQUIRE_NOTHROW( response = do_request( request ) ); 73 74 REQUIRE_THAT( 75 response, 76 Catch::Matchers::EndsWith( 77 "0123456789\n" 78 "FILE1\n" 79 "0123456789\n" ) ); 80 81 other_thread.stop_and_join(); 82 } 83 84 TEST_CASE( "sendfile the same file several times" , "[sendfile][same-several-times]" ) 85 { 86 using http_server_t = 87 restinio::http_server_t< 88 restinio::traits_t< 89 restinio::asio_timer_manager_t, 90 utest_logger_t > >; 91 92 http_server_t http_server{ 93 restinio::own_io_context(), __anoned0e0aff0302( )94 []( auto & settings ){ 95 settings 96 .port( utest_default_port() ) 97 .address( "127.0.0.1" ) 98 .request_handler( 99 []( auto req ){ 100 if( restinio::http_method_get() == req->header().method() ) 101 { 102 req->create_response() 103 .append_header( "Server", "RESTinio utest server" ) 104 .append_header_date_field() 105 .append_header( "Content-Type", "text/plain; charset=utf-8" ) 106 .set_body( restinio::sendfile( "test/sendfile/f1.dat" ) ) 107 .set_body( restinio::sendfile( "test/sendfile/f1.dat" ) ) 108 .set_body( restinio::sendfile( "test/sendfile/f1.dat" ) ) 109 .set_body( restinio::sendfile( "test/sendfile/f1.dat" ) ) 110 .done(); 111 return restinio::request_accepted(); 112 } 113 114 return restinio::request_rejected(); 115 } ); 116 } 117 }; 118 119 other_work_thread_for_server_t<http_server_t> other_thread{ http_server }; 120 other_thread.run(); 121 122 const std::string request{ 123 "GET / HTTP/1.0\r\n" 124 "From: unit-test\r\n" 125 "User-Agent: unit-test\r\n" 126 "Content-Type: application/x-www-form-urlencoded\r\n" 127 "Connection: close\r\n" 128 "\r\n" 129 }; 130 std::string response; 131 132 REQUIRE_NOTHROW( response = do_request( request ) ); 133 134 REQUIRE_THAT( 135 response, 136 Catch::Matchers::EndsWith( 137 "0123456789\n" 138 "FILE1\n" 139 "0123456789\n" ) ); 140 141 other_thread.stop_and_join(); 142 } 143 144 TEST_CASE( "sendfile 2 files" , "[sendfile][n-files]" ) 145 { 146 using router_t = restinio::router::express_router_t<>; 147 148 auto router = std::make_unique< router_t >(); 149 150 const std::string dir{ "test/sendfile/" }; 151 152 router->http_get( 153 "/:f1/:f2", __anoned0e0aff0502( auto req, auto params )154 [ & ]( auto req, auto params ){ 155 return 156 req->create_response() 157 .append_header( "Server", "RESTinio Benchmark" ) 158 .append_header_date_field() 159 .append_header( "Content-Type", "text/plain; charset=utf-8" ) 160 .append_body( 161 restinio::sendfile( 162 dir + restinio::cast_to< std::string >( params[ "f1" ] ) + ".dat" ) ) 163 .append_body( 164 restinio::sendfile( 165 dir + restinio::cast_to< std::string >( params[ "f2" ] ) + ".dat" ) ) 166 .done(); 167 } ); 168 router->http_get( 169 "/:f1/:regularBuffer/:f2", __anoned0e0aff0602( auto req, auto params )170 [ & ]( auto req, auto params ){ 171 return 172 req->create_response() 173 .append_header( "Server", "RESTinio Benchmark" ) 174 .append_header_date_field() 175 .append_header( "Content-Type", "text/plain; charset=utf-8" ) 176 .append_body( 177 restinio::sendfile( 178 dir + restinio::cast_to< std::string >( params[ "f1" ] ) + ".dat" ) ) 179 .append_body( 180 restinio::cast_to< std::string >( params[ "regularBuffer" ] ) ) 181 .append_body( 182 restinio::sendfile( 183 dir + restinio::cast_to< std::string >( params[ "f2" ] ) + ".dat" ) ) 184 .done(); 185 } ); 186 187 188 using http_server_t = 189 restinio::http_server_t< 190 restinio::traits_t< 191 restinio::asio_timer_manager_t, 192 logger_to_use_t, 193 router_t > >; 194 195 http_server_t http_server{ 196 restinio::own_io_context(), __anoned0e0aff0702( )197 [&]( auto & settings ){ 198 settings 199 .port( utest_default_port() ) 200 .address( "127.0.0.1" ) 201 .request_handler( std::move( router ) ); 202 } 203 }; 204 205 other_work_thread_for_server_t<http_server_t> other_thread{ http_server }; 206 other_thread.run(); 207 208 { 209 const std::string request{ 210 "GET /f1/f2 HTTP/1.0\r\n" 211 "From: unit-test\r\n" 212 "User-Agent: unit-test\r\n" 213 "Content-Type: application/x-www-form-urlencoded\r\n" 214 "Connection: close\r\n" 215 "\r\n" 216 }; 217 std::string response; 218 219 REQUIRE_NOTHROW( response = do_request( request ) ); 220 221 REQUIRE_THAT( 222 response, 223 Catch::Matchers::EndsWith( 224 "0123456789\n" 225 "FILE1\n" 226 "0123456789\n" 227 "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC\n" ) ); 228 } 229 230 { 231 const std::string request{ 232 "GET /f2/f1 HTTP/1.0\r\n" 233 "From: unit-test\r\n" 234 "User-Agent: unit-test\r\n" 235 "Content-Type: application/x-www-form-urlencoded\r\n" 236 "Connection: close\r\n" 237 "\r\n" 238 }; 239 std::string response; 240 241 REQUIRE_NOTHROW( response = do_request( request ) ); 242 243 REQUIRE_THAT( 244 response, 245 Catch::Matchers::EndsWith( 246 "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC\n" 247 "0123456789\n" 248 "FILE1\n" 249 "0123456789\n" ) ); 250 } 251 252 253 { 254 const std::string request{ 255 "GET /f1/REGULARBUFFER/f2 HTTP/1.0\r\n" 256 "From: unit-test\r\n" 257 "User-Agent: unit-test\r\n" 258 "Content-Type: application/x-www-form-urlencoded\r\n" 259 "Connection: close\r\n" 260 "\r\n" 261 }; 262 std::string response; 263 264 REQUIRE_NOTHROW( response = do_request( request ) ); 265 266 REQUIRE_THAT( 267 response, 268 Catch::Matchers::EndsWith( 269 "0123456789\n" 270 "FILE1\n" 271 "0123456789\n" 272 "REGULARBUFFER" 273 "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC\n" ) ); 274 } 275 276 other_thread.stop_and_join(); 277 } 278 279 TEST_CASE( "sendfile offsets_and_size" , "[sendfile][offset][size]" ) 280 { 281 using router_t = restinio::router::express_router_t<>; 282 283 auto router = std::make_unique< router_t >(); 284 285 const std::string dir{ "test/sendfile/" }; 286 287 router->http_get( 288 R"(/:fname/:offset(\d+))", __anoned0e0aff0802( auto req, auto params )289 [ & ]( auto req, auto params ){ 290 auto fname = dir + restinio::cast_to< std::string >( params[ "fname" ] ) + ".dat"; 291 return 292 req->create_response() 293 .append_header( "Server", "RESTinio Benchmark" ) 294 .append_header_date_field() 295 .append_header( "Content-Type", "text/plain; charset=utf-8" ) 296 .append_body( 297 restinio::sendfile( fname ) 298 .offset_and_size( restinio::cast_to< restinio::file_offset_t >( params[ "offset" ] ) ) ) 299 .done(); 300 } ); 301 302 router->http_get( 303 R"(/:fname/:offset(\d+)/:size(\d+))", __anoned0e0aff0902( auto req, auto params )304 [ & ]( auto req, auto params ){ 305 auto fname = dir + restinio::cast_to< std::string >( params[ "fname" ] ) + ".dat"; 306 const auto offset = restinio::cast_to< restinio::file_offset_t >( params[ "offset" ] ); 307 const auto size = restinio::cast_to< restinio::file_size_t >( params[ "size" ] ); 308 return 309 req->create_response() 310 .append_header( "Server", "RESTinio Benchmark" ) 311 .append_header_date_field() 312 .append_header( "Content-Type", "text/plain; charset=utf-8" ) 313 .append_body( 314 restinio::sendfile( fname ).offset_and_size( offset, size ) ) 315 .done(); 316 } ); 317 318 using http_server_t = 319 restinio::http_server_t< 320 restinio::traits_t< 321 restinio::asio_timer_manager_t, 322 logger_to_use_t, 323 router_t > >; 324 325 http_server_t http_server{ 326 restinio::own_io_context(), __anoned0e0aff0a02( )327 [&]( auto & settings ){ 328 settings 329 .port( utest_default_port() ) 330 .address( "127.0.0.1" ) 331 .request_handler( std::move( router ) ); 332 } 333 }; 334 335 other_work_thread_for_server_t<http_server_t> other_thread{ http_server }; 336 other_thread.run(); 337 338 { 339 const std::string all_file{ "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC\n" }; 340 341 for( std::size_t i = 0; i < all_file.size(); ++i ) 342 { 343 const std::string request{ 344 "GET /f2/" + std::to_string( i ) + " HTTP/1.0\r\n" 345 "From: unit-test\r\n" 346 "User-Agent: unit-test\r\n" 347 "Content-Type: application/x-www-form-urlencoded\r\n" 348 "Connection: close\r\n" 349 "\r\n" 350 }; 351 352 std::string response; 353 REQUIRE_NOTHROW( response = do_request( request ) ); 354 355 REQUIRE_THAT( 356 response, 357 Catch::Matchers::EndsWith( 358 all_file.substr( i ) ) ); 359 } 360 } 361 362 { 363 const std::string single_string{ "SENDILE 012345678901234567890123456789012345678901234567890123456789\n" }; 364 365 for( std::size_t n = 0; n < 3301; n+= 300 ) 366 for( std::size_t i = 0; i < single_string.size(); ++i ) 367 { 368 const std::string request{ 369 "GET /f3/" + 370 std::to_string( n * single_string.size() ) + "/" + 371 std::to_string( i ) + " HTTP/1.0\r\n" 372 "From: unit-test\r\n" 373 "User-Agent: unit-test\r\n" 374 "Content-Type: application/x-www-form-urlencoded\r\n" 375 "Connection: close\r\n" 376 "\r\n" 377 }; 378 379 std::string response; 380 REQUIRE_NOTHROW( response = do_request( request ) ); 381 382 REQUIRE_THAT( 383 response, 384 Catch::Matchers::EndsWith( 385 single_string.substr( 0, i ) ) ); 386 } 387 } 388 389 other_thread.stop_and_join(); 390 } 391 392 TEST_CASE( "sendfile chunks" , "[sendfile][chunk]" ) 393 { 394 using router_t = restinio::router::express_router_t<>; 395 396 auto router = std::make_unique< router_t >(); 397 398 const std::string dir{ "test/sendfile/" }; 399 400 router->http_get( 401 R"(/:fname/:chunk(\d+))", __anoned0e0aff0b02( auto req, auto params )402 [ & ]( auto req, auto params ){ 403 auto fname = dir + restinio::cast_to< std::string >( params[ "fname" ] ) + ".dat"; 404 const auto chunk_size = restinio::cast_to< restinio::file_size_t >( params[ "chunk" ] ); 405 return 406 req->create_response() 407 .append_header( "Server", "RESTinio Benchmark" ) 408 .append_header_date_field() 409 .append_header( "Content-Type", "text/plain; charset=utf-8" ) 410 .append_body( 411 restinio::sendfile( fname ).chunk_size( chunk_size ) ) 412 .done(); 413 } ); 414 415 using http_server_t = 416 restinio::http_server_t< 417 restinio::traits_t< 418 restinio::asio_timer_manager_t, 419 logger_to_use_t, 420 router_t > >; 421 422 http_server_t http_server{ 423 restinio::own_io_context(), __anoned0e0aff0c02( )424 [&]( auto & settings ){ 425 settings 426 .port( utest_default_port() ) 427 .address( "127.0.0.1" ) 428 .request_handler( std::move( router ) ); 429 } 430 }; 431 432 other_work_thread_for_server_t<http_server_t> other_thread{ http_server }; 433 other_thread.run(); 434 435 { 436 const std::string request{ 437 "GET /f1/4 HTTP/1.0\r\n" 438 "From: unit-test\r\n" 439 "User-Agent: unit-test\r\n" 440 "Content-Type: application/x-www-form-urlencoded\r\n" 441 "Connection: close\r\n" 442 "\r\n" 443 }; 444 445 std::string response; 446 REQUIRE_NOTHROW( response = do_request( request ) ); 447 448 REQUIRE_THAT( 449 response, 450 Catch::Matchers::EndsWith( 451 "0123456789\n" 452 "FILE1\n" 453 "0123456789\n" ) ); 454 } 455 456 { 457 const std::string request{ 458 "GET /f2/5 HTTP/1.0\r\n" 459 "From: unit-test\r\n" 460 "User-Agent: unit-test\r\n" 461 "Content-Type: application/x-www-form-urlencoded\r\n" 462 "Connection: close\r\n" 463 "\r\n" 464 }; 465 466 std::string response; 467 REQUIRE_NOTHROW( response = do_request( request ) ); 468 469 REQUIRE_THAT( 470 response, 471 Catch::Matchers::EndsWith( 472 "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC\n" ) ); 473 } 474 475 { 476 const std::string request{ 477 "GET /f1/1 HTTP/1.0\r\n" 478 "From: unit-test\r\n" 479 "User-Agent: unit-test\r\n" 480 "Content-Type: application/x-www-form-urlencoded\r\n" 481 "Connection: close\r\n" 482 "\r\n" 483 }; 484 485 std::string response; 486 REQUIRE_NOTHROW( response = do_request( request ) ); 487 488 REQUIRE_THAT( 489 response, 490 Catch::Matchers::EndsWith( 491 "0123456789\n" 492 "FILE1\n" 493 "0123456789\n" ) ); 494 } 495 496 { 497 const std::string request{ 498 "GET /f2/1 HTTP/1.0\r\n" 499 "From: unit-test\r\n" 500 "User-Agent: unit-test\r\n" 501 "Content-Type: application/x-www-form-urlencoded\r\n" 502 "Connection: close\r\n" 503 "\r\n" 504 }; 505 506 std::string response; 507 REQUIRE_NOTHROW( response = do_request( request ) ); 508 509 REQUIRE_THAT( 510 response, 511 Catch::Matchers::EndsWith( 512 "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC\n" ) ); 513 } 514 515 other_thread.stop_and_join(); 516 } 517 518 TEST_CASE( "sendfile errors" , "[sendfile][error]" ) 519 { 520 using router_t = restinio::router::express_router_t<>; 521 522 auto router = std::make_unique< router_t >(); 523 524 const std::string dir{ "test/sendfile/" }; 525 526 router->http_get( 527 R"(/:fname/:offset(\d+))", __anoned0e0aff0d02( auto req, auto params )528 [ & ]( auto req, auto params ){ 529 auto fname = dir + restinio::cast_to< std::string >( params[ "fname" ] ) + ".dat"; 530 return 531 req->create_response() 532 .append_header( "Server", "RESTinio Benchmark" ) 533 .append_header_date_field() 534 .append_header( "Content-Type", "text/plain; charset=utf-8" ) 535 .append_body( 536 restinio::sendfile( fname ) 537 .offset_and_size( restinio::cast_to< restinio::file_offset_t >( params[ "offset" ] ) ) ) 538 .done(); 539 } ); 540 541 router->http_get( 542 R"(/:fname/:offset(\d+)/:size(\d+))", __anoned0e0aff0e02( auto req, auto params )543 [ & ]( auto req, auto params ){ 544 auto fname = dir + restinio::cast_to< std::string >( params[ "fname" ] ) + ".dat"; 545 const auto offset = restinio::cast_to< restinio::file_offset_t >( params[ "offset" ] ); 546 const auto size = restinio::cast_to< restinio::file_size_t >( params[ "size" ] ); 547 return 548 req->create_response() 549 .append_header( "Server", "RESTinio Benchmark" ) 550 .append_header_date_field() 551 .append_header( "Content-Type", "text/plain; charset=utf-8" ) 552 .append_body( 553 restinio::sendfile( fname ).offset_and_size( offset, size ) ) 554 .done(); 555 } ); 556 557 using http_server_t = 558 restinio::http_server_t< 559 restinio::traits_t< 560 restinio::asio_timer_manager_t, 561 logger_to_use_t, 562 router_t > >; 563 564 http_server_t http_server{ 565 restinio::own_io_context(), __anoned0e0aff0f02( )566 [&]( auto & settings ){ 567 settings 568 .port( utest_default_port() ) 569 .address( "127.0.0.1" ) 570 .request_handler( std::move( router ) ); 571 } 572 }; 573 574 other_work_thread_for_server_t<http_server_t> other_thread{ http_server }; 575 other_thread.run(); 576 577 { 578 const std::string request{ 579 "GET /nosuchfile/0 HTTP/1.0\r\n" 580 "From: unit-test\r\n" 581 "User-Agent: unit-test\r\n" 582 "Content-Type: application/x-www-form-urlencoded\r\n" 583 "Connection: close\r\n" 584 "\r\n" 585 }; 586 587 std::string response; 588 REQUIRE_THROWS( response = do_request( request ) ); 589 } 590 591 { 592 const std::string request{ 593 "GET /f2/1024 HTTP/1.0\r\n" 594 "From: unit-test\r\n" 595 "User-Agent: unit-test\r\n" 596 "Content-Type: application/x-www-form-urlencoded\r\n" 597 "Connection: close\r\n" 598 "\r\n" 599 }; 600 601 std::string response; 602 REQUIRE_THROWS( response = do_request( request ) ); 603 } 604 605 other_thread.stop_and_join(); 606 } 607 608 TEST_CASE( "sendfile with invalid descriptor with " , "[sendfile][error][is_valid]" ) 609 { 610 { 611 const std::string fname = "must_not_exist_file_name"; 612 613 REQUIRE_THROWS( restinio::sendfile( fname ) ); 614 } 615 616 { 617 const std::string fname = "test/sendfile/f1.dat"; __anoned0e0aff1002( restinio::sendfile_t sf )618 auto accept_moved = []( restinio::sendfile_t sf ){ REQUIRE( sf.is_valid() ); }; 619 auto sf = restinio::sendfile( fname ); 620 621 accept_moved( std::move( sf ) ); 622 623 REQUIRE_FALSE( sf.is_valid() ); 624 } 625 } 626 627 TEST_CASE( "sendfile_chunk_size_guarded_value_t " , "[chunk_size_guarded_value]" ) 628 { 629 { 630 restinio::sendfile_chunk_size_guarded_value_t chunk( 42 ); 631 REQUIRE( chunk.value() == 42 ); 632 } 633 { 634 restinio::sendfile_chunk_size_guarded_value_t chunk( 0 ); 635 REQUIRE( chunk.value() == restinio::sendfile_default_chunk_size ); 636 } 637 { 638 restinio::sendfile_chunk_size_guarded_value_t chunk( restinio::sendfile_max_chunk_size + 10 ); 639 REQUIRE( chunk.value() == restinio::sendfile_max_chunk_size ); 640 } 641 } 642 643 TEST_CASE( "sendfile with partially-read response" , 644 "[sendfile][close-during-read]" ) 645 { 646 using http_server_t = 647 restinio::http_server_t< 648 restinio::traits_t< 649 restinio::asio_timer_manager_t, 650 utest_logger_t > >; 651 652 http_server_t http_server{ 653 restinio::own_io_context(), __anoned0e0aff1102( )654 []( auto & settings ){ 655 settings 656 .port( utest_default_port() ) 657 .address( "127.0.0.1" ) 658 .request_handler( 659 []( auto req ){ 660 if( restinio::http_method_get() == req->header().method() ) 661 { 662 req->create_response() 663 .append_header( "Server", "RESTinio utest server" ) 664 .append_header_date_field() 665 .append_header( "Content-Type", "text/plain; charset=utf-8" ) 666 .set_body( restinio::sendfile( "test/sendfile/f3.dat" ) ) 667 .done(); 668 return restinio::request_accepted(); 669 } 670 671 return restinio::request_rejected(); 672 } ); 673 } 674 }; 675 676 other_work_thread_for_server_t<http_server_t> other_thread{ http_server }; 677 other_thread.run(); 678 679 #if defined(SIGPIPE) 680 auto old_sig = signal(SIGPIPE, SIG_IGN); __anoned0e0aff1302null681 auto sig_restorer = restinio::utils::at_scope_exit( [&old_sig] { 682 signal(SIGPIPE, old_sig); 683 } ); 684 #endif 685 686 const std::string request{ 687 "GET / HTTP/1.0\r\n" 688 "From: unit-test\r\n" 689 "User-Agent: unit-test\r\n" 690 "Content-Type: application/x-www-form-urlencoded\r\n" 691 "Connection: close\r\n" 692 "\r\n" 693 }; 694 695 for(unsigned int i = 0; i != 20; ++i ) { 696 do_with_socket( __anoned0e0aff1402( auto & socket, auto & ) 697 [&request]( auto & socket, auto & /*io_context*/ ) { 698 restinio::asio_ns::streambuf b; 699 std::ostream req_stream(&b); 700 req_stream << request; 701 restinio::asio_ns::write( socket, b ); 702 703 restinio::asio_ns::streambuf response_stream; 704 restinio::asio_ns::read_until( socket, response_stream, "\r\n\r\n" ); 705 std::array<char, 10> body; 706 restinio::asio_ns::read( socket, restinio::asio_ns::buffer(body) ); 707 708 socket.close(); 709 } ); 710 } 711 712 other_thread.stop_and_join(); 713 } 714 715